@@ -107,13 +107,12 @@ class PenaltyManager:
107
107
108
108
Parameters
109
109
----------
110
+ initial_penalties
111
+ Initial penalty values for units of load (idx 0), duration (1), and
112
+ distance (2) violations. These values are clipped to the range
113
+ ``[MIN_PENALTY, MAX_PENALTY]``.
110
114
params
111
115
PenaltyManager parameters. If not provided, a default will be used.
112
- initial_penalties
113
- Initial penalty values for unit load (idx 0), duration (1), and
114
- distance (2) violations. Defaults to ``(20, 6, 6)`` for backwards
115
- compatibility. These values are clipped to the range ``[MIN_PENALTY,
116
- MAX_PENALTY]``.
117
116
"""
118
117
119
118
MIN_PENALTY : float = 0.1
@@ -122,22 +121,31 @@ class PenaltyManager:
122
121
123
122
def __init__ (
124
123
self ,
124
+ initial_penalties : tuple [list [float ], float , float ],
125
125
params : PenaltyParams = PenaltyParams (),
126
- initial_penalties : tuple [float , float , float ] = (20 , 6 , 6 ),
127
126
):
128
127
self ._params = params
129
128
self ._penalties = np .clip (
130
- initial_penalties ,
129
+ initial_penalties [ 0 ] + list ( initial_penalties [ 1 :]) ,
131
130
self .MIN_PENALTY ,
132
131
self .MAX_PENALTY ,
133
132
)
134
133
134
+ # Tracks recent feasibilities for each penalty dimension.
135
135
self ._feas_lists : list [list [bool ]] = [
136
- [], # tracks recent load feasibility
137
- [], # track recent time feasibility
138
- [], # track recent distance feasibility
136
+ [] for _ in range (len (self ._penalties ))
139
137
]
140
138
139
+ def penalties (self ) -> tuple [list [float ], float , float ]:
140
+ """
141
+ Returns the current penalty values.
142
+ """
143
+ return (
144
+ self ._penalties [:- 2 ].tolist (), # loads
145
+ self ._penalties [- 2 ], # duration
146
+ self ._penalties [- 1 ], # distance
147
+ )
148
+
141
149
@classmethod
142
150
def init_from (
143
151
cls ,
@@ -170,18 +178,18 @@ def init_from(
170
178
avg_distance = np .minimum .reduce (distances ).mean ()
171
179
avg_duration = np .minimum .reduce (durations ).mean ()
172
180
173
- avg_load = 0
181
+ avg_load = np . zeros (( data . num_load_dimensions ,))
174
182
if data .num_clients != 0 and data .num_load_dimensions != 0 :
175
183
pickups = np .array ([c .pickup for c in data .clients ()])
176
184
deliveries = np .array ([c .delivery for c in data .clients ()])
177
- avg_load = np .maximum (pickups , deliveries ).mean ()
185
+ avg_load = np .maximum (pickups , deliveries ).mean (axis = 0 )
178
186
179
187
# Initial penalty parameters are meant to weigh an average increase
180
188
# in the relevant value by the same amount as the average edge cost.
181
- init_load = avg_cost / max (avg_load , 1 )
189
+ init_load = avg_cost / np . maximum (avg_load , 1 )
182
190
init_tw = avg_cost / max (avg_duration , 1 )
183
191
init_dist = avg_cost / max (avg_distance , 1 )
184
- return cls (params , (init_load , init_tw , init_dist ))
192
+ return cls ((init_load . tolist () , init_tw , init_dist ), params )
185
193
186
194
def _compute (self , penalty : float , feas_percentage : float ) -> float :
187
195
# Computes and returns the new penalty value, given the current value
@@ -196,9 +204,7 @@ def _compute(self, penalty: float, feas_percentage: float) -> float:
196
204
else :
197
205
new_penalty = self ._params .penalty_decrease * penalty
198
206
199
- clipped = np .clip (new_penalty , self .MIN_PENALTY , self .MAX_PENALTY )
200
-
201
- if clipped == self .MAX_PENALTY :
207
+ if new_penalty >= self .MAX_PENALTY :
202
208
msg = """
203
209
A penalty parameter has reached its maximum value. This means PyVRP
204
210
struggles to find a feasible solution for the instance that's being
@@ -208,7 +214,7 @@ def _compute(self, penalty: float, feas_percentage: float) -> float:
208
214
"""
209
215
warn (msg , PenaltyBoundWarning )
210
216
211
- return clipped
217
+ return np . clip ( new_penalty , self . MIN_PENALTY , self . MAX_PENALTY )
212
218
213
219
def _register (self , feas_list : list [bool ], penalty : float , is_feas : bool ):
214
220
feas_list .append (is_feas )
@@ -224,13 +230,13 @@ def register(self, sol: Solution):
224
230
"""
225
231
Registers the feasibility dimensions of the given solution.
226
232
"""
227
- args = [
228
- not sol .has_excess_load () ,
233
+ is_feasible = [
234
+ * [ excess == 0 for excess in sol .excess_load ()] ,
229
235
not sol .has_time_warp (),
230
236
not sol .has_excess_distance (),
231
237
]
232
238
233
- for idx , is_feas in enumerate (args ):
239
+ for idx , is_feas in enumerate (is_feasible ):
234
240
feas_list = self ._feas_lists [idx ]
235
241
penalty = self ._penalties [idx ]
236
242
self ._penalties [idx ] = self ._register (feas_list , penalty , is_feas )
@@ -239,10 +245,12 @@ def cost_evaluator(self) -> CostEvaluator:
239
245
"""
240
246
Get a cost evaluator using the current penalty values.
241
247
"""
242
- return CostEvaluator (* self ._penalties )
248
+ * loads , tw , dist = self ._penalties
249
+ return CostEvaluator (loads , tw , dist )
243
250
244
251
def booster_cost_evaluator (self ) -> CostEvaluator :
245
252
"""
246
253
Get a cost evaluator using the boosted current penalty values.
247
254
"""
248
- return CostEvaluator (* (self ._penalties * self ._params .repair_booster ))
255
+ * loads , tw , dist = self ._penalties * self ._params .repair_booster
256
+ return CostEvaluator (loads , tw , dist )
0 commit comments