Skip to content

Commit 5dc46e8

Browse files
committed
fix: effects sankey
1 parent e2420a7 commit 5dc46e8

1 file changed

Lines changed: 93 additions & 43 deletions

File tree

flixopt/statistics_accessor.py

Lines changed: 93 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1155,45 +1155,96 @@ def _prepare_sankey_data(
11551155

11561156
return ds, title
11571157

1158-
def _prepare_effects_sankey_data(
1158+
def _build_effects_sankey(
11591159
self,
1160-
effect: str,
11611160
select: SelectType | None,
1162-
) -> tuple[xr.Dataset, str]:
1163-
"""Prepare effects data for Sankey diagram.
1161+
colors: ColorType | None,
1162+
**plotly_kwargs: Any,
1163+
) -> tuple[go.Figure, xr.Dataset]:
1164+
"""Build Sankey diagram showing contributions from components to effects.
1165+
1166+
Creates a Sankey with:
1167+
- Left side: Components (grouped by type)
1168+
- Right side: Effects (costs, CO2, etc.)
1169+
- Links: Contributions from each component to each effect
11641170
11651171
Args:
1166-
effect: Effect name to display (e.g., 'costs', 'CO2').
11671172
select: xarray-style selection.
1173+
colors: Color specification for nodes.
1174+
**plotly_kwargs: Additional Plotly layout arguments.
11681175
11691176
Returns:
1170-
Tuple of (Dataset with flow labels as variables, title string).
1177+
Tuple of (Plotly Figure, Dataset with link data).
11711178
"""
11721179
total_effects = self._stats.total_effects
1173-
if effect not in total_effects:
1174-
available = list(total_effects.data_vars)
1175-
raise ValueError(f"Effect '{effect}' not found. Available effects: {available}")
11761180

1177-
effect_data = total_effects[effect]
1178-
effect_data = _apply_selection(effect_data, select)
1181+
# Collect all links: component -> effect
1182+
nodes: set[str] = set()
1183+
links: dict[str, list] = {'source': [], 'target': [], 'value': [], 'label': []}
11791184

1180-
# Sum over any remaining dimensions (period, scenario)
1181-
for dim in ['period', 'scenario']:
1182-
if dim in effect_data.dims:
1183-
effect_data = effect_data.sum(dim=dim)
1185+
for effect_name in total_effects.data_vars:
1186+
effect_data = total_effects[effect_name]
1187+
effect_data = _apply_selection(effect_data, select)
11841188

1185-
# Extract flow-related contributors and create a Dataset with flow labels as variables
1186-
flow_labels = list(self._fs.flows.keys())
1187-
contributors = effect_data.coords['contributor'].values
1189+
# Sum over any remaining dimensions
1190+
for dim in ['period', 'scenario']:
1191+
if dim in effect_data.dims:
1192+
effect_data = effect_data.sum(dim=dim)
1193+
1194+
contributors = effect_data.coords['contributor'].values
1195+
components = effect_data.coords['component'].values
11881196

1189-
result = {}
1190-
for flow_label in flow_labels:
1191-
if flow_label in contributors:
1192-
value = float(effect_data.sel(contributor=flow_label).values)
1193-
if abs(value) > 1e-6:
1194-
result[flow_label] = xr.DataArray(value)
1197+
for contributor, component in zip(contributors, components, strict=False):
1198+
value = float(effect_data.sel(contributor=contributor).values)
1199+
if not np.isfinite(value) or abs(value) < 1e-6:
1200+
continue
11951201

1196-
return xr.Dataset(result), f'{effect.capitalize()} per Flow'
1202+
# Use component as source node, effect as target
1203+
source = str(component)
1204+
target = f'[{effect_name}]' # Bracket notation to distinguish effects
1205+
1206+
nodes.add(source)
1207+
nodes.add(target)
1208+
links['source'].append(source)
1209+
links['target'].append(target)
1210+
links['value'].append(abs(value))
1211+
links['label'].append(f'{contributor}{effect_name}: {value:.2f}')
1212+
1213+
# Create figure
1214+
node_list = list(nodes)
1215+
node_indices = {n: i for i, n in enumerate(node_list)}
1216+
1217+
color_map = process_colors(colors, node_list)
1218+
node_colors = [color_map[node] for node in node_list]
1219+
1220+
fig = go.Figure(
1221+
data=[
1222+
go.Sankey(
1223+
node=dict(
1224+
pad=15, thickness=20, line=dict(color='black', width=0.5), label=node_list, color=node_colors
1225+
),
1226+
link=dict(
1227+
source=[node_indices[s] for s in links['source']],
1228+
target=[node_indices[t] for t in links['target']],
1229+
value=links['value'],
1230+
label=links['label'],
1231+
),
1232+
)
1233+
]
1234+
)
1235+
fig.update_layout(title='Effect Contributions by Component', **plotly_kwargs)
1236+
1237+
sankey_ds = xr.Dataset(
1238+
{'value': ('link', links['value'])},
1239+
coords={
1240+
'link': range(len(links['value'])),
1241+
'source': ('link', links['source']),
1242+
'target': ('link', links['target']),
1243+
'label': ('link', links['label']),
1244+
},
1245+
)
1246+
1247+
return fig, sankey_ds
11971248

11981249
def _build_sankey_links(
11991250
self,
@@ -1283,8 +1334,7 @@ def _create_sankey_figure(
12831334
def sankey(
12841335
self,
12851336
*,
1286-
mode: Literal['flow_hours', 'sizes', 'peak_flow'] = 'flow_hours',
1287-
effect: str | None = None,
1337+
mode: Literal['flow_hours', 'sizes', 'peak_flow', 'effects'] = 'flow_hours',
12881338
timestep: int | str | None = None,
12891339
aggregate: Literal['sum', 'mean'] = 'sum',
12901340
select: SelectType | None = None,
@@ -1299,8 +1349,7 @@ def sankey(
12991349
- 'flow_hours': Energy/material amounts (default)
13001350
- 'sizes': Investment capacities
13011351
- 'peak_flow': Maximum flow rates
1302-
effect: Effect name to display instead of mode (e.g., 'costs', 'CO2').
1303-
If provided, shows effect contributions per flow. Overrides mode.
1352+
- 'effects': Component contributions to all effects (costs, CO2, etc.)
13041353
timestep: Specific timestep to show, or None for aggregation (flow_hours only).
13051354
aggregate: How to aggregate if timestep is None ('sum' or 'mean', flow_hours only).
13061355
select: xarray-style selection.
@@ -1318,25 +1367,26 @@ def sankey(
13181367
>>> flow_system.statistics.plot.sankey(mode='sizes')
13191368
>>> # Show peak flow rates
13201369
>>> flow_system.statistics.plot.sankey(mode='peak_flow')
1321-
>>> # Show costs per flow
1322-
>>> flow_system.statistics.plot.sankey(effect='costs')
1323-
>>> # Show CO2 emissions per flow
1324-
>>> flow_system.statistics.plot.sankey(effect='CO2')
1370+
>>> # Show effect contributions (components -> effects like costs, CO2)
1371+
>>> flow_system.statistics.plot.sankey(mode='effects')
13251372
"""
13261373
self._stats._require_solution()
13271374

1328-
if effect is not None:
1329-
ds, title = self._prepare_effects_sankey_data(effect, select)
1375+
if mode == 'effects':
1376+
fig, sankey_ds = self._build_effects_sankey(select, colors, **plotly_kwargs)
13301377
else:
13311378
ds, title = self._prepare_sankey_data(mode, timestep, aggregate, select)
1332-
1333-
nodes, links = self._build_sankey_links(ds)
1334-
fig = self._create_sankey_figure(nodes, links, colors, title, **plotly_kwargs)
1335-
1336-
sankey_ds = xr.Dataset(
1337-
{'value': ('link', links['value'])},
1338-
coords={'link': links['label'], 'source': ('link', links['source']), 'target': ('link', links['target'])},
1339-
)
1379+
nodes, links = self._build_sankey_links(ds)
1380+
fig = self._create_sankey_figure(nodes, links, colors, title, **plotly_kwargs)
1381+
1382+
sankey_ds = xr.Dataset(
1383+
{'value': ('link', links['value'])},
1384+
coords={
1385+
'link': links['label'],
1386+
'source': ('link', links['source']),
1387+
'target': ('link', links['target']),
1388+
},
1389+
)
13401390

13411391
if show is None:
13421392
show = CONFIG.Plotting.default_show

0 commit comments

Comments
 (0)