diff --git a/CHANGELOG.md b/CHANGELOG.md index 72090ccba..17fcfec37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -124,6 +124,12 @@ This replaces `specific_share_to_other_effects_*` parameters and inverts the dir * `fit_to_model_coords()` method for data alignment * `fit_effects_to_model_coords()` method for effect data processing * `connect_and_transform()` method replacing several operations +* **Testing improvements**: Eliminated warnings during test execution + * Updated deprecated code patterns in tests and examples (e.g., `sink`/`source` → `inputs`/`outputs`, `'H'` → `'h'` frequency) + * Refactored plotting logic to handle test environments explicitly with non-interactive backends + * Added comprehensive warning filters in `__init__.py` and `pyproject.toml` to suppress third-party library warnings + * Improved test fixtures with proper figure cleanup to prevent memory leaks + * Enhanced backend detection and handling in `plotting.py` for both Matplotlib and Plotly Until here --> diff --git a/examples/00_Minmal/minimal_example.py b/examples/00_Minmal/minimal_example.py index e9ef241ff..b508f966c 100644 --- a/examples/00_Minmal/minimal_example.py +++ b/examples/00_Minmal/minimal_example.py @@ -37,13 +37,15 @@ # Heat load component with a fixed thermal demand profile heat_load = fx.Sink( 'Heat Demand', - sink=fx.Flow(label='Thermal Load', bus='District Heating', size=1, fixed_relative_profile=thermal_load_profile), + inputs=[ + fx.Flow(label='Thermal Load', bus='District Heating', size=1, fixed_relative_profile=thermal_load_profile) + ], ) # Gas source component with cost-effect per flow hour gas_source = fx.Source( 'Natural Gas Tariff', - source=fx.Flow(label='Gas Flow', bus='Natural Gas', size=1000, effects_per_flow_hour=0.04), # 0.04 €/kWh + outputs=[fx.Flow(label='Gas Flow', bus='Natural Gas', size=1000, effects_per_flow_hour=0.04)], # 0.04 €/kWh ) # --- Build the Flow System --- diff --git a/examples/01_Simple/simple_example.py b/examples/01_Simple/simple_example.py index 62fa8f6a9..c39d85c7a 100644 --- a/examples/01_Simple/simple_example.py +++ b/examples/01_Simple/simple_example.py @@ -37,7 +37,7 @@ label='CO2', unit='kg', description='CO2_e-Emissionen', - maximum_operation_per_hour=1000, # Max CO2 emissions per hour + maximum_per_hour=1000, # Max CO2 emissions per hour ) # --- Define Flow System Components --- @@ -77,18 +77,20 @@ # Heat Demand Sink: Represents a fixed heat demand profile heat_sink = fx.Sink( label='Heat Demand', - sink=fx.Flow(label='Q_th_Last', bus='Fernwärme', size=1, fixed_relative_profile=heat_demand_per_h), + inputs=[fx.Flow(label='Q_th_Last', bus='Fernwärme', size=1, fixed_relative_profile=heat_demand_per_h)], ) # Gas Source: Gas tariff source with associated costs and CO2 emissions gas_source = fx.Source( label='Gastarif', - source=fx.Flow(label='Q_Gas', bus='Gas', size=1000, effects_per_flow_hour={costs.label: 0.04, CO2.label: 0.3}), + outputs=[ + fx.Flow(label='Q_Gas', bus='Gas', size=1000, effects_per_flow_hour={costs.label: 0.04, CO2.label: 0.3}) + ], ) # Power Sink: Represents the export of electricity to the grid power_sink = fx.Sink( - label='Einspeisung', sink=fx.Flow(label='P_el', bus='Strom', effects_per_flow_hour=-1 * power_prices) + label='Einspeisung', inputs=[fx.Flow(label='P_el', bus='Strom', effects_per_flow_hour=-1 * power_prices)] ) # --- Build the Flow System --- diff --git a/examples/02_Complex/complex_example.py b/examples/02_Complex/complex_example.py index 506e7ac78..5bfd6f37e 100644 --- a/examples/02_Complex/complex_example.py +++ b/examples/02_Complex/complex_example.py @@ -147,33 +147,39 @@ # 5.a) Heat demand profile Waermelast = fx.Sink( 'Wärmelast', - sink=fx.Flow( - 'Q_th_Last', # Heat sink - bus='Fernwärme', # Linked bus - size=1, - fixed_relative_profile=heat_demand, # Fixed demand profile - ), + inputs=[ + fx.Flow( + 'Q_th_Last', # Heat sink + bus='Fernwärme', # Linked bus + size=1, + fixed_relative_profile=heat_demand, # Fixed demand profile + ) + ], ) # 5.b) Gas tariff Gasbezug = fx.Source( 'Gastarif', - source=fx.Flow( - 'Q_Gas', - bus='Gas', # Gas source - size=1000, # Nominal size - effects_per_flow_hour={Costs.label: 0.04, CO2.label: 0.3}, - ), + outputs=[ + fx.Flow( + 'Q_Gas', + bus='Gas', # Gas source + size=1000, # Nominal size + effects_per_flow_hour={Costs.label: 0.04, CO2.label: 0.3}, + ) + ], ) # 5.c) Feed-in of electricity Stromverkauf = fx.Sink( 'Einspeisung', - sink=fx.Flow( - 'P_el', - bus='Strom', # Feed-in tariff for electricity - effects_per_flow_hour=-1 * electricity_price, # Negative price for feed-in - ), + inputs=[ + fx.Flow( + 'P_el', + bus='Strom', # Feed-in tariff for electricity + effects_per_flow_hour=-1 * electricity_price, # Negative price for feed-in + ) + ], ) # --- Build FlowSystem --- diff --git a/examples/03_Calculation_types/example_calculation_types.py b/examples/03_Calculation_types/example_calculation_types.py index c23e14d0a..a07eedc94 100644 --- a/examples/03_Calculation_types/example_calculation_types.py +++ b/examples/03_Calculation_types/example_calculation_types.py @@ -108,39 +108,43 @@ # 4. Sinks and Sources # Heat Load Profile a_waermelast = fx.Sink( - 'Wärmelast', sink=fx.Flow('Q_th_Last', bus='Fernwärme', size=1, fixed_relative_profile=TS_heat_demand) + 'Wärmelast', inputs=[fx.Flow('Q_th_Last', bus='Fernwärme', size=1, fixed_relative_profile=TS_heat_demand)] ) # Electricity Feed-in a_strom_last = fx.Sink( - 'Stromlast', sink=fx.Flow('P_el_Last', bus='Strom', size=1, fixed_relative_profile=TS_electricity_demand) + 'Stromlast', inputs=[fx.Flow('P_el_Last', bus='Strom', size=1, fixed_relative_profile=TS_electricity_demand)] ) # Gas Tariff a_gas_tarif = fx.Source( 'Gastarif', - source=fx.Flow('Q_Gas', bus='Gas', size=1000, effects_per_flow_hour={costs.label: gas_price, CO2.label: 0.3}), + outputs=[ + fx.Flow('Q_Gas', bus='Gas', size=1000, effects_per_flow_hour={costs.label: gas_price, CO2.label: 0.3}) + ], ) # Coal Tariff a_kohle_tarif = fx.Source( 'Kohletarif', - source=fx.Flow('Q_Kohle', bus='Kohle', size=1000, effects_per_flow_hour={costs.label: 4.6, CO2.label: 0.3}), + outputs=[fx.Flow('Q_Kohle', bus='Kohle', size=1000, effects_per_flow_hour={costs.label: 4.6, CO2.label: 0.3})], ) # Electricity Tariff and Feed-in a_strom_einspeisung = fx.Sink( - 'Einspeisung', sink=fx.Flow('P_el', bus='Strom', size=1000, effects_per_flow_hour=TS_electricity_price_sell) + 'Einspeisung', inputs=[fx.Flow('P_el', bus='Strom', size=1000, effects_per_flow_hour=TS_electricity_price_sell)] ) a_strom_tarif = fx.Source( 'Stromtarif', - source=fx.Flow( - 'P_el', - bus='Strom', - size=1000, - effects_per_flow_hour={costs.label: TS_electricity_price_buy, CO2.label: 0.3}, - ), + outputs=[ + fx.Flow( + 'P_el', + bus='Strom', + size=1000, + effects_per_flow_hour={costs.label: TS_electricity_price_buy, CO2.label: 0.3}, + ) + ], ) # Flow System Setup diff --git a/examples/04_Scenarios/scenario_example.py b/examples/04_Scenarios/scenario_example.py index a2f32d666..601074dc4 100644 --- a/examples/04_Scenarios/scenario_example.py +++ b/examples/04_Scenarios/scenario_example.py @@ -43,7 +43,7 @@ label='CO2', unit='kg', description='CO2_e-Emissionen', - maximum_operation_per_hour=1000, # Max CO2 emissions per hour + maximum_per_hour=1000, # Max CO2 emissions per hour ) # --- Define Flow System Components --- @@ -90,18 +90,20 @@ # Heat Demand Sink: Represents a fixed heat demand profile heat_sink = fx.Sink( label='Heat Demand', - sink=fx.Flow(label='Q_th_Last', bus='Fernwärme', size=1, fixed_relative_profile=heat_demand_per_h), + inputs=[fx.Flow(label='Q_th_Last', bus='Fernwärme', size=1, fixed_relative_profile=heat_demand_per_h)], ) # Gas Source: Gas tariff source with associated costs and CO2 emissions gas_source = fx.Source( label='Gastarif', - source=fx.Flow(label='Q_Gas', bus='Gas', size=1000, effects_per_flow_hour={costs.label: 0.04, CO2.label: 0.3}), + outputs=[ + fx.Flow(label='Q_Gas', bus='Gas', size=1000, effects_per_flow_hour={costs.label: 0.04, CO2.label: 0.3}) + ], ) # Power Sink: Represents the export of electricity to the grid power_sink = fx.Sink( - label='Einspeisung', sink=fx.Flow(label='P_el', bus='Strom', effects_per_flow_hour=-1 * power_prices) + label='Einspeisung', inputs=[fx.Flow(label='P_el', bus='Strom', effects_per_flow_hour=-1 * power_prices)] ) # --- Build the Flow System --- diff --git a/examples/05_Two-stage-optimization/two_stage_optimization.py b/examples/05_Two-stage-optimization/two_stage_optimization.py index 52c47f006..333fa29b1 100644 --- a/examples/05_Two-stage-optimization/two_stage_optimization.py +++ b/examples/05_Two-stage-optimization/two_stage_optimization.py @@ -84,30 +84,34 @@ charging=fx.Flow('Q_th_load', size=137, bus='Fernwärme'), discharging=fx.Flow('Q_th_unload', size=158, bus='Fernwärme'), ), - fx.Sink('Wärmelast', sink=fx.Flow('Q_th_Last', bus='Fernwärme', size=1, fixed_relative_profile=heat_demand)), + fx.Sink( + 'Wärmelast', inputs=[fx.Flow('Q_th_Last', bus='Fernwärme', size=1, fixed_relative_profile=heat_demand)] + ), fx.Source( 'Gastarif', - source=fx.Flow('Q_Gas', bus='Gas', size=1000, effects_per_flow_hour={'costs': gas_price, 'CO2': 0.3}), + outputs=[fx.Flow('Q_Gas', bus='Gas', size=1000, effects_per_flow_hour={'costs': gas_price, 'CO2': 0.3})], ), fx.Source( 'Kohletarif', - source=fx.Flow('Q_Kohle', bus='Kohle', size=1000, effects_per_flow_hour={'costs': 4.6, 'CO2': 0.3}), + outputs=[fx.Flow('Q_Kohle', bus='Kohle', size=1000, effects_per_flow_hour={'costs': 4.6, 'CO2': 0.3})], ), fx.Source( 'Einspeisung', - source=fx.Flow( - 'P_el', bus='Strom', size=1000, effects_per_flow_hour={'costs': electricity_price + 0.5, 'CO2': 0.3} - ), + outputs=[ + fx.Flow( + 'P_el', bus='Strom', size=1000, effects_per_flow_hour={'costs': electricity_price + 0.5, 'CO2': 0.3} + ) + ], ), fx.Sink( 'Stromlast', - sink=fx.Flow('P_el_Last', bus='Strom', size=1, fixed_relative_profile=electricity_demand), + inputs=[fx.Flow('P_el_Last', bus='Strom', size=1, fixed_relative_profile=electricity_demand)], ), fx.Source( 'Stromtarif', - source=fx.Flow( - 'P_el', bus='Strom', size=1000, effects_per_flow_hour={'costs': electricity_price, 'CO2': 0.3} - ), + outputs=[ + fx.Flow('P_el', bus='Strom', size=1000, effects_per_flow_hour={'costs': electricity_price, 'CO2': 0.3}) + ], ), ) diff --git a/flixopt/__init__.py b/flixopt/__init__.py index 34306ae32..479f935df 100644 --- a/flixopt/__init__.py +++ b/flixopt/__init__.py @@ -2,6 +2,7 @@ This module bundles all common functionality of flixopt and sets up the logging """ +import warnings from importlib.metadata import version __version__ = version('flixopt') @@ -37,3 +38,33 @@ ) CONFIG.load_config() + + +# === Runtime warning suppression for third-party libraries === +# These warnings are from dependencies and cannot be fixed by end users. +# They are suppressed at runtime to provide a cleaner user experience. +# These filters match the test configuration in pyproject.toml for consistency. + +# tsam: Time series aggregation library +# - FutureWarning: Upcoming API changes in tsam (will be fixed in future tsam releases) +warnings.filterwarnings('ignore', category=FutureWarning, module='tsam') +# - UserWarning: Informational message about minimal value constraints +warnings.filterwarnings('ignore', category=UserWarning, message='.*minimal value.*exceeds.*', module='tsam') +# TODO: Might be able to fix it in flixopt? + +# linopy: Linear optimization library +# - UserWarning: Coordinate mismatch warnings that don't affect functionality and are expected. +warnings.filterwarnings( + 'ignore', category=UserWarning, message='Coordinates across variables not equal', module='linopy' +) +# - FutureWarning: join parameter default will change in future versions +warnings.filterwarnings( + 'ignore', + category=FutureWarning, + message="default value for join will change from join='outer' to join='exact'", + module='linopy', +) + +# numpy: Core numerical library +# - RuntimeWarning: Binary incompatibility warnings from compiled extensions (safe to ignore). numpy 1->2 +warnings.filterwarnings('ignore', category=RuntimeWarning, message='numpy\\.ndarray size changed') diff --git a/flixopt/plotting.py b/flixopt/plotting.py index 5c5cdc850..7b0c68c1b 100644 --- a/flixopt/plotting.py +++ b/flixopt/plotting.py @@ -27,9 +27,11 @@ import itertools import logging +import os import pathlib from typing import TYPE_CHECKING, Any, Literal +import matplotlib import matplotlib.colors as mcolors import matplotlib.pyplot as plt import numpy as np @@ -1450,20 +1452,49 @@ def export_figure( if filename.suffix != '.html': logger.warning(f'To save a Plotly figure, using .html. Adjusting suffix for {filename}') filename = filename.with_suffix('.html') - if show and not save: - fig.show() - elif save and show: - plotly.offline.plot(fig, filename=str(filename)) - elif save and not show: - fig.write_html(str(filename)) + + try: + is_test_env = 'PYTEST_CURRENT_TEST' in os.environ + + if is_test_env: + # Test environment: never open browser, only save if requested + if save: + fig.write_html(str(filename)) + # Ignore show flag in tests + else: + # Production environment: respect show and save flags + if save and show: + # Save and auto-open in browser + plotly.offline.plot(fig, filename=str(filename)) + elif save and not show: + # Save without opening + fig.write_html(str(filename)) + elif show and not save: + # Show interactively without saving + fig.show() + # If neither save nor show: do nothing + finally: + # Cleanup to prevent socket warnings + if hasattr(fig, '_renderer'): + fig._renderer = None + return figure_like elif isinstance(figure_like, tuple): fig, ax = figure_like if show: - fig.show() + # Only show if using interactive backend and not in test environment + backend = matplotlib.get_backend().lower() + is_interactive = backend not in {'agg', 'pdf', 'ps', 'svg', 'template'} + is_test_env = 'PYTEST_CURRENT_TEST' in os.environ + + if is_interactive and not is_test_env: + plt.show() + if save: fig.savefig(str(filename), dpi=300) + plt.close(fig) # Close figure to free memory + return fig, ax raise TypeError(f'Figure type not supported: {type(figure_like)}') diff --git a/flixopt/results.py b/flixopt/results.py index 2de3c0ea8..e571bc558 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -666,7 +666,7 @@ def _create_effects_dataset(self, mode: Literal['temporal', 'periodic', 'total'] component_arrays.append(arr.expand_dims(component=[component])) - ds[effect] = xr.concat(component_arrays, dim='component', coords='minimal').rename(effect) + ds[effect] = xr.concat(component_arrays, dim='component', coords='minimal', join='outer').rename(effect) # For now include a test to ensure correctness suffix = { diff --git a/pyproject.toml b/pyproject.toml index 5a2351eac..99f5295fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -189,6 +189,28 @@ markers = [ ] addopts = '-m "not examples"' # Skip examples by default +# Warning filter configuration for pytest +# Filters are processed in order; first match wins +# Format: "action:message:category:module" +filterwarnings = [ + # === Default behavior: show all warnings === + "default", + + # === Treat flixopt warnings as errors (strict mode for our code) === + # This ensures we catch deprecations, future changes, and user warnings in our own code + "error::DeprecationWarning:flixopt", + "error::FutureWarning:flixopt", + "error::UserWarning:flixopt", + + # === Third-party warnings (mirrored from __init__.py) === + "ignore::FutureWarning:tsam", + "ignore:.*minimal value.*exceeds.*:UserWarning:tsam", + "ignore:Coordinates across variables not equal:UserWarning:linopy", + "ignore:default value for join will change from join='outer' to join='exact':FutureWarning:linopy", + "ignore:numpy\\.ndarray size changed:RuntimeWarning", + "ignore:.*network visualization is still experimental.*:UserWarning:flixopt", +] + [tool.bandit] skips = ["B101", "B506"] # assert_used and yaml_load exclude_dirs = ["tests/"] diff --git a/tests/conftest.py b/tests/conftest.py index 2244eacb8..069d5c1ee 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -351,21 +351,21 @@ class Sinks: def heat_load(thermal_profile): """Create thermal heat load sink""" return fx.Sink( - 'Wärmelast', sink=fx.Flow('Q_th_Last', bus='Fernwärme', size=1, fixed_relative_profile=thermal_profile) + 'Wärmelast', inputs=[fx.Flow('Q_th_Last', bus='Fernwärme', size=1, fixed_relative_profile=thermal_profile)] ) @staticmethod def electricity_feed_in(electrical_price_profile): """Create electricity feed-in sink""" return fx.Sink( - 'Einspeisung', sink=fx.Flow('P_el', bus='Strom', effects_per_flow_hour=-1 * electrical_price_profile) + 'Einspeisung', inputs=[fx.Flow('P_el', bus='Strom', effects_per_flow_hour=-1 * electrical_price_profile)] ) @staticmethod def electricity_load(electrical_profile): """Create electrical load sink (for flow_system_long)""" return fx.Sink( - 'Stromlast', sink=fx.Flow('P_el_Last', bus='Strom', size=1, fixed_relative_profile=electrical_profile) + 'Stromlast', inputs=[fx.Flow('P_el_Last', bus='Strom', size=1, fixed_relative_profile=electrical_profile)] ) @@ -376,14 +376,14 @@ class Sources: def gas_with_costs_and_co2(): """Standard gas tariff with CO2 emissions""" source = Sources.gas_with_costs() - source.source.effects_per_flow_hour = {'costs': 0.04, 'CO2': 0.3} + source.outputs[0].effects_per_flow_hour = {'costs': 0.04, 'CO2': 0.3} return source @staticmethod def gas_with_costs(): """Simple gas tariff without CO2""" return fx.Source( - 'Gastarif', source=fx.Flow(label='Q_Gas', bus='Gas', size=1000, effects_per_flow_hour={'costs': 0.04}) + 'Gastarif', outputs=[fx.Flow(label='Q_Gas', bus='Gas', size=1000, effects_per_flow_hour={'costs': 0.04})] ) @@ -405,7 +405,7 @@ def simple_flow_system() -> fx.FlowSystem: # Define effects costs = Effects.costs_with_co2_share() co2 = Effects.co2() - co2.maximum_operation_per_hour = 1000 + co2.maximum_per_hour = 1000 # Create components boiler = Converters.Boilers.simple() @@ -436,7 +436,7 @@ def simple_flow_system_scenarios() -> fx.FlowSystem: # Define effects costs = Effects.costs_with_co2_share() co2 = Effects.co2() - co2.maximum_operation_per_hour = 1000 + co2.maximum_per_hour = 1000 # Create components boiler = Converters.Boilers.simple() @@ -570,21 +570,23 @@ def flow_system_long(): Effects.co2(), Effects.primary_energy(), fx.Sink( - 'Wärmelast', sink=fx.Flow('Q_th_Last', bus='Fernwärme', size=1, fixed_relative_profile=thermal_load_ts) + 'Wärmelast', inputs=[fx.Flow('Q_th_Last', bus='Fernwärme', size=1, fixed_relative_profile=thermal_load_ts)] + ), + fx.Sink( + 'Stromlast', inputs=[fx.Flow('P_el_Last', bus='Strom', size=1, fixed_relative_profile=electrical_load_ts)] ), - fx.Sink('Stromlast', sink=fx.Flow('P_el_Last', bus='Strom', size=1, fixed_relative_profile=electrical_load_ts)), fx.Source( 'Kohletarif', - source=fx.Flow('Q_Kohle', bus='Kohle', size=1000, effects_per_flow_hour={'costs': 4.6, 'CO2': 0.3}), + outputs=[fx.Flow('Q_Kohle', bus='Kohle', size=1000, effects_per_flow_hour={'costs': 4.6, 'CO2': 0.3})], ), fx.Source( 'Gastarif', - source=fx.Flow('Q_Gas', bus='Gas', size=1000, effects_per_flow_hour={'costs': gas_price, 'CO2': 0.3}), + outputs=[fx.Flow('Q_Gas', bus='Gas', size=1000, effects_per_flow_hour={'costs': gas_price, 'CO2': 0.3})], ), - fx.Sink('Einspeisung', sink=fx.Flow('P_el', bus='Strom', size=1000, effects_per_flow_hour=p_feed_in)), + fx.Sink('Einspeisung', inputs=[fx.Flow('P_el', bus='Strom', size=1000, effects_per_flow_hour=p_feed_in)]), fx.Source( 'Stromtarif', - source=fx.Flow('P_el', bus='Strom', size=1000, effects_per_flow_hour={'costs': p_sell, 'CO2': 0.3}), + outputs=[fx.Flow('P_el', bus='Strom', size=1000, effects_per_flow_hour={'costs': p_sell, 'CO2': 0.3})], ), ) @@ -794,3 +796,44 @@ def assert_sets_equal(set1: Iterable, set2: Iterable, msg=''): error_msg = f'{msg}: {error_msg}' raise AssertionError(error_msg) + + +# ============================================================================ +# PLOTTING CLEANUP FIXTURES +# ============================================================================ + + +@pytest.fixture(autouse=True) +def cleanup_figures(): + """ + Cleanup matplotlib figures after each test. + + This fixture runs automatically after every test to: + - Close all matplotlib figures to prevent memory leaks + """ + yield + # Close all matplotlib figures + import matplotlib.pyplot as plt + + plt.close('all') + + +@pytest.fixture(scope='session', autouse=True) +def set_test_environment(): + """ + Configure plotting for test environment. + + This fixture runs once per test session to: + - Set matplotlib to use non-interactive 'Agg' backend + - Set plotly to use non-interactive 'json' renderer + - Prevent GUI windows from opening during tests + """ + import matplotlib + + matplotlib.use('Agg') # Use non-interactive backend + + import plotly.io as pio + + pio.renderers.default = 'json' # Use non-interactive renderer + + yield diff --git a/tests/test_bus.py b/tests/test_bus.py index 8834132d5..0a5b19d8d 100644 --- a/tests/test_bus.py +++ b/tests/test_bus.py @@ -12,8 +12,8 @@ def test_bus(self, basic_flow_system_linopy_coords, coords_config): bus = fx.Bus('TestBus', excess_penalty_per_flow_hour=None) flow_system.add_elements( bus, - fx.Sink('WärmelastTest', sink=fx.Flow('Q_th_Last', 'TestBus')), - fx.Source('GastarifTest', source=fx.Flow('Q_Gas', 'TestBus')), + fx.Sink('WärmelastTest', inputs=[fx.Flow('Q_th_Last', 'TestBus')]), + fx.Source('GastarifTest', outputs=[fx.Flow('Q_Gas', 'TestBus')]), ) model = create_linopy_model(flow_system) @@ -31,8 +31,8 @@ def test_bus_penalty(self, basic_flow_system_linopy_coords, coords_config): bus = fx.Bus('TestBus') flow_system.add_elements( bus, - fx.Sink('WärmelastTest', sink=fx.Flow('Q_th_Last', 'TestBus')), - fx.Source('GastarifTest', source=fx.Flow('Q_Gas', 'TestBus')), + fx.Sink('WärmelastTest', inputs=[fx.Flow('Q_th_Last', 'TestBus')]), + fx.Source('GastarifTest', outputs=[fx.Flow('Q_Gas', 'TestBus')]), ) model = create_linopy_model(flow_system) @@ -73,8 +73,8 @@ def test_bus_with_coords(self, basic_flow_system_linopy_coords, coords_config): bus = fx.Bus('TestBus', excess_penalty_per_flow_hour=None) flow_system.add_elements( bus, - fx.Sink('WärmelastTest', sink=fx.Flow('Q_th_Last', 'TestBus')), - fx.Source('GastarifTest', source=fx.Flow('Q_Gas', 'TestBus')), + fx.Sink('WärmelastTest', inputs=[fx.Flow('Q_th_Last', 'TestBus')]), + fx.Source('GastarifTest', outputs=[fx.Flow('Q_Gas', 'TestBus')]), ) model = create_linopy_model(flow_system) diff --git a/tests/test_component.py b/tests/test_component.py index 40cfe2f69..4a7c4afd8 100644 --- a/tests/test_component.py +++ b/tests/test_component.py @@ -462,13 +462,15 @@ def test_transmission_balanced(self, basic_flow_system, highs_solver): last2 = fx.Sink( 'Wärmelast2', - sink=fx.Flow( - 'Q_th_Last', - bus='Wärme lokal', - size=1, - fixed_relative_profile=flow_system.components['Wärmelast'].sink.fixed_relative_profile - * np.array([0, 0, 0, 0, 0, 1, 1, 1, 1, 1]), - ), + inputs=[ + fx.Flow( + 'Q_th_Last', + bus='Wärme lokal', + size=1, + fixed_relative_profile=flow_system.components['Wärmelast'].inputs[0].fixed_relative_profile + * np.array([0, 0, 0, 0, 0, 1, 1, 1, 1, 1]), + ) + ], ) transmission = fx.Transmission( @@ -530,13 +532,15 @@ def test_transmission_unbalanced(self, basic_flow_system, highs_solver): last2 = fx.Sink( 'Wärmelast2', - sink=fx.Flow( - 'Q_th_Last', - bus='Wärme lokal', - size=1, - fixed_relative_profile=flow_system.components['Wärmelast'].sink.fixed_relative_profile - * np.array([0, 0, 0, 0, 0, 1, 1, 1, 1, 1]), - ), + inputs=[ + fx.Flow( + 'Q_th_Last', + bus='Wärme lokal', + size=1, + fixed_relative_profile=flow_system.components['Wärmelast'].inputs[0].fixed_relative_profile + * np.array([0, 0, 0, 0, 0, 1, 1, 1, 1, 1]), + ) + ], ) transmission = fx.Transmission( diff --git a/tests/test_dataconverter.py b/tests/test_dataconverter.py index 798324436..0f12a1af3 100644 --- a/tests/test_dataconverter.py +++ b/tests/test_dataconverter.py @@ -910,7 +910,7 @@ def test_complex_multid_scenario(self): """Complex real-world scenario with multi-D array and broadcasting.""" # Energy system data: time x technology, broadcast to regions coords = { - 'time': pd.date_range('2024-01-01', periods=24, freq='H', name='time'), # 24 hours + 'time': pd.date_range('2024-01-01', periods=24, freq='h', name='time'), # 24 hours 'technology': pd.Index(['solar', 'wind', 'gas', 'coal'], name='technology'), # 4 technologies 'region': pd.Index(['north', 'south', 'east'], name='region'), # 3 regions } diff --git a/tests/test_flow.py b/tests/test_flow.py index 14100feb6..bf1723a7d 100644 --- a/tests/test_flow.py +++ b/tests/test_flow.py @@ -16,7 +16,7 @@ def test_flow_minimal(self, basic_flow_system_linopy_coords, coords_config): flow = fx.Flow('Wärme', bus='Fernwärme', size=100) - flow_system.add_elements(fx.Sink('Sink', sink=flow)) + flow_system.add_elements(fx.Sink('Sink', inputs=[flow])) model = create_linopy_model(flow_system) @@ -54,7 +54,7 @@ def test_flow(self, basic_flow_system_linopy_coords, coords_config): load_factor_max=0.9, ) - flow_system.add_elements(fx.Sink('Sink', sink=flow)) + flow_system.add_elements(fx.Sink('Sink', inputs=[flow])) model = create_linopy_model(flow_system) # total_flow_hours @@ -112,7 +112,7 @@ def test_effects_per_flow_hour(self, basic_flow_system_linopy_coords, coords_con flow = fx.Flow( 'Wärme', bus='Fernwärme', effects_per_flow_hour={'costs': costs_per_flow_hour, 'CO2': co2_per_flow_hour} ) - flow_system.add_elements(fx.Sink('Sink', sink=flow), fx.Effect('CO2', 't', '')) + flow_system.add_elements(fx.Sink('Sink', inputs=[flow]), fx.Effect('CO2', 't', '')) model = create_linopy_model(flow_system) costs, co2 = flow_system.effects['costs'], flow_system.effects['CO2'] @@ -154,7 +154,7 @@ def test_flow_invest(self, basic_flow_system_linopy_coords, coords_config): relative_maximum=np.linspace(0.5, 1, timesteps.size), ) - flow_system.add_elements(fx.Sink('Sink', sink=flow)) + flow_system.add_elements(fx.Sink('Sink', inputs=[flow])) model = create_linopy_model(flow_system) assert_sets_equal( @@ -217,7 +217,7 @@ def test_flow_invest_optional(self, basic_flow_system_linopy_coords, coords_conf relative_maximum=np.linspace(0.5, 1, timesteps.size), ) - flow_system.add_elements(fx.Sink('Sink', sink=flow)) + flow_system.add_elements(fx.Sink('Sink', inputs=[flow])) model = create_linopy_model(flow_system) assert_sets_equal( @@ -292,7 +292,7 @@ def test_flow_invest_optional_wo_min_size(self, basic_flow_system_linopy_coords, relative_maximum=np.linspace(0.5, 1, timesteps.size), ) - flow_system.add_elements(fx.Sink('Sink', sink=flow)) + flow_system.add_elements(fx.Sink('Sink', inputs=[flow])) model = create_linopy_model(flow_system) assert_sets_equal( @@ -367,7 +367,7 @@ def test_flow_invest_wo_min_size_non_optional(self, basic_flow_system_linopy_coo relative_maximum=np.linspace(0.5, 1, timesteps.size), ) - flow_system.add_elements(fx.Sink('Sink', sink=flow)) + flow_system.add_elements(fx.Sink('Sink', inputs=[flow])) model = create_linopy_model(flow_system) assert_sets_equal( @@ -425,7 +425,7 @@ def test_flow_invest_fixed_size(self, basic_flow_system_linopy_coords, coords_co relative_maximum=0.9, ) - flow_system.add_elements(fx.Sink('Sink', sink=flow)) + flow_system.add_elements(fx.Sink('Sink', inputs=[flow])) model = create_linopy_model(flow_system) assert_sets_equal( @@ -464,7 +464,7 @@ def test_flow_invest_with_effects(self, basic_flow_system_linopy_coords, coords_ ), ) - flow_system.add_elements(fx.Sink('Sink', sink=flow), co2) + flow_system.add_elements(fx.Sink('Sink', inputs=[flow]), co2) model = create_linopy_model(flow_system) # Check investment effects @@ -501,7 +501,7 @@ def test_flow_invest_divest_effects(self, basic_flow_system_linopy_coords, coord ), ) - flow_system.add_elements(fx.Sink('Sink', sink=flow)) + flow_system.add_elements(fx.Sink('Sink', inputs=[flow])) model = create_linopy_model(flow_system) # Check divestment effects @@ -528,7 +528,7 @@ def test_flow_on(self, basic_flow_system_linopy_coords, coords_config): relative_maximum=0.8, on_off_parameters=fx.OnOffParameters(), ) - flow_system.add_elements(fx.Sink('Sink', sink=flow)) + flow_system.add_elements(fx.Sink('Sink', inputs=[flow])) model = create_linopy_model(flow_system) assert_sets_equal( @@ -595,7 +595,7 @@ def test_effects_per_running_hour(self, basic_flow_system_linopy_coords, coords_ effects_per_running_hour={'costs': costs_per_running_hour, 'CO2': co2_per_running_hour} ), ) - flow_system.add_elements(fx.Sink('Sink', sink=flow), fx.Effect('CO2', 't', '')) + flow_system.add_elements(fx.Sink('Sink', inputs=[flow]), fx.Effect('CO2', 't', '')) model = create_linopy_model(flow_system) costs, co2 = flow_system.effects['costs'], flow_system.effects['CO2'] @@ -655,7 +655,7 @@ def test_consecutive_on_hours(self, basic_flow_system_linopy_coords, coords_conf ), ) - flow_system.add_elements(fx.Sink('Sink', sink=flow)) + flow_system.add_elements(fx.Sink('Sink', inputs=[flow])) model = create_linopy_model(flow_system) assert {'Sink(Wärme)|consecutive_on_hours', 'Sink(Wärme)|on'}.issubset(set(flow.submodel.variables)) @@ -738,7 +738,7 @@ def test_consecutive_on_hours_previous(self, basic_flow_system_linopy_coords, co previous_flow_rate=np.array([10, 20, 30, 0, 20, 20, 30]), # Previously on for 3 steps ) - flow_system.add_elements(fx.Sink('Sink', sink=flow)) + flow_system.add_elements(fx.Sink('Sink', inputs=[flow])) model = create_linopy_model(flow_system) assert {'Sink(Wärme)|consecutive_on_hours', 'Sink(Wärme)|on'}.issubset(set(flow.submodel.variables)) @@ -818,7 +818,7 @@ def test_consecutive_off_hours(self, basic_flow_system_linopy_coords, coords_con ), ) - flow_system.add_elements(fx.Sink('Sink', sink=flow)) + flow_system.add_elements(fx.Sink('Sink', inputs=[flow])) model = create_linopy_model(flow_system) assert {'Sink(Wärme)|consecutive_off_hours', 'Sink(Wärme)|off'}.issubset(set(flow.submodel.variables)) @@ -901,7 +901,7 @@ def test_consecutive_off_hours_previous(self, basic_flow_system_linopy_coords, c previous_flow_rate=np.array([10, 20, 30, 0, 20, 0, 0]), # Previously off for 2 steps ) - flow_system.add_elements(fx.Sink('Sink', sink=flow)) + flow_system.add_elements(fx.Sink('Sink', inputs=[flow])) model = create_linopy_model(flow_system) assert {'Sink(Wärme)|consecutive_off_hours', 'Sink(Wärme)|off'}.issubset(set(flow.submodel.variables)) @@ -983,7 +983,7 @@ def test_switch_on_constraints(self, basic_flow_system_linopy_coords, coords_con ), ) - flow_system.add_elements(fx.Sink('Sink', sink=flow)) + flow_system.add_elements(fx.Sink('Sink', inputs=[flow])) model = create_linopy_model(flow_system) # Check that variables exist @@ -1045,7 +1045,7 @@ def test_on_hours_limits(self, basic_flow_system_linopy_coords, coords_config): ), ) - flow_system.add_elements(fx.Sink('Sink', sink=flow)) + flow_system.add_elements(fx.Sink('Sink', inputs=[flow])) model = create_linopy_model(flow_system) # Check that variables exist @@ -1081,7 +1081,7 @@ def test_flow_on_invest_optional(self, basic_flow_system_linopy_coords, coords_c relative_maximum=0.8, on_off_parameters=fx.OnOffParameters(), ) - flow_system.add_elements(fx.Sink('Sink', sink=flow)) + flow_system.add_elements(fx.Sink('Sink', inputs=[flow])) model = create_linopy_model(flow_system) assert_sets_equal( @@ -1182,7 +1182,7 @@ def test_flow_on_invest_non_optional(self, basic_flow_system_linopy_coords, coor relative_maximum=0.8, on_off_parameters=fx.OnOffParameters(), ) - flow_system.add_elements(fx.Sink('Sink', sink=flow)) + flow_system.add_elements(fx.Sink('Sink', inputs=[flow])) model = create_linopy_model(flow_system) assert_sets_equal( @@ -1281,7 +1281,7 @@ def test_fixed_relative_profile(self, basic_flow_system_linopy_coords, coords_co fixed_relative_profile=profile, ) - flow_system.add_elements(fx.Sink('Sink', sink=flow)) + flow_system.add_elements(fx.Sink('Sink', inputs=[flow])) model = create_linopy_model(flow_system) assert_var_equal( @@ -1308,7 +1308,7 @@ def test_fixed_profile_with_investment(self, basic_flow_system_linopy_coords, co fixed_relative_profile=profile, ) - flow_system.add_elements(fx.Sink('Sink', sink=flow)) + flow_system.add_elements(fx.Sink('Sink', inputs=[flow])) model = create_linopy_model(flow_system) assert_var_equal( diff --git a/tests/test_functional.py b/tests/test_functional.py index 54dd2e99f..17caea2b5 100644 --- a/tests/test_functional.py +++ b/tests/test_functional.py @@ -73,9 +73,9 @@ def flow_system_base(timesteps: pd.DatetimeIndex) -> fx.FlowSystem: flow_system.add_elements( fx.Sink( label='Wärmelast', - sink=fx.Flow(label='Wärme', bus='Fernwärme', fixed_relative_profile=data.thermal_demand, size=1), + inputs=[fx.Flow(label='Wärme', bus='Fernwärme', fixed_relative_profile=data.thermal_demand, size=1)], ), - fx.Source(label='Gastarif', source=fx.Flow(label='Gas', bus='Gas', effects_per_flow_hour=1)), + fx.Source(label='Gastarif', outputs=[fx.Flow(label='Gas', bus='Gas', effects_per_flow_hour=1)]), ) return flow_system @@ -551,7 +551,7 @@ def test_on_total_bounds(solver_fixture, time_steps_fixture): ), ), ) - flow_system.all_elements['Wärmelast'].sink.fixed_relative_profile = np.array( + flow_system.all_elements['Wärmelast'].inputs[0].fixed_relative_profile = np.array( [0, 10, 20, 0, 12] ) # Else its non deterministic @@ -620,7 +620,7 @@ def test_consecutive_on_off(solver_fixture, time_steps_fixture): Q_th=fx.Flow('Q_th', bus='Fernwärme', size=100), ), ) - flow_system.all_elements['Wärmelast'].sink.fixed_relative_profile = np.array([5, 10, 20, 18, 12]) + flow_system.all_elements['Wärmelast'].inputs[0].fixed_relative_profile = np.array([5, 10, 20, 18, 12]) # Else its non deterministic solve_and_load(flow_system, solver_fixture) @@ -682,7 +682,7 @@ def test_consecutive_off(solver_fixture, time_steps_fixture): ), ), ) - flow_system.all_elements['Wärmelast'].sink.fixed_relative_profile = np.array( + flow_system.all_elements['Wärmelast'].inputs[0].fixed_relative_profile = np.array( [5, 0, 20, 18, 12] ) # Else its non deterministic diff --git a/tests/test_plots.py b/tests/test_plots.py index ba52cd21c..61c26c510 100644 --- a/tests/test_plots.py +++ b/tests/test_plots.py @@ -7,7 +7,6 @@ import matplotlib.pyplot as plt import numpy as np import pandas as pd -import plotly import pytest from flixopt import plotting @@ -18,6 +17,14 @@ class TestPlots(unittest.TestCase): def setUp(self): np.random.seed(72) + def tearDown(self): + """Cleanup matplotlib and plotly resources""" + plt.close('all') + # Force garbage collection to cleanup any lingering resources + import gc + + gc.collect() + @staticmethod def get_sample_data( nr_of_columns: int = 7, @@ -51,38 +58,44 @@ def get_sample_data( def test_bar_plots(self): data = self.get_sample_data(nr_of_columns=10, nr_of_periods=1, time_steps_per_period=24) - plotly.offline.plot(plotting.with_plotly(data, 'stacked_bar')) + # Create plotly figure (json renderer doesn't need .show()) + _ = plotting.with_plotly(data, 'stacked_bar') plotting.with_matplotlib(data, 'stacked_bar') - plt.show() + plt.savefig(f'test_plot_{self._testMethodName}.png', bbox_inches='tight') + plt.close('all') # Close all figures to prevent memory leaks data = self.get_sample_data( nr_of_columns=10, nr_of_periods=5, time_steps_per_period=24, drop_fraction_of_indices=0.3 ) - plotly.offline.plot(plotting.with_plotly(data, 'stacked_bar')) + # Create plotly figure (json renderer doesn't need .show()) + _ = plotting.with_plotly(data, 'stacked_bar') plotting.with_matplotlib(data, 'stacked_bar') - plt.show() + plt.savefig(f'test_plot_{self._testMethodName}.png', bbox_inches='tight') + plt.close('all') # Close all figures to prevent memory leaks def test_line_plots(self): data = self.get_sample_data(nr_of_columns=10, nr_of_periods=1, time_steps_per_period=24) - plotly.offline.plot(plotting.with_plotly(data, 'line')) + _ = plotting.with_plotly(data, 'line') plotting.with_matplotlib(data, 'line') - plt.show() + plt.savefig(f'test_plot_{self._testMethodName}.png', bbox_inches='tight') + plt.close('all') # Close all figures to prevent memory leaks data = self.get_sample_data( nr_of_columns=10, nr_of_periods=5, time_steps_per_period=24, drop_fraction_of_indices=0.3 ) - plotly.offline.plot(plotting.with_plotly(data, 'line')) + _ = plotting.with_plotly(data, 'line') plotting.with_matplotlib(data, 'line') - plt.show() + plt.savefig(f'test_plot_{self._testMethodName}.png', bbox_inches='tight') + plt.close('all') # Close all figures to prevent memory leaks def test_stacked_line_plots(self): data = self.get_sample_data(nr_of_columns=10, nr_of_periods=1, time_steps_per_period=24) - plotly.offline.plot(plotting.with_plotly(data, 'area')) + _ = plotting.with_plotly(data, 'area') data = self.get_sample_data( nr_of_columns=10, nr_of_periods=5, time_steps_per_period=24, drop_fraction_of_indices=0.3 ) - plotly.offline.plot(plotting.with_plotly(data, 'area')) + _ = plotting.with_plotly(data, 'area') def test_heat_map_plots(self): # Generate single-column data with datetime index for heatmap @@ -91,9 +104,10 @@ def test_heat_map_plots(self): # Convert data for heatmap plotting using 'day' as period and 'hour' steps heatmap_data = plotting.reshape_to_2d(data.iloc[:, 0].values.flatten(), 24) # Plotting heatmaps with Plotly and Matplotlib - plotly.offline.plot(plotting.heat_map_plotly(pd.DataFrame(heatmap_data))) + _ = plotting.heat_map_plotly(pd.DataFrame(heatmap_data)) plotting.heat_map_matplotlib(pd.DataFrame(heatmap_data)) - plt.show() + plt.savefig(f'test_plot_{self._testMethodName}.png', bbox_inches='tight') + plt.close('all') # Close all figures to prevent memory leaks def test_heat_map_plots_resampling(self): date_range = pd.date_range(start='2023-01-01', end='2023-03-21', freq='5min') @@ -113,21 +127,24 @@ def test_heat_map_plots_resampling(self): data = df_irregular # Convert data for heatmap plotting using 'day' as period and 'hour' steps heatmap_data = plotting.heat_map_data_from_df(data, 'MS', 'D') - plotly.offline.plot(plotting.heat_map_plotly(heatmap_data)) + _ = plotting.heat_map_plotly(heatmap_data) plotting.heat_map_matplotlib(pd.DataFrame(heatmap_data)) - plt.show() + plt.savefig(f'test_plot_{self._testMethodName}.png', bbox_inches='tight') + plt.close('all') # Close all figures to prevent memory leaks heatmap_data = plotting.heat_map_data_from_df(data, 'W', 'h', fill='ffill') # Plotting heatmaps with Plotly and Matplotlib - plotly.offline.plot(plotting.heat_map_plotly(pd.DataFrame(heatmap_data))) + _ = plotting.heat_map_plotly(pd.DataFrame(heatmap_data)) plotting.heat_map_matplotlib(pd.DataFrame(heatmap_data)) - plt.show() + plt.savefig(f'test_plot_{self._testMethodName}.png', bbox_inches='tight') + plt.close('all') # Close all figures to prevent memory leaks heatmap_data = plotting.heat_map_data_from_df(data, 'D', 'h', fill='ffill') # Plotting heatmaps with Plotly and Matplotlib - plotly.offline.plot(plotting.heat_map_plotly(pd.DataFrame(heatmap_data))) + _ = plotting.heat_map_plotly(pd.DataFrame(heatmap_data)) plotting.heat_map_matplotlib(pd.DataFrame(heatmap_data)) - plt.show() + plt.savefig(f'test_plot_{self._testMethodName}.png', bbox_inches='tight') + plt.close('all') # Close all figures to prevent memory leaks if __name__ == '__main__': diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py index c00e5d6a2..56c3169b7 100644 --- a/tests/test_scenarios.py +++ b/tests/test_scenarios.py @@ -56,7 +56,7 @@ def test_system(): # Create a demand sink with scenario-dependent profiles demand = Flow(label='Demand', bus=electricity_bus.label_full, fixed_relative_profile=demand_profiles) - demand_sink = Sink('Demand', sink=demand) + demand_sink = Sink('Demand', inputs=[demand]) # Create a power source with investment option power_gen = Flow( @@ -69,7 +69,7 @@ def test_system(): ), effects_per_flow_hour={'costs': 20}, # €/MWh ) - generator = Source('Generator', source=power_gen) + generator = Source('Generator', outputs=[power_gen]) # Create a storage for electricity storage_charge = Flow(label='Charge', bus=electricity_bus.label_full, size=10) @@ -130,11 +130,11 @@ def flow_system_complex_scenarios() -> fx.FlowSystem: fx.Bus('Strom'), fx.Bus('Fernwärme'), fx.Bus('Gas'), - fx.Sink('Wärmelast', sink=fx.Flow('Q_th_Last', 'Fernwärme', size=1, fixed_relative_profile=thermal_load)), + fx.Sink('Wärmelast', inputs=[fx.Flow('Q_th_Last', 'Fernwärme', size=1, fixed_relative_profile=thermal_load)]), fx.Source( - 'Gastarif', source=fx.Flow('Q_Gas', 'Gas', size=1000, effects_per_flow_hour={'costs': 0.04, 'CO2': 0.3}) + 'Gastarif', outputs=[fx.Flow('Q_Gas', 'Gas', size=1000, effects_per_flow_hour={'costs': 0.04, 'CO2': 0.3})] ), - fx.Sink('Einspeisung', sink=fx.Flow('P_el', 'Strom', effects_per_flow_hour=-1 * electrical_load)), + fx.Sink('Einspeisung', inputs=[fx.Flow('P_el', 'Strom', effects_per_flow_hour=-1 * electrical_load)]), ) boiler = fx.linear_converters.Boiler(