Example #1
0
def define_default_scaling_factors(b):
    """
    Method to set default scaling factors for the property package. Scaling
    factors are based on the default initial value for each variable provided
    in the state_bounds config argument.
    """
    # Get bounds and initial values from config args
    units = b.get_metadata().derived_units
    state_bounds = b.config.state_bounds

    if state_bounds is None:
        return

    try:
        f_bounds = state_bounds["flow_mol"]
        if len(f_bounds) == 4:
            f_init = pyunits.convert_value(f_bounds[1],
                                           from_units=f_bounds[3],
                                           to_units=units["flow_mole"])
        else:
            f_init = f_bounds[1]
    except KeyError:
        f_init = 1

    try:
        p_bounds = state_bounds["pressure"]
        if len(p_bounds) == 4:
            p_init = pyunits.convert_value(p_bounds[1],
                                           from_units=p_bounds[3],
                                           to_units=units["pressure"])
        else:
            p_init = p_bounds[1]
    except KeyError:
        p_init = 1

    try:
        t_bounds = state_bounds["temperature"]
        if len(t_bounds) == 4:
            t_init = pyunits.convert_value(t_bounds[1],
                                           from_units=t_bounds[3],
                                           to_units=units["temperature"])
        else:
            t_init = t_bounds[1]
    except KeyError:
        t_init = 1

    # Set default scaling factors
    b.set_default_scaling("flow_mol", 1 / f_init)
    b.set_default_scaling("flow_mol_phase", 1 / f_init)
    b.set_default_scaling("flow_mol_comp", 1 / f_init)
    b.set_default_scaling("flow_mol_phase_comp", 1 / f_init)
    b.set_default_scaling("pressure", 1 / p_init)
    b.set_default_scaling("temperature", 1 / t_init)
Example #2
0
def get_bounds_from_config(b, state, base_units):
    """
    Method to take a 3- or 4-tuple state definition config argument and return
    tuples for the bounds and default value of the Var object.

    Expects the form (lower, default, upper, units) where units is optional

    Args:
        b - StateBlock on which the state vars are to be constructed
        state - name of state var as a string (to be matched with config dict)
        base_units - base units of state var to be used if conversion required

    Returns:
        bounds - 2-tuple of state var bounds in base units
        default_val - default value of state var in base units
    """
    try:
        var_config = b.params.config.state_bounds[state]
    except (KeyError, TypeError):
        # State definition missing
        return (None, None), None

    if len(var_config) == 4:
        # Units provided, need to convert values
        bounds = (pyunits.convert_value(var_config[0],
                                        from_units=var_config[3],
                                        to_units=base_units),
                  pyunits.convert_value(var_config[2],
                                        from_units=var_config[3],
                                        to_units=base_units))
        default_val = pyunits.convert_value(var_config[1],
                                            from_units=var_config[3],
                                            to_units=base_units)
    else:
        bounds = (var_config[0], var_config[2])
        default_val = var_config[1]

    return bounds, default_val
    def test_conservation(self, btx):
        assert abs(value(btx.fs.unit.inlet_1.flow_mol[0] -
                         btx.fs.unit.outlet_1.flow_mol[0])) <= 1e-6
        assert abs(value(btx.fs.unit.inlet_2.flow_mol[0] -
                         btx.fs.unit.outlet_2.flow_mol[0])) <= 1e-6

        shell = value(
                btx.fs.unit.outlet_1.flow_mol[0] *
                (btx.fs.unit.shell.properties_in[0].enth_mol -
                 btx.fs.unit.shell.properties_out[0].enth_mol))
        tube = pyunits.convert_value(value(
                btx.fs.unit.outlet_2.flow_mol[0] *
                (btx.fs.unit.tube.properties_in[0].enth_mol -
                 btx.fs.unit.tube.properties_out[0].enth_mol)),
            from_units=pyunits.kJ/pyunits.s,
            to_units=pyunits.J/pyunits.s)
        assert abs(shell + tube) <= 1e-6
Example #4
0
    def build(self):
        super(ComponentData, self).build()

        # If the component_list does not exist, add reference to new Component
        # The IF is mostly for backwards compatability, to allow for old-style
        # property packages where the component_list already exists but we
        # need to add new Component objects
        if not self.config._component_list_exists:
            if not self.config._electrolyte:
                self.__add_to_component_list()
            else:
                self._add_to_electrolyte_component_list()

        base_units = self.parent_block().get_metadata().default_units
        if isinstance(base_units["mass"], _PyomoUnit):
            # Backwards compatability check
            p_units = (base_units["mass"] / base_units["length"] /
                       base_units["time"]**2)
        else:
            # Backwards compatability check
            p_units = None

        # Create Param for molecular weight if provided
        if "mw" in self.config.parameter_data:
            if isinstance(self.config.parameter_data["mw"], tuple):
                mw_init = pyunits.convert_value(
                    self.config.parameter_data["mw"][0],
                    from_units=self.config.parameter_data["mw"][1],
                    to_units=base_units["mass"] / base_units["amount"])
            else:
                _log.debug("{} no units provided for parameter mw - assuming "
                           "default units".format(self.name))
                mw_init = self.config.parameter_data["mw"]
            self.mw = Param(initialize=mw_init,
                            units=base_units["mass"] / base_units["amount"])

        # Create Vars for common parameters
        param_dict = {
            "pressure_crit": p_units,
            "temperature_crit": base_units["temperature"],
            "omega": None
        }
        for p, u in param_dict.items():
            if p in self.config.parameter_data:
                self.add_component(p, Var(units=u))
                set_param_from_config(self, p)
Example #5
0
def set_param_value(b, param, units, config=None, index=None):
    """
    Utility method to set parameter value from a config block. This allows for
    converting units if required. This method directly sets the value of the
    parameter.

    Args:
        b - block on which parameter and config block are defined
        param - name of parameter as str. Used to find param and config arg
        units - units of param object (used if conversion required)
        config - (optional) config block to get parameter data from. If
                unset, assumes b.config.
        index - (optional) used for pure component properties where a single
                property may have multiple parameters associated with it.

    Returns:
        None
    """
    if config is None:
        config = b.config

    if index is None:
        param_obj = getattr(b, param)
        p_data = config.parameter_data[param]
    else:
        param_obj = getattr(b, param+"_"+index)
        p_data = config.parameter_data[param][index]

    if isinstance(p_data, tuple):
        if units is None and p_data[1] is None:
            param_obj.value = p_data[0]
        else:
            param_obj.value = pyunits.convert_value(
                p_data[0], from_units=p_data[1], to_units=units)
    else:
        _log.debug("{} no units provided for parameter {} - assuming default "
                   "units".format(b.name, param))
        param_obj.value = p_data
Example #6
0
    def build(self):
        # build always starts by calling super().build()
        # This triggers a lot of boilerplate in the background for you
        super().build()

        # this creates blank scaling factors, which are populated later
        self.scaling_factor = Suffix(direction=Suffix.EXPORT)

        # Next, get the base units of measurement from the property definition
        units_meta = self.config.property_package.get_metadata().get_derived_units

        # check the optional config arg 'chemical_additives'
        common_msg = "The 'chemical_additives' dict MUST contain a dict of 'parameter_data' for " + \
                     "each chemical name. That 'parameter_data' dict MUST contain 'mw_chem', " + \
                     "'moles_salt_per_mole_additive', and 'mw_salt' as keys. Users are also " + \
                     "required to provide the values for the molecular weights and the units " + \
                     "within a tuple arg. Example format provided below.\n\n" + \
                     "{'chem_name_1': \n" + \
                     "     {'parameter_data': \n" + \
                     "        {'mw_additive': (value, units), \n" + \
                     "         'moles_salt_per_mole_additive': value, \n" + \
                     "         'mw_salt': (value, units)} \n" + \
                     "     }, \n" + \
                     "}\n\n"
        mw_adds = {}
        mw_salts = {}
        molar_rat = {}
        for j in self.config.chemical_additives:
            if type(self.config.chemical_additives[j]) != dict:
                raise ConfigurationError("\n Did not provide a 'dict' for chemical \n" + common_msg)
            if 'parameter_data' not in self.config.chemical_additives[j]:
                raise ConfigurationError("\n Did not provide a 'parameter_data' for chemical \n" + common_msg)
            if 'mw_additive' not in self.config.chemical_additives[j]['parameter_data']:
                raise ConfigurationError("\n Did not provide a 'mw_additive' for chemical \n" + common_msg)
            if 'moles_salt_per_mole_additive' not in self.config.chemical_additives[j]['parameter_data']:
                raise ConfigurationError("\n Did not provide a 'moles_salt_per_mole_additive' for chemical \n" + common_msg)
            if 'mw_salt' not in self.config.chemical_additives[j]['parameter_data']:
                raise ConfigurationError("\n Did not provide a 'mw_salt' for chemical \n" + common_msg)
            if type(self.config.chemical_additives[j]['parameter_data']['mw_additive']) != tuple:
                raise ConfigurationError("\n Did not provide a tuple for 'mw_additive' \n" + common_msg)
            if type(self.config.chemical_additives[j]['parameter_data']['mw_salt']) != tuple:
                raise ConfigurationError("\n Did not provide a tuple for 'mw_salt' \n" + common_msg)
            if not isinstance(self.config.chemical_additives[j]['parameter_data']['moles_salt_per_mole_additive'], (int,float)):
                raise ConfigurationError("\n Did not provide a number for 'moles_salt_per_mole_additive' \n" + common_msg)

            #Populate temp dicts for parameter and variable setting
            mw_adds[j] = pyunits.convert_value(self.config.chemical_additives[j]['parameter_data']['mw_additive'][0],
                        from_units=self.config.chemical_additives[j]['parameter_data']['mw_additive'][1], to_units=pyunits.kg/pyunits.mol)
            mw_salts[j] = pyunits.convert_value(self.config.chemical_additives[j]['parameter_data']['mw_salt'][0],
                        from_units=self.config.chemical_additives[j]['parameter_data']['mw_salt'][1], to_units=pyunits.kg/pyunits.mol)
            molar_rat[j] = self.config.chemical_additives[j]['parameter_data']['moles_salt_per_mole_additive']

        # Add unit variables
        # Linear relationship between TSS (mg/L) and Turbidity (NTU)
        #           TSS (mg/L) = Turbidity (NTU) * slope + intercept
        #   Default values come from the following paper:
        #       H. Rugner, M. Schwientek,B. Beckingham, B. Kuch, P. Grathwohl,
        #       Environ. Earth Sci. 69 (2013) 373-380. DOI: 10.1007/s12665-013-2307-1
        self.slope = Var(
            self.flowsheet().config.time,
            initialize=1.86,
            bounds=(1e-8, 10),
            domain=NonNegativeReals,
            units=pyunits.mg/pyunits.L,
            doc='Slope relation between TSS (mg/L) and Turbidity (NTU)')

        self.intercept = Var(
            self.flowsheet().config.time,
            initialize=0,
            bounds=(0, 10),
            domain=NonNegativeReals,
            units=pyunits.mg/pyunits.L,
            doc='Intercept relation between TSS (mg/L) and Turbidity (NTU)')

        self.initial_turbidity_ntu = Var(
            self.flowsheet().config.time,
            initialize=50,
            bounds=(0, 10000),
            domain=NonNegativeReals,
            units=pyunits.dimensionless,
            doc='Initial measured Turbidity (NTU) from Jar Test')

        self.final_turbidity_ntu = Var(
            self.flowsheet().config.time,
            initialize=1,
            bounds=(0, 10000),
            domain=NonNegativeReals,
            units=pyunits.dimensionless,
            doc='Final measured Turbidity (NTU) from Jar Test')

        self.chemical_doses = Var(
            self.flowsheet().config.time,
            self.config.chemical_additives.keys(),
            initialize=0,
            bounds=(0, 100),
            domain=NonNegativeReals,
            units=pyunits.mg/pyunits.L,
            doc='Dosages of the set of chemical additives')

        self.chemical_mw = Param(
            self.config.chemical_additives.keys(),
            mutable=True,
            initialize=mw_adds,
            domain=NonNegativeReals,
            units=pyunits.kg/pyunits.mol,
            doc='Molecular weights of the set of chemical additives')

        self.salt_mw = Param(
            self.config.chemical_additives.keys(),
            mutable=True,
            initialize=mw_salts,
            domain=NonNegativeReals,
            units=pyunits.kg/pyunits.mol,
            doc='Molecular weights of the produced salts from chemical additives')

        self.salt_from_additive_mole_ratio = Param(
            self.config.chemical_additives.keys(),
            mutable=True,
            initialize=molar_rat,
            domain=NonNegativeReals,
            units=pyunits.mol/pyunits.mol,
            doc='Moles of the produced salts from 1 mole of chemical additives')


        # Build control volume for feed side
        self.control_volume = ControlVolume0DBlock(default={
            "dynamic": False,
            "has_holdup": False,
            "property_package": self.config.property_package,
            "property_package_args": self.config.property_package_args})

        self.control_volume.add_state_blocks(
            has_phase_equilibrium=False)

        self.control_volume.add_material_balances(
            balance_type=self.config.material_balance_type,
            has_mass_transfer=True)

        # NOTE: This checks for if an energy_balance_type is defined
        if hasattr(self.config, "energy_balance_type"):
            self.control_volume.add_energy_balances(
                balance_type=self.config.energy_balance_type,
                has_enthalpy_transfer=False)

        self.control_volume.add_momentum_balances(
            balance_type=self.config.momentum_balance_type,
            has_pressure_change=False)

        # Add ports
        self.add_inlet_port(name='inlet', block=self.control_volume)
        self.add_outlet_port(name='outlet', block=self.control_volume)

        # Check _phase_component_set for required items
        if ('Liq', 'TDS') not in self.config.property_package._phase_component_set:
            raise ConfigurationError(
                "Coagulation-Flocculation model MUST contain ('Liq','TDS') as a component, but "
                "the property package has only specified the following components {}"
                    .format([p for p in self.config.property_package._phase_component_set]))
        if ('Liq', 'Sludge') not in self.config.property_package._phase_component_set:
            raise ConfigurationError(
                "Coagulation-Flocculation model MUST contain ('Liq','Sludge') as a component, but "
                "the property package has only specified the following components {}"
                    .format([p for p in self.config.property_package._phase_component_set]))
        if ('Liq', 'TSS') not in self.config.property_package._phase_component_set:
            raise ConfigurationError(
                "Coagulation-Flocculation model MUST contain ('Liq','TSS') as a component, but "
                "the property package has only specified the following components {}"
                    .format([p for p in self.config.property_package._phase_component_set]))

        # -------- Add constraints ---------
        # Adds isothermal constraint if no energy balance present
        if not hasattr(self.config, "energy_balance_type"):
            @self.Constraint(self.flowsheet().config.time,
                             doc="Isothermal condition")
            def eq_isothermal(self, t):
                return (self.control_volume.properties_out[t].temperature == self.control_volume.properties_in[t].temperature)

        # Constraint for tss loss rate based on measured final turbidity
        self.tss_loss_rate = Var(
            self.flowsheet().config.time,
            initialize=1,
            bounds=(0, 100),
            domain=NonNegativeReals,
            units=units_meta('mass')*units_meta('time')**-1,
            doc='Mass per time loss rate of TSS based on the measured final turbidity')

        @self.Constraint(self.flowsheet().config.time,
                         doc="Constraint for the loss rate of TSS to be used in mass_transfer_term")
        def eq_tss_loss_rate(self, t):
            tss_out = pyunits.convert(self.slope[t]*self.final_turbidity_ntu[t] + self.intercept[t],
                                    to_units=units_meta('mass')*units_meta('length')**-3)
            input_rate = self.control_volume.properties_in[t].flow_mass_phase_comp['Liq','TSS']
            exit_rate = self.control_volume.properties_out[t].flow_vol_phase['Liq']*tss_out

            return (self.tss_loss_rate[t] == input_rate - exit_rate)

        # Constraint for tds gain rate based on 'chemical_doses' and 'chemical_additives'
        if self.config.chemical_additives:
            self.tds_gain_rate = Var(
                self.flowsheet().config.time,
                initialize=0,
                bounds=(0, 100),
                domain=NonNegativeReals,
                units=units_meta('mass')*units_meta('time')**-1,
                doc='Mass per time gain rate of TDS based on the chemicals added for coagulation')

            @self.Constraint(self.flowsheet().config.time,
                             doc="Constraint for the loss rate of TSS to be used in mass_transfer_term")
            def eq_tds_gain_rate(self, t):
                sum = 0
                for j in self.config.chemical_additives.keys():
                    chem_dose = pyunits.convert(self.chemical_doses[t, j],
                                    to_units=units_meta('mass')*units_meta('length')**-3)
                    chem_dose = chem_dose/self.chemical_mw[j] * \
                            self.salt_from_additive_mole_ratio[j] * \
                            self.salt_mw[j]*self.control_volume.properties_out[t].flow_vol_phase['Liq']
                    sum = sum+chem_dose

                return (self.tds_gain_rate[t] == sum)

        # Add constraints for mass transfer terms
        @self.Constraint(self.flowsheet().config.time,
                         self.config.property_package.phase_list,
                         self.config.property_package.component_list,
                         doc="Mass transfer term")
        def eq_mass_transfer_term(self, t, p, j):
            if (p, j) == ('Liq', 'TSS'):
                return self.control_volume.mass_transfer_term[t, p, j] == -self.tss_loss_rate[t]
            elif (p, j) == ('Liq', 'Sludge'):
                return self.control_volume.mass_transfer_term[t, p, j] == self.tss_loss_rate[t]
            elif (p, j) == ('Liq', 'TDS'):
                if self.config.chemical_additives:
                    return self.control_volume.mass_transfer_term[t, p, j] == self.tds_gain_rate[t]
                else:
                    return self.control_volume.mass_transfer_term[t, p, j] == 0.0
            else:
                return self.control_volume.mass_transfer_term[t, p, j] == 0.0
    def initialize(
        self,
        state_args_1=None,
        state_args_2=None,
        outlvl=idaeslog.NOTSET,
        solver="ipopt",
        optarg={"tol": 1e-6},
        duty=None,
    ):
        """
        Heat exchanger initialization method.

        Args:
            state_args_1 : a dict of arguments to be passed to the property
                initialization for the hot side (see documentation of the specific
                property package) (default = {}).
            state_args_2 : a dict of arguments to be passed to the property
                initialization for the cold side (see documentation of the specific
                property package) (default = {}).
            outlvl : sets output level of initialization routine
            optarg : solver options dictionary object (default={'tol': 1e-6})
            solver : str indicating which solver to use during
                     initialization (default = 'ipopt')
            duty : an initial guess for the amount of heat transfered. This
                should be a tuple in the form (value, units), (default
                = (1000 J/s))

        Returns:
            None

        """
        # Set solver options
        init_log = idaeslog.getInitLogger(self.name, outlvl, tag="unit")
        solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="unit")

        hot_side = getattr(self, self.config.hot_side_name)
        cold_side = getattr(self, self.config.cold_side_name)

        opt = SolverFactory(solver)
        opt.options = optarg
        flags1 = hot_side.initialize(outlvl=outlvl,
                                     optarg=optarg,
                                     solver=solver,
                                     state_args=state_args_1)

        init_log.info_high("Initialization Step 1a (hot side) Complete.")

        flags2 = cold_side.initialize(outlvl=outlvl,
                                      optarg=optarg,
                                      solver=solver,
                                      state_args=state_args_2)

        init_log.info_high("Initialization Step 1b (cold side) Complete.")
        # ---------------------------------------------------------------------
        # Solve unit without heat transfer equation
        # if costing block exists, deactivate
        if hasattr(self, "costing"):
            self.costing.deactivate()

        self.heat_transfer_equation.deactivate()

        # Get side 1 and side 2 heat units, and convert duty as needed
        s1_units = hot_side.heat.get_units()
        s2_units = cold_side.heat.get_units()

        if duty is None:
            # Assume 1000 J/s and check for unitless properties
            if s1_units is None and s2_units is None:
                # Backwards compatability for unitless properties
                s1_duty = -1000
                s2_duty = 1000
            else:
                s1_duty = pyunits.convert_value(-1000,
                                                from_units=pyunits.W,
                                                to_units=s1_units)
                s2_duty = pyunits.convert_value(1000,
                                                from_units=pyunits.W,
                                                to_units=s2_units)
        else:
            # Duty provided with explicit units
            s1_duty = -pyunits.convert_value(
                duty[0], from_units=duty[1], to_units=s1_units)
            s2_duty = pyunits.convert_value(duty[0],
                                            from_units=duty[1],
                                            to_units=s2_units)

        cold_side.heat.fix(s2_duty)
        for i in hot_side.heat:
            hot_side.heat[i].value = s1_duty

        with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
            res = opt.solve(self, tee=slc.tee)
        init_log.info_high("Initialization Step 2 {}.".format(
            idaeslog.condition(res)))
        cold_side.heat.unfix()
        self.heat_transfer_equation.activate()
        # ---------------------------------------------------------------------
        # Solve unit
        with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
            res = opt.solve(self, tee=slc.tee)
        init_log.info_high("Initialization Step 3 {}.".format(
            idaeslog.condition(res)))
        # ---------------------------------------------------------------------
        # Release Inlet state
        hot_side.release_state(flags1, outlvl=outlvl)
        cold_side.release_state(flags2, outlvl=outlvl)

        init_log.info("Initialization Completed, {}".format(
            idaeslog.condition(res)))
        # if costing block exists, activate and initialize
        if hasattr(self, "costing"):
            self.costing.activate()
            costing.initialize(self.costing)
    def initialize(
        self,
        hot_side_state_args=None,
        cold_side_state_args=None,
        outlvl=idaeslog.NOTSET,
        solver=None,
        optarg=None,
        duty=None,
    ):
        """
        Heat exchanger initialization method.

        Args:
            hot_side_state_args : a dict of arguments to be passed to the
                property initialization for the hot side (see documentation of
                the specific property package) (default = None).
            cold_side_state_args : a dict of arguments to be passed to the
                property initialization for the cold side (see documentation of
                the specific property package) (default = None).
            outlvl : sets output level of initialization routine
            optarg : solver options dictionary object (default=None, use
                     default solver options)
            solver : str indicating which solver to use during
                     initialization (default = None, use default solver)
            duty : an initial guess for the amount of heat transfered. This
                should be a tuple in the form (value, units), (default
                = (1000 J/s))

        Returns:
            None

        """
        # Set solver options
        init_log = idaeslog.getInitLogger(self.name, outlvl, tag="unit")
        solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="unit")

        hot_side = self.hot_side
        cold_side = self.cold_side

        # Create solver
        opt = get_solver(solver, optarg)

        flags1 = hot_side.initialize(outlvl=outlvl,
                                     optarg=optarg,
                                     solver=solver,
                                     state_args=hot_side_state_args)

        init_log.info_high("Initialization Step 1a (hot side) Complete.")

        flags2 = cold_side.initialize(outlvl=outlvl,
                                      optarg=optarg,
                                      solver=solver,
                                      state_args=cold_side_state_args)

        init_log.info_high("Initialization Step 1b (cold side) Complete.")

        # ---------------------------------------------------------------------
        # Solve unit without heat transfer equation
        # if costing block exists, deactivate
        if hasattr(self, "costing"):
            self.costing.deactivate()

        self.energy_balance_constraint.deactivate()
        self.effectiveness_correlation.deactivate()
        self.effectiveness.fix(0.68)

        # Get side 1 and side 2 heat units, and convert duty as needed
        s1_units = hot_side.heat.get_units()
        s2_units = cold_side.heat.get_units()

        if duty is None:
            # Assume 1000 J/s and check for unitless properties
            if s1_units is None and s2_units is None:
                # Backwards compatability for unitless properties
                s1_duty = -1000
                s2_duty = 1000
            else:
                s1_duty = pyunits.convert_value(-1000,
                                                from_units=pyunits.W,
                                                to_units=s1_units)
                s2_duty = pyunits.convert_value(1000,
                                                from_units=pyunits.W,
                                                to_units=s2_units)
        else:
            # Duty provided with explicit units
            s1_duty = -pyunits.convert_value(
                duty[0], from_units=duty[1], to_units=s1_units)
            s2_duty = pyunits.convert_value(duty[0],
                                            from_units=duty[1],
                                            to_units=s2_units)

        cold_side.heat.fix(s2_duty)
        for i in hot_side.heat:
            hot_side.heat[i].value = s1_duty

        with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
            res = opt.solve(self, tee=slc.tee)

        init_log.info_high("Initialization Step 2 {}.".format(
            idaeslog.condition(res)))

        cold_side.heat.unfix()
        self.energy_balance_constraint.activate()

        for t in self.effectiveness:
            calculate_variable_from_constraint(
                self.effectiveness[t], self.effectiveness_correlation[t])

        # ---------------------------------------------------------------------
        # Solve unit with new effectiveness factor
        with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
            res = opt.solve(self, tee=slc.tee)
        init_log.info_high("Initialization Step 3 {}.".format(
            idaeslog.condition(res)))

        self.effectiveness_correlation.activate()
        self.effectiveness.unfix()

        # ---------------------------------------------------------------------
        # Final solve of full modelr
        with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
            res = opt.solve(self, tee=slc.tee)
        init_log.info_high("Initialization Step 4 {}.".format(
            idaeslog.condition(res)))

        # ---------------------------------------------------------------------
        # Release Inlet state
        hot_side.release_state(flags1, outlvl=outlvl)
        cold_side.release_state(flags2, outlvl=outlvl)

        init_log.info("Initialization Completed, {}".format(
            idaeslog.condition(res)))

        # if costing block exists, activate and initialize
        if hasattr(self, "costing"):
            self.costing.activate()
            costing.initialize(self.costing)
Example #9
0
    def build(self):
        # build always starts by calling super().build()
        # This triggers a lot of boilerplate in the background for you
        super().build()

        # this creates blank scaling factors, which are populated later
        self.scaling_factor = Suffix(direction=Suffix.EXPORT)

        # Next, get the base units of measurement from the property definition
        units_meta = self.config.property_package.get_metadata(
        ).get_derived_units

        # Check configs for errors
        common_msg = (
            "The 'chemical_mapping_data' dict MUST contain a dict of names that map \n"
            +
            "to each chemical name in the property package for boron and borate. \n"
            +
            "Optionally, user may provide names that also map to protons and hydroxide, \n"
            +
            "as well as to the cation from the caustic additive. The 'caustic_additive' \n"
            + "must be a dict that contains molecular weight and charge.\n\n" +
            "Example:\n" + "-------\n" +
            "{'boron_name': 'B[OH]3',    #[is required]\n" +
            " 'borate_name': 'B[OH]4_-', #[is required]\n" +
            " 'proton_name': 'H_+',      #[is OPTIONAL]\n" +
            " 'hydroxide_name': 'OH_-',  #[is OPTIONAL]\n" +
            " 'caustic_additive': \n" +
            "     {'additive_name': 'NaOH',                    #[is OPTIONAL]\n"
            +
            "      'cation_name': 'Na_+',                      #[is required]\n"
            +
            "      'mw_additive': (40, pyunits.g/pyunits.mol), #[is required]\n"
            +
            "      'moles_cation_per_additive': 1,             #[is required]\n"
            + "     }, \n" + "}\n\n")
        if (type(self.config.chemical_mapping_data) != dict
                or self.config.chemical_mapping_data == {}):
            raise ConfigurationError(
                "\n\n Did not provide a 'dict' for 'chemical_mapping_data' \n"
                + common_msg)
        if ("boron_name" not in self.config.chemical_mapping_data
                or "borate_name" not in self.config.chemical_mapping_data or
                "caustic_additive" not in self.config.chemical_mapping_data):
            raise ConfigurationError(
                "\n\n Missing some required information in 'chemical_mapping_data' \n"
                + common_msg)
        if ("mw_additive"
                not in self.config.chemical_mapping_data["caustic_additive"]
                or "moles_cation_per_additive"
                not in self.config.chemical_mapping_data["caustic_additive"]
                or "cation_name"
                not in self.config.chemical_mapping_data["caustic_additive"]):
            raise ConfigurationError(
                "\n\n Missing some required information in 'chemical_mapping_data' \n"
                + common_msg)
        if (type(self.config.chemical_mapping_data["caustic_additive"]
                 ["mw_additive"]) != tuple):
            raise ConfigurationError(
                "\n Did not provide a tuple for 'mw_additive' \n" + common_msg)

        # Assign name IDs locally for reference later when building constraints
        self.boron_name_id = self.config.chemical_mapping_data["boron_name"]
        self.borate_name_id = self.config.chemical_mapping_data["borate_name"]
        if "proton_name" in self.config.chemical_mapping_data:
            self.proton_name_id = self.config.chemical_mapping_data[
                "proton_name"]
        else:
            self.proton_name_id = None
        if "hydroxide_name" in self.config.chemical_mapping_data:
            self.hydroxide_name_id = self.config.chemical_mapping_data[
                "hydroxide_name"]
        else:
            self.hydroxide_name_id = None
        if "cation_name" in self.config.chemical_mapping_data[
                "caustic_additive"]:
            self.cation_name_id = self.config.chemical_mapping_data[
                "caustic_additive"]["cation_name"]
        else:
            self.cation_name_id = None
        if "additive_name" in self.config.chemical_mapping_data[
                "caustic_additive"]:
            self.caustic_chem_name = self.config.chemical_mapping_data[
                "caustic_additive"]["additive_name"]
        else:
            self.caustic_chem_name = None

        # Cross reference and check given names with set of valid names
        if self.boron_name_id not in self.config.property_package.component_list:
            raise ConfigurationError(
                "\n Given 'boron_name' {" + self.boron_name_id +
                "} does not match " +
                "any species name from the property package \n{}".format(
                    [c for c in self.config.property_package.component_list]))
        if self.borate_name_id not in self.config.property_package.component_list:
            raise ConfigurationError(
                "\n Given 'borate_name' {" + self.borate_name_id +
                "} does not match " +
                "any species name from the property package \n{}".format(
                    [c for c in self.config.property_package.component_list]))
        if self.proton_name_id != None:
            if self.proton_name_id not in self.config.property_package.component_list:
                raise ConfigurationError(
                    "\n Given 'proton_name' {" + self.proton_name_id +
                    "} does not match " +
                    "any species name from the property package \n{}".format([
                        c for c in self.config.property_package.component_list
                    ]))
        if self.hydroxide_name_id != None:
            if (self.hydroxide_name_id
                    not in self.config.property_package.component_list):
                raise ConfigurationError(
                    "\n Given 'hydroxide_name' {" + self.hydroxide_name_id +
                    "} does not match " +
                    "any species name from the property package \n{}".format([
                        c for c in self.config.property_package.component_list
                    ]))
        if self.cation_name_id != None:
            if self.cation_name_id not in self.config.property_package.component_list:
                raise ConfigurationError(
                    "\n Given 'cation_name' {" + self.cation_name_id +
                    "} does not match " +
                    "any species name from the property package \n{}".format([
                        c for c in self.config.property_package.component_list
                    ]))

        # check for existence of inherent reactions
        #   This is to ensure that no degeneracy could be introduced
        #   in the system of equations (may not need this explicit check)
        if hasattr(self.config.property_package, "inherent_reaction_idx"):
            raise ConfigurationError(
                "\n Property Package CANNOT contain 'inherent_reactions' \n")

        # cation set reference
        cation_set = self.config.property_package.cation_set

        # anion set reference
        anion_set = self.config.property_package.anion_set

        # Add param to store all charges of ions for convenience
        self.ion_charge = Param(
            anion_set | cation_set,
            initialize=1,
            mutable=True,
            units=pyunits.dimensionless,
            doc="Ion charge",
        )

        # Loop through full set and try to assign charge
        for j in self.config.property_package.component_list:
            if j in anion_set or j in cation_set:
                self.ion_charge[
                    j] = self.config.property_package.get_component(
                        j).config.charge

        # Add unit variables and parameters
        mw_add = pyunits.convert_value(
            self.config.chemical_mapping_data["caustic_additive"]
            ["mw_additive"][0],
            from_units=self.config.chemical_mapping_data["caustic_additive"]
            ["mw_additive"][1],
            to_units=pyunits.kg / pyunits.mol,
        )
        self.caustic_mw = Param(
            mutable=True,
            initialize=mw_add,
            domain=NonNegativeReals,
            units=pyunits.kg / pyunits.mol,
            doc="Molecular weight of the caustic additive",
        )
        self.additive_molar_ratio = Param(
            mutable=True,
            initialize=self.config.chemical_mapping_data["caustic_additive"]
            ["moles_cation_per_additive"],
            domain=NonNegativeReals,
            units=pyunits.dimensionless,
            doc="Moles of cation per moles of caustic additive",
        )
        self.caustic_dose_rate = Var(
            self.flowsheet().config.time,
            initialize=0,
            bounds=(0, None),
            domain=NonNegativeReals,
            units=pyunits.kg / pyunits.s,
            doc="Dosage rate of the set of caustic additive",
        )

        # Reaction parameters
        self.Kw_0 = Param(
            mutable=True,
            initialize=60.91,
            domain=NonNegativeReals,
            units=pyunits.mol**2 / pyunits.m**6,
            doc="Water dissociation pre-exponential constant",
        )
        self.dH_w = Param(
            mutable=True,
            initialize=55830,
            domain=NonNegativeReals,
            units=pyunits.J / pyunits.mol,
            doc="Water dissociation enthalpy",
        )
        self.Ka_0 = Param(
            mutable=True,
            initialize=0.000163,
            domain=NonNegativeReals,
            units=pyunits.mol / pyunits.m**3,
            doc="Boron dissociation pre-exponential constant",
        )
        self.dH_a = Param(
            mutable=True,
            initialize=13830,
            domain=NonNegativeReals,
            units=pyunits.J / pyunits.mol,
            doc="Boron dissociation enthalpy",
        )

        # molarity vars (for approximate boron speciation)
        #       Used to establish the mass transfer rates by
        #       first solving a coupled equilibrium system
        #
        #   ENE: [H+] = [OH-] + [A-] + (Alk - n*[base])
        #   MB:  TB = [HA] + [A-]
        #   rw:  Kw = [H+][OH-]
        #   ra:  Ka[HA] = [H+][A-]
        #
        #       Alk = sum(n*Anions) - sum(n*Cations) (from props)
        #       [base] = (Dose/MW)
        #       TB = [HA]_inlet + [A-]_inlet (from props)

        # NOTE: These variables are internal to the unit model
        #   and are used to establish what the mass transfer
        #   constraints need to be in order to achieve a specific
        #   pH and boron speciation at the exit of the unit.
        self.conc_mol_H = Var(
            self.flowsheet().config.time,
            initialize=1e-4,
            bounds=(0, None),
            domain=NonNegativeReals,
            units=pyunits.mol / pyunits.m**3,
            doc="Resulting molarity of protons",
        )
        self.conc_mol_OH = Var(
            self.flowsheet().config.time,
            initialize=1e-4,
            bounds=(0, None),
            domain=NonNegativeReals,
            units=pyunits.mol / pyunits.m**3,
            doc="Resulting molarity of hydroxide",
        )
        self.conc_mol_Boron = Var(
            self.flowsheet().config.time,
            initialize=1e-2,
            bounds=(0, None),
            domain=NonNegativeReals,
            units=pyunits.mol / pyunits.m**3,
            doc="Resulting molarity of Boron",
        )
        self.conc_mol_Borate = Var(
            self.flowsheet().config.time,
            initialize=1e-2,
            bounds=(0, None),
            domain=NonNegativeReals,
            units=pyunits.mol / pyunits.m**3,
            doc="Resulting molarity of Borate",
        )

        # Variables for volume and retention time
        self.reactor_volume = Var(
            initialize=1,
            bounds=(0, None),
            domain=NonNegativeReals,
            units=pyunits.m**3,
            doc="Volume of the reactor",
        )
        self.reactor_retention_time = Var(
            self.flowsheet().config.time,
            initialize=500,
            bounds=(0, None),
            domain=NonNegativeReals,
            units=pyunits.s,
            doc="Hydraulic retention time of the reactor",
        )

        # Build control volume for feed side
        self.control_volume = ControlVolume0DBlock(
            default={
                "dynamic": False,
                "has_holdup": False,
                "property_package": self.config.property_package,
                "property_package_args": self.config.property_package_args,
                "reaction_package": None,
                "reaction_package_args": None,
            })

        self.control_volume.add_state_blocks(has_phase_equilibrium=False)

        self.control_volume.add_material_balances(
            balance_type=self.config.material_balance_type,
            has_mass_transfer=True,
            has_rate_reactions=False,
            has_equilibrium_reactions=False,
        )

        # NOTE: This checks for if an energy_balance_type is defined
        if hasattr(self.config, "energy_balance_type"):
            self.control_volume.add_energy_balances(
                balance_type=self.config.energy_balance_type,
                has_enthalpy_transfer=False,
            )

        self.control_volume.add_momentum_balances(
            balance_type=self.config.momentum_balance_type,
            has_pressure_change=False)

        # Add ports
        self.add_inlet_port(name="inlet", block=self.control_volume)
        self.add_outlet_port(name="outlet", block=self.control_volume)

        # -------- Add constraints ---------
        # Adds isothermal constraint if no energy balance present
        if not hasattr(self.config, "energy_balance_type"):

            @self.Constraint(self.flowsheet().config.time,
                             doc="Isothermal condition")
            def eq_isothermal(self, t):
                return (self.control_volume.properties_out[t].temperature ==
                        self.control_volume.properties_in[t].temperature)

        # Constraints for volume and retention time
        @self.Constraint(
            self.flowsheet().config.time,
            doc="Reactor volume constraint",
        )
        def eq_reactor_volume(self, t):
            Q = pyunits.convert(
                self.control_volume.properties_out[t].flow_vol_phase["Liq"],
                to_units=pyunits.m**3 / pyunits.s,
            )
            return self.reactor_volume == Q * self.reactor_retention_time[t]

        # Constraints for mass transfer terms
        @self.Constraint(
            self.flowsheet().config.time,
            doc="Electroneutrality condition",
        )
        def eq_electroneutrality(self, t):
            ResIons = 0
            for j in self.ion_charge:
                conc = self.control_volume.properties_out[
                    t].conc_mol_phase_comp["Liq", j]
                if (j == self.boron_name_id or j == self.borate_name_id
                        or j == self.proton_name_id
                        or j == self.hydroxide_name_id):
                    ResIons += 0.0
                else:
                    ResIons += -self.ion_charge[j] * conc
            conc_mol_H = pyunits.convert(
                self.conc_mol_H[t],
                to_units=units_meta("amount") * units_meta("length")**-3,
            )
            conc_mol_OH = pyunits.convert(
                self.conc_mol_OH[t],
                to_units=units_meta("amount") * units_meta("length")**-3,
            )
            conc_mol_Borate = pyunits.convert(
                self.conc_mol_Borate[t],
                to_units=units_meta("amount") * units_meta("length")**-3,
            )

            return conc_mol_H == conc_mol_OH + conc_mol_Borate + ResIons

        @self.Constraint(
            self.flowsheet().config.time,
            doc="Total boron balance",
        )
        def eq_total_boron(self, t):
            inlet_Boron = self.control_volume.properties_in[
                t].conc_mol_phase_comp["Liq", self.boron_name_id]
            inlet_Borate = self.control_volume.properties_in[
                t].conc_mol_phase_comp["Liq", self.borate_name_id]
            conc_mol_Borate = pyunits.convert(
                self.conc_mol_Borate[t],
                to_units=units_meta("amount") * units_meta("length")**-3,
            )
            conc_mol_Boron = pyunits.convert(
                self.conc_mol_Boron[t],
                to_units=units_meta("amount") * units_meta("length")**-3,
            )
            return inlet_Boron + inlet_Borate == conc_mol_Borate + conc_mol_Boron

        @self.Constraint(
            self.flowsheet().config.time,
            doc="Water dissociation",
        )
        def eq_water_dissociation(self, t):
            return (self.Kw_0 *
                    exp(-self.dH_w / Constants.gas_constant /
                        self.control_volume.properties_out[t].temperature)
                    ) == self.conc_mol_H[t] * self.conc_mol_OH[t]

        @self.Constraint(
            self.flowsheet().config.time,
            doc="Boron dissociation",
        )
        def eq_boron_dissociation(self, t):
            return (self.Ka_0 *
                    exp(-self.dH_a / Constants.gas_constant /
                        self.control_volume.properties_out[t].temperature)
                    ) * self.conc_mol_Boron[t] == self.conc_mol_H[
                        t] * self.conc_mol_Borate[t]

        # Add constraints for mass transfer terms
        @self.Constraint(
            self.flowsheet().config.time,
            self.config.property_package.phase_list,
            self.config.property_package.component_list,
            doc="Mass transfer term",
        )
        def eq_mass_transfer_term(self, t, p, j):
            map = {
                self.boron_name_id: self.conc_mol_Boron[t],
                self.borate_name_id: self.conc_mol_Borate[t],
                self.proton_name_id: self.conc_mol_H[t],
                self.hydroxide_name_id: self.conc_mol_OH[t],
            }

            if (j == self.boron_name_id or j == self.borate_name_id
                    or j == self.proton_name_id
                    or j == self.hydroxide_name_id):
                c_out = pyunits.convert(
                    map[j],
                    to_units=units_meta("amount") * units_meta("length")**-3,
                )
                input_rate = self.control_volume.properties_in[
                    t].flow_mol_phase_comp[p, j]
                exit_rate = (
                    self.control_volume.properties_out[t].flow_vol_phase[p] *
                    c_out)

                loss_rate = input_rate - exit_rate
                return self.control_volume.mass_transfer_term[t, p,
                                                              j] == -loss_rate

            elif j == self.cation_name_id:
                dose_rate = pyunits.convert(
                    self.caustic_dose_rate[t] / self.caustic_mw *
                    self.additive_molar_ratio,
                    to_units=units_meta("amount") / units_meta("time"),
                )
                return self.control_volume.mass_transfer_term[t, p,
                                                              j] == dose_rate

            else:
                return self.control_volume.mass_transfer_term[t, p, j] == 0.0