# -*- coding: utf-8 -*- """ A series of test based on an analytical solution to simple network problem. """ import pywr.core import datetime import numpy as np import pytest from helpers import assert_model from fixtures import simple_linear_model import pywr.parameters @pytest.mark.parametrize("in_flow, out_flow, benefit", [(10.0, 10.0, 10.0), (10.0, 0.0, 0.0)]) def test_linear_model(simple_linear_model, in_flow, out_flow, benefit): """ Test the simple_linear_model with different basic input and output values """ simple_linear_model.nodes["Input"].max_flow = in_flow simple_linear_model.nodes["Output"].min_flow = out_flow simple_linear_model.nodes["Output"].cost = -benefit expected_sent = in_flow if benefit > 1.0 else out_flow expected_node_results = { "Input": expected_sent, "Link": expected_sent, "Output": expected_sent, } assert_model(simple_linear_model, expected_node_results) @pytest.fixture(params=[ (10.0, 5.0, 5.0, 0.0, 0.0, 0.0), (10.0, 5.0, 5.0, 0.0, 10.0, 0.0), (10.0, 5.0, 5.0, 0.0, 10.0, 2.0), (10.0, 5.0, 0.0, 5.0, 10.0, 2.0), ]) def linear_model_with_storage(request): """ Make a simple model with a single Input and Output and an offline Storage Node Input -> Link -> Output | ^ v | Storage """ in_flow, out_flow, out_benefit, strg_benefit, current_volume, min_volume = request.param max_strg_out = 10.0 max_volume = 10.0 model = pywr.core.Model() inpt = pywr.core.Input(model, name="Input", min_flow=in_flow, max_flow=in_flow) lnk = pywr.core.Link(model, name="Link", cost=0.1) inpt.connect(lnk) otpt = pywr.core.Output(model, name="Output", min_flow=out_flow, cost=-out_benefit) lnk.connect(otpt) strg = pywr.core.Storage(model, name="Storage", max_volume=max_volume, min_volume=min_volume, initial_volume=current_volume, cost=-strg_benefit) strg.connect(otpt) lnk.connect(strg) avail_volume = max(current_volume - min_volume, 0.0) avail_refill = max_volume - current_volume expected_sent = in_flow+min(max_strg_out, avail_volume) if out_benefit > strg_benefit else max(out_flow, in_flow-avail_refill) expected_node_results = { "Input": in_flow, "Link": in_flow, "Output": expected_sent, "Storage Output": 0.0, "Storage Input": min(max_strg_out, avail_volume) if out_benefit > 1.0 else 0.0, "Storage": min_volume if out_benefit > strg_benefit else max_volume, } return model, expected_node_results def test_linear_model_with_storage(linear_model_with_storage): assert_model(*linear_model_with_storage) @pytest.fixture def two_domain_linear_model(request): """ Make a simple model with two domains, each with a single Input and Output Input -> Link -> Output : river | across the domain Output <- Link <- Input : grid """ river_flow = 864.0 # Ml/d power_plant_cap = 24 # GWh/d power_plant_flow_req = 18.0 # Ml/GWh power_demand = 12 # GWh/d power_benefit = 10.0 # £/GWh river_domain = pywr.core.Domain('river') grid_domain = pywr.core.Domain('grid') model = pywr.core.Model() # Create river network river_inpt = pywr.core.Input(model, name="Catchment", max_flow=river_flow, domain=river_domain) river_lnk = pywr.core.Link(model, name="Reach", domain=river_domain) river_inpt.connect(river_lnk) river_otpt = pywr.core.Output(model, name="Abstraction", domain=river_domain, cost=0.0) river_lnk.connect(river_otpt) # Create grid network grid_inpt = pywr.core.Input(model, name="Power Plant", max_flow=power_plant_cap, domain=grid_domain, conversion_factor=1/power_plant_flow_req) grid_lnk = pywr.core.Link(model, name="Transmission", cost=1.0, domain=grid_domain) grid_inpt.connect(grid_lnk) grid_otpt = pywr.core.Output(model, name="Substation", max_flow=power_demand, cost=-power_benefit, domain=grid_domain) grid_lnk.connect(grid_otpt) # Connect grid to river river_otpt.connect(grid_inpt) expected_requested = {'river': 0.0, 'grid': 0.0} expected_sent = {'river': power_demand*power_plant_flow_req, 'grid': power_demand} expected_node_results = { "Catchment": power_demand*power_plant_flow_req, "Reach": power_demand*power_plant_flow_req, "Abstraction": power_demand*power_plant_flow_req, "Power Plant": power_demand, "Transmission": power_demand, "Substation": power_demand, } return model, expected_node_results def test_two_domain_linear_model(two_domain_linear_model): assert_model(*two_domain_linear_model) @pytest.fixture def two_cross_domain_output_single_input(request): """ Make a simple model with two domains. Thre are two Output nodes both connect to an Input node in a different domain. In this example the rivers should be able to provide flow to the grid with a total flow equal to the sum of their respective parts. Input -> Link -> Output : river | across the domain Input -> Link -> Output : grid | across the domain Input -> Link -> Output : river """ river_flow = 10.0 expected_node_results = {} model = pywr.core.Model() # Create grid network grid_inpt = pywr.core.Input(model, name="Input", domain='grid',) grid_lnk = pywr.core.Link(model, name="Link", cost=1.0, domain='grid') grid_inpt.connect(grid_lnk) grid_otpt = pywr.core.Output(model, name="Output", max_flow=50.0, cost=-10.0, domain='grid') grid_lnk.connect(grid_otpt) # Create river network for i in range(2): river_inpt = pywr.core.Input(model, name="Catchment {}".format(i), max_flow=river_flow, domain='river') river_lnk = pywr.core.Link(model, name="Reach {}".format(i), domain='river') river_inpt.connect(river_lnk) river_otpt = pywr.core.Output(model, name="Abstraction {}".format(i), domain='river', cost=0.0) river_lnk.connect(river_otpt) # Connect grid to river river_otpt.connect(grid_inpt) expected_node_results.update({ "Catchment {}".format(i): river_flow, "Reach {}".format(i): river_flow, "Abstraction {}".format(i): river_flow }) expected_node_results.update({ "Input": river_flow*2, "Link": river_flow*2, "Output": river_flow*2, }) return model, expected_node_results @pytest.mark.xfail def test_two_cross_domain_output_single_input(two_cross_domain_output_single_input): # TODO This test currently fails because of the simple way in which the cross # domain paths work. It can not cope with two Outputs connected to one # input. assert_model(*two_cross_domain_output_single_input) @pytest.fixture() def simple_linear_inline_model(request): """ Make a simple model with a single Input and Output nodes inline of a route. Input 0 -> Input 1 -> Link -> Output 0 -> Output 1 """ model = pywr.core.Model() inpt0 = pywr.core.Input(model, name="Input 0") inpt1 = pywr.core.Input(model, name="Input 1") inpt0.connect(inpt1) lnk = pywr.core.Link(model, name="Link", cost=1.0) inpt1.connect(lnk) otpt0 = pywr.core.Output(model, name="Output 0") lnk.connect(otpt0) otpt1 = pywr.core.Output(model, name="Output 1") otpt0.connect(otpt1) return model @pytest.mark.skipif(pywr.core.Model().solver.name == "glpk-edge", reason="Not valid for GLPK Edge based solver.") @pytest.mark.parametrize("in_flow_1, out_flow_0, link_flow", [(10.0, 10.0, 15.0), (0.0, 0.0, 10.0)]) def test_simple_linear_inline_model(simple_linear_inline_model, in_flow_1, out_flow_0, link_flow): """ Test the test_simple_linear_inline_model with different flow constraints """ model = simple_linear_inline_model model.nodes["Input 0"].max_flow = 10.0 model.nodes["Input 1"].max_flow = in_flow_1 model.nodes["Link"].max_flow = link_flow model.nodes["Output 0"].max_flow = out_flow_0 model.nodes["Input 1"].cost = 1.0 model.nodes["Output 0"].cost = -10.0 model.nodes["Output 1"].cost = -5.0 expected_sent = min(link_flow, 10+in_flow_1) expected_node_results = { "Input 0": 10.0, "Input 1": max(expected_sent-10.0, 0.0), "Link": expected_sent, "Output 0": min(expected_sent, out_flow_0), "Output 1": max(expected_sent - out_flow_0, 0.0), } assert_model(model, expected_node_results) @pytest.fixture() def bidirectional_model(request): """ Make a simple model with a single Input and Output. Input 0 -> Link 0 -> Output 0 | ^ v | Input 1 -> Link 1 -> Output 1 """ model = pywr.core.Model() for i in range(2): inpt = pywr.core.Input(model, name="Input {}".format(i)) lnk = pywr.core.Link(model, name="Link {}".format(i)) inpt.connect(lnk) otpt = pywr.core.Output(model, name="Output {}".format(i)) lnk.connect(otpt) # Create bidirectional link (i.e. a cycle) model.nodes['Link 0'].connect(model.nodes['Link 1']) model.nodes['Link 1'].connect(model.nodes['Link 0']) return model def test_bidirectional_model(bidirectional_model): """ Test the simple_linear_model with different basic input and output values """ model = bidirectional_model model.nodes["Input 0"].max_flow = 10.0 model.nodes["Input 1"].max_flow = 10.0 model.nodes["Output 0"].max_flow = 10.0 model.nodes["Output 1"].max_flow = 15.0 model.nodes["Output 0"].cost = -5.0 model.nodes["Output 1"].cost = -10.0 model.nodes["Link 0"].cost = 1.0 model.nodes["Link 1"].cost = 1.0 expected_node_results = { "Input 0": 10.0, "Input 1": 10.0, "Link 0": 10.0, "Link 1": 15.0, "Output 0": 5.0, "Output 1": 15.0, } assert_model(model, expected_node_results) def make_simple_model(supply_amplitude, demand, frequency, initial_volume): """ Make a simple model, supply -> reservoir -> demand. supply is a annual cosine function with amplitude supply_amplitude and frequency """ model = pywr.core.Model() S = supply_amplitude w = frequency class SupplyFunc(pywr.parameters.Parameter): def value(self, ts, si): # Take the mean flow of the day (i.e. offset by half a day) t = ts.dayofyear - 0.5 v = S*np.cos(t*w)+S return v max_flow = SupplyFunc(model) supply = pywr.core.Input(model, name='supply', max_flow=max_flow, min_flow=max_flow) demand = pywr.core.Output(model, name='demand', max_flow=demand, cost=-10) res = pywr.core.Storage(model, name='reservoir', max_volume=1e6, initial_volume=initial_volume) supply_res_link = pywr.core.Link(model, name='link1') res_demand_link = pywr.core.Link(model, name='link2') supply.connect(supply_res_link) supply_res_link.connect(res) res.connect(res_demand_link) res_demand_link.connect(demand) return model def test_analytical(): """ Run the test model though a year with analytical solution values to ensure reservoir just contains sufficient volume. """ S = 100.0 # supply amplitude D = S # demand w = 2*np.pi/365 # frequency (annual) V0 = S/w # initial reservoir level model = make_simple_model(S, D, w, V0) T = np.arange(1, 365) V_anal = S*(np.sin(w*T)/w+T) - D*T + V0 V_model = np.empty(T.shape) for i, t in enumerate(T): model.step() V_model[i] = model.nodes['reservoir'].volume[0] # Relative error from initial volume error = np.abs(V_model - V_anal) / V0 assert np.all(error < 1e-4)