diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..a1e8c89e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +# http://editorconfig.org + +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +[*.py] +lines_after_imports = 2 +max_line_length = 120 \ No newline at end of file diff --git a/Dockerfile-tox b/Dockerfile-tox new file mode 100644 index 00000000..4250fea8 --- /dev/null +++ b/Dockerfile-tox @@ -0,0 +1,47 @@ +# Build +# docker build -f Dockerfile-tox -t echo-tox:latest . +# Run +# docker run --volume .:/opt/tox/echo echo-tox:latest +# pull official base image +FROM ubuntu:22.04 +# Prevent having to answer question about geographic area +ARG DEBIAN_FRONTEND=noninteractive + +RUN apt-get update --quiet --yes && \ + apt-get install --quiet --yes build-essential ca-certificates git python3 python3-dev python3-venv software-properties-common + +# Install cbc (solver used by) +RUN apt-get install --quiet --yes coinor-cbc coinor-libcbc-dev + +# Install deadsnakes (required for tox testing) +RUN add-apt-repository --yes --update ppa:deadsnakes/ppa && \ + apt-get install --quiet --yes python3.8 python3.8-dev python3.8-venv python3.9 python3.9-dev python3.9-venv python3.10 python3.10-dev python3.10-venv python3.11 python3.11-dev python3.11-venv python3.12 python3.12-dev python3.12-venv && \ + apt-get autoremove -yqq --purge && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +# Create user +ARG uid=${UID:-1000} +ARG gid=${GID:-1000} +RUN groupadd group --gid=$gid +RUN useradd user --uid=$uid --gid=$gid --home-dir=/tmp --no-create-home +USER $uid:$gid + +# Create working directory +WORKDIR /opt/tox + +# Install required python packages +RUN python3 -m venv ./tox-venv +RUN ./tox-venv/bin/python3 -m pip install --upgrade pip +RUN ./tox-venv/bin/python3 -m pip install tox + +# Configure environment +ENV LANG=C.UTF-8 +ENV LC_ALL=C.UTF-8 +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONUNBUFFERED 1 + +WORKDIR /opt/tox/echo + +# Defaults +ENTRYPOINT [ "../tox-venv/bin/tox" ] diff --git a/README.md b/README.md index a12da93b..b5e1a636 100644 --- a/README.md +++ b/README.md @@ -36,13 +36,74 @@ If you plan to use echo_scenario to solve power flows then you will need to inst The following solvers can be used Free for academic use only: -- CPLEX (recommended): This has been tested the most. It requires a license but is free for academic users. To install, follow instructions [here](https://www.ibm.com/products/ilog-cplex-optimization-studio). After installing CPLEX you will need to add the binaries to your system path. +- CPLEX (recommended): This has been tested the most. It requires a license but is free for academic users. To install, follow instructions [here](https://www.ibm.com/products/ilog-cplex-optimization-studio). After installing CPLEX you will need to add the binaries to your system path. - GUROBI: Minimal testing: It requires a license but is free for academic users. Check their website for instructions on installing https://www.gurobi.com/documentation/9.5/remoteservices/linux_installation.html Open source: - CBC: This solver can be used provided you only include linear costs (no quadratic costs or regularisation). Information on the solver is available here https://github.com/coin-or/Cbc . For installing on ubuntu run `sudo apt-get install -y coinor-cbc1 +## Testing + +### Running Tests + +The test suite can be run with, + +```sh +$ pytest +``` + +This will run the tests using the default optimiser engine (currently set to `cbc`). We default to `cbc` since this is the only solver available to the Github action. + +Some of the tests require a solver capable of performing non-linear optimisations. The `cbc` solver is not able to do this. To run the complete set of tests, you will need to have a solver capable of non-linear optimizations (for example `cplex`) installed on your system. You can run pytest and override the default solver using the environment variable `TESTING_OPTIMISER_ENGINE`. For example: + +```sh +$ TESTING_OPTIMISER_ENGINE=cplex pytest +``` + +See the section below 'Writing tests', for instructions on how to mark a test as requiring a non-linear solver. + +### Writing tests + +Most tests will require an `engine_settings` object. A fixture `engine_settings` has been provided, which should be used by any tests that require engine settings. It is strongly recommended **not** to use `scenario.engine_settings_from_environment()` in order to obtain an engine settings object, since this can result in the your newly-written tests running on a different solver to the rest of the test suite. + +A small number of tests might need to be run with a solver capable of non-linear optimizations. These tests should be marked with a special pytest mark decorator: + +```py +import pytest + +@pytest.mark.nonlinear +def test_that_requires_nonlinear_solver(): + ... +``` + +### Tox + +Tox makes it easy to test echo on multiple versions of python. The configuration file for tox is called `tox.ini`, which +is located in the project root. Looking in `tox.ini`, the `env_list` parameter lists all the environments we will test against. + +To prevent echo developers having to install all the different versions of python on their system, tox has been setup +to run in a docker container. + +To test with tox, the first step is to build the docker image, + +```sh +docker build -f Dockerfile-tox -t echo-tox:latest . +``` + +This should only need doing once. Re-run the docker build command if you modify the Dockerfile, `Dockerfile-tox`. + +The test suite can be run across the different environments (python versions) with, + +```sh +docker run -t --volume .:/opt/tox/echo echo-tox:latest -- -W ignore::DeprecationWarning +``` + +- The command that gets run is `tox`, which you can see by looking at the `ENTRYPOINT` in the Dockerfile, `Dockerfile-tox`. +- The `-t` gives us colored output in the terminal. +- The `--volume` switch mounts echo project inside the container +- Arguments/options before the `--` are passed to `tox`. Anything after `--` is passed to the command `tox` runs i.e. `pytest`. In this case we are using `-W ignore::DeprecationWarning` to suppress an error raised because of deprecated datetime code in pandas (or one of its dependencies) + ## Documentation ### Building the documentation diff --git a/pyproject.toml b/pyproject.toml index d40119a6..5d1d6c8f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,6 +53,7 @@ dependencies = [ "scikit-learn>=1.1", "orjson>=3.8", "shortuuid>=1.0.11,<2", + "rich>=13.5.2,<14" ] [project.optional-dependencies] diff --git a/src/echo/models/scenario.py b/src/echo/models/scenario.py index 99eef9d4..62d928d5 100644 --- a/src/echo/models/scenario.py +++ b/src/echo/models/scenario.py @@ -50,10 +50,12 @@ class EchoConcreteModel(ConcreteModel): def engine_settings_from_environment(optimiser_engine: Optional[str] = None) -> EngineSettings: """Configure the optimiser through setting appropriate environmental variables.""" + DEFAULT_OPTIMISER_ENGINE = "cplex" + + # optimiser_engine is None or "" (empty string) if not optimiser_engine: - optimiser_engine = os.environ.get( - "OPTIMISER_ENGINE", "cplex" - ) # Default to cplex, as we seem to want quadratic costs + optimiser_engine = os.environ.get("OPTIMISER_ENGINE", DEFAULT_OPTIMISER_ENGINE) + optimiser_engine = optimiser_engine.lower() return EngineSettings( engine=optimiser_engine, @@ -61,3 +63,9 @@ def engine_settings_from_environment(optimiser_engine: Optional[str] = None) -> bigM=5000000, # This value has been arbitrarily chosen smallM=0.0001, # This value has been arbitrarily chosen ) + + +def can_optimiser_do_non_linear_optimisation(optimiser: str) -> bool: + """Returns True if optimiser can *only* do linear optimisations""" + linear_optimisers = ["cbc"] # TODO Make this list complete + return optimiser not in linear_optimisers diff --git a/tests/conftest.py b/tests/conftest.py index d095e248..fccb43d4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,15 +1,77 @@ import os import pytest +from rich.console import Console + +from echo.models import scenario + + +def default_testing_optimiser_engine(): + """The default test optimiser engine + + If this is changed, make sure that the replacement optimiser engine + is installed in the github action. + + This is deliberately not a fixture so it can be accessed in the `pytest_terminal_summary` hook + """ + return "cbc" + + +def testing_optimiser_engine(): + """Gets the optimiser engine for testing. + + Defaults to default_testing_optimiser_engine() but can be overriden by the + `TESTING_OPTIMISER_ENGINE` environment variable. + + This is deliberately not a fixture so it can be accessed in the `pytest_terminal_summary` hook + """ + return os.environ.get("TESTING_OPTIMISER_ENGINE", default_testing_optimiser_engine()) @pytest.fixture -def can_optimiser_do_non_linear_optimisation() -> bool: - # This test contains non-linear optimisation, so cbc won't be able to run it. Check the environment and skip this - # test if cbc is the optimiser engine - optimiser_engine = os.environ.get("OPTIMISER_ENGINE") +def engine_settings() -> scenario.EngineSettings: + """Fixture returning engine settings for tests + + This fixture should be used by all tests that required engine settings. + It is strongly recommended not to use `scenario.engine_settings_from_environment()` which is + intended for running echo and not testing echo. + """ + return scenario.engine_settings_from_environment(optimiser_engine=testing_optimiser_engine()) + + +# Based off this stackoverflow answer: https://stackoverflow.com/a/28198398 +@pytest.fixture(autouse=True) +def skip_by_optimiser_engine_not_do_non_linear(request, engine_settings): + optimiser = engine_settings.engine + + # If test marked as nonlinear... + if request.node.get_closest_marker("nonlinear"): + # and optimiser can't do non-linear optimisations... + if not scenario.can_optimiser_do_non_linear_optimisation(optimiser=optimiser): + # ...then skip + pytest.skip(f"Optimiser '{optimiser}' cannot do non-linear optimisation") + + +def pytest_terminal_summary(terminalreporter, exitstatus, config): + + DEFAULT_SOLVER = default_testing_optimiser_engine() - if optimiser_engine == "cbc": - return False + if testing_optimiser_engine() != DEFAULT_SOLVER: + return - return True + with Console() as console: + console.print( + "\n============================== Echo Solver Info ==============================", style="magenta" + ) + console.print( + f"This pytest run used the default solver '{DEFAULT_SOLVER}' (like the Github Action).", end="\n\n" + ) + console.print( + "It is important to be aware that certain solvers (e.g. 'cbc') cannot perform all tests.", end="\n\n" + ) + console.print("To run pytest with a different optimiser, set the 'TESTING_OPTIMISER_ENGINE'") + console.print("environment variable. For example, to use cplex:", end="\n\n") + console.print("$ TESTING_OPTIMISER_ENGINE=cplex pytest") + console.print( + "================================================================================", style="magenta" + ) diff --git a/tests/test_asset_types.py b/tests/test_asset_types.py index cff36b26..6b89f54a 100644 --- a/tests/test_asset_types.py +++ b/tests/test_asset_types.py @@ -6,7 +6,7 @@ from echo.models.carbon import CarbonAggregation, CarbonSink, CarbonSource from echo.models.electrical import ElectricalDemand, ElectricalPort, ElectricalStorage from echo.models.gas import FlexGasPort, GasBoilerFixedCOP -from echo.models.scenario import ScenarioSettings, engine_settings_from_environment +from echo.models.scenario import ScenarioSettings from echo.models.thermal import FixedThermalPort, HeatSink, SimpleChiller from echo.objectives.base import ObjectiveSet from echo.objectives.tariff import ImportTariff @@ -15,7 +15,7 @@ N_INTERVALS = 48 -def test_gas_boiler_fixed_cop(): +def test_gas_boiler_fixed_cop(engine_settings): expansion_periods = 1 time_periods = 48 interval_duration = 30 @@ -42,7 +42,7 @@ def test_gas_boiler_fixed_cop(): number_of_intervals=time_periods, number_of_expansion_intervals=expansion_periods, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, ) @@ -58,7 +58,7 @@ def test_gas_boiler_fixed_cop(): assert gas_mains[i] * boiler.cop == hl_p[(0, i)] * -1 -def test_modulating_gas_boiler(): +def test_modulating_gas_boiler(engine_settings): expansion_periods = 1 time_periods = 48 interval_duration = 30 @@ -85,7 +85,7 @@ def test_modulating_gas_boiler(): number_of_intervals=time_periods, number_of_expansion_intervals=expansion_periods, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, ) @@ -96,7 +96,7 @@ def test_modulating_gas_boiler(): assert boiler_input[i] * boiler.cop == -1 * boiler_output[i] -def test_chiller_operation(): +def test_chiller_operation(engine_settings): expansion_periods = 1 time_periods = 48 interval_duration = 30 @@ -127,7 +127,7 @@ def test_chiller_operation(): number_of_intervals=time_periods, number_of_expansion_intervals=expansion_periods, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, ) @@ -153,7 +153,7 @@ def test_chiller_operation(): assert chiller_input[i] == 3 -def test_carbon_aggregation(): +def test_carbon_aggregation(engine_settings): expansion_periods = 1 time_periods = 48 interval_duration = 30 @@ -210,7 +210,7 @@ def test_carbon_aggregation(): number_of_intervals=time_periods, number_of_expansion_intervals=expansion_periods, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, objective_set=ObjectiveSet(objective_list=[import_tariff]), ) diff --git a/tests/test_contingency_capacity.py b/tests/test_contingency_capacity.py index c3d49ff8..0049655a 100644 --- a/tests/test_contingency_capacity.py +++ b/tests/test_contingency_capacity.py @@ -4,13 +4,13 @@ from echo.models.agnostic import FlexPort, TellegenNode from echo.models.base import Node, OptimisationGraph from echo.models.electrical import ElectricalDemand, ElectricalGeneration, ElectricalStorage, Inverter -from echo.models.scenario import ScenarioSettings, engine_settings_from_environment +from echo.models.scenario import ScenarioSettings from echo.objectives.base import ObjectiveSet from echo.objectives.contingency import ContingencyNegative from echo.optimiser import optimise -def test_negative_contingency_respects_hybrid_inverter_constraints(): +def test_negative_contingency_respects_hybrid_inverter_constraints(engine_settings): expansion_periods = 1 time_periods = 48 interval_duration = 30 # min @@ -74,7 +74,7 @@ def test_negative_contingency_respects_hybrid_inverter_constraints(): number_of_intervals=time_periods, number_of_expansion_intervals=expansion_periods, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, objective_set=objective_set, ) @@ -87,7 +87,7 @@ def test_negative_contingency_respects_hybrid_inverter_constraints(): np.testing.assert_almost_equal(cont_neg_p[i], -5.0) -def test_negative_contingency_maximisation_curtails_solar(): +def test_negative_contingency_maximisation_curtails_solar(engine_settings): expansion_periods = 1 time_periods = 48 interval_duration = 30 # min @@ -153,7 +153,7 @@ def test_negative_contingency_maximisation_curtails_solar(): number_of_intervals=time_periods, number_of_expansion_intervals=expansion_periods, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, objective_set=objective_set, ) @@ -170,7 +170,7 @@ def test_negative_contingency_maximisation_curtails_solar(): np.testing.assert_almost_equal(sol_p[i], 0.0) -def test_negative_contingency_calculation_with_no_available_energy(): +def test_negative_contingency_calculation_with_no_available_energy(engine_settings): expansion_periods = 1 time_periods = 48 interval_duration = 30 # min @@ -235,7 +235,7 @@ def test_negative_contingency_calculation_with_no_available_energy(): number_of_intervals=time_periods, number_of_expansion_intervals=expansion_periods, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, objective_set=objective_set, ) diff --git a/tests/test_contingency_positive_capacity.py b/tests/test_contingency_positive_capacity.py index 44d00581..27efb344 100644 --- a/tests/test_contingency_positive_capacity.py +++ b/tests/test_contingency_positive_capacity.py @@ -4,13 +4,13 @@ from echo.models.agnostic import FlexPort, TellegenNode from echo.models.base import Node, OptimisationGraph from echo.models.electrical import ElectricalDemand, ElectricalGeneration, ElectricalStorage, Inverter -from echo.models.scenario import ScenarioSettings, engine_settings_from_environment +from echo.models.scenario import ScenarioSettings from echo.objectives.base import ObjectiveSet from echo.objectives.contingency import ContingencyPositive from echo.optimiser import optimise -def test_positive_contingency_unaffected_by_uncurtailable_solar_capacity(): +def test_positive_contingency_unaffected_by_uncurtailable_solar_capacity(engine_settings): expansion_periods = 1 time_periods = 48 interval_duration = 30 # min @@ -61,7 +61,7 @@ def test_positive_contingency_unaffected_by_uncurtailable_solar_capacity(): number_of_intervals=time_periods, number_of_expansion_intervals=expansion_periods, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, objective_set=objective_set, ) @@ -71,7 +71,7 @@ def test_positive_contingency_unaffected_by_uncurtailable_solar_capacity(): assert cont_pos_p[i] == 5.0 -def test_storage_discharge_and_solar_curtailment_to_maximise_positive_contingency_(): +def test_storage_discharge_and_solar_curtailment_to_maximise_positive_contingency_(engine_settings): expansion_periods = 1 time_periods = 48 interval_duration = 30 # min @@ -123,7 +123,7 @@ def test_storage_discharge_and_solar_curtailment_to_maximise_positive_contingenc number_of_intervals=time_periods, number_of_expansion_intervals=expansion_periods, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, objective_set=objective_set, ) @@ -192,7 +192,7 @@ def test_storage_discharge_and_solar_curtailment_to_maximise_positive_contingenc # assert sol_p[i] == 0.0 -def test_positive_contingency_calculation_with_storage_full(): +def test_positive_contingency_calculation_with_storage_full(engine_settings): expansion_periods = 1 time_periods = 48 interval_duration = 30 # min @@ -258,7 +258,7 @@ def test_positive_contingency_calculation_with_storage_full(): number_of_intervals=time_periods, number_of_expansion_intervals=expansion_periods, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, objective_set=objective_set, ) diff --git a/tests/test_evs.py b/tests/test_evs.py index 89ec77c7..6cfe78e8 100644 --- a/tests/test_evs.py +++ b/tests/test_evs.py @@ -10,14 +10,14 @@ from echo.models.base import Node, OptimisationGraph from echo.models.electrical import EV, ElectricalDemand, ElectricalGeneration, Inverter from echo.models.prebuilt import FlexElectricalNode -from echo.models.scenario import ScenarioSettings, engine_settings_from_environment +from echo.models.scenario import ScenarioSettings from echo.objectives.base import ObjectiveSet from echo.objectives.power import PeakNegativePower from echo.objectives.tariff import ImportTariff, ThroughputCost from echo.optimiser import optimise -def test_v0g(): +def test_v0g(engine_settings): # Set up hyper params time_periods = 96 # number of time periods to run the optimisation for interval_duration = 15 # each time period is 15 mins long @@ -75,12 +75,12 @@ def test_v0g(): number_of_expansion_intervals=expansion_periods, discount_rate=discount_rate, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, ) -def test_v0g_2(): +def test_v0g_2(engine_settings): """Like test_v0g, just different parameters.""" # Set up hyper params @@ -140,12 +140,12 @@ def test_v0g_2(): number_of_expansion_intervals=expansion_periods, discount_rate=discount_rate, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, ) -def test_v0g_3(): +def test_v0g_3(engine_settings): """Like test_v0g_2, just different parameters, but discharge first, then charge.""" # Set up hyper params @@ -205,12 +205,12 @@ def test_v0g_3(): number_of_expansion_intervals=expansion_periods, discount_rate=discount_rate, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, ) -def test_v0g_with_stateful_data_injection(): +def test_v0g_with_stateful_data_injection(engine_settings): # Extends off test_v0g() # Available and usage combinations that work @@ -298,12 +298,12 @@ def test_v0g_with_stateful_data_injection(): number_of_expansion_intervals=expansion_periods, discount_rate=discount_rate, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, ) -def test_v0g_output_matches_expectation(): +def test_v0g_output_matches_expectation(engine_settings): """This example if taken from scripts/manual_examples/ev_example. The aim of this test is to determine if the output of a v0g ev optimisation matches expectations, ie. does the graph @@ -372,7 +372,7 @@ def test_v0g_output_matches_expectation(): number_of_expansion_intervals=expansion_periods, discount_rate=discount_rate, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, ) @@ -442,7 +442,7 @@ def test_v0g_output_matches_expectation(): assert np.array_equal(soc, expected_soc) -def test_v0g_output_matches_expectation_after_initialise_data_with_expanding_dataset(): +def test_v0g_output_matches_expectation_after_initialise_data_with_expanding_dataset(engine_settings): """This example builds on test_v0g_output_matches_expectation. The aim of this test is to determine if the output of a v0g ev optimisation matches expectations, ie. does the graph @@ -537,7 +537,7 @@ def test_v0g_output_matches_expectation_after_initialise_data_with_expanding_dat number_of_expansion_intervals=expansion_periods, discount_rate=discount_rate, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, ) @@ -600,7 +600,7 @@ def test_v0g_output_matches_expectation_after_initialise_data_with_expanding_dat assert np.array_equal(soc, expected_soc) -def test_v0g_output_matches_expectation_after_initialise_data_with_contracting_dataset(): +def test_v0g_output_matches_expectation_after_initialise_data_with_contracting_dataset(engine_settings): """This example builds on test_v0g_output_matches_expectation_after_initialise_data_with_expanding_dataset The aim of this test is to determine if the output of a v0g ev optimisation matches expectations, ie. does the graph @@ -701,7 +701,7 @@ def test_v0g_output_matches_expectation_after_initialise_data_with_contracting_d number_of_expansion_intervals=expansion_periods, discount_rate=discount_rate, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, ) @@ -718,7 +718,7 @@ def test_v0g_output_matches_expectation_after_initialise_data_with_contracting_d # pass -def test_v1g_no_objective(): +def test_v1g_no_objective(engine_settings): # Set up hyper params time_periods = 96 # number of time periods to run the optimisation for interval_duration = 15 # each time period is 15 mins long @@ -776,12 +776,12 @@ def test_v1g_no_objective(): number_of_expansion_intervals=expansion_periods, discount_rate=discount_rate, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, ) -def test_v1g_no_objective_2(): +def test_v1g_no_objective_2(engine_settings): """Like test_v1g, just different parameters.""" # Set up hyper params @@ -841,12 +841,12 @@ def test_v1g_no_objective_2(): number_of_expansion_intervals=expansion_periods, discount_rate=discount_rate, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, ) -def test_v1g_no_objective_3(): +def test_v1g_no_objective_3(engine_settings): """Like test_v1g_2, just different parameters, but discharge first, then charge.""" # Set up hyper params @@ -906,12 +906,12 @@ def test_v1g_no_objective_3(): number_of_expansion_intervals=expansion_periods, discount_rate=discount_rate, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, ) -def test_v2g_no_objective(): +def test_v2g_no_objective(engine_settings): # Set up hyper params time_periods = 96 # number of time periods to run the optimisation for interval_duration = 15 # each time period is 15 mins long @@ -969,12 +969,12 @@ def test_v2g_no_objective(): number_of_expansion_intervals=expansion_periods, discount_rate=discount_rate, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, ) -def test_v2g_no_objective_2(): +def test_v2g_no_objective_2(engine_settings): """Like test_v1g, just different parameters.""" # Set up hyper params @@ -1034,12 +1034,12 @@ def test_v2g_no_objective_2(): number_of_expansion_intervals=expansion_periods, discount_rate=discount_rate, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, ) -def test_v2g_no_objective_3(): +def test_v2g_no_objective_3(engine_settings): """Like test_v1g_2, just different parameters, but discharge first, then charge.""" # Set up hyper params @@ -1099,12 +1099,12 @@ def test_v2g_no_objective_3(): number_of_expansion_intervals=expansion_periods, discount_rate=discount_rate, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, ) -def test_simple_v1g_with_stateful_data_injection(): +def test_simple_v1g_with_stateful_data_injection(engine_settings): """Get V1G to behave properly""" good_available_usage = (np.array([1, 1, 0, 0, 1, 1]), np.array([0, 0, 10, 10, 0, 0])) good_soc = np.array([50, 50, 40, 30, 30, 30]) @@ -1189,7 +1189,7 @@ def test_simple_v1g_with_stateful_data_injection(): number_of_expansion_intervals=expansion_periods, discount_rate=discount_rate, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, verbose=False, ) @@ -1201,7 +1201,7 @@ def test_simple_v1g_with_stateful_data_injection(): assert np.allclose(soc, good_soc, rtol=10**-5) -def test_simple_v2g_with_stateful_data_injection_2(): +def test_simple_v2g_with_stateful_data_injection_2(engine_settings): """Get V1G to behave properly""" good_available_usage = (np.array([1, 1, 0, 0, 1, 1]), np.array([0, 0, 10, 10, 0, 0])) @@ -1284,7 +1284,7 @@ def test_simple_v2g_with_stateful_data_injection_2(): number_of_expansion_intervals=expansion_periods, discount_rate=discount_rate, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, verbose=False, ) @@ -1299,7 +1299,7 @@ def test_simple_v2g_with_stateful_data_injection_2(): assert any(np.not_equal(soc, np.array([0] * 6))) -def test_simple_v1g_with_stateful_data_injection_2(): +def test_simple_v1g_with_stateful_data_injection_2(engine_settings): """Get V1G to behave properly""" # Available and usage combinations that work @@ -1403,7 +1403,7 @@ def test_simple_v1g_with_stateful_data_injection_2(): number_of_expansion_intervals=expansion_periods, discount_rate=discount_rate, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, verbose=False, ) @@ -1428,7 +1428,7 @@ def test_simple_v1g_with_stateful_data_injection_2(): assert np.allclose(ev_to_cp_flow, np.array([0, 0, 0, 0, 5, 0]), rtol=10**-5) -def test_simple_v0g_with_stateful_data_injection_for_invalid_input_detection(): +def test_simple_v0g_with_stateful_data_injection_for_invalid_input_detection(engine_settings): """Get V1G to behave properly""" bad_available_usages: List[Tuple[list, list]] = [ @@ -1524,13 +1524,13 @@ def test_simple_v0g_with_stateful_data_injection_for_invalid_input_detection(): number_of_expansion_intervals=expansion_periods, discount_rate=discount_rate, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, verbose=False, ) -def test_v1g_with_objective(): +def test_v1g_with_objective(engine_settings): """Like test_v1g_no_objective_2, but with objective""" # Set up hyper params @@ -1618,7 +1618,7 @@ def test_v1g_with_objective(): number_of_expansion_intervals=expansion_periods, discount_rate=discount_rate, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, objective_set=objective_set, ) @@ -1630,7 +1630,7 @@ def test_v1g_with_objective(): assert np.allclose(soc, expected_soc, rtol=10**-5) -def test_v1g_with_load_with_objective(): +def test_v1g_with_load_with_objective(engine_settings): """Like test_v1g_with_objective, but with a load. Expect delayed charging behaviour. @@ -1731,7 +1731,7 @@ def test_v1g_with_load_with_objective(): number_of_expansion_intervals=expansion_periods, discount_rate=discount_rate, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, objective_set=objective_set, ) @@ -1743,7 +1743,7 @@ def test_v1g_with_load_with_objective(): assert np.allclose(soc, expected_soc, rtol=10**-5) -def test_v2g_with_load_with_objective(): +def test_v2g_with_load_with_objective(engine_settings): """Like test_v1g_with_load_with_objective, but with a load. Expect delayed charging behaviour. @@ -1848,7 +1848,7 @@ def test_v2g_with_load_with_objective(): number_of_expansion_intervals=expansion_periods, discount_rate=discount_rate, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, objective_set=objective_set, ) @@ -1860,7 +1860,7 @@ def test_v2g_with_load_with_objective(): assert np.allclose(soc, expected_soc, rtol=10**-5) -def test_v2g_with_load_with_objective_v2g_behaviour(): +def test_v2g_with_load_with_objective_v2g_behaviour(engine_settings): """Like test_v1g_with_load_with_objective, but with a load. Expect delayed EV to supply some power to load behaviour. @@ -1965,7 +1965,7 @@ def test_v2g_with_load_with_objective_v2g_behaviour(): number_of_expansion_intervals=expansion_periods, discount_rate=discount_rate, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, objective_set=objective_set, ) @@ -1977,7 +1977,7 @@ def test_v2g_with_load_with_objective_v2g_behaviour(): assert np.allclose(soc, expected_soc, rtol=10**-5) -def test_v1g_with_load_with_objective_with_stateful_data_injection(): +def test_v1g_with_load_with_objective_with_stateful_data_injection(engine_settings): """Like test_v1g_with_load_with_objective, now using the initialise data function. This test is a bit different to the V0G tests with initialise data in that this will just replicate the procedure @@ -2098,7 +2098,7 @@ def test_v1g_with_load_with_objective_with_stateful_data_injection(): number_of_expansion_intervals=expansion_periods, discount_rate=discount_rate, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, objective_set=objective_set, ) @@ -2110,7 +2110,7 @@ def test_v1g_with_load_with_objective_with_stateful_data_injection(): assert np.allclose(soc, expected_soc, rtol=10**-5) -def test_v2g_with_load_with_objective_with_stateful_data_injection(): +def test_v2g_with_load_with_objective_with_stateful_data_injection(engine_settings): """Like test_v2g_with_load_with_objective, now using the initialise data function. This test is a bit different to the V0G tests with initialise data in that this will just replicate the procedure @@ -2235,7 +2235,7 @@ def test_v2g_with_load_with_objective_with_stateful_data_injection(): number_of_expansion_intervals=expansion_periods, discount_rate=discount_rate, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, objective_set=objective_set, ) @@ -2337,7 +2337,7 @@ def test_node_and_port_uids_on_ev_are_set_properly_when_injecting_stateful_data( assert old_port_uids == new_port_uids -def test_v1g_with_load_with_objective_with_stateful_data_injection_with_mes_defaults(): +def test_v1g_with_load_with_objective_with_stateful_data_injection_with_mes_defaults(engine_settings): """Like test_v1g_with_load_with_objective_with_stateful_data_injection but with mes defaults for initial ev attributes. @@ -2460,7 +2460,7 @@ def test_v1g_with_load_with_objective_with_stateful_data_injection_with_mes_defa number_of_expansion_intervals=expansion_periods, discount_rate=discount_rate, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, objective_set=objective_set, ) diff --git a/tests/test_hybrid_inverter_behaviour.py b/tests/test_hybrid_inverter_behaviour.py index fbe79af0..9c5a868c 100644 --- a/tests/test_hybrid_inverter_behaviour.py +++ b/tests/test_hybrid_inverter_behaviour.py @@ -4,12 +4,12 @@ from echo.models.agnostic import FlexPort, TellegenNode from echo.models.base import Node, OptimisationGraph from echo.models.electrical import ElectricalDemand, ElectricalGeneration, ElectricalStorage, Inverter -from echo.models.scenario import ScenarioSettings, engine_settings_from_environment +from echo.models.scenario import ScenarioSettings from echo.objectives.base import ObjectiveSet, TotalFlow from echo.optimiser import optimise -def test_hybrid_inverter_limits_battery_discharge_rate(): +def test_hybrid_inverter_limits_battery_discharge_rate(engine_settings): expansion_periods = 1 time_periods = 48 interval_duration = 30 # min @@ -68,7 +68,7 @@ def test_hybrid_inverter_limits_battery_discharge_rate(): number_of_intervals=time_periods, number_of_expansion_intervals=expansion_periods, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, objective_set=ObjectiveSet(objective_list=[TotalFlow(component=grid.ports["grid"])]), ) @@ -77,7 +77,7 @@ def test_hybrid_inverter_limits_battery_discharge_rate(): np.testing.assert_almost_equal(optimise_results.values(cp.ports["grid"].port_name, 0)[i], 1.0) -def test_hybrid_inverter_limits_path_flows(): +def test_hybrid_inverter_limits_path_flows(engine_settings): expansion_periods = 1 time_periods = 48 interval_duration = 30 # min @@ -138,7 +138,7 @@ def test_hybrid_inverter_limits_path_flows(): number_of_intervals=time_periods, number_of_expansion_intervals=expansion_periods, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, objective_set=ObjectiveSet(objective_list=[TotalFlow(component=grid.ports["grid"])]), ) diff --git a/tests/test_inverter_efficiency_behaviour.py b/tests/test_inverter_efficiency_behaviour.py index f9bd52f9..74eab18d 100644 --- a/tests/test_inverter_efficiency_behaviour.py +++ b/tests/test_inverter_efficiency_behaviour.py @@ -4,14 +4,14 @@ from echo.models.agnostic import FlexPort, TellegenNode from echo.models.base import Node, OptimisationGraph from echo.models.electrical import ElectricalDemand, ElectricalGeneration, ElectricalStorage, Inverter -from echo.models.scenario import ScenarioSettings, engine_settings_from_environment +from echo.models.scenario import ScenarioSettings from echo.objectives.base import ObjectiveSet, TotalExportFlow, TotalImportFlow from echo.optimiser import optimise N_INTERVALS = 48 -def test_hybrid_inverter_dc_ac_efficiency(): +def test_hybrid_inverter_dc_ac_efficiency(engine_settings): expansion_periods = 1 time_periods = 48 interval_duration = 30 # min @@ -71,7 +71,7 @@ def test_hybrid_inverter_dc_ac_efficiency(): number_of_intervals=time_periods, number_of_expansion_intervals=expansion_periods, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, objective_set=ObjectiveSet(objective_list=[TotalImportFlow(component=cp.ports["grid"])]), ) @@ -91,7 +91,7 @@ def test_hybrid_inverter_dc_ac_efficiency(): np.testing.assert_almost_equal(sol_p[i] + sto_p[i], inv_p[i] / 0.9, 6) -def test_hybrid_inverter_dc_dc_efficiency(): +def test_hybrid_inverter_dc_dc_efficiency(engine_settings): expansion_periods = 1 time_periods = 48 interval_duration = 30 # min @@ -153,7 +153,7 @@ def test_hybrid_inverter_dc_dc_efficiency(): number_of_intervals=time_periods, number_of_expansion_intervals=expansion_periods, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, objective_set=ObjectiveSet(objective_list=[TotalImportFlow(component=cp.ports["grid"])]), ) diff --git a/tests/test_load_control_schemes.py b/tests/test_load_control_schemes.py index 93a3dd86..4cebbbb8 100644 --- a/tests/test_load_control_schemes.py +++ b/tests/test_load_control_schemes.py @@ -4,12 +4,12 @@ from echo.models.agnostic import FlexPort, TellegenNode, TimeDelayNode from echo.models.base import Node, OptimisationGraph, Transform, TransformNode, TransformTerm from echo.models.electrical import BoundedElectricalLoad, ElectricalDemand, ElectricalPort -from echo.models.scenario import ScenarioSettings, engine_settings_from_environment +from echo.models.scenario import ScenarioSettings from echo.objectives.base import ObjectiveSet, TotalFlow from echo.optimiser import optimise -def test_simple_bounded_load(): +def test_simple_bounded_load(engine_settings): expansion_periods = 1 time_periods = 48 interval_duration = 30 @@ -34,7 +34,7 @@ def test_simple_bounded_load(): number_of_intervals=time_periods, number_of_expansion_intervals=expansion_periods, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, objective_set=ObjectiveSet(objective_list=[TotalFlow(component=grid.ports["grid"])]), ) @@ -47,7 +47,7 @@ def test_simple_bounded_load(): @pytest.mark.parametrize("time_delay", [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0]) -def test_time_delay_node(time_delay): +def test_time_delay_node(engine_settings, time_delay): expansion_periods = 1 time_periods = 48 interval_duration = 30 @@ -76,7 +76,7 @@ def test_time_delay_node(time_delay): number_of_intervals=time_periods, number_of_expansion_intervals=expansion_periods, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, ) @@ -91,7 +91,7 @@ def test_time_delay_node(time_delay): assert load_import[i] == -1 * grid[int(i - td.time_delay)] -def test_feedback_loop(): +def test_feedback_loop(engine_settings): expansion_periods = 1 time_periods = 48 interval_duration = 30 @@ -142,7 +142,7 @@ def test_feedback_loop(): number_of_intervals=time_periods, number_of_expansion_intervals=expansion_periods, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, ) diff --git a/tests/test_new_demand_tariffs.py b/tests/test_new_demand_tariffs.py index d8b5dd90..7c4bebc2 100644 --- a/tests/test_new_demand_tariffs.py +++ b/tests/test_new_demand_tariffs.py @@ -4,13 +4,13 @@ from echo.models.agnostic import FlexPort from echo.models.base import Node, OptimisationGraph from echo.models.electrical import ElectricalDemand, ElectricalGeneration -from echo.models.scenario import ScenarioSettings, engine_settings_from_environment +from echo.models.scenario import ScenarioSettings from echo.objectives.base import ObjectiveSet from echo.objectives.tariff import DemandTariffObjective, ExportDemandCharge, ImportDemandCharge from echo.optimiser import optimise -def test_system_import_demand_tariff(): +def test_system_import_demand_tariff(engine_settings): """Test that we correctly calculate the max import demand""" expansion_periods = 1 @@ -50,7 +50,7 @@ def test_system_import_demand_tariff(): number_of_intervals=time_periods, number_of_expansion_intervals=expansion_periods, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, objective_set=objective_set, ) @@ -60,7 +60,7 @@ def test_system_import_demand_tariff(): np.testing.assert_array_almost_equal(max_demand, max_in_window - minimum_demand) -def test_system_export_demand_tariff(): +def test_system_export_demand_tariff(engine_settings): """Test that we correctly calculate the max export demand""" expansion_periods = 1 @@ -100,7 +100,7 @@ def test_system_export_demand_tariff(): number_of_intervals=time_periods, number_of_expansion_intervals=expansion_periods, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, objective_set=objective_set, ) @@ -110,7 +110,7 @@ def test_system_export_demand_tariff(): np.testing.assert_array_almost_equal(max_demand, max_in_window - minimum_demand) -def test_system_import_demand_tariff_two_resets(): +def test_system_import_demand_tariff_two_resets(engine_settings): """Test that we correctly calculate the max import demand when we use a reset period""" expansion_periods = 1 @@ -150,7 +150,7 @@ def test_system_import_demand_tariff_two_resets(): number_of_intervals=time_periods, number_of_expansion_intervals=expansion_periods, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, objective_set=objective_set, ) diff --git a/tests/test_objective_functions.py b/tests/test_objective_functions.py index d589c58d..72e2d13a 100644 --- a/tests/test_objective_functions.py +++ b/tests/test_objective_functions.py @@ -4,13 +4,13 @@ from echo.models.agnostic import FlexPort, TellegenNode from echo.models.base import Node, OptimisationGraph from echo.models.electrical import ElectricalDemand, ElectricalGeneration, ElectricalStorage, Inverter -from echo.models.scenario import ScenarioSettings, engine_settings_from_environment +from echo.models.scenario import ScenarioSettings from echo.objectives.base import ObjectiveSet from echo.objectives.tariff import DemandTariffObjective, ExportTariff, ImportDemandCharge, ImportTariff, ThroughputCost from echo.optimiser import optimise -def test_objectives_sum_correctly(): +def test_objectives_sum_correctly(engine_settings): expansion_periods = 1 time_periods = 48 interval_duration = 30 # min @@ -95,7 +95,7 @@ def test_objectives_sum_correctly(): number_of_intervals=time_periods, number_of_expansion_intervals=expansion_periods, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, objective_set=obj_set, ) diff --git a/tests/test_path_tracing.py b/tests/test_path_tracing.py index 7f0fde4f..d73025ff 100644 --- a/tests/test_path_tracing.py +++ b/tests/test_path_tracing.py @@ -1,14 +1,15 @@ import numpy as np +import pytest from echo.configuration import Units from echo.models.agnostic import FlexPort, TellegenNode from echo.models.base import Node, OptimisationGraph from echo.models.electrical import ElectricalDemand, ElectricalGeneration, ElectricalStorage, Inverter -from echo.models.scenario import ScenarioSettings, engine_settings_from_environment +from echo.models.scenario import ScenarioSettings from echo.optimiser import optimise -def test_partitioning_regions_for_path_flow(): +def test_partitioning_regions_for_path_flow(engine_settings): expansion_periods = 1 time_periods = 48 interval_duration = 30 # min @@ -72,7 +73,7 @@ def test_partitioning_regions_for_path_flow(): number_of_intervals=time_periods, number_of_expansion_intervals=expansion_periods, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, ) @@ -95,10 +96,8 @@ def test_partitioning_regions_for_path_flow(): ) -def test_regularisation_of_path_flows(can_optimiser_do_non_linear_optimisation): - # Check if the optimiser can do non-linear problems. If not, skip test. - if not can_optimiser_do_non_linear_optimisation: - return +@pytest.mark.nonlinear +def test_regularisation_of_path_flows(engine_settings): expansion_periods = 1 time_periods = 48 @@ -143,7 +142,7 @@ def test_regularisation_of_path_flows(can_optimiser_do_non_linear_optimisation): number_of_intervals=time_periods, number_of_expansion_intervals=expansion_periods, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, ) diff --git a/tests/test_peak_power_objective.py b/tests/test_peak_power_objective.py index fff11e87..69e7a109 100644 --- a/tests/test_peak_power_objective.py +++ b/tests/test_peak_power_objective.py @@ -1,9 +1,10 @@ import numpy as np +import pytest from echo.configuration import Units from echo.models.agnostic import ControlledLoad, FlexPort from echo.models.base import Node, OptimisationGraph -from echo.models.scenario import ScenarioSettings, engine_settings_from_environment +from echo.models.scenario import ScenarioSettings from echo.objectives.base import ObjectiveSet from echo.objectives.power import QuadraticPower from echo.optimiser import optimise @@ -11,10 +12,8 @@ N_INTERVALS = 48 -def test_controlled_load_with_peak_power_objective(can_optimiser_do_non_linear_optimisation): - # Check if the optimiser can do non-linear problems. If not, skip test. - if not can_optimiser_do_non_linear_optimisation: - return +@pytest.mark.nonlinear +def test_controlled_load_with_peak_power_objective(engine_settings): expansion_periods = 1 time_periods = 48 @@ -41,7 +40,7 @@ def test_controlled_load_with_peak_power_objective(can_optimiser_do_non_linear_o number_of_intervals=time_periods, number_of_expansion_intervals=expansion_periods, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, objective_set=objective_set, ) diff --git a/tests/test_peak_power_optimisation.py b/tests/test_peak_power_optimisation.py index ff244520..44985e4e 100644 --- a/tests/test_peak_power_optimisation.py +++ b/tests/test_peak_power_optimisation.py @@ -2,7 +2,7 @@ from echo.models.agnostic import FlexPort, TellegenNode from echo.models.base import Node, OptimisationGraph from echo.models.electrical import ElectricalDemand, ElectricalPort, ElectricalStorage -from echo.models.scenario import ScenarioSettings, engine_settings_from_environment +from echo.models.scenario import ScenarioSettings from echo.objectives.base import ObjectiveSet from echo.objectives.power import PeakNegativePower, PeakPositivePower from echo.optimiser import optimise @@ -10,7 +10,7 @@ N_INTERVALS = 48 -def test_peak_positive_power_objective(): +def test_peak_positive_power_objective(engine_settings): expansion_periods = 1 time_periods = 48 interval_duration = 30 @@ -55,7 +55,7 @@ def test_peak_positive_power_objective(): number_of_intervals=time_periods, number_of_expansion_intervals=expansion_periods, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, objective_set=objective_set, ) @@ -67,7 +67,7 @@ def test_peak_positive_power_objective(): assert sto_p[i] == -0.25 -def test_peak_negative_power_objective(): +def test_peak_negative_power_objective(engine_settings): expansion_periods = 1 time_periods = 48 interval_duration = 30 @@ -113,7 +113,7 @@ def test_peak_negative_power_objective(): number_of_intervals=time_periods, number_of_expansion_intervals=expansion_periods, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, objective_set=objective_set, ) diff --git a/tests/test_simple_controlled_load.py b/tests/test_simple_controlled_load.py index 8bc9a5fd..ef0f8f6e 100644 --- a/tests/test_simple_controlled_load.py +++ b/tests/test_simple_controlled_load.py @@ -3,12 +3,12 @@ from echo.configuration import Units from echo.models.agnostic import ControlledLoad, FlexPort from echo.models.base import Edge, Node, OptimisationGraph -from echo.models.scenario import ScenarioSettings, engine_settings_from_environment +from echo.models.scenario import ScenarioSettings from echo.objectives.base import ObjectiveSet, TotalExportFlow, TotalFlow, TotalImportFlow from echo.optimiser import optimise -def test_simple_controlled_load_does_minimum_energy_action(): +def test_simple_controlled_load_does_minimum_energy_action(engine_settings): expansion_periods = 1 time_periods = 48 interval_duration = 30 @@ -31,7 +31,7 @@ def test_simple_controlled_load_does_minimum_energy_action(): number_of_intervals=time_periods, number_of_expansion_intervals=expansion_periods, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, objective_set=ObjectiveSet(objective_list=[TotalExportFlow(component=grid.ports["grid"])]), ) @@ -47,7 +47,7 @@ def test_simple_controlled_load_does_minimum_energy_action(): # assert optimiser.values(grid.ports['grid'].port_name, 0)[i] == optimiser.values(cl.port_name, 0)[i]*-1 -def test_simple_controlled_load_does_minimum_power_action(): +def test_simple_controlled_load_does_minimum_power_action(engine_settings): expansion_periods = 1 time_periods = 48 interval_duration = 30 @@ -71,7 +71,7 @@ def test_simple_controlled_load_does_minimum_power_action(): number_of_intervals=time_periods, number_of_expansion_intervals=expansion_periods, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, objective_set=ObjectiveSet(objective_list=[TotalExportFlow(component=grid.ports["grid"])]), ) @@ -84,7 +84,7 @@ def test_simple_controlled_load_does_minimum_power_action(): np.testing.assert_almost_equal(grid_export[i], load_import[i]) -def test_simple_controlled_load_limited_to_max_energy(): +def test_simple_controlled_load_limited_to_max_energy(engine_settings): expansion_periods = 1 time_periods = 48 interval_duration = 30 @@ -109,7 +109,7 @@ def test_simple_controlled_load_limited_to_max_energy(): number_of_intervals=time_periods, number_of_expansion_intervals=expansion_periods, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, objective_set=ObjectiveSet(objective_list=[TotalExportFlow(component=grid.ports["grid"], minimise=False)]), ) @@ -125,7 +125,7 @@ def test_simple_controlled_load_limited_to_max_energy(): # assert grid_export[i] == load_import[i] -def test_simple_controlled_load_limited_to_max_power(): +def test_simple_controlled_load_limited_to_max_power(engine_settings): expansion_periods = 1 time_periods = 48 interval_duration = 30 @@ -150,7 +150,7 @@ def test_simple_controlled_load_limited_to_max_power(): number_of_intervals=time_periods, number_of_expansion_intervals=expansion_periods, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, objective_set=ObjectiveSet(objective_list=[TotalImportFlow(component=grid.ports["grid"], minimise=False)]), ) diff --git a/tests/test_simple_solar_curtailment.py b/tests/test_simple_solar_curtailment.py index 9eda68f6..90aeecca 100644 --- a/tests/test_simple_solar_curtailment.py +++ b/tests/test_simple_solar_curtailment.py @@ -4,14 +4,14 @@ from echo.models.agnostic import FlexPort, FlexSink, TellegenNode from echo.models.base import Node, OptimisationGraph from echo.models.electrical import ElectricalGeneration -from echo.models.scenario import ScenarioSettings, engine_settings_from_environment +from echo.models.scenario import ScenarioSettings from echo.objectives.base import ObjectiveSet, TotalFlow, TotalImportFlow from echo.optimiser import optimise N_INTERVALS = 48 -def test_solar_generation_limited_by_inverter_size(): +def test_solar_generation_limited_by_inverter_size(engine_settings): expansion_periods = 1 time_periods = 48 interval_duration = 30 # min @@ -50,7 +50,7 @@ def test_solar_generation_limited_by_inverter_size(): number_of_intervals=time_periods, number_of_expansion_intervals=expansion_periods, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, objective_set=ObjectiveSet(objective_list=[TotalFlow(component=inverter.ports["cp"], minimise=False)]), ) @@ -66,7 +66,7 @@ def test_solar_generation_limited_by_inverter_size(): np.testing.assert_almost_equal(cp_p[i], min(i, 5.0)) -def test_non_curtailable_system_not_curtailed(): +def test_non_curtailable_system_not_curtailed(engine_settings): expansion_periods = 1 time_periods = 48 interval_duration = 30 # min @@ -98,7 +98,7 @@ def test_non_curtailable_system_not_curtailed(): number_of_intervals=time_periods, number_of_expansion_intervals=expansion_periods, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, objective_set=ObjectiveSet(objective_list=[TotalImportFlow(component=grid.ports["grid"])]), ) @@ -113,7 +113,7 @@ def test_non_curtailable_system_not_curtailed(): np.testing.assert_almost_equal(root_p[i], 5.0) -def test_curtailable_system_curtailed(): +def test_curtailable_system_curtailed(engine_settings): expansion_periods = 1 time_periods = 48 interval_duration = 30 # min @@ -147,7 +147,7 @@ def test_curtailable_system_curtailed(): number_of_intervals=time_periods, number_of_expansion_intervals=expansion_periods, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, objective_set=ObjectiveSet(objective_list=[TotalFlow(component=grid.ports["grid"])]), ) diff --git a/tests/test_slack_variables.py b/tests/test_slack_variables.py index fe611311..48c656ed 100644 --- a/tests/test_slack_variables.py +++ b/tests/test_slack_variables.py @@ -4,14 +4,14 @@ from echo.models.agnostic import FlexPort, TellegenNode from echo.models.base import Node, OptimisationGraph from echo.models.electrical import ElectricalGeneration -from echo.models.scenario import ScenarioSettings, engine_settings_from_environment +from echo.models.scenario import ScenarioSettings from echo.objectives.base import ObjectiveSet, TotalFlow from echo.optimiser import optimise N_INTERVALS = 48 -def test_export_slack_var_is_minimised(): +def test_export_slack_var_is_minimised(engine_settings): """Connect curtailable solar to a connection pt with a flow constraint and slack vars enabled. The optimiser should curtail the solar rather than allowing the slack to be nonzero.""" expansion_periods = 1 @@ -45,7 +45,7 @@ def test_export_slack_var_is_minimised(): number_of_intervals=time_periods, number_of_expansion_intervals=expansion_periods, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, objective_set=ObjectiveSet(objective_list=[TotalFlow(component=grid.ports["grid"], minimise=False)]), ) @@ -55,7 +55,7 @@ def test_export_slack_var_is_minimised(): np.testing.assert_almost_equal(inv_export_slack, 0) -def test_import_slack_var_is_minimised(): +def test_import_slack_var_is_minimised(engine_settings): expansion_periods = 1 time_periods = 48 interval_duration = 30 # min @@ -87,7 +87,7 @@ def test_import_slack_var_is_minimised(): number_of_intervals=time_periods, number_of_expansion_intervals=expansion_periods, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, objective_set=ObjectiveSet(objective_list=[TotalFlow(component=pv1, minimise=False)]), ) @@ -98,7 +98,7 @@ def test_import_slack_var_is_minimised(): np.testing.assert_almost_equal(g_import_slack, 0) -def test_slack_vars_take_up_slack_when_forced_to(): +def test_slack_vars_take_up_slack_when_forced_to(engine_settings): expansion_periods = 1 time_periods = 48 interval_duration = 30 # min @@ -130,7 +130,7 @@ def test_slack_vars_take_up_slack_when_forced_to(): number_of_intervals=time_periods, number_of_expansion_intervals=expansion_periods, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, ) diff --git a/tests/test_tariffs.py b/tests/test_tariffs.py index f9ae2167..55b87e2c 100644 --- a/tests/test_tariffs.py +++ b/tests/test_tariffs.py @@ -2,7 +2,7 @@ import numpy as np import pytest -from hypothesis import given, settings +from hypothesis import HealthCheck, given, settings from hypothesis.extra.numpy import arrays from hypothesis.strategies import floats @@ -11,7 +11,7 @@ from echo.models.base import Node, OptimisationGraph from echo.models.electrical import ElectricalDemand, ElectricalPort, ElectricalStorage from echo.models.prebuilt import FlexNode, Load -from echo.models.scenario import ScenarioSettings, engine_settings_from_environment +from echo.models.scenario import ScenarioSettings from echo.objectives.base import ObjectiveSet from echo.objectives.tariff import ( BlockImportTariff, @@ -28,7 +28,7 @@ "minimum_demand,demand", [(0.0, 1.0), (1.0, 1.0), (2.0, 1.0), (0.0, 2.0), (1.0, 2.0), (2.0, 2.0)] ) @pytest.mark.parametrize("battery_capacity", [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0]) -def test_system_precharges_for_demand_tariff(demand, minimum_demand, battery_capacity): +def test_system_precharges_for_demand_tariff(engine_settings, demand, minimum_demand, battery_capacity): """ Test that we appropriately minimise the demand charge associated with the demand period. The tests use the minimal objective function that gives a well-defined result, @@ -91,7 +91,7 @@ def test_system_precharges_for_demand_tariff(demand, minimum_demand, battery_cap number_of_intervals=time_periods, number_of_expansion_intervals=expansion_periods, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, objective_set=objective_set, ) @@ -106,9 +106,9 @@ def test_system_precharges_for_demand_tariff(demand, minimum_demand, battery_cap ) -@settings(deadline=3000) +@settings(deadline=3000, suppress_health_check=(HealthCheck.function_scoped_fixture,)) @given(arrays(float, 12, elements=floats(1, 100))) -def test_demand_charge_minimised_given_random_demand_in_period(demand_period_demand): +def test_demand_charge_minimised_given_random_demand_in_period(engine_settings, demand_period_demand): minimum_demand = 0.0 demand = np.concatenate([np.zeros(24), demand_period_demand, np.ones(12)]) @@ -182,7 +182,7 @@ def test_demand_charge_minimised_given_random_demand_in_period(demand_period_dem number_of_intervals=time_periods, number_of_expansion_intervals=expansion_periods, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, objective_set=objective_set, ) @@ -198,7 +198,7 @@ def test_demand_charge_minimised_given_random_demand_in_period(demand_period_dem np.testing.assert_array_almost_equal(optimise_results.values(b1.neg, 0)[24:36], expected_discharge, 3) -def test_system_path_flows_adjust_to_path_tariffs(): +def test_system_path_flows_adjust_to_path_tariffs(engine_settings): """Tests whether path flows reroute based on path tariffs""" expansion_periods = 1 @@ -255,7 +255,7 @@ def test_system_path_flows_adjust_to_path_tariffs(): number_of_intervals=time_periods, number_of_expansion_intervals=expansion_periods, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, objective_set=objective_set, ) @@ -266,7 +266,7 @@ def test_system_path_flows_adjust_to_path_tariffs(): np.testing.assert_array_almost_equal(grid_to_load_vals[24:36], np.ones(12) * net_load) -def test_path_flows_respect_port_constraints(): +def test_path_flows_respect_port_constraints(engine_settings): expansion_periods = 1 time_periods = 48 interval_duration = 30 @@ -324,7 +324,7 @@ def test_path_flows_respect_port_constraints(): number_of_intervals=time_periods, number_of_expansion_intervals=expansion_periods, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, objective_set=objective_set, ) @@ -350,7 +350,7 @@ def test_path_flows_respect_port_constraints(): # This test sometimes fails due to the non-deterministic nature of optimizations. # We don't want this test to block a merging a pull request due to a failing run of the Github Action @pytest.mark.skipif(os.getenv("CI") == "true", reason="don't perform non-deterministic test in Github action") -def test_demand_tariff_reset_periods(): +def test_demand_tariff_reset_periods(engine_settings): expansion_periods = 1 expansion_periods = 1 day_periods = 48 @@ -387,7 +387,7 @@ def test_demand_tariff_reset_periods(): number_of_intervals=time_periods, number_of_expansion_intervals=expansion_periods, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, objective_set=objective_set, ) @@ -402,7 +402,7 @@ def test_demand_tariff_reset_periods(): np.testing.assert_almost_equal(max_opt, max_calc, 5) -def test_block_tariff(): +def test_block_tariff(engine_settings): time_periods = 24 interval_duration = 60 expansion_periods = 1 @@ -427,7 +427,7 @@ def test_block_tariff(): number_of_intervals=time_periods, number_of_expansion_intervals=expansion_periods, ), - engine_settings=engine_settings_from_environment(), + engine_settings=engine_settings, graph=system, objective_set=objective_set, ) diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..cdfc1a87 --- /dev/null +++ b/tox.ini @@ -0,0 +1,23 @@ +; Check https://devguide.python.org/versions/ to see which versions of python to support +; (recommended 'security' and 'bugfix' only) +[tox] +requires = + tox>=4 +env_list = python{3.10,3.11,3.12} + +[testenv] +description = run unit tests +basepython = + ; .package: python3.8 + python3.8: python3.8 + python3.9: python3.9 + python3.10: python3.10 + python3.11: python3.11 + python3.12: python3.12 +extras = + test +commands = + pytest {posargs} +setenv = + ; Skips the nondeterministic test in test_tariffs.py + CI = true