How to use the unit test harness

Overview

This guide shows you how to use the unit test harness to generate tests for WaterTAP unit models. The purpose of this tool is to standardize testing so developers don’t need to write tests from the ground-up for each unit model.

How To

Begin by importing the following essential functions. This example assumes a test file is being created for an anaerobic digester.

import pytest

from pyomo.environ import ConcreteModel

from idaes.core import FlowsheetBlock, UnitModelCostingBlock
from watertap.core.solvers import get_solver
import idaes.core.util.scaling as iscale

from watertap.costing import WaterTAPCosting

# The following imports are unit-model specific
from watertap.unit_models.anaerobic_digester import AD
from watertap.property_models.unit_specific.anaerobic_digestion.adm1_properties import ADM1ParameterBlock
from watertap.property_models.unit_specific.anaerobic_digestion.adm1_properties_vapor import ADM1_vaporParameterBlock
from watertap.property_models.unit_specific.anaerobic_digestion.adm1_reactions import ADM1ReactionParameterBlock
from watertap.unit_models.tests.unit_test_harness import UnitTestHarness

# Get the default solver for testing
solver = get_solver()

Next, set up the build function which will create the flowsheet and specify the property package, reaction package, unit model configuration (named fs.unit), operating conditions, and scaling factors for any variables that are badly scaled. Then, set up the configure function which will use the model, m, returned from build to assert that the specified unit model variables are equivalent to their expected values. This function must also include at least one conservation check in which the user specifies an inlet expression and an outlet expression, and the test harness will assert that the two expressions are equivalent. This functionality should be used to ensure the unit model maintains conservation (mass, energy, momentum, etc.). If failures arise after running the test file, error messages will be displayed that prompt you to modify the build function, address the discrepancy between the expected value for a variable (user-input) and its actual value, and/or address the discrepancy between the conservation expressions.

def build():
    # Create the ConcreteModel and FlowsheetBlock
    m = ConcreteModel()
    m.fs = FlowsheetBlock(dynamic=False)

    # Set up the property package and a reaction package, if relevant
    m.fs.props = ADM1ParameterBlock()
    m.fs.props_vap = ADM1_vaporParameterBlock()
    m.fs.rxn_props = ADM1ReactionParameterBlock(property_package=m.fs.props)

    # Create the unit model and specify configration options
    m.fs.unit = AD(
        liquid_property_package=m.fs.props,
        vapor_property_package=m.fs.props_vap,
        reaction_package=m.fs.rxn_props,
        has_heat_transfer=True,
        has_pressure_change=False,
    )

    # Set the operating conditions
    m.fs.unit.inlet.flow_vol.fix(170 / 24 / 3600)
    m.fs.unit.inlet.temperature.fix(308.15)
    m.fs.unit.inlet.pressure.fix(101325)

    m.fs.unit.inlet.conc_mass_comp[0, "S_su"].fix(0.01)
    m.fs.unit.inlet.conc_mass_comp[0, "S_aa"].fix(0.001)
    m.fs.unit.inlet.conc_mass_comp[0, "S_fa"].fix(0.001)
    m.fs.unit.inlet.conc_mass_comp[0, "S_va"].fix(0.001)
    m.fs.unit.inlet.conc_mass_comp[0, "S_bu"].fix(0.001)
    m.fs.unit.inlet.conc_mass_comp[0, "S_pro"].fix(0.001)
    m.fs.unit.inlet.conc_mass_comp[0, "S_ac"].fix(0.001)
    m.fs.unit.inlet.conc_mass_comp[0, "S_h2"].fix(1e-8)
    m.fs.unit.inlet.conc_mass_comp[0, "S_ch4"].fix(1e-5)
    m.fs.unit.inlet.conc_mass_comp[0, "S_IC"].fix(0.48)
    m.fs.unit.inlet.conc_mass_comp[0, "S_IN"].fix(0.14)
    m.fs.unit.inlet.conc_mass_comp[0, "S_I"].fix(0.02)

    m.fs.unit.inlet.conc_mass_comp[0, "X_c"].fix(2)
    m.fs.unit.inlet.conc_mass_comp[0, "X_ch"].fix(5)
    m.fs.unit.inlet.conc_mass_comp[0, "X_pr"].fix(20)
    m.fs.unit.inlet.conc_mass_comp[0, "X_li"].fix(5)
    m.fs.unit.inlet.conc_mass_comp[0, "X_su"].fix(0.0)
    m.fs.unit.inlet.conc_mass_comp[0, "X_aa"].fix(0.010)
    m.fs.unit.inlet.conc_mass_comp[0, "X_fa"].fix(0.010)
    m.fs.unit.inlet.conc_mass_comp[0, "X_c4"].fix(0.010)
    m.fs.unit.inlet.conc_mass_comp[0, "X_pro"].fix(0.010)
    m.fs.unit.inlet.conc_mass_comp[0, "X_ac"].fix(0.010)
    m.fs.unit.inlet.conc_mass_comp[0, "X_h2"].fix(0.010)
    m.fs.unit.inlet.conc_mass_comp[0, "X_I"].fix(25)

    m.fs.unit.inlet.cations[0].fix(0.04)
    m.fs.unit.inlet.anions[0].fix(0.02)

    m.fs.unit.volume_liquid.fix(3400)
    m.fs.unit.volume_vapor.fix(300)
    m.fs.unit.liquid_outlet.temperature.fix(308.15)

    # Set scaling factors for badly scaled variables
    iscale.set_scaling_factor(
    m.fs.unit.liquid_phase.mass_transfer_term[0, "Liq", "S_h2"], 1e7
    )

    iscale.calculate_scaling_factors(m.fs.unit)

    return m

class TestAnaerobicDigester(UnitTestHarness):
    def configure(self):
        m = build()

        # Check the expected unit model outputs

        self.unit_solutions[m.fs.unit.liquid_outlet.pressure[0]] = 101325
        self.unit_solutions[m.fs.unit.liquid_outlet.temperature[0]] = 308.15
        self.unit_solutions[
            m.fs.unit.liquid_outlet.conc_mass_comp[0, "S_I"]
        ] = 0.3287724
        self.unit_solutions[
            m.fs.unit.liquid_outlet.conc_mass_comp[0, "S_aa"]
        ] = 0.00531408
        self.unit_solutions[
            m.fs.unit.liquid_outlet.conc_mass_comp[0, "S_ac"]
        ] = 0.1977833
        self.unit_solutions[
            m.fs.unit.liquid_outlet.conc_mass_comp[0, "S_bu"]
        ] = 0.0132484
        self.unit_solutions[
            m.fs.unit.liquid_outlet.conc_mass_comp[0, "S_ch4"]
        ] = 0.0549707
        self.unit_solutions[
            m.fs.unit.liquid_outlet.conc_mass_comp[0, "S_fa"]
        ] = 0.0986058
        self.unit_solutions[
            m.fs.unit.liquid_outlet.conc_mass_comp[0, "S_h2"]
        ] = 2.35916e-07
        self.unit_solutions[
            m.fs.unit.liquid_outlet.conc_mass_comp[0, "S_pro"]
        ] = 0.0157813
        self.unit_solutions[
            m.fs.unit.liquid_outlet.conc_mass_comp[0, "S_su"]
        ] = 0.01195333
        self.unit_solutions[
            m.fs.unit.liquid_outlet.conc_mass_comp[0, "S_va"]
        ] = 0.011622969
        self.unit_solutions[m.fs.unit.liquid_outlet.conc_mass_comp[0, "X_I"]] = 25.6217
        self.unit_solutions[
            m.fs.unit.liquid_outlet.conc_mass_comp[0, "X_aa"]
        ] = 1.1793147
        self.unit_solutions[
            m.fs.unit.liquid_outlet.conc_mass_comp[0, "X_ac"]
        ] = 0.760653
        self.unit_solutions[m.fs.unit.liquid_outlet.conc_mass_comp[0, "X_c"]] = 0.308718
        self.unit_solutions[
            m.fs.unit.liquid_outlet.conc_mass_comp[0, "X_c4"]
        ] = 0.431974
        self.unit_solutions[
            m.fs.unit.liquid_outlet.conc_mass_comp[0, "X_ch"]
        ] = 0.027947465
        self.unit_solutions[
            m.fs.unit.liquid_outlet.conc_mass_comp[0, "X_fa"]
        ] = 0.2430681
        self.unit_solutions[
            m.fs.unit.liquid_outlet.conc_mass_comp[0, "X_h2"]
        ] = 0.3170629
        self.unit_solutions[
            m.fs.unit.liquid_outlet.conc_mass_comp[0, "X_li"]
        ] = 0.0294834
        self.unit_solutions[
            m.fs.unit.liquid_outlet.conc_mass_comp[0, "X_pr"]
        ] = 0.102574392
        self.unit_solutions[
            m.fs.unit.liquid_outlet.conc_mass_comp[0, "X_pro"]
        ] = 0.137323
        self.unit_solutions[
            m.fs.unit.liquid_outlet.conc_mass_comp[0, "X_su"]
        ] = 0.420219
        self.unit_solutions[
            m.fs.unit.liquid_outlet.conc_mass_comp[0, "S_IC"]
        ] = 1.8320212
        self.unit_solutions[
            m.fs.unit.liquid_outlet.conc_mass_comp[0, "S_IN"]
        ] = 1.8235307
        self.unit_solutions[m.fs.unit.liquid_outlet.anions[0]] = 0.0200033
        self.unit_solutions[m.fs.unit.liquid_outlet.cations[0]] = 0.0400066
        self.unit_solutions[m.fs.unit.vapor_outlet.pressure[0]] = 106659.5225
        self.unit_solutions[m.fs.unit.vapor_outlet.temperature[0]] = 308.15
        self.unit_solutions[m.fs.unit.vapor_outlet.flow_vol[0]] = 0.03249637
        self.unit_solutions[
            m.fs.unit.vapor_outlet.conc_mass_comp[0, "S_ch4"]
        ] = 1.6216465
        self.unit_solutions[
            m.fs.unit.vapor_outlet.conc_mass_comp[0, "S_co2"]
        ] = 0.169417
        self.unit_solutions[m.fs.unit.KH_co2[0]] = 0.02714666
        self.unit_solutions[m.fs.unit.KH_ch4[0]] = 0.001161902
        self.unit_solutions[m.fs.unit.KH_h2[0]] = 0.0007384652
        self.unit_solutions[m.fs.unit.electricity_consumption[0]] = 23.7291667
        self.unit_solutions[m.fs.unit.hydraulic_retention_time[0]] = 1880470.588
        self.unit_solutions[m.fs.unit.costing.capital_cost] = 2166581.415

        # Conservation check

        self.conservation_equality = {
            "Check 1": {
                "in": m.fs.unit.inlet.flow_vol[0],
                "out": (
                    m.fs.unit.liquid_outlet.flow_vol[0] * m.fs.props.dens_mass
                    + m.fs.unit.vapor_outlet.flow_vol[0] * m.fs.props_vap.dens_mass
                )
                / m.fs.props.dens_mass,
            },
            "Check 2": {
                "in": (
                    m.fs.unit.inlet.flow_vol[0]
                    * m.fs.props.dens_mass
                    * m.fs.props.cp_mass
                    * (m.fs.unit.inlet.temperature[0] - m.fs.props.temperature_ref)
                )
                - (
                    m.fs.unit.liquid_outlet.flow_vol[0]
                    * m.fs.props.dens_mass
                    * m.fs.props.cp_mass
                    * (
                        m.fs.unit.liquid_outlet.temperature[0]
                        - m.fs.props.temperature_ref
                    )
                ),
                "out": -1 * m.fs.unit.liquid_phase.enthalpy_transfer[0],
            },
        }

        return m