1717from routetools .land import Land
1818from routetools .vectorfield import vectorfield_fourvortices
1919from routetools .weather import weather_penalty as _weather_penalty
20+ from routetools .weather import weather_penalty_smooth as _weather_penalty_smooth
2021
2122
2223@jit # type: ignore[misc]
@@ -288,7 +289,11 @@ def _cma_evolution_strategy(
288289 ]
289290 | None = None ,
290291 penalty : float = 1e10 ,
292+ land_distance_weight : float = 0.0 ,
293+ land_distance_epsilon : float = 1.0 ,
291294 weather_penalty_weight : float = 0.0 ,
295+ weather_penalty_type : str = "hard" ,
296+ weather_penalty_sharpness : float = 5.0 ,
292297 tws_limit : float = 20.0 ,
293298 hs_limit : float = 7.0 ,
294299 travel_stw : float | None = None ,
@@ -310,21 +315,25 @@ def _cma_evolution_strategy(
310315 cost_fn : Callable [[jnp .ndarray ], jnp .ndarray ] | None = None ,
311316 land_margin : int = 0 ,
312317 verbose : bool = True ,
318+ bounds : list [list [float ]] | None = None ,
313319 ** kwargs : dict [str , Any ],
314320) -> cma .CMAEvolutionStrategy :
315321 curve : jnp .ndarray
316322 # Initialize the optimizer
323+ inopts : dict [str , Any ] = {
324+ "popsize" : popsize ,
325+ "tolfun" : tolfun ,
326+ "maxfevals" : maxfevals ,
327+ "seed" : seed ,
328+ "CSA_dampfac" : damping , # v positive multiplier for step-size damping
329+ }
330+ if bounds is not None :
331+ inopts ["bounds" ] = bounds
332+ inopts |= kwargs
317333 es = cma .CMAEvolutionStrategy (
318334 x0 ,
319335 sigma0 ,
320- inopts = {
321- "popsize" : popsize ,
322- "tolfun" : tolfun ,
323- "maxfevals" : maxfevals ,
324- "seed" : seed ,
325- "CSA_dampfac" : damping , # v positive multiplier for step-size damping
326- }
327- | kwargs ,
336+ inopts = inopts ,
328337 )
329338 # Check if the land penalization is consistent
330339 if land is not None :
@@ -379,18 +388,33 @@ def _cma_evolution_strategy(
379388 # toward fewer land points.
380389 cost = jnp .where (has_land , penalty + land_count , cost )
381390
391+ # Smooth distance-to-land penalty via EDT
392+ if land is not None and land_distance_weight > 0 :
393+ cost += land .distance_penalty (
394+ curve , weight = land_distance_weight , epsilon = land_distance_epsilon
395+ )
396+
382397 # Weather constraint penalization
383398 if weather_penalty_weight > 0 and (
384399 windfield is not None or wavefield is not None
385400 ):
386- cost += _weather_penalty (
387- curve ,
401+ _wp_fn = (
402+ _weather_penalty_smooth
403+ if weather_penalty_type == "smooth"
404+ else _weather_penalty
405+ )
406+ _wp_kwargs : dict [str , Any ] = dict (
388407 windfield = windfield ,
389408 wavefield = wavefield ,
390409 tws_limit = tws_limit ,
391410 hs_limit = hs_limit ,
392411 penalty = weather_penalty_weight ,
412+ travel_time = travel_time ,
413+ time_offset = time_offset ,
393414 )
415+ if weather_penalty_type == "smooth" :
416+ _wp_kwargs ["sharpness" ] = weather_penalty_sharpness
417+ cost += _wp_fn (curve , ** _wp_kwargs )
394418
395419 # Replace the worst solutions with the best found so far
396420 if keep_top > 0 and es .countiter > 1 :
@@ -435,7 +459,11 @@ def optimize(
435459 ]
436460 | None = None ,
437461 penalty : float = 1e10 ,
462+ land_distance_weight : float = 0.0 ,
463+ land_distance_epsilon : float = 1.0 ,
438464 weather_penalty_weight : float = 0.0 ,
465+ weather_penalty_type : str = "hard" ,
466+ weather_penalty_sharpness : float = 5.0 ,
439467 tws_limit : float = 20.0 ,
440468 hs_limit : float = 7.0 ,
441469 travel_stw : float | None = None ,
@@ -458,6 +486,7 @@ def optimize(
458486 cost_fn : Callable [[jnp .ndarray ], jnp .ndarray ] | None = None ,
459487 land_margin : int = 0 ,
460488 verbose : bool = True ,
489+ bounds : list [list [float ]] | None = None ,
461490) -> tuple [jnp .ndarray , dict [str , Any ]]:
462491 """
463492 Solve the vessel routing problem for a given vector field.
@@ -488,9 +517,17 @@ def optimize(
488517 penalty : float, optional
489518 Large penalty applied to routes that intersect land (death-penalty
490519 scheme), by default 1e10
520+ land_distance_weight : float, optional
521+ Weight for the smooth distance-to-land penalty via EDT.
522+ Set to 0 (default) to disable.
523+ land_distance_epsilon : float, optional
524+ Regularisation constant for the EDT penalty (default 1.0).
491525 weather_penalty_weight : float, optional
492526 Penalty weight for weather constraint violations (TWS, Hs).
493527 Set to 0 (default) to disable weather penalties.
528+ weather_penalty_type : str, optional
529+ ``"hard"`` (step function, default) or ``"smooth"`` (squared-ReLU
530+ ramp from :func:`weather_penalty_smooth`).
494531 tws_limit : float, optional
495532 Maximum allowed true wind speed in m/s, by default 20.0
496533 hs_limit : float, optional
@@ -529,6 +566,10 @@ def optimize(
529566 Random seed for reproducibility. By default jnp.nan
530567 verbose : bool, optional
531568 By default True
569+ bounds : list[list[float]] | None, optional
570+ Per-dimension ``[lower, upper]`` bounds for CMA-ES control-point
571+ parameters. Each list has length ``2*(K-2)``. ``None`` disables
572+ bounds (default).
532573
533574 Returns
534575 -------
@@ -594,7 +635,11 @@ def optimize(
594635 wavefield = wavefield ,
595636 windfield = windfield ,
596637 penalty = penalty ,
638+ land_distance_weight = land_distance_weight ,
639+ land_distance_epsilon = land_distance_epsilon ,
597640 weather_penalty_weight = weather_penalty_weight ,
641+ weather_penalty_type = weather_penalty_type ,
642+ weather_penalty_sharpness = weather_penalty_sharpness ,
598643 tws_limit = tws_limit ,
599644 hs_limit = hs_limit ,
600645 travel_stw = travel_stw ,
@@ -616,6 +661,7 @@ def optimize(
616661 cost_fn = cost_fn ,
617662 land_margin = land_margin ,
618663 verbose = verbose ,
664+ bounds = bounds ,
619665 )
620666 time_end = time .time ()
621667 if verbose :
@@ -660,14 +706,23 @@ def optimize(
660706 if weather_penalty_weight > 0 and (
661707 windfield is not None or wavefield is not None
662708 ):
663- cost_initial += _weather_penalty (
664- curve0 [jnp .newaxis , :, :],
709+ _wp_fn_c0 = (
710+ _weather_penalty_smooth
711+ if weather_penalty_type == "smooth"
712+ else _weather_penalty
713+ )
714+ _wp_kwargs_c0 : dict [str , Any ] = dict (
665715 windfield = windfield ,
666716 wavefield = wavefield ,
667717 tws_limit = tws_limit ,
668718 hs_limit = hs_limit ,
669719 penalty = weather_penalty_weight ,
670- ).item ()
720+ travel_time = travel_time ,
721+ time_offset = time_offset ,
722+ )
723+ if weather_penalty_type == "smooth" :
724+ _wp_kwargs_c0 ["sharpness" ] = weather_penalty_sharpness
725+ cost_initial += _wp_fn_c0 (curve0 [jnp .newaxis , :, :], ** _wp_kwargs_c0 ).item ()
671726 if cost_initial < cost_best :
672727 warnings .warn (
673728 "[WARNING] The optimized curve has a higher cost "
@@ -706,7 +761,11 @@ def optimize_with_increasing_penalization(
706761 penalty_init : float = 0 ,
707762 penalty_increment : float = 10 ,
708763 maxiter : int = 10 ,
764+ land_distance_weight : float = 0.0 ,
765+ land_distance_epsilon : float = 1.0 ,
709766 weather_penalty_weight : float = 0.0 ,
767+ weather_penalty_type : str = "hard" ,
768+ weather_penalty_sharpness : float = 5.0 ,
710769 tws_limit : float = 20.0 ,
711770 hs_limit : float = 7.0 ,
712771 travel_stw : float | None = None ,
@@ -761,6 +820,8 @@ def optimize_with_increasing_penalization(
761820 weather_penalty_weight : float, optional
762821 Penalty weight for weather constraint violations (TWS, Hs).
763822 Set to 0 (default) to disable weather penalties.
823+ weather_penalty_type : str, optional
824+ ``"hard"`` (step function, default) or ``"smooth"`` (squared-ReLU).
764825 tws_limit : float, optional
765826 Maximum allowed true wind speed in m/s, by default 20.0
766827 hs_limit : float, optional
@@ -823,7 +884,11 @@ def optimize_with_increasing_penalization(
823884 wavefield = wavefield ,
824885 windfield = windfield ,
825886 penalty = penalty ,
887+ land_distance_weight = land_distance_weight ,
888+ land_distance_epsilon = land_distance_epsilon ,
826889 weather_penalty_weight = weather_penalty_weight ,
890+ weather_penalty_type = weather_penalty_type ,
891+ weather_penalty_sharpness = weather_penalty_sharpness ,
827892 tws_limit = tws_limit ,
828893 hs_limit = hs_limit ,
829894 travel_stw = travel_stw ,
0 commit comments