@@ -38,15 +38,19 @@ def weighted_sum(objectives: Dataset, parameters: Any, **kwargs) -> DataArray:
3838 "weighted_sum" ,
3939]
4040
41- from collections .abc import Mapping , MutableMapping , Sequence
41+ from collections .abc import Hashable , Mapping , MutableMapping , Sequence
4242from typing import (
4343 Any ,
4444 Callable ,
4545)
4646
47+ import numpy as np
48+ import xarray as xr
4749from xarray import DataArray , Dataset
4850
4951from muse .registration import registrator
52+ from muse .timeslices import drop_timeslice
53+ from muse .utilities import tupled_dimension
5054
5155PARAMS_TYPE = Sequence [tuple [str , bool , float ]]
5256"""Standard decision parameter type.
@@ -170,63 +174,180 @@ def weighted_sum(objectives: Dataset, parameters: Mapping[str, float]) -> DataAr
170174def lexical_comparison (
171175 objectives : Dataset , parameters : PARAMS_TYPE | Sequence [tuple [str , float ]]
172176) -> DataArray :
173- """Lexical comparison over the objectives .
177+ """Lexicographic comparison using the best available replacements as reference .
174178
175179 Lexical comparison operates by binning the objectives into bins of width
176- w_i = min_j(p_i o_i^j). Once binned, dimensions other than `asset` and
177- `technology` are reduced by taking the max, e.g. the largest constraint.
178- Finally, the objectives are ranked lexographically, in the order given by the
179- parameters.
180+
181+ w_i = min_j(p_i o_i^j),
182+
183+ where o_i^j are the objective values of the candidate replacements and the
184+ minimum is taken over the replacement dimension. Once binned, dimensions
185+ other than ``asset`` and ``replacement`` are reduced by taking the maximum
186+ (e.g. the largest constraint). Finally, the objectives are ranked
187+ lexicographically in the order given by ``parameters``.
180188
181189 The result is an array of tuples which can subsequently be compared
182190 lexicographically.
183191 """
184- from muse .utilities import lexical_comparison
185-
186192 assert len (parameters ) > 0
187- if len (parameters [0 ]) == 3 :
188- parameters = [(u [0 ], coeff_sign (u [1 ], u [2 ])) for u in parameters ]
189- assert set (objectives .data_vars ).issuperset ([u [0 ] for u in parameters ])
190- order = [u [0 ] for u in parameters ]
191193
192- binsize = objectives .copy ()
193- for obj_name , weight in parameters :
194- binsize [obj_name ] = binsize [obj_name ] * weight
194+ # Convert (name, min/max, coefficient) specifications to
195+ # (name, signed coefficient), where the sign encodes whether the
196+ # objective should be minimised or maximised.
197+ if len (parameters [0 ]) == 3 :
198+ parameters = [
199+ (name , coeff_sign (minmax , coeff )) for name , minmax , coeff in parameters
200+ ]
201+
202+ # Lexicographic priority of the objectives.
203+ order = tuple (name for name , _ in parameters )
204+
205+ # All requested objectives must be present.
206+ assert set (objectives .data_vars ).issuperset (order )
207+
208+ # Define the bin widths as
209+ #
210+ # w_i = min_j(p_i o_i^j),
211+ #
212+ # i.e. the weighted objective values of the candidate replacements,
213+ # taking the minimum over the replacement dimension.
214+ binsize = objectives .copy (deep = True )
215+
216+ # Temporarily flatten the timeslice MultiIndex to avoid xarray's
217+ # deprecated Dataset assignment path when modifying variables.
218+ if "timeslice" in binsize .indexes :
219+ index_names = binsize .indexes ["timeslice" ].names
220+ binsize = binsize .reset_index ("timeslice" )
221+
222+ # Apply the objective weights (including minimisation/maximisation
223+ # direction encoded in the sign).
224+ for name , weight in parameters :
225+ binsize [name ] *= weight
226+
227+ # Restore the original timeslice MultiIndex structure.
228+ if "timeslice" in objectives .indexes :
229+ binsize = binsize .set_index (timeslice = index_names )
230+
231+ # Use the smallest weighted objective across replacements as the
232+ # bin width for each objective.
195233 binsize = binsize .min ("replacement" )
196234
197- return lexical_comparison (objectives , binsize , order = order , bin_last = False ).rank (
198- "replacement"
199- )
235+ # Construct lexicographically comparable tuples and rank the
236+ # replacement options accordingly.
237+ return _lexical_comparison (
238+ objectives ,
239+ binsize ,
240+ order = order ,
241+ keep_last_continuous = True ,
242+ ).rank ("replacement" )
200243
201244
202245@register_decision (name = "retro_lexo" )
203246def retro_lexical_comparison (
204- objectives : Dataset , parameters : PARAMS_TYPE | Sequence [tuple [str , float ]]
247+ objectives : Dataset ,
248+ parameters : PARAMS_TYPE | Sequence [tuple [str , float ]],
205249) -> DataArray :
206- """Lexical comparison over the objectives .
250+ """Lexicographic comparison using current assets as reference .
207251
208252 Lexical comparison operates by binning the objectives into bins of width
209- w_i = p_i o_i, where i are the current assets. Once binned, dimensions other
210- than `asset` and `replacement` are reduced by taking the max, e.g. the
211- largest constraint. Finally, the objectives are ranked lexographically, in
212- the order given by the parameters.
253+
254+ w_i = p_i o_i,
255+
256+ where o_i are the objective values of the current assets. Once binned,
257+ dimensions other than ``asset`` and ``replacement`` are reduced by taking
258+ the maximum (e.g. the largest constraint). Finally, the objectives are
259+ ranked lexicographically in the order given by ``parameters``.
213260
214261 The result is an array of tuples which can subsequently be compared
215262 lexicographically.
216263 """
217- from muse .utilities import lexical_comparison
218-
219264 assert len (parameters ) > 0
265+
266+ # Convert (name, min/max, coefficient) specifications to
267+ # (name, signed coefficient), where the sign encodes whether the
268+ # objective should be minimised or maximised.
220269 if len (parameters [0 ]) == 3 :
221- parameters = [(u [0 ], coeff_sign (u [1 ], u [2 ])) for u in parameters ]
270+ parameters = [
271+ (name , coeff_sign (minmax , coeff )) for name , minmax , coeff in parameters
272+ ]
273+
274+ # Retrofitting compares candidate replacements against the current
275+ # asset, so every asset must also appear amongst the replacements.
222276 assert objectives .asset .isin (objectives .replacement ).all ()
223- assert set (objectives .data_vars ).issuperset ([u [0 ] for u in parameters ])
224277
225- order = [u [0 ] for u in parameters ]
226- binsize = Dataset (dict (parameters )) * objectives .sel (replacement = objectives .asset )
227- return lexical_comparison (objectives , binsize , order = order , bin_last = False ).rank (
228- "replacement"
229- )
278+ # Lexicographic priority of the objectives.
279+ order = tuple (name for name , _ in parameters )
280+
281+ # All requested objectives must be present.
282+ assert set (objectives .data_vars ).issuperset (order )
283+
284+ # Define the bin widths as
285+ #
286+ # w_i = p_i o_i,
287+ #
288+ # where o_i are the objective values of the current assets. The
289+ # objective values are selected by matching each asset to itself
290+ # along the replacement dimension.
291+ binwidths = Dataset (dict (parameters )) * objectives .sel (replacement = objectives .asset )
292+
293+ # Construct lexicographically comparable tuples and rank the
294+ # replacement options accordingly.
295+ return _lexical_comparison (
296+ objectives ,
297+ binwidths ,
298+ order = order ,
299+ keep_last_continuous = True ,
300+ ).rank ("replacement" )
301+
302+
303+ def _lexical_comparison (
304+ objectives : xr .Dataset ,
305+ binwidths : xr .Dataset ,
306+ order : Sequence [Hashable ],
307+ * ,
308+ keep_last_continuous : bool = False ,
309+ ) -> xr .DataArray :
310+ """Lexical comparison over the objectives.
311+
312+ Lexical comparison operates by binning the objectives into bins of width
313+ ``binwidths``. Once binned, dimensions other than ``asset`` and
314+ ``replacement`` are reduced by taking the maximum (e.g. the largest
315+ constraint). Finally, the objectives are ranked lexicographically in the
316+ order given by ``order``.
317+
318+ Arguments:
319+ objectives: xr.Dataset containing the objectives to rank.
320+ binwidths: Bin widths used to discretise the objectives.
321+ order: Order in which objectives are compared lexicographically.
322+ keep_last_continuous: Whether the final objective should be left as a
323+ continuous value, rather than being discretised.
324+
325+ Result:
326+ An array of tuples which can subsequently be compared
327+ lexicographically.
328+ """
329+ # Restrict to the objectives participating in the comparison and
330+ # temporarily flatten the timeslice MultiIndex to avoid Dataset
331+ # assignment issues in xarray.
332+ result = drop_timeslice (objectives [list (order )]).copy ()
333+
334+ # All objectives are discretised except, optionally, the final
335+ # tie-breaking objective.
336+ discretized = order [:- 1 ] if keep_last_continuous else order
337+
338+ # Convert objectives to integer-valued bins.
339+ for name in discretized :
340+ result [name ] = np .floor (result [name ] / binwidths [name ]).astype (np .int64 )
341+
342+ # Preserve the final objective as a continuous quantity for
343+ # tie-breaking, while still normalising by its bin width.
344+ if keep_last_continuous :
345+ name = order [- 1 ]
346+ result [name ] = result [name ] / binwidths [name ]
347+
348+ # Combine the ordered objectives into tuples that can be compared
349+ # lexicographically.
350+ return result .to_array (dim = "objective" ).reduce (tupled_dimension , dim = "objective" )
230351
231352
232353def _epsilon_constraints (
0 commit comments