diff --git a/src/fluxopt/stats.py b/src/fluxopt/stats.py index 9af6367..eea7a2b 100644 --- a/src/fluxopt/stats.py +++ b/src/fluxopt/stats.py @@ -100,3 +100,33 @@ def effect_contributions(self) -> xr.Dataset: self._result.data, self._result.solution, ) + + def summary(self) -> xr.Dataset: + """Headline KPIs overview. + + Returns a tidy dataset with the objective value, total effects, + and per-flow full load hours. + """ + import numpy as np + import xarray as xr + + ds = xr.Dataset() + ds['objective'] = xr.DataArray(self._result.objective) + + if self._result.effect_totals is not None and len(self._result.effect_totals) > 0: + ds['effect_totals'] = self._result.effect_totals + + # Compute full load hours + combined_size = self._result.data.flows.size.copy() + sizes = self._result.sizes + # `sizes` is an empty 0-d DataArray when no flow has an investment size; + # only fill from it when it actually carries per-flow sizes. + if 'flow' in sizes.dims: + combined_size = combined_size.fillna(sizes) + + with xr.set_options(keep_attrs=True): + flh = self.total_flow_hours / combined_size + flh = flh.where(np.isfinite(flh)) + + ds['full_load_hours'] = flh + return ds diff --git a/tests/test_stats_summary.py b/tests/test_stats_summary.py new file mode 100644 index 0000000..9060024 --- /dev/null +++ b/tests/test_stats_summary.py @@ -0,0 +1,36 @@ +import numpy as np +from conftest import ts + +from fluxopt import Carrier, Effect, Flow, Port, optimize + + +def test_stats_summary_quickstart(): + """`result.stats.summary()` exposes objective, effect totals and full-load hours.""" + demand = Flow('elec', size=100, fixed_relative_profile=[0.5, 0.8, 0.6]) + source = Flow('elec', size=200, effects_per_flow_hour={'cost': 0.04}) + + result = optimize( + timesteps=ts(3), + carriers=[Carrier('elec')], + effects=[Effect('cost')], + objective_effects='cost', + ports=[Port('grid', imports=[source]), Port('demand', exports=[demand])], + ) + + summary = result.stats.summary() + + # KPIs are present... + assert 'objective' in summary + assert 'effect_totals' in summary + assert 'full_load_hours' in summary + + # ...and carry meaningful content, not just keys. + assert np.isfinite(summary['objective'].item()) + assert 'cost' in summary['effect_totals'].coords['effect'].values + assert np.isfinite(summary['effect_totals'].sel(effect='cost').item()) + + flh = summary['full_load_hours'] + assert 'grid(elec)' in flh.coords['flow'].values + source_flh = flh.sel(flow='grid(elec)').item() + assert np.isfinite(source_flh) + assert source_flh >= 0