From 19ad3df7ffd7a3d42d82bad371ab7f8266b5fe3c Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 12 Sep 2025 14:36:54 +0200 Subject: [PATCH 01/42] Update docstrings of Interfaces with claude --- flixopt/interface.py | 823 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 748 insertions(+), 75 deletions(-) diff --git a/flixopt/interface.py b/flixopt/interface.py index c38d6c619..5cbd06f45 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -20,14 +20,61 @@ @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 linear segment within a piecewise linear function. + + This class represents a single linear segment that forms part of a larger + piecewise linear relationship. Each Piece defines the domain boundaries + (start and end points) for one segment of the function, enabling the + modeling of complex non-linear relationships through linear approximations. + + Pieces are the fundamental building blocks for: + - Equipment efficiency curves that change with operating conditions + - Cost functions with different rates at different scales + - Conversion processes with variable rates across operating ranges + - Constraint relationships that vary by operating region + + Args: + start: The x-coordinate values marking the beginning of this linear segment. + These define the lower bound of the domain where this piece is active. + end: The x-coordinate values marking the end of this linear segment. + These define the upper bound of the domain where this piece is active. + + Examples: + Creating a piece for an efficiency curve segment: + + ```python + # Represents efficiency from 40% to 80% load + efficiency_piece = Piece(start=40, end=80) + ``` + + Multiple pieces forming a complete piecewise function: + + ```python + # Low efficiency at part load (0-50% load) + low_load_piece = Piece(start=0, end=50) + + # High efficiency at full load (50-100% load) + full_load_piece = Piece(start=50, end=100) + ``` + + Time-varying piece boundaries: + + ```python + # Operating range that changes with time (e.g., seasonal equipment limits) + seasonal_piece = Piece( + start=[10, 20, 30, 25], # Minimum capacity by season + end=[80, 100, 90, 70] # Maximum capacity by season + ) + ``` - Args: - start: The x-values of the piece. - end: The end of the piece. - """ + Note: + Pieces typically overlap at their boundaries (end of one piece = start of next) + to ensure continuity in the piecewise function. Gaps between pieces can be + used to model forbidden operating regions. + + """ + + def __init__(self, start: NumericData, end: NumericData): self.start = start self.end = end @@ -38,13 +85,87 @@ 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 composed of multiple linear segments. + + This class creates complex non-linear relationships by combining multiple + Piece objects into a single piecewise linear function. Each piece represents + a different linear segment that applies over a specific domain range, + allowing accurate approximation of curved relationships through linear + interpolation between breakpoints. + + Piecewise functions are essential for modeling: + - Equipment performance curves (efficiency vs load, capacity vs temperature) + - Economic relationships (marginal costs, economies of scale) + - Physical processes (heat transfer, pressure drop, reaction kinetics) + - Regulatory constraints (tiered pricing, emissions limits) + - Multi-modal behavior (different operating regimes) + + The class provides convenient iteration and indexing to access individual + pieces and integrates seamlessly with optimization solvers through data + transformation capabilities. + + Args: + pieces: List of Piece objects defining the linear segments of the function. + Pieces should typically be ordered by their domain (start values) and + may overlap at boundaries to ensure continuity. Gaps between pieces + can represent forbidden operating regions. + + Examples: + Heat pump COP (Coefficient of Performance) curve: + + ```python + cop_curve = Piecewise([ + Piece(start=0, end=25), # Low ambient temp: poor COP + Piece(start=25, end=50), # Moderate temp: better COP + Piece(start=50, end=75), # High temp: best COP + ]) + ``` + + Tiered electricity pricing: + + ```python + electricity_cost = Piecewise([ + Piece(start=0, end=100), # First 100 kWh: low rate + Piece(start=100, end=500), # Next 400 kWh: medium rate + Piece(start=500, end=1000), # Above 500 kWh: high rate + ]) + ``` + + Equipment with minimum load and forbidden range: + + ```python + turbine_operation = Piecewise([ + Piece(start=0, end=0), # Off state (point) + Piece(start=40, end=100), # Operating range (gap 0-40) + ]) + ``` + + Seasonal capacity variation: + + ```python + seasonal_capacity = Piecewise([ + Piece(start=[10, 15, 20, 12], end=[80, 90, 85, 75]), # By season + ]) + ``` + + Note: + The Piecewise class supports standard Python container operations: + + - Length: `len(piecewise)` returns number of pieces + - Indexing: `piecewise[i]` accesses the i-th piece + - Iteration: `for piece in piecewise:` loops over all pieces + + Common Use Cases: + - Power plant heat rate curves: fuel consumption vs electrical output + - HVAC equipment: capacity and efficiency vs outdoor temperature + - Industrial processes: conversion efficiency vs throughput + - Financial modeling: progressive tax rates, bulk pricing discounts + - Transportation: fuel consumption vs speed, load capacity vs distance + - Storage systems: charge/discharge efficiency vs state of charge - Args: - pieces: The pieces of the piecewise. - """ + """ + + def __init__(self, pieces: List[Piece]): self.pieces = pieces def __len__(self): @@ -63,15 +184,168 @@ def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): @register_class_for_io class PiecewiseConversion(Interface): + """Define piecewise linear conversion relationships between multiple flows. + + This class models complex conversion processes where the relationship between + input and output flows changes at different operating points, such as: + + - Variable efficiency equipment (heat pumps, engines, turbines) + - Multi-stage chemical processes with different conversion rates + - Equipment with discrete operating modes + - Systems with capacity constraints and thresholds + + Args: + piecewises: Dictionary mapping flow labels to their Piecewise conversion functions. + Keys are flow names (e.g., 'electricity_in', 'heat_out', 'fuel_consumed'). + Values are Piecewise objects defining conversion factors at different operating points. + All Piecewise objects must have the same number of pieces and compatible domains + to ensure consistent conversion relationships across operating ranges. + + Note: + Special modeling features: + + - **Gaps**: Express forbidden operating ranges by creating non-contiguous pieces. + Example: `[(0,50), (100,200)]` - cannot operate between 50-100 units + - **Points**: Express discrete operating points using pieces with identical start/end. + Example: `[(50,50), (100,100)]` - can only operate at exactly 50 or 100 units + + Examples: + Heat pump with variable COP (Coefficient of Performance): + + ```python + 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 output + Piece(35, 75), # High load COP=3.0: 35-75 kW heat output + ] + ), + } + ) + # At 15 kW electricity input → 52.5 kW heat output (interpolated) + ``` + + Engine with fuel consumption and emissions: + + ```python + PiecewiseConversion( + { + 'fuel_input': Piecewise( + [ + Piece(5, 15), # Part load: 5-15 L/h fuel + Piece(15, 30), # Full load: 15-30 L/h fuel + ] + ), + 'power_output': Piecewise( + [ + Piece(10, 25), # Part load: 10-25 kW output + Piece(25, 45), # Full load: 25-45 kW output + ] + ), + 'co2_emissions': Piecewise( + [ + Piece(12, 35), # Part load: 12-35 kg/h CO2 + Piece(35, 78), # Full load: 35-78 kg/h CO2 + ] + ), + } + ) + ``` + + Discrete operating modes (on/off equipment): + + ```python + PiecewiseConversion( + { + 'electricity_in': Piecewise( + [ + Piece(0, 0), # Off mode: no consumption + Piece(20, 20), # On mode: fixed 20 kW consumption + ] + ), + 'cooling_out': Piecewise( + [ + Piece(0, 0), # Off mode: no cooling + Piece(60, 60), # On mode: fixed 60 kW cooling + ] + ), + } + ) + ``` + + Equipment with forbidden operating range: + + ```python + PiecewiseConversion( + { + 'steam_input': Piecewise( + [ + Piece(0, 100), # Low pressure operation + Piece(200, 500), # High pressure (gap: 100-200) + ] + ), + 'power_output': Piecewise( + [ + Piece(0, 80), # Low efficiency at low pressure + Piece(180, 400), # High efficiency at high pressure + ] + ), + } + ) + ``` + + Multi-product chemical reactor: + + ```python + fx.PiecewiseConversion( + { + 'feedstock': fx.Piecewise( + [ + fx.Piece(10, 50), # Small batch: 10-50 kg/h + fx.Piece(50, 200), # Large batch: 50-200 kg/h + ] + ), + 'product_A': fx.Piecewise( + [ + fx.Piece(7, 32), # Small batch yield: 70% + fx.Piece(32, 140), # Large batch yield: 70% + ] + ), + 'product_B': fx.Piecewise( + [ + fx.Piece(2, 12), # Small batch: 20% to product B + fx.Piece(12, 45), # Large batch: better selectivity + ] + ), + 'waste': fx.Piecewise( + [ + fx.Piece(1, 6), # Small batch waste: 10% + fx.Piece(6, 15), # Large batch waste: 7.5% + ] + ), + } + ) + ``` + + Common Use Cases: + - Heat pumps/chillers: COP varies with load and ambient conditions + - Power plants: Heat rate curves showing fuel efficiency vs output + - Chemical reactors: Conversion rates and selectivity vs throughput + - Compressors/pumps: Power consumption vs flow rate + - Multi-stage processes: Different conversion rates per stage + - Equipment with minimum loads: Cannot operate below threshold + - Batch processes: Discrete production campaigns + + """ + 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)] - - Args: - piecewises: Dict of Piecewises defining the conversion factors. flow labels as keys, piecewise as values - """ self.piecewises = piecewises def items(self): @@ -84,14 +358,115 @@ 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 variable-dependent effects using piecewise linear functions. + + This class models complex relationships where the effects (costs, emissions, + resources) associated with a decision variable change non-linearly based on + the variable's value. The origin piecewise function defines the primary + variable's behavior, while the shares define how this variable contributes + to different system effects at different operating levels. + + This is particularly useful for modeling: + - Scale-dependent costs (bulk discounts, economies of scale) + - Load-dependent emissions (efficiency changes with throughput) + - Capacity-dependent resource consumption + - Multi-tier pricing structures + - Performance-dependent environmental impacts + + Args: + piecewise_origin: Piecewise function defining the behavior of the primary variable + that drives the effects. This establishes the domain and operating ranges. + piecewise_shares: Dictionary mapping effect names to their corresponding + Piecewise functions. Keys are effect identifiers (e.g., 'cost', 'CO2', 'water'). + Values are Piecewise objects defining how much of each effect is generated + per unit of the origin variable at different operating levels. + + Note: + **Implementation Status**: This functionality is not yet fully implemented + for non-scalar shares. Currently limited to scalar effect relationships. + + Examples: + Manufacturing process with scale-dependent costs and emissions: + + ```python + # Production volume affects unit costs and emissions + manufacturing_effects = PiecewiseEffects( + piecewise_origin=Piecewise([ + Piece(0, 1000), # Small scale production + Piece(1000, 5000), # Medium scale production + Piece(5000, 10000) # Large scale production + ]), + piecewise_shares={ + 'unit_cost': Piecewise([ + Piece(50, 45), # High unit cost at low volume + Piece(45, 35), # Decreasing cost with scale + Piece(35, 30) # Lowest cost at high volume + ]), + 'CO2_intensity': Piecewise([ + Piece(2.5, 2.0), # Higher emissions per unit at low efficiency + Piece(2.0, 1.5), # Better efficiency at medium scale + Piece(1.5, 1.2) # Best efficiency at large scale + ]) + } + ) + ``` + + Power plant with load-dependent heat rate and emissions: + + ```python + power_plant_effects = PiecewiseEffects( + piecewise_origin=Piecewise([ + Piece(100, 300), # Minimum load to rated capacity (MW) + Piece(300, 500) # Overload operation + ]), + piecewise_shares={ + 'fuel_rate': Piecewise([ + Piece(11.5, 10.2), # Heat rate: BTU/kWh (less efficient at part load) + Piece(10.2, 10.8) # Heat rate increases at overload + ]), + 'NOx_rate': Piecewise([ + Piece(0.15, 0.12), # NOx emissions: lb/MWh + Piece(0.12, 0.18) # Higher emissions at overload + ]) + } + ) + ``` + + Tiered water pricing with consumption-dependent rates: + + ```python + water_pricing = PiecewiseEffects( + piecewise_origin=Piecewise([ + Piece(0, 10), # Basic tier: 0-10 m³/month + Piece(10, 50), # Standard tier: 10-50 m³/month + Piece(50, 200) # High consumption: >50 m³/month + ]), + piecewise_shares={ + 'cost_per_m3': Piecewise([ + Piece(1.20, 1.20), # Flat rate for basic consumption + Piece(2.50, 2.50), # Higher rate for standard tier + Piece(4.00, 4.00) # Premium rate for high consumption + ]), + 'infrastructure_fee': Piecewise([ + Piece(0.10, 0.10), # Low infrastructure impact + Piece(0.25, 0.25), # Medium infrastructure impact + Piece(0.50, 0.50) # High infrastructure impact + ]) + } + ) + ``` + + Common Use Cases: + - Manufacturing: Scale economies, learning curves, capacity utilization effects + - Energy systems: Load-dependent efficiency, emissions, and maintenance costs + - Transportation: Distance-dependent rates, fuel efficiency curves + - Utilities: Tiered pricing, demand charges, infrastructure costs + - Environmental modeling: Threshold effects, cumulative impacts + - Financial instruments: Progressive rates, risk premiums - Args: - piecewise_origin: Piecewise of the related variable - piecewise_shares: Piecewise defining the shares to different Effects - """ + """ + + def __init__(self, piecewise_origin: Piecewise, piecewise_shares: Dict[str, Piecewise]): self.piecewise_origin = piecewise_origin self.piecewise_shares = piecewise_shares @@ -104,8 +479,180 @@ def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): @register_class_for_io class InvestParameters(Interface): - """ - collects arguments for invest-stuff + """Define comprehensive investment decision parameters for optimization models. + + This class encapsulates all parameters needed to model investment decisions + in optimization problems, including sizing constraints, cost structures, + and operational flexibility. It supports multiple cost modeling approaches + from simple linear relationships to complex piecewise functions, enabling + accurate representation of real-world investment economics. + + Investment modeling capabilities include: + - Fixed vs. continuous sizing decisions + - Multiple cost components (fixed, variable, piecewise) + - Optional investments with divestment penalties + - Technology learning curves and economies of scale + - Multi-period investment planning with proper annualization + + Args: + fixed_size: When specified, constrains the investment to exactly this size, + creating a binary invest/don't-invest decision. When None, allows + continuous sizing between minimum_size and maximum_size bounds. + minimum_size: Minimum investment size for continuous sizing decisions. + Defaults to CONFIG.modeling.EPSILON to avoid numerical issues. + Ignored when fixed_size is specified. + maximum_size: Maximum investment size for continuous sizing decisions. + Defaults to CONFIG.modeling.BIG to represent unlimited capacity. + Ignored when fixed_size is specified. + optional: Controls investment optionality. When True (default), the + optimization can choose not to invest. When False, forces investment + to occur, useful for mandatory infrastructure or replacement decisions. + fix_effects: Fixed costs incurred once if the investment is made, regardless + of the investment size. Typical examples include permitting costs, + connection fees, or base equipment costs. Dictionary mapping effect + names to scalar values (e.g., {'cost': 10000, 'CO2': 500}). + **Important**: Costs must be annualized to the optimization time period. + specific_effects: Variable costs proportional to investment size, representing + per-unit costs like €/kW_nominal or €/m²_nominal. Dictionary mapping + effect names to unit cost values (e.g., {'cost': 1200, 'CO2': 0.5}). + **Important**: Costs must be annualized to the optimization time period. + piecewise_effects: Complex non-linear cost relationships using PiecewiseEffects, + enabling modeling of bulk discounts, technology learning curves, or + economies of scale. Can be combined with fix_effects and specific_effects. + **Important**: Costs must be annualized to the optimization time period. + divest_effects: Costs incurred if the investment is NOT made, such as + demolition costs for existing equipment, contractual penalties, or + opportunity costs. Dictionary mapping effect names to scalar values. + + Note: + **Cost Annualization**: All cost values must be properly annualized to match + the optimization model's time period. For example, if modeling annual decisions + but equipment has a 20-year lifetime, divide capital costs by 20 or use + appropriate discount rates to convert to equivalent annual costs. + + 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 +666,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 +691,177 @@ def maximum_size(self): @register_class_for_io class OnOffParameters(Interface): + """Define binary operation constraints and costs for equipment with discrete states. + + This class models equipment that operates in discrete on/off states rather than + continuous operation, capturing the operational constraints and economic impacts + of binary decisions. It addresses real-world equipment behavior including + minimum run times, startup costs, cycling limitations, and maintenance schedules. + + Common applications include: + - Power plants with minimum load requirements and startup costs + - Industrial equipment with batch processes or discrete operating modes + - HVAC systems with thermostat control and equipment cycling + - Process equipment with startup/shutdown sequences + - Backup generators and emergency systems + - Maintenance scheduling and equipment availability + + 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 +875,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 From 8051bd94a3b747f149036f37d5ab13edde953e76 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 12 Sep 2025 18:33:58 +0200 Subject: [PATCH 02/42] Correct claude --- flixopt/interface.py | 34 +++++++++------------------------- 1 file changed, 9 insertions(+), 25 deletions(-) diff --git a/flixopt/interface.py b/flixopt/interface.py index 5cbd06f45..d64ccae2b 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -27,16 +27,10 @@ class Piece(Interface): (start and end points) for one segment of the function, enabling the modeling of complex non-linear relationships through linear approximations. - Pieces are the fundamental building blocks for: - - Equipment efficiency curves that change with operating conditions - - Cost functions with different rates at different scales - - Conversion processes with variable rates across operating ranges - - Constraint relationships that vary by operating region - Args: - start: The x-coordinate values marking the beginning of this linear segment. + start: Values marking the beginning of this linear segment. These define the lower bound of the domain where this piece is active. - end: The x-coordinate values marking the end of this linear segment. + end: Values marking the end of this linear segment. These define the upper bound of the domain where this piece is active. Examples: @@ -60,17 +54,18 @@ class Piece(Interface): Time-varying piece boundaries: ```python - # Operating range that changes with time (e.g., seasonal equipment limits) - seasonal_piece = Piece( - start=[10, 20, 30, 25], # Minimum capacity by season - end=[80, 100, 90, 70] # Maximum capacity by season + # Operating range that changes with time) + time_dependent_piece = Piece( + start=np.array([10, 20, 30, 25]), # Minimum capacity by timestep + end=np.array([80, 100, 90, 70]) # Maximum capacity by timestep ) ``` Note: - Pieces typically overlap at their boundaries (end of one piece = start of next) + Pieces typically "touch" at their boundaries (end of one piece = start of next) to ensure continuity in the piecewise function. Gaps between pieces can be used to model forbidden operating regions. + Overlapping Pieces effectively leave the decision on which piece is active open. """ @@ -85,7 +80,7 @@ def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): @register_class_for_io class Piecewise(Interface): - """Define a piecewise linear function composed of multiple linear segments. + """Define a piecewise linear function composed of `Pieces`. This class creates complex non-linear relationships by combining multiple Piece objects into a single piecewise linear function. Each piece represents @@ -93,17 +88,6 @@ class Piecewise(Interface): allowing accurate approximation of curved relationships through linear interpolation between breakpoints. - Piecewise functions are essential for modeling: - - Equipment performance curves (efficiency vs load, capacity vs temperature) - - Economic relationships (marginal costs, economies of scale) - - Physical processes (heat transfer, pressure drop, reaction kinetics) - - Regulatory constraints (tiered pricing, emissions limits) - - Multi-modal behavior (different operating regimes) - - The class provides convenient iteration and indexing to access individual - pieces and integrates seamlessly with optimization solvers through data - transformation capabilities. - Args: pieces: List of Piece objects defining the linear segments of the function. Pieces should typically be ordered by their domain (start values) and From 56b6e48cab27163b3e0ede8a48d3793f0f0fc8e7 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 12 Sep 2025 22:17:41 +0200 Subject: [PATCH 03/42] Update Effect docstring --- flixopt/effects.py | 43 ++++++++++++++++++++----------------------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/flixopt/effects.py b/flixopt/effects.py index 911ec55eb..a0871b2de 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -26,8 +26,26 @@ @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 + Effect represents impacts like costs, CO2 emissions, area usage, etc. + Components, Flows, and other system elements can contribute to an Effect. One Effect is chosen as the Objective of the Optimization. + + Args: + label: The label of the Element. Used to identify it in the FlowSystem + unit: The unit of effect, e.g. €, kg_CO2, kWh_primaryEnergy + description: The descriptive name + is_standard: True, if Standard-Effect (for direct input of value without effect dictionary), else False + is_objective: True, if optimization target + specific_share_to_other_effects_operation: Effect contributions from operation, e.g. 180 €/t_CO2, input as {costs: 180} + specific_share_to_other_effects_invest: Effect contributions from investment, e.g. 180 €/t_CO2, input as {costs: 180} + minimum_operation: Minimal sum (operation only) of the effect + maximum_operation: Maximal sum (operation only) of the effect + minimum_operation_per_hour: Min. value per hour (operation only) for each timestep + maximum_operation_per_hour: Max. value per hour (operation only) for each timestep + minimum_invest: Minimal sum (investment only) of the effect + maximum_invest: Maximal sum (investment only) 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. """ def __init__( @@ -49,27 +67,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 From 93e2b66de51658dcf26c096c40e2ceba29938395 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 12 Sep 2025 22:20:03 +0200 Subject: [PATCH 04/42] Update Bus docstring --- flixopt/elements.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/flixopt/elements.py b/flixopt/elements.py index a0bd8c91f..667c4df7f 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -92,20 +92,21 @@ 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 represents nodal balances between the flow rates. + A Bus has incoming and outgoing Flows, and is the connection point of + energy carriers (electricity, heat, gas, etc.) or materials flows in between different Components. + + 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. """ 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] = [] From d0b4ce4d0ab64a6f21b7874a6a4d7019216482a8 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 12 Sep 2025 22:20:21 +0200 Subject: [PATCH 05/42] Update Flow docstring --- flixopt/elements.py | 59 ++++++++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 30 deletions(-) diff --git a/flixopt/elements.py b/flixopt/elements.py index 667c4df7f..25a9d5044 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -149,6 +149,35 @@ 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**. + + 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: Label 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. """ def __init__( @@ -168,36 +197,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 From 8efa9f06c3a78b11f479b9dca12cb2caf06708ac Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 12 Sep 2025 22:22:19 +0200 Subject: [PATCH 06/42] Update Storage docstring --- flixopt/components.py | 50 +++++++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/flixopt/components.py b/flixopt/components.py index e06616daf..c91ed7b54 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -120,7 +120,30 @@ 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 an efficiency factor. + They maintain a charge state that represents the stored amount, bounded by capacity limits. + The charge state evolves based on charging, discharging, and losses over time. + + For mathematical details see class StorageModel + + Args: + label: The label of the Element. Used to identify it in the FlowSystem + charging: Ingoing flow for loading the storage + discharging: Outgoing flow for unloading the storage + 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. """ def __init__( @@ -140,31 +163,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, From 6e9ff4d4ef4b3a1f9d63d0862c33ff2a123d56fe Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 12 Sep 2025 22:23:09 +0200 Subject: [PATCH 07/42] Update Source docstring --- flixopt/components.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/flixopt/components.py b/flixopt/components.py index c91ed7b54..7b7da8a5e 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -666,6 +666,18 @@ 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, or renewable energy sources. + + Args: + label: The label of the Element. Used to identify it in the FlowSystem + outputs: Output-flows from the 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. + prevent_simultaneous_flow_rates: If True, only one output flow can be active at a time + """ + def __init__( self, label: str, @@ -674,12 +686,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( From cfd3682c4dd3ed056d919b169ab9ecafd67f47a0 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 12 Sep 2025 22:23:23 +0200 Subject: [PATCH 08/42] Update Sink docstring --- flixopt/components.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/flixopt/components.py b/flixopt/components.py index 7b7da8a5e..86d0c5fbc 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -712,6 +712,18 @@ 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, or material consumption. + + Args: + label: The label of the Element. Used to identify it in the FlowSystem + inputs: Input-flows into the sink + meta_data: Used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types. + prevent_simultaneous_flow_rates: If True, only one input flow can be active at a time + """ + def __init__( self, label: str, @@ -720,12 +732,6 @@ def __init__( prevent_simultaneous_flow_rates: bool = False, **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. - """ sink = kwargs.pop('sink', None) if sink is not None: warnings.warn( From c315b8344a9070f51c0479626d1a647d067401ac Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 12 Sep 2025 22:25:36 +0200 Subject: [PATCH 09/42] Update Calculation docstrings --- flixopt/calculation.py | 39 ++++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/flixopt/calculation.py b/flixopt/calculation.py index c7367cad2..500c55d3c 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -131,7 +131,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: @@ -190,7 +193,24 @@ 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 problemand 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 """ def __init__( @@ -202,21 +222,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 From ba23ff3a3f437cb29c0b4626267e958980889d76 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 12 Sep 2025 22:26:19 +0200 Subject: [PATCH 10/42] Update TimeSeriesData docstrings --- flixopt/core.py | 43 +++++++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/flixopt/core.py b/flixopt/core.py index fa1d51803..f32514eac 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -97,28 +97,31 @@ 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 From fea5198130b568f9b476fc14b2de055507bcdb67 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 12 Sep 2025 22:27:32 +0200 Subject: [PATCH 11/42] Update FlowSystem docstrings --- flixopt/flow_system.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 19f8a240f..64ed5a5de 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -29,7 +29,17 @@ 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! """ def __init__( @@ -38,15 +48,6 @@ def __init__( hours_of_last_timestep: Optional[float] = None, 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! - """ self.time_series_collection = TimeSeriesCollection( timesteps=timesteps, hours_of_last_timestep=hours_of_last_timestep, From d05c4aee7463db27f4038f89ded8c8c83bd06af6 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 12 Sep 2025 22:53:06 +0200 Subject: [PATCH 12/42] ruff format and lint --- flixopt/calculation.py | 4 +- flixopt/components.py | 18 +- flixopt/core.py | 7 +- flixopt/effects.py | 2 +- flixopt/elements.py | 4 +- flixopt/flow_system.py | 2 +- flixopt/interface.py | 378 ++++++++++++++++++++++------------------- 7 files changed, 226 insertions(+), 189 deletions(-) diff --git a/flixopt/calculation.py b/flixopt/calculation.py index 64e14743f..22577ef59 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -128,7 +128,7 @@ def summary(self): class FullCalculation(Calculation): """ 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. """ @@ -190,7 +190,7 @@ def _activate_time_series(self): class AggregatedCalculation(FullCalculation): """ 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. diff --git a/flixopt/components.py b/flixopt/components.py index 984e5c710..37f1da8b9 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -125,13 +125,13 @@ class Storage(Component): Storages have one incoming and one outgoing Flow each with an efficiency factor. They maintain a charge state that represents the stored amount, bounded by capacity limits. The charge state evolves based on charging, discharging, and losses over time. - + For mathematical details see class StorageModel - + Args: label: The label of the Element. Used to identify it in the FlowSystem charging: Ingoing flow for loading the storage - discharging: Outgoing flow for unloading the storage + discharging: Outgoing flow for unloading the storage 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 @@ -668,16 +668,16 @@ def prevent_simultaneous_sink_and_source(self) -> bool: class Source(Component): """ A Source generates or provides energy or material flows into the system. - + Sources represent supply points like power plants, fuel suppliers, or renewable energy sources. - + Args: label: The label of the Element. Used to identify it in the FlowSystem outputs: Output-flows from the 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. prevent_simultaneous_flow_rates: If True, only one output flow can be active at a time """ - + def __init__( self, label: str, @@ -719,16 +719,16 @@ def source(self) -> Flow: class Sink(Component): """ A Sink consumes energy or material flows from the system. - + Sinks represent demand points like electrical loads, heat demands, or material consumption. - + Args: label: The label of the Element. Used to identify it in the FlowSystem inputs: Input-flows into the sink meta_data: Used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types. prevent_simultaneous_flow_rates: If True, only one input flow can be active at a time """ - + def __init__( self, label: str, diff --git a/flixopt/core.py b/flixopt/core.py index 90dcecb37..8b4be7ea3 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -103,10 +103,10 @@ 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 @@ -115,7 +115,7 @@ class TimeSeriesData: 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. @@ -124,6 +124,7 @@ class TimeSeriesData: 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): self.data = data diff --git a/flixopt/effects.py b/flixopt/effects.py index d076bf332..19cbeaa4a 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -28,7 +28,7 @@ class Effect(Element): """ Effect represents impacts like costs, CO2 emissions, area usage, etc. Components, Flows, and other system elements can contribute to an Effect. One Effect is chosen as the Objective of the Optimization. - + Args: label: The label of the Element. Used to identify it in the FlowSystem unit: The unit of effect, e.g. €, kg_CO2, kWh_primaryEnergy diff --git a/flixopt/elements.py b/flixopt/elements.py index 452c048e3..283d0cef9 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -95,7 +95,7 @@ class Bus(Element): Buses represents nodal balances between the flow rates. A Bus has incoming and outgoing Flows, and is the connection point of energy carriers (electricity, heat, gas, etc.) or materials flows in between different Components. - + 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) @@ -149,7 +149,7 @@ 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**. - + 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: Label of the bus the flow is connected to. diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 51956b727..e4887053c 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -30,7 +30,7 @@ class FlowSystem: """ 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: diff --git a/flixopt/interface.py b/flixopt/interface.py index d64ccae2b..1b240bc4b 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -46,8 +46,8 @@ class Piece(Interface): ```python # Low efficiency at part load (0-50% load) low_load_piece = Piece(start=0, end=50) - - # High efficiency at full load (50-100% load) + + # High efficiency at full load (50-100% load) full_load_piece = Piece(start=50, end=100) ``` @@ -57,7 +57,7 @@ class Piece(Interface): # Operating range that changes with time) time_dependent_piece = Piece( start=np.array([10, 20, 30, 25]), # Minimum capacity by timestep - end=np.array([80, 100, 90, 70]) # Maximum capacity by timestep + end=np.array([80, 100, 90, 70]), # Maximum capacity by timestep ) ``` @@ -68,7 +68,7 @@ class Piece(Interface): Overlapping Pieces effectively leave the decision on which piece is active open. """ - + def __init__(self, start: NumericData, end: NumericData): self.start = start self.end = end @@ -98,57 +98,65 @@ class Piecewise(Interface): Heat pump COP (Coefficient of Performance) curve: ```python - cop_curve = Piecewise([ - Piece(start=0, end=25), # Low ambient temp: poor COP - Piece(start=25, end=50), # Moderate temp: better COP - Piece(start=50, end=75), # High temp: best COP - ]) + cop_curve = Piecewise( + [ + Piece(start=0, end=25), # Low ambient temp: poor COP + Piece(start=25, end=50), # Moderate temp: better COP + Piece(start=50, end=75), # High temp: best COP + ] + ) ``` Tiered electricity pricing: ```python - electricity_cost = Piecewise([ - Piece(start=0, end=100), # First 100 kWh: low rate - Piece(start=100, end=500), # Next 400 kWh: medium rate - Piece(start=500, end=1000), # Above 500 kWh: high rate - ]) + electricity_cost = Piecewise( + [ + Piece(start=0, end=100), # First 100 kWh: low rate + Piece(start=100, end=500), # Next 400 kWh: medium rate + Piece(start=500, end=1000), # Above 500 kWh: high rate + ] + ) ``` Equipment with minimum load and forbidden range: ```python - turbine_operation = Piecewise([ - Piece(start=0, end=0), # Off state (point) - Piece(start=40, end=100), # Operating range (gap 0-40) - ]) + turbine_operation = Piecewise( + [ + Piece(start=0, end=0), # Off state (point) + Piece(start=40, end=100), # Operating range (gap 0-40) + ] + ) ``` Seasonal capacity variation: ```python - seasonal_capacity = Piecewise([ - Piece(start=[10, 15, 20, 12], end=[80, 90, 85, 75]), # By season - ]) + seasonal_capacity = Piecewise( + [ + Piece(start=[10, 15, 20, 12], end=[80, 90, 85, 75]), # By season + ] + ) ``` Note: The Piecewise class supports standard Python container operations: - + - Length: `len(piecewise)` returns number of pieces - Indexing: `piecewise[i]` accesses the i-th piece - Iteration: `for piece in piecewise:` loops over all pieces Common Use Cases: - Power plant heat rate curves: fuel consumption vs electrical output - - HVAC equipment: capacity and efficiency vs outdoor temperature + - HVAC equipment: capacity and efficiency vs outdoor temperature - Industrial processes: conversion efficiency vs throughput - Financial modeling: progressive tax rates, bulk pricing discounts - Transportation: fuel consumption vs speed, load capacity vs distance - Storage systems: charge/discharge efficiency vs state of charge """ - + def __init__(self, pieces: List[Piece]): self.pieces = pieces @@ -328,7 +336,7 @@ class PiecewiseConversion(Interface): - Batch processes: Discrete production campaigns """ - + def __init__(self, piecewises: Dict[str, Piecewise]): self.piecewises = piecewises @@ -366,7 +374,7 @@ class PiecewiseEffects(Interface): per unit of the origin variable at different operating levels. Note: - **Implementation Status**: This functionality is not yet fully implemented + **Implementation Status**: This functionality is not yet fully implemented for non-scalar shares. Currently limited to scalar effect relationships. Examples: @@ -375,23 +383,29 @@ class PiecewiseEffects(Interface): ```python # Production volume affects unit costs and emissions manufacturing_effects = PiecewiseEffects( - piecewise_origin=Piecewise([ - Piece(0, 1000), # Small scale production - Piece(1000, 5000), # Medium scale production - Piece(5000, 10000) # Large scale production - ]), + piecewise_origin=Piecewise( + [ + Piece(0, 1000), # Small scale production + Piece(1000, 5000), # Medium scale production + Piece(5000, 10000), # Large scale production + ] + ), piecewise_shares={ - 'unit_cost': Piecewise([ - Piece(50, 45), # High unit cost at low volume - Piece(45, 35), # Decreasing cost with scale - Piece(35, 30) # Lowest cost at high volume - ]), - 'CO2_intensity': Piecewise([ - Piece(2.5, 2.0), # Higher emissions per unit at low efficiency - Piece(2.0, 1.5), # Better efficiency at medium scale - Piece(1.5, 1.2) # Best efficiency at large scale - ]) - } + 'unit_cost': Piecewise( + [ + Piece(50, 45), # High unit cost at low volume + Piece(45, 35), # Decreasing cost with scale + Piece(35, 30), # Lowest cost at high volume + ] + ), + 'CO2_intensity': Piecewise( + [ + Piece(2.5, 2.0), # Higher emissions per unit at low efficiency + Piece(2.0, 1.5), # Better efficiency at medium scale + Piece(1.5, 1.2), # Best efficiency at large scale + ] + ), + }, ) ``` @@ -399,20 +413,26 @@ class PiecewiseEffects(Interface): ```python power_plant_effects = PiecewiseEffects( - piecewise_origin=Piecewise([ - Piece(100, 300), # Minimum load to rated capacity (MW) - Piece(300, 500) # Overload operation - ]), + piecewise_origin=Piecewise( + [ + Piece(100, 300), # Minimum load to rated capacity (MW) + Piece(300, 500), # Overload operation + ] + ), piecewise_shares={ - 'fuel_rate': Piecewise([ - Piece(11.5, 10.2), # Heat rate: BTU/kWh (less efficient at part load) - Piece(10.2, 10.8) # Heat rate increases at overload - ]), - 'NOx_rate': Piecewise([ - Piece(0.15, 0.12), # NOx emissions: lb/MWh - Piece(0.12, 0.18) # Higher emissions at overload - ]) - } + 'fuel_rate': Piecewise( + [ + Piece(11.5, 10.2), # Heat rate: BTU/kWh (less efficient at part load) + Piece(10.2, 10.8), # Heat rate increases at overload + ] + ), + 'NOx_rate': Piecewise( + [ + Piece(0.15, 0.12), # NOx emissions: lb/MWh + Piece(0.12, 0.18), # Higher emissions at overload + ] + ), + }, ) ``` @@ -420,23 +440,29 @@ class PiecewiseEffects(Interface): ```python water_pricing = PiecewiseEffects( - piecewise_origin=Piecewise([ - Piece(0, 10), # Basic tier: 0-10 m³/month - Piece(10, 50), # Standard tier: 10-50 m³/month - Piece(50, 200) # High consumption: >50 m³/month - ]), + piecewise_origin=Piecewise( + [ + Piece(0, 10), # Basic tier: 0-10 m³/month + Piece(10, 50), # Standard tier: 10-50 m³/month + Piece(50, 200), # High consumption: >50 m³/month + ] + ), piecewise_shares={ - 'cost_per_m3': Piecewise([ - Piece(1.20, 1.20), # Flat rate for basic consumption - Piece(2.50, 2.50), # Higher rate for standard tier - Piece(4.00, 4.00) # Premium rate for high consumption - ]), - 'infrastructure_fee': Piecewise([ - Piece(0.10, 0.10), # Low infrastructure impact - Piece(0.25, 0.25), # Medium infrastructure impact - Piece(0.50, 0.50) # High infrastructure impact - ]) - } + 'cost_per_m3': Piecewise( + [ + Piece(1.20, 1.20), # Flat rate for basic consumption + Piece(2.50, 2.50), # Higher rate for standard tier + Piece(4.00, 4.00), # Premium rate for high consumption + ] + ), + 'infrastructure_fee': Piecewise( + [ + Piece(0.10, 0.10), # Low infrastructure impact + Piece(0.25, 0.25), # Medium infrastructure impact + Piece(0.50, 0.50), # High infrastructure impact + ] + ), + }, ) ``` @@ -449,7 +475,7 @@ class PiecewiseEffects(Interface): - Financial instruments: Progressive rates, risk premiums """ - + def __init__(self, piecewise_origin: Piecewise, piecewise_shares: Dict[str, Piecewise]): self.piecewise_origin = piecewise_origin self.piecewise_shares = piecewise_shares @@ -474,7 +500,7 @@ class InvestParameters(Interface): Investment modeling capabilities include: - Fixed vs. continuous sizing decisions - Multiple cost components (fixed, variable, piecewise) - - Optional investments with divestment penalties + - Optional investments with divestment penalties - Technology learning curves and economies of scale - Multi-period investment planning with proper annualization @@ -522,13 +548,13 @@ class InvestParameters(Interface): fixed_size=100, # 100 kW system (binary decision) optional=True, fix_effects={ - 'cost': 25000, # Installation and permitting costs - 'CO2': -50000 # Avoided emissions over lifetime + '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 - } + 'cost': 1200, # €1200/kW for panels (annualized) + 'CO2': -800, # kg CO2 avoided per kW annually + }, ) ``` @@ -536,27 +562,31 @@ class InvestParameters(Interface): ```python battery_investment = InvestParameters( - minimum_size=10, # Minimum viable system size (kWh) - maximum_size=1000, # Maximum installable capacity + 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 + '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_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 - ]) - } - ) + '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 + ] + ) + }, + ), ) ``` @@ -568,17 +598,17 @@ class InvestParameters(Interface): maximum_size=200, optional=True, # Can choose not to replace fix_effects={ - 'cost': 15000, # Installation costs - 'disruption': 3 # Days of downtime + 'cost': 15000, # Installation costs + 'disruption': 3, # Days of downtime }, specific_effects={ - 'cost': 400, # €400/kW capacity - 'maintenance': 25 # Annual maintenance per kW + 'cost': 400, # €400/kW capacity + 'maintenance': 25, # Annual maintenance per kW }, divest_effects={ - 'cost': 8000, # Demolition if not replaced - 'environmental': 100 # Disposal fees - } + 'cost': 8000, # Demolition if not replaced + 'environmental': 100, # Disposal fees + }, ) ``` @@ -589,15 +619,15 @@ class InvestParameters(Interface): gas_turbine = InvestParameters( fixed_size=50, # MW fix_effects={'cost': 2500000, 'CO2': 1250000}, - specific_effects={'fuel_cost': 45, 'maintenance': 12} + specific_effects={'fuel_cost': 45, 'maintenance': 12}, ) - - # Wind farm option + + # 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} + specific_effects={'cost': 1800000, 'land_use': 0.5}, ) ``` @@ -608,30 +638,36 @@ class InvestParameters(Interface): 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_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 - ]) - } - ) + '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 + - 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 @@ -684,7 +720,7 @@ class OnOffParameters(Interface): Common applications include: - Power plants with minimum load requirements and startup costs - - Industrial equipment with batch processes or discrete operating modes + - Industrial equipment with batch processes or discrete operating modes - HVAC systems with thermostat control and equipment cycling - Process equipment with startup/shutdown sequences - Backup generators and emergency systems @@ -736,18 +772,18 @@ class OnOffParameters(Interface): ```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 + '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 + '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_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 + on_hours_total_max=6000, # Annual operating limit ) ``` @@ -756,20 +792,20 @@ class OnOffParameters(Interface): ```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 + '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 + '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_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 + switch_on_total_max=200, # Maximum 200 batches per year + on_hours_total_max=4000, # Maximum production time ) ``` @@ -778,18 +814,18 @@ class OnOffParameters(Interface): ```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 + '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 + '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 + 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 ) ``` @@ -798,20 +834,20 @@ class OnOffParameters(Interface): ```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 + '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 + '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) + 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) ) ``` @@ -820,19 +856,19 @@ class OnOffParameters(Interface): ```python battery_cycling = OnOffParameters( effects_per_switch_on={ - 'cycle_degradation': 0.01, # % capacity loss per cycle - 'inverter_startup': 0.5 # kWh losses during startup + '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 + '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 + 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 ) ``` @@ -840,12 +876,12 @@ class OnOffParameters(Interface): - 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 + - 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, From 781da07d38034c949de123b367b97d86ac0e7207 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Fri, 12 Sep 2025 23:22:31 +0200 Subject: [PATCH 13/42] =?UTF-8?q?=F0=9F=93=9D=20Add=20docstrings=20to=20`f?= =?UTF-8?q?eature/312-improve-and-move-docstrings`=20(#317)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 📝 Add docstrings to `feature/312-improve-and-move-docstrings` Docstrings generation was requested by @FBumann. * https://github.com/flixOpt/flixopt/pull/316#issuecomment-3286800236 The following files were modified: * `flixopt/calculation.py` * `flixopt/components.py` * `flixopt/core.py` * `flixopt/effects.py` * `flixopt/elements.py` * `flixopt/flow_system.py` * `flixopt/interface.py` * ruff format and lint * Manual updates * Manual updates --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: FBumann <117816358+FBumann@users.noreply.github.com> --- flixopt/calculation.py | 1 + flixopt/components.py | 33 +++++++++++++++++++++++++++++++++ flixopt/effects.py | 4 ++++ flixopt/elements.py | 11 +++++++++++ flixopt/flow_system.py | 17 +++++++++++++++++ flixopt/interface.py | 12 ++++++++++++ 6 files changed, 78 insertions(+) diff --git a/flixopt/calculation.py b/flixopt/calculation.py index 22577ef59..de62f47f9 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -207,6 +207,7 @@ class AggregatedCalculation(FullCalculation): 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__( diff --git a/flixopt/components.py b/flixopt/components.py index 37f1da8b9..1df7d7b92 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -580,6 +580,19 @@ def relative_charge_state_bounds(self) -> Tuple[NumericData, NumericData]: class SourceAndSink(Component): """ class for source (output-flow) and sink (input-flow) in one commponent + A SourceAndSink consumes AND provides energy or material flows from and to the system. + + Sources can represent markets where energy or material can be bought or sold. + + Args: + label: The label of the Element. Used to identify it in the FlowSystem + inputs: Input-flows into the SourceAndSink + outputs: Output-flows from the SourceAndSink + prevent_simultaneous_flow_rates: If True, only one output flow can be active at a time + meta_data: Used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types. + + Deprecated: + The deprecated `sink` and `source` kwargs are accepted for compatibility but will be removed in future releases. """ def __init__( @@ -676,6 +689,9 @@ class Source(Component): outputs: Output-flows from the 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. prevent_simultaneous_flow_rates: If True, only one output flow can be active at a time + + Deprecated: + The deprecated `source` kwarg is accepted for compatibility but will be removed in future releases. """ def __init__( @@ -727,6 +743,9 @@ class Sink(Component): inputs: Input-flows into the sink meta_data: Used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types. prevent_simultaneous_flow_rates: If True, only one input flow can be active at a time + + Deprecated: + The deprecated `sink` kwarg is accepted for compatibility but will be removed in future releases. """ def __init__( @@ -737,6 +756,20 @@ def __init__( prevent_simultaneous_flow_rates: bool = False, **kwargs, ): + """ + 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: warnings.warn( diff --git a/flixopt/effects.py b/flixopt/effects.py index 19cbeaa4a..8ed82a24c 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -46,6 +46,10 @@ class Effect(Element): 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. + + Notes: + - Bounds may be None to indicate unbounded in that direction. + - The unit of the effect is only informative and does not affect the optimization. """ def __init__( diff --git a/flixopt/elements.py b/flixopt/elements.py index 283d0cef9..1b444b11e 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -102,6 +102,10 @@ class Bus(Element): (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. + + Notes: + The constructor also initializes empty `inputs` and `outputs` lists for connected Flow objects. + The registration of connections is handled automatically by the FlowSystem. """ def __init__( @@ -150,6 +154,13 @@ class Flow(Element): 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**. + Notes: + - If `size` is None, a large default (CONFIG.modeling.BIG) is used. + - If `previous_flow_rate` is provided as a list, it is converted to a NumPy array. + + Deprecated: + - Passing a Bus object to `bus` is deprecated. Pass the bus label string instead. + 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: Label of the bus the flow is connected to. diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index e4887053c..657d30f87 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -40,6 +40,10 @@ class FlowSystem: 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__( @@ -48,6 +52,19 @@ def __init__( hours_of_last_timestep: Optional[float] = None, hours_of_previous_timesteps: Optional[Union[int, float, np.ndarray]] = None, ): + """ + 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, hours_of_last_timestep=hours_of_last_timestep, diff --git a/flixopt/interface.py b/flixopt/interface.py index 1b240bc4b..78a50f181 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -161,6 +161,12 @@ 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: @@ -341,6 +347,12 @@ 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): From c9e88d927857a463cd64c4eaa3d96e2c6a25969d Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 12 Sep 2025 23:42:33 +0200 Subject: [PATCH 14/42] Add docstring for LinearConverter --- flixopt/components.py | 140 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 124 insertions(+), 16 deletions(-) diff --git a/flixopt/components.py b/flixopt/components.py index 1df7d7b92..f82ffad7b 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 From d8d35756a2e6db7a23c3cd7d43056a2d0a8ba696 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 12 Sep 2025 23:50:52 +0200 Subject: [PATCH 15/42] Add docstring for Storage --- flixopt/components.py | 145 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 126 insertions(+), 19 deletions(-) diff --git a/flixopt/components.py b/flixopt/components.py index f82ffad7b..529b9cc2d 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -230,28 +230,135 @@ class Storage(Component): """ A Storage models the temporary storage and release of energy or material. - Storages have one incoming and one outgoing Flow each with an efficiency factor. - They maintain a charge state that represents the stored amount, bounded by capacity limits. - The charge state evolves based on charging, discharging, and losses over time. + 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. - For mathematical details see class StorageModel + 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: Ingoing flow for loading the storage - discharging: Outgoing flow for unloading the storage - 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. + 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__( From c98f5d33c4525e7ab3dc2f3b58dfd4466ef8f324 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 12 Sep 2025 23:51:56 +0200 Subject: [PATCH 16/42] Improve docstring of PiecewiseEffects --- flixopt/interface.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/flixopt/interface.py b/flixopt/interface.py index 78a50f181..3439b8cb5 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -386,8 +386,10 @@ class PiecewiseEffects(Interface): per unit of the origin variable at different operating levels. Note: - **Implementation Status**: This functionality is not yet fully implemented - for non-scalar shares. Currently limited to scalar effect relationships. + **Difference from PiecewiseConversion**: While PiecewiseConversion models + relationships between flow variables, PiecewiseEffects models how a single + decision variable contributes to multiple system effects (costs, emissions, etc.) + based on its operating level. Examples: Manufacturing process with scale-dependent costs and emissions: From aa61d8be3587621a3f449b4bb0cde75fa8f40910 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 13 Sep 2025 00:01:07 +0200 Subject: [PATCH 17/42] Improve docstring of Transmission --- flixopt/components.py | 129 +++++++++++++++++++++++++++++++++++------- 1 file changed, 107 insertions(+), 22 deletions(-) diff --git a/flixopt/components.py b/flixopt/components.py index 529b9cc2d..e2c4fcf9c 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -465,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, @@ -484,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], From 34a9263e1100606cbc46d694fe999e6da5dc6e9f Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 13 Sep 2025 00:02:55 +0200 Subject: [PATCH 18/42] Improve docstring of Sink and Source --- flixopt/components.py | 139 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 129 insertions(+), 10 deletions(-) diff --git a/flixopt/components.py b/flixopt/components.py index e2c4fcf9c..7dfb2ceb9 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -982,13 +982,72 @@ class Source(Component): """ A Source generates or provides energy or material flows into the system. - Sources represent supply points like power plants, fuel suppliers, or renewable energy sources. + 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 - meta_data: Used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types. - prevent_simultaneous_flow_rates: If True, only one output flow can be active at a time + 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 + costs={'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. @@ -1036,13 +1095,73 @@ class Sink(Component): """ A Sink consumes energy or material flows from the system. - Sinks represent demand points like electrical loads, heat demands, or material consumption. + 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 - meta_data: Used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types. - prevent_simultaneous_flow_rates: If True, only one input flow can be active at a time + 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 + costs={'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. From 60ca7f52e4267bc8bf79a7e427cad28bbc1402a9 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 13 Sep 2025 00:05:06 +0200 Subject: [PATCH 19/42] Improve docstring of Bus --- flixopt/elements.py | 61 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 50 insertions(+), 11 deletions(-) diff --git a/flixopt/elements.py b/flixopt/elements.py index 1b444b11e..fc9e91159 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -92,20 +92,59 @@ def _plausibility_checks(self) -> None: @register_class_for_io class Bus(Element): """ - Buses represents nodal balances between the flow rates. - A Bus has incoming and outgoing Flows, and is the connection point of - energy carriers (electricity, heat, gas, etc.) or materials flows in between different Components. + 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: 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. + 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 + ) + ``` - Notes: - The constructor also initializes empty `inputs` and `outputs` lists for connected Flow objects. - The registration of connections is handled automatically by the FlowSystem. + 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__( From 4920d330347f21893d78834522e652bbc48c801d Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 13 Sep 2025 00:09:24 +0200 Subject: [PATCH 20/42] Improve docstring of linear_converters.py --- flixopt/linear_converters.py | 197 +++++++++++++++++++++++++++++------ 1 file changed, 168 insertions(+), 29 deletions(-) diff --git a/flixopt/linear_converters.py b/flixopt/linear_converters.py index 3fd032632..cd854043c 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], @@ -101,6 +143,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 +206,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], @@ -187,6 +274,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 +348,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} From d9c2c52707209ad406e13b5f1f28a14cb11a9bd2 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 13 Sep 2025 00:15:59 +0200 Subject: [PATCH 21/42] Improve docstring of linear_converters.py --- flixopt/linear_converters.py | 204 ++++++++++++++++++++++++++++++----- 1 file changed, 175 insertions(+), 29 deletions(-) diff --git a/flixopt/linear_converters.py b/flixopt/linear_converters.py index cd854043c..7f3169c7d 100644 --- a/flixopt/linear_converters.py +++ b/flixopt/linear_converters.py @@ -101,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, @@ -110,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], @@ -230,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, @@ -239,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], @@ -387,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, @@ -397,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} From e9d3d63630d654a807b26cf4e71bc6cd6a4dd605 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 13 Sep 2025 00:25:11 +0200 Subject: [PATCH 22/42] Improve docstring of SourceAndSink --- flixopt/components.py | 91 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 75 insertions(+), 16 deletions(-) diff --git a/flixopt/components.py b/flixopt/components.py index 7dfb2ceb9..17f780cc9 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -879,17 +879,84 @@ 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 consumes AND provides energy or material flows from and to the system. + A SourceAndSink combines both supply and demand capabilities in a single component. - Sources can represent markets where energy or material can be bought or sold. + 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 - outputs: Output-flows from the SourceAndSink - prevent_simultaneous_flow_rates: If True, only one output flow can be active at a time - meta_data: Used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types. + 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. @@ -904,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) From a1f0db74696b2bf24f9c69118c2e1759eb70524e Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 13 Sep 2025 00:29:02 +0200 Subject: [PATCH 23/42] Improve docstring of Effect --- flixopt/effects.py | 121 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 99 insertions(+), 22 deletions(-) diff --git a/flixopt/effects.py b/flixopt/effects.py index 8ed82a24c..57b038dde 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -26,30 +26,107 @@ @register_class_for_io class Effect(Element): """ - Effect represents impacts like costs, CO2 emissions, area usage, etc. - Components, Flows, and other system elements 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 effect, e.g. €, kg_CO2, kWh_primaryEnergy - description: The descriptive name - is_standard: True, if Standard-Effect (for direct input of value without effect dictionary), else False - is_objective: True, if optimization target - specific_share_to_other_effects_operation: Effect contributions from operation, e.g. 180 €/t_CO2, input as {costs: 180} - specific_share_to_other_effects_invest: Effect contributions from investment, e.g. 180 €/t_CO2, input as {costs: 180} - minimum_operation: Minimal sum (operation only) of the effect - maximum_operation: Maximal sum (operation only) of the effect - minimum_operation_per_hour: Min. value per hour (operation only) for each timestep - maximum_operation_per_hour: Max. value per hour (operation only) for each timestep - minimum_invest: Minimal sum (investment only) of the effect - maximum_invest: Maximal sum (investment only) 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. - - Notes: - - Bounds may be None to indicate unbounded in that direction. - - The unit of the effect is only informative and does not affect the optimization. + 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__( From f0b4ab42acf417142179c7c6852e40acb5f8d818 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 13 Sep 2025 00:30:37 +0200 Subject: [PATCH 24/42] Improve docstring of Component --- flixopt/elements.py | 60 +++++++++++++++++++++++++++++++-------------- 1 file changed, 42 insertions(+), 18 deletions(-) diff --git a/flixopt/elements.py b/flixopt/elements.py index fc9e91159..304f49247 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 [] From fc2f9a4914a2c503cf22519f30a76ce50b631192 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 13 Sep 2025 00:57:44 +0200 Subject: [PATCH 25/42] Typos --- flixopt/calculation.py | 2 +- flixopt/interface.py | 8 ++++---- flixopt/plotting.py | 4 ++-- flixopt/results.py | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/flixopt/calculation.py b/flixopt/calculation.py index de62f47f9..8e24b0693 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -196,7 +196,7 @@ class AggregatedCalculation(FullCalculation): Note: The quality of the solution depends on the choice of aggregation parameters. - The optimal parameters depend on the specific problemand the characteristics of the time series data. + 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: diff --git a/flixopt/interface.py b/flixopt/interface.py index 3439b8cb5..65a5e4286 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -942,22 +942,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""" + """Determines whether the OFF Variable is needed or not""" 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 off 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/plotting.py b/flixopt/plotting.py index e4c440aaf..c6512e4dd 100644 --- a/flixopt/plotting.py +++ b/flixopt/plotting.py @@ -519,8 +519,8 @@ def heat_map_plotly( color_map: The color scale to use for the heatmap. Default is 'viridis'. Plotly supports various color scales like 'Cividis', 'Inferno', etc. categorical_labels: If True, the x and y axes are treated as categorical data (i.e., the index and columns will not be interpreted as continuous data). Default is True. If False, the axes are treated as continuous, which may be useful for time series or numeric data. - show: Wether to show the figure after creation. (This includes saving the figure) - save: Wether to save the figure after creation (without showing) + show: Whether to show the figure after creation. (This includes saving the figure) + save: Whether to save the figure after creation (without showing) path: Path to save the figure. Returns: diff --git a/flixopt/results.py b/flixopt/results.py index 223e3708e..7f2adf04d 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -269,8 +269,8 @@ def to_file( 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. + document_model: Whether to document the mathematical formulations in the model. + save_linopy_model: Whether 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 = self.folder if folder is None else pathlib.Path(folder) From 00776bf60191d7e4b64e947f2b3fe9da20b7df2b Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 13 Sep 2025 19:05:56 +0200 Subject: [PATCH 26/42] Improve Piece Docstring --- flixopt/interface.py | 51 ++++++++++++++++++++------------------------ 1 file changed, 23 insertions(+), 28 deletions(-) diff --git a/flixopt/interface.py b/flixopt/interface.py index 65a5e4286..5644dd2bd 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -20,52 +20,47 @@ @register_class_for_io class Piece(Interface): - """Define a linear segment within a piecewise linear function. + """Define a single linear segment with specified domain boundaries. - This class represents a single linear segment that forms part of a larger - piecewise linear relationship. Each Piece defines the domain boundaries - (start and end points) for one segment of the function, enabling the - modeling of complex non-linear relationships through linear approximations. + 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: Values marking the beginning of this linear segment. - These define the lower bound of the domain where this piece is active. - end: Values marking the end of this linear segment. - These define the upper bound of the domain where this piece is active. + 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: - Creating a piece for an efficiency curve segment: + Basic piece for equipment efficiency curve: ```python - # Represents efficiency from 40% to 80% load - efficiency_piece = Piece(start=40, end=80) + # Single segment from 40% to 80% load + efficiency_segment = Piece(start=40, end=80) ``` - Multiple pieces forming a complete piecewise function: + Piece with time-varying boundaries: ```python - # Low efficiency at part load (0-50% load) - low_load_piece = Piece(start=0, end=50) - - # High efficiency at full load (50-100% load) - full_load_piece = Piece(start=50, end=100) + # 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 + ) ``` - Time-varying piece boundaries: + Fixed operating point (start equals end): ```python - # Operating range that changes with time) - time_dependent_piece = Piece( - start=np.array([10, 20, 30, 25]), # Minimum capacity by timestep - end=np.array([80, 100, 90, 70]), # Maximum capacity by timestep - ) + # Equipment that operates at exactly 50 MW + fixed_output = Piece(start=50, end=50) ``` Note: - Pieces typically "touch" at their boundaries (end of one piece = start of next) - to ensure continuity in the piecewise function. Gaps between pieces can be - used to model forbidden operating regions. - Overlapping Pieces effectively leave the decision on which piece is active open. + 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. """ From 7e1601ed3d66ed515517b53edda7db0c035af330 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 13 Sep 2025 19:10:52 +0200 Subject: [PATCH 27/42] Improve Piecewise Docstring --- flixopt/interface.py | 102 ++++++++++++++++++++++++++++--------------- 1 file changed, 68 insertions(+), 34 deletions(-) diff --git a/flixopt/interface.py b/flixopt/interface.py index 5644dd2bd..dc8f1df4f 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -75,52 +75,74 @@ def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): @register_class_for_io class Piecewise(Interface): - """Define a piecewise linear function composed of `Pieces`. + """Define a piecewise linear function by combining multiple `Piece`s together. This class creates complex non-linear relationships by combining multiple - Piece objects into a single piecewise linear function. Each piece represents - a different linear segment that applies over a specific domain range, - allowing accurate approximation of curved relationships through linear - interpolation between breakpoints. + Piece objects into a single piecewise linear function. Args: - pieces: List of Piece objects defining the linear segments of the function. - Pieces should typically be ordered by their domain (start values) and - may overlap at boundaries to ensure continuity. Gaps between pieces - can represent forbidden operating regions. + 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: - Heat pump COP (Coefficient of Performance) curve: + Continuous efficiency curve (touching pieces): ```python - cop_curve = Piecewise( + efficiency_curve = Piecewise( [ - Piece(start=0, end=25), # Low ambient temp: poor COP - Piece(start=25, end=50), # Moderate temp: better COP - Piece(start=50, end=75), # High temp: best COP + 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) ] ) ``` - Tiered electricity pricing: + Equipment with forbidden operating range (gap): ```python - electricity_cost = Piecewise( + turbine_operation = Piecewise( [ - Piece(start=0, end=100), # First 100 kWh: low rate - Piece(start=100, end=500), # Next 400 kWh: medium rate - Piece(start=500, end=1000), # Above 500 kWh: high rate + Piece(start=0, end=0), # Off state (point operation) + Piece(start=40, end=100), # Operating range (gap: 0-40 forbidden) ] ) ``` - Equipment with minimum load and forbidden range: + Flexible operation with overlapping options: ```python - turbine_operation = Piecewise( + flexible_operation = Piecewise( [ - Piece(start=0, end=0), # Off state (point) - Piece(start=40, end=100), # Operating range (gap 0-40) + 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 ] ) ``` @@ -130,25 +152,37 @@ class Piecewise(Interface): ```python seasonal_capacity = Piecewise( [ - Piece(start=[10, 15, 20, 12], end=[80, 90, 85, 75]), # By season + Piece(start=[10, 15, 20, 12], end=[80, 90, 85, 75]), # Varies by time ] ) ``` - Note: + Container Operations: The Piecewise class supports standard Python container operations: - - Length: `len(piecewise)` returns number of pieces - - Indexing: `piecewise[i]` accesses the i-th piece - - Iteration: `for piece in piecewise:` loops over all pieces + ```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 plant heat rate curves: fuel consumption vs electrical output - - HVAC equipment: capacity and efficiency vs outdoor temperature - - Industrial processes: conversion efficiency vs throughput - - Financial modeling: progressive tax rates, bulk pricing discounts - - Transportation: fuel consumption vs speed, load capacity vs distance - - Storage systems: charge/discharge efficiency vs state of charge + - 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 """ From 9fc5b5109dbceab1b9fd4585d27700b07534dd53 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 13 Sep 2025 19:14:26 +0200 Subject: [PATCH 28/42] Improve PiecewiseConversion Docstring --- flixopt/interface.py | 180 +++++++++++++++++++++++++------------------ 1 file changed, 105 insertions(+), 75 deletions(-) diff --git a/flixopt/interface.py b/flixopt/interface.py index dc8f1df4f..a08ef92f5 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -211,36 +211,37 @@ def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): @register_class_for_io class PiecewiseConversion(Interface): - """Define piecewise linear conversion relationships between multiple flows. + """Define coordinated piecewise linear relationships between multiple flows. - This class models complex conversion processes where the relationship between - input and output flows changes at different operating points, such as: + 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. - - Variable efficiency equipment (heat pumps, engines, turbines) - - Multi-stage chemical processes with different conversion rates - - Equipment with discrete operating modes - - Systems with capacity constraints and thresholds + 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 conversion functions. - Keys are flow names (e.g., 'electricity_in', 'heat_out', 'fuel_consumed'). - Values are Piecewise objects defining conversion factors at different operating points. - All Piecewise objects must have the same number of pieces and compatible domains - to ensure consistent conversion relationships across operating ranges. - - Note: - Special modeling features: - - - **Gaps**: Express forbidden operating ranges by creating non-contiguous pieces. - Example: `[(0,50), (100,200)]` - cannot operate between 50-100 units - - **Points**: Express discrete operating points using pieces with identical start/end. - Example: `[(50,50), (100,100)]` - can only operate at exactly 50 or 100 units + 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 variable COP (Coefficient of Performance): + Heat pump with coordinated efficiency changes: ```python - PiecewiseConversion( + heat_pump_pc = PiecewiseConversion( { 'electricity_in': Piecewise( [ @@ -250,125 +251,154 @@ class PiecewiseConversion(Interface): ), 'heat_out': Piecewise( [ - Piece(0, 35), # Low load COP=3.5: 0-35 kW heat output - Piece(35, 75), # High load COP=3.0: 35-75 kW heat output + 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 input → 52.5 kW heat output (interpolated) + # At 15 kW electricity → 52.5 kW heat + 3.75 m³/h cooling water ``` - Engine with fuel consumption and emissions: + Combined cycle power plant with synchronized flows: ```python - PiecewiseConversion( + power_plant_pc = PiecewiseConversion( { - 'fuel_input': Piecewise( + '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(5, 15), # Part load: 5-15 L/h fuel - Piece(15, 30), # Full load: 15-30 L/h fuel + Piece(60, 135), # Part load: 60-135 MW_e (45% efficiency) + Piece(135, 250), # Full load: 135-250 MW_e (50% efficiency) ] ), - 'power_output': Piecewise( + 'steam_export': Piecewise( [ - Piece(10, 25), # Part load: 10-25 kW output - Piece(25, 45), # Full load: 25-45 kW output + Piece(20, 35), # Part load: 20-35 MW_th steam + Piece(35, 50), # Full load: 35-50 MW_th steam ] ), 'co2_emissions': Piecewise( [ - Piece(12, 35), # Part load: 12-35 kg/h CO2 - Piece(35, 78), # Full load: 35-78 kg/h CO2 + Piece(30, 60), # Part load: 30-60 t/h CO2 + Piece(60, 100), # Full load: 60-100 t/h CO2 ] ), } ) ``` - Discrete operating modes (on/off equipment): + Chemical reactor with multiple products and waste: ```python - PiecewiseConversion( + reactor_pc = PiecewiseConversion( { - 'electricity_in': Piecewise( + 'feedstock': Piecewise( [ - Piece(0, 0), # Off mode: no consumption - Piece(20, 20), # On mode: fixed 20 kW consumption + 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 ] ), - 'cooling_out': Piecewise( + 'product_B': Piecewise( [ - Piece(0, 0), # Off mode: no cooling - Piece(60, 60), # On mode: fixed 60 kW cooling + 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 forbidden operating range: + Equipment with discrete operating modes: ```python - PiecewiseConversion( + compressor_pc = PiecewiseConversion( { - 'steam_input': Piecewise( + 'electricity': Piecewise( [ - Piece(0, 100), # Low pressure operation - Piece(200, 500), # High pressure (gap: 100-200) + Piece(0, 0), # Off mode: no consumption + Piece(45, 45), # Low mode: fixed 45 kW + Piece(85, 85), # High mode: fixed 85 kW ] ), - 'power_output': Piecewise( + 'compressed_air': Piecewise( [ - Piece(0, 80), # Low efficiency at low pressure - Piece(180, 400), # High efficiency at high pressure + Piece(0, 0), # Off mode: no production + Piece(250, 250), # Low mode: 250 Nm³/h + Piece(500, 500), # High mode: 500 Nm³/h ] ), } ) ``` - Multi-product chemical reactor: + Equipment with forbidden operating range: ```python - fx.PiecewiseConversion( + steam_turbine_pc = PiecewiseConversion( { - 'feedstock': fx.Piecewise( + 'steam_in': Piecewise( [ - fx.Piece(10, 50), # Small batch: 10-50 kg/h - fx.Piece(50, 200), # Large batch: 50-200 kg/h - ] - ), - 'product_A': fx.Piecewise( - [ - fx.Piece(7, 32), # Small batch yield: 70% - fx.Piece(32, 140), # Large batch yield: 70% + Piece(0, 100), # Low pressure operation + Piece(200, 500), # High pressure (gap: 100-200 forbidden) ] ), - 'product_B': fx.Piecewise( + 'electricity_out': Piecewise( [ - fx.Piece(2, 12), # Small batch: 20% to product B - fx.Piece(12, 45), # Large batch: better selectivity + Piece(0, 30), # Low pressure: poor efficiency + Piece(80, 220), # High pressure: good efficiency ] ), - 'waste': fx.Piecewise( + 'condensate_out': Piecewise( [ - fx.Piece(1, 6), # Small batch waste: 10% - fx.Piece(6, 15), # Large batch waste: 7.5% + 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: - - Heat pumps/chillers: COP varies with load and ambient conditions - - Power plants: Heat rate curves showing fuel efficiency vs output - - Chemical reactors: Conversion rates and selectivity vs throughput - - Compressors/pumps: Power consumption vs flow rate - - Multi-stage processes: Different conversion rates per stage - - Equipment with minimum loads: Cannot operate below threshold - - Batch processes: Discrete production campaigns + - 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 """ From 46737ac027fa43859aec8703c185c54fe3209b32 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 13 Sep 2025 19:16:54 +0200 Subject: [PATCH 29/42] Improve PiecewiseEffects Docstring --- flixopt/interface.py | 186 ++++++++++++++++++++++++++++--------------- 1 file changed, 123 insertions(+), 63 deletions(-) diff --git a/flixopt/interface.py b/flixopt/interface.py index a08ef92f5..f8d45a48b 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -421,131 +421,191 @@ def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): @register_class_for_io class PiecewiseEffects(Interface): - """Define variable-dependent effects using piecewise linear functions. + """Define how a single decision variable contributes to system effects with piecewise rates. - This class models complex relationships where the effects (costs, emissions, - resources) associated with a decision variable change non-linearly based on - the variable's value. The origin piecewise function defines the primary - variable's behavior, while the shares define how this variable contributes - to different system effects at different operating levels. + 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. - This is particularly useful for modeling: - - Scale-dependent costs (bulk discounts, economies of scale) - - Load-dependent emissions (efficiency changes with throughput) - - Capacity-dependent resource consumption - - Multi-tier pricing structures - - Performance-dependent environmental impacts + 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 - Args: - piecewise_origin: Piecewise function defining the behavior of the primary variable - that drives the effects. This establishes the domain and operating ranges. - piecewise_shares: Dictionary mapping effect names to their corresponding - Piecewise functions. Keys are effect identifiers (e.g., 'cost', 'CO2', 'water'). - Values are Piecewise objects defining how much of each effect is generated - per unit of the origin variable at different operating levels. + Relationship to PiecewiseConversion: + **PiecewiseConversion**: Models synchronized relationships between multiple + flow variables (e.g., fuel_in, electricity_out, emissions_out all coordinated). - Note: - **Difference from PiecewiseConversion**: While PiecewiseConversion models - relationships between flow variables, PiecewiseEffects models how a single - decision variable contributes to multiple system effects (costs, emissions, etc.) - based on its operating level. + **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 process with scale-dependent costs and emissions: + Manufacturing with economies of scale: ```python - # Production volume affects unit costs and emissions - manufacturing_effects = PiecewiseEffects( + production_effects = PiecewiseEffects( piecewise_origin=Piecewise( [ - Piece(0, 1000), # Small scale production - Piece(1000, 5000), # Medium scale production - Piece(5000, 10000), # Large scale production + 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), # High unit cost at low volume - Piece(45, 35), # Decreasing cost with scale - Piece(35, 30), # Lowest cost at high volume + 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(2.5, 2.0), # Higher emissions per unit at low efficiency - Piece(2.0, 1.5), # Better efficiency at medium scale - Piece(1.5, 1.2), # Best efficiency at large scale + 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 plant with load-dependent heat rate and emissions: + Power generation with load-dependent characteristics: ```python - power_plant_effects = PiecewiseEffects( + generator_effects = PiecewiseEffects( piecewise_origin=Piecewise( [ - Piece(100, 300), # Minimum load to rated capacity (MW) - Piece(300, 500), # Overload operation + 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(11.5, 10.2), # Heat rate: BTU/kWh (less efficient at part load) - Piece(10.2, 10.8), # Heat rate increases at overload + 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.15, 0.12), # NOx emissions: lb/MWh - Piece(0.12, 0.18), # Higher emissions at overload + 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) ] ), }, ) ``` - Tiered water pricing with consumption-dependent rates: + Progressive utility pricing structure: ```python - water_pricing = PiecewiseEffects( + electricity_billing = PiecewiseEffects( piecewise_origin=Piecewise( [ - Piece(0, 10), # Basic tier: 0-10 m³/month - Piece(10, 50), # Standard tier: 10-50 m³/month - Piece(50, 200), # High consumption: >50 m³/month + 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={ - 'cost_per_m3': Piecewise( + 'energy_rate': Piecewise( [ - Piece(1.20, 1.20), # Flat rate for basic consumption - Piece(2.50, 2.50), # Higher rate for standard tier - Piece(4.00, 4.00), # Premium rate for high consumption + 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 ] ), - 'infrastructure_fee': Piecewise( + 'carbon_tax': Piecewise( [ - Piece(0.10, 0.10), # Low infrastructure impact - Piece(0.25, 0.25), # Medium infrastructure impact - Piece(0.50, 0.50), # High infrastructure impact + 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: Scale economies, learning curves, capacity utilization effects - - Energy systems: Load-dependent efficiency, emissions, and maintenance costs - - Transportation: Distance-dependent rates, fuel efficiency curves - - Utilities: Tiered pricing, demand charges, infrastructure costs - - Environmental modeling: Threshold effects, cumulative impacts - - Financial instruments: Progressive rates, risk premiums + - 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 """ From 59c13c238c00174e620794c5b6598c36c9802c51 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 13 Sep 2025 19:54:48 +0200 Subject: [PATCH 30/42] Improve InvestParameters Docstring --- flixopt/interface.py | 84 +++++++++++++++++++++++--------------------- 1 file changed, 44 insertions(+), 40 deletions(-) diff --git a/flixopt/interface.py b/flixopt/interface.py index f8d45a48b..44dd8dd61 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -622,56 +622,60 @@ def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): @register_class_for_io class InvestParameters(Interface): - """Define comprehensive investment decision parameters for optimization models. + """Define investment decision parameters with flexible sizing and effect modeling. - This class encapsulates all parameters needed to model investment decisions - in optimization problems, including sizing constraints, cost structures, - and operational flexibility. It supports multiple cost modeling approaches - from simple linear relationships to complex piecewise functions, enabling - accurate representation of real-world investment economics. + 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 modeling capabilities include: - - Fixed vs. continuous sizing decisions - - Multiple cost components (fixed, variable, piecewise) - - Optional investments with divestment penalties - - Technology learning curves and economies of scale - - Multi-period investment planning with proper annualization + 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, constrains the investment to exactly this size, - creating a binary invest/don't-invest decision. When None, allows - continuous sizing between minimum_size and maximum_size bounds. - minimum_size: Minimum investment size for continuous sizing decisions. - Defaults to CONFIG.modeling.EPSILON to avoid numerical issues. + 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: Maximum investment size for continuous sizing decisions. - Defaults to CONFIG.modeling.BIG to represent unlimited capacity. + 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 investment optionality. When True (default), the + optional: Controls whether investment is required. When True (default), optimization can choose not to invest. When False, forces investment - to occur, useful for mandatory infrastructure or replacement decisions. - fix_effects: Fixed costs incurred once if the investment is made, regardless - of the investment size. Typical examples include permitting costs, - connection fees, or base equipment costs. Dictionary mapping effect - names to scalar values (e.g., {'cost': 10000, 'CO2': 500}). - **Important**: Costs must be annualized to the optimization time period. + 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 like €/kW_nominal or €/m²_nominal. Dictionary mapping - effect names to unit cost values (e.g., {'cost': 1200, 'CO2': 0.5}). - **Important**: Costs must be annualized to the optimization time period. - piecewise_effects: Complex non-linear cost relationships using PiecewiseEffects, - enabling modeling of bulk discounts, technology learning curves, or - economies of scale. Can be combined with fix_effects and specific_effects. - **Important**: Costs must be annualized to the optimization time period. + 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 costs for existing equipment, contractual penalties, or - opportunity costs. Dictionary mapping effect names to scalar values. + demolition of existing equipment, contractual penalties, or lost opportunities. + Dictionary mapping effect names to values. - Note: - **Cost Annualization**: All cost values must be properly annualized to match - the optimization model's time period. For example, if modeling annual decisions - but equipment has a 20-year lifetime, divide capital costs by 20 or use - appropriate discount rates to convert to equivalent annual costs. + 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): From 539e3fd71493447eeff65e93cf3d9029d62c10fc Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 13 Sep 2025 19:56:12 +0200 Subject: [PATCH 31/42] Improve OnOffParameters Docstring --- flixopt/interface.py | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/flixopt/interface.py b/flixopt/interface.py index 44dd8dd61..5e5b3a63e 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -848,20 +848,25 @@ def maximum_size(self): @register_class_for_io class OnOffParameters(Interface): - """Define binary operation constraints and costs for equipment with discrete states. - - This class models equipment that operates in discrete on/off states rather than - continuous operation, capturing the operational constraints and economic impacts - of binary decisions. It addresses real-world equipment behavior including - minimum run times, startup costs, cycling limitations, and maintenance schedules. - - Common applications include: - - Power plants with minimum load requirements and startup costs - - Industrial equipment with batch processes or discrete operating modes - - HVAC systems with thermostat control and equipment cycling - - Process equipment with startup/shutdown sequences - - Backup generators and emergency systems - - Maintenance scheduling and equipment availability + """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 From 9635bcac5af07641970e1223c599a59aec2e10c7 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 13 Sep 2025 20:09:02 +0200 Subject: [PATCH 32/42] Improve Flow Docstring --- flixopt/elements.py | 169 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 135 insertions(+), 34 deletions(-) diff --git a/flixopt/elements.py b/flixopt/elements.py index 304f49247..81ccdaa2a 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -213,45 +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: - - If `size` is None, a large default (CONFIG.modeling.BIG) is used. - - If `previous_flow_rate` is provided as a list, it is converted to a NumPy array. + - 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 a Bus object to `bus` is deprecated. Pass the bus label string instead. + Passing Bus objects to `bus` parameter. Use bus label strings instead. - 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: Label 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. """ def __init__( From cad324f012f62dba7e5ac1b31e9b61ed54a07e26 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 13 Sep 2025 20:16:53 +0200 Subject: [PATCH 33/42] Improve SegmentedCalculation --- flixopt/calculation.py | 127 +++++++++++++++++++++++++++++++++++------ 1 file changed, 108 insertions(+), 19 deletions(-) diff --git a/flixopt/calculation.py b/flixopt/calculation.py index 8e24b0693..4096bac6f 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -295,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, @@ -304,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 From 2eeebd1484c6ca144d7078b6018fc4791667f3c2 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 13 Sep 2025 20:22:38 +0200 Subject: [PATCH 34/42] Improve results.py and plotting.py --- flixopt/plotting.py | 149 +++++++++++++++++++++++++++----- flixopt/results.py | 202 +++++++++++++++++++++++++++++++++++++------- 2 files changed, 300 insertions(+), 51 deletions(-) diff --git a/flixopt/plotting.py b/flixopt/plotting.py index c6512e4dd..258d6eed7 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 @@ -38,13 +57,53 @@ 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 +111,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 7f2adf04d..8b93df983 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -26,36 +26,83 @@ 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 @@ -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: @@ -634,8 +681,99 @@ def get_shares_from(self, element: str): 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 From 7d8bc4e0014dbd778356342af5162b30aea40111 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 13 Sep 2025 20:39:17 +0200 Subject: [PATCH 35/42] Improve results.py methods --- flixopt/results.py | 280 ++++++++++++++++++++++++++------------------- 1 file changed, 161 insertions(+), 119 deletions(-) diff --git a/flixopt/results.py b/flixopt/results.py index 8b93df983..53acf9ff9 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -107,21 +107,14 @@ class CalculationResults: @classmethod def from_file(cls, folder: Union[str, pathlib.Path], name: str): - """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) @@ -148,20 +141,13 @@ def from_file(cls, folder: Union[str, pathlib.Path], name: str): @classmethod def from_calculation(cls, calculation: 'Calculation'): - """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, @@ -181,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 @@ -220,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 @@ -245,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) @@ -290,7 +279,16 @@ def plot_network( path: Optional[pathlib.Path] = None, show: bool = False, ) -> 'pyvis.network.Network': - """See flixopt.flow_system.FlowSystem.plot_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 @@ -310,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: Whether to document the mathematical formulations in the model. - save_linopy_model: Whether 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 @@ -370,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.') @@ -382,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) @@ -434,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( @@ -478,16 +479,18 @@ def plot_node_balance_pie( 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. + """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 (only 'plotly' supported). + + Returns: + plotly.graph_objects.Figure: Pie chart figure. """ inputs = ( sanitize_dataset( @@ -584,7 +587,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] @@ -596,16 +599,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( @@ -643,15 +649,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') @@ -676,7 +685,14 @@ 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}->')]] @@ -789,7 +805,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') @@ -838,7 +862,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] @@ -855,17 +886,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), @@ -882,7 +915,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 @@ -912,19 +951,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' @@ -963,19 +1004,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() @@ -1018,12 +1058,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 From e3a9d4632fd01c2a840b6dabb6d7f68962e8a59b Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 13 Sep 2025 20:57:49 +0200 Subject: [PATCH 36/42] Add logical grouping to Docstrings --- flixopt/calculation.py | 5 ++++- flixopt/components.py | 38 ++++++++++++++++++++++++++++---------- flixopt/elements.py | 29 ++++++++++++++++++++--------- flixopt/interface.py | 10 ++++++++++ flixopt/results.py | 12 ++++++------ 5 files changed, 68 insertions(+), 26 deletions(-) diff --git a/flixopt/calculation.py b/flixopt/calculation.py index 4096bac6f..49cdf9959 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -320,8 +320,12 @@ class SegmentedCalculation(Calculation): compared to full-horizon solutions due to limited foresight in each segment. Args: + **Basic Configuration:** name: Unique identifier for the calculation, used in result files and logging. flow_system: The FlowSystem to optimize, containing all components, flows, and buses. + folder: Directory for saving results. Defaults to current working directory + 'results'. + + **Segmentation Parameters:** 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. @@ -330,7 +334,6 @@ class SegmentedCalculation(Calculation): 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: diff --git a/flixopt/components.py b/flixopt/components.py index 17f780cc9..71c984764 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -37,13 +37,12 @@ class LinearConverter(Component): behavior approximated through piecewise linear segments. Args: + **Basic Configuration:** 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 Definition:** 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 @@ -54,6 +53,14 @@ class LinearConverter(Component): 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. + + **Operational Features:** + 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. + + **Metadata:** meta_data: Used to store additional information about the Element. Not used internally, but saved in results. Only use Python native types. @@ -240,6 +247,7 @@ class Storage(Component): and investment-optimized storage systems with comprehensive techno-economic modeling. Args: + **Core Configuration:** 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. @@ -248,10 +256,22 @@ class Storage(Component): 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. + + **Charge State Bounds:** 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. + + **Efficiency Parameters:** + 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. + + **Initial/Final Conditions:** 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. @@ -259,15 +279,13 @@ class Storage(Component): 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. + + **Operational Constraints:** 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. + + **Metadata:** meta_data: Used to store additional information about the Element. Not used internally, but saved in results. Only use Python native types. diff --git a/flixopt/elements.py b/flixopt/elements.py index 81ccdaa2a..dc5a0bc29 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -232,6 +232,7 @@ class Flow(Element): - **OnOffParameters**: Used for `on_off_parameters` when flow has discrete states Args: + **Core Configuration:** 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. @@ -239,29 +240,39 @@ class Flow(Element): - Scalar value for fixed capacity - InvestParameters for investment-based sizing decisions - None to use large default value (CONFIG.modeling.BIG) + + **Operational Constraints:** 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. + 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. + + **Effects:** + 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). + + **Utilization Limits:** 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). + flow_hours_total_min: Minimum cumulative flow-hours over time horizon. + Alternative to load_factor_min for contractual or operational requirements. + flow_hours_total_max: Maximum cumulative flow-hours over time horizon. + Alternative to load_factor_max for absolute energy/material limits. + + **Advanced Features:** 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. + + **Metadata:** meta_data: Additional information stored with results but not used in optimization. Must contain only Python native types (dict, list, str, int, float, bool). diff --git a/flixopt/interface.py b/flixopt/interface.py index 5e5b3a63e..ff1561fd1 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -643,6 +643,7 @@ class InvestParameters(Interface): - **Divestment Effects**: Penalties for not investing (demolition, opportunity costs) Args: + **Investment Sizing:** 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 @@ -654,6 +655,8 @@ class InvestParameters(Interface): 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). + + **Effects:** 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}). @@ -869,6 +872,7 @@ class OnOffParameters(Interface): - **Process Equipment**: Compressors, pumps with operational constraints Args: + **Effects:** 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 @@ -876,12 +880,16 @@ class OnOffParameters(Interface): 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}). + + **Total Hour Constraints:** 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 Operation and shutdown:** 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 @@ -895,6 +903,8 @@ class OnOffParameters(Interface): consecutive_off_hours_max: Maximum continuous shutdown duration before mandatory restart. Models equipment preservation, process stability, or contractual requirements for minimum activity levels. + + **Cycling Limits:** 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). diff --git a/flixopt/results.py b/flixopt/results.py index 53acf9ff9..faa56606a 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -171,12 +171,12 @@ def __init__( Usually, this class is instantiated by the Calculation class, or by loading from file. Args: - solution: Optimization solution dataset. - flow_system: Flow system configuration dataset. - name: Calculation name. - summary: Calculation metadata. - folder: Results storage folder. - model: Linopy optimization model. + solution: Optimization solution dataset containing all variable values and results. + flow_system: Flow system configuration dataset with complete system parameters. + name: Unique identifier for this calculation, used in file names and display. + summary: Calculation metadata including solver status, timing, and optimization statistics. + folder: Directory path for result storage and loading. Defaults to current working directory + 'results'. + model: Original linopy optimization model for advanced analysis. May be None for loaded results. """ self.solution = solution self.flow_system = flow_system From e3b981e679b4a9176488e496dfa0f1860c4ca412 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 13 Sep 2025 20:59:50 +0200 Subject: [PATCH 37/42] Revert "Add logical grouping to Docstrings" This reverts commit e3a9d4632fd01c2a840b6dabb6d7f68962e8a59b. --- flixopt/calculation.py | 5 +---- flixopt/components.py | 38 ++++++++++---------------------------- flixopt/elements.py | 29 +++++++++-------------------- flixopt/interface.py | 10 ---------- flixopt/results.py | 12 ++++++------ 5 files changed, 26 insertions(+), 68 deletions(-) diff --git a/flixopt/calculation.py b/flixopt/calculation.py index 49cdf9959..4096bac6f 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -320,12 +320,8 @@ class SegmentedCalculation(Calculation): compared to full-horizon solutions due to limited foresight in each segment. Args: - **Basic Configuration:** name: Unique identifier for the calculation, used in result files and logging. flow_system: The FlowSystem to optimize, containing all components, flows, and buses. - folder: Directory for saving results. Defaults to current working directory + 'results'. - - **Segmentation Parameters:** 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. @@ -334,6 +330,7 @@ class SegmentedCalculation(Calculation): 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: diff --git a/flixopt/components.py b/flixopt/components.py index 71c984764..17f780cc9 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -37,12 +37,13 @@ class LinearConverter(Component): behavior approximated through piecewise linear segments. Args: - **Basic Configuration:** 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. - - **Conversion Definition:** + 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 @@ -53,14 +54,6 @@ class LinearConverter(Component): 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. - - **Operational Features:** - 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. - - **Metadata:** meta_data: Used to store additional information about the Element. Not used internally, but saved in results. Only use Python native types. @@ -247,7 +240,6 @@ class Storage(Component): and investment-optimized storage systems with comprehensive techno-economic modeling. Args: - **Core Configuration:** 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. @@ -256,22 +248,10 @@ class Storage(Component): 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. - - **Charge State Bounds:** 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. - - **Efficiency Parameters:** - 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. - - **Initial/Final Conditions:** 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. @@ -279,13 +259,15 @@ class Storage(Component): 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. - - **Operational Constraints:** + 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. - - **Metadata:** meta_data: Used to store additional information about the Element. Not used internally, but saved in results. Only use Python native types. diff --git a/flixopt/elements.py b/flixopt/elements.py index dc5a0bc29..81ccdaa2a 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -232,7 +232,6 @@ class Flow(Element): - **OnOffParameters**: Used for `on_off_parameters` when flow has discrete states Args: - **Core Configuration:** 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. @@ -240,39 +239,29 @@ class Flow(Element): - Scalar value for fixed capacity - InvestParameters for investment-based sizing decisions - None to use large default value (CONFIG.modeling.BIG) - - **Operational Constraints:** 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. - 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. - - **Effects:** - 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). - - **Utilization Limits:** 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. - flow_hours_total_min: Minimum cumulative flow-hours over time horizon. - Alternative to load_factor_min for contractual or operational requirements. - flow_hours_total_max: Maximum cumulative flow-hours over time horizon. - Alternative to load_factor_max for absolute energy/material limits. - - **Advanced Features:** + 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. - - **Metadata:** meta_data: Additional information stored with results but not used in optimization. Must contain only Python native types (dict, list, str, int, float, bool). diff --git a/flixopt/interface.py b/flixopt/interface.py index ff1561fd1..5e5b3a63e 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -643,7 +643,6 @@ class InvestParameters(Interface): - **Divestment Effects**: Penalties for not investing (demolition, opportunity costs) Args: - **Investment Sizing:** 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 @@ -655,8 +654,6 @@ class InvestParameters(Interface): 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). - - **Effects:** 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}). @@ -872,7 +869,6 @@ class OnOffParameters(Interface): - **Process Equipment**: Compressors, pumps with operational constraints Args: - **Effects:** 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 @@ -880,16 +876,12 @@ class OnOffParameters(Interface): 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}). - - **Total Hour Constraints:** 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 Operation and shutdown:** 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 @@ -903,8 +895,6 @@ class OnOffParameters(Interface): consecutive_off_hours_max: Maximum continuous shutdown duration before mandatory restart. Models equipment preservation, process stability, or contractual requirements for minimum activity levels. - - **Cycling Limits:** 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). diff --git a/flixopt/results.py b/flixopt/results.py index faa56606a..53acf9ff9 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -171,12 +171,12 @@ def __init__( Usually, this class is instantiated by the Calculation class, or by loading from file. Args: - solution: Optimization solution dataset containing all variable values and results. - flow_system: Flow system configuration dataset with complete system parameters. - name: Unique identifier for this calculation, used in file names and display. - summary: Calculation metadata including solver status, timing, and optimization statistics. - folder: Directory path for result storage and loading. Defaults to current working directory + 'results'. - model: Original linopy optimization model for advanced analysis. May be None for loaded results. + 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 From 12141b47b7c3d90613034792cd3dcbffb121d9fc Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 13 Sep 2025 21:05:50 +0200 Subject: [PATCH 38/42] Update flixopt/components.py Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- flixopt/components.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flixopt/components.py b/flixopt/components.py index 17f780cc9..55bbd4842 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -1071,7 +1071,7 @@ class Source(Component): label='natural_gas_flow', bus=gas_bus, size=1000, # Maximum 1000 kW supply capacity - costs={'cost': 0.04}, # €0.04/kWh gas cost + effects_per_flow_hour={'cost': 0.04}, # €0.04/kWh gas cost ) ], ) From fe7ed53aff1bb935231ad60800dd7b62308d28f9 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 13 Sep 2025 21:06:11 +0200 Subject: [PATCH 39/42] Update flixopt/components.py Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- flixopt/components.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flixopt/components.py b/flixopt/components.py index 55bbd4842..1c5cba3bf 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -1210,7 +1210,7 @@ class Sink(Component): label='electricity_process', bus=electricity_bus, size=5000, # Base electrical load - costs={'cost': -0.1}, # Value of service (negative cost) + effects_per_flow_hour={'cost': -0.1}, # Value of service (negative cost) ), Flow( label='steam_process', From 6381a1ab0f16f5a4aad58bf4b9dfe13ebe01f58a Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 13 Sep 2025 21:11:32 +0200 Subject: [PATCH 40/42] Some fixes detected by code rabbit --- flixopt/interface.py | 4 ++-- flixopt/plotting.py | 12 +++++++----- flixopt/results.py | 6 +++--- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/flixopt/interface.py b/flixopt/interface.py index 5e5b3a63e..771313ccc 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -1070,12 +1070,12 @@ def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): @property def use_off(self) -> bool: - """Determines whether 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 whether 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 diff --git a/flixopt/plotting.py b/flixopt/plotting.py index 258d6eed7..47d707497 100644 --- a/flixopt/plotting.py +++ b/flixopt/plotting.py @@ -52,8 +52,13 @@ ] # 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)) +try: + registry = plt.colormaps # Matplotlib >=3.7 + if 'portland' not in registry: + registry.register(mcolors.LinearSegmentedColormap.from_list('portland', _portland_colors)) +except AttributeError: # 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]] @@ -630,9 +635,6 @@ def heat_map_plotly( color_map: The color scale to use for the heatmap. Default is 'viridis'. Plotly supports various color scales like 'Cividis', 'Inferno', etc. categorical_labels: If True, the x and y axes are treated as categorical data (i.e., the index and columns will not be interpreted as continuous data). Default is True. If False, the axes are treated as continuous, which may be useful for time series or numeric data. - show: Whether to show the figure after creation. (This includes saving the figure) - save: Whether to save the figure after creation (without showing) - path: Path to save the figure. Returns: A Plotly figure object containing the heatmap. This can be further customized and saved diff --git a/flixopt/results.py b/flixopt/results.py index 53acf9ff9..b90df19a5 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -278,7 +278,7 @@ def plot_network( ] = True, path: Optional[pathlib.Path] = None, show: bool = False, - ) -> 'pyvis.network.Network': + ) -> Optional['pyvis.network.Network']: """Plot interactive network visualization of the system. Args: @@ -478,7 +478,7 @@ def plot_node_balance_pie( save: Union[bool, pathlib.Path] = False, show: bool = True, engine: plotting.PlottingEngine = 'plotly', - ) -> plotly.graph_objects.Figure: + ) -> Union[plotly.graph_objects.Figure, Tuple[plt.Figure, List[plt.Axes]]]: """Plot pie chart of flow hours distribution. Args: @@ -487,7 +487,7 @@ def plot_node_balance_pie( text_info: Information to display on pie slices. save: Whether to save plot. show: Whether to display plot. - engine: Plotting engine (only 'plotly' supported). + engine: Plotting engine ('plotly' or 'matplotlib'). Returns: plotly.graph_objects.Figure: Pie chart figure. From 319f0e9c60d044e3be2d52c05bde0baeee171fcd Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 16 Sep 2025 19:51:43 +0200 Subject: [PATCH 41/42] Improvements from coderabbit --- flixopt/plotting.py | 6 +++--- flixopt/results.py | 3 --- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/flixopt/plotting.py b/flixopt/plotting.py index 47d707497..ada62ccc2 100644 --- a/flixopt/plotting.py +++ b/flixopt/plotting.py @@ -52,11 +52,11 @@ ] # Check if the colormap already exists before registering it -try: - registry = plt.colormaps # Matplotlib >=3.7 +if hasattr(plt, 'colormaps'): # Matplotlib >= 3.7 + registry = plt.colormaps if 'portland' not in registry: registry.register(mcolors.LinearSegmentedColormap.from_list('portland', _portland_colors)) -except AttributeError: # Matplotlib <3.7 +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)) diff --git a/flixopt/results.py b/flixopt/results.py index b90df19a5..fbd7182ee 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -488,9 +488,6 @@ def plot_node_balance_pie( save: Whether to save plot. show: Whether to display plot. engine: Plotting engine ('plotly' or 'matplotlib'). - - Returns: - plotly.graph_objects.Figure: Pie chart figure. """ inputs = ( sanitize_dataset( From 5502afec1ab216e3fe7f6f009afdaba25c43e3bc Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sun, 21 Sep 2025 21:14:22 +0200 Subject: [PATCH 42/42] Added entry to CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) 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