diff --git a/CHANGELOG.md b/CHANGELOG.md index 8545f4081..e70cf08cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ Until here --> ### Added ### Changed +- Greatly improved docstrings and documentation of all public classes ### Deprecated diff --git a/flixopt/calculation.py b/flixopt/calculation.py index f22f09af6..4096bac6f 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -127,7 +127,10 @@ def summary(self): class FullCalculation(Calculation): """ - class for defined way of solving a flow_system optimization + FullCalculation solves the complete optimization problem using all time steps. + + This is the most comprehensive calculation type that considers every time step + in the optimization, providing the most accurate but computationally intensive solution. """ def do_modeling(self) -> SystemModel: @@ -186,7 +189,25 @@ def _activate_time_series(self): class AggregatedCalculation(FullCalculation): """ - class for defined way of solving a flow_system optimization + AggregatedCalculation reduces computational complexity by clustering time series into typical periods. + + This calculation approach aggregates time series data using clustering techniques (tsam) to identify + representative time periods, significantly reducing computation time while maintaining solution accuracy. + + Note: + The quality of the solution depends on the choice of aggregation parameters. + The optimal parameters depend on the specific problem and the characteristics of the time series data. + For more information, refer to the [tsam documentation](https://tsam.readthedocs.io/en/latest/). + + Args: + name: Name of the calculation + flow_system: FlowSystem to be optimized + aggregation_parameters: Parameters for aggregation. See AggregationParameters class documentation + components_to_clusterize: List of Components to perform aggregation on. If None, all components are aggregated. + This equalizes variables in the components according to the typical periods computed in the aggregation + active_timesteps: DatetimeIndex of timesteps to use for calculation. If None, all timesteps are used + folder: Folder where results should be saved. If None, current working directory is used + aggregation: contains the aggregation model """ def __init__( @@ -198,21 +219,6 @@ def __init__( active_timesteps: Optional[pd.DatetimeIndex] = None, folder: Optional[pathlib.Path] = None, ): - """ - Class for Optimizing the `FlowSystem` including: - 1. Aggregating TimeSeriesData via typical periods using tsam. - 2. Equalizing variables of typical periods. - Args: - name: name of calculation - flow_system: flow_system which should be calculated - aggregation_parameters: Parameters for aggregation. See documentation of AggregationParameters class. - components_to_clusterize: List of Components to perform aggregation on. If None, then all components are aggregated. - This means, teh variables in the components are equalized to each other, according to the typical periods - computed in the DataAggregation - active_timesteps: pd.DatetimeIndex or None - list with indices, which should be used for calculation. If None, then all timesteps are used. - folder: folder where results should be saved. If None, then the current working directory is used. - """ super().__init__(name, flow_system, active_timesteps, folder=folder) self.aggregation_parameters = aggregation_parameters self.components_to_clusterize = components_to_clusterize @@ -289,6 +295,114 @@ def _perform_aggregation(self): class SegmentedCalculation(Calculation): + """Solve large optimization problems by dividing time horizon into (overlapping) segments. + + This class addresses memory and computational limitations of large-scale optimization + problems by decomposing the time horizon into smaller overlapping segments that are + solved sequentially. Each segment uses final values from the previous segment as + initial conditions, ensuring dynamic continuity across the solution. + + Key Concepts: + **Temporal Decomposition**: Divides long time horizons into manageable segments + **Overlapping Windows**: Segments share timesteps to improve storage dynamics + **Value Transfer**: Final states of one segment become initial states of the next + **Sequential Solving**: Each segment solved independently but with coupling + + Limitations and Constraints: + **Investment Parameters**: InvestParameters are not supported in segmented calculations + as investment decisions must be made for the entire time horizon, not per segment. + + **Global Constraints**: Time-horizon-wide constraints (flow_hours_total_min/max, + load_factor_min/max) may produce suboptimal results as they cannot be enforced + globally across segments. + + **Storage Dynamics**: While overlap helps, storage optimization may be suboptimal + compared to full-horizon solutions due to limited foresight in each segment. + + Args: + name: Unique identifier for the calculation, used in result files and logging. + flow_system: The FlowSystem to optimize, containing all components, flows, and buses. + timesteps_per_segment: Number of timesteps in each segment (excluding overlap). + Must be > 2 to avoid internal side effects. Larger values provide better + optimization at the cost of memory and computation time. + overlap_timesteps: Number of additional timesteps added to each segment. + Improves storage optimization by providing lookahead. Higher values + improve solution quality but increase computational cost. + nr_of_previous_values: Number of previous timestep values to transfer between + segments for initialization. Typically 1 is sufficient. + folder: Directory for saving results. Defaults to current working directory + 'results'. + + Examples: + Annual optimization with monthly segments: + + ```python + # 8760 hours annual data with monthly segments (730 hours) and 48-hour overlap + segmented_calc = SegmentedCalculation( + name='annual_energy_system', + flow_system=energy_system, + timesteps_per_segment=730, # ~1 month + overlap_timesteps=48, # 2 days overlap + folder=Path('results/segmented'), + ) + segmented_calc.do_modeling_and_solve(solver='gurobi') + ``` + + Weekly optimization with daily overlap: + + ```python + # Weekly segments for detailed operational planning + weekly_calc = SegmentedCalculation( + name='weekly_operations', + flow_system=industrial_system, + timesteps_per_segment=168, # 1 week (hourly data) + overlap_timesteps=24, # 1 day overlap + nr_of_previous_values=1, + ) + ``` + + Large-scale system with minimal overlap: + + ```python + # Large system with minimal overlap for computational efficiency + large_calc = SegmentedCalculation( + name='large_scale_grid', + flow_system=grid_system, + timesteps_per_segment=100, # Shorter segments + overlap_timesteps=5, # Minimal overlap + ) + ``` + + Design Considerations: + **Segment Size**: Balance between solution quality and computational efficiency. + Larger segments provide better optimization but require more memory and time. + + **Overlap Duration**: More overlap improves storage dynamics and reduces + end-effects but increases computational cost. Typically 5-10% of segment length. + + **Storage Systems**: Systems with large storage components benefit from longer + overlaps to capture charge/discharge cycles effectively. + + **Investment Decisions**: Use FullCalculation for problems requiring investment + optimization, as SegmentedCalculation cannot handle investment parameters. + + Common Use Cases: + - **Annual Planning**: Long-term planning with seasonal variations + - **Large Networks**: Spatially or temporally large energy systems + - **Memory-Limited Systems**: When full optimization exceeds available memory + - **Operational Planning**: Detailed short-term optimization with limited foresight + - **Sensitivity Analysis**: Quick approximate solutions for parameter studies + + Performance Tips: + - Start with FullCalculation and use this class if memory issues occur + - Use longer overlaps for systems with significant storage + - Monitor solution quality at segment boundaries for discontinuities + + Warning: + The evaluation of the solution is a bit more complex than FullCalculation or AggregatedCalculation + due to the overlapping individual solutions. + + """ + def __init__( self, name: str, @@ -298,25 +412,6 @@ def __init__( nr_of_previous_values: int = 1, folder: Optional[pathlib.Path] = None, ): - """ - Dividing and Modeling the problem in (overlapping) segments. - The final values of each Segment are recognized by the following segment, effectively coupling - charge_states and flow_rates between segments. - Because of this intersection, both modeling and solving is done in one step - - Take care: - Parameters like InvestParameters, sum_of_flow_hours and other restrictions over the total time_series - don't really work in this Calculation. Lower bounds to such SUMS can lead to weird results. - This is NOT yet explicitly checked for... - - Args: - name: name of calculation - flow_system: flow_system which should be calculated - timesteps_per_segment: The number of time_steps per individual segment (without the overlap) - overlap_timesteps: The number of time_steps that are added to each individual model. Used for better - results of storages) - folder: folder where results should be saved. If None, then the current working directory is used. - """ super().__init__(name, flow_system, folder=folder) self.timesteps_per_segment = timesteps_per_segment self.overlap_timesteps = overlap_timesteps diff --git a/flixopt/components.py b/flixopt/components.py index a84a89625..1c5cba3bf 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -25,7 +25,130 @@ @register_class_for_io class LinearConverter(Component): """ - Converts input-Flows into output-Flows via linear conversion factors + Converts input-Flows into output-Flows via linear conversion factors. + + LinearConverter models equipment that transforms one or more input flows into one or + more output flows through linear relationships. This includes heat exchangers, + electrical converters, chemical reactors, and other equipment where the + relationship between inputs and outputs can be expressed as linear equations. + + The component supports two modeling approaches: simple conversion factors for + straightforward linear relationships, or piecewise conversion for complex non-linear + behavior approximated through piecewise linear segments. + + Args: + label: The label of the Element. Used to identify it in the FlowSystem. + inputs: List of input Flows that feed into the converter. + outputs: List of output Flows that are produced by the converter. + on_off_parameters: Information about on and off state of LinearConverter. + Component is On/Off if all connected Flows are On/Off. This induces an + On-Variable (binary) in all Flows! If possible, use OnOffParameters in a + single Flow instead to keep the number of binary variables low. + conversion_factors: Linear relationships between flows expressed as a list of + dictionaries. Each dictionary maps flow labels to their coefficients in one + linear equation. The number of conversion factors must be less than the total + number of flows to ensure degrees of freedom > 0. Either 'conversion_factors' + OR 'piecewise_conversion' can be used, but not both. + For examples also look into the linear_converters.py file. + piecewise_conversion: Define piecewise linear relationships between flow rates + of different flows. Enables modeling of non-linear conversion behavior through + linear approximation. Either 'conversion_factors' or 'piecewise_conversion' + can be used, but not both. + meta_data: Used to store additional information about the Element. Not used + internally, but saved in results. Only use Python native types. + + Examples: + Simple 1:1 heat exchanger with 95% efficiency: + + ```python + heat_exchanger = LinearConverter( + label='primary_hx', + inputs=[hot_water_in], + outputs=[hot_water_out], + conversion_factors=[{'hot_water_in': 0.95, 'hot_water_out': 1}], + ) + ``` + + Multi-input heat pump with COP=3: + + ```python + heat_pump = LinearConverter( + label='air_source_hp', + inputs=[electricity_in], + outputs=[heat_output], + conversion_factors=[{'electricity_in': 3, 'heat_output': 1}], + ) + ``` + + Combined heat and power (CHP) unit with multiple outputs: + + ```python + chp_unit = LinearConverter( + label='gas_chp', + inputs=[natural_gas], + outputs=[electricity_out, heat_out], + conversion_factors=[ + {'natural_gas': 0.35, 'electricity_out': 1}, + {'natural_gas': 0.45, 'heat_out': 1}, + ], + ) + ``` + + Electrolyzer with multiple conversion relationships: + + ```python + electrolyzer = LinearConverter( + label='pem_electrolyzer', + inputs=[electricity_in, water_in], + outputs=[hydrogen_out, oxygen_out], + conversion_factors=[ + {'electricity_in': 1, 'hydrogen_out': 50}, # 50 kWh/kg H2 + {'water_in': 1, 'hydrogen_out': 9}, # 9 kg H2O/kg H2 + {'hydrogen_out': 8, 'oxygen_out': 1}, # Mass balance + ], + ) + ``` + + Complex converter with piecewise efficiency: + + ```python + variable_efficiency_converter = LinearConverter( + label='variable_converter', + inputs=[fuel_in], + outputs=[power_out], + piecewise_conversion=PiecewiseConversion( + { + 'fuel_in': Piecewise( + [ + Piece(0, 10), # Low load operation + Piece(10, 25), # High load operation + ] + ), + 'power_out': Piecewise( + [ + Piece(0, 3.5), # Lower efficiency at part load + Piece(3.5, 10), # Higher efficiency at full load + ] + ), + } + ), + ) + ``` + + Note: + Conversion factors define linear relationships where the sum of (coefficient × flow_rate) + equals zero for each equation: factor1×flow1 + factor2×flow2 + ... = 0 + Conversion factors define linear relationships. + `{flow1: a1, flow2: a2, ...}` leads to `a1×flow_rate1 + a2×flow_rate2 + ... = 0` + Unfortunately the current input format doest read intuitively: + {"electricity": 1, "H2": 50} means that the electricity_in flow rate is multiplied by 1 + and the hydrogen_out flow rate is multiplied by 50. THis leads to 50 electricity --> 1 H2. + + The system must have fewer conversion factors than total flows (degrees of freedom > 0) + to avoid over-constraining the problem. For n total flows, use at most n-1 conversion factors. + + When using piecewise_conversion, the converter operates on one piece at a time, + with binary variables determining which piece is active. """ @@ -39,21 +162,6 @@ def __init__( piecewise_conversion: Optional[PiecewiseConversion] = None, meta_data: Optional[Dict] = None, ): - """ - Args: - label: The label of the Element. Used to identify it in the FlowSystem - inputs: The input Flows - outputs: The output Flows - on_off_parameters: Information about on and off state of LinearConverter. - Component is On/Off, if all connected Flows are On/Off. This induces an On-Variable (binary) in all Flows! - If possible, use OnOffParameters in a single Flow instead to keep the number of binary variables low. - See class OnOffParameters. - conversion_factors: linear relation between flows. - Either 'conversion_factors' or 'piecewise_conversion' can be used! - piecewise_conversion: Define a piecewise linear relation between flow rates of different flows. - Either 'conversion_factors' or 'piecewise_conversion' can be used! - meta_data: used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types. - """ super().__init__(label, inputs, outputs, on_off_parameters, meta_data=meta_data) self.conversion_factors = conversion_factors or [] self.piecewise_conversion = piecewise_conversion @@ -120,7 +228,137 @@ def degrees_of_freedom(self): @register_class_for_io class Storage(Component): """ - Used to model the storage of energy or material. + A Storage models the temporary storage and release of energy or material. + + Storages have one incoming and one outgoing Flow, each with configurable efficiency + factors. They maintain a charge state variable that represents the stored amount, + bounded by capacity limits and evolving over time based on charging, discharging, + and self-discharge losses. + + The storage model handles complex temporal dynamics including initial conditions, + final state constraints, and time-varying parameters. It supports both fixed-size + and investment-optimized storage systems with comprehensive techno-economic modeling. + + Args: + label: The label of the Element. Used to identify it in the FlowSystem. + charging: Incoming flow for loading the storage. Represents energy or material + flowing into the storage system. + discharging: Outgoing flow for unloading the storage. Represents energy or + material flowing out of the storage system. + capacity_in_flow_hours: Nominal capacity/size of the storage in flow-hours + (e.g., kWh for electrical storage, m³ or kg for material storage). Can be a scalar + for fixed capacity or InvestParameters for optimization. + relative_minimum_charge_state: Minimum relative charge state (0-1 range). + Prevents deep discharge that could damage equipment. Default is 0. + relative_maximum_charge_state: Maximum relative charge state (0-1 range). + Accounts for practical capacity limits, safety margins or temperature impacts. Default is 1. + initial_charge_state: Storage charge state at the beginning of the time horizon. + Can be numeric value or 'lastValueOfSim', which is recommended for if the initial start state is not known. + Default is 0. + minimal_final_charge_state: Minimum absolute charge state required at the end + of the time horizon. Useful for ensuring energy security or meeting contracts. + maximal_final_charge_state: Maximum absolute charge state allowed at the end + of the time horizon. Useful for preventing overcharge or managing inventory. + eta_charge: Charging efficiency factor (0-1 range). Accounts for conversion + losses during charging. Default is 1 (perfect efficiency). + eta_discharge: Discharging efficiency factor (0-1 range). Accounts for + conversion losses during discharging. Default is 1 (perfect efficiency). + relative_loss_per_hour: Self-discharge rate per hour (typically 0-0.1 range). + Represents standby losses, leakage, or degradation. Default is 0. + prevent_simultaneous_charge_and_discharge: If True, prevents charging and + discharging simultaneously. Increases binary variables but improves model + realism and solution interpretation. Default is True. + meta_data: Used to store additional information about the Element. Not used + internally, but saved in results. Only use Python native types. + + Examples: + Battery energy storage system: + + ```python + battery = Storage( + label='lithium_battery', + charging=battery_charge_flow, + discharging=battery_discharge_flow, + capacity_in_flow_hours=100, # 100 kWh capacity + eta_charge=0.95, # 95% charging efficiency + eta_discharge=0.95, # 95% discharging efficiency + relative_loss_per_hour=0.001, # 0.1% loss per hour + relative_minimum_charge_state=0.1, # Never below 10% SOC + relative_maximum_charge_state=0.9, # Never above 90% SOC + ) + ``` + + Thermal storage with cycling constraints: + + ```python + thermal_storage = Storage( + label='hot_water_tank', + charging=heat_input, + discharging=heat_output, + capacity_in_flow_hours=500, # 500 kWh thermal capacity + initial_charge_state=250, # Start half full + # Impact of temperature on energy capacity + relative_maximum_charge_state=water_temperature_spread / rated_temeprature_spread, + eta_charge=0.90, # Heat exchanger losses + eta_discharge=0.85, # Distribution losses + relative_loss_per_hour=0.02, # 2% thermal loss per hour + prevent_simultaneous_charge_and_discharge=True, + ) + ``` + + Pumped hydro storage with investment optimization: + + ```python + pumped_hydro = Storage( + label='pumped_hydro', + charging=pump_flow, + discharging=turbine_flow, + capacity_in_flow_hours=InvestParameters( + minimum_size=1000, # Minimum economic scale + maximum_size=10000, # Site constraints + specific_effects={'cost': 150}, # €150/MWh capacity + fix_effects={'cost': 50_000_000}, # €50M fixed costs + ), + eta_charge=0.85, # Pumping efficiency + eta_discharge=0.90, # Turbine efficiency + initial_charge_state='lastValueOfSim', # Ensuring no deficit compared to start + relative_loss_per_hour=0.0001, # Minimal evaporation + ) + ``` + + Material storage with inventory management: + + ```python + fuel_storage = Storage( + label='natural_gas_storage', + charging=gas_injection, + discharging=gas_withdrawal, + capacity_in_flow_hours=10000, # 10,000 m³ storage volume + initial_charge_state=3000, # Start with 3,000 m³ + minimal_final_charge_state=1000, # Strategic reserve + maximal_final_charge_state=9000, # Prevent overflow + eta_charge=0.98, # Compression losses + eta_discharge=0.95, # Pressure reduction losses + relative_loss_per_hour=0.0005, # 0.05% leakage per hour + prevent_simultaneous_charge_and_discharge=False, # Allow flow-through + ) + ``` + + Note: + Charge state evolution follows the equation: + charge[t+1] = charge[t] × (1-loss_rate)^hours_per_step + + charge_flow[t] × eta_charge × hours_per_step - + discharge_flow[t] × hours_per_step / eta_discharge + + All efficiency parameters (eta_charge, eta_discharge) are dimensionless (0-1 range). + The relative_loss_per_hour parameter represents exponential decay per hour. + + When prevent_simultaneous_charge_and_discharge is True, binary variables are + created to enforce mutual exclusivity, which increases solution time but + prevents unrealistic simultaneous charging and discharging. + + Initial and final charge state constraints use absolute values (not relative), + matching the capacity_in_flow_hours units. """ def __init__( @@ -140,31 +378,6 @@ def __init__( prevent_simultaneous_charge_and_discharge: bool = True, meta_data: Optional[Dict] = None, ): - """ - Storages have one incoming and one outgoing Flow each with an efficiency. - Further, storages have a `size` and a `charge_state`. - Similarly to the flow-rate of a Flow, the `size` combined with a relative upper and lower bound - limits the `charge_state` of the storage. - - For mathematical details take a look at our online documentation - - Args: - label: The label of the Element. Used to identify it in the FlowSystem - charging: ingoing flow. - discharging: outgoing flow. - capacity_in_flow_hours: nominal capacity/size of the storage - relative_minimum_charge_state: minimum relative charge state. The default is 0. - relative_maximum_charge_state: maximum relative charge state. The default is 1. - initial_charge_state: storage charge_state at the beginning. The default is 0. - minimal_final_charge_state: minimal value of chargeState at the end of timeseries. - maximal_final_charge_state: maximal value of chargeState at the end of timeseries. - eta_charge: efficiency factor of charging/loading. The default is 1. - eta_discharge: efficiency factor of uncharging/unloading. The default is 1. - relative_loss_per_hour: loss per chargeState-Unit per hour. The default is 0. - prevent_simultaneous_charge_and_discharge: If True, loading and unloading at the same time is not possible. - Increases the number of binary variables, but is recommended for easier evaluation. The default is True. - meta_data: used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types. - """ # TODO: fixed_relative_chargeState implementieren super().__init__( label, @@ -252,11 +465,113 @@ def _plausibility_checks(self) -> None: @register_class_for_io class Transmission(Component): - # TODO: automatic on-Value in Flows if loss_abs - # TODO: loss_abs must be: investment_size * loss_abs_rel!!! - # TODO: investmentsize only on 1 flow - # TODO: automatic investArgs for both in-flows (or alternatively both out-flows!) - # TODO: optional: capacities should be recognised for losses + """ + Models transmission infrastructure that transports flows between two locations with losses. + + Transmission components represent physical infrastructure like pipes, cables, + transmission lines, or conveyor systems that transport energy or materials between + two points. They can model both unidirectional and bidirectional flow with + configurable loss mechanisms and operational constraints. + + The component supports complex transmission scenarios including relative losses + (proportional to flow), absolute losses (fixed when active), and bidirectional + operation with flow direction constraints. + + Args: + label: The label of the Element. Used to identify it in the FlowSystem. + in1: The primary inflow (side A). Pass InvestParameters here for capacity optimization. + out1: The primary outflow (side B). + in2: Optional secondary inflow (side B) for bidirectional operation. + If in1 has InvestParameters, in2 will automatically have matching capacity. + out2: Optional secondary outflow (side A) for bidirectional operation. + relative_losses: Proportional losses as fraction of throughput (e.g., 0.02 for 2% loss). + Applied as: output = input × (1 - relative_losses) + absolute_losses: Fixed losses that occur when transmission is active. + Automatically creates binary variables for on/off states. + on_off_parameters: Parameters defining binary operation constraints and costs. + prevent_simultaneous_flows_in_both_directions: If True, prevents simultaneous + flow in both directions. Increases binary variables but reflects physical + reality for most transmission systems. Default is True. + meta_data: Used to store additional information. Not used internally but saved + in results. Only use Python native types. + + Examples: + Simple electrical transmission line: + + ```python + power_line = Transmission( + label='110kv_line', + in1=substation_a_out, + out1=substation_b_in, + relative_losses=0.03, # 3% line losses + ) + ``` + + Bidirectional natural gas pipeline: + + ```python + gas_pipeline = Transmission( + label='interstate_pipeline', + in1=compressor_station_a, + out1=distribution_hub_b, + in2=compressor_station_b, + out2=distribution_hub_a, + relative_losses=0.005, # 0.5% friction losses + absolute_losses=50, # 50 kW compressor power when active + prevent_simultaneous_flows_in_both_directions=True, + ) + ``` + + District heating network with investment optimization: + + ```python + heating_network = Transmission( + label='dh_main_line', + in1=Flow( + label='heat_supply', + bus=central_plant_bus, + size=InvestParameters( + minimum_size=1000, # Minimum 1 MW capacity + maximum_size=10000, # Maximum 10 MW capacity + specific_effects={'cost': 200}, # €200/kW capacity + fix_effects={'cost': 500000}, # €500k fixed installation + ), + ), + out1=district_heat_demand, + relative_losses=0.15, # 15% thermal losses in distribution + ) + ``` + + Material conveyor with on/off operation: + + ```python + conveyor_belt = Transmission( + label='material_transport', + in1=loading_station, + out1=unloading_station, + absolute_losses=25, # 25 kW motor power when running + on_off_parameters=OnOffParameters( + effects_per_switch_on={'maintenance': 0.1}, + consecutive_on_hours_min=2, # Minimum 2-hour operation + switch_on_total_max=10, # Maximum 10 starts per day + ), + ) + ``` + + Note: + The transmission equation balances flows with losses: + output_flow = input_flow × (1 - relative_losses) - absolute_losses + + For bidirectional transmission, each direction has independent loss calculations. + + When using InvestParameters on in1, the capacity automatically applies to in2 + to maintain consistent bidirectional capacity without additional investment variables. + + Absolute losses force the creation of binary on/off variables, which increases + computational complexity but enables realistic modeling of equipment with + standby power consumption. + + """ def __init__( self, @@ -271,23 +586,6 @@ def __init__( prevent_simultaneous_flows_in_both_directions: bool = True, meta_data: Optional[Dict] = None, ): - """ - Initializes a Transmission component (Pipe, cable, ...) that models the flows between two sides - with potential losses. - - Args: - label: The label of the Element. Used to identify it in the FlowSystem - in1: The inflow at side A. Pass InvestmentParameters here. - out1: The outflow at side B. - in2: The optional inflow at side B. - If in1 got InvestParameters, the size of this Flow will be equal to in1 (with no extra effects!) - out2: The optional outflow at side A. - relative_losses: The relative loss between inflow and outflow, e.g., 0.02 for 2% loss. - absolute_losses: The absolute loss, occur only when the Flow is on. Induces the creation of the ON-Variable - on_off_parameters: Parameters defining the on/off behavior of the component. - prevent_simultaneous_flows_in_both_directions: If True, inflow and outflow are not allowed to be both non-zero at same timestep. - meta_data: used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types. - """ super().__init__( label, inputs=[flow for flow in (in1, in2) if flow is not None], @@ -581,7 +879,87 @@ def relative_charge_state_bounds(self) -> Tuple[NumericData, NumericData]: @register_class_for_io class SourceAndSink(Component): """ - class for source (output-flow) and sink (input-flow) in one commponent + A SourceAndSink combines both supply and demand capabilities in a single component. + + SourceAndSink components can both consume AND provide energy or material flows + from and to the system, making them ideal for modeling markets, (simple) storage facilities, + or bidirectional grid connections where buying and selling occur at the same location. + + Args: + label: The label of the Element. Used to identify it in the FlowSystem. + inputs: Input-flows into the SourceAndSink representing consumption/demand side. + outputs: Output-flows from the SourceAndSink representing supply/generation side. + prevent_simultaneous_flow_rates: If True, prevents simultaneous input and output + flows. This enforces that the component operates either as a source OR sink + at any given time, but not both simultaneously. Default is True. + meta_data: Used to store additional information about the Element. Not used + internally but saved in results. Only use Python native types. + + Examples: + Electricity market connection (buy/sell to grid): + + ```python + electricity_market = SourceAndSink( + label='grid_connection', + inputs=[electricity_purchase], # Buy from grid + outputs=[electricity_sale], # Sell to grid + prevent_simultaneous_flow_rates=True, # Can't buy and sell simultaneously + ) + ``` + + Natural gas storage facility: + + ```python + gas_storage_facility = SourceAndSink( + label='underground_gas_storage', + inputs=[gas_injection_flow], # Inject gas into storage + outputs=[gas_withdrawal_flow], # Withdraw gas from storage + prevent_simultaneous_flow_rates=True, # Injection or withdrawal, not both + ) + ``` + + District heating network connection: + + ```python + dh_connection = SourceAndSink( + label='district_heating_tie', + inputs=[heat_purchase_flow], # Purchase heat from network + outputs=[heat_sale_flow], # Sell excess heat to network + prevent_simultaneous_flow_rates=False, # May allow simultaneous flows + ) + ``` + + Industrial waste heat exchange: + + ```python + waste_heat_exchange = SourceAndSink( + label='industrial_heat_hub', + inputs=[ + waste_heat_input_a, # Receive waste heat from process A + waste_heat_input_b, # Receive waste heat from process B + ], + outputs=[ + useful_heat_supply_c, # Supply heat to process C + useful_heat_supply_d, # Supply heat to process D + ], + prevent_simultaneous_flow_rates=False, # Multiple simultaneous flows allowed + ) + ``` + + Note: + When prevent_simultaneous_flow_rates is True, binary variables are created to + ensure mutually exclusive operation between input and output flows, which + increases computational complexity but reflects realistic market or storage + operation constraints. + + SourceAndSink is particularly useful for modeling: + - Energy markets with bidirectional trading + - Storage facilities with injection/withdrawal operations + - Grid tie points with import/export capabilities + - Waste exchange networks with multiple participants + + Deprecated: + The deprecated `sink` and `source` kwargs are accepted for compatibility but will be removed in future releases. """ def __init__( @@ -593,14 +971,6 @@ def __init__( meta_data: Optional[Dict] = None, **kwargs, ): - """ - Args: - label: The label of the Element. Used to identify it in the FlowSystem - outputs: output-flows of this component - inputs: input-flows of this component - prevent_simultaneous_flow_rates: If True, inflow and outflow can not be active simultaniously. - meta_data: used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types. - """ source = kwargs.pop('source', None) sink = kwargs.pop('sink', None) prevent_simultaneous_sink_and_source = kwargs.pop('prevent_simultaneous_sink_and_source', None) @@ -668,6 +1038,80 @@ def prevent_simultaneous_sink_and_source(self) -> bool: @register_class_for_io class Source(Component): + """ + A Source generates or provides energy or material flows into the system. + + Sources represent supply points like power plants, fuel suppliers, renewable + energy sources, or any system boundary where flows originate. They provide + unlimited supply capability subject to flow constraints, demand patterns and effects. + + Args: + label: The label of the Element. Used to identify it in the FlowSystem. + outputs: Output-flows from the source. Can be single flow or list of flows + for sources providing multiple commodities or services. + meta_data: Used to store additional information about the Element. Not used + internally but saved in results. Only use Python native types. + prevent_simultaneous_flow_rates: If True, only one output flow can be active + at a time. Useful for modeling mutually exclusive supply options. Default is False. + + Examples: + Simple electricity grid connection: + + ```python + grid_source = Source(label='electrical_grid', outputs=[grid_electricity_flow]) + ``` + + Natural gas supply with cost and capacity constraints: + + ```python + gas_supply = Source( + label='gas_network', + outputs=[ + Flow( + label='natural_gas_flow', + bus=gas_bus, + size=1000, # Maximum 1000 kW supply capacity + effects_per_flow_hour={'cost': 0.04}, # €0.04/kWh gas cost + ) + ], + ) + ``` + + Multi-fuel power plant with switching constraints: + + ```python + multi_fuel_plant = Source( + label='flexible_generator', + outputs=[coal_electricity, gas_electricity, biomass_electricity], + prevent_simultaneous_flow_rates=True, # Can only use one fuel at a time + ) + ``` + + Renewable energy source with investment optimization: + + ```python + solar_farm = Source( + label='solar_pv', + outputs=[ + Flow( + label='solar_power', + bus=electricity_bus, + size=InvestParameters( + minimum_size=0, + maximum_size=50000, # Up to 50 MW + specific_effects={'cost': 800}, # €800/kW installed + fix_effects={'cost': 100000}, # €100k development costs + ), + fixed_relative_profile=solar_profile, # Hourly generation profile + ) + ], + ) + ``` + + Deprecated: + The deprecated `source` kwarg is accepted for compatibility but will be removed in future releases. + """ + def __init__( self, label: str, @@ -676,12 +1120,6 @@ def __init__( prevent_simultaneous_flow_rates: bool = False, **kwargs, ): - """ - Args: - label: The label of the Element. Used to identify it in the FlowSystem - outputs: output-flows of source - meta_data: used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types. - """ source = kwargs.pop('source', None) if source is not None: warnings.warn( @@ -713,6 +1151,81 @@ def source(self) -> Flow: @register_class_for_io class Sink(Component): + """ + A Sink consumes energy or material flows from the system. + + Sinks represent demand points like electrical loads, heat demands, material + consumption, or any system boundary where flows terminate. They provide + unlimited consumption capability subject to flow constraints, demand patterns and effects. + + Args: + label: The label of the Element. Used to identify it in the FlowSystem. + inputs: Input-flows into the sink. Can be single flow or list of flows + for sinks consuming multiple commodities or services. + meta_data: Used to store additional information about the Element. Not used + internally but saved in results. Only use Python native types. + prevent_simultaneous_flow_rates: If True, only one input flow can be active + at a time. Useful for modeling mutually exclusive consumption options. Default is False. + + Examples: + Simple electrical demand: + + ```python + electrical_load = Sink(label='building_load', inputs=[electricity_demand_flow]) + ``` + + Heat demand with time-varying profile: + + ```python + heat_demand = Sink( + label='district_heating_load', + inputs=[ + Flow( + label='heat_consumption', + bus=heat_bus, + fixed_relative_profile=hourly_heat_profile, # Demand profile + size=2000, # Peak demand of 2000 kW + ) + ], + ) + ``` + + Multi-energy building with switching capabilities: + + ```python + flexible_building = Sink( + label='smart_building', + inputs=[electricity_heating, gas_heating, heat_pump_heating], + prevent_simultaneous_flow_rates=True, # Can only use one heating mode + ) + ``` + + Industrial process with variable demand: + + ```python + factory_load = Sink( + label='manufacturing_plant', + inputs=[ + Flow( + label='electricity_process', + bus=electricity_bus, + size=5000, # Base electrical load + effects_per_flow_hour={'cost': -0.1}, # Value of service (negative cost) + ), + Flow( + label='steam_process', + bus=steam_bus, + size=3000, # Process steam demand + fixed_relative_profile=production_schedule, + ), + ], + ) + ``` + + Deprecated: + The deprecated `sink` kwarg is accepted for compatibility but will be removed in future releases. + """ + def __init__( self, label: str, @@ -722,10 +1235,18 @@ def __init__( **kwargs, ): """ - Args: - label: The label of the Element. Used to identify it in the FlowSystem - inputs: output-flows of source - meta_data: used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types. + Initialize a Sink (consumes flow from the system). + + Supports legacy `sink=` keyword for backward compatibility (deprecated): if `sink` is provided it is used as the single input flow and a DeprecationWarning is issued; specifying both `inputs` and `sink` raises ValueError. + + Parameters: + label (str): Unique element label. + inputs (List[Flow], optional): Input flows for the sink. + meta_data (dict, optional): Arbitrary metadata attached to the element. + prevent_simultaneous_flow_rates (bool, optional): If True, prevents simultaneous nonzero flow rates across the element's inputs by wiring that restriction into the base Component setup. + + Note: + The deprecated `sink` kwarg is accepted for compatibility but will be removed in future releases. """ sink = kwargs.pop('sink', None) if sink is not None: diff --git a/flixopt/core.py b/flixopt/core.py index 002fbab48..f110b438f 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -101,28 +101,32 @@ def as_dataarray(data: NumericData, timesteps: pd.DatetimeIndex) -> xr.DataArray class TimeSeriesData: + """ + TimeSeriesData wraps time series data with aggregation metadata for optimization. + + This class combines time series data with special characteristics needed for aggregated calculations. + It allows grouping related time series to prevent overweighting in optimization models. + + Example: + When you have multiple solar time series, they should share aggregation weight: + ```python + solar1 = TimeSeriesData(sol_array_1, agg_group='solar') + solar2 = TimeSeriesData(sol_array_2, agg_group='solar') + solar3 = TimeSeriesData(sol_array_3, agg_group='solar') + # These 3 series share one weight (each gets weight = 1/3 instead of 1) + ``` + + Args: + data: The timeseries data, which can be a scalar, array, or numpy array. + agg_group: The group this TimeSeriesData belongs to. agg_weight is split between group members. Default is None. + agg_weight: The weight for calculation_type 'aggregated', should be between 0 and 1. Default is None. + + Raises: + ValueError: If both agg_group and agg_weight are set. + """ + # TODO: Move to Interface.py def __init__(self, data: NumericData, agg_group: Optional[str] = None, agg_weight: Optional[float] = None): - """ - timeseries class for transmit timeseries AND special characteristics of timeseries, - i.g. to define weights needed in calculation_type 'aggregated' - EXAMPLE solar: - you have several solar timeseries. These should not be overweighted - compared to the remaining timeseries (i.g. heat load, price)! - fixed_relative_profile_solar1 = TimeSeriesData(sol_array_1, type = 'solar') - fixed_relative_profile_solar2 = TimeSeriesData(sol_array_2, type = 'solar') - fixed_relative_profile_solar3 = TimeSeriesData(sol_array_3, type = 'solar') - --> this 3 series of same type share one weight, i.e. internally assigned each weight = 1/3 - (instead of standard weight = 1) - - Args: - data: The timeseries data, which can be a scalar, array, or numpy array. - agg_group: The group this TimeSeriesData is a part of. agg_weight is split between members of a group. Default is None. - agg_weight: The weight for calculation_type 'aggregated', should be between 0 and 1. Default is None. - - Raises: - Exception: If both agg_group and agg_weight are set, an exception is raised. - """ self.data = data self.agg_group = agg_group self.agg_weight = agg_weight diff --git a/flixopt/effects.py b/flixopt/effects.py index de704c0c5..57b038dde 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -26,8 +26,107 @@ @register_class_for_io class Effect(Element): """ - Effect, i.g. costs, CO2 emissions, area, ... - Components, FLows, and so on can contribute to an Effect. One Effect is chosen as the Objective of the Optimization + Represents system-wide impacts like costs, emissions, resource consumption, or other effects. + + Effects capture the broader impacts of system operation and investment decisions beyond + the primary energy/material flows. Each Effect accumulates contributions from Components, + Flows, and other system elements. One Effect is typically chosen as the optimization + objective, while others can serve as constraints or tracking metrics. + + Effects support comprehensive modeling including operational and investment contributions, + cross-effect relationships (e.g., carbon pricing), and flexible constraint formulation. + + Args: + label: The label of the Element. Used to identify it in the FlowSystem. + unit: The unit of the effect (e.g., '€', 'kg_CO2', 'kWh_primary', 'm²'). + This is informative only and does not affect optimization calculations. + description: Descriptive name explaining what this effect represents. + is_standard: If True, this is a standard effect allowing direct value input + without effect dictionaries. Used for simplified effect specification (and less boilerplate code). + is_objective: If True, this effect serves as the optimization objective function. + Only one effect can be marked as objective per optimization. + specific_share_to_other_effects_operation: Operational cross-effect contributions. + Maps this effect's operational values to contributions to other effects + specific_share_to_other_effects_invest: Investment cross-effect contributions. + Maps this effect's investment values to contributions to other effects. + minimum_operation: Minimum allowed total operational contribution across all timesteps. + maximum_operation: Maximum allowed total operational contribution across all timesteps. + minimum_operation_per_hour: Minimum allowed operational contribution per timestep. + maximum_operation_per_hour: Maximum allowed operational contribution per timestep. + minimum_invest: Minimum allowed total investment contribution. + maximum_invest: Maximum allowed total investment contribution. + minimum_total: Minimum allowed total effect (operation + investment combined). + maximum_total: Maximum allowed total effect (operation + investment combined). + meta_data: Used to store additional information. Not used internally but saved + in results. Only use Python native types. + + Examples: + Basic cost objective: + + ```python + cost_effect = Effect(label='system_costs', unit='€', description='Total system costs', is_objective=True) + ``` + + CO2 emissions with carbon pricing: + + ```python + co2_effect = Effect( + label='co2_emissions', + unit='kg_CO2', + description='Carbon dioxide emissions', + specific_share_to_other_effects_operation={'costs': 50}, # €50/t_CO2 + maximum_total=1_000_000, # 1000 t CO2 annual limit + ) + ``` + + Land use constraint: + + ```python + land_use = Effect( + label='land_usage', + unit='m²', + description='Land area requirement', + maximum_total=50_000, # Maximum 5 hectares available + ) + ``` + + Primary energy tracking: + + ```python + primary_energy = Effect( + label='primary_energy', + unit='kWh_primary', + description='Primary energy consumption', + specific_share_to_other_effects_operation={'costs': 0.08}, # €0.08/kWh + ) + ``` + + Water consumption with tiered constraints: + + ```python + water_usage = Effect( + label='water_consumption', + unit='m³', + description='Industrial water usage', + minimum_operation_per_hour=10, # Minimum 10 m³/h for process stability + maximum_operation_per_hour=500, # Maximum 500 m³/h capacity limit + maximum_total=100_000, # Annual permit limit: 100,000 m³ + ) + ``` + + Note: + Effect bounds can be None to indicate no constraint in that direction. + + Cross-effect relationships enable sophisticated modeling like carbon pricing, + resource valuation, or multi-criteria optimization with weighted objectives. + + The unit field is purely informational - ensure dimensional consistency + across all contributions to each effect manually. + + Effects are accumulated as: + - Total = Σ(operational contributions) + Σ(investment contributions) + - Cross-effects add to target effects based on specific_share ratios + """ def __init__( @@ -49,27 +148,6 @@ def __init__( minimum_total: Optional[Scalar] = None, maximum_total: Optional[Scalar] = None, ): - """ - Args: - label: The label of the Element. Used to identify it in the FlowSystem - unit: The unit of effect, i.g. €, kg_CO2, kWh_primaryEnergy - description: The long name - is_standard: true, if Standard-Effect (for direct input of value without effect (alternatively to dict)) , else false - is_objective: true, if optimization target - specific_share_to_other_effects_operation: {effectType: TS, ...}, i.g. 180 €/t_CO2, input as {costs: 180}, optional - share to other effects (only operation) - specific_share_to_other_effects_invest: {effectType: TS, ...}, i.g. 180 €/t_CO2, input as {costs: 180}, optional - share to other effects (only invest). - minimum_operation: minimal sum (only operation) of the effect. - maximum_operation: maximal sum (nur operation) of the effect. - minimum_operation_per_hour: max. value per hour (only operation) of effect (=sum of all effect-shares) for each timestep! - maximum_operation_per_hour: min. value per hour (only operation) of effect (=sum of all effect-shares) for each timestep! - minimum_invest: minimal sum (only invest) of the effect - maximum_invest: maximal sum (only invest) of the effect - minimum_total: min sum of effect (invest+operation). - maximum_total: max sum of effect (invest+operation). - meta_data: used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types. - """ super().__init__(label, meta_data=meta_data) self.label = label self.unit = unit diff --git a/flixopt/elements.py b/flixopt/elements.py index e9a3ef65c..81ccdaa2a 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -25,11 +25,48 @@ @register_class_for_io class Component(Element): """ - A Component contains incoming and outgoing [`Flows`][flixopt.elements.Flow]. It defines how these Flows interact with each other. - The On or Off state of the Component is defined by all its Flows. Its on, if any of its FLows is On. - It's mathematically advisable to define the On/Off state in a FLow rather than a Component if possible, - as this introduces less binary variables to the Model - Constraints to the On/Off state are defined by the [`on_off_parameters`][flixopt.interface.OnOffParameters]. + Base class for all system components that transform, convert, or process flows. + + Components are the active elements in energy systems that define how input and output + Flows interact with each other. They represent equipment, processes, or logical + operations that transform energy or materials between different states, carriers, + or locations. + + Components serve as connection points between Buses through their associated Flows, + enabling the modeling of complex energy system topologies and operational constraints. + + Args: + label: The label of the Element. Used to identify it in the FlowSystem. + inputs: List of input Flows feeding into the component. These represent + energy/material consumption by the component. + outputs: List of output Flows leaving the component. These represent + energy/material production by the component. + on_off_parameters: Defines binary operation constraints and costs when the + component has discrete on/off states. Creates binary variables for all + connected Flows. For better performance, prefer defining OnOffParameters + on individual Flows when possible. + prevent_simultaneous_flows: List of Flows that cannot be active simultaneously. + Creates binary variables to enforce mutual exclusivity. Use sparingly as + it increases computational complexity. + meta_data: Used to store additional information. Not used internally but saved + in results. Only use Python native types. + + Note: + Component operational state is determined by its connected Flows: + - Component is "on" if ANY of its Flows is active (flow_rate > 0) + - Component is "off" only when ALL Flows are inactive (flow_rate = 0) + + Binary variables and constraints: + - on_off_parameters creates binary variables for ALL connected Flows + - prevent_simultaneous_flows creates binary variables for specified Flows + - For better computational performance, prefer Flow-level OnOffParameters + + Component is an abstract base class. In practice, use specialized subclasses: + - LinearConverter: Linear input/output relationships + - Storage: Temporal energy/material storage + - Transmission: Transport between locations + - Source/Sink: System boundaries + """ def __init__( @@ -41,19 +78,6 @@ def __init__( prevent_simultaneous_flows: Optional[List['Flow']] = None, meta_data: Optional[Dict] = None, ): - """ - Args: - label: The label of the Element. Used to identify it in the FlowSystem - inputs: input flows. - outputs: output flows. - on_off_parameters: Information about on and off state of Component. - Component is On/Off, if all connected Flows are On/Off. This induces an On-Variable (binary) in all Flows! - If possible, use OnOffParameters in a single Flow instead to keep the number of binary variables low. - See class OnOffParameters. - prevent_simultaneous_flows: Define a Group of Flows. Only one them can be on at a time. - Induces On-Variable in all Flows! If possible, use OnOffParameters in a single Flow instead. - meta_data: used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types. - """ super().__init__(label, meta_data=meta_data) self.inputs: List['Flow'] = inputs or [] self.outputs: List['Flow'] = outputs or [] @@ -92,20 +116,64 @@ def _plausibility_checks(self) -> None: @register_class_for_io class Bus(Element): """ - A Bus represents a nodal balance between the flow rates of its incoming and outgoing Flows. + Buses represent nodal balances between flow rates, serving as connection points. + + A Bus enforces energy or material balance constraints where the sum of all incoming + flows must equal the sum of all outgoing flows at each time step. Buses represent + physical or logical connection points for energy carriers (electricity, heat, gas) + or material flows between different Components. + + Args: + label: The label of the Element. Used to identify it in the FlowSystem. + excess_penalty_per_flow_hour: Penalty costs for bus balance violations. + When None, no excess/deficit is allowed (hard constraint). When set to a + value > 0, allows bus imbalances at penalty cost. Default is 1e5 (high penalty). + meta_data: Used to store additional information. Not used internally but saved + in results. Only use Python native types. + + Examples: + Electrical bus with strict balance: + + ```python + electricity_bus = Bus( + label='main_electrical_bus', + excess_penalty_per_flow_hour=None, # No imbalance allowed + ) + ``` + + Heat network with penalty for imbalances: + + ```python + heat_network = Bus( + label='district_heating_network', + excess_penalty_per_flow_hour=1000, # €1000/MWh penalty for imbalance + ) + ``` + + Material flow with time-varying penalties: + + ```python + material_hub = Bus( + label='material_processing_hub', + excess_penalty_per_flow_hour=waste_disposal_costs, # Time series + ) + ``` + + Note: + The bus balance equation enforced is: Σ(inflows) = Σ(outflows) + excess - deficit + + When excess_penalty_per_flow_hour is None, excess and deficit are forced to zero. + When a penalty cost is specified, the optimization can choose to violate the + balance if economically beneficial, paying the penalty. + The penalty is added to the objective directly. + + Empty `inputs` and `outputs` lists are initialized and populated automatically + by the FlowSystem during system setup. """ def __init__( self, label: str, excess_penalty_per_flow_hour: Optional[NumericDataTS] = 1e5, meta_data: Optional[Dict] = None ): - """ - Args: - label: The label of the Element. Used to identify it in the FlowSystem - excess_penalty_per_flow_hour: excess costs / penalty costs (bus balance compensation) - (none/ 0 -> no penalty). The default is 1e5. - (Take care: if you use a timeseries (no scalar), timeseries is aggregated if calculation_type = aggregated!) - meta_data: used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types. - """ super().__init__(label, meta_data=meta_data) self.excess_penalty_per_flow_hour = excess_penalty_per_flow_hour self.inputs: List[Flow] = [] @@ -145,9 +213,146 @@ def __init__(self): @register_class_for_io class Flow(Element): - r""" - A **Flow** moves energy (or material) between a [Bus][flixopt.elements.Bus] and a [Component][flixopt.elements.Component] in a predefined direction. - The flow-rate is the main optimization variable of the **Flow**. + """Define a directed flow of energy or material between bus and component. + + A Flow represents the transfer of energy (electricity, heat, fuel) or material + between a Bus and a Component in a specific direction. The flow rate is the + primary optimization variable, with constraints and costs defined through + various parameters. Flows can have fixed or variable sizes, operational + constraints, and complex on/off behavior. + + Key Concepts: + **Flow Rate**: The instantaneous rate of energy/material transfer (optimization variable) [kW, m³/h, kg/h] + **Flow Hours**: Amount of energy/material transferred per timestep. [kWh, m³, kg] + **Flow Size**: The maximum capacity or nominal rating of the flow [kW, m³/h, kg/h] + **Relative Bounds**: Flow rate limits expressed as fractions of flow size + + Integration with Parameter Classes: + - **InvestParameters**: Used for `size` when flow Size is an investment decision + - **OnOffParameters**: Used for `on_off_parameters` when flow has discrete states + + Args: + label: Unique identifier for the flow within its component. + The full label combines component and flow labels. + bus: Label of the bus this flow connects to. Must match a bus in the FlowSystem. + size: Flow capacity or nominal rating. Can be: + - Scalar value for fixed capacity + - InvestParameters for investment-based sizing decisions + - None to use large default value (CONFIG.modeling.BIG) + relative_minimum: Minimum flow rate as fraction of size. + Example: 0.2 means flow cannot go below 20% of rated capacity. + relative_maximum: Maximum flow rate as fraction of size (typically 1.0). + Values >1.0 allow temporary overload operation. + load_factor_min: Minimum average utilization over the time horizon (0-1). + Calculated as total flow hours divided by (size × total time). + load_factor_max: Maximum average utilization over the time horizon (0-1). + Useful for equipment duty cycle limits or maintenance scheduling. + effects_per_flow_hour: Operational costs and impacts per unit of flow-time. + Dictionary mapping effect names to unit costs (e.g., fuel costs, emissions). + on_off_parameters: Binary operation constraints using OnOffParameters. + Enables modeling of startup costs, minimum run times, cycling limits. + Only relevant when relative_minimum > 0 or discrete operation is required. + flow_hours_total_max: Maximum cumulative flow-hours over time horizon. + Alternative to load_factor_max for absolute energy/material limits. + flow_hours_total_min: Minimum cumulative flow-hours over time horizon. + Alternative to load_factor_min for contractual or operational requirements. + fixed_relative_profile: Predetermined flow pattern as fraction of size. + When specified, flow rate becomes: size × fixed_relative_profile(t). + Used for: demand profiles, renewable generation, fixed schedules. + previous_flow_rate: Initial flow state for startup/shutdown dynamics. + Used with on_off_parameters to determine initial on/off status. + If None, assumes flow was off in previous time period. + meta_data: Additional information stored with results but not used in optimization. + Must contain only Python native types (dict, list, str, int, float, bool). + + Examples: + Basic power flow with fixed capacity: + + ```python + generator_output = Flow( + label='electricity_out', + bus='electricity_grid', + size=100, # 100 MW capacity + relative_minimum=0.4, # Cannot operate below 40 MW + effects_per_flow_hour={'fuel_cost': 45, 'co2_emissions': 0.8}, + ) + ``` + + Investment decision for battery capacity: + + ```python + battery_flow = Flow( + label='electricity_storage', + bus='electricity_grid', + size=InvestParameters( + minimum_size=10, # Minimum 10 MWh + maximum_size=100, # Maximum 100 MWh + specific_effects={'cost': 150_000}, # €150k/MWh annualized + ), + ) + ``` + + Heat pump with startup costs and minimum run times: + + ```python + heat_pump = Flow( + label='heat_output', + bus='heating_network', + size=50, # 50 kW thermal + relative_minimum=0.3, # Minimum 15 kW output when on + effects_per_flow_hour={'electricity_cost': 25, 'maintenance': 2}, + on_off_parameters=OnOffParameters( + effects_per_switch_on={'startup_cost': 100, 'wear': 0.1}, + consecutive_on_hours_min=2, # Must run at least 2 hours + consecutive_off_hours_min=1, # Must stay off at least 1 hour + switch_on_total_max=200, # Maximum 200 starts per year + ), + ) + ``` + + Fixed renewable generation profile: + + ```python + solar_generation = Flow( + label='solar_power', + bus='electricity_grid', + size=25, # 25 MW installed capacity + fixed_relative_profile=np.array([0, 0.1, 0.4, 0.8, 0.9, 0.7, 0.3, 0.1, 0]), + effects_per_flow_hour={'maintenance_costs': 5}, # €5/MWh maintenance + ) + ``` + + Industrial process with annual utilization limits: + + ```python + production_line = Flow( + label='product_output', + bus='product_market', + size=1000, # 1000 units/hour capacity + load_factor_min=0.6, # Must achieve 60% annual utilization + load_factor_max=0.85, # Cannot exceed 85% for maintenance + effects_per_flow_hour={'variable_cost': 12, 'quality_control': 0.5}, + ) + ``` + + Design Considerations: + **Size vs Load Factors**: Use `flow_hours_total_min/max` for absolute limits, + `load_factor_min/max` for utilization-based constraints. + + **Relative Bounds**: Set `relative_minimum > 0` only when equipment cannot + operate below that level. Use `on_off_parameters` for discrete on/off behavior. + + **Fixed Profiles**: Use `fixed_relative_profile` for known exact patterns, + `relative_maximum` for upper bounds on optimization variables. + + Notes: + - Default size (CONFIG.modeling.BIG) is used when size=None + - List inputs for previous_flow_rate are converted to NumPy arrays + - Flow direction is determined by component input/output designation + + Deprecated: + Passing Bus objects to `bus` parameter. Use bus label strings instead. + """ def __init__( @@ -167,36 +372,6 @@ def __init__( previous_flow_rate: Optional[NumericData] = None, meta_data: Optional[Dict] = None, ): - r""" - Args: - label: The label of the FLow. Used to identify it in the FlowSystem. Its `full_label` consists of the label of the Component and the label of the Flow. - bus: blabel of the bus the flow is connected to. - size: size of the flow. If InvestmentParameters is used, size is optimized. - If size is None, a default value is used. - relative_minimum: min value is relative_minimum multiplied by size - relative_maximum: max value is relative_maximum multiplied by size. If size = max then relative_maximum=1 - load_factor_min: minimal load factor general: avg Flow per nominalVal/investSize - (e.g. boiler, kW/kWh=h; solarthermal: kW/m²; - def: :math:`load\_factor:= sumFlowHours/ (nominal\_val \cdot \Delta t_{tot})` - load_factor_max: maximal load factor (see minimal load factor) - effects_per_flow_hour: operational costs, costs per flow-"work" - on_off_parameters: If present, flow can be "off", i.e. be zero (only relevant if relative_minimum > 0) - Therefore a binary var "on" is used. Further, several other restrictions and effects can be modeled - through this On/Off State (See OnOffParameters) - flow_hours_total_max: maximum flow-hours ("flow-work") - (if size is not const, maybe load_factor_max is the better choice!) - flow_hours_total_min: minimum flow-hours ("flow-work") - (if size is not predefined, maybe load_factor_min is the better choice!) - fixed_relative_profile: fixed relative values for flow (if given). - flow_rate(t) := fixed_relative_profile(t) * size(t) - With this value, the flow_rate is no optimization-variable anymore. - (relative_minimum and relative_maximum are ignored) - used for fixed load or supply profiles, i.g. heat demand, wind-power, solarthermal - If the load-profile is just an upper limit, use relative_maximum instead. - previous_flow_rate: previous flow rate of the flow. Used to determine if and how long the - flow is already on / off. If None, the flow is considered to be off for one timestep. - meta_data: used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types. - """ super().__init__(label, meta_data=meta_data) self.size = size or CONFIG.modeling.BIG # Default size self.relative_minimum = relative_minimum diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index d911b8e63..e8227307a 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -29,7 +29,21 @@ class FlowSystem: """ - A FlowSystem organizes the high level Elements (Components & Effects). + A FlowSystem organizes the high level Elements (Components, Buses & Effects). + + This is the main container class that users work with to build and manage their System. + + Args: + timesteps: The timesteps of the model. + hours_of_last_timestep: The duration of the last time step. Uses the last time interval if not specified + hours_of_previous_timesteps: The duration of previous timesteps. + If None, the first time increment of time_series is used. + This is needed to calculate previous durations (for example consecutive_on_hours). + If you use an array, take care that its long enough to cover all previous values! + + Notes: + - Creates an empty registry for components and buses, an empty EffectCollection, and a placeholder for a SystemModel. + - The instance starts disconnected (self._connected == False) and will be connected automatically when trying to solve a calculation. """ def __init__( @@ -39,13 +53,17 @@ def __init__( hours_of_previous_timesteps: Optional[Union[int, float, np.ndarray]] = None, ): """ - Args: - timesteps: The timesteps of the model. - hours_of_last_timestep: The duration of the last time step. Uses the last time interval if not specified - hours_of_previous_timesteps: The duration of previous timesteps. - If None, the first time increment of time_series is used. - This is needed to calculate previous durations (for example consecutive_on_hours). - If you use an array, take care that its long enough to cover all previous values! + Initialize a FlowSystem that manages components, buses, effects, and their time-series. + + Parameters: + timesteps: DatetimeIndex defining the primary timesteps for the system's TimeSeriesCollection. + hours_of_last_timestep: Duration (in hours) of the final timestep; if None, inferred from timesteps or defaults in TimeSeriesCollection. + hours_of_previous_timesteps: Scalar or array-like durations (in hours) for the preceding timesteps; used to configure non-uniform timestep lengths. + + Notes: + Creates an empty registry for components and buses, an empty EffectCollection, and a placeholder for a SystemModel. + The instance starts disconnected (self._connected == False) and with no active network visualization app. + This can also be triggered manually with `_connect_network()`. """ self.time_series_collection = TimeSeriesCollection( timesteps=timesteps, diff --git a/flixopt/interface.py b/flixopt/interface.py index c38d6c619..771313ccc 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -20,14 +20,51 @@ @register_class_for_io class Piece(Interface): - def __init__(self, start: NumericData, end: NumericData): - """ - Define a Piece, which is part of a Piecewise object. + """Define a single linear segment with specified domain boundaries. - Args: - start: The x-values of the piece. - end: The end of the piece. - """ + This class represents one linear segment that will be combined with other + pieces to form complete piecewise linear functions. Each piece defines + a domain interval [start, end] where a linear relationship applies. + + Args: + start: Lower bound of the domain interval for this linear segment. + Can be scalar values or time series arrays for time-varying boundaries. + end: Upper bound of the domain interval for this linear segment. + Can be scalar values or time series arrays for time-varying boundaries. + + Examples: + Basic piece for equipment efficiency curve: + + ```python + # Single segment from 40% to 80% load + efficiency_segment = Piece(start=40, end=80) + ``` + + Piece with time-varying boundaries: + + ```python + # Capacity limits that change seasonally + seasonal_piece = Piece( + start=np.array([10, 20, 30, 25]), # Minimum capacity by season + end=np.array([80, 100, 90, 70]), # Maximum capacity by season + ) + ``` + + Fixed operating point (start equals end): + + ```python + # Equipment that operates at exactly 50 MW + fixed_output = Piece(start=50, end=50) + ``` + + Note: + Individual pieces are building blocks that gain meaning when combined + into Piecewise functions. See the Piecewise class for information about + how pieces interact and relate to each other. + + """ + + def __init__(self, start: NumericData, end: NumericData): self.start = start self.end = end @@ -38,16 +75,127 @@ def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): @register_class_for_io class Piecewise(Interface): - def __init__(self, pieces: List[Piece]): - """ - Define a Piecewise, consisting of a list of Pieces. + """Define a piecewise linear function by combining multiple `Piece`s together. - Args: - pieces: The pieces of the piecewise. - """ + This class creates complex non-linear relationships by combining multiple + Piece objects into a single piecewise linear function. + + Args: + pieces: List of Piece objects defining the linear segments. The arrangement + and relationships between pieces determine the function behavior: + - Touching pieces (end of one = start of next) ensure continuity + - Gaps between pieces create forbidden regions + - Overlapping pieces provide an extra choice for the optimizer + + Piece Relationship Patterns: + **Touching Pieces (Continuous Function)**: + Pieces that share boundary points create smooth, continuous functions + without gaps or overlaps. + + **Gaps Between Pieces (Forbidden Regions)**: + Non-contiguous pieces with gaps represent forbidden regions. + For example minimum load requirements or safety zones. + + **Overlapping Pieces (Flexible Operation)**: + Pieces with overlapping domains provide optimization flexibility, + allowing the solver to choose which segment to operate in. + + Examples: + Continuous efficiency curve (touching pieces): + + ```python + efficiency_curve = Piecewise( + [ + Piece(start=0, end=25), # Low load: 0-25 MW + Piece(start=25, end=75), # Medium load: 25-75 MW (touches at 25) + Piece(start=75, end=100), # High load: 75-100 MW (touches at 75) + ] + ) + ``` + + Equipment with forbidden operating range (gap): + + ```python + turbine_operation = Piecewise( + [ + Piece(start=0, end=0), # Off state (point operation) + Piece(start=40, end=100), # Operating range (gap: 0-40 forbidden) + ] + ) + ``` + + Flexible operation with overlapping options: + + ```python + flexible_operation = Piecewise( + [ + Piece(start=20, end=60), # Standard efficiency mode + Piece(start=50, end=90), # High efficiency mode (overlap: 50-60) + ] + ) + ``` + + Tiered pricing structure: + + ```python + electricity_pricing = Piecewise( + [ + Piece(start=0, end=100), # Tier 1: 0-100 kWh + Piece(start=100, end=500), # Tier 2: 100-500 kWh + Piece(start=500, end=1000), # Tier 3: 500-1000 kWh + ] + ) + ``` + + Seasonal capacity variation: + + ```python + seasonal_capacity = Piecewise( + [ + Piece(start=[10, 15, 20, 12], end=[80, 90, 85, 75]), # Varies by time + ] + ) + ``` + + Container Operations: + The Piecewise class supports standard Python container operations: + + ```python + piecewise = Piecewise([piece1, piece2, piece3]) + + len(piecewise) # Returns number of pieces (3) + piecewise[0] # Access first piece + for piece in piecewise: # Iterate over all pieces + print(piece.start, piece.end) + ``` + + Validation Considerations: + - Pieces are typically ordered by their start values + - Check for unintended gaps that might create infeasible regions + - Consider whether overlaps provide desired flexibility or create ambiguity + - Ensure time-varying pieces have consistent dimensions + + Common Use Cases: + - Power plants: Heat rate curves, efficiency vs load, emissions profiles + - HVAC systems: COP vs temperature, capacity vs conditions + - Industrial processes: Conversion rates vs throughput, quality vs speed + - Financial modeling: Tiered rates, progressive taxes, bulk discounts + - Transportation: Fuel efficiency curves, capacity vs speed + - Storage systems: Efficiency vs state of charge, power vs energy + - Renewable energy: Output vs weather conditions, curtailment strategies + + """ + + def __init__(self, pieces: List[Piece]): self.pieces = pieces def __len__(self): + """ + Return the number of Piece segments in this Piecewise container. + + Returns: + int: Count of contained Piece objects. + """ return len(self.pieces) def __getitem__(self, index) -> Piece: @@ -63,18 +211,207 @@ def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): @register_class_for_io class PiecewiseConversion(Interface): - def __init__(self, piecewises: Dict[str, Piecewise]): - """ - Define a piecewise conversion between multiple Flows. - --> "gaps" can be expressed by a piece not starting at the end of the prior piece: [(1,3), (4,5)] - --> "points" can expressed as piece with same begin and end: [(3,3), (4,4)] + """Define coordinated piecewise linear relationships between multiple flows. - Args: - piecewises: Dict of Piecewises defining the conversion factors. flow labels as keys, piecewise as values - """ + This class models conversion processes where multiple flows (inputs, outputs, + auxiliaries) have synchronized piecewise relationships. All flows change + together based on the same operating point, enabling accurate modeling of + complex equipment with variable performance characteristics. + + Multi-Flow Coordination: + All piecewise functions must have matching piece structures (same number + of pieces with compatible domains) to ensure synchronized operation. + When the equipment operates at a given point, ALL flows scale proportionally + within their respective pieces. + + Args: + piecewises: Dictionary mapping flow labels to their Piecewise functions. + Keys are flow identifiers (e.g., 'electricity_in', 'heat_out', 'fuel_consumed'). + Values are Piecewise objects that define each flow's behavior. + **Critical Requirement**: All Piecewise objects must have the same + number of pieces with compatible domains to ensure consistent operation. + + Operating Point Coordination: + When equipment operates at any point within a piece, all flows scale + proportionally within their corresponding pieces. This ensures realistic + equipment behavior where efficiency, consumption, and production rates + all change together. + + Examples: + Heat pump with coordinated efficiency changes: + + ```python + heat_pump_pc = PiecewiseConversion( + { + 'electricity_in': Piecewise( + [ + Piece(0, 10), # Low load: 0-10 kW electricity + Piece(10, 25), # High load: 10-25 kW electricity + ] + ), + 'heat_out': Piecewise( + [ + Piece(0, 35), # Low load COP=3.5: 0-35 kW heat + Piece(35, 75), # High load COP=3.0: 35-75 kW heat + ] + ), + 'cooling_water': Piecewise( + [ + Piece(0, 2.5), # Low load: 0-2.5 m³/h cooling + Piece(2.5, 6), # High load: 2.5-6 m³/h cooling + ] + ), + } + ) + # At 15 kW electricity → 52.5 kW heat + 3.75 m³/h cooling water + ``` + + Combined cycle power plant with synchronized flows: + + ```python + power_plant_pc = PiecewiseConversion( + { + 'natural_gas': Piecewise( + [ + Piece(150, 300), # Part load: 150-300 MW_th fuel + Piece(300, 500), # Full load: 300-500 MW_th fuel + ] + ), + 'electricity': Piecewise( + [ + Piece(60, 135), # Part load: 60-135 MW_e (45% efficiency) + Piece(135, 250), # Full load: 135-250 MW_e (50% efficiency) + ] + ), + 'steam_export': Piecewise( + [ + Piece(20, 35), # Part load: 20-35 MW_th steam + Piece(35, 50), # Full load: 35-50 MW_th steam + ] + ), + 'co2_emissions': Piecewise( + [ + Piece(30, 60), # Part load: 30-60 t/h CO2 + Piece(60, 100), # Full load: 60-100 t/h CO2 + ] + ), + } + ) + ``` + + Chemical reactor with multiple products and waste: + + ```python + reactor_pc = PiecewiseConversion( + { + 'feedstock': Piecewise( + [ + Piece(10, 50), # Small batch: 10-50 kg/h + Piece(50, 200), # Large batch: 50-200 kg/h + ] + ), + 'product_A': Piecewise( + [ + Piece(7, 35), # Small batch: 70% yield + Piece(35, 140), # Large batch: 70% yield + ] + ), + 'product_B': Piecewise( + [ + Piece(2, 10), # Small batch: 20% yield + Piece(10, 45), # Large batch: 22.5% yield (improved) + ] + ), + 'waste_stream': Piecewise( + [ + Piece(1, 5), # Small batch: 10% waste + Piece(5, 15), # Large batch: 7.5% waste (efficiency) + ] + ), + } + ) + ``` + + Equipment with discrete operating modes: + + ```python + compressor_pc = PiecewiseConversion( + { + 'electricity': Piecewise( + [ + Piece(0, 0), # Off mode: no consumption + Piece(45, 45), # Low mode: fixed 45 kW + Piece(85, 85), # High mode: fixed 85 kW + ] + ), + 'compressed_air': Piecewise( + [ + Piece(0, 0), # Off mode: no production + Piece(250, 250), # Low mode: 250 Nm³/h + Piece(500, 500), # High mode: 500 Nm³/h + ] + ), + } + ) + ``` + + Equipment with forbidden operating range: + + ```python + steam_turbine_pc = PiecewiseConversion( + { + 'steam_in': Piecewise( + [ + Piece(0, 100), # Low pressure operation + Piece(200, 500), # High pressure (gap: 100-200 forbidden) + ] + ), + 'electricity_out': Piecewise( + [ + Piece(0, 30), # Low pressure: poor efficiency + Piece(80, 220), # High pressure: good efficiency + ] + ), + 'condensate_out': Piecewise( + [ + Piece(0, 100), # Low pressure condensate + Piece(200, 500), # High pressure condensate + ] + ), + } + ) + ``` + + Design Patterns: + **Forbidden Ranges**: Use gaps between pieces to model equipment that cannot + operate in certain ranges (e.g., minimum loads, unstable regions). + + **Discrete Modes**: Use pieces with identical start/end values to model + equipment with fixed operating points (e.g., on/off, discrete speeds). + + **Efficiency Changes**: Coordinate input and output pieces to reflect + changing conversion efficiency across operating ranges. + + Common Use Cases: + - Power generation: Multi-fuel plants, cogeneration systems, renewable hybrids + - HVAC systems: Heat pumps, chillers with variable COP and auxiliary loads + - Industrial processes: Multi-product reactors, separation units, heat exchangers + - Transportation: Multi-modal systems, hybrid vehicles, charging infrastructure + - Water treatment: Multi-stage processes with varying energy and chemical needs + - Energy storage: Systems with efficiency changes and auxiliary power requirements + + """ + + def __init__(self, piecewises: Dict[str, Piecewise]): self.piecewises = piecewises def items(self): + """ + Return an iterator over (flow_label, Piecewise) pairs stored in this PiecewiseConversion. + + This is a thin convenience wrapper around the internal mapping and yields the same view + as dict.items(), where each key is a flow label (str) and each value is a Piecewise. + """ return self.piecewises.items() def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): @@ -84,14 +421,195 @@ def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): @register_class_for_io class PiecewiseEffects(Interface): - def __init__(self, piecewise_origin: Piecewise, piecewise_shares: Dict[str, Piecewise]): - """ - Define piecewise effects related to a variable. + """Define how a single decision variable contributes to system effects with piecewise rates. - Args: - piecewise_origin: Piecewise of the related variable - piecewise_shares: Piecewise defining the shares to different Effects - """ + This class models situations where a decision variable (the origin) generates + different types of system effects (costs, emissions, resource consumption) at + rates that change non-linearly with the variable's operating level. Unlike + PiecewiseConversion which coordinates multiple flows, PiecewiseEffects focuses + on how one variable impacts multiple system-wide effects. + + Key Concept - Origin vs. Effects: + - **Origin**: The primary decision variable (e.g., production level, capacity, size) + - **Shares**: The amounts which this variable contributes to different system effects + + Relationship to PiecewiseConversion: + **PiecewiseConversion**: Models synchronized relationships between multiple + flow variables (e.g., fuel_in, electricity_out, emissions_out all coordinated). + + **PiecewiseEffects**: Models how one variable contributes to system-wide + effects at variable rates (e.g., production_level → costs, emissions, resources). + + Args: + piecewise_origin: Piecewise function defining the behavior of the primary + decision variable. This establishes the operating domain and ranges. + piecewise_shares: Dictionary mapping effect names to their rate functions. + Keys are effect identifiers (e.g., 'cost_per_unit', 'CO2_intensity'). + Values are Piecewise objects defining the contribution rate per unit + of the origin variable at different operating levels. + + Mathematical Relationship: + For each effect: Total_Effect = Origin_Variable × Share_Rate(Origin_Level) + + This enables modeling of: + - Economies of scale (decreasing unit costs with volume) + - Learning curves (improving efficiency with experience) + - Threshold effects (changing rates at different scales) + - Progressive pricing (increasing rates with consumption) + + Examples: + Manufacturing with economies of scale: + + ```python + production_effects = PiecewiseEffects( + piecewise_origin=Piecewise( + [ + Piece(0, 1000), # Small scale: 0-1000 units/month + Piece(1000, 5000), # Medium scale: 1000-5000 units/month + Piece(5000, 10000), # Large scale: 5000-10000 units/month + ] + ), + piecewise_shares={ + 'unit_cost': Piecewise( + [ + Piece(50, 45), # €50-45/unit (scale benefits) + Piece(45, 35), # €45-35/unit (bulk materials) + Piece(35, 30), # €35-30/unit (automation benefits) + ] + ), + 'labor_hours': Piecewise( + [ + Piece(2.5, 2.0), # 2.5-2.0 hours/unit (learning curve) + Piece(2.0, 1.5), # 2.0-1.5 hours/unit (efficiency gains) + Piece(1.5, 1.2), # 1.5-1.2 hours/unit (specialization) + ] + ), + 'CO2_intensity': Piecewise( + [ + Piece(15, 12), # 15-12 kg CO2/unit (process optimization) + Piece(12, 9), # 12-9 kg CO2/unit (equipment efficiency) + Piece(9, 7), # 9-7 kg CO2/unit (renewable energy) + ] + ), + }, + ) + ``` + + Power generation with load-dependent characteristics: + + ```python + generator_effects = PiecewiseEffects( + piecewise_origin=Piecewise( + [ + Piece(50, 200), # Part load operation: 50-200 MW + Piece(200, 350), # Rated operation: 200-350 MW + Piece(350, 400), # Overload operation: 350-400 MW + ] + ), + piecewise_shares={ + 'fuel_rate': Piecewise( + [ + Piece(12.0, 10.5), # Heat rate: 12.0-10.5 GJ/MWh (part load penalty) + Piece(10.5, 9.8), # Heat rate: 10.5-9.8 GJ/MWh (optimal efficiency) + Piece(9.8, 11.2), # Heat rate: 9.8-11.2 GJ/MWh (overload penalty) + ] + ), + 'maintenance_factor': Piecewise( + [ + Piece(0.8, 1.0), # Low stress operation + Piece(1.0, 1.0), # Design operation + Piece(1.0, 1.5), # High stress operation + ] + ), + 'NOx_rate': Piecewise( + [ + Piece(0.20, 0.15), # NOx: 0.20-0.15 kg/MWh + Piece(0.15, 0.12), # NOx: 0.15-0.12 kg/MWh (optimal combustion) + Piece(0.12, 0.25), # NOx: 0.12-0.25 kg/MWh (overload penalties) + ] + ), + }, + ) + ``` + + Progressive utility pricing structure: + + ```python + electricity_billing = PiecewiseEffects( + piecewise_origin=Piecewise( + [ + Piece(0, 200), # Basic usage: 0-200 kWh/month + Piece(200, 800), # Standard usage: 200-800 kWh/month + Piece(800, 2000), # High usage: 800-2000 kWh/month + ] + ), + piecewise_shares={ + 'energy_rate': Piecewise( + [ + Piece(0.12, 0.12), # Basic rate: €0.12/kWh + Piece(0.18, 0.18), # Standard rate: €0.18/kWh + Piece(0.28, 0.28), # Premium rate: €0.28/kWh + ] + ), + 'carbon_tax': Piecewise( + [ + Piece(0.02, 0.02), # Low carbon tax: €0.02/kWh + Piece(0.03, 0.03), # Medium carbon tax: €0.03/kWh + Piece(0.05, 0.05), # High carbon tax: €0.05/kWh + ] + ), + }, + ) + ``` + + Data center with capacity-dependent efficiency: + + ```python + datacenter_effects = PiecewiseEffects( + piecewise_origin=Piecewise( + [ + Piece(100, 500), # Low utilization: 100-500 servers + Piece(500, 2000), # Medium utilization: 500-2000 servers + Piece(2000, 5000), # High utilization: 2000-5000 servers + ] + ), + piecewise_shares={ + 'power_per_server': Piecewise( + [ + Piece(0.8, 0.6), # 0.8-0.6 kW/server (inefficient cooling) + Piece(0.6, 0.4), # 0.6-0.4 kW/server (optimal efficiency) + Piece(0.4, 0.5), # 0.4-0.5 kW/server (thermal limits) + ] + ), + 'cooling_overhead': Piecewise( + [ + Piece(0.4, 0.3), # 40%-30% cooling overhead + Piece(0.3, 0.2), # 30%-20% cooling overhead + Piece(0.2, 0.25), # 20%-25% cooling overhead + ] + ), + }, + ) + ``` + + Design Patterns: + **Economies of Scale**: Decreasing unit costs/impacts with increased scale + **Learning Curves**: Improving efficiency rates with experience/volume + **Threshold Effects**: Step changes in rates at specific operating levels + **Progressive Pricing**: Increasing rates for higher consumption levels + **Capacity Utilization**: Optimal efficiency at design points, penalties at extremes + + Common Use Cases: + - Manufacturing: Production scaling, learning effects, quality improvements + - Energy systems: Generator efficiency curves, renewable capacity factors + - Logistics: Transportation rates, warehouse utilization, delivery optimization + - Utilities: Progressive pricing, infrastructure cost allocation + - Financial services: Risk premiums, transaction fees, volume discounts + - Environmental modeling: Pollution intensity, resource consumption rates + + """ + + def __init__(self, piecewise_origin: Piecewise, piecewise_shares: Dict[str, Piecewise]): self.piecewise_origin = piecewise_origin self.piecewise_shares = piecewise_shares @@ -104,8 +622,194 @@ def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): @register_class_for_io class InvestParameters(Interface): - """ - collects arguments for invest-stuff + """Define investment decision parameters with flexible sizing and effect modeling. + + This class models investment decisions in optimization problems, supporting + both binary (invest/don't invest) and continuous sizing choices with + comprehensive cost structures. It enables realistic representation of + investment economics including fixed costs, scale effects, and divestment penalties. + + Investment Decision Types: + **Binary Investments**: Fixed size investments creating yes/no decisions + (e.g., install a specific generator, build a particular facility) + + **Continuous Sizing**: Variable size investments with minimum/maximum bounds + (e.g., battery capacity from 10-1000 kWh, pipeline diameter optimization) + + Cost Modeling Approaches: + - **Fixed Effects**: One-time costs independent of size (permits, connections) + - **Specific Effects**: Linear costs proportional to size (€/kW, €/m²) + - **Piecewise Effects**: Non-linear relationships (bulk discounts, learning curves) + - **Divestment Effects**: Penalties for not investing (demolition, opportunity costs) + + Args: + fixed_size: When specified, creates a binary investment decision at exactly + this size. When None, allows continuous sizing between minimum and maximum bounds. + minimum_size: Lower bound for continuous sizing decisions. Defaults to a small + positive value (CONFIG.modeling.EPSILON) to avoid numerical issues. + Ignored when fixed_size is specified. + maximum_size: Upper bound for continuous sizing decisions. Defaults to a large + value (CONFIG.modeling.BIG) representing unlimited capacity. + Ignored when fixed_size is specified. + optional: Controls whether investment is required. When True (default), + optimization can choose not to invest. When False, forces investment + to occur (useful for mandatory upgrades or replacement decisions). + fix_effects: Fixed costs incurred once if investment is made, regardless + of size. Dictionary mapping effect names to values + (e.g., {'cost': 10000, 'CO2_construction': 500}). + specific_effects: Variable costs proportional to investment size, representing + per-unit costs (€/kW, €/m²). Dictionary mapping effect names to unit values + (e.g., {'cost': 1200, 'steel_required': 0.5}). + piecewise_effects: Non-linear cost relationships using PiecewiseEffects for + economies of scale, learning curves, or threshold effects. Can be combined + with fix_effects and specific_effects. + divest_effects: Costs incurred if the investment is NOT made, such as + demolition of existing equipment, contractual penalties, or lost opportunities. + Dictionary mapping effect names to values. + + Cost Annualization Requirements: + All cost values must be properly weighted to match the optimization model's time horizon. + For long-term investments, the cost values should be annualized to the corresponding operation time (annuity). + + - Use equivalent annual cost (capital cost / equipment lifetime) + - Apply appropriate discount rates for present value calculations + - Account for inflation, escalation, and financing costs + + Example: €1M equipment with 20-year life → €50k/year fixed cost + + Examples: + Simple binary investment (solar panels): + + ```python + solar_investment = InvestParameters( + fixed_size=100, # 100 kW system (binary decision) + optional=True, + fix_effects={ + 'cost': 25000, # Installation and permitting costs + 'CO2': -50000, # Avoided emissions over lifetime + }, + specific_effects={ + 'cost': 1200, # €1200/kW for panels (annualized) + 'CO2': -800, # kg CO2 avoided per kW annually + }, + ) + ``` + + Flexible sizing with economies of scale: + + ```python + battery_investment = InvestParameters( + minimum_size=10, # Minimum viable system size (kWh) + maximum_size=1000, # Maximum installable capacity + optional=True, + fix_effects={ + 'cost': 5000, # Grid connection and control system + 'installation_time': 2, # Days for fixed components + }, + piecewise_effects=PiecewiseEffects( + piecewise_origin=Piecewise( + [ + Piece(0, 100), # Small systems + Piece(100, 500), # Medium systems + Piece(500, 1000), # Large systems + ] + ), + piecewise_shares={ + 'cost': Piecewise( + [ + Piece(800, 750), # High cost/kWh for small systems + Piece(750, 600), # Medium cost/kWh + Piece(600, 500), # Bulk discount for large systems + ] + ) + }, + ), + ) + ``` + + Mandatory replacement with divestment costs: + + ```python + boiler_replacement = InvestParameters( + minimum_size=50, + maximum_size=200, + optional=True, # Can choose not to replace + fix_effects={ + 'cost': 15000, # Installation costs + 'disruption': 3, # Days of downtime + }, + specific_effects={ + 'cost': 400, # €400/kW capacity + 'maintenance': 25, # Annual maintenance per kW + }, + divest_effects={ + 'cost': 8000, # Demolition if not replaced + 'environmental': 100, # Disposal fees + }, + ) + ``` + + Multi-technology comparison: + + ```python + # Gas turbine option + gas_turbine = InvestParameters( + fixed_size=50, # MW + fix_effects={'cost': 2500000, 'CO2': 1250000}, + specific_effects={'fuel_cost': 45, 'maintenance': 12}, + ) + + # Wind farm option + wind_farm = InvestParameters( + minimum_size=20, + maximum_size=100, + fix_effects={'cost': 1000000, 'CO2': -5000000}, + specific_effects={'cost': 1800000, 'land_use': 0.5}, + ) + ``` + + Technology learning curve: + + ```python + hydrogen_electrolyzer = InvestParameters( + minimum_size=1, + maximum_size=50, # MW + piecewise_effects=PiecewiseEffects( + piecewise_origin=Piecewise( + [ + Piece(0, 5), # Small scale: early adoption + Piece(5, 20), # Medium scale: cost reduction + Piece(20, 50), # Large scale: mature technology + ] + ), + piecewise_shares={ + 'capex': Piecewise( + [ + Piece(2000, 1800), # Learning reduces costs + Piece(1800, 1400), # Continued cost reduction + Piece(1400, 1200), # Technology maturity + ] + ), + 'efficiency': Piecewise( + [ + Piece(65, 68), # Improving efficiency + Piece(68, 72), # with scale and experience + Piece(72, 75), # Best efficiency at scale + ] + ), + }, + ), + ) + ``` + + Common Use Cases: + - Power generation: Plant sizing, technology selection, retrofit decisions + - Industrial equipment: Capacity expansion, efficiency upgrades, replacements + - Infrastructure: Network expansion, facility construction, system upgrades + - Energy storage: Battery sizing, pumped hydro, compressed air systems + - Transportation: Fleet expansion, charging infrastructure, modal shifts + - Buildings: HVAC systems, insulation upgrades, renewable integration + """ def __init__( @@ -119,31 +823,6 @@ def __init__( piecewise_effects: Optional[PiecewiseEffects] = None, divest_effects: Optional['EffectValuesUserScalar'] = None, ): - """ - Args: - fix_effects: Fixed investment costs if invested. (Attention: Annualize costs to chosen period!) - divest_effects: Fixed divestment costs (if not invested, e.g., demolition costs or contractual penalty). - fixed_size: Determines if the investment size is fixed. - optional: If True, investment is not forced. - specific_effects: Specific costs, e.g., in €/kW_nominal or €/m²_nominal. - Example: {costs: 3, CO2: 0.3} with costs and CO2 representing an Object of class Effect - (Attention: Annualize costs to chosen period!) - piecewise_effects: Linear piecewise relation [invest_pieces, cost_pieces]. - Example 1: - [ [5, 25, 25, 100], # size in kW - {costs: [50,250,250,800], # € - PE: [5, 25, 25, 100] # kWh_PrimaryEnergy - } - ] - Example 2 (if only standard-effect): - [ [5, 25, 25, 100], # kW # size in kW - [50,250,250,800] # value for standart effect, typically € - ] # € - (Attention: Annualize costs to chosen period!) - (Args 'specific_effects' and 'fix_effects' can be used in parallel to Investsizepieces) - minimum_size: Min nominal value (only if: size_is_fixed = False). Defaults to CONFIG.modeling.EPSILON. - maximum_size: Max nominal value (only if: size_is_fixed = False). Defaults to CONFIG.modeling.BIG. - """ self.fix_effects: EffectValuesUser = fix_effects or {} self.divest_effects: EffectValuesUser = divest_effects or {} self.fixed_size = fixed_size @@ -169,6 +848,182 @@ def maximum_size(self): @register_class_for_io class OnOffParameters(Interface): + """Define operational constraints and effects for binary on/off equipment behavior. + + This class models equipment that operates in discrete states (on/off) rather than + continuous operation, capturing realistic operational constraints and associated + costs. It handles complex equipment behavior including startup costs, minimum + run times, cycling limitations, and maintenance scheduling requirements. + + Key Modeling Capabilities: + **Switching Costs**: One-time costs for starting equipment (fuel, wear, labor) + **Runtime Constraints**: Minimum and maximum continuous operation periods + **Cycling Limits**: Maximum number of starts to prevent excessive wear + **Operating Hours**: Total runtime limits and requirements over time horizon + + Typical Equipment Applications: + - **Power Plants**: Combined cycle units, steam turbines with startup costs + - **Industrial Processes**: Batch reactors, furnaces with thermal cycling + - **HVAC Systems**: Chillers, boilers with minimum run times + - **Backup Equipment**: Emergency generators, standby systems + - **Process Equipment**: Compressors, pumps with operational constraints + + Args: + effects_per_switch_on: Costs or impacts incurred for each transition from + off state (var_on=0) to on state (var_on=1). Represents startup costs, + wear and tear, or other switching impacts. Dictionary mapping effect + names to values (e.g., {'cost': 500, 'maintenance_hours': 2}). + effects_per_running_hour: Ongoing costs or impacts while equipment operates + in the on state. Includes fuel costs, labor, consumables, or emissions. + Dictionary mapping effect names to hourly values (e.g., {'fuel_cost': 45}). + on_hours_total_min: Minimum total operating hours across the entire time horizon. + Ensures equipment meets minimum utilization requirements or contractual + obligations (e.g., power purchase agreements, maintenance schedules). + on_hours_total_max: Maximum total operating hours across the entire time horizon. + Limits equipment usage due to maintenance schedules, fuel availability, + environmental permits, or equipment lifetime constraints. + consecutive_on_hours_min: Minimum continuous operating duration once started. + Models minimum run times due to thermal constraints, process stability, + or efficiency considerations. Can be time-varying to reflect different + constraints across the planning horizon. + consecutive_on_hours_max: Maximum continuous operating duration in one campaign. + Models mandatory maintenance intervals, process batch sizes, or + equipment thermal limits requiring periodic shutdowns. + consecutive_off_hours_min: Minimum continuous shutdown duration between operations. + Models cooling periods, maintenance requirements, or process constraints + that prevent immediate restart after shutdown. + consecutive_off_hours_max: Maximum continuous shutdown duration before mandatory + restart. Models equipment preservation, process stability, or contractual + requirements for minimum activity levels. + switch_on_total_max: Maximum number of startup operations across the time horizon. + Limits equipment cycling to reduce wear, maintenance costs, or comply + with operational constraints (e.g., grid stability requirements). + force_switch_on: When True, creates switch-on variables even without explicit + switch_on_total_max constraint. Useful for tracking or reporting startup + events without enforcing limits. + + Note: + **Time Series Boundary Handling**: The final time period constraints for + consecutive_on_hours_min/max and consecutive_off_hours_min/max are not + enforced, allowing the optimization to end with ongoing campaigns that + may be shorter than the specified minimums or longer than maximums. + + Examples: + Combined cycle power plant with startup costs and minimum run time: + + ```python + power_plant_operation = OnOffParameters( + effects_per_switch_on={ + 'startup_cost': 25000, # €25,000 per startup + 'startup_fuel': 150, # GJ natural gas for startup + 'startup_time': 4, # Hours to reach full output + 'maintenance_impact': 0.1, # Fractional life consumption + }, + effects_per_running_hour={ + 'fixed_om': 125, # Fixed O&M costs while running + 'auxiliary_power': 2.5, # MW parasitic loads + }, + consecutive_on_hours_min=8, # Minimum 8-hour run once started + consecutive_off_hours_min=4, # Minimum 4-hour cooling period + on_hours_total_max=6000, # Annual operating limit + ) + ``` + + Industrial batch process with cycling limits: + + ```python + batch_reactor = OnOffParameters( + effects_per_switch_on={ + 'setup_cost': 1500, # Labor and materials for startup + 'catalyst_consumption': 5, # kg catalyst per batch + 'cleaning_chemicals': 200, # L cleaning solution + }, + effects_per_running_hour={ + 'steam': 2.5, # t/h process steam + 'electricity': 150, # kWh electrical load + 'cooling_water': 50, # m³/h cooling water + }, + consecutive_on_hours_min=12, # Minimum batch size (12 hours) + consecutive_on_hours_max=24, # Maximum batch size (24 hours) + consecutive_off_hours_min=6, # Cleaning and setup time + switch_on_total_max=200, # Maximum 200 batches per year + on_hours_total_max=4000, # Maximum production time + ) + ``` + + HVAC system with thermostat control and maintenance: + + ```python + hvac_operation = OnOffParameters( + effects_per_switch_on={ + 'compressor_wear': 0.5, # Hours of compressor life per start + 'inrush_current': 15, # kW peak demand on startup + }, + effects_per_running_hour={ + 'electricity': 25, # kW electrical consumption + 'maintenance': 0.12, # €/hour maintenance reserve + }, + consecutive_on_hours_min=1, # Minimum 1-hour run to avoid cycling + consecutive_off_hours_min=0.5, # 30-minute minimum off time + switch_on_total_max=2000, # Limit cycling for compressor life + on_hours_total_min=2000, # Minimum operation for humidity control + on_hours_total_max=5000, # Maximum operation for energy budget + ) + ``` + + Backup generator with testing and maintenance requirements: + + ```python + backup_generator = OnOffParameters( + effects_per_switch_on={ + 'fuel_priming': 50, # L diesel for system priming + 'wear_factor': 1.0, # Start cycles impact on maintenance + 'testing_labor': 2, # Hours technician time per test + }, + effects_per_running_hour={ + 'fuel_consumption': 180, # L/h diesel consumption + 'emissions_permit': 15, # € emissions allowance cost + 'noise_penalty': 25, # € noise compliance cost + }, + consecutive_on_hours_min=0.5, # Minimum test duration (30 min) + consecutive_off_hours_max=720, # Maximum 30 days between tests + switch_on_total_max=52, # Weekly testing limit + on_hours_total_min=26, # Minimum annual testing (0.5h × 52) + on_hours_total_max=200, # Maximum runtime (emergencies + tests) + ) + ``` + + Peak shaving battery with cycling degradation: + + ```python + battery_cycling = OnOffParameters( + effects_per_switch_on={ + 'cycle_degradation': 0.01, # % capacity loss per cycle + 'inverter_startup': 0.5, # kWh losses during startup + }, + effects_per_running_hour={ + 'standby_losses': 2, # kW standby consumption + 'cooling': 5, # kW thermal management + 'inverter_losses': 8, # kW conversion losses + }, + consecutive_on_hours_min=1, # Minimum discharge duration + consecutive_on_hours_max=4, # Maximum continuous discharge + consecutive_off_hours_min=1, # Minimum rest between cycles + switch_on_total_max=365, # Daily cycling limit + force_switch_on=True, # Track all cycling events + ) + ``` + + Common Use Cases: + - Power generation: Thermal plant cycling, renewable curtailment, grid services + - Industrial processes: Batch production, maintenance scheduling, equipment rotation + - Buildings: HVAC control, lighting systems, elevator operations + - Transportation: Fleet management, charging infrastructure, maintenance windows + - Storage systems: Battery cycling, pumped hydro, compressed air systems + - Emergency equipment: Backup generators, safety systems, emergency lighting + + """ + def __init__( self, effects_per_switch_on: Optional['EffectValuesUser'] = None, @@ -182,26 +1037,6 @@ def __init__( switch_on_total_max: Optional[int] = None, force_switch_on: bool = False, ): - """ - Bundles information about the on and off state of an Element. - If no parameters are given, the default is to create a binary variable for the on state - without further constraints or effects and a variable for the total on hours. - - Args: - effects_per_switch_on: cost of one switch from off (var_on=0) to on (var_on=1), - unit i.g. in Euro - effects_per_running_hour: costs for operating, i.g. in € per hour - on_hours_total_min: min. overall sum of operating hours. - on_hours_total_max: max. overall sum of operating hours. - consecutive_on_hours_min: min sum of operating hours in one piece - (last on-time period of timeseries is not checked and can be shorter) - consecutive_on_hours_max: max sum of operating hours in one piece - consecutive_off_hours_min: min sum of non-operating hours in one piece - (last off-time period of timeseries is not checked and can be shorter) - consecutive_off_hours_max: max sum of non-operating hours in one piece - switch_on_total_max: max nr of switchOn operations - force_switch_on: force creation of switch on variable, even if there is no switch_on_total_max - """ self.effects_per_switch_on: EffectValuesUser = effects_per_switch_on or {} self.effects_per_running_hour: EffectValuesUser = effects_per_running_hour or {} self.on_hours_total_min: Scalar = on_hours_total_min @@ -235,22 +1070,22 @@ def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): @property def use_off(self) -> bool: - """Determines wether the OFF Variable is needed or not""" + """Proxy: whether OFF variable is required""" return self.use_consecutive_off_hours @property def use_consecutive_on_hours(self) -> bool: - """Determines wether a Variable for consecutive off hours is needed or not""" + """Determines whether a Variable for consecutive on hours is needed or not""" return any(param is not None for param in [self.consecutive_on_hours_min, self.consecutive_on_hours_max]) @property def use_consecutive_off_hours(self) -> bool: - """Determines wether a Variable for consecutive off hours is needed or not""" + """Determines whether a Variable for consecutive off hours is needed or not""" return any(param is not None for param in [self.consecutive_off_hours_min, self.consecutive_off_hours_max]) @property def use_switch_on(self) -> bool: - """Determines wether a Variable for SWITCH-ON is needed or not""" + """Determines whether a Variable for SWITCH-ON is needed or not""" return ( any( param not in (None, {}) diff --git a/flixopt/linear_converters.py b/flixopt/linear_converters.py index 3fd032632..7f3169c7d 100644 --- a/flixopt/linear_converters.py +++ b/flixopt/linear_converters.py @@ -18,6 +18,57 @@ @register_class_for_io class Boiler(LinearConverter): + """ + A specialized LinearConverter representing a fuel-fired boiler for thermal energy generation. + + Boilers convert fuel input into thermal energy with a specified efficiency factor. + This is a simplified wrapper around LinearConverter with predefined conversion + relationships for thermal generation applications. + + Args: + label: The label of the Element. Used to identify it in the FlowSystem. + eta: Thermal efficiency factor (0-1 range). Defines the ratio of thermal + output to fuel input energy content. + Q_fu: Fuel input-flow representing fuel consumption. + Q_th: Thermal output-flow representing heat generation. + on_off_parameters: Parameters defining binary operation constraints and costs. + meta_data: Used to store additional information. Not used internally but + saved in results. Only use Python native types. + + Examples: + Natural gas boiler: + + ```python + gas_boiler = Boiler( + label='natural_gas_boiler', + eta=0.85, # 85% thermal efficiency + Q_fu=natural_gas_flow, + Q_th=hot_water_flow, + ) + ``` + + Biomass boiler with seasonal efficiency variation: + + ```python + biomass_boiler = Boiler( + label='wood_chip_boiler', + eta=seasonal_efficiency_profile, # Time-varying efficiency + Q_fu=biomass_flow, + Q_th=district_heat_flow, + on_off_parameters=OnOffParameters( + consecutive_on_hours_min=4, # Minimum 4-hour operation + effects_per_switch_on={'startup_fuel': 50}, # Startup fuel penalty + ), + ) + ``` + + Note: + The conversion relationship is: Q_th = Q_fu × eta + + Efficiency should be between 0 and 1, where 1 represents perfect conversion + (100% of fuel energy converted to useful thermal output). + """ + def __init__( self, label: str, @@ -27,15 +78,6 @@ def __init__( on_off_parameters: OnOffParameters = None, meta_data: Optional[Dict] = None, ): - """ - Args: - label: The label of the Element. Used to identify it in the FlowSystem - eta: thermal efficiency. - Q_fu: fuel input-flow - Q_th: thermal output-flow. - on_off_parameters: Parameters defining the on/off behavior of the component. - meta_data: used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types. - """ super().__init__( label, inputs=[Q_fu], @@ -59,6 +101,61 @@ def eta(self, value): @register_class_for_io class Power2Heat(LinearConverter): + """ + A specialized LinearConverter representing electric resistance heating or power-to-heat conversion. + + Power2Heat components convert electrical energy directly into thermal energy through + resistance heating elements, electrode boilers, or other direct electric heating + technologies. This is a simplified wrapper around LinearConverter with predefined + conversion relationships for electric heating applications. + + Args: + label: The label of the Element. Used to identify it in the FlowSystem. + eta: Thermal efficiency factor (0-1 range). For resistance heating this is + typically close to 1.0 (nearly 100% efficiency), but may be lower for + electrode boilers or systems with distribution losses. + P_el: Electrical input-flow representing electricity consumption. + Q_th: Thermal output-flow representing heat generation. + on_off_parameters: Parameters defining binary operation constraints and costs. + meta_data: Used to store additional information. Not used internally but + saved in results. Only use Python native types. + + Examples: + Electric resistance heater: + + ```python + electric_heater = Power2Heat( + label='resistance_heater', + eta=0.98, # 98% efficiency (small losses) + P_el=electricity_flow, + Q_th=space_heating_flow, + ) + ``` + + Electrode boiler for industrial steam: + + ```python + electrode_boiler = Power2Heat( + label='electrode_steam_boiler', + eta=0.95, # 95% efficiency including boiler losses + P_el=industrial_electricity, + Q_th=process_steam_flow, + on_off_parameters=OnOffParameters( + consecutive_on_hours_min=1, # Minimum 1-hour operation + effects_per_switch_on={'startup_cost': 100}, + ), + ) + ``` + + Note: + The conversion relationship is: Q_th = P_el × eta + + Unlike heat pumps, Power2Heat systems cannot exceed 100% efficiency (eta ≤ 1.0) + as they only convert electrical energy without extracting additional energy + from the environment. However, they provide fast response times and precise + temperature control. + """ + def __init__( self, label: str, @@ -68,15 +165,6 @@ def __init__( on_off_parameters: OnOffParameters = None, meta_data: Optional[Dict] = None, ): - """ - Args: - label: The label of the Element. Used to identify it in the FlowSystem - eta: thermal efficiency. - P_el: electric input-flow - Q_th: thermal output-flow. - on_off_parameters: Parameters defining the on/off behavior of the component. - meta_data: used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types. - """ super().__init__( label, inputs=[P_el], @@ -101,6 +189,60 @@ def eta(self, value): @register_class_for_io class HeatPump(LinearConverter): + """ + A specialized LinearConverter representing an electric heat pump for thermal energy generation. + + Heat pumps convert electrical energy into thermal energy with a Coefficient of + Performance (COP) greater than 1, making them more efficient than direct electric + heating. This is a simplified wrapper around LinearConverter with predefined + conversion relationships for heat pump applications. + + Args: + label: The label of the Element. Used to identify it in the FlowSystem. + COP: Coefficient of Performance (typically 1-20 range). Defines the ratio of + thermal output to electrical input. COP > 1 indicates the heat pump extracts + additional energy from the environment. + P_el: Electrical input-flow representing electricity consumption. + Q_th: Thermal output-flow representing heat generation. + on_off_parameters: Parameters defining binary operation constraints and costs. + meta_data: Used to store additional information. Not used internally but + saved in results. Only use Python native types. + + Examples: + Air-source heat pump with constant COP: + + ```python + air_hp = HeatPump( + label='air_source_heat_pump', + COP=3.5, # COP of 3.5 (350% efficiency) + P_el=electricity_flow, + Q_th=heating_flow, + ) + ``` + + Ground-source heat pump with temperature-dependent COP: + + ```python + ground_hp = HeatPump( + label='geothermal_heat_pump', + COP=temperature_dependent_cop, # Time-varying COP based on ground temp + P_el=electricity_flow, + Q_th=radiant_heating_flow, + on_off_parameters=OnOffParameters( + consecutive_on_hours_min=2, # Avoid frequent cycling + effects_per_running_hour={'maintenance': 0.5}, + ), + ) + ``` + + Note: + The conversion relationship is: Q_th = P_el × COP + + COP should be greater than 1 for realistic heat pump operation, with typical + values ranging from 2-6 depending on technology and operating conditions. + Higher COP values indicate more efficient heat extraction from the environment. + """ + def __init__( self, label: str, @@ -110,15 +252,6 @@ def __init__( on_off_parameters: OnOffParameters = None, meta_data: Optional[Dict] = None, ): - """ - Args: - label: The label of the Element. Used to identify it in the FlowSystem - COP: Coefficient of performance. - P_el: electricity input-flow. - Q_th: thermal output-flow. - on_off_parameters: Parameters defining the on/off behavior of the component. - meta_data: used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types. - """ super().__init__( label, inputs=[P_el], @@ -143,6 +276,62 @@ def COP(self, value): # noqa: N802 @register_class_for_io class CoolingTower(LinearConverter): + """ + A specialized LinearConverter representing a cooling tower for waste heat rejection. + + Cooling towers consume electrical energy (for fans, pumps) to reject thermal energy + to the environment through evaporation and heat transfer. The electricity demand + is typically a small fraction of the thermal load being rejected. This component + has no thermal outputs as the heat is rejected to the environment. + + Args: + label: The label of the Element. Used to identify it in the FlowSystem. + specific_electricity_demand: Auxiliary electricity demand per unit of cooling + power (dimensionless, typically 0.01-0.05 range). Represents the fraction + of thermal power that must be supplied as electricity for fans and pumps. + P_el: Electrical input-flow representing electricity consumption for fans/pumps. + Q_th: Thermal input-flow representing waste heat to be rejected to environment. + on_off_parameters: Parameters defining binary operation constraints and costs. + meta_data: Used to store additional information. Not used internally but + saved in results. Only use Python native types. + + Examples: + Industrial cooling tower: + + ```python + cooling_tower = CoolingTower( + label='process_cooling_tower', + specific_electricity_demand=0.025, # 2.5% auxiliary power + P_el=cooling_electricity, + Q_th=waste_heat_flow, + ) + ``` + + Power plant condenser cooling: + + ```python + condenser_cooling = CoolingTower( + label='power_plant_cooling', + specific_electricity_demand=0.015, # 1.5% auxiliary power + P_el=auxiliary_electricity, + Q_th=condenser_waste_heat, + on_off_parameters=OnOffParameters( + consecutive_on_hours_min=4, # Minimum operation time + effects_per_running_hour={'water_consumption': 2.5}, # m³/h + ), + ) + ``` + + Note: + The conversion relationship is: P_el = Q_th × specific_electricity_demand + + The cooling tower consumes electrical power proportional to the thermal load. + No thermal energy is produced - all thermal input is rejected to the environment. + + Typical specific electricity demands range from 1-5% of the thermal cooling load, + depending on tower design, climate conditions, and operational requirements. + """ + def __init__( self, label: str, @@ -152,15 +341,6 @@ def __init__( on_off_parameters: OnOffParameters = None, meta_data: Optional[Dict] = None, ): - """ - Args: - label: The label of the Element. Used to identify it in the FlowSystem - specific_electricity_demand: auxiliary electricty demand per cooling power, i.g. 0.02 (2 %). - P_el: electricity input-flow. - Q_th: thermal input-flow. - on_off_parameters: Parameters defining the on/off behavior of the component. - meta_data: used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types. - """ super().__init__( label, inputs=[P_el, Q_th], @@ -187,6 +367,69 @@ def specific_electricity_demand(self, value): @register_class_for_io class CHP(LinearConverter): + """ + A specialized LinearConverter representing a Combined Heat and Power (CHP) unit. + + CHP units simultaneously generate both electrical and thermal energy from a single + fuel input, providing higher overall efficiency than separate generation. This is + a wrapper around LinearConverter with predefined conversion relationships for + cogeneration applications. + + Args: + label: The label of the Element. Used to identify it in the FlowSystem. + eta_th: Thermal efficiency factor (0-1 range). Defines the fraction of fuel + energy converted to useful thermal output. + eta_el: Electrical efficiency factor (0-1 range). Defines the fraction of fuel + energy converted to electrical output. + Q_fu: Fuel input-flow representing fuel consumption. + P_el: Electrical output-flow representing electricity generation. + Q_th: Thermal output-flow representing heat generation. + on_off_parameters: Parameters defining binary operation constraints and costs. + meta_data: Used to store additional information. Not used internally but + saved in results. Only use Python native types. + + Examples: + Natural gas CHP unit: + + ```python + gas_chp = CHP( + label='natural_gas_chp', + eta_th=0.45, # 45% thermal efficiency + eta_el=0.35, # 35% electrical efficiency (80% total) + Q_fu=natural_gas_flow, + P_el=electricity_flow, + Q_th=district_heat_flow, + ) + ``` + + Industrial CHP with operational constraints: + + ```python + industrial_chp = CHP( + label='industrial_chp', + eta_th=0.40, + eta_el=0.38, + Q_fu=fuel_gas_flow, + P_el=plant_electricity, + Q_th=process_steam, + on_off_parameters=OnOffParameters( + consecutive_on_hours_min=8, # Minimum 8-hour operation + effects_per_switch_on={'startup_cost': 5000}, + on_hours_total_max=6000, # Annual operating limit + ), + ) + ``` + + Note: + The conversion relationships are: + - Q_th = Q_fu × eta_th (thermal output) + - P_el = Q_fu × eta_el (electrical output) + + Total efficiency (eta_th + eta_el) should be ≤ 1.0, with typical combined + efficiencies of 80-90% for modern CHP units. This provides significant + efficiency gains compared to separate heat and power generation. + """ + def __init__( self, label: str, @@ -198,17 +441,6 @@ def __init__( on_off_parameters: OnOffParameters = None, meta_data: Optional[Dict] = None, ): - """ - Args: - label: The label of the Element. Used to identify it in the FlowSystem - eta_th: thermal efficiency. - eta_el: electrical efficiency. - Q_fu: fuel input-flow. - P_el: electricity output-flow. - Q_th: heat output-flow. - on_off_parameters: Parameters defining the on/off behavior of the component. - meta_data: used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types. - """ heat = {Q_fu.label: eta_th, Q_th.label: 1} electricity = {Q_fu.label: eta_el, P_el.label: 1} @@ -248,6 +480,70 @@ def eta_el(self, value): @register_class_for_io class HeatPumpWithSource(LinearConverter): + """ + A specialized LinearConverter representing a heat pump with explicit heat source modeling. + + This component models a heat pump that extracts thermal energy from a heat source + (ground, air, water) and upgrades it using electrical energy to provide higher-grade + thermal output. Unlike the simple HeatPump class, this explicitly models both the + heat source extraction and electrical consumption with their interdependent relationships. + + Args: + label: The label of the Element. Used to identify it in the FlowSystem. + COP: Coefficient of Performance (typically 1-20 range). Defines the ratio of + thermal output to electrical input. The heat source extraction is automatically + calculated as Q_ab = Q_th × (COP-1)/COP. + P_el: Electrical input-flow representing electricity consumption for compressor. + Q_ab: Heat source input-flow representing thermal energy extracted from environment + (ground, air, water source). + Q_th: Thermal output-flow representing useful heat delivered to the application. + on_off_parameters: Parameters defining binary operation constraints and costs. + meta_data: Used to store additional information. Not used internally but + saved in results. Only use Python native types. + + Examples: + Ground-source heat pump with explicit ground coupling: + + ```python + ground_source_hp = HeatPumpWithSource( + label='geothermal_heat_pump', + COP=4.5, # High COP due to stable ground temperature + P_el=electricity_flow, + Q_ab=ground_heat_extraction, # Heat extracted from ground loop + Q_th=building_heating_flow, + ) + ``` + + Air-source heat pump with temperature-dependent performance: + + ```python + waste_heat_pump = HeatPumpWithSource( + label='waste_heat_pump', + COP=temperature_dependent_cop, # Varies with temperature of heat source + P_el=electricity_consumption, + Q_ab=industrial_heat_extraction, # Heat extracted from a industrial process or waste water + Q_th=heat_supply, + on_off_parameters=OnOffParameters( + consecutive_on_hours_min=0.5, # 30-minute minimum runtime + effects_per_switch_on={'costs': 1000}, + ), + ) + ``` + + Note: + The conversion relationships are: + - Q_th = P_el × COP (thermal output from electrical input) + - Q_ab = Q_th × (COP-1)/COP (heat source extraction) + - Energy balance: Q_th = P_el + Q_ab + + This formulation explicitly tracks the heat source, which is + important for systems where the source capacity or temperature is limited, + or where the impact of heat extraction must be considered. + + COP should be > 1 for thermodynamically valid operation, with typical + values of 2-6 depending on source and sink temperatures. + """ + def __init__( self, label: str, @@ -258,17 +554,6 @@ def __init__( on_off_parameters: OnOffParameters = None, meta_data: Optional[Dict] = None, ): - """ - Args: - label: The label of the Element. Used to identify it in the FlowSystem - COP: Coefficient of performance. - Q_ab: Heatsource input-flow. - P_el: electricity input-flow. - Q_th: thermal output-flow. - on_off_parameters: Parameters defining the on/off behavior of the component. - meta_data: used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types. - """ - # super: electricity = {P_el.label: COP, Q_th.label: 1} heat_source = {Q_ab.label: COP / (COP - 1), Q_th.label: 1} diff --git a/flixopt/plotting.py b/flixopt/plotting.py index 3abb30842..a682736e1 100644 --- a/flixopt/plotting.py +++ b/flixopt/plotting.py @@ -1,7 +1,26 @@ -""" -This module contains the plotting functionality of the flixopt framework. -It provides high level functions to plot data with plotly and matplotlib. -It's meant to be used in results.py, but is designed to be used by the end user as well. +"""Comprehensive visualization toolkit for flixopt optimization results and data analysis. + +This module provides a unified plotting interface supporting both Plotly (interactive) +and Matplotlib (static) backends for visualizing energy system optimization results. +It offers specialized plotting functions for time series, heatmaps, network diagrams, +and statistical analyses commonly needed in energy system modeling. + +Key Features: + **Dual Backend Support**: Seamless switching between Plotly and Matplotlib + **Energy System Focus**: Specialized plots for power flows, storage states, emissions + **Color Management**: Intelligent color processing and palette management + **Export Capabilities**: High-quality export for reports and publications + **Integration Ready**: Designed for use with CalculationResults and standalone analysis + +Main Plot Types: + - **Time Series**: Flow rates, power profiles, storage states over time + - **Heatmaps**: High-resolution temporal data visualization with customizable aggregation + - **Network Diagrams**: System topology with flow visualization + - **Statistical Plots**: Distribution analysis, correlation studies, performance metrics + - **Comparative Analysis**: Multi-scenario and sensitivity study visualizations + +The module integrates seamlessly with flixopt's result classes while remaining +accessible for standalone data visualization tasks. """ import itertools @@ -33,18 +52,63 @@ ] # Check if the colormap already exists before registering it -if 'portland' not in plt.colormaps: - plt.colormaps.register(mcolors.LinearSegmentedColormap.from_list('portland', _portland_colors)) +if hasattr(plt, 'colormaps'): # Matplotlib >= 3.7 + registry = plt.colormaps + if 'portland' not in registry: + registry.register(mcolors.LinearSegmentedColormap.from_list('portland', _portland_colors)) +else: # Matplotlib < 3.7 + if 'portland' not in [c for c in plt.colormaps()]: + plt.register_cmap(name='portland', cmap=mcolors.LinearSegmentedColormap.from_list('portland', _portland_colors)) ColorType = Union[str, List[str], Dict[str, str]] -"""Identifier for the colors to use. -Use the name of a colorscale, a list of colors or a dictionary of labels to colors. -The colors must be valid color strings (HEX or names). Depending on the Engine used, other formats are possible. -See also: -- https://htmlcolorcodes.com/color-names/ -- https://matplotlib.org/stable/tutorials/colors/colormaps.html -- https://plotly.com/python/builtin-colorscales/ +"""Flexible color specification type supporting multiple input formats for visualization. + +Color specifications can take several forms to accommodate different use cases: + +**Named Colormaps** (str): + - Standard colormaps: 'viridis', 'plasma', 'cividis', 'tab10', 'Set1' + - Energy-focused: 'portland' (custom flixopt colormap for energy systems) + - Backend-specific maps available in Plotly and Matplotlib + +**Color Lists** (List[str]): + - Explicit color sequences: ['red', 'blue', 'green', 'orange'] + - HEX codes: ['#FF0000', '#0000FF', '#00FF00', '#FFA500'] + - Mixed formats: ['red', '#0000FF', 'green', 'orange'] + +**Label-to-Color Mapping** (Dict[str, str]): + - Explicit associations: {'Wind': 'skyblue', 'Solar': 'gold', 'Gas': 'brown'} + - Ensures consistent colors across different plots and datasets + - Ideal for energy system components with semantic meaning + +Examples: + ```python + # Named colormap + colors = 'viridis' # Automatic color generation + + # Explicit color list + colors = ['red', 'blue', 'green', '#FFD700'] + + # Component-specific mapping + colors = { + 'Wind_Turbine': 'skyblue', + 'Solar_Panel': 'gold', + 'Natural_Gas': 'brown', + 'Battery': 'green', + 'Electric_Load': 'darkred' + } + ``` + +Color Format Support: + - **Named Colors**: 'red', 'blue', 'forestgreen', 'darkorange' + - **HEX Codes**: '#FF0000', '#0000FF', '#228B22', '#FF8C00' + - **RGB Tuples**: (255, 0, 0), (0, 0, 255) [Matplotlib only] + - **RGBA**: 'rgba(255,0,0,0.8)' [Plotly only] + +References: + - HTML Color Names: https://htmlcolorcodes.com/color-names/ + - Matplotlib Colormaps: https://matplotlib.org/stable/tutorials/colors/colormaps.html + - Plotly Built-in Colorscales: https://plotly.com/python/builtin-colorscales/ """ PlottingEngine = Literal['plotly', 'matplotlib'] @@ -52,16 +116,68 @@ class ColorProcessor: - """Class to handle color processing for different visualization engines.""" + """Intelligent color management system for consistent multi-backend visualization. + + This class provides unified color processing across Plotly and Matplotlib backends, + ensuring consistent visual appearance regardless of the plotting engine used. + It handles color palette generation, named colormap translation, and intelligent + color cycling for complex datasets with many categories. + + Key Features: + **Backend Agnostic**: Automatic color format conversion between engines + **Palette Management**: Support for named colormaps, custom palettes, and color lists + **Intelligent Cycling**: Smart color assignment for datasets with many categories + **Fallback Handling**: Graceful degradation when requested colormaps are unavailable + **Energy System Colors**: Built-in palettes optimized for energy system visualization + + Color Input Types: + - **Named Colormaps**: 'viridis', 'plasma', 'portland', 'tab10', etc. + - **Color Lists**: ['red', 'blue', 'green'] or ['#FF0000', '#0000FF', '#00FF00'] + - **Label Dictionaries**: {'Generator': 'red', 'Storage': 'blue', 'Load': 'green'} + + Examples: + Basic color processing: + + ```python + # Initialize for Plotly backend + processor = ColorProcessor(engine='plotly', default_colormap='viridis') + + # Process different color specifications + colors = processor.process_colors('plasma', ['Gen1', 'Gen2', 'Storage']) + colors = processor.process_colors(['red', 'blue', 'green'], ['A', 'B', 'C']) + colors = processor.process_colors({'Wind': 'skyblue', 'Solar': 'gold'}, ['Wind', 'Solar', 'Gas']) + + # Switch to Matplotlib + processor = ColorProcessor(engine='matplotlib') + mpl_colors = processor.process_colors('tab10', component_labels) + ``` + + Energy system visualization: + + ```python + # Specialized energy system palette + energy_colors = { + 'Natural_Gas': '#8B4513', # Brown + 'Electricity': '#FFD700', # Gold + 'Heat': '#FF4500', # Red-orange + 'Cooling': '#87CEEB', # Sky blue + 'Hydrogen': '#E6E6FA', # Lavender + 'Battery': '#32CD32', # Lime green + } + + processor = ColorProcessor('plotly') + flow_colors = processor.process_colors(energy_colors, flow_labels) + ``` - def __init__(self, engine: PlottingEngine = 'plotly', default_colormap: str = 'viridis'): - """ - Initialize the color processor. + Args: + engine: Plotting backend ('plotly' or 'matplotlib'). Determines output color format. + default_colormap: Fallback colormap when requested palettes are unavailable. + Common options: 'viridis', 'plasma', 'tab10', 'portland'. - Args: - engine: The plotting engine to use ('plotly' or 'matplotlib') - default_colormap: Default colormap to use if none is specified - """ + """ + + def __init__(self, engine: PlottingEngine = 'plotly', default_colormap: str = 'viridis'): + """Initialize the color processor with specified backend and defaults.""" if engine not in ['plotly', 'matplotlib']: raise TypeError(f'engine must be "plotly" or "matplotlib", but is {engine}') self.engine = engine diff --git a/flixopt/results.py b/flixopt/results.py index aaa2ff161..46181a61f 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -26,55 +26,95 @@ class CalculationResults: - """Results container for Calculation results. - - This class is used to collect the results of a Calculation. - It provides access to component, bus, and effect - results, and includes methods for filtering, plotting, and saving results. - - The recommended way to create instances is through the class methods - `from_file()` or `from_calculation()`, rather than direct initialization. + """Comprehensive container for optimization calculation results and analysis tools. + + This class provides unified access to all optimization results including flow rates, + component states, bus balances, and system effects. It offers powerful analysis + capabilities through filtering, plotting, and export functionality, making it + the primary interface for post-processing optimization results. + + Key Features: + **Unified Access**: Single interface to all solution variables and constraints + **Element Results**: Direct access to component, bus, and effect-specific results + **Visualization**: Built-in plotting methods for heatmaps, time series, and networks + **Persistence**: Save/load functionality with compression for large datasets + **Analysis Tools**: Filtering, aggregation, and statistical analysis methods + + Result Organization: + - **Components**: Equipment-specific results (flows, states, constraints) + - **Buses**: Network node balances and energy flows + - **Effects**: System-wide impacts (costs, emissions, resource consumption) + - **Solution**: Raw optimization variables and their values + - **Metadata**: Calculation parameters, timing, and system configuration Attributes: - solution (xr.Dataset): Dataset containing optimization results. - flow_system (xr.Dataset): Dataset containing the flow system. - summary (Dict): Information about the calculation. - name (str): Name identifier for the calculation. - model (linopy.Model): The optimization model (if available). - folder (pathlib.Path): Path to the results directory. - components (Dict[str, ComponentResults]): Results for each component. - buses (Dict[str, BusResults]): Results for each bus. - effects (Dict[str, EffectResults]): Results for each effect. - timesteps_extra (pd.DatetimeIndex): The extended timesteps. - hours_per_timestep (xr.DataArray): Duration of each timestep in hours. - - Example: - Load results from saved files: - - >>> results = CalculationResults.from_file('results_dir', 'optimization_run_1') - >>> element_result = results['Boiler'] - >>> results.plot_heatmap('Boiler(Q_th)|flow_rate') - >>> results.to_file(compression=5) - >>> results.to_file(folder='new_results_dir', compression=5) # Save the results to a new folder + solution: Dataset containing all optimization variable solutions + flow_system: Dataset with complete system configuration and parameters. Restore the used FlowSystem for further analysis. + summary: Calculation metadata including solver status, timing, and statistics + name: Unique identifier for this calculation + model: Original linopy optimization model (if available) + folder: Directory path for result storage and loading + components: Dictionary mapping component labels to ComponentResults objects + buses: Dictionary mapping bus labels to BusResults objects + effects: Dictionary mapping effect names to EffectResults objects + timesteps_extra: Extended time index including boundary conditions + hours_per_timestep: Duration of each timestep for proper energy calculations + + Examples: + Load and analyze saved results: + + ```python + # Load results from file + results = CalculationResults.from_file('results', 'annual_optimization') + + # Access specific component results + boiler_results = results['Boiler_01'] + heat_pump_results = results['HeatPump_02'] + + # Plot component flow rates + results.plot_heatmap('Boiler_01(Natural_Gas)|flow_rate') + results['Boiler_01'].plot_node_balance() + + # Access raw solution dataarrays + electricity_flows = results.solution[['Generator_01(Grid)|flow_rate', 'HeatPump_02(Grid)|flow_rate']] + + # Filter and analyze results + peak_demand_hours = results.filter_solution(variable_dims='time') + costs_solution = results.effects['cost'].solution + ``` + + Advanced filtering and aggregation: + + ```python + # Filter by variable type + scalar_results = results.filter_solution(variable_dims='scalar') + time_series = results.filter_solution(variable_dims='time') + + # Custom data analysis leveraging xarray + peak_power = results.solution['Generator_01(Grid)|flow_rate'].max() + avg_efficiency = ( + results.solution['HeatPump(Heat)|flow_rate'] / results.solution['HeatPump(Electricity)|flow_rate'] + ).mean() + ``` + + Design Patterns: + **Factory Methods**: Use `from_file()` and `from_calculation()` for creation or access directly from `Calculation.results` + **Dictionary Access**: Use `results[element_label]` for element-specific results + **Lazy Loading**: Results objects created on-demand for memory efficiency + **Unified Interface**: Consistent API across different result types + """ @classmethod def from_file(cls, folder: Union[str, pathlib.Path], name: str) -> 'CalculationResults': - """Create CalculationResults instance by loading from saved files. - - This method loads the calculation results from previously saved files, - including the solution, flow system, model (if available), and metadata. + """Load CalculationResults from saved files. Args: - folder: Path to the directory containing the saved files. - name: Base name of the saved files (without file extensions). + folder: Directory containing saved files. + name: Base name of saved files (without extensions). Returns: - CalculationResults: A new instance containing the loaded data. - - Raises: - FileNotFoundError: If required files cannot be found. - ValueError: If files exist but cannot be properly loaded. + CalculationResults: Loaded instance. """ folder = pathlib.Path(folder) paths = fx_io.CalculationResultsPaths(folder, name) @@ -101,20 +141,13 @@ def from_file(cls, folder: Union[str, pathlib.Path], name: str) -> 'CalculationR @classmethod def from_calculation(cls, calculation: 'Calculation') -> 'CalculationResults': - """Create CalculationResults directly from a Calculation object. - - This method extracts the solution, flow system, and other relevant - information directly from an existing Calculation object. + """Create CalculationResults from a Calculation object. Args: - calculation: A Calculation object containing a solved model. + calculation: Calculation object with solved model. Returns: - CalculationResults: A new instance containing the results from - the provided calculation. - - Raises: - AttributeError: If the calculation doesn't have required attributes. + CalculationResults: New instance with extracted results. """ return cls( solution=calculation.model.solution, @@ -134,14 +167,16 @@ def __init__( folder: Optional[pathlib.Path] = None, model: Optional[linopy.Model] = None, ): - """ + """Initialize CalculationResults with optimization data. + Usually, this class is instantiated by the Calculation class, or by loading from file. + Args: - solution: The solution of the optimization. - flow_system: The flow_system that was used to create the calculation as a datatset. - name: The name of the calculation. - summary: Information about the calculation, - folder: The folder where the results are saved. - model: The linopy model that was used to solve the calculation. + solution: Optimization solution dataset. + flow_system: Flow system configuration dataset. + name: Calculation name. + summary: Calculation metadata. + folder: Results storage folder. + model: Linopy optimization model. """ self.solution = solution self.flow_system = flow_system @@ -173,24 +208,24 @@ def __getitem__(self, key: str) -> Union['ComponentResults', 'BusResults', 'Effe @property def storages(self) -> List['ComponentResults']: - """All storages in the results.""" + """Get all storage components in the results.""" return [comp for comp in self.components.values() if comp.is_storage] @property def objective(self) -> float: - """The objective result of the optimization.""" + """Get optimization objective value.""" return self.summary['Main Results']['Objective'] @property def variables(self) -> linopy.Variables: - """The variables of the optimization. Only available if the linopy.Model is available.""" + """Get optimization variables (requires linopy model).""" if self.model is None: raise ValueError('The linopy model is not available.') return self.model.variables @property def constraints(self) -> linopy.Constraints: - """The constraints of the optimization. Only available if the linopy.Model is available.""" + """Get optimization constraints (requires linopy model).""" if self.model is None: raise ValueError('The linopy model is not available.') return self.model.constraints @@ -198,13 +233,14 @@ def constraints(self) -> linopy.Constraints: def filter_solution( self, variable_dims: Optional[Literal['scalar', 'time']] = None, element: Optional[str] = None ) -> xr.Dataset: - """ - Filter the solution to a specific variable dimension and element. - If no element is specified, all elements are included. + """Filter solution by variable dimension and/or element. Args: - variable_dims: The dimension of the variables to filter for. - element: The element to filter for. + variable_dims: Variable dimension to filter ('scalar' or 'time'). + element: Element label to filter. + + Returns: + xr.Dataset: Filtered solution dataset. """ if element is not None: return filter_dataset(self[element].solution, variable_dims) @@ -242,8 +278,17 @@ def plot_network( ] = True, path: Optional[pathlib.Path] = None, show: bool = False, - ) -> 'pyvis.network.Network': - """See flixopt.flow_system.FlowSystem.plot_network""" + ) -> Optional['pyvis.network.Network']: + """Plot interactive network visualization of the system. + + Args: + controls: Enable/disable interactive controls. + path: Save path for network HTML. + show: Whether to display the plot. + + Returns: + pyvis.network.Network: Interactive network object. + """ try: from .flow_system import FlowSystem @@ -263,15 +308,14 @@ def to_file( document_model: bool = True, save_linopy_model: bool = False, ): - """ - Save the results to a file + """Save results to files. + Args: - folder: The folder where the results should be saved. Defaults to the folder of the calculation. - name: The name of the results file. If not provided, Defaults to the name of the calculation. - compression: The compression level to use when saving the solution file (0-9). 0 means no compression. - document_model: Wether to document the mathematical formulations in the model. - save_linopy_model: Wether to save the model to file. If True, the (linopy) model is saved as a .nc4 file. - The model file size is rougly 100 times larger than the solution file. + folder: Save folder (defaults to calculation folder). + name: File name (defaults to calculation name). + compression: Compression level 0-9. + document_model: Whether to document model formulations as yaml. + save_linopy_model: Whether to save linopy model file. """ folder = self.folder if folder is None else pathlib.Path(folder) name = self.name if name is None else name @@ -323,11 +367,10 @@ def __init__( @property def variables(self) -> linopy.Variables: - """ - Returns the variables of the element. + """Get element variables (requires linopy model). Raises: - ValueError: If the linopy model is not availlable. + ValueError: If linopy model is unavailable. """ if self._calculation_results.model is None: raise ValueError('The linopy model is not available.') @@ -335,22 +378,23 @@ def variables(self) -> linopy.Variables: @property def constraints(self) -> linopy.Constraints: - """ - Returns the variables of the element. + """Get element constraints (requires linopy model). Raises: - ValueError: If the linopy model is not availlable. + ValueError: If linopy model is unavailable. """ if self._calculation_results.model is None: raise ValueError('The linopy model is not available.') return self._calculation_results.model.constraints[self._constraint_names] def filter_solution(self, variable_dims: Optional[Literal['scalar', 'time']] = None) -> xr.Dataset: - """ - Filter the solution of the element by dimension. + """Filter element solution by dimension. Args: - variable_dims: The dimension of the variables to filter for. + variable_dims: Variable dimension to filter. + + Returns: + xr.Dataset: Filtered solution dataset. """ return filter_dataset(self.solution, variable_dims) @@ -387,12 +431,16 @@ def plot_node_balance( colors: plotting.ColorType = 'viridis', engine: plotting.PlottingEngine = 'plotly', ) -> Union[plotly.graph_objs.Figure, Tuple[plt.Figure, plt.Axes]]: - """ - Plots the node balance of the Component or Bus. + """Plot node balance flows. + Args: - save: Whether to save the plot or not. If a path is provided, the plot will be saved at that location. - show: Whether to show the plot or not. - engine: The engine to use for plotting. Can be either 'plotly' or 'matplotlib'. + save: Whether to save plot (path or boolean). + show: Whether to display plot. + colors: Color scheme. Also see plotly. + engine: Plotting engine ('plotly' or 'matplotlib'). + + Returns: + Figure object. """ if engine == 'plotly': figure_like = plotting.with_plotly( @@ -430,17 +478,16 @@ def plot_node_balance_pie( save: Union[bool, pathlib.Path] = False, show: bool = True, engine: plotting.PlottingEngine = 'plotly', - ) -> plotly.graph_objects.Figure: - """ - Plots a pie chart of the flow hours of the inputs and outputs of buses or components. + ) -> Union[plotly.graph_objects.Figure, Tuple[plt.Figure, List[plt.Axes]]]: + """Plot pie chart of flow hours distribution. Args: - colors: a colorscale or a list of colors to use for the plot - lower_percentage_group: The percentage of flow_hours that is grouped in "Others" (0...100) - text_info: What information to display on the pie plot - save: Whether to save the figure. - show: Whether to show the figure. - engine: Plotting engine to use. Only 'plotly' is implemented atm. + lower_percentage_group: Percentage threshold for "Others" grouping. + colors: Color scheme. Also see plotly. + text_info: Information to display on pie slices. + save: Whether to save plot. + show: Whether to display plot. + engine: Plotting engine ('plotly' or 'matplotlib'). """ inputs = ( sanitize_dataset( @@ -521,11 +568,11 @@ def node_balance( class BusResults(_NodeResults): - """Results for a Bus""" + """Results container for energy/material balance nodes in the system.""" class ComponentResults(_NodeResults): - """Results for a Component""" + """Results container for individual system components with specialized analysis tools.""" @property def is_storage(self) -> bool: @@ -537,7 +584,7 @@ def _charge_state(self) -> str: @property def charge_state(self) -> xr.DataArray: - """Get the solution of the charge state of the Storage.""" + """Get storage charge state solution.""" if not self.is_storage: raise ValueError(f'Cant get charge_state. "{self.label}" is not a storage') return self.solution[self._charge_state] @@ -549,16 +596,19 @@ def plot_charge_state( colors: plotting.ColorType = 'viridis', engine: plotting.PlottingEngine = 'plotly', ) -> plotly.graph_objs.Figure: - """ - Plots the charge state of a Storage. + """Plot storage charge state over time, combined with the node balance. + Args: - save: Whether to save the plot or not. If a path is provided, the plot will be saved at that location. - show: Whether to show the plot or not. - colors: The c - engine: Plotting engine to use. Only 'plotly' is implemented atm. + save: Whether to save plot. + show: Whether to display plot. + colors: Color scheme. Also see plotly. + engine: Plotting engine (only 'plotly' supported). + + Returns: + plotly.graph_objs.Figure: Charge state plot. Raises: - ValueError: If the Component is not a Storage. + ValueError: If component is not a storage. """ if engine != 'plotly': raise NotImplementedError( @@ -596,15 +646,18 @@ def plot_charge_state( def node_balance_with_charge_state( self, negate_inputs: bool = True, negate_outputs: bool = False, threshold: Optional[float] = 1e-5 ) -> xr.Dataset: - """ - Returns a dataset with the node balance of the Storage including its charge state. + """Get storage node balance including charge state. + Args: - negate_inputs: Whether to negate the inputs of the Storage. - negate_outputs: Whether to negate the outputs of the Storage. - threshold: The threshold for small values. + negate_inputs: Whether to negate input flows. + negate_outputs: Whether to negate output flows. + threshold: Threshold for small values. + + Returns: + xr.Dataset: Node balance with charge state. Raises: - ValueError: If the Component is not a Storage. + ValueError: If component is not a storage. """ if not self.is_storage: raise ValueError(f'Cant get charge_state. "{self.label}" is not a storage') @@ -629,13 +682,111 @@ class EffectResults(_ElementResults): """Results for an Effect""" def get_shares_from(self, element: str): - """Get the shares from an Element (without subelements) to the Effect""" + """Get effect shares from specific element. + + Args: + element: Element label to get shares from. + + Returns: + xr.Dataset: Element shares to this effect. + """ return self.solution[[name for name in self._variable_names if name.startswith(f'{element}->')]] class SegmentedCalculationResults: - """ - Class to store the results of a SegmentedCalculation. + """Results container for segmented optimization calculations with temporal decomposition. + + This class manages results from SegmentedCalculation runs where large optimization + problems are solved by dividing the time horizon into smaller, overlapping segments. + It provides unified access to results across all segments while maintaining the + ability to analyze individual segment behavior. + + Key Features: + **Unified Time Series**: Automatically assembles results from all segments into + continuous time series, removing overlaps and boundary effects + **Segment Analysis**: Access individual segment results for debugging and validation + **Consistency Checks**: Verify solution continuity at segment boundaries + **Memory Efficiency**: Handles large datasets that exceed single-segment memory limits + + Temporal Handling: + The class manages the complex task of combining overlapping segment solutions + into coherent time series, ensuring proper treatment of: + - Storage state continuity between segments + - Flow rate transitions at segment boundaries + - Aggregated results over the full time horizon + + Examples: + Load and analyze segmented results: + + ```python + # Load segmented calculation results + results = SegmentedCalculationResults.from_file('results', 'annual_segmented') + + # Access unified results across all segments + full_timeline = results.all_timesteps + total_segments = len(results.segment_results) + + # Analyze individual segments + for i, segment in enumerate(results.segment_results): + print(f'Segment {i + 1}: {len(segment.solution.time)} timesteps') + segment_costs = segment.effects['cost'].total_value + + # Check solution continuity at boundaries + segment_boundaries = results.get_boundary_analysis() + max_discontinuity = segment_boundaries['max_storage_jump'] + ``` + + Create from segmented calculation: + + ```python + # After running segmented calculation + segmented_calc = SegmentedCalculation( + name='annual_system', + flow_system=system, + timesteps_per_segment=730, # Monthly segments + overlap_timesteps=48, # 2-day overlap + ) + segmented_calc.do_modeling_and_solve(solver='gurobi') + + # Extract unified results + results = SegmentedCalculationResults.from_calculation(segmented_calc) + + # Save combined results + results.to_file(compression=5) + ``` + + Performance analysis across segments: + + ```python + # Compare segment solve times + solve_times = [seg.summary['durations']['solving'] for seg in results.segment_results] + avg_solve_time = sum(solve_times) / len(solve_times) + + # Verify solution quality consistency + segment_objectives = [seg.summary['objective_value'] for seg in results.segment_results] + + # Storage continuity analysis + if 'Battery' in results.segment_results[0].components: + storage_continuity = results.check_storage_continuity('Battery') + ``` + + Design Considerations: + **Boundary Effects**: Monitor solution quality at segment interfaces where + foresight is limited compared to full-horizon optimization. + + **Memory Management**: Individual segment results are maintained for detailed + analysis while providing unified access for system-wide metrics. + + **Validation Tools**: Built-in methods to verify temporal consistency and + identify potential issues from segmentation approach. + + Common Use Cases: + - **Large-Scale Analysis**: Annual or multi-year optimization results + - **Memory-Constrained Systems**: Results from systems exceeding hardware limits + - **Segment Validation**: Verifying segmentation approach effectiveness + - **Performance Monitoring**: Comparing segmented vs. full-horizon solutions + - **Debugging**: Identifying issues specific to temporal decomposition + """ @classmethod @@ -651,7 +802,15 @@ def from_calculation(cls, calculation: 'SegmentedCalculation'): @classmethod def from_file(cls, folder: Union[str, pathlib.Path], name: str): - """Create SegmentedCalculationResults directly from file""" + """Load SegmentedCalculationResults from saved files. + + Args: + folder: Directory containing saved files. + name: Base name of saved files. + + Returns: + SegmentedCalculationResults: Loaded instance. + """ folder = pathlib.Path(folder) path = folder / name nc_file = path.with_suffix('.nc4') @@ -700,7 +859,14 @@ def segment_names(self) -> List[str]: return [segment.name for segment in self.segment_results] def solution_without_overlap(self, variable_name: str) -> xr.DataArray: - """Returns the solution of a variable without overlapping timesteps""" + """Get variable solution removing segment overlaps. + + Args: + variable_name: Name of variable to extract. + + Returns: + xr.DataArray: Continuous solution without overlaps. + """ dataarrays = [ result.solution[variable_name].isel(time=slice(None, self.timesteps_per_segment)) for result in self.segment_results[:-1] @@ -717,17 +883,19 @@ def plot_heatmap( show: bool = True, engine: plotting.PlottingEngine = 'plotly', ) -> Union[plotly.graph_objs.Figure, Tuple[plt.Figure, plt.Axes]]: - """ - Plots a heatmap of the solution of a variable. + """Plot heatmap of variable solution across segments. Args: - variable_name: The name of the variable to plot. - heatmap_timeframes: The timeframes to use for the heatmap. - heatmap_timesteps_per_frame: The timesteps per frame to use for the heatmap. - color_map: The color map to use for the heatmap. - save: Whether to save the plot or not. If a path is provided, the plot will be saved at that location. - show: Whether to show the plot or not. - engine: The engine to use for plotting. Can be either 'plotly' or 'matplotlib'. + variable_name: Variable to plot. + heatmap_timeframes: Time aggregation level. + heatmap_timesteps_per_frame: Timesteps per frame. + color_map: Color scheme. Also see plotly. + save: Whether to save plot. + show: Whether to display plot. + engine: Plotting engine. + + Returns: + Figure object. """ return plot_heatmap( dataarray=self.solution_without_overlap(variable_name), @@ -744,7 +912,13 @@ def plot_heatmap( def to_file( self, folder: Optional[Union[str, pathlib.Path]] = None, name: Optional[str] = None, compression: int = 5 ): - """Save the results to a file""" + """Save segmented results to files. + + Args: + folder: Save folder (defaults to instance folder). + name: File name (defaults to instance name). + compression: Compression level 0-9. + """ folder = self.folder if folder is None else pathlib.Path(folder) name = self.name if name is None else name path = folder / name @@ -774,19 +948,21 @@ def plot_heatmap( show: bool = True, engine: plotting.PlottingEngine = 'plotly', ): - """ - Plots a heatmap of the solution of a variable. + """Plot heatmap of time series data. Args: - dataarray: The dataarray to plot. - name: The name of the variable to plot. - folder: The folder to save the plot to. - heatmap_timeframes: The timeframes to use for the heatmap. - heatmap_timesteps_per_frame: The timesteps per frame to use for the heatmap. - color_map: The color map to use for the heatmap. - save: Whether to save the plot or not. If a path is provided, the plot will be saved at that location. - show: Whether to show the plot or not. - engine: The engine to use for plotting. Can be either 'plotly' or 'matplotlib'. + dataarray: Data to plot. + name: Variable name for title. + folder: Save folder. + heatmap_timeframes: Time aggregation level. + heatmap_timesteps_per_frame: Timesteps per frame. + color_map: Color scheme. Also see plotly. + save: Whether to save plot. + show: Whether to display plot. + engine: Plotting engine. + + Returns: + Figure object. """ heatmap_data = plotting.heat_map_data_from_df( dataarray.to_dataframe(name), heatmap_timeframes, heatmap_timesteps_per_frame, 'ffill' @@ -825,19 +1001,18 @@ def sanitize_dataset( drop_small_vars: bool = True, zero_small_values: bool = False, ) -> xr.Dataset: - """ - Sanitizes a dataset by handling small values (dropping or zeroing) and optionally reindexing the time axis. + """Clean dataset by handling small values and reindexing time. Args: - ds: The dataset to sanitize. - timesteps: The timesteps to reindex the dataset to. If None, the original timesteps are kept. - threshold: The threshold for small values processing. If None, no processing is done. - negate: The variables to negate. If None, no variables are negated. - drop_small_vars: If True, drops variables where all values are below threshold. - zero_small_values: If True, sets values below threshold to zero. + ds: Dataset to sanitize. + timesteps: Time index for reindexing (optional). + threshold: Threshold for small values processing. + negate: Variables to negate. + drop_small_vars: Whether to drop variables below threshold. + zero_small_values: Whether to zero values below threshold. Returns: - xr.Dataset: The sanitized dataset. + xr.Dataset: Sanitized dataset. """ # Create a copy to avoid modifying the original ds = ds.copy() @@ -880,12 +1055,14 @@ def filter_dataset( ds: xr.Dataset, variable_dims: Optional[Literal['scalar', 'time']] = None, ) -> xr.Dataset: - """ - Filters a dataset by its dimensions. + """Filter dataset by variable dimensions. Args: - ds: The dataset to filter. - variable_dims: The dimension of the variables to filter for. + ds: Dataset to filter. + variable_dims: Variable dimension to filter ('scalar' or 'time'). + + Returns: + xr.Dataset: Filtered dataset. """ if variable_dims is None: return ds