flixopt 3.0.1__py3-none-any.whl → 6.0.0rc7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flixopt/__init__.py +57 -49
- flixopt/carrier.py +159 -0
- flixopt/clustering/__init__.py +51 -0
- flixopt/clustering/base.py +1746 -0
- flixopt/clustering/intercluster_helpers.py +201 -0
- flixopt/color_processing.py +372 -0
- flixopt/comparison.py +819 -0
- flixopt/components.py +848 -270
- flixopt/config.py +853 -496
- flixopt/core.py +111 -98
- flixopt/effects.py +294 -284
- flixopt/elements.py +484 -223
- flixopt/features.py +220 -118
- flixopt/flow_system.py +2026 -389
- flixopt/interface.py +504 -286
- flixopt/io.py +1718 -55
- flixopt/linear_converters.py +291 -230
- flixopt/modeling.py +304 -181
- flixopt/network_app.py +2 -1
- flixopt/optimization.py +788 -0
- flixopt/optimize_accessor.py +373 -0
- flixopt/plot_result.py +143 -0
- flixopt/plotting.py +1177 -1034
- flixopt/results.py +1331 -372
- flixopt/solvers.py +12 -4
- flixopt/statistics_accessor.py +2412 -0
- flixopt/stats_accessor.py +75 -0
- flixopt/structure.py +954 -120
- flixopt/topology_accessor.py +676 -0
- flixopt/transform_accessor.py +2277 -0
- flixopt/types.py +120 -0
- flixopt-6.0.0rc7.dist-info/METADATA +290 -0
- flixopt-6.0.0rc7.dist-info/RECORD +36 -0
- {flixopt-3.0.1.dist-info → flixopt-6.0.0rc7.dist-info}/WHEEL +1 -1
- flixopt/aggregation.py +0 -382
- flixopt/calculation.py +0 -672
- flixopt/commons.py +0 -51
- flixopt/utils.py +0 -86
- flixopt-3.0.1.dist-info/METADATA +0 -209
- flixopt-3.0.1.dist-info/RECORD +0 -26
- {flixopt-3.0.1.dist-info → flixopt-6.0.0rc7.dist-info}/licenses/LICENSE +0 -0
- {flixopt-3.0.1.dist-info → flixopt-6.0.0rc7.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Optimization accessor for FlowSystem.
|
|
3
|
+
|
|
4
|
+
This module provides the OptimizeAccessor class that enables the
|
|
5
|
+
`flow_system.optimize(...)` pattern with extensible optimization methods.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import sys
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
import xarray as xr
|
|
15
|
+
from tqdm import tqdm
|
|
16
|
+
|
|
17
|
+
from .config import CONFIG
|
|
18
|
+
from .io import suppress_output
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from .flow_system import FlowSystem
|
|
22
|
+
from .solvers import _Solver
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger('flixopt')
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class OptimizeAccessor:
|
|
28
|
+
"""
|
|
29
|
+
Accessor for optimization methods on FlowSystem.
|
|
30
|
+
|
|
31
|
+
This class provides the optimization API for FlowSystem, accessible via
|
|
32
|
+
`flow_system.optimize`. It supports both direct calling (standard optimization)
|
|
33
|
+
and method access for specialized optimization modes.
|
|
34
|
+
|
|
35
|
+
Examples:
|
|
36
|
+
Standard optimization (via __call__):
|
|
37
|
+
|
|
38
|
+
>>> flow_system.optimize(solver)
|
|
39
|
+
>>> print(flow_system.solution)
|
|
40
|
+
|
|
41
|
+
Rolling horizon optimization:
|
|
42
|
+
|
|
43
|
+
>>> segments = flow_system.optimize.rolling_horizon(solver, horizon=168)
|
|
44
|
+
>>> print(flow_system.solution) # Combined result
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, flow_system: FlowSystem) -> None:
|
|
48
|
+
"""
|
|
49
|
+
Initialize the accessor with a reference to the FlowSystem.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
flow_system: The FlowSystem to optimize.
|
|
53
|
+
"""
|
|
54
|
+
self._fs = flow_system
|
|
55
|
+
|
|
56
|
+
def __call__(self, solver: _Solver, normalize_weights: bool | None = None) -> FlowSystem:
|
|
57
|
+
"""
|
|
58
|
+
Build and solve the optimization model in one step.
|
|
59
|
+
|
|
60
|
+
This is a convenience method that combines `build_model()` and `solve()`.
|
|
61
|
+
Use this for simple optimization workflows. For more control (e.g., inspecting
|
|
62
|
+
the model before solving, or adding custom constraints), use `build_model()`
|
|
63
|
+
and `solve()` separately.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
solver: The solver to use (e.g., HighsSolver, GurobiSolver).
|
|
67
|
+
normalize_weights: Deprecated. Scenario weights are now always normalized in FlowSystem.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
The FlowSystem, for method chaining.
|
|
71
|
+
|
|
72
|
+
Examples:
|
|
73
|
+
Simple optimization:
|
|
74
|
+
|
|
75
|
+
>>> flow_system.optimize(HighsSolver())
|
|
76
|
+
>>> print(flow_system.solution['Boiler(Q_th)|flow_rate'])
|
|
77
|
+
|
|
78
|
+
Access element solutions directly:
|
|
79
|
+
|
|
80
|
+
>>> flow_system.optimize(solver)
|
|
81
|
+
>>> boiler = flow_system.components['Boiler']
|
|
82
|
+
>>> print(boiler.solution)
|
|
83
|
+
|
|
84
|
+
Method chaining:
|
|
85
|
+
|
|
86
|
+
>>> solution = flow_system.optimize(solver).solution
|
|
87
|
+
"""
|
|
88
|
+
if normalize_weights is not None:
|
|
89
|
+
import warnings
|
|
90
|
+
|
|
91
|
+
from .config import DEPRECATION_REMOVAL_VERSION
|
|
92
|
+
|
|
93
|
+
warnings.warn(
|
|
94
|
+
f'\n\nnormalize_weights parameter is deprecated and will be removed in {DEPRECATION_REMOVAL_VERSION}. '
|
|
95
|
+
'Scenario weights are now always normalized when set on FlowSystem.\n',
|
|
96
|
+
DeprecationWarning,
|
|
97
|
+
stacklevel=2,
|
|
98
|
+
)
|
|
99
|
+
self._fs.build_model()
|
|
100
|
+
self._fs.solve(solver)
|
|
101
|
+
return self._fs
|
|
102
|
+
|
|
103
|
+
def rolling_horizon(
|
|
104
|
+
self,
|
|
105
|
+
solver: _Solver,
|
|
106
|
+
horizon: int = 100,
|
|
107
|
+
overlap: int = 0,
|
|
108
|
+
nr_of_previous_values: int = 1,
|
|
109
|
+
) -> list[FlowSystem]:
|
|
110
|
+
"""
|
|
111
|
+
Solve the optimization using a rolling horizon approach.
|
|
112
|
+
|
|
113
|
+
Divides the time horizon into overlapping segments that are solved sequentially.
|
|
114
|
+
Each segment uses final values from the previous segment as initial conditions,
|
|
115
|
+
ensuring dynamic continuity across the solution. The combined solution is stored
|
|
116
|
+
on the original FlowSystem.
|
|
117
|
+
|
|
118
|
+
This approach is useful for:
|
|
119
|
+
- Large-scale problems that exceed memory limits
|
|
120
|
+
- Annual planning with seasonal variations
|
|
121
|
+
- Operational planning with limited foresight
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
solver: The solver to use (e.g., HighsSolver, GurobiSolver).
|
|
125
|
+
horizon: Number of timesteps in each segment (excluding overlap).
|
|
126
|
+
Must be > 2. Larger values provide better optimization at the cost
|
|
127
|
+
of memory and computation time. Default: 100.
|
|
128
|
+
overlap: Number of additional timesteps added to each segment for lookahead.
|
|
129
|
+
Improves storage optimization by providing foresight. Higher values
|
|
130
|
+
improve solution quality but increase computational cost. Default: 0.
|
|
131
|
+
nr_of_previous_values: Number of previous timestep values to transfer between
|
|
132
|
+
segments for initialization (e.g., for uptime/downtime tracking). Default: 1.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
List of segment FlowSystems, each with their individual solution.
|
|
136
|
+
The combined solution (with overlaps trimmed) is stored on the original FlowSystem.
|
|
137
|
+
|
|
138
|
+
Raises:
|
|
139
|
+
ValueError: If horizon <= 2 or overlap < 0.
|
|
140
|
+
ValueError: If horizon + overlap > total timesteps.
|
|
141
|
+
ValueError: If InvestParameters are used (not supported in rolling horizon).
|
|
142
|
+
|
|
143
|
+
Examples:
|
|
144
|
+
Basic rolling horizon optimization:
|
|
145
|
+
|
|
146
|
+
>>> segments = flow_system.optimize.rolling_horizon(
|
|
147
|
+
... solver,
|
|
148
|
+
... horizon=168, # Weekly segments
|
|
149
|
+
... overlap=24, # 1-day lookahead
|
|
150
|
+
... )
|
|
151
|
+
>>> print(flow_system.solution) # Combined result
|
|
152
|
+
|
|
153
|
+
Inspect individual segments:
|
|
154
|
+
|
|
155
|
+
>>> for i, seg in enumerate(segments):
|
|
156
|
+
... print(f'Segment {i}: {seg.solution["costs"].item():.2f}')
|
|
157
|
+
|
|
158
|
+
Note:
|
|
159
|
+
- InvestParameters are not supported as investment decisions require
|
|
160
|
+
full-horizon optimization.
|
|
161
|
+
- Global constraints (flow_hours_max, etc.) may produce suboptimal results
|
|
162
|
+
as they cannot be enforced globally across segments.
|
|
163
|
+
- Storage optimization may be suboptimal compared to full-horizon solutions
|
|
164
|
+
due to limited foresight in each segment.
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
# Validation
|
|
168
|
+
if horizon <= 2:
|
|
169
|
+
raise ValueError('horizon must be greater than 2 to avoid internal side effects.')
|
|
170
|
+
if overlap < 0:
|
|
171
|
+
raise ValueError('overlap must be non-negative.')
|
|
172
|
+
if nr_of_previous_values < 0:
|
|
173
|
+
raise ValueError('nr_of_previous_values must be non-negative.')
|
|
174
|
+
if nr_of_previous_values > horizon:
|
|
175
|
+
raise ValueError('nr_of_previous_values cannot exceed horizon.')
|
|
176
|
+
|
|
177
|
+
total_timesteps = len(self._fs.timesteps)
|
|
178
|
+
horizon_with_overlap = horizon + overlap
|
|
179
|
+
|
|
180
|
+
if horizon_with_overlap > total_timesteps:
|
|
181
|
+
raise ValueError(
|
|
182
|
+
f'horizon + overlap ({horizon_with_overlap}) cannot exceed total timesteps ({total_timesteps}).'
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# Ensure flow system is connected
|
|
186
|
+
if not self._fs.connected_and_transformed:
|
|
187
|
+
self._fs.connect_and_transform()
|
|
188
|
+
|
|
189
|
+
# Calculate segment indices
|
|
190
|
+
segment_indices = self._calculate_segment_indices(total_timesteps, horizon, overlap)
|
|
191
|
+
n_segments = len(segment_indices)
|
|
192
|
+
logger.info(
|
|
193
|
+
f'Starting Rolling Horizon Optimization - Segments: {n_segments}, Horizon: {horizon}, Overlap: {overlap}'
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# Create and solve segments
|
|
197
|
+
segment_flow_systems: list[FlowSystem] = []
|
|
198
|
+
|
|
199
|
+
progress_bar = tqdm(
|
|
200
|
+
enumerate(segment_indices),
|
|
201
|
+
total=n_segments,
|
|
202
|
+
desc='Solving segments',
|
|
203
|
+
unit='segment',
|
|
204
|
+
file=sys.stdout,
|
|
205
|
+
disable=not CONFIG.Solving.log_to_console,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
try:
|
|
209
|
+
for i, (start_idx, end_idx) in progress_bar:
|
|
210
|
+
progress_bar.set_description(f'Segment {i + 1}/{n_segments} (timesteps {start_idx}-{end_idx})')
|
|
211
|
+
|
|
212
|
+
# Suppress output when progress bar is shown (including logger and solver)
|
|
213
|
+
if CONFIG.Solving.log_to_console:
|
|
214
|
+
# Temporarily raise logger level to suppress INFO messages
|
|
215
|
+
original_level = logger.level
|
|
216
|
+
logger.setLevel(logging.WARNING)
|
|
217
|
+
try:
|
|
218
|
+
with suppress_output():
|
|
219
|
+
segment_fs = self._fs.transform.isel(time=slice(start_idx, end_idx))
|
|
220
|
+
if i > 0 and nr_of_previous_values > 0:
|
|
221
|
+
self._transfer_state(
|
|
222
|
+
source_fs=segment_flow_systems[i - 1],
|
|
223
|
+
target_fs=segment_fs,
|
|
224
|
+
horizon=horizon,
|
|
225
|
+
nr_of_previous_values=nr_of_previous_values,
|
|
226
|
+
)
|
|
227
|
+
segment_fs.build_model()
|
|
228
|
+
if i == 0:
|
|
229
|
+
self._check_no_investments(segment_fs)
|
|
230
|
+
segment_fs.solve(solver)
|
|
231
|
+
finally:
|
|
232
|
+
logger.setLevel(original_level)
|
|
233
|
+
else:
|
|
234
|
+
segment_fs = self._fs.transform.isel(time=slice(start_idx, end_idx))
|
|
235
|
+
if i > 0 and nr_of_previous_values > 0:
|
|
236
|
+
self._transfer_state(
|
|
237
|
+
source_fs=segment_flow_systems[i - 1],
|
|
238
|
+
target_fs=segment_fs,
|
|
239
|
+
horizon=horizon,
|
|
240
|
+
nr_of_previous_values=nr_of_previous_values,
|
|
241
|
+
)
|
|
242
|
+
segment_fs.build_model()
|
|
243
|
+
if i == 0:
|
|
244
|
+
self._check_no_investments(segment_fs)
|
|
245
|
+
segment_fs.solve(solver)
|
|
246
|
+
|
|
247
|
+
segment_flow_systems.append(segment_fs)
|
|
248
|
+
|
|
249
|
+
finally:
|
|
250
|
+
progress_bar.close()
|
|
251
|
+
|
|
252
|
+
# Combine segment solutions
|
|
253
|
+
logger.info('Combining segment solutions...')
|
|
254
|
+
self._finalize_solution(segment_flow_systems, horizon)
|
|
255
|
+
|
|
256
|
+
logger.info(f'Rolling horizon optimization completed: {n_segments} segments solved.')
|
|
257
|
+
|
|
258
|
+
return segment_flow_systems
|
|
259
|
+
|
|
260
|
+
def _calculate_segment_indices(self, total_timesteps: int, horizon: int, overlap: int) -> list[tuple[int, int]]:
|
|
261
|
+
"""Calculate start and end indices for each segment."""
|
|
262
|
+
segments = []
|
|
263
|
+
start = 0
|
|
264
|
+
while start < total_timesteps:
|
|
265
|
+
end = min(start + horizon + overlap, total_timesteps)
|
|
266
|
+
segments.append((start, end))
|
|
267
|
+
start += horizon # Move by horizon (not horizon + overlap)
|
|
268
|
+
if end == total_timesteps:
|
|
269
|
+
break
|
|
270
|
+
return segments
|
|
271
|
+
|
|
272
|
+
def _transfer_state(
|
|
273
|
+
self,
|
|
274
|
+
source_fs: FlowSystem,
|
|
275
|
+
target_fs: FlowSystem,
|
|
276
|
+
horizon: int,
|
|
277
|
+
nr_of_previous_values: int,
|
|
278
|
+
) -> None:
|
|
279
|
+
"""Transfer final state from source segment to target segment.
|
|
280
|
+
|
|
281
|
+
Transfers:
|
|
282
|
+
- Flow previous_flow_rate: Last nr_of_previous_values from non-overlap portion
|
|
283
|
+
- Storage initial_charge_state: Charge state at end of non-overlap portion
|
|
284
|
+
"""
|
|
285
|
+
from .components import Storage
|
|
286
|
+
|
|
287
|
+
solution = source_fs.solution
|
|
288
|
+
time_slice = slice(horizon - nr_of_previous_values, horizon)
|
|
289
|
+
|
|
290
|
+
# Transfer flow rates (for uptime/downtime tracking)
|
|
291
|
+
for label, target_flow in target_fs.flows.items():
|
|
292
|
+
var_name = f'{label}|flow_rate'
|
|
293
|
+
if var_name in solution:
|
|
294
|
+
values = solution[var_name].isel(time=time_slice).values
|
|
295
|
+
target_flow.previous_flow_rate = values.item() if values.size == 1 else values
|
|
296
|
+
|
|
297
|
+
# Transfer storage charge states
|
|
298
|
+
for label, target_comp in target_fs.components.items():
|
|
299
|
+
if isinstance(target_comp, Storage):
|
|
300
|
+
var_name = f'{label}|charge_state'
|
|
301
|
+
if var_name in solution:
|
|
302
|
+
target_comp.initial_charge_state = solution[var_name].isel(time=horizon).item()
|
|
303
|
+
|
|
304
|
+
def _check_no_investments(self, segment_fs: FlowSystem) -> None:
|
|
305
|
+
"""Check that no InvestParameters are used (not supported in rolling horizon)."""
|
|
306
|
+
from .features import InvestmentModel
|
|
307
|
+
|
|
308
|
+
invest_elements = []
|
|
309
|
+
for component in segment_fs.components.values():
|
|
310
|
+
for model in component.submodel.all_submodels:
|
|
311
|
+
if isinstance(model, InvestmentModel):
|
|
312
|
+
invest_elements.append(model.label_full)
|
|
313
|
+
|
|
314
|
+
if invest_elements:
|
|
315
|
+
raise ValueError(
|
|
316
|
+
f'InvestParameters are not supported in rolling horizon optimization. '
|
|
317
|
+
f'Found InvestmentModels: {invest_elements}. '
|
|
318
|
+
f'Use standard optimize() for problems with investments.'
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
def _finalize_solution(
|
|
322
|
+
self,
|
|
323
|
+
segment_flow_systems: list[FlowSystem],
|
|
324
|
+
horizon: int,
|
|
325
|
+
) -> None:
|
|
326
|
+
"""Combine segment solutions and compute derived values directly (no re-solve)."""
|
|
327
|
+
# Combine all solution variables from segments
|
|
328
|
+
combined_solution = self._combine_solutions(segment_flow_systems, horizon)
|
|
329
|
+
|
|
330
|
+
# Assign combined solution to the original FlowSystem
|
|
331
|
+
self._fs._solution = combined_solution
|
|
332
|
+
|
|
333
|
+
def _combine_solutions(
|
|
334
|
+
self,
|
|
335
|
+
segment_flow_systems: list[FlowSystem],
|
|
336
|
+
horizon: int,
|
|
337
|
+
) -> xr.Dataset:
|
|
338
|
+
"""Combine segment solutions into a single Dataset.
|
|
339
|
+
|
|
340
|
+
- Time-dependent variables: concatenated with overlap trimming
|
|
341
|
+
- Effect temporal/total: recomputed from per-timestep values
|
|
342
|
+
- Other scalars (including periodic): NaN (not meaningful for rolling horizon)
|
|
343
|
+
"""
|
|
344
|
+
if not segment_flow_systems:
|
|
345
|
+
raise ValueError('No segments to combine.')
|
|
346
|
+
|
|
347
|
+
effect_labels = set(self._fs.effects.keys())
|
|
348
|
+
combined_vars: dict[str, xr.DataArray] = {}
|
|
349
|
+
first_solution = segment_flow_systems[0].solution
|
|
350
|
+
first_variables = first_solution.variables
|
|
351
|
+
|
|
352
|
+
# Step 1: Time-dependent → concatenate; Scalars → NaN
|
|
353
|
+
for var_name in first_solution.data_vars:
|
|
354
|
+
if 'time' in first_variables[var_name].dims:
|
|
355
|
+
arrays = [
|
|
356
|
+
seg.solution[var_name].isel(
|
|
357
|
+
time=slice(None, horizon if i < len(segment_flow_systems) - 1 else None)
|
|
358
|
+
)
|
|
359
|
+
for i, seg in enumerate(segment_flow_systems)
|
|
360
|
+
]
|
|
361
|
+
combined_vars[var_name] = xr.concat(arrays, dim='time')
|
|
362
|
+
else:
|
|
363
|
+
combined_vars[var_name] = xr.DataArray(float('nan'))
|
|
364
|
+
|
|
365
|
+
# Step 2: Recompute effect totals from per-timestep values
|
|
366
|
+
for effect in effect_labels:
|
|
367
|
+
per_ts = f'{effect}(temporal)|per_timestep'
|
|
368
|
+
if per_ts in combined_vars:
|
|
369
|
+
temporal_sum = combined_vars[per_ts].sum(dim='time', skipna=True)
|
|
370
|
+
combined_vars[f'{effect}(temporal)'] = temporal_sum
|
|
371
|
+
combined_vars[effect] = temporal_sum # Total = temporal (periodic is NaN/unsupported)
|
|
372
|
+
|
|
373
|
+
return xr.Dataset(combined_vars)
|
flixopt/plot_result.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Plot result container for unified plotting API.
|
|
2
|
+
|
|
3
|
+
This module provides the PlotResult class that wraps plotting outputs
|
|
4
|
+
across the entire flixopt package, ensuring a consistent interface.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import TYPE_CHECKING, Any
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
import plotly.graph_objects as go
|
|
16
|
+
import xarray as xr
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class PlotResult:
|
|
21
|
+
"""Container returned by all plot methods. Holds both data and figure.
|
|
22
|
+
|
|
23
|
+
This class provides a unified interface for all plotting methods across
|
|
24
|
+
the flixopt package, enabling consistent method chaining and export options.
|
|
25
|
+
|
|
26
|
+
Attributes:
|
|
27
|
+
data: Prepared xarray Dataset used for the plot.
|
|
28
|
+
figure: Plotly figure object.
|
|
29
|
+
|
|
30
|
+
Examples:
|
|
31
|
+
Basic usage with chaining:
|
|
32
|
+
|
|
33
|
+
>>> result = flow_system.statistics.plot.balance('Bus')
|
|
34
|
+
>>> result.show().to_html('plot.html')
|
|
35
|
+
|
|
36
|
+
Accessing underlying data:
|
|
37
|
+
|
|
38
|
+
>>> result = flow_system.statistics.plot.flows()
|
|
39
|
+
>>> df = result.data.to_dataframe()
|
|
40
|
+
>>> result.to_csv('data.csv')
|
|
41
|
+
|
|
42
|
+
Customizing the figure:
|
|
43
|
+
|
|
44
|
+
>>> result = clustering.plot.compare()
|
|
45
|
+
>>> result.update(title='My Custom Title').show()
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
data: xr.Dataset
|
|
49
|
+
figure: go.Figure
|
|
50
|
+
|
|
51
|
+
def _repr_html_(self) -> str:
|
|
52
|
+
"""Return HTML representation for Jupyter notebook display."""
|
|
53
|
+
return self.figure.to_html(full_html=False, include_plotlyjs='cdn')
|
|
54
|
+
|
|
55
|
+
def show(self) -> PlotResult:
|
|
56
|
+
"""Display the figure. Returns self for chaining."""
|
|
57
|
+
self.figure.show()
|
|
58
|
+
return self
|
|
59
|
+
|
|
60
|
+
def update(self, **layout_kwargs: Any) -> PlotResult:
|
|
61
|
+
"""Update figure layout. Returns self for chaining.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
**layout_kwargs: Arguments passed to plotly's update_layout().
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Self for method chaining.
|
|
68
|
+
|
|
69
|
+
Examples:
|
|
70
|
+
>>> result.update(title='New Title', height=600)
|
|
71
|
+
"""
|
|
72
|
+
self.figure.update_layout(**layout_kwargs)
|
|
73
|
+
return self
|
|
74
|
+
|
|
75
|
+
def update_traces(self, **trace_kwargs: Any) -> PlotResult:
|
|
76
|
+
"""Update figure traces. Returns self for chaining.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
**trace_kwargs: Arguments passed to plotly's update_traces().
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Self for method chaining.
|
|
83
|
+
|
|
84
|
+
Examples:
|
|
85
|
+
>>> result.update_traces(line_width=2, marker_size=8)
|
|
86
|
+
"""
|
|
87
|
+
self.figure.update_traces(**trace_kwargs)
|
|
88
|
+
return self
|
|
89
|
+
|
|
90
|
+
def to_html(self, path: str | Path) -> PlotResult:
|
|
91
|
+
"""Save figure as interactive HTML. Returns self for chaining.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
path: File path for the HTML output.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Self for method chaining.
|
|
98
|
+
"""
|
|
99
|
+
self.figure.write_html(str(path))
|
|
100
|
+
return self
|
|
101
|
+
|
|
102
|
+
def to_image(self, path: str | Path, **kwargs: Any) -> PlotResult:
|
|
103
|
+
"""Save figure as static image. Returns self for chaining.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
path: File path for the image (format inferred from extension).
|
|
107
|
+
**kwargs: Additional arguments passed to write_image().
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Self for method chaining.
|
|
111
|
+
|
|
112
|
+
Examples:
|
|
113
|
+
>>> result.to_image('plot.png', scale=2)
|
|
114
|
+
>>> result.to_image('plot.svg')
|
|
115
|
+
"""
|
|
116
|
+
self.figure.write_image(str(path), **kwargs)
|
|
117
|
+
return self
|
|
118
|
+
|
|
119
|
+
def to_csv(self, path: str | Path, **kwargs: Any) -> PlotResult:
|
|
120
|
+
"""Export the underlying data to CSV. Returns self for chaining.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
path: File path for the CSV output.
|
|
124
|
+
**kwargs: Additional arguments passed to to_csv().
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Self for method chaining.
|
|
128
|
+
"""
|
|
129
|
+
self.data.to_dataframe().to_csv(path, **kwargs)
|
|
130
|
+
return self
|
|
131
|
+
|
|
132
|
+
def to_netcdf(self, path: str | Path, **kwargs: Any) -> PlotResult:
|
|
133
|
+
"""Export the underlying data to netCDF. Returns self for chaining.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
path: File path for the netCDF output.
|
|
137
|
+
**kwargs: Additional arguments passed to to_netcdf().
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
Self for method chaining.
|
|
141
|
+
"""
|
|
142
|
+
self.data.to_netcdf(path, **kwargs)
|
|
143
|
+
return self
|