@@ -49,7 +49,7 @@ def weighted_sum(objectives: Dataset, parameters: Any, **kwargs) -> DataArray:
4949from xarray import DataArray , Dataset
5050
5151from muse .registration import registrator
52- from muse .timeslices import drop_timeslice
52+ from muse .timeslices import broadcast_timeslice , drop_timeslice
5353from muse .utilities import tupled_dimension
5454
5555PARAMS_TYPE = Sequence [tuple [str , bool , float ]]
@@ -351,16 +351,33 @@ def _lexical_comparison(
351351
352352
353353def _epsilon_constraints (
354- objectives : Dataset , optimize : str , mask : Any | None = None , ** epsilons
354+ objectives : Dataset ,
355+ optimize : str ,
356+ mask : Any | None = None ,
357+ ** epsilons ,
355358) -> DataArray :
356- """Minimizes one objective subject to constraints on other objectives."""
359+ """Selects the best value of a target objective subject to epsilon constraints.
360+
361+ Each constraint enforces that an objective must be below (or above, after
362+ sign handling upstream) a threshold, aggregated over all non-(asset,
363+ replacement) dimensions.
364+ """
365+ # Start with all options feasible
357366 constraints = True
367+
368+ # Build feasibility mask from epsilon constraints
358369 for name , epsilon in epsilons .items ():
370+ # Reduce over all non-decision dimensions (e.g. timeslice, region)
359371 reduced_dims = set (objectives [name ].dims ) - {"asset" , "replacement" }
372+
373+ # All slices must satisfy constraint
360374 constraints = constraints & (objectives [name ] <= epsilon ).all (reduced_dims )
361375
376+ # Default mask = something worse than any feasible objective value
362377 if mask is None :
363378 mask = objectives [optimize ].max () + 1
379+
380+ # Return objective values, masking infeasible alternatives
364381 return objectives [optimize ].where (constraints , mask )
365382
366383
@@ -370,60 +387,79 @@ def epsilon_constraints(
370387 parameters : PARAMS_TYPE | Sequence [tuple [str , bool , float ]],
371388 mask : Any | None = None ,
372389) -> DataArray :
373- r """Minimizes first objective subject to constraints on other objectives .
390+ """Epsilon-constraint optimisation .
374391
375- The parameters are a sequence of tuples `(name, minimize, epsilon)`, where
376- `name` is the name of the objective, `minimize` is `True` if minimizing and
377- false if maximizing that objective, and `epsilon` is the constraint. The
378- first objective is the one that will be minimized according to:
392+ The first objective is optimised (min or max), while all subsequent
393+ objectives are treated as constraints of the form:
379394
380- Given objectives :math:`O^{(i)}_t`, with :math:`i \in [|1, N|]` and :math:`t` the
381- replacement technologies, this function computes the ranking with respect to
382- :math:`t`:
395+ objective_i <= epsilon_i
383396
384- .. math::
397+ after sign normalization.
398+ """
399+ assert set (objectives .data_vars ).issuperset ([p [0 ] for p in parameters ])
385400
386- \mathrm{ranking}_{O^{(i)}_t < \epsilon_i} O^{(0)}_t
401+ # Remove obj_data parameters if present
402+ optimize_name , optimize_minimize , _ = parameters [0 ]
387403
404+ # Encode optimization direction
405+ do_minimize = Dataset ({optimize_name : 1 if optimize_minimize else - 1 })
388406
389- The first tuple can be restricted to `(name, minimize)`, since `epsilon` is ignored.
407+ # Remaining objectives also get sign encoding
408+ for name , minimize , _ in parameters [1 :]:
409+ do_minimize [name ] = coeff_sign (minimize , 1 )
390410
391- The result is the matrix :math:`O^{(0)}` modified such minimizing over the
392- replacement dimension value would take into account the constraints and the
393- optimization direction (minimize or maximize). In other words, calling
394- `result.rank('replacement')` will yield the expected result.
395- """
396- assert set ( objectives . data_vars ). issuperset ([ param [ 0 ] for param in parameters ])
397- do_minimize = Dataset ({ k : coeff_sign ( v , 1 ) for k , v , _ in parameters [ 1 :]})
398- do_minimize [ parameters [ 0 ][ 0 ]] = 1 if parameters [ 0 ][ 1 ] else - 1
399- dict_params = { k : v for k , _ , v in parameters [ 1 :] if k in objectives .data_vars }
400- constraints = do_minimize * Dataset ( dict_params )
411+ # Extract epsilon constraints
412+ epsilons = {
413+ name : coeff_sign (minimize , 1 ) * eps
414+ for name , minimize , eps in parameters [ 1 :]
415+ if name in objectives . data_vars
416+ }
417+
418+ # Apply sign transformation + constraints
419+ if "timeslice" in objectives .indexes :
420+ do_minimize = broadcast_timeslice ( do_minimize )
401421 return _epsilon_constraints (
402- objectives * do_minimize , parameters [0 ][0 ], mask = mask , ** constraints .data_vars
422+ objectives * do_minimize ,
423+ optimize_name ,
424+ mask = mask ,
425+ ** epsilons ,
403426 )
404427
405428
406429@register_decision (name = "retro_epsilon" )
407430def retro_epsilon_constraints (
408- objectives : Dataset , parameters : PARAMS_TYPE
431+ objectives : Dataset ,
432+ parameters : PARAMS_TYPE ,
409433) -> DataArray :
410- """Epsilon constraints where the current tech is included .
434+ """Epsilon-constraint optimisation with asset-relative thresholds .
411435
412- Modifies the parameters to the function such that the existing technologies are
413- always competitive .
436+ Epsilon thresholds are adjusted so that the current technology is always
437+ feasible, ensuring it remains in the choice set .
414438 """
439+ # Extract current asset baseline
415440 asset_objectives = objectives .sel (replacement = objectives .asset )
416441
417- def transform (name , minimize , epsilon = None ):
442+ def adapt_param (name , minimize , epsilon = None ):
443+ """Adjust epsilon so that current asset is always feasible."""
418444 if epsilon is None :
419445 return name , minimize
420- am = getattr (asset_objectives , name )
421- new_eps = am .where (am > epsilon if minimize else am < epsilon , epsilon )
422- return name , minimize , new_eps
423446
424- parameters = [
425- transform (* param ) for param in parameters if param [0 ] in objectives .data_vars
426- ]
447+ current = asset_objectives [name ]
448+
449+ # Work in the same transformed logic as epsilon_constraints
450+ sign = - 1 if minimize else 1
451+
452+ # Ensure current asset is not excluded by its own constraint
453+ adjusted = current .where (
454+ (sign * current ) <= (sign * epsilon ),
455+ epsilon ,
456+ )
457+
458+ return name , minimize , adjusted
459+
460+ # Filter valid objectives and adapt epsilons
461+ parameters = [adapt_param (* p ) for p in parameters if p [0 ] in objectives .data_vars ]
462+
427463 return epsilon_constraints (objectives , parameters )
428464
429465
0 commit comments