Repository: oogeso/oogeso

Issue:
Add unit tests to check that the creation of constraints associated with all devices works as intended

Discussion:

Patch:
diff --git a/.gitignore b/.gitignore
index 37ed47d..b4da171 100644
--- a/.gitignore
+++ b/.gitignore
@@ -129,4 +129,7 @@ venv.bak/
 examples/result/
 
 # Temporary data
-.tmp
\ No newline at end of file
+.tmp
+
+# PyCharm
+.DS_Store
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index 9c85f6e..2c1a039 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [tool.poetry]
 name = "oogeso"
-version = "0.3.0"
+version = "0.4.0"
 description = "Offshore Oil and Gas Field Energy System Operational Optimisation (OOGESO)"
 authors = ["Harald Svendsen <harald.svendsen@sintef.no>"]
 license = "MIT License (http://opensource.org/licenses/MIT)"
diff --git a/src/oogeso/core/devices/compressor.py b/src/oogeso/core/devices/compressor.py
index 3ab47f5..1f8e836 100644
--- a/src/oogeso/core/devices/compressor.py
+++ b/src/oogeso/core/devices/compressor.py
@@ -78,7 +78,6 @@ class CompressorGas(Device):
 
     def _rules(self, pyomo_model: pyo.Model, t: int) -> Union[pyo.Expression, pyo.Constraint.Skip]:
         dev = self.id
-        dev_data: DeviceCompressorGasData = self.dev_data  # noqa: Fixme: dev_data not in use.
         node_obj: NetworkNode = self.node
         gas_data = self.carrier_data["gas"]
         gas_energy_content = gas_data.energy_value  # MJ/Sm3
@@ -114,7 +113,7 @@ class CompressorGas(Device):
         )
 
     def get_flow_var(self, pyomo_model: pyo.Model, t: int) -> float:
-        raise NotImplementedError()
+        return pyomo_model.varDeviceFlow[self.id, "gas", "in", t]
 
 
 def compute_compressor_demand(
diff --git a/src/oogeso/core/devices/gasheater.py b/src/oogeso/core/devices/gasheater.py
index 019675f..0f5c64c 100644
--- a/src/oogeso/core/devices/gasheater.py
+++ b/src/oogeso/core/devices/gasheater.py
@@ -24,12 +24,11 @@ class GasHeater(Device):
 
     def _rules(self, pyomo_model: pyo.Model, t: int) -> Union[pyo.Expression, pyo.Constraint.Skip]:
         dev = self.id
-        param_dev = self.params  # Fixme: Attribute params is missing
-        param_gas = pyomo_model.all_carriers["gas"].params
+        gas_data = self.carrier_data["gas"]
         # heat out = gas input * energy content * efficiency
-        gas_energy_content = param_gas["energy_value"]  # MJ/Sm3
+        gas_energy_content = gas_data.energy_value  # MJ/Sm3
         lhs = pyomo_model.varDeviceFlow[dev, "heat", "out", t]
-        rhs = pyomo_model.varDeviceFlow[dev, "gas", "in", t] * gas_energy_content * param_dev["eta"]
+        rhs = pyomo_model.varDeviceFlow[dev, "gas", "in", t] * gas_energy_content * self.dev_data.eta
         return lhs == rhs
 
     def define_constraints(self, pyomo_model: pyo.Model):
@@ -51,7 +50,9 @@ class GasHeater(Device):
     def compute_CO2(self, pyomo_model: pyo.Model, timesteps: List[int]) -> float:
         """
         Fixme: The variable d and model_pyomo was not set. Changed to model and self.dev_data. Correct?
+
+        Todo: Need testing here.
         """
         param_gas = self.carrier_data["gas"]
         gas_flow_co2 = param_gas.co2_content  # kg/m3
-        return sum(pyomo_model.varDeviceFlow[self.dev_data, "gas", "in", t] for t in timesteps) * gas_flow_co2
+        return sum(pyomo_model.varDeviceFlow[self.id, "gas", "in", t] for t in timesteps) * gas_flow_co2
diff --git a/src/oogeso/core/devices/heatpump.py b/src/oogeso/core/devices/heatpump.py
index 87e3025..eb8c96b 100644
--- a/src/oogeso/core/devices/heatpump.py
+++ b/src/oogeso/core/devices/heatpump.py
@@ -17,7 +17,7 @@ class HeatPump(Device):
 
     def __init__(
         self,
-        dev_data: dto.DeviceHeatPumpData,  # Fixme: Correct?
+        dev_data: dto.DeviceHeatPumpData,
         carrier_data_dict: Dict[str, dto.CarrierElData],  # Fixme: Correct?
     ):
         super().__init__(dev_data=dev_data, carrier_data_dict=carrier_data_dict)
@@ -29,8 +29,8 @@ class HeatPump(Device):
         dev = self.id
         # heat out = el in * efficiency
         lhs = pyomo_model.varDeviceFlow[dev, "heat", "out", t]
-        rhs = pyomo_model.varDeviceFlow[dev, "el", "in", t] * pyomo_model.paramDevice[dev]["eta"]
-        return pyo.Expression(lhs == rhs)
+        rhs = pyomo_model.varDeviceFlow[dev, "el", "in", t] * self.dev_data.eta
+        return lhs == rhs
 
     def define_constraints(self, pyomo_model: pyo.Model):
         """Specifies the list of constraints for the device"""
diff --git a/src/oogeso/core/devices/separator.py b/src/oogeso/core/devices/separator.py
index 4c252e0..0ac1f27 100644
--- a/src/oogeso/core/devices/separator.py
+++ b/src/oogeso/core/devices/separator.py
@@ -28,8 +28,8 @@ class Separator(Device):
         dev = self.id
         dev_data = self.dev_data
         node = dev_data.node_id
-        node_obj: NetworkNode = self.optimiser.all_nodes[node]  # Fixme: Unresolved reference
-        wellstream_prop: dto.CarrierWellstreamData = self.all_carriers["wellstream"]  # Fixme: Unresolved reference
+        node_obj: NetworkNode = self.node
+        wellstream_prop: dto.CarrierWellstreamData = self.carrier_data["wellstream"]
         GOR = wellstream_prop.gas_oil_ratio
         WC = wellstream_prop.water_cut
         comp_oil = (1 - WC) / (1 + GOR - GOR * WC)
@@ -91,11 +91,15 @@ class Separator(Device):
         return list_to_reconstruct
 
     def get_flow_var(self, pyomo_model: pyo.Model, t: int) -> float:
-        raise NotImplementedError("No get_flow_var defined for Separator")
+        return pyomo_model.varDeviceFlow[self.id, "wellstream", "in", t]
 
 
 class Separator2(Device):
-    "Wellstream separation into oil/gas/water"
+    """
+    Wellstream separation into oil/gas/water
+
+    Alternative separator model - using oil/gas/water input instead of
+    """
 
     carrier_in = ["oil", "gas", "water", "heat", "el"]
     carrier_out = ["oil", "gas", "water"]
@@ -114,8 +118,6 @@ class Separator2(Device):
         self.id = dev_data.id
         self.carrier_data = carrier_data_dict
 
-    # Alternative separator model - using oil/gas/water input instead of
-    # wellstream
     def _rule_separator2_flow(
         self, pyomo_model: pyo.Model, fc, t: int, i: int
     ) -> Union[pyo.Expression, pyo.Constraint.Skip]:
@@ -190,4 +192,8 @@ class Separator2(Device):
         return list_to_reconstruct
 
     def get_flow_var(self, pyomo_model: pyo.Model, t: int) -> float:
-        raise NotImplementedError("No get_flow_var defined for Separator")
+        return (
+            pyomo_model.varDeviceFlow[self.id, "gas", "in", t]
+            + pyomo_model.varDeviceFlow[self.id, "oil", "in", t]
+            + pyomo_model.varDeviceFlow[self.id, "water", "in", t]
+        )
diff --git a/src/oogeso/core/devices/storage.py b/src/oogeso/core/devices/storage.py
index 6720980..aa95c25 100644
--- a/src/oogeso/core/devices/storage.py
+++ b/src/oogeso/core/devices/storage.py
@@ -245,9 +245,9 @@ class StorageHydrogen(StorageDevice):
         # Cost associated with deviation from target value
         # below target = cost, above target = benefit   => gives signal to fill storage
         dev_data = self.dev_data
-        E_target = dev_data.E_max
+        # E_target = dev_data.E_max
         E_target = pyomo_model.paramDeviceEnergyTarget[self.id]
-        t_end = timesteps.last()
+        t_end = timesteps[-1]
         varE = pyomo_model.varDeviceStorageEnergy[self.id, t_end]
         stor_cost = dev_data.E_cost * (E_target - varE)
         # from cost (kr) to cost rate (kr/s):
diff --git a/src/oogeso/core/networks/pipelines.py b/src/oogeso/core/networks/pipelines.py
index 70f84dd..25d0f51 100644
--- a/src/oogeso/core/networks/pipelines.py
+++ b/src/oogeso/core/networks/pipelines.py
@@ -182,7 +182,6 @@ class Fluid(Network):
                 f = f.real
             elif carrier_data.darcy_friction is not None:
                 f = carrier_data.darcy_friction
-                Re = None
             else:
                 raise Exception("Must provide viscosity or darcy_friction for {}".format(carrier))
             (p2, Q0) = darcy_weissbach_p2(
diff --git a/src/oogeso/core/optimiser.py b/src/oogeso/core/optimiser.py
index c80b2a8..4eb4118 100644
--- a/src/oogeso/core/optimiser.py
+++ b/src/oogeso/core/optimiser.py
@@ -74,7 +74,6 @@ class OptimisationModel(pyo.ConcreteModel):
             sol.solver.termination_condition == pyopt.TerminationCondition.optimal
         ):
             logger.debug("Solved OK")
-            pass
         elif sol.solver.termination_condition == pyopt.TerminationCondition.infeasible:
             raise Exception("Infeasible solution")
         else:
@@ -602,7 +601,6 @@ class OptimisationModel(pyo.ConcreteModel):
         start_stop_costs = start_stop_costs / sumTime
         return start_stop_costs
 
-    logger.info("TODO: operating cost for el storage - needs improvement")
 
     def compute_operatingCosts(self, model: pyo.Model):
         """term in objective function to represent fuel costs or similar
diff --git a/src/oogeso/dto/__init__.py b/src/oogeso/dto/__init__.py
index 4361cc9..d889388 100644
--- a/src/oogeso/dto/__init__.py
+++ b/src/oogeso/dto/__init__.py
@@ -38,6 +38,7 @@ from .oogeso_input_data_objects_multienergy import (
     DeviceSinkWaterData,
     DeviceSourceElData,
     DeviceSourceGasData,
+    DeviceSourceOilData,
     DeviceSourceWaterData,
     DeviceStorageHydrogenData,
     DeviceWellGasLiftData,
diff --git a/src/oogeso/dto/oogeso_input_data_objects_multienergy.py b/src/oogeso/dto/oogeso_input_data_objects_multienergy.py
index c3cb13d..2b73c38 100644
--- a/src/oogeso/dto/oogeso_input_data_objects_multienergy.py
+++ b/src/oogeso/dto/oogeso_input_data_objects_multienergy.py
@@ -21,6 +21,11 @@ class DeviceSourceGasData(dto.DeviceData):
     naturalpressure: float = None
 
 
+@dataclass
+class DeviceSourceOilData(dto.DeviceData):
+    naturalpressure: float = None
+
+
 @dataclass
 class DeviceSourceWaterData(dto.DeviceData):
     naturalpressure: float = None
@@ -81,7 +86,7 @@ class DeviceFuelCellData(dto.DeviceData):
 
 @dataclass
 class DeviceGasHeaterData(dto.DeviceData):
-    pass
+    eta: float = None  # efficiency
 
 
 @dataclass
diff --git a/src/oogeso/dto/serialisation.py b/src/oogeso/dto/serialisation.py
index 65567c2..b9974f9 100644
--- a/src/oogeso/dto/serialisation.py
+++ b/src/oogeso/dto/serialisation.py
@@ -68,7 +68,7 @@ class DataclassJSONDecoder(json.JSONDecoder):
             data_nowcast = dct["data_nowcast"]
         return dto.TimeSeriesData(name, data, data_nowcast)
 
-    def object_hook(self, dct):  # pylint: disable=E0202
+    def object_hook(self, dct):  # noqa
         carriers = []
         nodes = []
         edges = []
@@ -109,8 +109,8 @@ class DataclassJSONDecoder(json.JSONDecoder):
 
     def default(self, obj: Any):
         if isinstance(obj, dto.EnergySystemData):
-            return dto.EnergySystemData(obj=obj)
-        return json.JSONEncoder.default(self, obj)
+            return dto.EnergySystemData(obj=obj)  # noqa
+        return json.JSONEncoder.default(self, obj)  # noqa
 
 
 def serialize_oogeso_data(energy_system_data: dto.EnergySystemData):
@@ -128,7 +128,7 @@ class OogesoResultJSONDecoder(json.JSONDecoder):
     def __init__(self, *args, **kwargs):
         super().__init__(object_hook=self.object_hook, *args, **kwargs)
 
-    def object_hook(self, dct):  # pylint: disable=E0202
+    def object_hook(self, dct):  # noqa
         res_dfs = {}
         # profiles_nowcast = {}
         if "device_flow" in dct:
diff --git a/src/oogeso/plots/__init__.py b/src/oogeso/plots/__init__.py
index 1350222..8424ec8 100644
--- a/src/oogeso/plots/__init__.py
+++ b/src/oogeso/plots/__init__.py
@@ -7,7 +7,6 @@ from .plots import (
     plot_device_profile,
     plot_df,
     plot_el_backup,
-    plot_el_backup2,
     plot_export_revenue,
     plot_gas_turbine_efficiency,
     plot_network,
diff --git a/src/oogeso/plots/plots.py b/src/oogeso/plots/plots.py
index 04dd761..4ec2760 100644
--- a/src/oogeso/plots/plots.py
+++ b/src/oogeso/plots/plots.py
@@ -536,7 +536,7 @@ def plot_CO2_intensity(sim_result, filename=None):
     ylabel = "CO2 intensity (kgCO2/Sm3oe)"
     dfplot = sim_result.co2_intensity
     if plotter == "plotly":
-        fig = plotly.subplots.make_subplots(rows=1, cols=1)
+        # fig = plotly.subplots.make_subplots(rows=1, cols=1)
         # ,shared_xaxes=True,vertical_spacing=0.05)
         fig = px.line(dfplot, x=dfplot.index, y=dfplot.values)  # ,title=title)
         fig.update_xaxes(title_text=xlabel)
@@ -739,7 +739,6 @@ def plot_network(
                 num_out = 0
                 for d, dev_obj in devs.items():
                     dev_model = dev_obj.dev_data.model
-                    devlabel = d  # use index as label
                     devlabel = "{}\n{}".format(d, dev_model)
                     if plotDevName:
                         dev_name = dev_obj.dev_data.name
@@ -869,7 +868,7 @@ def plot_network(
                         )
                         # edgelabel = "{}-{}".format(edgelabel, edge_data.pressure["to"])
                 else:
-                    edgelabel = numberformat.format(res.dfEdgeFlow[(i, timestep)])
+                    edgelabel = numberformat.format(res.edge_flow[(i, timestep)])
                     # Add loss
                     if (not hide_losses) and (res.edge_loss is not None) and ((i, timestep) in res.edge_loss):
                         # taillabel = " " + edgelabel
@@ -1053,7 +1052,7 @@ def plot_reserve(
 def plot_el_backup(sim_result, filename=None, showMargin=False, returnMargin=False):
     """plot reserve capacity vs device power output"""
     res = sim_result
-    res_dev = res.el_backup
+    res_dev = res.el_backup.unstack("device")
     dfP = res.device_flow.copy()
     carrier = "el"
     mask_carrier = dfP.index.get_level_values("carrier") == carrier
@@ -1112,88 +1111,3 @@ def plot_el_backup(sim_result, filename=None, showMargin=False, returnMargin=Fal
     if returnMargin:
         return dfMargin
     return fig
-
-
-def recompute_elBackup(res, optimisation_model: pyo.Model):
-    """Compute reserve
-    should give the same as mc.compute_el_reserve"""
-    optimiser = optimisation_model
-    model = optimiser  # noqa
-    all_devices = optimiser.all_devices
-
-    # used capacity for all (relevant) devices:
-    carrier = "el"
-    devices_elin = optimiser.getDevicesInout(carrier_in=carrier)
-    devices_elout = optimiser.getDevicesInout(carrier_out=carrier)
-    dfP = res.device_flow
-    mask_carrier = dfP.index.get_level_values("carrier") == carrier
-    mask_out = dfP.index.get_level_values("terminal") == "out"
-    mask_in = dfP.index.get_level_values("terminal") == "out"
-    dfPin = dfP[mask_carrier & mask_in]
-    dfPin.index = dfPin.index.droplevel(level=("carrier", "terminal"))
-    dfPin = dfPin.unstack(0)
-    dfPin = dfPin[devices_elin]
-    dfPout = dfP[mask_carrier & mask_out]
-    dfPout.index = dfPout.index.droplevel(level=("carrier", "terminal"))
-    dfPout = dfPout.unstack(0)
-    dfPout = dfPout[devices_elout]
-
-    # reserve (unused) capacity by other devices:
-    res_dev = pd.DataFrame(columns=dfPout.columns, index=dfPout.index)
-    df_ison = res.device_is_on.unstack(0)
-    for dev in devices_elout:
-        otherdevs = [d for d in devices_elout if d != dev]
-        cap_avail = 0
-        for d in otherdevs:
-            dev2 = all_devices[d].dev_data
-            devmodel = dev2.model  # noqa
-            maxValue = dev2.flow_max
-            if dev2.profile is not None:
-                extprofile = dev2.profile
-                maxValue = maxValue * res.profiles_nowcast[extprofile]
-            ison = 1
-            if dev2.start_stop is not None:
-                ison = df_ison[d]
-            cap_avail += ison * maxValue
-        otherdevs_in = [d for d in devices_elin if d != dev]  # noqa
-        # TODO: include sheddable load
-        res_dev[dev] = cap_avail - dfPout[otherdevs].sum(axis=1)
-    return res_dev, dfPout
-
-
-def plot_el_backup2(mc, filename=None):
-    """plot reserve capacity vs device power output"""
-    res_dev, dfP = recompute_elBackup(mc)
-    plt.figure(figsize=(12, 4))
-    ax = plt.gca()
-    # default line zorder=2
-    # res_dev.plot(ax=ax, linestyle="-", legend=True, alpha=0.5)
-    res_dev.plot(ax=ax, drawstyle="steps-post", linestyle="-", legend=True, alpha=0.5)
-    #    # critical is the largest load:
-    #    c_critical = dfP.idxmax(axis=1)
-    # critical is the smallest reserve margin:
-    c_critical = (res_dev - dfP).idxmin(axis=1)  # noqa
-    #    pd.Series(res_dev.lookup(res_dev.index,c_critical)).plot(ax=ax,
-    #             linestyle='-',linewidth=2,label="CRITICAL",
-    #             zorder=2.1,legend=True,color="black")
-    (res_dev - dfP).min(axis=1).plot(ax=ax, linestyle="--", color="black", linewidth=2)
-
-    # use the same colors
-    plt.gca().set_prop_cycle(None)
-    # dfP.plot(ax=ax, linestyle=":", legend=False, alpha=0.5)
-    dfP.plot(ax=ax, drawstyle="steps-post", linestyle=":", legend=False, alpha=0.5)
-
-    #    pd.Series(dfP.lookup(dfP.index,c_critical)).plot(ax=ax,
-    #             linestyle=':',legend=False,linewidth=2,
-    #             zorder=2.1,color="black")
-    plt.title("Reserve capacity (solid line) vs device output (dotted line)")
-    #    leg = ax.get_legend()
-    #    leg.set_bbox_to_anchor((1.01,0))
-    #    leg.set_frame_on(False)
-    labels = list(dfP.columns) + ["MARGIN"]
-    ax.legend(labels, loc="lower left", bbox_to_anchor=(1.01, 0), frameon=False)
-    #    plt.legend(dfP.columns,loc='lower left', bbox_to_anchor =(1.01,0),
-    #               frameon=False)
-    if filename is not None:
-        plt.savefig(filename, bbox_inches="tight")
-    return ax
diff --git a/src/oogeso/utils/util.py b/src/oogeso/utils/util.py
index 4bfdbe5..386688f 100644
--- a/src/oogeso/utils/util.py
+++ b/src/oogeso/utils/util.py
@@ -23,7 +23,7 @@ def get_device_from_model_name(model_name: str) -> Callable:
         "heatpump": devices.HeatPump,
         "pumpoil": devices.PumpOil,
         "pumpwater": devices.PumpWater,
-        "separastor": devices.Separator,
+        "separator": devices.Separator,
         "separator2": devices.Separator2,
         "sinkel": devices.SinkEl,
         "sinkheat": devices.SinkHeat,


Test patch:
diff --git a/tests/test_devices_constraints.py b/tests/test_devices_constraints.py
new file mode 100644
index 0000000..b7d4638
--- /dev/null
+++ b/tests/test_devices_constraints.py
@@ -0,0 +1,267 @@
+from oogeso import dto
+from oogeso.core import OptimisationModel
+
+dev_data_generic = {
+    "id": "the_id",
+    "node_id": "the_node",
+    "name": "the_name",
+    "include": True,
+    "profile": "the_profile",
+    "flow_min": 10,
+    "flow_max": 20,
+    "max_ramp_up": None,
+    "max_ramp_down": None,
+    "op_cost": 0,
+}
+startstop_data = dto.StartStopData(
+    is_on_init=False,
+    penalty_start=1,
+    penalty_stop=0,
+    delay_start_minutes=30,
+    minimum_time_on_minutes=0,
+    minimum_time_off_minutes=0,
+)
+data_profile = dto.TimeSeriesData(
+    id="the_profile", data=[1.1, 1.2, 1.3, 1.4, 1.5, 1.6], data_nowcast=[1.3, 1.4, 1.5, 1.6, 1.7, 1.8]
+)
+
+# These tests, one for each device type, checks that the construction of Pyomo model
+# constraints associated with the deviecs goes through without error.
+# To test this, it is necessary to create a complete optimisation model with some
+# necessary node, edge, carrier data as well.
+
+
+def _build_lp_problem_with_single_dev(dev_data: dto.DeviceData):
+    """Builds a minimal Pyomo model with a single device"""
+    parameters = dto.OptimisationParametersData(
+        objective="penalty",
+        time_delta_minutes=15,
+        planning_horizon=4,
+        optimisation_timesteps=2,
+        forecast_timesteps=2,
+        time_reserve_minutes=30,
+    )
+    carriers = [
+        dto.CarrierElData(id="el"),
+        dto.CarrierGasData(
+            id="gas",
+            co2_content=2.34,
+            G_gravity=0.6,
+            Pb_basepressure_MPa=0.1,
+            R_individual_gas_constant=500,
+            Tb_basetemp_K=288,
+            Z_compressibility=0.9,
+            energy_value=40,
+            k_heat_capacity_ratio=1.27,
+            rho_density=0.8,
+        ),
+        dto.CarrierHeatData(id="heat"),
+        dto.CarrierOilData(id="oil", darcy_friction=0.02, rho_density=900, viscosity=0.002),
+        dto.CarrierWaterData(id="water", darcy_friction=0.01, rho_density=900, viscosity=0.01),
+        dto.CarrierHydrogenData(id="hydrogen"),
+        dto.CarrierWellstreamData(
+            id="wellstream", darcy_friction=0.01, rho_density=900, viscosity=0.01, water_cut=0.6, gas_oil_ratio=500
+        ),
+    ]
+
+    nodes = [dto.NodeData(id="the_node"), dto.NodeData(id="another_node1"), dto.NodeData(id="another_node2")]
+    # edges are needed to set nominal pressure, needed for compressor devices
+    edges = [
+        dto.EdgeGasData(id="edge1g", node_from="the_node", node_to="another_node1", pressure_from=1, pressure_to=1),
+        dto.EdgeGasData(id="edge2g", node_from="another_node2", node_to="the_node", pressure_from=1, pressure_to=1),
+        dto.EdgeWaterData(id="edge1w", node_from="the_node", node_to="another_node1", pressure_from=1, pressure_to=1),
+        dto.EdgeWaterData(id="edge2w", node_from="another_node2", node_to="the_node", pressure_from=1, pressure_to=1),
+        dto.EdgeOilData(id="edge1o", node_from="the_node", node_to="another_node1", pressure_from=1, pressure_to=1),
+        dto.EdgeOilData(id="edge2o", node_from="another_node2", node_to="the_node", pressure_from=1, pressure_to=1),
+    ]
+    devs = [dev_data]
+    profiles = [data_profile]
+    energy_system_data = dto.EnergySystemData(
+        parameters=parameters, carriers=carriers, nodes=nodes, edges=edges, devices=devs, profiles=profiles
+    )
+
+    # Create and initialize optimisation model. This will create the constraints
+    optimisation_model = OptimisationModel(energy_system_data)
+
+    # Chack that all method calls works without error (not caring about return value)
+    _device_method_calls(optimisation_model)
+
+    return optimisation_model
+
+
+def _device_method_calls(optimistion_model: OptimisationModel):
+    """Calls all common device methods"""
+    dev_obj = optimistion_model.all_devices["the_id"]
+    timesteps = [0]
+    # Check that these calls don't give errors
+    dev_obj.compute_CO2(optimistion_model, timesteps)
+    dev_obj.compute_cost_for_depleted_storage(optimistion_model, timesteps)
+    dev_obj.compute_el_reserve(optimistion_model, t=0)
+    dev_obj.compute_export(optimistion_model, value="volume", carriers=["oil", "gas", "el"], timesteps=timesteps)
+    dev_obj.compute_operating_costs(optimistion_model, timesteps)
+    dev_obj.compute_penalty(optimistion_model, timesteps)
+    dev_obj.compute_startup_penalty(optimistion_model, timesteps)
+    dev_obj.get_flow_var(optimistion_model, t=0)
+    dev_obj.get_max_flow(optimistion_model, t=0)
+    dev_obj.get_flow_upper_bound()
+    dev_obj.set_flow_upper_bound([data_profile])
+
+
+def test_powersource_constraints():
+    dev_data = dto.DevicePowerSourceData(
+        **dev_data_generic, start_stop=startstop_data, penalty_function=([0, 50], [1, 20])
+    )
+    optimisation_model = _build_lp_problem_with_single_dev(dev_data)
+    assert isinstance(optimisation_model, OptimisationModel)
+
+
+def test_powersink_constraints():
+    dev_data = dto.DevicePowerSinkData(**dev_data_generic)
+    optimisation_model = _build_lp_problem_with_single_dev(dev_data)
+    assert isinstance(optimisation_model, OptimisationModel)
+
+
+def test_compressor_el_constraints():
+    dev_data = dto.DeviceCompressorElData(**dev_data_generic, eta=0.6, Q0=5, temp_in=300)
+    optimisation_model = _build_lp_problem_with_single_dev(dev_data)
+    assert isinstance(optimisation_model, OptimisationModel)
+
+
+def test_compressor_gas_constraints():
+    dev_data = dto.DeviceCompressorGasData(**dev_data_generic, eta=0.6, Q0=5, temp_in=300)
+    optimisation_model = _build_lp_problem_with_single_dev(dev_data)
+    assert isinstance(optimisation_model, OptimisationModel)
+
+
+def test_electrolyser_constraints():
+    dev_data = dto.DeviceElectrolyserData(**dev_data_generic, eta=0.6, eta_heat=0.5)
+    optimisation_model = _build_lp_problem_with_single_dev(dev_data)
+    assert isinstance(optimisation_model, OptimisationModel)
+
+
+def test_fuelcell_constraints():
+    dev_data = dto.DeviceFuelCellData(**dev_data_generic, eta=0.6, eta_heat=0.5)
+    optimisation_model = _build_lp_problem_with_single_dev(dev_data)
+    assert isinstance(optimisation_model, OptimisationModel)
+
+
+def test_gasheater_constraints():
+    dev_data = dto.DeviceGasHeaterData(**dev_data_generic, eta=0.8)
+    optimisation_model = _build_lp_problem_with_single_dev(dev_data)
+    assert isinstance(optimisation_model, OptimisationModel)
+
+
+def test_gasturbine_constraints():
+    dev_data = dto.DeviceGasTurbineData(**dev_data_generic, start_stop=startstop_data, fuel_A=1, fuel_B=2, eta_heat=0.5)
+    optimisation_model = _build_lp_problem_with_single_dev(dev_data)
+    assert isinstance(optimisation_model, OptimisationModel)
+
+
+def test_heatpump_constraints():
+    dev_data = dto.DeviceHeatPumpData(**dev_data_generic, eta=3.0)
+    optimisation_model = _build_lp_problem_with_single_dev(dev_data)
+    assert isinstance(optimisation_model, OptimisationModel)
+
+
+def test_pump_oil_constraints():
+    dev_data = dto.DevicePumpOilData(**dev_data_generic, eta=0.6)
+    optimisation_model = _build_lp_problem_with_single_dev(dev_data)
+    assert isinstance(optimisation_model, OptimisationModel)
+
+
+def test_pump_water_constraints():
+    dev_data = dto.DevicePumpWaterData(**dev_data_generic, eta=0.6)
+    optimisation_model = _build_lp_problem_with_single_dev(dev_data)
+    assert isinstance(optimisation_model, OptimisationModel)
+
+
+def test_separator_constraints():
+    dev_data = dto.DeviceSeparatorData(**dev_data_generic, el_demand_factor=0.1, heat_demand_factor=0.5)
+    optimisation_model = _build_lp_problem_with_single_dev(dev_data)
+    assert isinstance(optimisation_model, OptimisationModel)
+
+
+def test_separator2_constraints():
+    dev_data = dto.DeviceSeparator2Data(**dev_data_generic, el_demand_factor=0.1, heat_demand_factor=0.5)
+    optimisation_model = _build_lp_problem_with_single_dev(dev_data)
+    assert isinstance(optimisation_model, OptimisationModel)
+
+
+def test_sink_el_constraints():
+    dev_data = dto.DeviceSinkElData(**dev_data_generic)
+    optimisation_model = _build_lp_problem_with_single_dev(dev_data)
+    assert isinstance(optimisation_model, OptimisationModel)
+
+
+def test_sink_gas_constraints():
+    dev_data = dto.DeviceSinkGasData(**dev_data_generic)
+    optimisation_model = _build_lp_problem_with_single_dev(dev_data)
+    assert isinstance(optimisation_model, OptimisationModel)
+
+
+def test_sink_heat_constraints():
+    dev_data = dto.DeviceSinkHeatData(**dev_data_generic)
+    optimisation_model = _build_lp_problem_with_single_dev(dev_data)
+    assert isinstance(optimisation_model, OptimisationModel)
+
+
+def test_sink_oil_constraints():
+    dev_data = dto.DeviceSinkOilData(**dev_data_generic)
+    optimisation_model = _build_lp_problem_with_single_dev(dev_data)
+    assert isinstance(optimisation_model, OptimisationModel)
+
+
+def test_sink_water_constraints():
+    dev_data = dto.DeviceSinkWaterData(**dev_data_generic)
+    optimisation_model = _build_lp_problem_with_single_dev(dev_data)
+    assert isinstance(optimisation_model, OptimisationModel)
+
+
+def test_source_el_constraints():
+    dev_data = dto.DeviceSourceElData(**dev_data_generic)
+    optimisation_model = _build_lp_problem_with_single_dev(dev_data)
+    assert isinstance(optimisation_model, OptimisationModel)
+
+
+def test_source_gas_constraints():
+    dev_data = dto.DeviceSourceGasData(**dev_data_generic, naturalpressure=10)
+    optimisation_model = _build_lp_problem_with_single_dev(dev_data)
+    assert isinstance(optimisation_model, OptimisationModel)
+
+
+def test_source_oil_constraints():
+    dev_data = dto.DeviceSourceOilData(**dev_data_generic, naturalpressure=10)
+    optimisation_model = _build_lp_problem_with_single_dev(dev_data)
+    assert isinstance(optimisation_model, OptimisationModel)
+
+
+def test_source_water_constraints():
+    dev_data = dto.DeviceSourceWaterData(**dev_data_generic, naturalpressure=1)
+    optimisation_model = _build_lp_problem_with_single_dev(dev_data)
+    assert isinstance(optimisation_model, OptimisationModel)
+
+
+def test_storage_el_constraints():
+    dev_data = dto.DeviceStorageElData(**dev_data_generic)
+    optimisation_model = _build_lp_problem_with_single_dev(dev_data)
+    assert isinstance(optimisation_model, OptimisationModel)
+
+
+def test_storage_hydrogen_constraints():
+    dev_data = dto.DeviceStorageHydrogenData(**dev_data_generic)
+    optimisation_model = _build_lp_problem_with_single_dev(dev_data)
+    assert isinstance(optimisation_model, OptimisationModel)
+
+
+def test_well_gas_lift_constraints():
+    dev_data = dto.DeviceWellGasLiftData(
+        **dev_data_generic, separator_pressure=10, gas_oil_ratio=500, water_cut=0.6, f_inj=2, injection_pressure=20
+    )
+    optimisation_model = _build_lp_problem_with_single_dev(dev_data)
+    assert isinstance(optimisation_model, OptimisationModel)
+
+
+def test_well_production_constraints():
+    dev_data = dto.DeviceWellProductionData(**dev_data_generic, wellhead_pressure=10)
+    optimisation_model = _build_lp_problem_with_single_dev(dev_data)
+    assert isinstance(optimisation_model, OptimisationModel)
diff --git a/tests/test_devices_create.py b/tests/test_devices_create.py
index 7bcd941..bc5f7d1 100644
--- a/tests/test_devices_create.py
+++ b/tests/test_devices_create.py
@@ -188,6 +188,15 @@ def test_source_gas():
     assert obj.dev_data.naturalpressure == 15
 
 
+def test_source_oil():
+    dev_data = dto.DeviceSourceOilData(**dev_data_generic, naturalpressure=15)
+    carrier_data_dict = {}
+    obj = devices.SourceOil(dev_data, carrier_data_dict)
+    assert isinstance(obj, devices.SourceOil)
+    assert obj.dev_data.flow_max == 20
+    assert obj.dev_data.naturalpressure == 15
+
+
 def test_source_water():
     dev_data = dto.DeviceSourceWaterData(**dev_data_generic, naturalpressure=15)
     carrier_data_dict = {}
diff --git a/tests/test_electricalsystem.py b/tests/test_electricalsystem.py
index 00cdd0b..175abb4 100644
--- a/tests/test_electricalsystem.py
+++ b/tests/test_electricalsystem.py
@@ -1,5 +1,5 @@
-import pandas as pd
 import numpy as np
+
 from oogeso.core.networks import electricalsystem as el
 
 
diff --git a/tests/test_plotting.py b/tests/test_plotting.py
new file mode 100644
index 0000000..ac6a61d
--- /dev/null
+++ b/tests/test_plotting.py
@@ -0,0 +1,154 @@
+import sys
+
+import pandas as pd
+import pytest
+
+import oogeso
+from oogeso import dto
+
+try:
+    from oogeso import plots as op
+except ImportError:
+    pass
+
+
+@pytest.mark.skipif(
+    not all([module in sys.modules for module in ["matplotlib", "plotly", "seaborn"]]),
+    reason="Plotting modules has not been installed",
+)
+def test_plot_co2_intensity(leogo_expected_result: dto.SimulationResult):
+    sim_result = leogo_expected_result
+    op.plot_CO2_intensity(sim_result)
+
+
+@pytest.mark.skipif(
+    not all([module in sys.modules for module in ["matplotlib", "plotly", "seaborn"]]),
+    reason="Plotting modules has not been installed",
+)
+def test_plot_co2_rate(leogo_expected_result: dto.SimulationResult):
+    sim_result = leogo_expected_result
+    op.plot_CO2_rate(sim_result)
+
+
+@pytest.mark.skipif(
+    not all([module in sys.modules for module in ["matplotlib", "plotly", "seaborn"]]),
+    reason="Plotting modules has not been installed",
+)
+def test_plot_co2_rate_per_dev(leogo_test_data: dto.EnergySystemData, leogo_expected_result: dto.SimulationResult):
+    sim_result = leogo_expected_result
+    optimisation_model = oogeso.OptimisationModel(leogo_test_data)
+    op.plot_CO2_rate_per_dev(sim_result, optimisation_model)
+
+
+@pytest.mark.skipif(
+    not all([module in sys.modules for module in ["matplotlib", "plotly", "seaborn"]]),
+    reason="Plotting modules has not been installed",
+)
+def test_plot_device_power_energy(
+    testcase2_data: dto.EnergySystemData, testcase2_expected_result: dto.SimulationResult
+):
+    sim_result = testcase2_expected_result
+    optimisation_model = oogeso.OptimisationModel(testcase2_data)
+    op.plot_device_power_energy(sim_result, optimisation_model, dev="battery")
+
+
+@pytest.mark.skipif(
+    not all([module in sys.modules for module in ["matplotlib", "plotly", "seaborn"]]),
+    reason="Plotting modules has not been installed",
+)
+def test_plot_device_power_flow_pressure(
+    leogo_test_data: dto.EnergySystemData, leogo_expected_result: dto.SimulationResult
+):
+    sim_result = leogo_expected_result
+    optimisation_model = oogeso.OptimisationModel(leogo_test_data)
+    op.plot_device_power_flow_pressure(sim_result, optimisation_model, dev="REC")
+
+
+@pytest.mark.skipif(
+    not all([module in sys.modules for module in ["matplotlib", "plotly", "seaborn"]]),
+    reason="Plotting modules has not been installed",
+)
+def test_plot_device_profile(leogo_test_data: dto.EnergySystemData, leogo_expected_result: dto.SimulationResult):
+    sim_result = leogo_expected_result
+    optimisation_model = oogeso.OptimisationModel(leogo_test_data)
+    op.plot_device_profile(sim_result, optimisation_model, devs=["wind"], include_forecasts=True)
+    op.plot_device_profile(
+        sim_result, optimisation_model, devs=["Gen1", "Gen2", "Gen3"], include_on_off=True, include_prep=True
+    )
+
+
+@pytest.mark.skipif(
+    not all([module in sys.modules for module in ["matplotlib", "plotly", "seaborn"]]),
+    reason="Plotting modules has not been installed",
+)
+def test_plot_df():
+    df = pd.DataFrame()
+    df["time"] = [0, 1, 2, 3, 4, 0, 1, 2, 3, 4]
+    df["value"] = [1, 2, 3, 4, 5, 3, 2, 1, 1, 0]
+    df["class"] = [1, 1, 1, 1, 1, 2, 2, 2, 2, 2]
+    op.plot_df(df, id_var="class")
+
+
+@pytest.mark.skipif(
+    not all([module in sys.modules for module in ["matplotlib", "plotly", "seaborn"]]),
+    reason="Plotting modules has not been installed",
+)
+def test_plot_el_backup(leogo_expected_result: dto.SimulationResult):
+    sim_result = leogo_expected_result
+    op.plot_el_backup(sim_result, showMargin=True)
+
+
+@pytest.mark.skipif(
+    not all([module in sys.modules for module in ["matplotlib", "plotly", "seaborn"]]),
+    reason="Plotting modules has not been installed",
+)
+def test_plot_export_revenue(leogo_expected_result: dto.SimulationResult):
+    sim_result = leogo_expected_result
+    op.plot_export_revenue(sim_result)
+
+
+@pytest.mark.skipif(
+    not all([module in sys.modules for module in ["matplotlib", "plotly", "seaborn"]]),
+    reason="Plotting modules has not been installed",
+)
+def test_plot_gas_turbine_efficiency():
+    op.plot_gas_turbine_efficiency()
+
+
+@pytest.mark.skipif(
+    not all([module in sys.modules for module in ["matplotlib", "plotly", "seaborn"]]),
+    reason="Plotting modules has not been installed",
+)
+def test_plot_network(leogo_test_data: dto.EnergySystemData, leogo_expected_result: dto.SimulationResult):
+    simulator = oogeso.Simulator(data=leogo_test_data)
+    simulator.result_object = leogo_expected_result
+    op.plot_network(simulator, timestep=1)
+    # If no errors, it's OK
+
+
+@pytest.mark.skipif(
+    not all([module in sys.modules for module in ["matplotlib", "plotly", "seaborn"]]),
+    reason="Plotting modules has not been installed",
+)
+def test_plot_profiles(leogo_test_data: dto.EnergySystemData):
+    op.plot_profiles(leogo_test_data.profiles)
+
+
+@pytest.mark.skipif(
+    not all([module in sys.modules for module in ["matplotlib", "plotly", "seaborn"]]),
+    reason="Plotting modules has not been installed",
+)
+def test_plot_reservek(leogo_test_data: dto.EnergySystemData, leogo_expected_result: dto.SimulationResult):
+    sim_result = leogo_expected_result
+    optimisation_model = oogeso.OptimisationModel(leogo_test_data)
+    op.plot_reserve(sim_result, optimisation_model)
+
+
+@pytest.mark.skipif(
+    not all([module in sys.modules for module in ["matplotlib", "plotly", "seaborn"]]),
+    reason="Plotting modules has not been installed",
+)
+def test_plot_sum_power_mix(leogo_test_data: dto.EnergySystemData, leogo_expected_result: dto.SimulationResult):
+    sim_result = leogo_expected_result
+    optimisation_model = oogeso.OptimisationModel(leogo_test_data)
+    op.plot_sum_power_mix(sim_result, optimisation_model, carrier="el")