Пример #1
0
class ReverseOsmosisData(_ReverseOsmosisBaseData):
    """
    Standard RO Unit Model Class:
    - zero dimensional model
    - steady state only
    - single liquid phase only
    """
    CONFIG = _ReverseOsmosisBaseData.CONFIG()

    def build(self):
        """
        Build the RO model.
        """
        # Call UnitModel.build to setup dynamics
        super().build()

        # for quacking like 1D model -> 0. is "in", 1. is "out"
        self.length_domain = Set(ordered=True,
                                 initialize=(0., 1.))  # inlet/outlet set
        add_object_reference(self, 'difference_elements', self.length_domain)
        self.first_element = self.length_domain.first()

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

        self.feed_side.add_state_blocks(has_phase_equilibrium=False)

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

        self.feed_side.add_energy_balances(
            balance_type=self.config.energy_balance_type,
            has_enthalpy_transfer=True)

        self.feed_side.add_momentum_balances(
            balance_type=self.config.momentum_balance_type,
            has_pressure_change=self.config.has_pressure_change)

        # for quacking like 1D model
        add_object_reference(
            self.feed_side, 'properties', {
                **{(t, 0.): self.feed_side.properties_in[t]
                   for t in self.flowsheet().config.time},
                **{(t, 1.): self.feed_side.properties_out[t]
                   for t in self.flowsheet().config.time}
            })

        # Add additional state blocks
        tmp_dict = dict(**self.config.property_package_args)
        tmp_dict["has_phase_equilibrium"] = False
        tmp_dict["parameters"] = self.config.property_package
        tmp_dict["defined_state"] = False  # these blocks are not inlets
        # Build permeate side
        self.permeate_side = self.config.property_package.state_block_class(
            self.flowsheet().config.time,
            self.length_domain,
            doc="Material properties of permeate along permeate channel",
            default=tmp_dict)
        self.mixed_permeate = self.config.property_package.state_block_class(
            self.flowsheet().config.time,
            doc="Material properties of mixed permeate exiting the module",
            default=tmp_dict)
        # Interface properties
        self.feed_side.properties_interface = self.config.property_package.state_block_class(
            self.flowsheet().config.time,
            self.length_domain,
            doc="Material properties of feed-side membrane interface",
            default=tmp_dict)

        # Add Ports
        self.add_inlet_port(name='inlet', block=self.feed_side)
        self.add_outlet_port(name='retentate', block=self.feed_side)
        self.add_port(name='permeate', block=self.mixed_permeate)

        # References for control volume
        # pressure change
        if (self.config.has_pressure_change
                and self.config.momentum_balance_type != 'none'):
            self.deltaP = Reference(self.feed_side.deltaP)

        self._make_performance()

        self._add_expressions()

    def _make_performance(self):

        units_meta = self.config.property_package.get_metadata(
        ).get_derived_units

        solvent_set = self.config.property_package.solvent_set
        solute_set = self.config.property_package.solute_set

        if self.config.pressure_change_type == PressureChangeType.calculated:
            self.dP_dx = Var(
                self.flowsheet().config.time,
                self.length_domain,
                initialize=-5e4,
                bounds=(-2e5, -1e3),
                domain=NegativeReals,
                units=units_meta('pressure') * units_meta('length')**-1,
                doc=
                "Pressure drop per unit length of feed channel at inlet and outlet"
            )
        elif self.config.pressure_change_type == PressureChangeType.fixed_per_unit_length:
            self.dP_dx = Var(
                self.flowsheet().config.time,
                initialize=-5e4,
                bounds=(-2e5, -1e3),
                domain=NegativeReals,
                units=units_meta('pressure') * units_meta('length')**-1,
                doc="pressure drop per unit length across feed channel")

        if ((self.config.pressure_change_type !=
             PressureChangeType.fixed_per_stage)
                or (self.config.mass_transfer_coefficient
                    == MassTransferCoefficient.calculated)):
            # comes from ControlVolume1D in 1DRO
            self.length = Var(initialize=10,
                              bounds=(0.1, 5e2),
                              domain=NonNegativeReals,
                              units=units_meta('length'),
                              doc='Effective membrane length')
            # not optional in 1DRO
            self.width = Var(initialize=1,
                             bounds=(0.1, 5e2),
                             domain=NonNegativeReals,
                             units=units_meta('length'),
                             doc='Effective feed-channel width')

        if (self.config.mass_transfer_coefficient
                == MassTransferCoefficient.calculated
                or self.config.pressure_change_type
                == PressureChangeType.calculated):
            self.area_cross = Var(initialize=1e-3 * 1 * 0.95,
                                  bounds=(0, 1e3),
                                  domain=NonNegativeReals,
                                  units=units_meta('length')**2,
                                  doc='Cross sectional area')

        super()._make_performance()

        # mass transfer
        def mass_transfer_phase_comp_initialize(b, t, p, j):
            return value(
                self.feed_side.properties_in[t].get_material_flow_terms(
                    'Liq', j) * self.recovery_mass_phase_comp[t, 'Liq', j])

        self.mass_transfer_phase_comp = Var(
            self.flowsheet().config.time,
            self.config.property_package.phase_list,
            self.config.property_package.component_list,
            initialize=mass_transfer_phase_comp_initialize,
            bounds=(1e-8, 1e6),
            domain=NonNegativeReals,
            units=units_meta('mass') * units_meta('time')**-1,
            doc='Mass transfer to permeate')

        # constraints for additional variables (i.e. variables not used in other constraints)

        @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):
            return self.mass_transfer_phase_comp[
                t, p, j] == -self.feed_side.mass_transfer_term[t, p, j]

        # Different expression in 1DRO
        @self.Constraint(self.flowsheet().config.time,
                         self.config.property_package.phase_list,
                         self.config.property_package.component_list,
                         doc="Permeate production")
        def eq_permeate_production(b, t, p, j):
            return (b.mixed_permeate[t].get_material_flow_terms(
                p, j) == b.area * b.flux_mass_phase_comp_avg[t, p, j])

        # Feed and permeate-side connection
        @self.Constraint(self.flowsheet().config.time,
                         self.config.property_package.phase_list,
                         self.config.property_package.component_list,
                         doc="Mass transfer from feed to permeate")
        def eq_connect_mass_transfer(b, t, p, j):
            return (b.mixed_permeate[t].get_material_flow_terms(
                p, j) == -b.feed_side.mass_transfer_term[t, p, j])

        # Non-existent in 1DRO
        @self.Constraint(self.flowsheet().config.time,
                         doc="Enthalpy transfer from feed to permeate")
        def eq_connect_enthalpy_transfer(b, t):
            return (b.mixed_permeate[t].get_enthalpy_flow_terms('Liq') ==
                    -b.feed_side.enthalpy_transfer[t])

        # # Permeate-side stateblocks
        # Not in 1DRO
        @self.Constraint(self.flowsheet().config.time,
                         self.length_domain,
                         solute_set,
                         doc="Permeate mass fraction")
        def eq_mass_frac_permeate(b, t, x, j):
            return (b.permeate_side[t, x].mass_frac_phase_comp['Liq', j] *
                    sum(self.flux_mass_phase_comp[t, x, 'Liq', jj]
                        for jj in self.config.property_package.component_list)
                    == self.flux_mass_phase_comp[t, x, 'Liq', j])

        # not in 1DRO
        @self.Constraint(self.flowsheet().config.time,
                         self.length_domain,
                         doc="Permeate flowrate")
        def eq_flow_vol_permeate(b, t, x):
            return b.permeate_side[t, x].flow_vol_phase[
                'Liq'] == b.mixed_permeate[t].flow_vol_phase['Liq']

        if self.config.pressure_change_type == PressureChangeType.fixed_per_unit_length:
            # Pressure change equation when dP/dx = user-specified constant,
            @self.Constraint(self.flowsheet().config.time,
                             doc="pressure change due to friction")
            def eq_pressure_change(b, t):
                return b.deltaP[t] == b.dP_dx[t] * b.length

        elif self.config.pressure_change_type == PressureChangeType.calculated:

            # Average pressure change per unit length due to friction
            @self.Expression(
                self.flowsheet().config.time,
                doc=
                "expression for average pressure change per unit length due to friction"
            )
            def dP_dx_avg(b, t):
                return 0.5 * sum(b.dP_dx[t, x] for x in b.length_domain)

            # Pressure change equation
            @self.Constraint(self.flowsheet().config.time,
                             doc="pressure change due to friction")
            def eq_pressure_change(b, t):
                return b.deltaP[t] == b.dP_dx_avg[t] * b.length

    def _add_expressions(self):
        super()._add_expressions()

        @self.Expression(self.flowsheet().config.time,
                         doc='Over pressure ratio')
        def over_pressure_ratio(b, t):
            return (b.feed_side.properties_out[t].pressure_osm
                    - b.permeate_side[t,1.].pressure_osm) / \
                    b.feed_side.properties_out[t].pressure

    def initialize_build(blk,
                         initialize_guess=None,
                         state_args=None,
                         outlvl=idaeslog.NOTSET,
                         solver=None,
                         optarg=None,
                         fail_on_warning=False,
                         ignore_dof=False):
        """
        General wrapper for RO initialization routines

        Keyword Arguments:

            initialize_guess : a dict of guesses for solvent_recovery, solute_recovery,
                               and cp_modulus. These guesses offset the initial values
                               for the retentate, permeate, and membrane interface
                               state blocks from the inlet feed
                               (default =
                               {'deltaP': -1e4,
                               'solvent_recovery': 0.5,
                               'solute_recovery': 0.01,
                               'cp_modulus': 1.1})
            state_args : a dict of arguments to be passed to the property
                         package(s) to provide an initial state for the inlet
                         feed side state block (see documentation of the specific
                         property package) (default = None).
            outlvl : sets output level of initialization routine
            optarg : solver options dictionary object (default=None)
            solver : solver object or string indicating which solver to use during
                     initialization, if None provided the default solver will be used
                     (default = None)
            fail_on_warning : boolean argument to fail or only produce  warning upon unsuccessful solve (default=False)
            ignore_dof : boolean argument to ignore when DOF != 0 (default=False)
        Returns:
            None
        """

        init_log = idaeslog.getInitLogger(blk.name, outlvl, tag="unit")
        solve_log = idaeslog.getSolveLogger(blk.name, outlvl, tag="unit")
        # Set solver and options
        opt = get_solver(solver, optarg)

        # ---------------------------------------------------------------------
        # Extract initial state of inlet feed
        source = blk.feed_side.properties_in[
            blk.flowsheet().config.time.first()]
        state_args = blk._get_state_args(source, blk.mixed_permeate[0],
                                         initialize_guess, state_args)

        # Initialize feed inlet state block
        flags_feed_side = blk.feed_side.properties_in.initialize(
            outlvl=outlvl,
            optarg=optarg,
            solver=solver,
            state_args=state_args['feed_side'],
            hold_state=True)

        init_log.info("Initialization Step 1 Complete.")
        if not ignore_dof:
            check_dof(blk, fail_flag=fail_on_warning, logger=init_log)
        # ---------------------------------------------------------------------
        # Initialize other state blocks
        # base properties on inlet state block

        blk.feed_side.properties_out.initialize(
            outlvl=outlvl,
            optarg=optarg,
            solver=solver,
            state_args=state_args['retentate'],
        )
        blk.feed_side.properties_interface.initialize(
            outlvl=outlvl,
            optarg=optarg,
            solver=solver,
            state_args=state_args['interface'],
        )
        blk.mixed_permeate.initialize(
            outlvl=outlvl,
            optarg=optarg,
            solver=solver,
            state_args=state_args['permeate'],
        )
        blk.permeate_side.initialize(
            outlvl=outlvl,
            optarg=optarg,
            solver=solver,
            state_args=state_args['permeate'],
        )
        init_log.info("Initialization Step 2 Complete.")

        # ---------------------------------------------------------------------
        # Solve unit
        with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
            res = opt.solve(blk, tee=slc.tee)
            # occasionally it might be worth retrying a solve
            if not check_optimal_termination(res):
                init_log.warn(
                    "Trouble solving ReverseOsmosis0D unit model, trying one more time"
                )
                res = opt.solve(blk, tee=slc.tee)
        check_solve(res,
                    checkpoint='Initialization Step 3',
                    logger=init_log,
                    fail_flag=fail_on_warning)
        # ---------------------------------------------------------------------
        # Release Inlet state
        blk.feed_side.release_state(flags_feed_side, outlvl)
        init_log.info("Initialization Complete: {}".format(
            idaeslog.condition(res)))

    def calculate_scaling_factors(self):
        # setting scaling factors for variables
        # will not override if the user does provide the scaling factor
        if iscale.get_scaling_factor(self.dens_solvent) is None:
            sf = iscale.get_scaling_factor(
                self.feed_side.properties_in[0].dens_mass_phase['Liq'])
            iscale.set_scaling_factor(self.dens_solvent, sf)

        super().calculate_scaling_factors()

        for (t, p, j), v in self.mass_transfer_phase_comp.items():
            sf = iscale.get_scaling_factor(
                self.feed_side.properties_in[t].get_material_flow_terms(p, j))
            if iscale.get_scaling_factor(v) is None:
                iscale.set_scaling_factor(v, sf)
            v = self.feed_side.mass_transfer_term[t, p, j]
            if iscale.get_scaling_factor(v) is None:
                iscale.set_scaling_factor(v, sf)

        if hasattr(self, 'area_cross'):
            if iscale.get_scaling_factor(self.area_cross) is None:
                iscale.set_scaling_factor(self.area_cross, 100)

        if hasattr(self, 'length'):
            if iscale.get_scaling_factor(self.length) is None:
                iscale.set_scaling_factor(self.length, 1)

        if hasattr(self, 'width'):
            if iscale.get_scaling_factor(self.width) is None:
                iscale.set_scaling_factor(self.width, 1)

        if hasattr(self, 'dP_dx'):
            for v in self.dP_dx.values():
                if iscale.get_scaling_factor(v) is None:
                    iscale.set_scaling_factor(v, 1e-4)
Пример #2
0
class ReverseOsmosis1DData(_ReverseOsmosisBaseData):
    """Standard 1D Reverse Osmosis Unit Model Class."""

    CONFIG = _ReverseOsmosisBaseData.CONFIG()

    CONFIG.declare(
        "area_definition",
        ConfigValue(
            default=DistributedVars.uniform,
            domain=In(DistributedVars),
            description="Argument for defining form of area variable",
            doc="""Argument defining whether area variable should be spatially
    variant or not. **default** - DistributedVars.uniform.
    **Valid values:** {
    DistributedVars.uniform - area does not vary across spatial domain,
    DistributedVars.variant - area can vary over the domain and is indexed
    by time and space.}"""))

    CONFIG.declare(
        "transformation_method",
        ConfigValue(
            default=useDefault,
            description="Discretization method to use for DAE transformation",
            doc="""Discretization method to use for DAE transformation. See Pyomo
    documentation for supported transformations."""))

    CONFIG.declare(
        "transformation_scheme",
        ConfigValue(
            default=useDefault,
            description="Discretization scheme to use for DAE transformation",
            doc="""Discretization scheme to use when transforming domain. See
    Pyomo documentation for supported schemes."""))

    CONFIG.declare(
        "finite_elements",
        ConfigValue(
            default=20,
            domain=int,
            description="Number of finite elements in length domain",
            doc="""Number of finite elements to use when discretizing length 
            domain (default=20)"""))

    CONFIG.declare(
        "collocation_points",
        ConfigValue(
            default=5,
            domain=int,
            description="Number of collocation points per finite element",
            doc="""Number of collocation points to use per finite element when
            discretizing length domain (default=5)"""))

    def _process_config(self):
        if self.config.transformation_method is useDefault:
            _log.warning("Discretization method was "
                         "not specified for the "
                         "reverse osmosis module. "
                         "Defaulting to finite "
                         "difference method.")
            self.config.transformation_method = "dae.finite_difference"

        if self.config.transformation_scheme is useDefault:
            _log.warning("Discretization scheme was "
                         "not specified for the "
                         "reverse osmosis module."
                         "Defaulting to backward finite "
                         "difference.")
            self.config.transformation_scheme = "BACKWARD"

    def build(self):
        """
        Build 1D RO model (pre-DAE transformation).

        Args:
            None

        Returns:
            None
        """
        # Call UnitModel.build to setup dynamics
        super().build()

        # Check configuration errors
        self._process_config()

        # Build 1D Control volume for feed side
        self.feed_side = feed_side = ControlVolume1DBlock(
            default={
                "dynamic": self.config.dynamic,
                "has_holdup": self.config.has_holdup,
                "area_definition": self.config.area_definition,
                "property_package": self.config.property_package,
                "property_package_args": self.config.property_package_args,
                "transformation_method": self.config.transformation_method,
                "transformation_scheme": self.config.transformation_scheme,
                "finite_elements": self.config.finite_elements,
                "collocation_points": self.config.collocation_points
            })

        # Add geometry to feed side
        feed_side.add_geometry()
        # Add state blocks to feed side
        feed_side.add_state_blocks(has_phase_equilibrium=False)
        # Populate feed side
        feed_side.add_material_balances(
            balance_type=self.config.material_balance_type,
            has_mass_transfer=True)
        feed_side.add_momentum_balances(
            balance_type=self.config.momentum_balance_type,
            has_pressure_change=self.config.has_pressure_change)
        # Apply transformation to feed side
        feed_side.apply_transformation()
        add_object_reference(self, 'length_domain',
                             self.feed_side.length_domain)
        self.first_element = self.length_domain.first()
        self.difference_elements = Set(ordered=True,
                                       initialize=(x
                                                   for x in self.length_domain
                                                   if x != self.first_element))

        # Add inlet/outlet ports for feed side
        self.add_inlet_port(name="inlet", block=feed_side)
        self.add_outlet_port(name="retentate", block=feed_side)
        # Make indexed stateblock and separate stateblock for permeate-side and permeate outlet, respectively.
        tmp_dict = dict(**self.config.property_package_args)
        tmp_dict["has_phase_equilibrium"] = False
        tmp_dict["parameters"] = self.config.property_package
        tmp_dict["defined_state"] = False  # these blocks are not inlets
        self.permeate_side = self.config.property_package.state_block_class(
            self.flowsheet().config.time,
            self.length_domain,
            doc="Material properties of permeate along permeate channel",
            default=tmp_dict)
        self.mixed_permeate = self.config.property_package.state_block_class(
            self.flowsheet().config.time,
            doc="Material properties of mixed permeate exiting the module",
            default=tmp_dict)

        # Membrane interface: indexed state block
        self.feed_side.properties_interface = self.config.property_package.state_block_class(
            self.flowsheet().config.time,
            self.length_domain,
            doc="Material properties of feed-side membrane interface",
            default=tmp_dict)

        # Add port to mixed_permeate
        self.add_port(name="permeate", block=self.mixed_permeate)

        # ==========================================================================
        """ Add references to control volume geometry."""
        add_object_reference(self, 'length', feed_side.length)
        add_object_reference(self, 'area_cross', feed_side.area)

        # Add reference to pressure drop for feed side only
        if (self.config.has_pressure_change is True and
                self.config.momentum_balance_type != MomentumBalanceType.none):
            add_object_reference(self, 'dP_dx', feed_side.deltaP)

        self._make_performance()

        self._add_expressions()

    def _make_performance(self):
        """
        Variables and constraints for unit model.

        Args:
            None

        Returns:
            None
        """

        solvent_set = self.config.property_package.solvent_set
        solute_set = self.config.property_package.solute_set

        # Units
        units_meta = \
            self.config.property_package.get_metadata().get_derived_units

        # ==========================================================================

        self.width = Var(initialize=1,
                         bounds=(1e-1, 1e3),
                         domain=NonNegativeReals,
                         units=units_meta('length'),
                         doc='Membrane width')

        super()._make_performance()

        # mass transfer
        def mass_transfer_phase_comp_initialize(b, t, x, p, j):
            return value(
                self.feed_side.properties[t, x].get_material_flow_terms(
                    'Liq', j) * self.recovery_mass_phase_comp[t, 'Liq', j])

        self.mass_transfer_phase_comp = Var(
            self.flowsheet().config.time,
            self.length_domain,
            self.config.property_package.phase_list,
            self.config.property_package.component_list,
            initialize=mass_transfer_phase_comp_initialize,
            bounds=(1e-8, 1e6),
            domain=NonNegativeReals,
            units=units_meta('mass') * units_meta('time')**-1 *
            units_meta('length')**-1,
            doc='Mass transfer to permeate')

        if self.config.has_pressure_change:
            self.deltaP = Var(self.flowsheet().config.time,
                              initialize=-1e5,
                              bounds=(-1e6, 0),
                              domain=NegativeReals,
                              units=units_meta('pressure'),
                              doc='Pressure drop across unit')

        # ==========================================================================
        # Mass transfer term equation

        @self.Constraint(self.flowsheet().config.time,
                         self.difference_elements,
                         self.config.property_package.phase_list,
                         self.config.property_package.component_list,
                         doc="Mass transfer term")
        def eq_mass_transfer_term(b, t, x, p, j):
            return b.mass_transfer_phase_comp[
                t, x, p, j] == -b.feed_side.mass_transfer_term[t, x, p, j]

        # ==========================================================================
        # Mass flux = feed mass transfer equation

        @self.Constraint(self.flowsheet().config.time,
                         self.difference_elements,
                         self.config.property_package.phase_list,
                         self.config.property_package.component_list,
                         doc="Mass transfer term")
        def eq_mass_flux_equal_mass_transfer(b, t, x, p, j):
            return b.flux_mass_phase_comp[
                t, x, p,
                j] * b.width == -b.feed_side.mass_transfer_term[t, x, p, j]

        # ==========================================================================
        # Mass flux equations (Jw and Js)

        # ==========================================================================
        # Final permeate mass flow rate (of solvent and solute) --> Mp,j, final = sum(Mp,j)

        @self.Constraint(self.flowsheet().config.time,
                         self.config.property_package.phase_list,
                         self.config.property_package.component_list,
                         doc="Permeate mass flow rates exiting unit")
        def eq_permeate_production(b, t, p, j):
            return (b.mixed_permeate[t].get_material_flow_terms(p, j) == sum(
                b.permeate_side[t, x].get_material_flow_terms(p, j)
                for x in b.difference_elements))

        # ==========================================================================
        # Feed and permeate-side mass transfer connection --> Mp,j = Mf,transfer = Jj * W * L/n

        @self.Constraint(self.flowsheet().config.time,
                         self.difference_elements,
                         self.config.property_package.phase_list,
                         self.config.property_package.component_list,
                         doc="Mass transfer from feed to permeate")
        def eq_connect_mass_transfer(b, t, x, p, j):
            return (b.permeate_side[t, x].get_material_flow_terms(
                p, j) == -b.feed_side.mass_transfer_term[t, x, p, j] *
                    b.length / b.nfe)

        ## ==========================================================================
        # Pressure drop
        if (self.config.pressure_change_type
                == PressureChangeType.fixed_per_unit_length
                or self.config.pressure_change_type
                == PressureChangeType.calculated):

            @self.Constraint(self.flowsheet().config.time,
                             doc='Pressure drop across unit')
            def eq_pressure_drop(b, t):
                return (b.deltaP[t] == sum(b.dP_dx[t, x] * b.length / b.nfe
                                           for x in b.difference_elements))

        if (self.config.pressure_change_type
                == PressureChangeType.fixed_per_stage
                and self.config.has_pressure_change):

            @self.Constraint(self.flowsheet().config.time,
                             self.length_domain,
                             doc='Fixed pressure drop across unit')
            def eq_pressure_drop(b, t, x):
                return b.deltaP[t] == b.length * b.dP_dx[t, x]

        ## ==========================================================================
        # Feed-side isothermal conditions
        # NOTE: this could go on the feed_side block, but that seems to hurt initialization
        #       in the tests for this unit
        @self.Constraint(self.flowsheet().config.time,
                         self.difference_elements,
                         doc="Isothermal assumption for feed channel")
        def eq_feed_isothermal(b, t, x):
            return b.feed_side.properties[t, b.first_element].temperature == \
                   b.feed_side.properties[t, x].temperature

    def initialize_build(blk,
                         initialize_guess=None,
                         state_args=None,
                         outlvl=idaeslog.NOTSET,
                         solver=None,
                         optarg=None,
                         fail_on_warning=False,
                         ignore_dof=False):
        """
        Initialization routine for 1D-RO unit.

        Keyword Arguments:
            initialize_guess : a dict of guesses for solvent_recovery, solute_recovery,
                               and cp_modulus. These guesses offset the initial values
                               for the retentate, permeate, and membrane interface
                               state blocks from the inlet feed
                               (default =
                               {'deltaP': -1e4,
                               'solvent_recovery': 0.5,
                               'solute_recovery': 0.01,
                               'cp_modulus': 1.1})
            state_args : a dict of arguments to be passed to the property
                         package(s) to provide an initial state for the inlet
                         feed side state block (see documentation of the specific
                         property package) (default = None).
            outlvl : sets output level of initialization routine
            solver : str indicating which solver to use during
                     initialization (default = None, use default solver)
            optarg : solver options dictionary object (default=None, use default solver options)
            fail_on_warning : boolean argument to fail or only produce  warning upon unsuccessful solve (default=False)
            ignore_dof : boolean argument to ignore when DOF != 0 (default=False)
        Returns:
            None

        """

        init_log = idaeslog.getInitLogger(blk.name, outlvl, tag="unit")
        solve_log = idaeslog.getSolveLogger(blk.name, outlvl, tag="unit")

        # Create solver
        opt = get_solver(solver, optarg)

        source = blk.feed_side.properties[blk.flowsheet().config.time.first(),
                                          blk.first_element]
        state_args = blk._get_state_args(source, blk.mixed_permeate[0],
                                         initialize_guess, state_args)

        # ---------------------------------------------------------------------
        # Step 1: Initialize feed_side, permeate_side, and mixed_permeate blocks
        flags_feed_side = blk.feed_side.initialize(
            outlvl=outlvl,
            optarg=optarg,
            solver=solver,
            state_args=state_args['feed_side'],
            hold_state=True)

        init_log.info("Initialization Step 1 Complete")
        if not ignore_dof:
            check_dof(blk, fail_flag=fail_on_warning, logger=init_log)
        # ---------------------------------------------------------------------
        # Initialize other state blocks
        # base properties on inlet state block

        flag_feed_side_properties_interface = blk.feed_side.properties_interface.initialize(
            outlvl=outlvl,
            optarg=optarg,
            solver=solver,
            state_args=state_args['interface'])
        flags_permeate_side = blk.permeate_side.initialize(
            outlvl=outlvl,
            optarg=optarg,
            solver=solver,
            state_args=state_args['permeate'])
        flags_mixed_permeate = blk.mixed_permeate.initialize(
            outlvl=outlvl,
            optarg=optarg,
            solver=solver,
            state_args=state_args['permeate'])
        init_log.info("Initialization Step 2 Complete.")

        # ---------------------------------------------------------------------
        # Solve unit
        with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
            res = opt.solve(blk, tee=slc.tee)
            # occasionally it might be worth retrying a solve
            if not check_optimal_termination(res):
                init_log.warn(
                    "Trouble solving ReverseOsmosis1D unit model, trying one more time"
                )
                res = opt.solve(blk, tee=slc.tee)
        check_solve(res,
                    logger=init_log,
                    fail_flag=fail_on_warning,
                    checkpoint='Initialization Step 3')
        # ---------------------------------------------------------------------
        # Release Inlet state
        blk.feed_side.release_state(flags_feed_side, outlvl)
        init_log.info("Initialization Complete: {}".format(
            idaeslog.condition(res)))

    def calculate_scaling_factors(self):
        if iscale.get_scaling_factor(self.dens_solvent) is None:
            sf = iscale.get_scaling_factor(
                self.feed_side.properties[0, 0].dens_mass_phase['Liq'])
            iscale.set_scaling_factor(self.dens_solvent, sf)

        super().calculate_scaling_factors()

        # these variables should have user input, if not there will be a warning
        if iscale.get_scaling_factor(self.width) is None:
            sf = iscale.get_scaling_factor(self.width, default=1, warning=True)
            iscale.set_scaling_factor(self.width, sf)

        if iscale.get_scaling_factor(self.length) is None:
            sf = iscale.get_scaling_factor(self.length,
                                           default=10,
                                           warning=True)
            iscale.set_scaling_factor(self.length, sf)

        # setting scaling factors for variables

        # will not override if the user provides the scaling factor
        ## default of 1 set by ControlVolume1D
        if iscale.get_scaling_factor(self.area_cross) == 1:
            iscale.set_scaling_factor(self.area_cross, 100)

        for (t, x, p, j), v in self.mass_transfer_phase_comp.items():
            sf = (iscale.get_scaling_factor(
                self.feed_side.properties[t, x].get_material_flow_terms(p, j))
                  / iscale.get_scaling_factor(self.feed_side.length)) * value(
                      self.nfe)
            if iscale.get_scaling_factor(v) is None:
                iscale.set_scaling_factor(v, sf)
            v = self.feed_side.mass_transfer_term[t, x, p, j]
            if iscale.get_scaling_factor(v) is None:
                iscale.set_scaling_factor(v, sf)

        if hasattr(self, 'deltaP'):
            for v in self.deltaP.values():
                if iscale.get_scaling_factor(v) is None:
                    iscale.set_scaling_factor(v, 1e-4)

        if hasattr(self, 'dP_dx'):
            for v in self.feed_side.pressure_dx.values():
                iscale.set_scaling_factor(v, 1e-5)
        else:
            for v in self.feed_side.pressure_dx.values():
                iscale.set_scaling_factor(v, 1e5)
Пример #3
0
class PressureChangerData(UnitModelBlockData):
    """
    Standard Compressor/Expander Unit Model Class
    """

    CONFIG = UnitModelBlockData.CONFIG()

    CONFIG.declare(
        "material_balance_type",
        ConfigValue(
            default=MaterialBalanceType.useDefault,
            domain=In(MaterialBalanceType),
            description="Material balance construction flag",
            doc="""Indicates what type of mass balance should be constructed,
**default** - MaterialBalanceType.useDefault.
**Valid values:** {
**MaterialBalanceType.useDefault - refer to property package for default
balance type
**MaterialBalanceType.none** - exclude material balances,
**MaterialBalanceType.componentPhase** - use phase component balances,
**MaterialBalanceType.componentTotal** - use total component balances,
**MaterialBalanceType.elementTotal** - use total element balances,
**MaterialBalanceType.total** - use total material balance.}""",
        ),
    )
    CONFIG.declare(
        "energy_balance_type",
        ConfigValue(
            default=EnergyBalanceType.useDefault,
            domain=In(EnergyBalanceType),
            description="Energy balance construction flag",
            doc="""Indicates what type of energy balance should be constructed,
**default** - EnergyBalanceType.useDefault.
**Valid values:** {
**EnergyBalanceType.useDefault - refer to property package for default
balance type
**EnergyBalanceType.none** - exclude energy balances,
**EnergyBalanceType.enthalpyTotal** - single enthalpy balance for material,
**EnergyBalanceType.enthalpyPhase** - enthalpy balances for each phase,
**EnergyBalanceType.energyTotal** - single energy balance for material,
**EnergyBalanceType.energyPhase** - energy balances for each phase.}""",
        ),
    )
    CONFIG.declare(
        "momentum_balance_type",
        ConfigValue(
            default=MomentumBalanceType.pressureTotal,
            domain=In(MomentumBalanceType),
            description="Momentum balance construction flag",
            doc="""Indicates what type of momentum balance should be
constructed, **default** - MomentumBalanceType.pressureTotal.
**Valid values:** {
**MomentumBalanceType.none** - exclude momentum balances,
**MomentumBalanceType.pressureTotal** - single pressure balance for material,
**MomentumBalanceType.pressurePhase** - pressure balances for each phase,
**MomentumBalanceType.momentumTotal** - single momentum balance for material,
**MomentumBalanceType.momentumPhase** - momentum balances for each phase.}""",
        ),
    )
    CONFIG.declare(
        "has_phase_equilibrium",
        ConfigValue(
            default=False,
            domain=In([True, False]),
            description="Phase equilibrium construction flag",
            doc="""Indicates whether terms for phase equilibrium should be
constructed, **default** = False.
**Valid values:** {
**True** - include phase equilibrium terms
**False** - exclude phase equilibrium terms.}""",
        ),
    )
    CONFIG.declare(
        "compressor",
        ConfigValue(
            default=True,
            domain=In([True, False]),
            description="Compressor flag",
            doc="""Indicates whether this unit should be considered a
            compressor (True (default), pressure increase) or an expander
            (False, pressure decrease).""",
        ),
    )
    CONFIG.declare(
        "thermodynamic_assumption",
        ConfigValue(
            default=ThermodynamicAssumption.isothermal,
            domain=In(ThermodynamicAssumption),
            description="Thermodynamic assumption to use",
            doc="""Flag to set the thermodynamic assumption to use for the unit.
                - ThermodynamicAssumption.isothermal (default)
                - ThermodynamicAssumption.isentropic
                - ThermodynamicAssumption.pump
                - ThermodynamicAssumption.adiabatic""",
        ),
    )
    CONFIG.declare(
        "property_package",
        ConfigValue(
            default=useDefault,
            domain=is_physical_parameter_block,
            description="Property package to use for control volume",
            doc="""Property parameter object used to define property
calculations, **default** - useDefault.
**Valid values:** {
**useDefault** - use default package from parent model or flowsheet,
**PropertyParameterObject** - a PropertyParameterBlock object.}""",
        ),
    )
    CONFIG.declare(
        "property_package_args",
        ConfigBlock(
            implicit=True,
            description="Arguments to use for constructing property packages",
            doc="""A ConfigBlock with arguments to be passed to a property
block(s) and used when constructing these,
**default** - None.
**Valid values:** {
see property package for documentation.}""",
        ),
    )
    CONFIG.declare(
        "support_isentropic_performance_curves",
        ConfigValue(
            default=False,
            domain=In([True, False]),
            doc="Include a block for performance curves, configure via"
                " isentropic_performance_curves.",
        ),
    )
    CONFIG.declare(
        "isentropic_performance_curves",
        IsentropicPerformanceCurveData.CONFIG(),
        # doc included in IsentropicPerformanceCurveData
    )

    def build(self):
        """

        Args:
            None

        Returns:
            None
        """
        # Call UnitModel.build
        super().build()

        # Add a control volume to the unit including setting up dynamics.
        self.control_volume = ControlVolume0DBlock(
            default={
                "dynamic": self.config.dynamic,
                "has_holdup": self.config.has_holdup,
                "property_package": self.config.property_package,
                "property_package_args": self.config.property_package_args,
            }
        )

        # Add geomerty variables to control volume
        if self.config.has_holdup:
            self.control_volume.add_geometry()

        # Add inlet and outlet state blocks to control volume
        self.control_volume.add_state_blocks(
            has_phase_equilibrium=self.config.has_phase_equilibrium
        )

        # Add mass balance
        # Set has_equilibrium is False for now
        # TO DO; set has_equilibrium to True
        self.control_volume.add_material_balances(
            balance_type=self.config.material_balance_type,
            has_phase_equilibrium=self.config.has_phase_equilibrium,
        )

        # Add energy balance
        self.control_volume.add_energy_balances(
            balance_type=self.config.energy_balance_type,
            has_work_transfer=True
        )

        # add momentum balance
        self.control_volume.add_momentum_balances(
            balance_type=self.config.momentum_balance_type,
            has_pressure_change=True
        )

        # Add Ports
        self.add_inlet_port()
        self.add_outlet_port()

        # Set Unit Geometry and holdup Volume
        if self.config.has_holdup is True:
            self.volume = Reference(self.control_volume.volume[:])

        # Construct performance equations
        # Set references to balance terms at unit level
        # Add Work transfer variable 'work'
        self.work_mechanical = Reference(self.control_volume.work[:])

        # Add Momentum balance variable 'deltaP'
        self.deltaP = Reference(self.control_volume.deltaP[:])

        # Performance Variables
        self.ratioP = Var(
            self.flowsheet().config.time, initialize=1.0, doc="Pressure Ratio"
        )

        # Pressure Ratio
        @self.Constraint(self.flowsheet().config.time,
                         doc="Pressure ratio constraint")
        def ratioP_calculation(b, t):
            return (
                b.ratioP[t] * b.control_volume.properties_in[t].pressure
                == b.control_volume.properties_out[t].pressure
            )

        # Construct equations for thermodynamic assumption
        if (self.config.thermodynamic_assumption ==
                ThermodynamicAssumption.isothermal):
            self.add_isothermal()
        elif (self.config.thermodynamic_assumption ==
              ThermodynamicAssumption.isentropic):
            self.add_isentropic()
        elif (self.config.thermodynamic_assumption ==
              ThermodynamicAssumption.pump):
            self.add_pump()
        elif (self.config.thermodynamic_assumption ==
              ThermodynamicAssumption.adiabatic):
            self.add_adiabatic()

    def add_pump(self):
        """
        Add constraints for the incompressible fluid assumption

        Args:
            None

        Returns:
            None
        """
        units_meta = self.config.property_package.get_metadata()

        self.work_fluid = Var(
            self.flowsheet().config.time,
            initialize=1.0,
            doc="Work required to increase the pressure of the liquid",
            units=units_meta.get_derived_units("power")
        )
        self.efficiency_pump = Var(
            self.flowsheet().config.time, initialize=1.0, doc="Pump efficiency"
        )

        @self.Constraint(self.flowsheet().config.time,
                         doc="Pump fluid work constraint")
        def fluid_work_calculation(b, t):
            return b.work_fluid[t] == (
                (
                    b.control_volume.properties_out[t].pressure
                    - b.control_volume.properties_in[t].pressure
                )
                * b.control_volume.properties_out[t].flow_vol
            )

        # Actual work
        @self.Constraint(
            self.flowsheet().config.time,
            doc="Actual mechanical work calculation"
        )
        def actual_work(b, t):
            if b.config.compressor:
                return b.work_fluid[t] == (
                    b.work_mechanical[t] * b.efficiency_pump[t]
                )
            else:
                return b.work_mechanical[t] == (
                    b.work_fluid[t] * b.efficiency_pump[t]
                )

    def add_isothermal(self):
        """
        Add constraints for isothermal assumption.

        Args:
            None

        Returns:
            None
        """
        # Isothermal constraint
        @self.Constraint(
            self.flowsheet().config.time,
            doc="For isothermal condition: Equate inlet and "
            "outlet temperature",
        )
        def isothermal(b, t):
            return (
                b.control_volume.properties_in[t].temperature
                == b.control_volume.properties_out[t].temperature
            )

    def add_adiabatic(self):
        """
        Add constraints for adiabatic assumption.

        Args:
            None

        Returns:
            None
        """
        @self.Constraint(self.flowsheet().config.time)
        def zero_work_equation(b, t):
            return self.control_volume.work[t] == 0

    def add_isentropic(self):
        """
        Add constraints for isentropic assumption.

        Args:
            None

        Returns:
            None
        """
        units_meta = self.config.property_package.get_metadata()

        # Get indexing sets from control volume
        # Add isentropic variables
        self.efficiency_isentropic = Var(
            self.flowsheet().config.time,
            initialize=0.8,
            doc="Efficiency with respect to an isentropic process [-]",
        )
        self.work_isentropic = Var(
            self.flowsheet().config.time,
            initialize=0.0,
            doc="Work input to unit if isentropic process",
            units=units_meta.get_derived_units("power")
        )

        # Build isentropic state block
        tmp_dict = dict(**self.config.property_package_args)
        tmp_dict["has_phase_equilibrium"] = self.config.has_phase_equilibrium
        tmp_dict["defined_state"] = False

        self.properties_isentropic = (
            self.config.property_package.build_state_block(
                self.flowsheet().config.time,
                doc="isentropic properties at outlet",
                default=tmp_dict)
        )

        # Connect isentropic state block properties
        @self.Constraint(
            self.flowsheet().config.time,
            doc="Pressure for isentropic calculations"
        )
        def isentropic_pressure(b, t):
            return (
                b.properties_isentropic[t].pressure
                == b.control_volume.properties_out[t].pressure
            )

        # This assumes isentropic composition is the same as outlet
        self.add_state_material_balances(self.config.material_balance_type,
                                         self.properties_isentropic,
                                         self.control_volume.properties_out)

        # This assumes isentropic entropy is the same as inlet
        @self.Constraint(self.flowsheet().config.time,
                         doc="Isentropic assumption")
        def isentropic(b, t):
            return (
                b.properties_isentropic[t].entr_mol
                == b.control_volume.properties_in[t].entr_mol
            )

        # Isentropic work
        @self.Constraint(
            self.flowsheet().config.time,
            doc="Calculate work of isentropic process"
        )
        def isentropic_energy_balance(b, t):
            return b.work_isentropic[t] == (
                sum(
                    b.properties_isentropic[t].get_enthalpy_flow_terms(p)
                    for p in b.properties_isentropic.phase_list
                )
                - sum(
                    b.control_volume.properties_in[
                        t].get_enthalpy_flow_terms(p)
                    for p in b.control_volume.properties_in.phase_list
                )
            )

        # Actual work
        @self.Constraint(
            self.flowsheet().config.time,
            doc="Actual mechanical work calculation"
        )
        def actual_work(b, t):
            if b.config.compressor:
                return b.work_isentropic[t] == (
                    b.work_mechanical[t] * b.efficiency_isentropic[t]
                )
            else:
                return b.work_mechanical[t] == (
                    b.work_isentropic[t] * b.efficiency_isentropic[t]
                )

        if self.config.support_isentropic_performance_curves:
            self.performance_curve = IsentropicPerformanceCurve(
                default=self.config.isentropic_performance_curves)

    def model_check(blk):
        """
        Check that pressure change matches with compressor argument (i.e. if
        compressor = True, pressure should increase or work should be positive)

        Args:
            None

        Returns:
            None
        """
        if blk.config.compressor:
            # Compressor
            # Check that pressure does not decrease
            if any(
                blk.deltaP[t].fixed and (value(blk.deltaP[t]) < 0.0)
                for t in blk.flowsheet().config.time
            ):
                _log.warning("{} Compressor set with negative deltaP."
                             .format(blk.name))
            if any(
                blk.ratioP[t].fixed and (value(blk.ratioP[t]) < 1.0)
                for t in blk.flowsheet().config.time
            ):
                _log.warning(
                    "{} Compressor set with ratioP less than 1."
                    .format(blk.name)
                )
            if any(
                blk.control_volume.properties_out[t].pressure.fixed
                and (
                    value(blk.control_volume.properties_in[t].pressure)
                    > value(blk.control_volume.properties_out[t].pressure)
                )
                for t in blk.flowsheet().config.time
            ):
                _log.warning(
                    "{} Compressor set with pressure decrease."
                    .format(blk.name)
                )
            # Check that work is not negative
            if any(
                blk.work_mechanical[t].fixed and (
                    value(blk.work_mechanical[t]) < 0.0)
                for t in blk.flowsheet().config.time
            ):
                _log.warning(
                    "{} Compressor maybe set with negative work."
                    .format(blk.name)
                )
        else:
            # Expander
            # Check that pressure does not increase
            if any(
                blk.deltaP[t].fixed and (value(blk.deltaP[t]) > 0.0)
                for t in blk.flowsheet().config.time
            ):
                _log.warning(
                    "{} Expander/turbine set with positive deltaP."
                    .format(blk.name)
                )
            if any(
                blk.ratioP[t].fixed and (value(blk.ratioP[t]) > 1.0)
                for t in blk.flowsheet().config.time
            ):
                _log.warning(
                    "{} Expander/turbine set with ratioP greater "
                    "than 1.".format(blk.name)
                )
            if any(
                blk.control_volume.properties_out[t].pressure.fixed
                and (
                    value(blk.control_volume.properties_in[t].pressure)
                    < value(blk.control_volume.properties_out[t].pressure)
                )
                for t in blk.flowsheet().config.time
            ):
                _log.warning(
                    "{} Expander/turbine maybe set with pressure ",
                    "increase.".format(blk.name),
                )
            # Check that work is not positive
            if any(
                blk.work_mechanical[t].fixed and (
                    value(blk.work_mechanical[t]) > 0.0)
                for t in blk.flowsheet().config.time
            ):
                _log.warning(
                    "{} Expander/turbine set with positive work."
                    .format(blk.name)
                )

        # Run holdup block model checks
        blk.control_volume.model_check()

        # Run model checks on isentropic property block
        try:
            for t in blk.flowsheet().config.time:
                blk.properties_in[t].model_check()
        except AttributeError:
            pass

    def initialize(
        blk,
        state_args=None,
        routine=None,
        outlvl=idaeslog.NOTSET,
        solver=None,
        optarg=None,
    ):
        """
        General wrapper for pressure changer initialization routines

        Keyword Arguments:
            routine : str stating which initialization routine to execute
                        * None - use routine matching thermodynamic_assumption
                        * 'isentropic' - use isentropic initialization routine
                        * 'isothermal' - use isothermal initialization routine
            state_args : a dict of arguments to be passed to the property
                         package(s) to provide an initial state for
                         initialization (see documentation of the specific
                         property package) (default = {}).
            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)

        Returns:
            None
        """
        # if costing block exists, deactivate
        try:
            blk.costing.deactivate()
        except AttributeError:
            pass

        if routine is None:
            # Use routine for specific type of unit
            routine = blk.config.thermodynamic_assumption

        # Call initialization routine
        if routine is ThermodynamicAssumption.isentropic:
            blk.init_isentropic(
                state_args=state_args,
                outlvl=outlvl,
                solver=solver,
                optarg=optarg
            )
        elif routine is ThermodynamicAssumption.adiabatic:
            blk.init_adiabatic(
                state_args=state_args,
                outlvl=outlvl,
                solver=solver,
                optarg=optarg
            )
        else:
            # Call the general initialization routine in UnitModelBlockData
            super().initialize(
                state_args=state_args,
                outlvl=outlvl,
                solver=solver,
                optarg=optarg
            )
        # if costing block exists, activate
        try:
            blk.costing.activate()
            costing.initialize(blk.costing)
        except AttributeError:
            pass

    def init_adiabatic(blk, state_args, outlvl, solver, optarg):
        """
        Initialization routine for adiabatic pressure changers.

        Keyword Arguments:
            state_args : a dict of arguments to be passed to the property
                         package(s) to provide an initial state for
                         initialization (see documentation of the specific
                         property package) (default = {}).
            outlvl : sets output level of initialization routine
            optarg : solver options dictionary object (default={})
            solver : str indicating which solver to use during
                     initialization (default = None)

        Returns:
            None
        """
        init_log = idaeslog.getInitLogger(blk.name, outlvl, tag="unit")
        solve_log = idaeslog.getSolveLogger(blk.name, outlvl, tag="unit")

        # Create solver
        opt = get_solver(solver, optarg)

        cv = blk.control_volume
        t0 = blk.flowsheet().config.time.first()
        state_args_out = {}

        if state_args is None:
            state_args = {}
            state_dict = (
                cv.properties_in[t0].define_port_members())

            for k in state_dict.keys():
                if state_dict[k].is_indexed():
                    state_args[k] = {}
                    for m in state_dict[k].keys():
                        state_args[k][m] = state_dict[k][m].value
                else:
                    state_args[k] = state_dict[k].value

        # Get initialisation guesses for outlet and isentropic states
        for k in state_args:
            if k == "pressure" and k not in state_args_out:
                # Work out how to estimate outlet pressure
                if cv.properties_out[t0].pressure.fixed:
                    # Fixed outlet pressure, use this value
                    state_args_out[k] = value(
                        cv.properties_out[t0].pressure)
                elif blk.deltaP[t0].fixed:
                    state_args_out[k] = value(
                        state_args[k] + blk.deltaP[t0])
                elif blk.ratioP[t0].fixed:
                    state_args_out[k] = value(
                        state_args[k] * blk.ratioP[t0])
                else:
                    # Not obvious what to do, use inlet state
                    state_args_out[k] = state_args[k]
            elif k not in state_args_out:
                state_args_out[k] = state_args[k]

        # Initialize state blocks
        flags = cv.properties_in.initialize(
            outlvl=outlvl,
            optarg=optarg,
            solver=solver,
            hold_state=True,
            state_args=state_args,
        )
        cv.properties_out.initialize(
            outlvl=outlvl,
            optarg=optarg,
            solver=solver,
            hold_state=False,
            state_args=state_args_out,
        )
        init_log.info_high("Initialization Step 1 Complete.")

        with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
            res = opt.solve(blk, tee=slc.tee)
        init_log.info_high("Initialization Step 2 {}."
                           .format(idaeslog.condition(res)))

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

        # ---------------------------------------------------------------------
        # Release Inlet state
        blk.control_volume.release_state(flags, outlvl)
        init_log.info(f"Initialization Complete: {idaeslog.condition(res)}")

    def init_isentropic(blk, state_args, outlvl, solver, optarg):
        """
        Initialization routine for isentropic pressure changers.

        Keyword Arguments:
            state_args : a dict of arguments to be passed to the property
                         package(s) to provide an initial state for
                         initialization (see documentation of the specific
                         property package) (default = {}).
            outlvl : sets output level of initialization routine
            optarg : solver options dictionary object (default={})
            solver : str indicating which solver to use during
                     initialization (default = None)

        Returns:
            None
        """
        init_log = idaeslog.getInitLogger(blk.name, outlvl, tag="unit")
        solve_log = idaeslog.getSolveLogger(blk.name, outlvl, tag="unit")

        # Create solver
        opt = get_solver(solver, optarg)

        cv = blk.control_volume
        t0 = blk.flowsheet().config.time.first()
        state_args_out = {}

        # performance curves exist and are active so initialize with them
        activate_performance_curves = (
            hasattr(blk, "performance_curve") and
            blk.performance_curve.has_constraints() and
            blk.performance_curve.active)
        if activate_performance_curves:
            blk.performance_curve.deactivate()
            # The performance curves will provide (maybe indirectly) efficency
            # and/or pressure ratio. To get through the standard isentropic
            # pressure changer init, we'll see if the user provided a guess for
            # pressure ratio or isentropic efficency and fix them if need. If
            # not fixed and no guess provided, fill in something reasonable
            # until the performance curves are turned on.
            unfix_eff = {}
            unfix_ratioP = {}
            for t in blk.flowsheet().config.time:
                if not (blk.ratioP[t].fixed or  blk.deltaP[t].fixed or
                    cv.properties_out[t].pressure.fixed):
                    if blk.config.compressor:
                        if not (value(blk.ratioP[t]) >= 1.01 and
                            value(blk.ratioP[t]) <= 50):
                            blk.ratioP[t] = 1.8
                    else:
                        if not (value(blk.ratioP[t]) >= 0.01 and
                            value(blk.ratioP[t]) <= 0.999):
                            blk.ratioP[t] = 0.7
                    blk.ratioP[t].fix()
                    unfix_ratioP[t] = True
                if not blk.efficiency_isentropic[t].fixed:
                    if not (value(blk.efficiency_isentropic[t]) >= 0.05 and
                        value(blk.efficiency_isentropic[t]) <= 1.0):
                        blk.efficiency_isentropic[t] = 0.8
                    blk.efficiency_isentropic[t].fix()
                    unfix_eff[t] = True

        if state_args is None:
            state_args = {}
            state_dict = (
                cv.properties_in[t0].define_port_members())

            for k in state_dict.keys():
                if state_dict[k].is_indexed():
                    state_args[k] = {}
                    for m in state_dict[k].keys():
                        state_args[k][m] = state_dict[k][m].value
                else:
                    state_args[k] = state_dict[k].value

        # Get initialisation guesses for outlet and isentropic states
        for k in state_args:
            if k == "pressure" and k not in state_args_out:
                # Work out how to estimate outlet pressure
                if cv.properties_out[t0].pressure.fixed:
                    # Fixed outlet pressure, use this value
                    state_args_out[k] = value(
                        cv.properties_out[t0].pressure)
                elif blk.deltaP[t0].fixed:
                    state_args_out[k] = value(
                        state_args[k] + blk.deltaP[t0])
                elif blk.ratioP[t0].fixed:
                    state_args_out[k] = value(
                        state_args[k] * blk.ratioP[t0])
                else:
                    # Not obvious what to do, use inlet state
                    state_args_out[k] = state_args[k]
            elif k not in state_args_out:
                state_args_out[k] = state_args[k]

        # Initialize state blocks
        flags = cv.properties_in.initialize(
            outlvl=outlvl,
            optarg=optarg,
            solver=solver,
            hold_state=True,
            state_args=state_args,
        )
        cv.properties_out.initialize(
            outlvl=outlvl,
            optarg=optarg,
            solver=solver,
            hold_state=False,
            state_args=state_args_out,
        )

        init_log.info_high("Initialization Step 1 Complete.")
        # ---------------------------------------------------------------------
        # Initialize Isentropic block

        blk.properties_isentropic.initialize(
            outlvl=outlvl,
            optarg=optarg,
            solver=solver,
            state_args=state_args_out,
        )

        init_log.info_high("Initialization Step 2 Complete.")

        # ---------------------------------------------------------------------
        # Solve for isothermal conditions
        if isinstance(
            blk.properties_isentropic[
                blk.flowsheet().config.time.first()].temperature,
            Var,
        ):
            blk.properties_isentropic[:].temperature.fix()
        elif isinstance(
            blk.properties_isentropic[
                blk.flowsheet().config.time.first()].enth_mol,
            Var,
        ):
            blk.properties_isentropic[:].enth_mol.fix()
        elif isinstance(
            blk.properties_isentropic[
                blk.flowsheet().config.time.first()].temperature,
            Expression,
        ):
            def tmp_rule(b, t):
                return blk.properties_isentropic[t].temperature == \
                    blk.control_volume.properties_in[t].temperature
            blk.tmp_init_constraint = Constraint(
                blk.flowsheet().config.time, rule=tmp_rule)

        blk.isentropic.deactivate()

        with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
            res = opt.solve(blk, tee=slc.tee)
        init_log.info_high("Initialization Step 3 {}."
                           .format(idaeslog.condition(res)))

        if isinstance(
            blk.properties_isentropic[
                blk.flowsheet().config.time.first()].temperature,
            Var,
        ):
            blk.properties_isentropic[:].temperature.unfix()
        elif isinstance(
            blk.properties_isentropic[
                blk.flowsheet().config.time.first()].enth_mol,
            Var,
        ):
            blk.properties_isentropic[:].enth_mol.unfix()
        elif isinstance(
            blk.properties_isentropic[
                blk.flowsheet().config.time.first()].temperature,
            Expression,
        ):
            blk.del_component(blk.tmp_init_constraint)

        blk.isentropic.activate()

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

        if activate_performance_curves:
            blk.performance_curve.activate()
            for t, v in unfix_eff.items():
                if v:
                    blk.efficiency_isentropic[t].unfix()
            for t, v in unfix_ratioP.items():
                if v:
                    blk.ratioP[t].unfix()
            with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
                res = opt.solve(blk, tee=slc.tee)
            init_log.info_high(f"Initialization Step 5 {idaeslog.condition(res)}.")

        # ---------------------------------------------------------------------
        # Release Inlet state
        blk.control_volume.release_state(flags, outlvl)
        init_log.info(f"Initialization Complete: {idaeslog.condition(res)}")

    def _get_performance_contents(self, time_point=0):
        var_dict = {}
        if hasattr(self, "deltaP"):
            var_dict["Mechanical Work"] = self.work_mechanical[time_point]
        if hasattr(self, "deltaP"):
            var_dict["Pressure Change"] = self.deltaP[time_point]
        if hasattr(self, "ratioP"):
            var_dict["Pressure Ratio"] = self.ratioP[time_point]
        if hasattr(self, "efficiency_pump"):
            var_dict["Efficiency"] = self.efficiency_pump[time_point]
        if hasattr(self, "efficiency_isentropic"):
            var_dict["Isentropic Efficiency"] = \
                self.efficiency_isentropic[time_point]

        return {"vars": var_dict}

    def get_costing(self, module=costing, year=None, **kwargs):
        if not hasattr(self.flowsheet(), "costing"):
            self.flowsheet().get_costing(year=year)

        self.costing = Block()
        module.pressure_changer_costing(
            self.costing,
            **kwargs)

    def calculate_scaling_factors(self):
        super().calculate_scaling_factors()

        if hasattr(self, "work_fluid"):
            for t, v in self.work_fluid.items():
                iscale.set_scaling_factor(
                    v,
                    iscale.get_scaling_factor(
                        self.control_volume.work[t],
                        default=1,
                        warning=True))

        if hasattr(self, "work_mechanical"):
            for t, v in self.work_mechanical.items():
                iscale.set_scaling_factor(
                    v,
                    iscale.get_scaling_factor(
                        self.control_volume.work[t],
                        default=1,
                        warning=True))

        if hasattr(self, "work_isentropic"):
            for t, v in self.work_isentropic.items():
                iscale.set_scaling_factor(
                    v,
                    iscale.get_scaling_factor(
                        self.control_volume.work[t],
                        default=1,
                        warning=True))

        if hasattr(self, "ratioP_calculation"):
            for t, c in self.ratioP_calculation.items():
                iscale.constraint_scaling_transform(
                    c,
                    iscale.get_scaling_factor(
                        self.control_volume.properties_in[t].pressure,
                        default=1,
                        warning=True),
                    overwrite=False)

        if hasattr(self, "fluid_work_calculation"):
            for t, c in self.fluid_work_calculation.items():
                iscale.constraint_scaling_transform(
                    c,
                    iscale.get_scaling_factor(
                        self.control_volume.deltaP[t],
                        default=1,
                        warning=True),
                    overwrite=False)

        if hasattr(self, "actual_work"):
            for t, c in self.actual_work.items():
                iscale.constraint_scaling_transform(
                    c,
                    iscale.get_scaling_factor(
                        self.control_volume.work[t],
                        default=1,
                        warning=True),
                    overwrite=False)

        if hasattr(self, "isentropic_pressure"):
            for t, c in self.isentropic_pressure.items():
                iscale.constraint_scaling_transform(
                    c,
                    iscale.get_scaling_factor(
                        self.control_volume.properties_in[t].pressure,
                        default=1,
                        warning=True),
                    overwrite=False)

        if hasattr(self, "isentropic"):
            for t, c in self.isentropic.items():
                iscale.constraint_scaling_transform(
                    c,
                    iscale.get_scaling_factor(
                        self.control_volume.properties_in[t].entr_mol,
                        default=1,
                        warning=True),
                    overwrite=False)

        if hasattr(self, "isentropic_energy_balance"):
            for t, c in self.isentropic_energy_balance.items():
                iscale.constraint_scaling_transform(
                    c,
                    iscale.get_scaling_factor(
                        self.control_volume.work[t],
                        default=1,
                        warning=True),
                    overwrite=False)

        if hasattr(self, "zero_work_equation"):
            for t, c in self.zero_work_equation.items():
                iscale.constraint_scaling_transform(
                    c,
                    iscale.get_scaling_factor(
                        self.control_volume.work[t],
                        default=1,
                        warning=True))

        if hasattr(self, "state_material_balances"):
            cvol = self.control_volume
            phase_list = cvol.properties_in.phase_list
            phase_component_set = cvol.properties_in.phase_component_set
            mb_type = cvol._constructed_material_balance_type
            if mb_type == MaterialBalanceType.componentPhase:
                for (t, p, j), c in self.state_material_balances.items():
                    sf = iscale.get_scaling_factor(
                        cvol.properties_in[t].get_material_flow_terms(p, j),
                        default=1,
                        warning=True)
                    iscale.constraint_scaling_transform(c, sf)
            elif mb_type == MaterialBalanceType.componentTotal:
                for (t, j), c in self.state_material_balances.items():
                    sf = iscale.min_scaling_factor(
                        [cvol.properties_in[t].get_material_flow_terms(p, j)
                         for p in phase_list if (p, j) in phase_component_set])
                    iscale.constraint_scaling_transform(c, sf)
            else:
                # There are some other material balance types but they create
                # constraints with different names.
                _log.warning(f"Unknown material balance type {mb_type}")

        if hasattr(self, "costing"):
            # import costing scaling factors
            costing.calculate_scaling_factors(self.costing)
Пример #4
0
class Electrodialysis0DData(UnitModelBlockData):
    """
    0D Electrodialysis Model
    """

    # CONFIG are options for the unit model
    CONFIG = ConfigBlock()  #

    CONFIG.declare(
        "dynamic",
        ConfigValue(
            domain=In([False]),
            default=False,
            description="Dynamic model flag - must be False",
            doc="""Indicates whether this model will be dynamic or not,
    **default** = False. The filtration unit does not support dynamic
    behavior, thus this must be False.""",
        ),
    )

    CONFIG.declare(
        "has_holdup",
        ConfigValue(
            default=False,
            domain=In([False]),
            description="Holdup construction flag - must be False",
            doc="""Indicates whether holdup terms should be constructed or not.
    **default** - False. The filtration unit does not have defined volume, thus
    this must be False.""",
        ),
    )

    CONFIG.declare(
        "operation_mode",
        ConfigValue(
            default="Constant_Current",
            domain=In(["Constant_Current", "Constant_Voltage"]),
            description="The electrical operation mode. To be selected between Constant Current and Constant Voltage",
        ),
    )

    CONFIG.declare(
        "material_balance_type",
        ConfigValue(
            default=MaterialBalanceType.useDefault,
            domain=In(MaterialBalanceType),
            description="Material balance construction flag",
            doc="""Indicates what type of mass balance should be constructed,
    **default** - MaterialBalanceType.useDefault.
    **Valid values:** {
    **MaterialBalanceType.useDefault - refer to property package for default
    balance type
    **MaterialBalanceType.none** - exclude material balances,
    **MaterialBalanceType.componentPhase** - use phase component balances,
    **MaterialBalanceType.componentTotal** - use total component balances,
    **MaterialBalanceType.elementTotal** - use total element balances,
    **MaterialBalanceType.total** - use total material balance.}""",
        ),
    )

    # # TODO: Consider adding the EnergyBalanceType config using the following code
    '''
    CONFIG.declare("energy_balance_type", ConfigValue(
        default=EnergyBalanceType.none,
        domain=In(EnergyBalanceType),
        description="Energy balance construction flag",
        doc="""Indicates what type of energy balance should be constructed,
    **default** - EnergyBalanceType.useDefault.
    **Valid values:** {
    **EnergyBalanceType.useDefault - refer to property package for default
    balance type
    **EnergyBalanceType.none** - exclude energy balances,
    **EnergyBalanceType.enthalpyTotal** - single enthalpy balance for material,
    **EnergyBalanceType.enthalpyPhase** - enthalpy balances for each phase,
    **EnergyBalanceType.energyTotal** - single energy balance for material,
    **EnergyBalanceType.energyPhase** - energy balances for each phase.}"""))
    '''

    CONFIG.declare(
        "momentum_balance_type",
        ConfigValue(
            default=MomentumBalanceType.pressureTotal,
            domain=In(MomentumBalanceType),
            description="Momentum balance construction flag",
            doc="""Indicates what type of momentum balance should be constructed,
    **default** - MomentumBalanceType.pressureTotal.
    **Valid values:** {
    **MomentumBalanceType.none** - exclude momentum balances,
    **MomentumBalanceType.pressureTotal** - single pressure balance for material,
    **MomentumBalanceType.pressurePhase** - pressure balances for each phase,
    **MomentumBalanceType.momentumTotal** - single momentum balance for material,
    **MomentumBalanceType.momentumPhase** - momentum balances for each phase.}""",
        ),
    )

    CONFIG.declare(
        "property_package",
        ConfigValue(
            default=useDefault,
            domain=is_physical_parameter_block,
            description="Property package to use for control volume",
            doc="""Property parameter object used to define property calculations,
    **default** - useDefault.
    **Valid values:** {
    **useDefault** - use default package from parent model or flowsheet,
    **PhysicalParameterObject** - a PhysicalParameterBlock object.}""",
        ),
    )

    CONFIG.declare(
        "property_package_args",
        ConfigBlock(
            implicit=True,
            description="Arguments to use for constructing property packages",
            doc="""A ConfigBlock with arguments to be passed to a property block(s)
    and used when constructing these,
    **default** - None.
    **Valid values:** {
    see property package for documentation.}""",
        ),
    )

    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

        # Create essential sets.
        self.membrane_set = Set(initialize=["cem", "aem"])

        # Create unit model parameters and vars
        self.water_density = Param(
            initialize=1000,
            mutable=False,
            units=pyunits.kg * pyunits.m**-3,
            doc="density of water",
        )

        self.cell_pair_num = Var(
            initialize=1,
            domain=NonNegativeIntegers,
            bounds=(1, 10000),
            units=pyunits.dimensionless,
            doc="cell pair number in a stack",
        )

        # electrodialysis cell dimensional properties
        self.cell_width = Var(
            initialize=0.1,
            bounds=(1e-3, 1e2),
            units=pyunits.meter,
            doc="The width of the electrodialysis cell, denoted as b in the model description",
        )
        self.cell_length = Var(
            initialize=0.5,
            bounds=(1e-3, 1e2),
            units=pyunits.meter,
            doc="The length of the electrodialysis cell, denoted as l in the model description",
        )
        self.spacer_thickness = Var(
            initialize=0.0001,
            units=pyunits.meter,
            doc="The distance between the concecutive aem and cem",
        )

        # Material and Operational properties
        self.membrane_thickness = Var(
            self.membrane_set,
            initialize=0.0001,
            bounds=(1e-6, 1e-1),
            units=pyunits.meter,
            doc="Membrane thickness",
        )
        self.solute_diffusivity_membrane = Var(
            self.membrane_set,
            self.config.property_package.ion_set
            | self.config.property_package.solute_set,
            initialize=1e-10,
            bounds=(1e-16, 1e-6),
            units=pyunits.meter**2 * pyunits.second**-1,
            doc="Solute (ionic and neutral) diffusivity in the membrane phase",
        )
        self.ion_trans_number_membrane = Var(
            self.membrane_set,
            self.config.property_package.ion_set,
            bounds=(0, 1),
            units=pyunits.dimensionless,
            doc="Ion transference number in the membrane phase",
        )
        self.water_trans_number_membrane = Var(
            self.membrane_set,
            initialize=5,
            bounds=(0, 50),
            units=pyunits.dimensionless,
            doc="Transference number of water in membranes",
        )
        self.water_permeability_membrane = Var(
            self.membrane_set,
            initialize=1e-14,
            units=pyunits.meter * pyunits.second**-1 * pyunits.pascal**-1,
            doc="Water permeability coefficient",
        )
        self.membrane_surface_resistance = Var(
            self.membrane_set,
            initialize=2e-4,
            bounds=(1e-6, 1),
            units=pyunits.ohm * pyunits.meter**2,
            doc="Surface resistance of membrane",
        )
        self.electrodes_resistance = Var(
            initialize=0,
            bounds=(0, 100),
            domain=NonNegativeReals,
            units=pyunits.ohm * pyunits.meter**2,
            doc="areal resistance of TWO electrode compartments of a stack",
        )
        self.current = Var(
            self.flowsheet().config.time,
            initialize=1,
            bounds=(0, 1000),
            units=pyunits.amp,
            doc="Current across a cell-pair or stack",
        )
        self.voltage = Var(
            self.flowsheet().config.time,
            initialize=100,
            bounds=(0, 1000),
            units=pyunits.volt,
            doc="Voltage across a stack, declared under the 'Constant Voltage' mode only",
        )
        self.current_utilization = Var(
            initialize=1,
            bounds=(0, 1),
            units=pyunits.dimensionless,
            doc="The current utilization including water electro-osmosis and ion diffusion",
        )

        # Performance metrics
        self.current_efficiency = Var(
            self.flowsheet().config.time,
            initialize=0.9,
            bounds=(0, 1),
            units=pyunits.dimensionless,
            doc="The overall current efficiency for deionizaiton",
        )
        self.power_electrical = Var(
            self.flowsheet().config.time,
            initialize=1,
            bounds=(0, 12100),
            domain=NonNegativeReals,
            units=pyunits.watt,
            doc="Electrical power consumption of a stack",
        )
        self.specific_power_electrical = Var(
            self.flowsheet().config.time,
            initialize=10,
            bounds=(0, 1000),
            domain=NonNegativeReals,
            units=pyunits.kW * pyunits.hour * pyunits.meter**-3,
            doc="Diluate-volume-flow-rate-specific electrical power consumption",
        )
        # TODO: consider adding more performance as needed.

        # Fluxes Vars for constructing mass transfer terms
        self.elec_migration_flux_in = Var(
            self.flowsheet().config.time,
            self.config.property_package.phase_list,
            self.config.property_package.component_list,
            units=pyunits.mole * pyunits.meter**-2 * pyunits.second**-1,
            doc="Molar flux_in of a component across the membrane driven by electrical migration",
        )
        self.elec_migration_flux_out = Var(
            self.flowsheet().config.time,
            self.config.property_package.phase_list,
            self.config.property_package.component_list,
            units=pyunits.mole * pyunits.meter**-2 * pyunits.second**-1,
            doc="Molar flux_out of a component across the membrane driven by electrical migration",
        )
        self.nonelec_flux_in = Var(
            self.flowsheet().config.time,
            self.config.property_package.phase_list,
            self.config.property_package.component_list,
            units=pyunits.mole * pyunits.meter**-2 * pyunits.second**-1,
            doc="Molar flux_in of a component across the membrane driven by non-electrical forces",
        )
        self.nonelec_flux_out = Var(
            self.flowsheet().config.time,
            self.config.property_package.phase_list,
            self.config.property_package.component_list,
            units=pyunits.mole * pyunits.meter**-2 * pyunits.second**-1,
            doc="Molar flux_out of a component across the membrane driven by non-electrical forces",
        )

        # Build control volume for the dilute channel
        self.diluate_channel = ControlVolume0DBlock(
            default={
                "dynamic": False,
                "has_holdup": False,
                "property_package": self.config.property_package,
                "property_package_args": self.config.property_package_args,
            }
        )
        self.diluate_channel.add_state_blocks(has_phase_equilibrium=False)
        self.diluate_channel.add_material_balances(
            balance_type=self.config.material_balance_type, has_mass_transfer=True
        )
        self.diluate_channel.add_momentum_balances(
            balance_type=self.config.momentum_balance_type, has_pressure_change=False
        )
        # # TODO: Consider adding energy balances

        # Build control volume for the concentrate channel
        self.concentrate_channel = ControlVolume0DBlock(
            default={
                "dynamic": False,
                "has_holdup": False,
                "property_package": self.config.property_package,
                "property_package_args": self.config.property_package_args,
            }
        )
        self.concentrate_channel.add_state_blocks(has_phase_equilibrium=False)
        self.concentrate_channel.add_material_balances(
            balance_type=self.config.material_balance_type, has_mass_transfer=True
        )
        self.concentrate_channel.add_momentum_balances(
            balance_type=self.config.momentum_balance_type, has_pressure_change=False
        )
        # # TODO: Consider adding energy balances

        # Add ports (creates inlets and outlets for each channel)
        self.add_inlet_port(name="inlet_diluate", block=self.diluate_channel)
        self.add_outlet_port(name="outlet_diluate", block=self.diluate_channel)
        self.add_inlet_port(name="inlet_concentrate", block=self.concentrate_channel)
        self.add_outlet_port(name="outlet_concentrate", block=self.concentrate_channel)

        # Build Constraints
        @self.Constraint(
            self.flowsheet().config.time,
            self.config.property_package.phase_list,
            doc="Current-Voltage relationship",
        )
        def eq_current_voltage_relation(self, t, p):
            surface_resistance_cp = (
                self.membrane_surface_resistance["aem"]
                + self.membrane_surface_resistance["cem"]
                + self.spacer_thickness
                / (
                    0.5
                    * (
                        self.concentrate_channel.properties_in[
                            t
                        ].electrical_conductivity_phase[p]
                        + self.concentrate_channel.properties_out[
                            t
                        ].electrical_conductivity_phase[p]
                        + self.diluate_channel.properties_in[
                            t
                        ].electrical_conductivity_phase[p]
                        + self.diluate_channel.properties_out[
                            t
                        ].electrical_conductivity_phase[p]
                    )
                )
            )
            return (
                self.current[t]
                * (
                    surface_resistance_cp * self.cell_pair_num
                    + self.electrodes_resistance
                )
                == self.voltage[t] * self.cell_width * self.cell_length
            )

        @self.Constraint(
            self.flowsheet().config.time,
            self.config.property_package.phase_list,
            self.config.property_package.component_list,
            doc="Equation for electrical migration flux_in",
        )
        def eq_elec_migration_flux_in(self, t, p, j):
            if j == "H2O":
                return self.elec_migration_flux_in[t, p, j] == (
                    self.water_trans_number_membrane["cem"]
                    + self.water_trans_number_membrane["aem"]
                ) * (
                    self.current[t]
                    / (self.cell_width * self.cell_length)
                    / Constants.faraday_constant
                )
            elif j in self.config.property_package.ion_set:
                return self.elec_migration_flux_in[t, p, j] == (
                    self.ion_trans_number_membrane["cem", j]
                    - self.ion_trans_number_membrane["aem", j]
                ) * (
                    self.current_utilization
                    * self.current[t]
                    / (self.cell_width * self.cell_length)
                ) / (
                    self.config.property_package.charge_comp[j]
                    * Constants.faraday_constant
                )
            else:
                return self.elec_migration_flux_out[t, p, j] == 0

        @self.Constraint(
            self.flowsheet().config.time,
            self.config.property_package.phase_list,
            self.config.property_package.component_list,
            doc="Equation for electrical migration flux_out",
        )
        def eq_elec_migration_flux_out(self, t, p, j):
            if j == "H2O":
                return self.elec_migration_flux_out[t, p, j] == (
                    self.water_trans_number_membrane["cem"]
                    + self.water_trans_number_membrane["aem"]
                ) * (
                    self.current[t]
                    / (self.cell_width * self.cell_length)
                    / Constants.faraday_constant
                )
            elif j in self.config.property_package.ion_set:
                return self.elec_migration_flux_out[t, p, j] == (
                    self.ion_trans_number_membrane["cem", j]
                    - self.ion_trans_number_membrane["aem", j]
                ) * (
                    self.current_utilization
                    * self.current[t]
                    / (self.cell_width * self.cell_length)
                ) / (
                    self.config.property_package.charge_comp[j]
                    * Constants.faraday_constant
                )
            else:
                return self.elec_migration_flux_out[t, p, j] == 0

        @self.Constraint(
            self.flowsheet().config.time,
            self.config.property_package.phase_list,
            self.config.property_package.component_list,
            doc="Equation for non-electrical flux_in",
        )
        def eq_nonelec_flux_in(self, t, p, j):
            if j == "H2O":
                return self.nonelec_flux_in[
                    t, p, j
                ] == self.water_density / self.config.property_package.mw_comp[j] * (
                    self.water_permeability_membrane["cem"]
                    + self.water_permeability_membrane["aem"]
                ) * (
                    self.concentrate_channel.properties_in[t].pressure_osm_phase[p]
                    - self.diluate_channel.properties_in[t].pressure_osm_phase[p]
                )
            else:
                return self.nonelec_flux_in[t, p, j] == -(
                    self.solute_diffusivity_membrane["cem", j]
                    / self.membrane_thickness["cem"]
                    + self.solute_diffusivity_membrane["aem", j]
                    / self.membrane_thickness["aem"]
                ) * (
                    self.concentrate_channel.properties_in[t].conc_mol_phase_comp[p, j]
                    - self.diluate_channel.properties_in[t].conc_mol_phase_comp[p, j]
                )

        @self.Constraint(
            self.flowsheet().config.time,
            self.config.property_package.phase_list,
            self.config.property_package.component_list,
            doc="Equation for non-electrical flux_out",
        )
        def eq_nonelec_flux_out(self, t, p, j):
            if j == "H2O":
                return self.nonelec_flux_out[
                    t, p, j
                ] == self.water_density / self.config.property_package.mw_comp[j] * (
                    self.water_permeability_membrane["cem"]
                    + self.water_permeability_membrane["aem"]
                ) * (
                    self.concentrate_channel.properties_out[t].pressure_osm_phase[p]
                    - self.diluate_channel.properties_out[t].pressure_osm_phase[p]
                )
            else:
                return self.nonelec_flux_out[t, p, j] == -(
                    self.solute_diffusivity_membrane["cem", j]
                    / self.membrane_thickness["cem"]
                    + self.solute_diffusivity_membrane["aem", j]
                    / self.membrane_thickness["aem"]
                ) * (
                    self.concentrate_channel.properties_out[t].conc_mol_phase_comp[p, j]
                    - self.diluate_channel.properties_out[t].conc_mol_phase_comp[p, j]
                )

        # Add constraints for mass transfer terms (diluate_channel)
        @self.Constraint(
            self.flowsheet().config.time,
            self.config.property_package.phase_list,
            self.config.property_package.component_list,
            doc="Mass transfer term for the diluate channel",
        )
        def eq_mass_transfer_term_diluate(self, t, p, j):
            return self.diluate_channel.mass_transfer_term[t, p, j] == -0.5 * (
                self.elec_migration_flux_in[t, p, j]
                + self.elec_migration_flux_out[t, p, j]
                + self.nonelec_flux_in[t, p, j]
                + self.nonelec_flux_out[t, p, j]
            ) * (self.cell_width * self.cell_length)

        # Add constraints for mass transfer terms (concentrate_channel)
        @self.Constraint(
            self.flowsheet().config.time,
            self.config.property_package.phase_list,
            self.config.property_package.component_list,
            doc="Mass transfer term for the concentrate channel",
        )
        def eq_mass_transfer_term_concentrate(self, t, p, j):
            return self.concentrate_channel.mass_transfer_term[t, p, j] == 0.5 * (
                self.elec_migration_flux_in[t, p, j]
                + self.elec_migration_flux_out[t, p, j]
                + self.nonelec_flux_in[t, p, j]
                + self.nonelec_flux_out[t, p, j]
            ) * (self.cell_width * self.cell_length)

        # Add isothermal condition
        @self.Constraint(
            self.flowsheet().config.time,
            doc="Isothermal condition for the diluate channel",
        )
        def eq_isothermal_diluate(self, t):
            return (
                self.diluate_channel.properties_in[t].temperature
                == self.diluate_channel.properties_out[t].temperature
            )

        @self.Constraint(
            self.flowsheet().config.time,
            doc="Isothermal condition for the concentrate channel",
        )
        def eq_isothermal_concentrate(self, t):
            return (
                self.concentrate_channel.properties_in[t].temperature
                == self.concentrate_channel.properties_out[t].temperature
            )

        @self.Constraint(
            self.flowsheet().config.time,
            doc="Electrical power consumption of a stack",
        )
        def eq_power_electrical(self, t):
            return self.power_electrical[t] == self.current[t] * self.voltage[t]

        @self.Constraint(
            self.flowsheet().config.time,
            doc="Diluate_volume_flow_rate_specific electrical power consumption of a stack",
        )
        def eq_specific_power_electrical(self, t):
            return (
                pyunits.convert(
                    self.specific_power_electrical[t],
                    pyunits.watt * pyunits.second * pyunits.meter**-3,
                )
                * self.diluate_channel.properties_out[t].flow_vol_phase["Liq"]
                == self.current[t] * self.voltage[t]
            )

        @self.Constraint(
            self.flowsheet().config.time,
            doc="Overall current efficiency evaluation",
        )
        def eq_current_efficiency(self, t):
            return (
                self.current_efficiency[t] * self.current[t]
                == sum(
                    self.diluate_channel.properties_in[t].flow_mol_phase_comp["Liq", j]
                    * self.config.property_package.charge_comp[j]
                    - self.diluate_channel.properties_out[t].flow_mol_phase_comp[
                        "Liq", j
                    ]
                    * self.config.property_package.charge_comp[j]
                    for j in self.config.property_package.cation_set
                )
                * Constants.faraday_constant
            )

    # initialize method
    def initialize_build(
        blk, state_args=None, outlvl=idaeslog.NOTSET, solver=None, optarg=None
    ):
        """
        General wrapper for pressure changer initialization routines

        Keyword Arguments:
            state_args : a dict of arguments to be passed to the property
                         package(s) to provide an initial state for
                         initialization (see documentation of the specific
                         property package) (default = {}).
            outlvl : sets output level of initialization routine
            optarg : solver options dictionary object (default=None)
            solver : str indicating which solver to use during
                     initialization (default = None)

        Returns: None
        """
        init_log = idaeslog.getInitLogger(blk.name, outlvl, tag="unit")
        solve_log = idaeslog.getSolveLogger(blk.name, outlvl, tag="unit")
        # Set solver options
        opt = get_solver(solver, optarg)

        # ---------------------------------------------------------------------

        # Set the outlet has the same intial condition of the inlet.
        for k in blk.keys():
            for j in blk[k].config.property_package.component_list:
                blk[k].diluate_channel.properties_out[0].flow_mol_phase_comp[
                    "Liq", j
                ] = value(
                    blk[k]
                    .diluate_channel.properties_in[0]
                    .flow_mol_phase_comp["Liq", j]
                )
                blk[k].concentrate_channel.properties_out[0].flow_mol_phase_comp[
                    "Liq", j
                ] = value(
                    blk[k]
                    .concentrate_channel.properties_in[0]
                    .flow_mol_phase_comp["Liq", j]
                )
        # Initialize diluate_channel block
        flags_diluate = blk.diluate_channel.initialize(
            outlvl=outlvl,
            optarg=optarg,
            solver=solver,
            state_args=state_args,
            hold_state=True,
        )
        init_log.info_high("Initialization Step 1 Complete.")
        # ---------------------------------------------------------------------
        # Initialize concentrate_side block
        flags_concentrate = blk.concentrate_channel.initialize(
            outlvl=outlvl,
            optarg=optarg,
            solver=solver,
            state_args=state_args,  # inlet var
            hold_state=True,
        )
        init_log.info_high("Initialization Step 2 Complete.")
        # ---------------------------------------------------------------------
        # Solve unit
        with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
            res = opt.solve(blk, tee=slc.tee)
        init_log.info_high("Initialization Step 3 {}.".format(idaeslog.condition(res)))
        # ---------------------------------------------------------------------
        # Release state
        blk.diluate_channel.release_state(flags_diluate, outlvl)
        init_log.info("Initialization Complete: {}".format(idaeslog.condition(res)))
        blk.concentrate_channel.release_state(flags_concentrate, outlvl)
        init_log.info("Initialization Complete: {}".format(idaeslog.condition(res)))

    def calculate_scaling_factors(self):
        super().calculate_scaling_factors()
        # Var scaling
        # The following Vars' sf are allowed to be provided by users or set by default.
        if (
            iscale.get_scaling_factor(self.solute_diffusivity_membrane, warning=True)
            is None
        ):
            iscale.set_scaling_factor(self.solute_diffusivity_membrane, 1e10)
        if iscale.get_scaling_factor(self.membrane_thickness, warning=True) is None:
            iscale.set_scaling_factor(self.membrane_thickness, 1e4)
        if (
            iscale.get_scaling_factor(self.water_permeability_membrane, warning=True)
            is None
        ):
            iscale.set_scaling_factor(self.water_permeability_membrane, 1e14)
        if iscale.get_scaling_factor(self.cell_length, warning=True) is None:
            iscale.set_scaling_factor(self.cell_length, 1e1)
        if iscale.get_scaling_factor(self.cell_width, warning=True) is None:
            iscale.set_scaling_factor(self.cell_width, 1e1)
        if iscale.get_scaling_factor(self.spacer_thickness, warning=True) is None:
            iscale.set_scaling_factor(self.spacer_thickness, 1e4)
        if (
            iscale.get_scaling_factor(self.membrane_surface_resistance, warning=True)
            is None
        ):
            iscale.set_scaling_factor(self.membrane_surface_resistance, 1e4)
        if iscale.get_scaling_factor(self.electrodes_resistance, warning=True) is None:
            iscale.set_scaling_factor(self.electrodes_resistance, 1e4)
        if iscale.get_scaling_factor(self.current, warning=True) is None:
            iscale.set_scaling_factor(self.current, 1)
        if iscale.get_scaling_factor(self.voltage, warning=True) is None:
            iscale.set_scaling_factor(self.voltage, 1e-1)
        # The folloing Vars are built for constructing constraints and their sf are computed from other Vars.
        iscale.set_scaling_factor(
            self.elec_migration_flux_in,
            iscale.get_scaling_factor(self.current)
            * iscale.get_scaling_factor(self.cell_length) ** -1
            * iscale.get_scaling_factor(self.cell_width) ** -1
            * 1e5,
        )
        iscale.set_scaling_factor(
            self.elec_migration_flux_out,
            iscale.get_scaling_factor(self.current)
            * iscale.get_scaling_factor(self.cell_length) ** -1
            * iscale.get_scaling_factor(self.cell_width) ** -1
            * 1e5,
        )
        iscale.set_scaling_factor(
            self.power_electrical,
            iscale.get_scaling_factor(self.current)
            * iscale.get_scaling_factor(self.voltage),
        )
        for ind, c in self.specific_power_electrical.items():
            iscale.set_scaling_factor(
                self.specific_power_electrical[ind],
                3.6e6
                * iscale.get_scaling_factor(self.current[ind])
                * iscale.get_scaling_factor(self.voltage[ind])
                * iscale.get_scaling_factor(
                    self.diluate_channel.properties_out[ind].flow_vol_phase["Liq"]
                )
                ** -1,
            )

        # Constraint scaling
        for ind, c in self.eq_current_voltage_relation.items():
            iscale.constraint_scaling_transform(
                c, iscale.get_scaling_factor(self.membrane_surface_resistance)
            )
        for ind, c in self.eq_power_electrical.items():
            iscale.constraint_scaling_transform(
                c, iscale.get_scaling_factor(self.power_electrical)
            )
        for ind, c in self.eq_specific_power_electrical.items():
            iscale.constraint_scaling_transform(
                c, iscale.get_scaling_factor(self.specific_power_electrical[ind])
            )

        for ind, c in self.eq_elec_migration_flux_in.items():
            iscale.constraint_scaling_transform(
                c, iscale.get_scaling_factor(self.elec_migration_flux_in)
            )
        for ind, c in self.eq_elec_migration_flux_out.items():
            iscale.constraint_scaling_transform(
                c, iscale.get_scaling_factor(self.elec_migration_flux_out)
            )
        for ind, c in self.eq_nonelec_flux_in.items():
            if ind[2] == "H2O":
                sf = (
                    1e-3
                    * 0.018
                    * iscale.get_scaling_factor(self.water_permeability_membrane)
                    * iscale.get_scaling_factor(
                        self.concentrate_channel.properties_in[
                            ind[0]
                        ].pressure_osm_phase[ind[1]]
                    )
                )
            sf = (
                iscale.get_scaling_factor(self.solute_diffusivity_membrane)
                / iscale.get_scaling_factor(self.membrane_thickness)
                * iscale.get_scaling_factor(
                    self.concentrate_channel.properties_in[ind[0]].conc_mol_phase_comp[
                        ind[1], ind[2]
                    ]
                )
            )
            iscale.set_scaling_factor(self.nonelec_flux_in[ind], sf)
            iscale.constraint_scaling_transform(c, sf)
        for ind, c in self.eq_nonelec_flux_out.items():
            if ind[2] == "H2O":
                sf = (
                    1e-3
                    * 0.018
                    * iscale.get_scaling_factor(self.water_permeability_membrane)
                    * iscale.get_scaling_factor(
                        self.concentrate_channel.properties_out[
                            ind[0]
                        ].pressure_osm_phase[ind[1]]
                    )
                )
            else:
                sf = (
                    iscale.get_scaling_factor(self.solute_diffusivity_membrane)
                    / iscale.get_scaling_factor(self.membrane_thickness)
                    * iscale.get_scaling_factor(
                        self.concentrate_channel.properties_out[
                            ind[0]
                        ].conc_mol_phase_comp[ind[1], ind[2]]
                    )
                )

            iscale.set_scaling_factor(self.nonelec_flux_out[ind], sf)
            iscale.constraint_scaling_transform(c, sf)
        for ind, c in self.eq_mass_transfer_term_diluate.items():
            iscale.constraint_scaling_transform(
                c,
                min(
                    iscale.get_scaling_factor(self.elec_migration_flux_in[ind]),
                    iscale.get_scaling_factor(
                        self.nonelec_flux_in[ind], self.elec_migration_flux_out[ind]
                    ),
                    iscale.get_scaling_factor(self.nonelec_flux_out[ind]),
                ),
            )
        for ind, c in self.eq_mass_transfer_term_concentrate.items():
            iscale.constraint_scaling_transform(
                c,
                min(
                    iscale.get_scaling_factor(self.elec_migration_flux_in[ind]),
                    iscale.get_scaling_factor(
                        self.nonelec_flux_in[ind], self.elec_migration_flux_out[ind]
                    ),
                    iscale.get_scaling_factor(self.nonelec_flux_out[ind]),
                ),
            )

        for ind, c in self.eq_power_electrical.items():
            iscale.constraint_scaling_transform(
                c,
                iscale.get_scaling_factor(self.power_electrical[ind]),
            )

        for ind, c in self.eq_specific_power_electrical.items():
            iscale.constraint_scaling_transform(
                c,
                iscale.get_scaling_factor(self.specific_power_electrical[ind])
                * iscale.get_scaling_factor(
                    self.diluate_channel.properties_out[ind].flow_vol_phase["Liq"]
                ),
            )
        for ind, c in self.eq_current_efficiency.items():
            iscale.constraint_scaling_transform(
                c, iscale.get_scaling_factor(self.current[ind])
            )

        for ind, c in self.eq_isothermal_diluate.items():
            iscale.constraint_scaling_transform(
                c, self.diluate_channel.properties_in[ind].temperature
            )
        for ind, c in self.eq_isothermal_concentrate.items():
            iscale.constraint_scaling_transform(
                c, self.concentrate_channel.properties_in[ind].temperature
            )

    def _get_stream_table_contents(self, time_point=0):
        return create_stream_table_dataframe(
            {
                "Diluate Channel Inlet": self.inlet_diluate,
                "Concentrate Channel Inlet": self.inlet_concentrate,
                "Diluate Channel Outlet": self.outlet_diluate,
                "Concentrate Channel Outlet": self.outlet_concentrate,
            },
            time_point=time_point,
        )

    def _get_performance_contents(self, time_point=0):
        return {
            "vars": {
                "Electrical power consumption(Watt)": self.power_electrical[time_point],
                "Specific electrical power consumption (kWh/m**3)": self.specific_power_electrical[
                    time_point
                ],
                "Current efficiency for deionzation": self.current_efficiency[
                    time_point
                ],
            },
            "exprs": {},
            "params": {},
        }
Пример #5
0
class FlueGasStateBlockData(StateBlockData):
    """
    This is an example of a property package for calculating the thermophysical
    properties of flue gas using the ideal gas assumption.
    """
    def build(self):
        """
        Callable method for Block construction
        """
        super(FlueGasStateBlockData, self).build()
        comps = self.params.component_list
        # Add state variables
        self.flow_mol_comp = Var(comps,
                                 domain=Reals,
                                 initialize=1.0,
                                 bounds=(0, 1e6),
                                 doc='Component molar flowrate [mol/s]')
        self.pressure = Var(domain=Reals,
                            initialize=1.01325e5,
                            bounds=(1, 5e7),
                            doc='State pressure [Pa]')
        self.temperature = Var(domain=Reals,
                               initialize=500,
                               bounds=(200, 1500),
                               doc='State temperature [K]')

        # Add expressions for some basic oft-used quantiies
        self.flow_mol = Expression(expr=sum(self.flow_mol_comp[j]
                                            for j in comps))

        def rule_mole_frac(b, c):
            return b.flow_mol_comp[c] / b.flow_mol

        self.mole_frac_comp = Expression(comps,
                                         rule=rule_mole_frac,
                                         doc='mole fraction of component i')

        self.flow_mass = Expression(expr=sum(
            self.flow_mol_comp[j] * self.params.mw_comp[j] for j in comps),
                                    doc='total mass flow')

        def rule_mw_comp(b, j):
            return b.params.mw_comp[j]

        self.mw_comp = Expression(comps, rule=rule_mw_comp)

        def rule_mw(b):
            return sum(b.mw_comp[j] * b.mole_frac_comp[j] for j in comps)

        self.mw = Expression(rule=rule_mw)

        self.pressure_crit = Expression(expr=sum(self.params.pressure_crit[j] *
                                                 self.mole_frac_comp[j]
                                                 for j in comps))
        self.temperature_crit = Expression(expr=sum(
            self.params.temperature_crit[j] * self.mole_frac_comp[j]
            for j in comps))
        self.pressure_red = Expression(expr=self.pressure / self.pressure_crit)
        self.temperature_red = Expression(expr=self.temperature /
                                          self.temperature_crit)

        self.compress_fact = Expression(expr=1.0,
                                        doc='Vapor Compressibility Factor')

        def rule_dens_mol_phase(b, p):
            return b.pressure / b.compress_fact / \
                constants.Constants.gas_constant / b.temperature

        self.dens_mol_phase = Expression(self.params.phase_list,
                                         rule=rule_dens_mol_phase,
                                         doc='Molar Density')

        self.flow_vol = Expression(doc='Volumetric Flowrate',
                                   expr=self.flow_mol /
                                   self.dens_mol_phase["Vap"])

    def _heat_cap_calc(self):
        # heat capacity J/mol-K
        self.cp_mol = Var(initialize=1000, doc='heat capacity [J/mol-K]')

        def rule_cp_phase(b, p):
            # This property module only has one phase
            return self.cp_mol

        self.cp_mol_phase = Expression(self.params.phase_list,
                                       rule=rule_cp_phase)

        try:
            coeff = self.params.cp_mol_ig_comp_coeff
            ft = sum(self.flow_mol_comp[j] for j in self.params.component_list)
            t = self.temperature / 1000
            self.heat_cap_correlation = Constraint(
                expr=(self.cp_mol * ft == sum(
                    self.flow_mol_comp[j] *
                    (coeff['A', j] + coeff['B', j] * t + coeff['C', j] * t**2 +
                     coeff['D', j] * t**3 + coeff['E', j] / t**2)
                    for j in self.params.component_list)))
        except AttributeError:
            self.del_component(self.cp_mol)
            self.del_component(self.heat_cap_correlation)

    def _enthalpy_calc(self):
        self.enth_mol = Var(doc='Specific Enthalpy [J/mol]')

        def rule_enth_phase(b, p):
            # This property module only has one phase
            return self.enth_mol

        self.enth_mol_phase = Expression(self.params.phase_list,
                                         rule=rule_enth_phase)

        def enthalpy_correlation(b):
            coeff = self.params.cp_mol_ig_comp_coeff
            ft = sum(self.flow_mol_comp[j] for j in self.params.component_list)
            t = self.temperature / 1000
            kJ_to_J = 1000
            return self.enth_mol * ft == (sum(
                kJ_to_J * self.flow_mol_comp[j] *
                (coeff['A', j] * t + coeff['B', j] * t**2 / 2 +
                 coeff['C', j] * t**3 / 3 + coeff['D', j] * t**4 / 4 -
                 coeff['E', j] / t + coeff['F', j])
                for j in self.params.component_list))
            # NOTE: the H term (from the Shomate Equation) is not
            # included here so that the reference state enthalpy is the
            # enthalpy of formation (not 0).

        try:
            self.enthalpy_correlation = Constraint(rule=enthalpy_correlation)
        except AttributeError:
            self.del_component(self.enth_mol_phase)
            self.del_component(self.enth_mol)
            self.del_component(self.enthalpy_correlation)

    def _entropy_calc(self):
        self.entr_mol = Var(doc='Specific Entropy [J/mol/K]')

        # Specific Entropy

        def rule_entr_phase(b, p):
            # This property module only has one phase
            return self.entr_mol

        self.entr_mol_phase = Expression(self.params.phase_list,
                                         rule=rule_entr_phase)

        def entropy_correlation(b):
            coeff = self.params.cp_mol_ig_comp_coeff
            ft = sum(self.flow_mol_comp[j] for j in self.params.component_list)
            t = self.temperature / 1000
            n = self.flow_mol_comp
            x = self.mole_frac_comp
            r_gas = constants.Constants.gas_constant
            return self.entr_mol * ft  == \
                sum(n[j] * (
                    coeff['A', j] * log(t) +
                    coeff['B', j] * t +
                    coeff['C', j] * t**2 / 2 +
                    coeff['D', j] * t**3 / 3 -
                    coeff['E', j] / t**2 / 2 +
                    coeff['G', j] +
                    r_gas * log(x[j])) for j in self.params.component_list)

        try:
            self.entropy_correlation = Constraint(rule=entropy_correlation)
        except AttributeError:
            self.del_component(self.entr_mol_phase)
            self.del_component(self.entropy_correlation)

    def _vapor_pressure(self):
        # Vapour Pressure
        self.pressure_sat = Var(initialize=101325, doc="Vapour pressure [Pa]")

        def vapor_pressure_correlation(b):
            return (log(b.pressure_sat) * sum(
                b.flow_mol_comp[j] for j in b.params.component_list) == sum(
                    (b.params.vapor_pressure_coeff[j, 'A'] * b.temperature -
                     (b.params.vapor_pressure_coeff[j, 'B'] /
                      (b.temperature + b._param.vapor_pressure_coeff[j, 'C']))
                     ) * b.flow_mol_comp[j] for j in b.params.component_list))

        try:
            self.vapor_pressure_correlation = \
                Constraint(rule=vapor_pressure_correlation)
        except AttributeError:
            self.del_component(self.pressure_sat)
            self.del_component(self.vapor_pressure_correlation)

    def _therm_cond(self):
        comps = self.params.component_list
        self.therm_cond_comp = Var(comps,
                                   initialize=0.05,
                                   doc='thermal conductivity J/m-K-s')
        self.therm_cond = Var(
            initialize=0.05, doc='thermal conductivity of gas mixture J/m-K-s')
        self.visc_d_comp = Var(comps,
                               initialize=2e-5,
                               doc='dynamic viscocity of pure gas species')
        self.visc_d = Var(initialize=2e-5,
                          doc='viscosity of gas mixture kg/m-s')
        self.sigma = Param(comps,
                           initialize={
                               'O2': 3.458,
                               'N2': 3.621,
                               'NO': 3.47,
                               'CO2': 3.763,
                               'H2O': 2.605,
                               'SO2': 4.29
                           },
                           doc='collision diameter in Angstrom (10e-10 mts)')
        self.ep_Kappa = Param(
            comps,
            initialize={
                'O2': 107.4,
                'N2': 97.53,
                'NO': 119.0,
                'CO2': 244.0,
                'H2O': 572.4,
                'SO2': 252.0
            },
            doc="characteristic energy of interaction between pair of molecules "
            "K = Boltzmann constant in Kelvin")
        try:

            def rule_therm_cond(b, c):
                return b.therm_cond_comp[c] == (
                    ((b.params.cp_mol_ig_comp_coeff['A', c] +
                      b.params.cp_mol_ig_comp_coeff['B', c] *
                      (b.temperature / 1000) +
                      b.params.cp_mol_ig_comp_coeff['C', c] *
                      (b.temperature / 1000)**2 +
                      b.params.cp_mol_ig_comp_coeff['D', c] *
                      (b.temperature / 1000)**3 +
                      b.params.cp_mol_ig_comp_coeff['E', c] /
                      (b.temperature / 1000)**2) / b.params.mw_comp[c]) +
                    1.25 *
                    (constants.Constants.gas_constant / b.params.mw_comp[c])
                ) * b.visc_d_comp[c]

            self.therm_cond_con = Constraint(comps, rule=rule_therm_cond)

            def rule_theta(b, c):
                return b.temperature / b.ep_Kappa[c]

            self.theta = Expression(comps, rule=rule_theta)

            def rule_omega(b, c):
                return (1.5794145 + 0.00635771 * b.theta[c] -
                        0.7314 * log(b.theta[c]) +
                        0.2417357 * log(b.theta[c])**2 -
                        0.0347045 * log(b.theta[c])**3)

            self.omega = Expression(comps, rule=rule_omega)

            # Pure gas viscocity
            def rule_visc_d(b, c):
                return (b.visc_d_comp[c] * b.sigma[c]**2 *
                        b.omega[c] == 2.6693e-6 *
                        sqrt(b.params.mw_comp[c] * 1000 * b.temperature))

            self.visc_d_con = Constraint(comps, rule=rule_visc_d)

            # section to calculate viscosity of gas mixture
            def rule_phi(b, i, j):
                return (1 / 2.8284 *
                        (1 +
                         (b.params.mw_comp[i] / b.params.mw_comp[j]))**(-0.5) *
                        (1 + sqrt(b.visc_d_comp[i] / b.visc_d_comp[j]) *
                         (b.params.mw_comp[j] / b.params.mw_comp[i])**0.25)**2)

            self.phi_ij = Expression(comps, comps, rule=rule_phi)

            # viscosity of Gas mixture kg/m-s
            def rule_visc_d_mix(b):
                return b.visc_d == sum(
                    (b.mole_frac_comp[i] * b.visc_d_comp[i]) /
                    sum(b.mole_frac_comp[j] * b.phi_ij[i, j] for j in comps)
                    for i in comps)

            self.vis_d_mix_con = Constraint(rule=rule_visc_d_mix)

            # thermal conductivity of gas mixture in kg/m-s
            def rule_therm_mix(b):
                return b.therm_cond == sum(
                    (b.mole_frac_comp[i] * b.therm_cond_comp[i]) /
                    sum(b.mole_frac_comp[j] * b.phi_ij[i, j] for j in comps)
                    for i in comps)

            self.therm_mix_con = Constraint(rule=rule_therm_mix)

        except AttributeError:
            self.del_component(self.therm_cond_comp)
            self.del_component(self.therm_cond)
            self.del_component(self.visc_d_comp)
            self.del_component(self.visc_d)
            self.del_component(self.omega)
            self.del_component(self.theta)
            self.del_component(self.phi_ij)
            self.del_component(self.sigma)
            self.del_component(self.ep_Kappa)
            self.del_component(self.therm_cond_con)
            self.del_component(self.theta_con)
            self.del_component(self.omega_con)
            self.del_component(self.visc_d_con)
            self.del_component(self.phi_con)

    def default_material_balance_type(self):
        return MaterialBalanceType.componentTotal

    def default_energy_balance_type(self):
        return EnergyBalanceType.enthalpyTotal

    def get_material_flow_terms(self, p, j):
        return self.flow_mol_comp[j]

    def get_material_flow_basis(self):
        return MaterialFlowBasis.molar

    def get_enthalpy_flow_terms(self, p):
        if not self.is_property_constructed("enthalpy_flow_terms"):
            try:

                def rule_enthalpy_flow_terms(b, p):
                    return self.enth_mol_phase[p] * self.flow_mol

                self.enthalpy_flow_terms = Expression(
                    self.params.phase_list, rule=rule_enthalpy_flow_terms)
            except AttributeError:
                self.del_component(enthalpy_flow_terms)
        return self.enthalpy_flow_terms[p]

    def get_material_density_terms(self, p, j):
        return self.dens_mol_phase[p]

    def get_energy_density_terms(self, p):
        if not self.is_property_constructed("energy_density_terms"):
            try:

                def rule_energy_density_terms(b, p):
                    return self.enth_mol_phase[p] * \
                        self.dens_mol_phase[p] - self.pressure

                self.energy_density_terms = Expression(
                    self.params.phase_list, rule=rule_energy_density_terms)
            except AttributeError:
                self.del_component(energy_density_terms)
        return self.energy_density_terms[p]

    def define_state_vars(self):
        return {
            "flow_mol_comp": self.flow_mol_comp,
            "temperature": self.temperature,
            "pressure": self.pressure
        }

    def model_check(self):
        """
        Model checks for property block
        """
        # Check temperature bounds
        for v in self.compoent_object_data(Var, descend_into=True):
            if value(v) < v.lb:
                _log_error(f"{v} is below lower bound in {self.name}")
            if value(v) > v.ub:
                _log_error(f"{v} is above upper bound in {self.name}")

    def calculate_scaling_factors(self):
        super().calculate_scaling_factors()

        # Get some scale factors that are frequently used to calculate others
        sf_flow = iscale.get_scaling_factor(self.flow_mol)
        sf_mol_fraction = {}
        comps = self.params.component_list
        for i in comps:
            sf_mol_fraction[i] = iscale.get_scaling_factor(
                self.mole_frac_comp[i])
        # calculate flow_mol_comp scale factors
        for i, c in self.flow_mol_comp.items():
            iscale.set_scaling_factor(c, sf_flow * sf_mol_fraction[i])

        if self.is_property_constructed("energy_density_terms"):
            for i, c in self.energy_density_terms.items():
                sf1 = iscale.get_scaling_factor(self.enth_mol_phase[i])
                sf2 = iscale.get_scaling_factor(self.dens_mol_phase[i])
                iscale.set_scaling_factor(c, sf1 * sf2)

        if self.is_property_constructed("enthalpy_flow_terms"):
            for i, c in self.enthalpy_flow_terms.items():
                sf1 = iscale.get_scaling_factor(self.enth_mol_phase[i])
                sf2 = iscale.get_scaling_factor(self.flow_mol)
                iscale.set_scaling_factor(c, sf1 * sf2)

        if self.is_property_constructed("heat_cap_correlation"):
            iscale.constraint_scaling_transform(
                self.heat_cap_correlation,
                iscale.get_scaling_factor(self.cp_mol) *
                iscale.get_scaling_factor(self.flow_mol))
        if self.is_property_constructed("enthalpy_correlation"):
            for p, c in self.enthalpy_correlation.items():
                iscale.constraint_scaling_transform(
                    c,
                    iscale.get_scaling_factor(self.enth_mol) *
                    iscale.get_scaling_factor(self.flow_mol))
        if self.is_property_constructed("entropy_correlation"):
            iscale.constraint_scaling_transform(
                self.entropy_correlation,
                iscale.get_scaling_factor(self.entr_mol) *
                iscale.get_scaling_factor(self.flow_mol))
        if self.is_property_constructed("vapor_pressure_correlation"):
            iscale.constraint_scaling_transform(
                self.vapor_pressure_correlation,
                log(iscale.get_scaling_factor(self.pressure_sat)) *
                iscale.get_scaling_factor(self.flow_mol))
        if self.is_property_constructed("therm_cond_con"):
            for i, c in self.therm_cond_con.items():
                iscale.constraint_scaling_transform(
                    c, iscale.get_scaling_factor(self.therm_cond_comp[i]))
        if self.is_property_constructed("therm_mix_con"):
            iscale.constraint_scaling_transform(
                self.therm_mix_con, iscale.get_scaling_factor(self.therm_cond))
        if self.is_property_constructed("visc_d_con"):
            for i, c in self.visc_d_con.items():
                iscale.constraint_scaling_transform(
                    c, iscale.get_scaling_factor(self.visc_d_comp[i]))
        if self.is_property_constructed("visc_d_mix_con"):
            iscale.constraint_scaling_transform(
                self.visc_d_mix_con, iscale.get_scaling_factor(self.visc_d))
Пример #6
0
class DSPMDEStateBlockData(StateBlockData):
    def build(self):
        """Callable method for Block construction."""
        super().build()

        self.scaling_factor = Suffix(direction=Suffix.EXPORT)

        # Add state variables
        self.flow_mol_phase_comp = Var(
            self.params.phase_list,
            self.params.component_list,
            initialize=100, #todo: revisit
            bounds=(1e-8, None),
            domain=NonNegativeReals,
            units=pyunits.mol/pyunits.s,
            doc='Mole flow rate')

        self.temperature = Var(
            initialize=298.15,
            bounds=(273.15, 373.15),
            domain=NonNegativeReals,
            units=pyunits.K,
            doc='State temperature')

        self.pressure = Var(
            initialize=101325,
            bounds=(1e5, 5e7),
            domain=NonNegativeReals,
            units=pyunits.Pa,
            doc='State pressure')

    # -----------------------------------------------------------------------------
    # Property Methods
    def _mass_frac_phase_comp(self):
        self.mass_frac_phase_comp = Var(
            self.params.phase_list,
            self.params.component_list,
            initialize=lambda b,p,j : 0.4037 if j == "H2O" else 0.0033, #todo: revisit
            bounds=(1e-6, 1.001),
            units=pyunits.kg/pyunits.kg,
            doc='Mass fraction')

        def rule_mass_frac_phase_comp(b, j):
            return (b.mass_frac_phase_comp['Liq', j] == b.flow_mass_phase_comp['Liq', j] /
                    sum(b.flow_mass_phase_comp['Liq', j]
                        for j in self.params.component_list))
        self.eq_mass_frac_phase_comp = Constraint(self.params.component_list, rule=rule_mass_frac_phase_comp)

    def _dens_mass_phase(self):
        self.dens_mass_phase = Var(
            ['Liq'],
            initialize=1e3,
            bounds=(5e2, 2e3),
            units=pyunits.kg * pyunits.m ** -3,
            doc="Mass density")
        #TODO: reconsider this approach for solution density based on arbitrary solute_list
        def rule_dens_mass_phase(b):
            return (b.dens_mass_phase['Liq'] == 1000 * pyunits.kg * pyunits.m**-3)
        self.eq_dens_mass_phase = Constraint(rule=rule_dens_mass_phase)

    def _flow_vol_phase(self):
        self.flow_vol_phase = Var(
            self.params.phase_list,
            initialize=1,
            bounds=(1e-8, None),
            units=pyunits.m ** 3 / pyunits.s,
            doc="Volumetric flow rate")

        def rule_flow_vol_phase(b):
            return (b.flow_vol_phase['Liq']
                    == sum(b.flow_mol_phase_comp['Liq', j]*b.mw_comp[j] for j in self.params.component_list)
                    / b.dens_mass_phase['Liq'])
        self.eq_flow_vol_phase = Constraint(rule=rule_flow_vol_phase)

    def _flow_vol(self):

        def rule_flow_vol(b):
            return sum(b.flow_vol_phase[p] for p in self.params.phase_list)
        self.flow_vol = Expression(rule=rule_flow_vol)

    def _conc_mol_phase_comp(self):
        self.conc_mol_phase_comp = Var(
            self.params.phase_list,
            self.params.component_list,
            initialize=10,
            bounds=(1e-6, None),
            units=pyunits.mol * pyunits.m ** -3,
            doc="Molar concentration")

        def rule_conc_mol_phase_comp(b, j):
            return (b.conc_mol_phase_comp['Liq', j] * b.params.mw_comp[j] ==
                    b.conc_mass_phase_comp['Liq', j])
        self.eq_conc_mol_phase_comp = Constraint(self.params.component_list, rule=rule_conc_mol_phase_comp)

    def _conc_mass_phase_comp(self):
        self.conc_mass_phase_comp = Var(
            self.params.phase_list,
            self.params.component_list,
            initialize=10,
            bounds=(1e-3, 2e3),
            units=pyunits.kg * pyunits.m ** -3,
            doc="Mass concentration")

        def rule_conc_mass_phase_comp(b, j):
            return (b.conc_mass_phase_comp['Liq', j] ==
                    b.dens_mass_phase['Liq'] * b.mass_frac_phase_comp['Liq', j])
        self.eq_conc_mass_phase_comp = Constraint(self.params.component_list, rule=rule_conc_mass_phase_comp)

    def _flow_mass_phase_comp(self):
        self.flow_mass_phase_comp = Var(
            self.params.phase_list,
            self.params.component_list,
            initialize=100,
            bounds=(1e-8, None),
            units=pyunits.kg / pyunits.s,
            doc="Component Mass flowrate")

        def rule_flow_mass_phase_comp(b, j):
            return (b.flow_mass_phase_comp['Liq', j] ==
                    b.flow_mol_phase_comp['Liq', j] * b.params.mw_comp[j])
        self.eq_flow_mass_phase_comp = Constraint(self.params.component_list, rule=rule_flow_mass_phase_comp)

    def _mole_frac_phase_comp(self):
        self.mole_frac_phase_comp = Var(
            self.params.phase_list,
            self.params.component_list,
            initialize=0.1,
            bounds=(1e-6, 1.001),
            units=pyunits.dimensionless,
            doc="Mole fraction")

        def rule_mole_frac_phase_comp(b, j):
            return (b.mole_frac_phase_comp['Liq', j] == b.flow_mol_phase_comp['Liq', j] /
                    sum(b.flow_mol_phase_comp['Liq', j] for j in b.params.component_list))
        self.eq_mole_frac_phase_comp = Constraint(self.params.component_list, rule=rule_mole_frac_phase_comp)

    def _molality_comp(self):
        self.molality_comp = Var(
            self.params.solute_set,
            initialize=1,
            bounds=(1e-4, 10),
            units=pyunits.mole / pyunits.kg,
            doc="Molality")

        def rule_molality_comp(b, j):
            return (b.molality_comp[j] ==
                    b.flow_mol_phase_comp['Liq', j]
                    / b.flow_mol_phase_comp['Liq', 'H2O']
                    / b.params.mw_comp['H2O'])

        self.eq_molality_comp = Constraint(self.params.solute_set, rule=rule_molality_comp)

    def _radius_stokes_comp(self):
        add_object_reference(self, "radius_stokes_comp", self.params.radius_stokes_comp)

    def _diffus_phase_comp(self):
        add_object_reference(self, "diffus_phase_comp", self.params.diffus_phase_comp)

    def _visc_d_phase(self):
        add_object_reference(self, "visc_d_phase", self.params.visc_d_phase)

    def _mw_comp(self):
        add_object_reference(self, "mw_comp", self.params.mw_comp)

    def _charge_comp(self):
        add_object_reference(self, "charge_comp", self.params.charge_comp)

    def _dielectric_constant(self):
        add_object_reference(self, "dielectric_constant", self.params.dielectric_constant)

    def _act_coeff_phase_comp(self):
        self.act_coeff_phase_comp = Var(
            self.phase_list,
            self.params.solute_set,
            initialize=1,
            bounds=(1e-4, 1.001),
            units=pyunits.dimensionless,
            doc="activity coefficient of component")

        def rule_act_coeff_phase_comp(b, j):
            if b.params.config.activity_coefficient_model == ActivityCoefficientModel.ideal:
                return b.act_coeff_phase_comp['Liq', j] == 1
            elif b.params.config.activity_coefficient_model == ActivityCoefficientModel.davies:
                raise NotImplementedError(f"Davies model has not been implemented yet.")
        self.eq_act_coeff_phase_comp = Constraint(self.params.solute_set,
                                                  rule=rule_act_coeff_phase_comp)

    #TODO: change osmotic pressure calc
    def _pressure_osm(self):
        self.pressure_osm = Var(
            initialize=1e6,
            bounds=(5e2, 5e7),
            units=pyunits.Pa,
            doc="van't Hoff Osmotic pressure")

        def rule_pressure_osm(b):
            return (b.pressure_osm ==
                    sum(b.conc_mol_phase_comp['Liq', j] for j in self.params.solute_set)
                    * Constants.gas_constant * b.temperature)
        self.eq_pressure_osm = Constraint(rule=rule_pressure_osm)

    # -----------------------------------------------------------------------------
    # General Methods
    # NOTE: For scaling in the control volume to work properly, these methods must
    # return a pyomo Var or Expression

    def get_material_flow_terms(self, p, j):
        """Create material flow terms for control volume."""
        return self.flow_mol_phase_comp[p, j]

    # TODO: add enthalpy terms later
    # def get_enthalpy_flow_terms(self, p):
    #     """Create enthalpy flow terms."""
    #     return self.enth_flow

    # TODO: make property package compatible with dynamics
    # def get_material_density_terms(self, p, j):
    #     """Create material density terms."""

    # def get_enthalpy_density_terms(self, p):
    #     """Create enthalpy density terms."""

    def default_material_balance_type(self):
        return MaterialBalanceType.componentTotal

    # TODO: augment model with energybalance later
    # def default_energy_balance_type(self):
    #     return EnergyBalanceType.enthalpyTotal

    def get_material_flow_basis(self):
        return MaterialFlowBasis.molar

    def define_state_vars(self):
        """Define state vars."""
        return {"flow_mol_phase_comp": self.flow_mol_phase_comp,
                "temperature": self.temperature,
                "pressure": self.pressure}

    def assert_electroneutrality(self, tol=None, tee=False):
        if tol is None:
            tol = 1e-6
            for j in self.params.solute_set:
                if not self.flow_mol_phase_comp['Liq', j].is_fixed():
                    raise AssertionError(
                        f"{self.flow_mol_phase_comp['Liq', j]} was not fixed. Fix flow_mol_phase_comp for each solute"
                        f" to check that electroneutrality is satisfied.")
        val = value(sum(self.charge_comp[j] * self.flow_mol_phase_comp['Liq', j]
                    for j in self.params.solute_set))
        if abs(val) <= tol:
            if tee:
                return print('Electroneutrality satisfied')
        else:
            raise AssertionError(f"Electroneutrality condition violated. Ion concentrations should be adjusted to bring "
                                 f"the result of {val} closer towards 0.")

    # -----------------------------------------------------------------------------
    # Scaling methods
    def calculate_scaling_factors(self):
        super().calculate_scaling_factors()

        # setting scaling factors for variables

        # default scaling factors have already been set with
        # idaes.core.property_base.calculate_scaling_factors()
        # for the following variables: pressure,
        # temperature, dens_mass, visc_d_phase, diffus_phase_comp

        # these variables should have user input
        if iscale.get_scaling_factor(self.flow_mol_phase_comp['Liq', 'H2O']) is None:
            sf = iscale.get_scaling_factor(self.flow_mol_phase_comp['Liq', 'H2O'], default=1, warning=True)
            iscale.set_scaling_factor(self.flow_mol_phase_comp['Liq', 'H2O'], sf)

        for j in self.params.solute_set:
            if iscale.get_scaling_factor(self.flow_mol_phase_comp['Liq', j]) is None:
                sf = iscale.get_scaling_factor(self.flow_mol_phase_comp['Liq', j], default=1, warning=True)
                iscale.set_scaling_factor(self.flow_mol_phase_comp['Liq', j], sf)

        # scaling factors for parameters
        for j, v in self.mw_comp.items():
            if iscale.get_scaling_factor(v) is None:
                iscale.set_scaling_factor(self.mw_comp[j], 1e1)
        for ind, v in self.diffus_phase_comp.items():
            if iscale.get_scaling_factor(v) is None:
                iscale.set_scaling_factor(self.diffus_phase_comp[ind], 1e10)

        for p, v in self.dens_mass_phase.items():
            if iscale.get_scaling_factor(v) is None:
                iscale.set_scaling_factor(self.dens_mass_phase[p], 1e-2)
        for p, v in self.visc_d_phase.items():
            if iscale.get_scaling_factor(v) is None:
                iscale.set_scaling_factor(self.visc_d_phase[p], 1e3)

        if self.is_property_constructed('mole_frac_phase_comp'):
            for j in self.params.component_list:
                if iscale.get_scaling_factor(self.mole_frac_phase_comp['Liq', j]) is None:
                    if j == 'H2O':
                        iscale.set_scaling_factor(self.mole_frac_phase_comp['Liq', j], 1)
                    else:
                        sf = (iscale.get_scaling_factor(self.flow_mol_phase_comp['Liq', j])
                              / iscale.get_scaling_factor(self.flow_mol_phase_comp['Liq', 'H2O']))
                        iscale.set_scaling_factor(self.mole_frac_phase_comp['Liq', j], sf)

        if self.is_property_constructed('conc_mol_phase_comp'):
            for j in self.params.component_list:
                if iscale.get_scaling_factor(self.conc_mol_phase_comp['Liq', j]) is None:
                    sf_dens = iscale.get_scaling_factor(self.dens_mass_phase['Liq'])
                    sf = (sf_dens * iscale.get_scaling_factor(self.mole_frac_phase_comp['Liq', j], default=1)
                          / iscale.get_scaling_factor(self.mw_comp[j]))
                    iscale.set_scaling_factor(self.conc_mol_phase_comp['Liq', j], sf)

        if self.is_property_constructed('flow_mass_phase_comp'):
            for j in self.params.component_list:
                if iscale.get_scaling_factor(self.flow_mass_phase_comp['Liq', j]) is None:
                    sf = iscale.get_scaling_factor(self.flow_mol_phase_comp['Liq', j], default=1)
                    sf *= iscale.get_scaling_factor(self.mw_comp[j])
                    iscale.set_scaling_factor(self.flow_mass_phase_comp['Liq', j], sf)

        # these variables do not typically require user input,
        # will not override if the user does provide the scaling factor
        if self.is_property_constructed('pressure_osm'):
            if iscale.get_scaling_factor(self.pressure_osm) is None:
                sf = iscale.get_scaling_factor(self.pressure)
                iscale.set_scaling_factor(self.pressure_osm, sf)

        if self.is_property_constructed('mass_frac_phase_comp'):
            for j in self.params.component_list:
                comp = self.params.get_component(j)
                if iscale.get_scaling_factor(self.mass_frac_phase_comp['Liq', j]) is None:
                    if comp.is_solute():
                        sf = (iscale.get_scaling_factor(self.flow_mass_phase_comp['Liq', j], default=1)
                              / iscale.get_scaling_factor(self.flow_mass_phase_comp['Liq', 'H2O'], default=1))
                        iscale.set_scaling_factor(self.mass_frac_phase_comp['Liq', j], sf)
                    elif comp.is_solvent():
                        iscale.set_scaling_factor(self.mass_frac_phase_comp['Liq', j], 100)
                    else:
                        raise TypeError(f'comp={comp}, j = {j}')

        if self.is_property_constructed('flow_vol_phase'):
            sf = (iscale.get_scaling_factor(self.flow_mol_phase_comp['Liq', 'H2O'], default=1)
                  * iscale.get_scaling_factor(self.mw_comp[j])
                  / iscale.get_scaling_factor(self.dens_mass_phase['Liq']))
            iscale.set_scaling_factor(self.flow_vol_phase, sf)

        if self.is_property_constructed('flow_vol'):
            sf = iscale.get_scaling_factor(self.flow_vol_phase)
            iscale.set_scaling_factor(self.flow_vol, sf)

        if self.is_property_constructed('conc_mass_phase_comp'):
            for j in self.params.component_list:
                sf_dens = iscale.get_scaling_factor(self.dens_mass_phase['Liq'])
                if iscale.get_scaling_factor(self.conc_mass_phase_comp['Liq', j]) is None:
                    if j == 'H2O':
                        # solvents typically have a mass fraction between 0.5-1
                        iscale.set_scaling_factor(self.conc_mass_phase_comp['Liq', j], sf_dens)
                    else:
                        iscale.set_scaling_factor(
                            self.conc_mass_phase_comp['Liq', j],
                            sf_dens * iscale.get_scaling_factor(self.mass_frac_phase_comp['Liq', j],default=1,warning=True))


        if self.is_property_constructed('molality_comp'):
            for j in self.params.solute_set:
                if iscale.get_scaling_factor(self.molality_comp[j]) is None:
                    sf = (iscale.get_scaling_factor(self.flow_mol_phase_comp['Liq', j])
                          / iscale.get_scaling_factor(self.flow_mol_phase_comp['Liq', 'H2O'])
                          / iscale.get_scaling_factor(self.mw_comp[j]))
                    iscale.set_scaling_factor(self.molality_comp[j], sf)

        if self.is_property_constructed('act_coeff_phase_comp'):
            for j in self.params.solute_set:
                if iscale.get_scaling_factor(self.act_coeff_phase_comp['Liq', j]) is None:
                    iscale.set_scaling_factor(self.act_coeff_phase_comp['Liq', j], 1)

        # transforming constraints
        # property relationships with no index, simple constraint
        if self.is_property_constructed('pressure_osm'):
            sf = iscale.get_scaling_factor(self.pressure_osm, default=1, warning=True)
            iscale.constraint_scaling_transform(self.eq_pressure_osm, sf)

        # # property relationships with phase index, but simple constraint
        for v_str in ('flow_vol_phase', 'dens_mass_phase'):
            if self.is_property_constructed(v_str):
                v = getattr(self, v_str)
                sf = iscale.get_scaling_factor(v['Liq'], default=1, warning=True)
                c = getattr(self, 'eq_' + v_str)
                iscale.constraint_scaling_transform(c, sf)

        # property relationship indexed by component
        v_str_lst_comp = ['molality_comp']
        for v_str in v_str_lst_comp:
            if self.is_property_constructed(v_str):
                v_comp = getattr(self, v_str)
                c_comp = getattr(self, 'eq_' + v_str)
                for j, c in c_comp.items():
                    sf = iscale.get_scaling_factor(v_comp[j], default=1, warning=True)
                    iscale.constraint_scaling_transform(c, sf)

        # property relationships indexed by component and phase
        v_str_lst_phase_comp = ['mass_frac_phase_comp', 'conc_mass_phase_comp', 'flow_mass_phase_comp',
                                'mole_frac_phase_comp', 'conc_mol_phase_comp', 'act_coeff_phase_comp']
        for v_str in v_str_lst_phase_comp:
            if self.is_property_constructed(v_str):
                v_comp = getattr(self, v_str)
                c_comp = getattr(self, 'eq_' + v_str)
                for j, c in c_comp.items():
                    sf = iscale.get_scaling_factor(v_comp['Liq', j], default=1, warning=True)
                    iscale.constraint_scaling_transform(c, sf)
class PressureChangerData(UnitModelBlockData):
    """
    Standard Compressor/Expander Unit Model Class
    """

    CONFIG = UnitModelBlockData.CONFIG()

    CONFIG.declare(
        "material_balance_type",
        ConfigValue(
            default=MaterialBalanceType.useDefault,
            domain=In(MaterialBalanceType),
            description="Material balance construction flag",
            doc="""Indicates what type of mass balance should be constructed,
**default** - MaterialBalanceType.useDefault.
**Valid values:** {
**MaterialBalanceType.useDefault - refer to property package for default
balance type
**MaterialBalanceType.none** - exclude material balances,
**MaterialBalanceType.componentPhase** - use phase component balances,
**MaterialBalanceType.componentTotal** - use total component balances,
**MaterialBalanceType.elementTotal** - use total element balances,
**MaterialBalanceType.total** - use total material balance.}""",
        ),
    )
    CONFIG.declare(
        "energy_balance_type",
        ConfigValue(
            default=EnergyBalanceType.useDefault,
            domain=In(EnergyBalanceType),
            description="Energy balance construction flag",
            doc="""Indicates what type of energy balance should be constructed,
**default** - EnergyBalanceType.useDefault.
**Valid values:** {
**EnergyBalanceType.useDefault - refer to property package for default
balance type
**EnergyBalanceType.none** - exclude energy balances,
**EnergyBalanceType.enthalpyTotal** - single enthalpy balance for material,
**EnergyBalanceType.enthalpyPhase** - enthalpy balances for each phase,
**EnergyBalanceType.energyTotal** - single energy balance for material,
**EnergyBalanceType.energyPhase** - energy balances for each phase.}""",
        ),
    )
    CONFIG.declare(
        "momentum_balance_type",
        ConfigValue(
            default=MomentumBalanceType.pressureTotal,
            domain=In(MomentumBalanceType),
            description="Momentum balance construction flag",
            doc="""Indicates what type of momentum balance should be constructed,
**default** - MomentumBalanceType.pressureTotal.
**Valid values:** {
**MomentumBalanceType.none** - exclude momentum balances,
**MomentumBalanceType.pressureTotal** - single pressure balance for material,
**MomentumBalanceType.pressurePhase** - pressure balances for each phase,
**MomentumBalanceType.momentumTotal** - single momentum balance for material,
**MomentumBalanceType.momentumPhase** - momentum balances for each phase.}""",
        ),
    )
    CONFIG.declare(
        "has_phase_equilibrium",
        ConfigValue(
            default=False,
            domain=In([True, False]),
            description="Phase equilibrium construction flag",
            doc="""Indicates whether terms for phase equilibrium should be
constructed, **default** = False.
**Valid values:** {
**True** - include phase equilibrium terms
**False** - exclude phase equilibrium terms.}""",
        ),
    )
    CONFIG.declare(
        "compressor",
        ConfigValue(
            default=True,
            domain=In([True, False]),
            description="Compressor flag",
            doc="""Indicates whether this unit should be considered a
            compressor (True (default), pressure increase) or an expander
            (False, pressure decrease).""",
        ),
    )
    CONFIG.declare(
        "thermodynamic_assumption",
        ConfigValue(
            default=ThermodynamicAssumption.isothermal,
            domain=In(ThermodynamicAssumption),
            description="Thermodynamic assumption to use",
            doc="""Flag to set the thermodynamic assumption to use for the unit.
                - ThermodynamicAssumption.isothermal (default)
                - ThermodynamicAssumption.isentropic
                - ThermodynamicAssumption.pump
                - ThermodynamicAssumption.adiabatic""",
        ),
    )
    CONFIG.declare(
        "property_package",
        ConfigValue(
            default=useDefault,
            domain=is_physical_parameter_block,
            description="Property package to use for control volume",
            doc="""Property parameter object used to define property calculations,
**default** - useDefault.
**Valid values:** {
**useDefault** - use default package from parent model or flowsheet,
**PropertyParameterObject** - a PropertyParameterBlock object.}""",
        ),
    )
    CONFIG.declare(
        "property_package_args",
        ConfigBlock(
            implicit=True,
            description="Arguments to use for constructing property packages",
            doc="""A ConfigBlock with arguments to be passed to a property block(s)
and used when constructing these,
**default** - None.
**Valid values:** {
see property package for documentation.}""",
        ),
    )

    def build(self):
        """

        Args:
            None

        Returns:
            None
        """
        # Call UnitModel.build
        super(PressureChangerData, self).build()

        # Add a control volume to the unit including setting up dynamics.
        self.control_volume = ControlVolume0DBlock(
            default={
                "dynamic": self.config.dynamic,
                "has_holdup": self.config.has_holdup,
                "property_package": self.config.property_package,
                "property_package_args": self.config.property_package_args,
            }
        )

        # Add geomerty variables to control volume
        if self.config.has_holdup:
            self.control_volume.add_geometry()

        # Add inlet and outlet state blocks to control volume
        self.control_volume.add_state_blocks(
            has_phase_equilibrium=self.config.has_phase_equilibrium
        )

        # Add mass balance
        # Set has_equilibrium is False for now
        # TO DO; set has_equilibrium to True
        self.control_volume.add_material_balances(
            balance_type=self.config.material_balance_type,
            has_phase_equilibrium=self.config.has_phase_equilibrium,
        )

        # Add energy balance
        self.control_volume.add_energy_balances(
            balance_type=self.config.energy_balance_type, has_work_transfer=True
        )

        # add momentum balance
        self.control_volume.add_momentum_balances(
            balance_type=self.config.momentum_balance_type, has_pressure_change=True
        )

        # Add Ports
        self.add_inlet_port()
        self.add_outlet_port()

        # Set Unit Geometry and holdup Volume
        if self.config.has_holdup is True:
            add_object_reference(self, "volume", self.control_volume.volume)

        # Construct performance equations
        # Set references to balance terms at unit level
        # Add Work transfer variable 'work' as necessary
        add_object_reference(self, "work_mechanical", self.control_volume.work)

        # Add Momentum balance variable 'deltaP' as necessary
        add_object_reference(self, "deltaP", self.control_volume.deltaP)

        # Performance Variables
        self.ratioP = Var(
            self.flowsheet().config.time, initialize=1.0, doc="Pressure Ratio"
        )

        # Pressure Ratio
        @self.Constraint(self.flowsheet().config.time, doc="Pressure ratio constraint")
        def ratioP_calculation(b, t):
            return (
                b.ratioP[t] * b.control_volume.properties_in[t].pressure
                == b.control_volume.properties_out[t].pressure
            )

        # Construct equations for thermodynamic assumption
        if self.config.thermodynamic_assumption == ThermodynamicAssumption.isothermal:
            self.add_isothermal()
        elif self.config.thermodynamic_assumption == ThermodynamicAssumption.isentropic:
            self.add_isentropic()
        elif self.config.thermodynamic_assumption == ThermodynamicAssumption.pump:
            self.add_pump()
        elif self.config.thermodynamic_assumption == ThermodynamicAssumption.adiabatic:
            self.add_adiabatic()

    def add_pump(self):
        """
        Add constraints for the incompressible fluid assumption

        Args:
            None

        Returns:
            None
        """

        self.work_fluid = Var(
            self.flowsheet().config.time,
            initialize=1.0,
            doc="Work required to increase the pressure of the liquid",
        )
        self.efficiency_pump = Var(
            self.flowsheet().config.time, initialize=1.0, doc="Pump efficiency"
        )

        @self.Constraint(self.flowsheet().config.time, doc="Pump fluid work constraint")
        def fluid_work_calculation(b, t):
            return b.work_fluid[t] == (
                (
                    b.control_volume.properties_out[t].pressure
                    - b.control_volume.properties_in[t].pressure
                )
                * b.control_volume.properties_out[t].flow_vol
            )

        # Actual work
        @self.Constraint(
            self.flowsheet().config.time, doc="Actual mechanical work calculation"
        )
        def actual_work(b, t):
            if b.config.compressor:
                return b.work_fluid[t] == (
                    b.work_mechanical[t] * b.efficiency_pump[t]
                )
            else:
                return b.work_mechanical[t] == (
                    b.work_fluid[t] * b.efficiency_pump[t]
                )

    def add_isothermal(self):
        """
        Add constraints for isothermal assumption.

        Args:
            None

        Returns:
            None
        """
        # Isothermal constraint
        @self.Constraint(
            self.flowsheet().config.time,
            doc="For isothermal condition: Equate inlet and " "outlet temperature",
        )
        def isothermal(b, t):
            return (
                b.control_volume.properties_in[t].temperature
                == b.control_volume.properties_out[t].temperature
            )

    def add_adiabatic(self):
        """
        Add constraints for adiabatic assumption.

        Args:
            None

        Returns:
            None
        """
        # Isothermal constraint
        @self.Constraint(
            self.flowsheet().config.time,
            doc="For isothermal condition: Equate inlet and " "outlet enthalpy",
        )
        def adiabatic(b, t):
            return (
                b.control_volume.properties_in[t].enth_mol
                == b.control_volume.properties_out[t].enth_mol
            )

    def add_isentropic(self):
        """
        Add constraints for isentropic assumption.

        Args:
            None

        Returns:
            None
        """
        # Get indexing sets from control volume
        # Add isentropic variables
        self.efficiency_isentropic = Var(
            self.flowsheet().config.time,
            initialize=0.8,
            doc="Efficiency with respect to an " "isentropic process [-]",
        )
        self.work_isentropic = Var(
            self.flowsheet().config.time,
            initialize=0.0,
            doc="Work input to unit if isentropic " "process [-]",
        )

        # Build isentropic state block
        tmp_dict = dict(**self.config.property_package_args)
        tmp_dict["has_phase_equilibrium"] = self.config.has_phase_equilibrium
        tmp_dict["defined_state"] = False

        self.properties_isentropic = self.config.property_package.build_state_block(
            self.flowsheet().config.time,
            doc="isentropic properties at outlet",
            default=tmp_dict,
        )

        # Connect isentropic state block properties
        @self.Constraint(
            self.flowsheet().config.time, doc="Pressure for isentropic calculations"
        )
        def isentropic_pressure(b, t):
            return (
                b.properties_isentropic[t].pressure
                == b.control_volume.properties_out[t].pressure
            )

        # This assumes isentropic composition is the same as outlet
        self.add_state_material_balances(self.config.material_balance_type,
                                         self.properties_isentropic,
                                         self.control_volume.properties_out)

        # This assumes isentropic entropy is the same as inlet
        @self.Constraint(self.flowsheet().config.time, doc="Isentropic assumption")
        def isentropic(b, t):
            return (
                b.properties_isentropic[t].entr_mol
                == b.control_volume.properties_in[t].entr_mol
            )

        # Isentropic work
        @self.Constraint(
            self.flowsheet().config.time, doc="Calculate work of isentropic process"
        )
        def isentropic_energy_balance(b, t):
            return b.work_isentropic[t] == (
                sum(
                    b.properties_isentropic[t].get_enthalpy_flow_terms(p)
                    for p in b.config.property_package.phase_list
                )
                - sum(
                    b.control_volume.properties_in[t].get_enthalpy_flow_terms(p)
                    for p in b.config.property_package.phase_list
                )
            )

        # Actual work
        @self.Constraint(
            self.flowsheet().config.time, doc="Actual mechanical work calculation"
        )
        def actual_work(b, t):
            if b.config.compressor:
                return b.work_isentropic[t] == (
                    b.work_mechanical[t] * b.efficiency_isentropic[t]
                )
            else:
                return b.work_mechanical[t] == (
                    b.work_isentropic[t] * b.efficiency_isentropic[t]
                )

    def model_check(blk):
        """
        Check that pressure change matches with compressor argument (i.e. if
        compressor = True, pressure should increase or work should be positive)

        Args:
            None

        Returns:
            None
        """
        if blk.config.compressor:
            # Compressor
            # Check that pressure does not decrease
            if any(
                blk.deltaP[t].fixed and (value(blk.deltaP[t]) < 0.0)
                for t in blk.flowsheet().config.time
            ):
                _log.warning("{} Compressor set with negative deltaP.".format(blk.name))
            if any(
                blk.ratioP[t].fixed and (value(blk.ratioP[t]) < 1.0)
                for t in blk.flowsheet().config.time
            ):
                _log.warning(
                    "{} Compressor set with ratioP less than 1.".format(blk.name)
                )
            if any(
                blk.control_volume.properties_out[t].pressure.fixed
                and (
                    value(blk.control_volume.properties_in[t].pressure)
                    > value(blk.control_volume.properties_out[t].pressure)
                )
                for t in blk.flowsheet().config.time
            ):
                _log.warning(
                    "{} Compressor set with pressure decrease.".format(blk.name)
                )
            # Check that work is not negative
            if any(
                blk.work_mechanical[t].fixed and (value(blk.work_mechanical[t]) < 0.0)
                for t in blk.flowsheet().config.time
            ):
                _log.warning(
                    "{} Compressor maybe set with negative work.".format(blk.name)
                )
        else:
            # Expander
            # Check that pressure does not increase
            if any(
                blk.deltaP[t].fixed and (value(blk.deltaP[t]) > 0.0)
                for t in blk.flowsheet().config.time
            ):
                _log.warning(
                    "{} Expander/turbine set with positive deltaP.".format(blk.name)
                )
            if any(
                blk.ratioP[t].fixed and (value(blk.ratioP[t]) > 1.0)
                for t in blk.flowsheet().config.time
            ):
                _log.warning(
                    "{} Expander/turbine set with ratioP greater "
                    "than 1.".format(blk.name)
                )
            if any(
                blk.control_volume.properties_out[t].pressure.fixed
                and (
                    value(blk.control_volume.properties_in[t].pressure)
                    < value(blk.control_volume.properties_out[t].pressure)
                )
                for t in blk.flowsheet().config.time
            ):
                _log.warning(
                    "{} Expander/turbine maybe set with pressure ",
                    "increase.".format(blk.name),
                )
            # Check that work is not positive
            if any(
                blk.work_mechanical[t].fixed and (value(blk.work_mechanical[t]) > 0.0)
                for t in blk.flowsheet().config.time
            ):
                _log.warning(
                    "{} Expander/turbine set with positive work.".format(blk.name)
                )

        # Run holdup block model checks
        blk.control_volume.model_check()

        # Run model checks on isentropic property block
        try:
            for t in blk.flowsheet().config.time:
                blk.properties_in[t].model_check()
        except AttributeError:
            pass

    def initialize(
        blk,
        state_args=None,
        routine=None,
        outlvl=idaeslog.NOTSET,
        solver="ipopt",
        optarg={"tol": 1e-6},
    ):
        """
        General wrapper for pressure changer initialization routines

        Keyword Arguments:
            routine : str stating which initialization routine to execute
                        * None - use routine matching thermodynamic_assumption
                        * 'isentropic' - use isentropic initialization routine
                        * 'isothermal' - use isothermal initialization routine
            state_args : a dict of arguments to be passed to the property
                         package(s) to provide an initial state for
                         initialization (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 whcih solver to use during
                     initialization (default = 'ipopt')

        Returns:
            None
        """
        # if costing block exists, deactivate
        try:
            blk.costing.deactivate()
        except AttributeError:
            pass

        if routine is None:
            # Use routine for specific type of unit
            routine = blk.config.thermodynamic_assumption

        # Call initialization routine
        if routine is ThermodynamicAssumption.isentropic:
            blk.init_isentropic(
                state_args=state_args, outlvl=outlvl, solver=solver, optarg=optarg
            )
        else:
            # Call the general initialization routine in UnitModelBlockData
            super(PressureChangerData, blk).initialize(
                state_args=state_args, outlvl=outlvl, solver=solver, optarg=optarg
            )
        # if costing block exists, activate
        try:
            blk.costing.activate()
        except AttributeError:
            pass

    def init_isentropic(blk, state_args, outlvl, solver, optarg):
        """
        Initialization routine for unit (default solver ipopt)

        Keyword Arguments:
            state_args : a dict of arguments to be passed to the property
                         package(s) to provide an initial state for
                         initialization (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 whcih solver to use during
                     initialization (default = 'ipopt')

        Returns:
            None
        """
        init_log = idaeslog.getInitLogger(blk.name, outlvl, tag="unit")
        solve_log = idaeslog.getSolveLogger(blk.name, outlvl, tag="unit")
        # Set solver options
        opt = SolverFactory(solver)
        opt.options = optarg

        # ---------------------------------------------------------------------
        # Initialize holdup block
        flags = blk.control_volume.initialize(
            outlvl=outlvl,
            optarg=optarg,
            solver=solver,
            state_args=state_args,
        )
        init_log.info_high("Initialization Step 1 Complete.")
        # ---------------------------------------------------------------------
        # Initialize Isentropic block

        # Set state_args from inlet state
        if state_args is None:
            state_args = {}
            state_dict = blk.control_volume.properties_in[
                blk.flowsheet().config.time.first()
            ].define_port_members()

            for k in state_dict.keys():
                if state_dict[k].is_indexed():
                    state_args[k] = {}
                    for m in state_dict[k].keys():
                        state_args[k][m] = state_dict[k][m].value
                else:
                    state_args[k] = state_dict[k].value

        blk.properties_isentropic.initialize(
            outlvl=outlvl,
            optarg=optarg,
            solver=solver,
            state_args=state_args,
        )

        init_log.info_high("Initialization Step 2 Complete.")

        # ---------------------------------------------------------------------
        # Solve for isothermal conditions
        if isinstance(
            blk.properties_isentropic[blk.flowsheet().config.time.first()].temperature,
            Var,
        ):
            blk.properties_isentropic[:].temperature.fix()
        elif isinstance(
            blk.properties_isentropic[blk.flowsheet().config.time.first()].enth_mol,
            Var,
        ):
            blk.properties_isentropic[:].enth_mol.fix()
        elif isinstance(
            blk.properties_isentropic[blk.flowsheet().config.time.first()].temperature,
            Expression,
        ):
            def tmp_rule(b, t):
                return blk.properties_isentropic[t].temperature == \
                    blk.control_volume.properties_in[t].temperature
            blk.tmp_init_constraint = Constraint(
                blk.flowsheet().config.time, rule=tmp_rule)

        blk.isentropic.deactivate()


        with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
            res = opt.solve(blk, tee=slc.tee)
        init_log.info_high("Initialization Step 3 {}.".format(idaeslog.condition(res)))

        if isinstance(
            blk.properties_isentropic[blk.flowsheet().config.time.first()].temperature,
            Var,
        ):
            blk.properties_isentropic[:].temperature.unfix()
        elif isinstance(
            blk.properties_isentropic[blk.flowsheet().config.time.first()].enth_mol,
            Var,
        ):
            blk.properties_isentropic[:].enth_mol.unfix()
        elif isinstance(
            blk.properties_isentropic[blk.flowsheet().config.time.first()].temperature,
            Expression,
        ):
            blk.del_component(blk.tmp_init_constraint)

        blk.isentropic.activate()

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

        # ---------------------------------------------------------------------
        # Release Inlet state
        blk.control_volume.release_state(flags, outlvl + 1)
        init_log.info(
            "Initialization Complete: {}".format(idaeslog.condition(res))
        )

    def _get_performance_contents(self, time_point=0):
        var_dict = {}
        if hasattr(self, "deltaP"):
            var_dict["Mechanical Work"] = self.work_mechanical[time_point]
        if hasattr(self, "deltaP"):
            var_dict["Pressure Change"] = self.deltaP[time_point]
        if hasattr(self, "ratioP"):
            var_dict["Pressure Ratio"] = self.ratioP[time_point]
        if hasattr(self, "efficiency_pump"):
            var_dict["Efficiency"] = self.efficiency_pump[time_point]
        if hasattr(self, "efficiency_isentropic"):
            var_dict["Isentropic Efficiency"] = \
                self.efficiency_isentropic[time_point]

        return {"vars": var_dict}

    def get_costing(self, module=costing, Mat_factor="stain_steel",
                    mover_type="compressor",
                    compressor_type="centrifugal",
                    driver_mover_type="electrical_motor",
                    pump_type="centrifugal",
                    pump_type_factor='1.4',
                    pump_motor_type_factor='open',
                    year=None):
        if not hasattr(self.flowsheet(), "costing"):
            self.flowsheet().get_costing(year=year)

        self.costing = Block()
        module.pressure_changer_costing(self.costing, Mat_factor=Mat_factor,
                                        mover_type=mover_type,
                                        compressor_type=compressor_type,
                                        driver_mover_type=driver_mover_type,
                                        pump_type=pump_type,
                                        pump_type_factor=pump_type_factor,
                                        pump_motor_type_factor=pump_motor_type_factor)

    def calculate_scaling_factors(self):
        super().calculate_scaling_factors()

        if hasattr(self, "work_fluid"):
            for t, v in self.work_fluid.items():
                iscale.set_scaling_factor(
                    v,
                    iscale.get_scaling_factor(
                        self.control_volume.work[t],
                        default=1,
                        warning=True))

        if hasattr(self, "work_mechanical"):
            for t, v in self.work_mechanical.items():
                iscale.set_scaling_factor(
                    v,
                    iscale.get_scaling_factor(
                        self.control_volume.work[t],
                        default=1,
                        warning=True))

        if hasattr(self, "work_isentropic"):
            for t, v in self.work_isentropic.items():
                iscale.set_scaling_factor(
                    v,
                    iscale.get_scaling_factor(
                        self.control_volume.work[t],
                        default=1,
                        warning=True))

        if hasattr(self, "ratioP_calculation"):
            for t, c in self.ratioP_calculation.items():
                iscale.constraint_scaling_transform(
                    c,
                    iscale.get_scaling_factor(
                        self.control_volume.properties_in[t].pressure,
                        default=1,
                        warning=True))

        if hasattr(self, "fluid_work_calculation"):
            for t, c in self.fluid_work_calculation.items():
                iscale.constraint_scaling_transform(
                    c,
                    iscale.get_scaling_factor(
                        self.control_volume.deltaP[t],
                        default=1,
                        warning=True))

        if hasattr(self, "actual_work"):
            for t, c in self.actual_work.items():
                iscale.constraint_scaling_transform(
                    c,
                    iscale.get_scaling_factor(
                        self.control_volume.work[t],
                        default=1,
                        warning=True))

        if hasattr(self, "adiabatic"):
            for t, c in self.adiabatic.items():
                iscale.constraint_scaling_transform(
                    c,
                    iscale.get_scaling_factor(
                        self.control_volume.properties_in[t].enth_mol,
                        default=1,
                        warning=True))

        if hasattr(self, "isentropic_pressure"):
            for t, c in self.isentropic_pressure.items():
                iscale.constraint_scaling_transform(
                    c,
                    iscale.get_scaling_factor(
                        self.control_volume.properties_in[t].pressure,
                        default=1,
                        warning=True))

        if hasattr(self, "isentropic"):
            for t, c in self.isentropic.items():
                iscale.constraint_scaling_transform(
                    c,
                    iscale.get_scaling_factor(
                        self.control_volume.properties_in[t].entr_mol,
                        default=1,
                        warning=True))

        if hasattr(self, "isentropic_energy_balance"):
            for t, c in self.isentropic_energy_balance.items():
                iscale.constraint_scaling_transform(
                    c,
                    iscale.get_scaling_factor(
                        self.control_volume.work[t],
                        default=1,
                        warning=True))
Пример #8
0
class FlueGasStateBlockData(StateBlockData):
    """
    This is an example of a property package for calculating the thermophysical
    properties of flue gas using the ideal gas assumption.
    """

    def build(self):
        """
        Callable method for Block construction
        """
        super(FlueGasStateBlockData, self).build()
        comps = self.params.component_list
        # Add state variables
        self.flow_mol_comp = Var(
            comps,
            domain=Reals,
            initialize=1.0,
            bounds=(0, 1e6),
            doc='Component molar flowrate [mol/s]',
            units=pyunits.mol/pyunits.s
        )
        self.pressure = Var(
            domain=Reals,
            initialize=1.01325e5,
            bounds=(1, 5e7),
            doc='State pressure [Pa]',
            units=pyunits.Pa
        )
        self.temperature = Var(
            domain=Reals,
            initialize=500,
            bounds=(200, 1500),
            doc='State temperature [K]',
            units=pyunits.K
        )

        # Add expressions for some basic oft-used quantiies
        self.flow_mol = Expression(
            expr=sum(self.flow_mol_comp[j] for j in comps))

        def rule_mole_frac(b, c):
            return b.flow_mol_comp[c] / b.flow_mol
        self.mole_frac_comp = Expression(
            comps,
            rule=rule_mole_frac,
            doc='mole fraction of component i'
        )

        self.flow_mass = Expression(
            expr=sum(self.flow_mol_comp[j] * self.params.mw_comp[j]
                     for j in comps),
            doc='total mass flow')

        def rule_mw_comp(b, j):
            return b.params.mw_comp[j]
        self.mw_comp = Expression(comps, rule=rule_mw_comp)

        def rule_mw(b):
            return sum(b.mw_comp[j] * b.mole_frac_comp[j] for j in comps)
        self.mw = Expression(rule=rule_mw)

        self.pressure_crit = Expression(
            expr=sum(
                self.params.pressure_crit[j] *
                self.mole_frac_comp[j] for j in comps))
        self.temperature_crit = Expression(
            expr=sum(
                self.params.temperature_crit[j] *
                self.mole_frac_comp[j] for j in comps))
        self.pressure_red = Expression(
            expr=self.pressure / self.pressure_crit)
        self.temperature_red = Expression(
            expr=self.temperature / self.temperature_crit)

        self.compress_fact = Expression(
            expr=1.0, doc='Vapor Compressibility Factor')

        def rule_dens_mol_phase(b, p):
            return b.pressure / b.compress_fact / \
                constants.Constants.gas_constant / b.temperature
        self.dens_mol_phase = Expression(
            self.params.phase_list,
            rule=rule_dens_mol_phase,
            doc='Molar Density')

        self.flow_vol = Expression(
            doc='Volumetric Flowrate',
            expr=self.flow_mol / self.dens_mol_phase["Vap"])

    def _heat_cap_calc(self):
        # heat capacity J/mol-K
        self.cp_mol = Var(initialize=1000,
                          doc='heat capacity [J/mol-K]',
                          units=pyunits.J/pyunits.mol/pyunits.K)

        def rule_cp_phase(b, p):
            # This property module only has one phase
            return self.cp_mol
        self.cp_mol_phase = Expression(self.params.phase_list,
                                       rule=rule_cp_phase)

        try:
            ft = sum(self.flow_mol_comp[j] for j in self.params.component_list)
            t = pyunits.convert(self.temperature, to_units=pyunits.kK)
            self.heat_cap_correlation = Constraint(expr=(
                self.cp_mol * ft ==
                sum(self.flow_mol_comp[j] * (
                    self.params.cp_mol_ig_comp_coeff_A[j] +
                    self.params.cp_mol_ig_comp_coeff_B[j] * t +
                    self.params.cp_mol_ig_comp_coeff_C[j] * t**2 +
                    self.params.cp_mol_ig_comp_coeff_D[j] * t**3 +
                    self.params.cp_mol_ig_comp_coeff_E[j] / t**2)
                    for j in self.params.component_list)))
        except AttributeError:
            self.del_component(self.cp_mol)
            self.del_component(self.heat_cap_correlation)

    def _enthalpy_calc(self):
        self.enth_mol = Var(doc='Specific Enthalpy [J/mol]',
                            units=pyunits.J/pyunits.mol)

        def rule_enth_phase(b, p):
            # This property module only has one phase
            return self.enth_mol
        self.enth_mol_phase = Expression(
            self.params.phase_list, rule=rule_enth_phase)

        def enthalpy_correlation(b):
            ft = sum(self.flow_mol_comp[j] for j in self.params.component_list)
            t = pyunits.convert(self.temperature, to_units=pyunits.kK)
            return self.enth_mol * ft == sum(
                self.flow_mol_comp[j] * pyunits.convert(
                        self.params.cp_mol_ig_comp_coeff_A[j] * t +
                        self.params.cp_mol_ig_comp_coeff_B[j] * t**2 / 2 +
                        self.params.cp_mol_ig_comp_coeff_C[j] * t**3 / 3 +
                        self.params.cp_mol_ig_comp_coeff_D[j] * t**4 / 4 -
                        self.params.cp_mol_ig_comp_coeff_E[j] / t +
                        self.params.cp_mol_ig_comp_coeff_F[j],
                        to_units=pyunits.J/pyunits.mol)
                for j in self.params.component_list)
            # NOTE: the H term (from the Shomate Equation) is not
            # included here so that the reference state enthalpy is the
            # enthalpy of formation (not 0).
        try:
            self.enthalpy_correlation = Constraint(rule=enthalpy_correlation)
        except AttributeError:
            self.del_component(self.enth_mol_phase)
            self.del_component(self.enth_mol)
            self.del_component(self.enthalpy_correlation)

    def _entropy_calc(self):
        self.entr_mol = Var(doc='Specific Entropy [J/mol/K]',
                            units=pyunits.J/pyunits.mol/pyunits.K)
        # Specific Entropy

        def rule_entr_phase(b, p):
            # This property module only has one phase
            return self.entr_mol
        self.entr_mol_phase = Expression(
            self.params.phase_list, rule=rule_entr_phase)

        def entropy_correlation(b):
            ft = sum(self.flow_mol_comp[j] for j in self.params.component_list)
            t = pyunits.convert(self.temperature, to_units=pyunits.kK)
            n = self.flow_mol_comp
            x = self.mole_frac_comp
            p = self.pressure
            r_gas = constants.Constants.gas_constant
            return (self.entr_mol + r_gas * log(p/1e5)) * ft == \
                sum(n[j] * (
                    self.params.cp_mol_ig_comp_coeff_A[j]*log(t) +
                    self.params.cp_mol_ig_comp_coeff_B[j]*t +
                    self.params.cp_mol_ig_comp_coeff_C[j]*t**2 / 2 +
                    self.params.cp_mol_ig_comp_coeff_D[j]*t**3 / 3 -
                    self.params.cp_mol_ig_comp_coeff_E[j]/t**2 / 2 +
                    self.params.cp_mol_ig_comp_coeff_G[j] +
                    r_gas * log(x[j])) for j in self.params.component_list)

        try:
            self.entropy_correlation = Constraint(rule=entropy_correlation)
        except AttributeError:
            self.del_component(self.entr_mol_phase)
            self.del_component(self.entropy_correlation)

    def _therm_cond(self):
        comps = self.params.component_list
        self.therm_cond_comp = Var(
            comps,
            initialize=0.05,
            doc='thermal conductivity J/m-K-s',
            units=pyunits.J/pyunits.m/pyunits.K/pyunits.s)
        self.therm_cond = Var(
            initialize=0.05,
            doc='thermal conductivity of gas mixture J/m-K-s',
            units=pyunits.J/pyunits.m/pyunits.K/pyunits.s)
        self.visc_d_comp = Var(
            comps,
            initialize=2e-5,
            doc='dynamic viscocity of pure gas species',
            units=pyunits.kg/pyunits.m/pyunits.s)
        self.visc_d = Var(
            initialize=2e-5, doc='viscosity of gas mixture kg/m-s',
            units=pyunits.kg/pyunits.m/pyunits.s)

        try:
            def rule_therm_cond(b, c):
                t = pyunits.convert(b.temperature, to_units=pyunits.kK)
                return b.therm_cond_comp[c] == (
                    ((b.params.cp_mol_ig_comp_coeff_A[c] +
                      b.params.cp_mol_ig_comp_coeff_B[c]*t +
                      b.params.cp_mol_ig_comp_coeff_C[c]*t**2 +
                      b.params.cp_mol_ig_comp_coeff_D[c]*t**3 +
                      b.params.cp_mol_ig_comp_coeff_E[c]/t**2) /
                     b.params.mw_comp[c]) +
                    1.25 *
                    (constants.Constants.gas_constant /
                     b.params.mw_comp[c])) * b.visc_d_comp[c]
            self.therm_cond_con = Constraint(comps, rule=rule_therm_cond)

            def rule_theta(b, c):
                return b.temperature / b.params.ep_Kappa[c]
            self.theta = Expression(comps, rule=rule_theta)

            def rule_omega(b, c):
                return (1.5794145
                        + 0.00635771 * b.theta[c]
                        - 0.7314 * log(b.theta[c])
                        + 0.2417357 * log(b.theta[c])**2
                        - 0.0347045 * log(b.theta[c])**3)
            self.omega = Expression(comps, rule=rule_omega)

            # Pure gas viscocity - from Chapman-Enskog theory
            def rule_visc_d(b, c):
                return (pyunits.convert(
                    b.visc_d_comp[c],
                    to_units=pyunits.g/pyunits.cm/pyunits.s) *
                    b.params.sigma[c]**2 * b.omega[c] ==
                    b.params.ce_param * sqrt(
                        pyunits.convert(b.params.mw_comp[c],
                                        to_units=pyunits.g/pyunits.mol) *
                        b.temperature))
            self.visc_d_con = Constraint(comps, rule=rule_visc_d)

            # section to calculate viscosity of gas mixture
            def rule_phi(b, i, j):
                return (
                    1/2.8284 *
                    (1 + (b.params.mw_comp[i] / b.params.mw_comp[j]))**(-0.5) *
                    (1 + sqrt(b.visc_d_comp[i] / b.visc_d_comp[j]) *
                     (b.params.mw_comp[j] / b.params.mw_comp[i])**0.25)**2)
            self.phi_ij = Expression(
                comps,
                comps,
                rule=rule_phi
            )

            # viscosity of Gas mixture kg/m-s
            def rule_visc_d_mix(b):
                return b.visc_d == sum(
                    (b.mole_frac_comp[i] * b.visc_d_comp[i])
                    / sum(b.mole_frac_comp[j] * b.phi_ij[i, j] for j in comps)
                    for i in comps)
            self.vis_d_mix_con = Constraint(rule=rule_visc_d_mix)

            # thermal conductivity of gas mixture in kg/m-s
            def rule_therm_mix(b):
                return b.therm_cond == sum(
                    (b.mole_frac_comp[i] * b.therm_cond_comp[i])
                    / sum(b.mole_frac_comp[j] * b.phi_ij[i, j] for j in comps)
                    for i in comps)
            self.therm_mix_con = Constraint(rule=rule_therm_mix)

        except AttributeError:
            self.del_component(self.therm_cond_comp)
            self.del_component(self.therm_cond)
            self.del_component(self.visc_d_comp)
            self.del_component(self.visc_d)
            self.del_component(self.omega)
            self.del_component(self.theta)
            self.del_component(self.phi_ij)
            self.del_component(self.sigma)
            self.del_component(self.ep_Kappa)
            self.del_component(self.therm_cond_con)
            self.del_component(self.theta_con)
            self.del_component(self.omega_con)
            self.del_component(self.visc_d_con)
            self.del_component(self.phi_con)

    def default_material_balance_type(self):
        return MaterialBalanceType.componentTotal

    def default_energy_balance_type(self):
        return EnergyBalanceType.enthalpyTotal

    def get_material_flow_terms(self, p, j):
        return self.flow_mol_comp[j]

    def get_material_flow_basis(self):
        return MaterialFlowBasis.molar

    def get_enthalpy_flow_terms(self, p):
        if not self.is_property_constructed("enthalpy_flow_terms"):
            try:
                def rule_enthalpy_flow_terms(b, p):
                    return self.enth_mol_phase[p] * self.flow_mol
                self.enthalpy_flow_terms = Expression(
                    self.params.phase_list,
                    rule=rule_enthalpy_flow_terms
                )
            except AttributeError:
                self.del_component(self.enthalpy_flow_terms)
        return self.enthalpy_flow_terms[p]

    def get_material_density_terms(self, p, j):
        return self.dens_mol_phase[p]

    def get_energy_density_terms(self, p):
        if not self.is_property_constructed("energy_density_terms"):
            try:
                def rule_energy_density_terms(b, p):
                    return self.enth_mol_phase[p] * \
                        self.dens_mol_phase[p] - self.pressure
                self.energy_density_terms = Expression(
                    self.params.phase_list,
                    rule=rule_energy_density_terms
                )
            except AttributeError:
                self.del_component(self.energy_density_terms)
        return self.energy_density_terms[p]

    def define_state_vars(self):
        return {
            "flow_mol_comp": self.flow_mol_comp,
            "temperature": self.temperature,
            "pressure": self.pressure
        }

    def model_check(self):
        """
        Model checks for property block
        """
        # Check temperature bounds
        for v in self.compoent_object_data(Var, descend_into=True):
            if value(v) < v.lb:
                _log.error(f"{v} is below lower bound in {self.name}")
            if value(v) > v.ub:
                _log.error(f"{v} is above upper bound in {self.name}")

    def calculate_scaling_factors(self):
        super().calculate_scaling_factors()

        # Get some scale factors that are frequently used to calculate others
        sf_flow = iscale.get_scaling_factor(self.flow_mol)
        sf_mol_fraction = {}
        comps = self.params.component_list
        for i in comps:
            sf_mol_fraction[i] = iscale.get_scaling_factor(self.mole_frac_comp[i])
        # calculate flow_mol_comp scale factors
        for i, c in self.flow_mol_comp.items():
            iscale.set_scaling_factor(c, sf_flow * sf_mol_fraction[i])

        if self.is_property_constructed("energy_density_terms"):
            for i, c in self.energy_density_terms.items():
                sf1 = iscale.get_scaling_factor(self.enth_mol_phase[i])
                sf2 = iscale.get_scaling_factor(self.dens_mol_phase[i])
                iscale.set_scaling_factor(c, sf1 * sf2)

        if self.is_property_constructed("enthalpy_flow_terms"):
            for i, c in self.enthalpy_flow_terms.items():
                sf1 = iscale.get_scaling_factor(self.enth_mol_phase[i])
                sf2 = iscale.get_scaling_factor(self.flow_mol)
                iscale.set_scaling_factor(c, sf1 * sf2)

        if self.is_property_constructed("heat_cap_correlation"):
            iscale.constraint_scaling_transform(
                self.heat_cap_correlation,
                iscale.get_scaling_factor(self.cp_mol) *
                iscale.get_scaling_factor(self.flow_mol),
                overwrite=False
            )
        if self.is_property_constructed("enthalpy_correlation"):
            for p, c in self.enthalpy_correlation.items():
                iscale.constraint_scaling_transform(
                    c,
                    iscale.get_scaling_factor(self.enth_mol) *
                    iscale.get_scaling_factor(self.flow_mol),
                    overwrite=False
                )
        if self.is_property_constructed("entropy_correlation"):
            iscale.constraint_scaling_transform(
                self.entropy_correlation,
                iscale.get_scaling_factor(self.entr_mol) *
                iscale.get_scaling_factor(self.flow_mol),
                overwrite=False
            )
        if self.is_property_constructed("vapor_pressure_correlation"):
            iscale.constraint_scaling_transform(
                self.vapor_pressure_correlation,
                log(iscale.get_scaling_factor(self.pressure_sat)) *
                iscale.get_scaling_factor(self.flow_mol),
                overwrite=False
            )
        if self.is_property_constructed("therm_cond_con"):
            for i, c in self.therm_cond_con.items():
                iscale.constraint_scaling_transform(
                    c,
                    iscale.get_scaling_factor(self.therm_cond_comp[i]),
                    overwrite=False)
        if self.is_property_constructed("therm_mix_con"):
            iscale.constraint_scaling_transform(
                self.therm_mix_con,
                iscale.get_scaling_factor(self.therm_cond),
                overwrite=False)
        if self.is_property_constructed("visc_d_con"):
            for i, c in self.visc_d_con.items():
                iscale.constraint_scaling_transform(
                    c,
                    iscale.get_scaling_factor(self.visc_d_comp[i]),
                    overwrite=False)
        if self.is_property_constructed("visc_d_mix_con"):
            iscale.constraint_scaling_transform(
                self.visc_d_mix_con,
                iscale.get_scaling_factor(self.visc_d),
                overwrite=False)
Пример #9
0
class FeedZOData(FeedData):
    """
    Zero-Order feed block.
    """

    CONFIG = FeedData.CONFIG()

    def build(self):
        super().build()

        units = self.config.property_package.get_metadata().get_derived_units
        comp_list = self.config.property_package.solute_set

        self.flow_vol = Var(self.flowsheet().time,
                            initialize=1,
                            units=units("volume") / units("time"),
                            doc="Volumetric flowrate in feed")

        self.conc_mass_comp = Var(self.flowsheet().time,
                                  comp_list,
                                  initialize=1,
                                  units=units("density_mass"),
                                  doc="Component mass concentrations")

        def rule_Q(blk, t):
            return (self.flow_vol[t] * self.properties[t].dens_mass == sum(
                self.properties[t].flow_mass_comp[j]
                for j in self.properties[t].component_list))

        self.flow_vol_constraint = Constraint(self.flowsheet().time,
                                              rule=rule_Q)

        def rule_C(blk, t, j):
            return (self.conc_mass_comp[t, j] *
                    sum(self.properties[t].flow_mass_comp[k]
                        for k in self.properties[t].component_list) ==
                    self.properties[t].flow_mass_comp[j] *
                    self.properties[t].dens_mass)

        self.conc_mass_constraint = Constraint(self.flowsheet().time,
                                               comp_list,
                                               rule=rule_C)

    def load_feed_data_from_database(self, overwrite=False):
        """
        Method to load initial flowrate and concentrations from database.

        Args:
            overwrite - (default = False), indicates whether fixed values
                        should be overwritten by values from database or not.

        Returns:
            None

        Raises:
            KeyError if flowrate or concentration values not defined in data
        """
        # Get database and water source from property package
        db = self.config.property_package.config.database
        water_source = self.config.property_package.config.water_source

        # Get feed data from database
        data = db.get_source_data(water_source)

        for t in self.flowsheet().time:
            if overwrite or not self.flow_vol[t].fixed:
                try:
                    val = data["default_flow"]["value"]
                    units = getattr(pyunits, data["default_flow"]["units"])
                    self.flow_vol[t].fix(val * units)
                except KeyError:
                    _log.info(
                        f"{self.name} no default flowrate was defined "
                        f"in database water source. Value was not fixed.")

        for (t, j), v in self.conc_mass_comp.items():
            if overwrite or not v.fixed:
                try:
                    val = data["solutes"][j]["value"]
                    units = getattr(pyunits, data["solutes"][j]["units"])
                    v.fix(val * units)
                except KeyError:
                    _log.info(f"{self.name} component {j} was not defined in "
                              f"database water source. Value was not fixed.")

        # Set initial values for mass flows in properties based on these
        # Assuming density of 1000
        for t in self.flowsheet().time:
            for j in self.properties[t].params.solute_set:
                self.properties[t].flow_mass_comp[j].set_value(
                    value(self.flow_vol[t] * self.conc_mass_comp[t, j]))
            self.properties[t].flow_mass_comp["H2O"].set_value(
                value(self.flow_vol[t] * 1000))

    def initialize(blk,
                   state_args=None,
                   outlvl=idaeslog.NOTSET,
                   solver=None,
                   optarg=None):
        '''
        This method calls the initialization method of the Feed state block.

        Keyword Arguments:
            state_args : a dict of arguments to be passed to the property
                           package(s) to provide an initial state for
                           initialization (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)

        Returns:
            None
        '''
        # ---------------------------------------------------------------------
        init_log = idaeslog.getInitLogger(blk.name, outlvl, tag="unit")
        solve_log = idaeslog.getSolveLogger(blk.name, outlvl, tag="unit")

        if optarg is None:
            optarg = {}

        opt = get_solver(solver, optarg)

        if state_args is None:
            state_args = {}

        # Initialize state block
        blk.properties.initialize(outlvl=outlvl,
                                  optarg=optarg,
                                  solver=solver,
                                  state_args=state_args)

        with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
            res = opt.solve(blk, tee=slc.tee)
        init_log.info("Initialization complete: {}.".format(
            idaeslog.condition(res)))

        if not check_optimal_termination(res):
            raise InitializationError(
                f"{blk.name} failed to initialize successfully. Please check "
                f"the output logs for more information.")
Пример #10
0
class HydrogenTankData(UnitModelBlockData):
    """
    Simple hydrogen tank model.
    Unit model to store or supply compressed hydrogen.
    
    """
    CONFIG = ConfigBlock()

    # This model is based on steady state material & energy balances.
    # The accumulation term is computed based on the tank state at
    # previous time step. Thus, dynamic option is turned off.
    # However, a dynamic analysis can be performed by creating
    # an instance of this model for every time step.
    CONFIG.declare(
        "dynamic",
        ConfigValue(domain=In([False]),
                    default=False,
                    description="Dynamic model flag - must be False",
                    doc="""Indicats if Hydrogen tank model is dynamic,
**default** = False. Equilibrium Reactors do not support dynamic behavior."""))
    CONFIG.declare(
        "has_holdup",
        ConfigValue(
            default=False,
            domain=In([False]),
            description="Holdup construction flag - must be False",
            doc="""Indicates whether holdup terms should be constructed or not.
**default** - False. Hydrogen tank model uses custom equations for holdup."""))
    CONFIG.declare(
        "momentum_balance_type",
        ConfigValue(
            default=MomentumBalanceType.pressureTotal,
            domain=In(MomentumBalanceType),
            description="Momentum balance construction flag",
            doc="""Indicates what type of momentum balance should be constructed,
**default** - MomentumBalanceType.pressureTotal.
**Valid values:** {
**MomentumBalanceType.none** - exclude momentum balances,
**MomentumBalanceType.pressureTotal** - single pressure balance for material,
**MomentumBalanceType.pressurePhase** - pressure balances for each phase,
**MomentumBalanceType.momentumTotal** - single momentum balance for material,
**MomentumBalanceType.momentumPhase** - momentum balances for each phase.}"""))
    CONFIG.declare(
        "property_package",
        ConfigValue(
            default=useDefault,
            domain=is_physical_parameter_block,
            description="Property package to use for control volume",
            doc=
            """Property parameter object used to define property calculations,
**default** - useDefault.
**Valid values:** {
**useDefault** - use default package from parent model or flowsheet,
**PhysicalParameterObject** - a PhysicalParameterBlock object.}"""))
    CONFIG.declare(
        "property_package_args",
        ConfigBlock(
            implicit=True,
            description="Arguments to use for constructing property packages",
            doc=
            """A ConfigBlock with arguments to be passed to a property block(s)
and used when constructing these,
**default** - None.
**Valid values:** {
see property package for documentation.}"""))

    def build(self):
        """Building model
        Args:
            None
        Returns:
            None
        """
        super().build()

        # Build Control Volume
        self.control_volume = ControlVolume0DBlock(
            default={
                "dynamic": self.config.dynamic,
                "property_package": self.config.property_package,
                "property_package_args": self.config.property_package_args
            })

        # add inlet and outlet states
        self.control_volume.add_state_blocks(has_phase_equilibrium=False)

        # add tank volume
        self.control_volume.add_geometry()

        # add phase fractions
        self.control_volume._add_phase_fractions()

        # add pressure balance
        self.control_volume.add_momentum_balances(
            balance_type=self.config.momentum_balance_type,
            has_pressure_change=True)

        # add a state block 'previous_state' for the storage tank
        # this state block is needed to compute material and energy holdup
        # at previous or the starting time step using the property package
        # for a given P_prev and T_prev
        # NOTE: there is no flow in the previous state so,
        # flow_mol state variable is fixed to 0
        self.previous_state = (self.config.property_package.build_state_block(
            self.flowsheet().config.time, doc="Tank state at previous time"))

        # previous state should not have any flow
        self.previous_state[:].flow_mol.fix(0)

        # add local lists for easy use
        phase_list = self.control_volume.properties_in.phase_list
        pc_set = self.control_volume.properties_in.phase_component_set
        component_list = self.control_volume.properties_in.component_list

        # Get units from property package
        units = self.config.property_package.\
            get_metadata().get_derived_units
        if (self.control_volume.properties_in[
                self.flowsheet().config.time.first()].get_material_flow_basis(
                ) == MaterialFlowBasis.molar):
            flow_units = units("flow_mole")
            material_units = units("amount")
        elif (self.control_volume.properties_in[
                self.flowsheet().config.time.first()].get_material_flow_basis(
                ) == MaterialFlowBasis.mass):
            flow_units = units("flow_mass")
            material_units = units("mass")

        # Add Inlet and Outlet Ports
        self.add_inlet_port()
        self.add_outlet_port()

        # Define Vars for Tank volume calculations
        self.tank_diameter = Var(self.flowsheet().config.time,
                                 within=NonNegativeReals,
                                 initialize=1.0,
                                 doc="Diameter of storage tank in m",
                                 units=units("length"))

        self.tank_length = Var(self.flowsheet().config.time,
                               within=NonNegativeReals,
                               initialize=1.0,
                               doc="Length of storage tank in m",
                               units=units("length"))

        # Tank volume calculation
        @self.Constraint(self.flowsheet().config.time)
        def volume_cons(b, t):
            return (b.control_volume.volume[t] == const.pi * b.tank_length[t] *
                    ((b.tank_diameter[t] / 2)**2))

        # define Vars for the model
        self.dt = Var(self.flowsheet().config.time,
                      domain=NonNegativeReals,
                      initialize=100,
                      doc="Time step for holdup calculation",
                      units=units("time"))

        self.heat_duty = Var(
            self.flowsheet().config.time,
            domain=Reals,
            initialize=0.0,
            doc="Heat transferred from surroundings, 0 for adiabatic",
            units=units("power"))

        self.material_accumulation = Var(
            self.flowsheet().config.time,
            pc_set,
            within=Reals,
            initialize=1.0,
            doc="Accumulation of material in tank",
            units=flow_units)

        self.energy_accumulation = Var(self.flowsheet().config.time,
                                       phase_list,
                                       within=Reals,
                                       initialize=1.0,
                                       doc="Energy accumulation",
                                       units=units("power"))

        self.material_holdup = Var(self.flowsheet().config.time,
                                   pc_set,
                                   within=Reals,
                                   initialize=1.0,
                                   doc="Material holdup in tank",
                                   units=material_units)

        self.energy_holdup = Var(self.flowsheet().config.time,
                                 phase_list,
                                 within=Reals,
                                 initialize=1.0,
                                 doc="Energy holdup in tank",
                                 units=units("energy"))

        self.previous_material_holdup = Var(
            self.flowsheet().config.time,
            pc_set,
            within=Reals,
            initialize=1.0,
            doc="Tank material holdup at previous time",
            units=material_units)

        self.previous_energy_holdup = Var(
            self.flowsheet().config.time,
            phase_list,
            within=Reals,
            initialize=1.0,
            doc="Tank energy holdup at previous time",
            units=units("energy"))

        # Adiabatic operations are assumed
        # Fixing the heat_duty to 0 here to avoid any misakes at use
        # TODO: remove this once the isothermal constraints are added
        self.heat_duty.fix(0)

        # Computing material and energy holdup in the tank at previous time
        # using previous state Pressure and Temperature of the tank
        @self.Constraint(self.flowsheet().config.time,
                         pc_set,
                         doc="Material holdup at previous time")
        def previous_material_holdup_rule(b, t, p, j):
            return (
                b.previous_material_holdup[t, p,
                                           j] == b.control_volume.volume[t] *
                b.control_volume.phase_fraction[t, p] *
                b.previous_state[t].get_material_density_terms(p, j))

        @self.Constraint(self.flowsheet().config.time,
                         phase_list,
                         doc="Energy holdup at previous time")
        def previous_energy_holdup_rule(b, t, p):
            if (self.control_volume.properties_in[t].get_material_flow_basis()
                    == MaterialFlowBasis.molar):
                return (b.previous_energy_holdup[t, p] == (
                    sum(b.previous_material_holdup[t, p, j]
                        for j in component_list) *
                    b.previous_state[t].energy_internal_mol_phase[p]))
            if (self.control_volume.properties_in[t].get_material_flow_basis()
                    == MaterialFlowBasis.mass):
                return (b.previous_energy_holdup[t, p] == (
                    sum(b.previous_material_holdup[t, p, j]
                        for j in component_list) *
                    (b.previous_state[t].energy_internal_mol_phase[p] /
                     b.previous_state[t].mw)))

        # component material balances
        @self.Constraint(self.flowsheet().config.time,
                         pc_set,
                         doc="Material balances")
        def material_balances(b, t, p, j):
            if (p, j) in pc_set:
                return (
                    b.material_accumulation[t, p, j] ==
                    (b.control_volume.properties_in[t].\
                     get_material_flow_terms(p, j) -
                     b.control_volume.properties_out[t].\
                         get_material_flow_terms(p, j))
                    )
            else:
                return Constraint.Skip

        # integration of material accumulation
        @self.Constraint(self.flowsheet().config.time,
                         pc_set,
                         doc="Material holdup integration")
        def material_holdup_integration(b, t, p, j):
            if (p, j) in pc_set:
                return b.material_holdup[t, p, j] == (
                    b.dt[t] * b.material_accumulation[t, p, j] +
                    b.previous_material_holdup[t, p, j])

        # material holdup calculation
        @self.Constraint(self.flowsheet().config.time,
                         pc_set,
                         doc="Material holdup calculations")
        def material_holdup_calculation(b, t, p, j):
            if (p, j) in pc_set:
                return (
                    b.material_holdup[t, p, j] == (
                        b.control_volume.volume[t] *
                        b.control_volume.phase_fraction[t, p] *
                        b.control_volume.properties_out[t].\
                            get_material_density_terms(p, j)))

        # energy accumulation
        @self.Constraint(self.flowsheet().config.time,
                         doc="Energy accumulation")
        def energy_accumulation_equation(b, t):
            return (sum(b.energy_accumulation[t, p] for p in phase_list) *
                    b.dt[t] == sum(b.energy_holdup[t, p] for p in phase_list) -
                    sum(b.previous_energy_holdup[t, p] for p in phase_list))

        # energy holdup calculation
        @self.Constraint(self.flowsheet().config.time,
                         phase_list,
                         doc="Energy holdup calculation")
        def energy_holdup_calculation(b, t, p):
            if (self.control_volume.properties_in[t].get_material_flow_basis()
                    == MaterialFlowBasis.molar):
                return (
                    b.energy_holdup[t, p] ==
                    (sum(b.material_holdup[t, p, j] for j in component_list)
                    * b.control_volume.properties_out[t].\
                    energy_internal_mol_phase[p])
                    )
            if (self.control_volume.properties_in[t].get_material_flow_basis()
                    == MaterialFlowBasis.mass):
                return (
                    b.energy_holdup[t, p] ==
                    (sum(b.material_holdup[t, p, j] for j in component_list)
                    * (b.control_volume.properties_out[t].\
                    energy_internal_mol_phase[p]/
                    b.control_volume.properties_out[t].mw))
                    )

        # Energy balance based on internal energy, as follows:
        #     n_final * U_final =
        #                        n_previous * U_previous +
        #                        n_inlet * H_inlet - n_outlet * H_outlet
        #     where, n is number of moles, U is internal energy, H is enthalpy
        @self.Constraint(self.flowsheet().config.time, doc="Energy balance")
        def energy_balances(b, t):
            return (sum(
                b.energy_holdup[t, p]
                for p in phase_list) == sum(b.previous_energy_holdup[t, p]
                                            for p in phase_list) + b.dt[t] *
                    (sum(b.control_volume.properties_in[t].
                         get_enthalpy_flow_terms(p) for p in phase_list) -
                     sum(b.control_volume.properties_out[t].
                         get_enthalpy_flow_terms(p) for p in phase_list)))

    def initialize(blk,
                   state_args=None,
                   outlvl=idaeslog.NOTSET,
                   solver=None,
                   optarg=None):
        '''
        Hydrogen tank model initialization routine.

        Keyword Arguments:
            state_args : a dict of arguments to be passed to the property
                           package(s) for the control_volume of the model to
                           provide an initial state for initialization
                           (see documentation of the specific property package)
                           (default = None).
            outlvl : sets output level of initialisation routine

                     * 0 = no output (default)
                     * 1 = return solver state for each step in routine
                     * 2 = return solver state for each step in subroutines
                     * 3 = include solver output infomation (tee=True)

            optarg : solver options dictionary object (default={'tol': 1e-6})
            solver : str indicating whcih solver to use during
                     initialization (default = 'ipopt')

        Returns:
            None
        '''

        if state_args is None:
            state_args = dict()

        init_log = idaeslog.getInitLogger(blk.name, outlvl, tag="unit")
        solve_log = idaeslog.getSolveLogger(blk.name, outlvl, tag="unit")

        opt = get_solver(solver, optarg)

        init_log.info_low("Starting initialization...")

        flags = blk.control_volume.initialize(state_args=state_args,
                                              outlvl=outlvl,
                                              optarg=optarg,
                                              solver=solver)
        flag_previous_state = blk.previous_state.initialize(
            outlvl=outlvl,
            optarg=optarg,
            solver=solver,
            hold_state=True,
            state_args=state_args,
        )
        blk.previous_state[0].sum_mole_frac_out.deactivate()

        init_log.info_high("Initialization Step 1 Complete.")

        with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
            res = opt.solve(blk, tee=slc.tee)
        init_log.info_high("Initialization Step 2 {}.".format(
            idaeslog.condition(res)))
        blk.previous_state[0].sum_mole_frac_out.activate()
        blk.control_volume.release_state(flags, outlvl)
        blk.previous_state.release_state(flag_previous_state, outlvl)
        init_log.info("Initialization Complete.")

    def calculate_scaling_factors(self):
        super().calculate_scaling_factors()

        if hasattr(self, "previous_state"):
            for t, v in self.previous_state.items():
                iscale.set_scaling_factor(v.flow_mol, 1e-3)
                iscale.set_scaling_factor(v.pressure, 1e-5)
                iscale.set_scaling_factor(v.temperature, 1e-1)

        if hasattr(self, "tank_diameter"):
            for t, v in self.tank_diameter.items():
                iscale.set_scaling_factor(v, 1)

        if hasattr(self, "tank_length"):
            for t, v in self.tank_length.items():
                iscale.set_scaling_factor(v, 1)

        if hasattr(self, "heat_duty"):
            for t, v in self.heat_duty.items():
                iscale.set_scaling_factor(v, 1e-5)

        if hasattr(self, "material_accumulation"):
            for (t, p, j), v in self.material_accumulation.items():
                iscale.set_scaling_factor(v, 1e-3)

        if hasattr(self, "energy_accumulation"):
            for (t, p), v in self.energy_accumulation.items():
                iscale.set_scaling_factor(v, 1e-3)

        if hasattr(self, "material_holdup"):
            for (t, p, j), v in self.material_holdup.items():
                iscale.set_scaling_factor(v, 1e-5)

        if hasattr(self, "energy_holdup"):
            for (t, p), v in self.energy_holdup.items():
                iscale.set_scaling_factor(v, 1e-5)

        if hasattr(self, "previous_material_holdup"):
            for (t, p, j), v in self.previous_material_holdup.items():
                iscale.set_scaling_factor(v, 1e-5)

        if hasattr(self, "previous_energy_holdup"):
            for (t, p), v in self.previous_energy_holdup.items():
                iscale.set_scaling_factor(v, 1e-5)

        # Volume constraint
        if hasattr(self, "volume_cons"):
            for t, c in self.volume_cons.items():
                iscale.constraint_scaling_transform(
                    c,
                    iscale.get_scaling_factor(self.tank_length[t],
                                              default=1,
                                              warning=True))

        # Previous time Material Holdup Rule
        if hasattr(self, "previous_material_holdup_rule"):
            for (t, i), c in self.previous_material_holdup_rule.items():
                iscale.constraint_scaling_transform(
                    c,
                    iscale.get_scaling_factor(self.material_holdup[t, i, j],
                                              default=1,
                                              warning=True))

        # Previous time Energy Holdup Rule
        if hasattr(self, "previous_energy_holdup_rule"):
            for (t, i), c in self.previous_energy_holdup_rule.items():
                iscale.constraint_scaling_transform(
                    c,
                    iscale.get_scaling_factor(self.energy_holdup[t, i],
                                              default=1,
                                              warning=True))

        # Material Balances
        if hasattr(self, "material_balances"):
            for (t, i, j), c in self.material_balances.items():
                iscale.constraint_scaling_transform(
                    c,
                    iscale.get_scaling_factor(self.material_accumulation[t, i,
                                                                         j],
                                              default=1,
                                              warning=True))

        # Material Holdup Integration
        if hasattr(self, "material_holdup_integration"):
            for (t, i, j), c in self.material_holdup_integration.items():
                iscale.constraint_scaling_transform(
                    c,
                    iscale.get_scaling_factor(self.material_holdup[t, i, j],
                                              default=1,
                                              warning=True))

        # Material Holdup Constraints
        if hasattr(self, "material_holdup_calculation"):
            for (t, i, j), c in self.material_holdup_calculation.items():
                iscale.constraint_scaling_transform(
                    c,
                    iscale.get_scaling_factor(self.material_holdup[t, i, j],
                                              default=1,
                                              warning=True))

        # Enthalpy Balances
        if hasattr(self, "energy_accumulation_equation"):
            for t, c in self.energy_accumulation_equation.items():
                iscale.constraint_scaling_transform(
                    c,
                    iscale.get_scaling_factor(self.energy_accumulation[t, p],
                                              default=1,
                                              warning=True))

        # Energy Holdup Integration
        if hasattr(self, "energy_holdup_calculation"):
            for (t, i), c in self.energy_holdup_calculation.items():
                iscale.constraint_scaling_transform(
                    c,
                    iscale.get_scaling_factor(self.energy_holdup[t, i],
                                              default=1,
                                              warning=True))

        # Energy Balance Equation
        if hasattr(self, "energy_balances"):
            for t, c in self.energy_balances.items():
                iscale.constraint_scaling_transform(
                    c,
                    iscale.get_scaling_factor(self.energy_holdup[t, i],
                                              default=1,
                                              warning=True))
Пример #11
0
class WaterStateBlockData(StateBlockData):
    """
    General purpose StateBlock for Zero-Order unit models.
    """
    def build(self):
        super().build()

        # Create state variables
        self.flow_mass_comp = Var(self.component_list,
                                  initialize=1,
                                  domain=PositiveReals,
                                  doc='Mass flowrate of each component',
                                  units=pyunits.kg / pyunits.s)

    # -------------------------------------------------------------------------
    # Other properties
    def _conc_mass_comp(self):
        def rule_cmc(blk, j):
            return (blk.flow_mass_comp[j] / sum(self.flow_mass_comp[k]
                                                for k in self.component_list) *
                    blk.dens_mass)

        self.conc_mass_comp = Expression(self.component_list, rule=rule_cmc)

    def _dens_mass(self):
        self.dens_mass = Param(initialize=self.params.dens_mass_default,
                               units=pyunits.kg / pyunits.m**3,
                               mutable=True,
                               doc="Mass density of flow")

    def _flow_vol(self):
        self.flow_vol = Expression(expr=sum(self.flow_mass_comp[j]
                                            for j in self.component_list) /
                                   self.dens_mass)

    def _visc_d(self):
        self.visc_d = Param(initialize=self.params.visc_d_default,
                            units=pyunits.kg / pyunits.m / pyunits.s,
                            mutable=True,
                            doc="Dynamic viscosity of solution")

    def get_material_flow_terms(blk, p, j):
        return blk.flow_mass_comp[j]

    def get_enthalpy_flow_terms(blk, p):
        raise NotImplementedError

    def get_material_density_terms(blk, p, j):
        return blk.conc_mass_comp[j]

    def get_energy_density_terms(blk, p):
        raise NotImplementedError

    def default_material_balance_type(self):
        return MaterialBalanceType.componentTotal

    def default_energy_balance_type(self):
        return EnergyBalanceType.none

    def define_state_vars(blk):
        return {"flow_mass_comp": blk.flow_mass_comp}

    def define_display_vars(blk):
        return {
            "Volumetric Flowrate": blk.flow_vol,
            "Mass Concentration": blk.conc_mass_comp
        }

    def get_material_flow_basis(blk):
        return MaterialFlowBasis.mass

    def calculate_scaling_factors(self):
        # Get default scale factors and do calculations from base classes
        super().calculate_scaling_factors()

        d_sf_Q = self.params.default_scaling_factor["flow_vol"]
        d_sf_c = self.params.default_scaling_factor["conc_mass_comp"]

        for j, v in self.flow_mass_comp.items():
            if iscale.get_scaling_factor(v) is None:
                iscale.set_scaling_factor(v, d_sf_Q * d_sf_c)

        if self.is_property_constructed("flow_vol"):
            if iscale.get_scaling_factor(self.flow_vol) is None:
                iscale.set_scaling_factor(self.flow_vol, d_sf_Q)

        if self.is_property_constructed("conc_mass_comp"):
            for j, v in self.conc_mass_comp.items():
                sf_c = iscale.get_scaling_factor(self.conc_mass_comp[j])
                if sf_c is None:
                    try:
                        sf_c = self.params.default_scaling_factor[(
                            "conc_mass_comp", j)]
                    except KeyError:
                        sf_c = d_sf_c
                    iscale.set_scaling_factor(self.conc_mass_comp[j], sf_c)
Пример #12
0
class NanoFiltrationData(UnitModelBlockData):
    """
    Standard NF Unit Model Class:
    - zero dimensional model
    - steady state only
    - single liquid phase only
    """
    CONFIG = ConfigBlock()

    CONFIG.declare("dynamic", ConfigValue(
        domain=In([False]),
        default=False,
        description="Dynamic model flag - must be False",
        doc="""Indicates whether this model will be dynamic or not,
    **default** = False. NF units do not support dynamic
    behavior."""))
    CONFIG.declare("has_holdup", ConfigValue(
        default=False,
        domain=In([False]),
        description="Holdup construction flag - must be False",
        doc="""Indicates whether holdup terms should be constructed or not.
    **default** - False. NF units do not have defined volume, thus
    this must be False."""))
    CONFIG.declare("material_balance_type", ConfigValue(
        default=MaterialBalanceType.useDefault,
        domain=In(MaterialBalanceType),
        description="Material balance construction flag",
        doc="""Indicates what type of mass balance should be constructed,
    **default** - MaterialBalanceType.useDefault.
    **Valid values:** {
    **MaterialBalanceType.useDefault - refer to property package for default
    balance type
    **MaterialBalanceType.none** - exclude material balances,
    **MaterialBalanceType.componentPhase** - use phase component balances,
    **MaterialBalanceType.componentTotal** - use total component balances,
    **MaterialBalanceType.elementTotal** - use total element balances,
    **MaterialBalanceType.total** - use total material balance.}"""))
    CONFIG.declare("energy_balance_type", ConfigValue(
        default=EnergyBalanceType.useDefault,
        domain=In(EnergyBalanceType),
        description="Energy balance construction flag",
        doc="""Indicates what type of energy balance should be constructed,
    **default** - EnergyBalanceType.useDefault.
    **Valid values:** {
    **EnergyBalanceType.useDefault - refer to property package for default
    balance type
    **EnergyBalanceType.none** - exclude energy balances,
    **EnergyBalanceType.enthalpyTotal** - single enthalpy balance for material,
    **EnergyBalanceType.enthalpyPhase** - enthalpy balances for each phase,
    **EnergyBalanceType.energyTotal** - single energy balance for material,
    **EnergyBalanceType.energyPhase** - energy balances for each phase.}"""))
    CONFIG.declare("momentum_balance_type", ConfigValue(
        default=MomentumBalanceType.pressureTotal,
        domain=In(MomentumBalanceType),
        description="Momentum balance construction flag",
        doc="""Indicates what type of momentum balance should be constructed,
    **default** - MomentumBalanceType.pressureTotal.
    **Valid values:** {
    **MomentumBalanceType.none** - exclude momentum balances,
    **MomentumBalanceType.pressureTotal** - single pressure balance for material,
    **MomentumBalanceType.pressurePhase** - pressure balances for each phase,
    **MomentumBalanceType.momentumTotal** - single momentum balance for material,
    **MomentumBalanceType.momentumPhase** - momentum balances for each phase.}"""))
    CONFIG.declare("has_pressure_change", ConfigValue(
        default=False,
        domain=In([True, False]),
        description="Pressure change term construction flag",
        doc="""Indicates whether terms for pressure change should be
    constructed,
    **default** - False.
    **Valid values:** {
    **True** - include pressure change terms,
    **False** - exclude pressure change terms.}"""))
    CONFIG.declare("property_package", ConfigValue(
        default=useDefault,
        domain=is_physical_parameter_block,
        description="Property package to use for control volume",
        doc="""Property parameter object used to define property calculations,
    **default** - useDefault.
    **Valid values:** {
    **useDefault** - use default package from parent model or flowsheet,
    **PhysicalParameterObject** - a PhysicalParameterBlock object.}"""))
    CONFIG.declare("property_package_args", ConfigBlock(
        implicit=True,
        description="Arguments to use for constructing property packages",
        doc="""A ConfigBlock with arguments to be passed to a property block(s)
    and used when constructing these,
    **default** - None.
    **Valid values:** {
    see property package for documentation.}"""))


    def build(self):
        # Call UnitModel.build to setup dynamics
        super().build()

        self.scaling_factor = Suffix(direction=Suffix.EXPORT)

        if (len(self.config.property_package.phase_list) > 1
                or 'Liq' not in [p for p in self.config.property_package.phase_list]):
            raise ConfigurationError(
                "NF model only supports one liquid phase ['Liq'],"
                "the property package has specified the following phases {}"
                    .format([p for p in self.config.property_package.phase_list]))

        units_meta = self.config.property_package.get_metadata().get_derived_units

        # TODO: update IDAES such that solvent and solute lists are automatically created on the parameter block
        self.solvent_list = Set()
        self.solute_list = Set()
        for c in self.config.property_package.component_list:
            comp = self.config.property_package.get_component(c)
            try:
                if comp.is_solvent():
                    self.solvent_list.add(c)
                if comp.is_solute():
                    self.solute_list.add(c)
            except TypeError:
                raise ConfigurationError("NF model only supports one solvent and one or more solutes,"
                                         "the provided property package has specified a component '{}' "
                                         "that is not a solvent or solute".format(c))
        if len(self.solvent_list) > 1:
            raise ConfigurationError("NF model only supports one solvent component,"
                                     "the provided property package has specified {} solvent components"
                                     .format(len(self.solvent_list)))

        # Add unit parameters
        self.A_comp = Var(
            self.flowsheet().config.time,
            self.solvent_list,
            initialize=1e-12,
            bounds=(1e-18, 1e-6),
            domain=NonNegativeReals,
            units=units_meta('length')*units_meta('pressure')**-1*units_meta('time')**-1,
            doc='Solvent permeability coeff.')
        self.B_comp = Var(
            self.flowsheet().config.time,
            self.solute_list,
            initialize=1e-8,
            bounds=(1e-11, 1e-5),
            domain=NonNegativeReals,
            units=units_meta('length')*units_meta('time')**-1,
            doc='Solute permeability coeff.')
        self.sigma = Var(
            self.flowsheet().config.time,
            initialize=0.5,
            bounds=(1e-8, 1e6),
            domain=NonNegativeReals,
            units=pyunits.dimensionless,
            doc='Reflection coefficient')
        self.dens_solvent = Param(
            initialize=1000,
            units=units_meta('mass')*units_meta('length')**-3,
            doc='Pure water density')

        # Add unit variables
        self.flux_mass_phase_comp_in = Var(
            self.flowsheet().config.time,
            self.config.property_package.phase_list,
            self.config.property_package.component_list,
            initialize=1e-3,
            bounds=(1e-12, 1e6),
            units=units_meta('mass')*units_meta('length')**-2*units_meta('time')**-1,
            doc='Flux at feed inlet')
        self.flux_mass_phase_comp_out = Var(
            self.flowsheet().config.time,
            self.config.property_package.phase_list,
            self.config.property_package.component_list,
            initialize=1e-3,
            bounds=(1e-12, 1e6),
            units=units_meta('mass')*units_meta('length')**-2*units_meta('time')**-1,
            doc='Flux at feed outlet')
        self.avg_conc_mass_phase_comp_in = Var(
            self.flowsheet().config.time,
            self.config.property_package.phase_list,
            self.solute_list,
            initialize=1e-3,
            bounds=(1e-8, 1e6),
            domain=NonNegativeReals,
            units=units_meta('mass')*units_meta('length')**-3,
            doc='Average solute concentration at feed inlet')
        self.avg_conc_mass_phase_comp_out = Var(
            self.flowsheet().config.time,
            self.config.property_package.phase_list,
            self.solute_list,
            initialize=1e-3,
            bounds=(1e-8, 1e6),
            domain=NonNegativeReals,
            units=units_meta('mass')*units_meta('length')**-3,
            doc='Average solute concentration at feed outlet')
        self.area = Var(
            initialize=1,
            bounds=(1e-8, 1e6),
            domain=NonNegativeReals,
            units=units_meta('length') ** 2,
            doc='Membrane area')

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

        self.feed_side.add_state_blocks(
            has_phase_equilibrium=False)

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

        self.feed_side.add_energy_balances(
            balance_type=self.config.energy_balance_type,
            has_enthalpy_transfer=True)

        self.feed_side.add_momentum_balances(
            balance_type=self.config.momentum_balance_type,
            has_pressure_change=self.config.has_pressure_change)

        # Add permeate block
        tmp_dict = dict(**self.config.property_package_args)
        tmp_dict["has_phase_equilibrium"] = False
        tmp_dict["parameters"] = self.config.property_package
        tmp_dict["defined_state"] = False  # permeate block is not an inlet
        self.properties_permeate = self.config.property_package.state_block_class(
            self.flowsheet().config.time,
            doc="Material properties of permeate",
            default=tmp_dict)

        # Add Ports
        self.add_inlet_port(name='inlet', block=self.feed_side)
        self.add_outlet_port(name='retentate', block=self.feed_side)
        self.add_port(name='permeate', block=self.properties_permeate)

        # References for control volume
        # pressure change
        if (self.config.has_pressure_change is True and
                self.config.momentum_balance_type != 'none'):
            self.deltaP = Reference(self.feed_side.deltaP)

        # mass transfer
        self.mass_transfer_phase_comp = Var(
            self.flowsheet().config.time,
            self.config.property_package.phase_list,
            self.config.property_package.component_list,
            initialize=1,
            bounds=(1e-8, 1e6),
            domain=NonNegativeReals,
            units=units_meta('mass')*units_meta('time')**-1,
            doc='Mass transfer to permeate')

        @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):
            return self.mass_transfer_phase_comp[t, p, j] == -self.feed_side.mass_transfer_term[t, p, j]

        # NF performance equations
        @self.Expression(self.flowsheet().config.time,
                         self.config.property_package.phase_list,
                         self.config.property_package.component_list,
                         doc="Average flux expression")
        def flux_mass_phase_comp_avg(b, t, p, j):
            return 0.5 * (b.flux_mass_phase_comp_in[t, p, j] + b.flux_mass_phase_comp_out[t, p, j])

        @self.Constraint(self.flowsheet().config.time,
                         self.config.property_package.phase_list,
                         self.config.property_package.component_list,
                         doc="Permeate production")
        def eq_permeate_production(b, t, p, j):
            return (b.properties_permeate[t].get_material_flow_terms(p, j)
                    == b.area * b.flux_mass_phase_comp_avg[t, p, j])

        @self.Constraint(self.flowsheet().config.time,
                         self.config.property_package.phase_list,
                         self.config.property_package.component_list,
                         doc="Inlet water and salt flux")
        def eq_flux_in(b, t, p, j):
            prop_feed = b.feed_side.properties_in[t]
            prop_perm = b.properties_permeate[t]
            comp = self.config.property_package.get_component(j)
            if comp.is_solvent():
                return (b.flux_mass_phase_comp_in[t, p, j] == b.A_comp[t, j] * b.dens_solvent
                        * ((prop_feed.pressure - prop_perm.pressure)
                           - b.sigma[t] * (prop_feed.pressure_osm - prop_perm.pressure_osm)))
            elif comp.is_solute():
                return (b.flux_mass_phase_comp_in[t, p, j] == b.B_comp[t, j]
                        * (prop_feed.conc_mass_phase_comp[p, j] - prop_perm.conc_mass_phase_comp[p, j])
                        + ((1 - b.sigma[t]) * b.flux_mass_phase_comp_in[t, p, j]
                           * 1 / b.dens_solvent * b.avg_conc_mass_phase_comp_in[t, p, j])
                        )

        @self.Constraint(self.flowsheet().config.time,
                         self.config.property_package.phase_list,
                         self.config.property_package.component_list,
                         doc="Outlet water and salt flux")
        def eq_flux_out(b, t, p, j):
            prop_feed = b.feed_side.properties_out[t]
            prop_perm = b.properties_permeate[t]
            comp = self.config.property_package.get_component(j)
            if comp.is_solvent():
                return (b.flux_mass_phase_comp_out[t, p, j] == b.A_comp[t, j] * b.dens_solvent
                        * ((prop_feed.pressure - prop_perm.pressure)
                           - b.sigma[t] * (prop_feed.pressure_osm - prop_perm.pressure_osm)))
            elif comp.is_solute():
                return (b.flux_mass_phase_comp_out[t, p, j] == b.B_comp[t, j]
                        * (prop_feed.conc_mass_phase_comp[p, j] - prop_perm.conc_mass_phase_comp[p, j])
                        + ((1 - b.sigma[t]) * b.flux_mass_phase_comp_out[t, p, j]
                           * 1 / b.dens_solvent * b.avg_conc_mass_phase_comp_out[t, p, j])
                        )

        # Average concentration
        # COMMENT: Chen approximation of logarithmic average implemented
        @self.Constraint(self.flowsheet().config.time,
                         self.config.property_package.phase_list,
                         self.solute_list,
                         doc="Average inlet concentration")
        def eq_avg_conc_in(b, t, p, j):
            prop_feed = b.feed_side.properties_in[t]
            prop_perm = b.properties_permeate[t]
            return (b.avg_conc_mass_phase_comp_in[t, p, j] ==
                    (prop_feed.conc_mass_phase_comp[p, j] * prop_perm.conc_mass_phase_comp[p, j] *
                     (prop_feed.conc_mass_phase_comp[p, j] + prop_perm.conc_mass_phase_comp[p, j])/2)**(1/3))

        @self.Constraint(self.flowsheet().config.time,
                         self.config.property_package.phase_list,
                         self.solute_list,
                         doc="Average inlet concentration")
        def eq_avg_conc_out(b, t, p, j):
            prop_feed = b.feed_side.properties_out[t]
            prop_perm = b.properties_permeate[t]
            return (b.avg_conc_mass_phase_comp_out[t, p, j] ==
                    (prop_feed.conc_mass_phase_comp[p, j] * prop_perm.conc_mass_phase_comp[p, j] *
                     (prop_feed.conc_mass_phase_comp[p, j] + prop_perm.conc_mass_phase_comp[p, j])/2)**(1/3))

        # Feed and permeate-side connection
        @self.Constraint(self.flowsheet().config.time,
                         self.config.property_package.phase_list,
                         self.config.property_package.component_list,
                         doc="Mass transfer from feed to permeate")
        def eq_connect_mass_transfer(b, t, p, j):
            return (b.properties_permeate[t].get_material_flow_terms(p, j)
                    == -b.feed_side.mass_transfer_term[t, p, j])

        @self.Constraint(self.flowsheet().config.time,
                         doc="Enthalpy transfer from feed to permeate")
        def eq_connect_enthalpy_transfer(b, t):
            return (b.properties_permeate[t].get_enthalpy_flow_terms('Liq')
                    == -b.feed_side.enthalpy_transfer[t])

        @self.Constraint(self.flowsheet().config.time,
                         doc="Isothermal assumption for permeate")
        def eq_permeate_isothermal(b, t):
            return b.feed_side.properties_out[t].temperature == \
                   b.properties_permeate[t].temperature

    def initialize(
            blk,
            state_args=None,
            outlvl=idaeslog.NOTSET,
            solver="ipopt",
            optarg={"tol": 1e-6}):
        """
        General wrapper for pressure changer initialization routines
        Keyword Arguments:
            state_args : a dict of arguments to be passed to the property
                         package(s) to provide an initial state for
                         initialization (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')
        Returns:
            None
        """
        init_log = idaeslog.getInitLogger(blk.name, outlvl, tag="unit")
        solve_log = idaeslog.getSolveLogger(blk.name, outlvl, tag="unit")
        # Set solver options
        # TODO: update with new initialization solver API for IDAES
        opt = SolverFactory(solver)
        opt.options = optarg

        # ---------------------------------------------------------------------
        # Initialize holdup block
        flags = blk.feed_side.initialize(
            outlvl=outlvl,
            optarg=optarg,
            solver=solver,
            state_args=state_args,
        )
        init_log.info_high("Initialization Step 1 Complete.")
        # ---------------------------------------------------------------------
        # Initialize permeate
        # Set state_args from inlet state
        if state_args is None:
            state_args = {}
            state_dict = blk.feed_side.properties_in[
                blk.flowsheet().config.time.first()].define_port_members()

            for k in state_dict.keys():
                if state_dict[k].is_indexed():
                    state_args[k] = {}
                    for m in state_dict[k].keys():
                        state_args[k][m] = state_dict[k][m].value
                else:
                    state_args[k] = state_dict[k].value

        blk.properties_permeate.initialize(
            outlvl=outlvl,
            optarg=optarg,
            solver=solver,
            state_args=state_args,
        )
        init_log.info_high("Initialization Step 2 Complete.")

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

        # ---------------------------------------------------------------------
        # Release Inlet state
        blk.feed_side.release_state(flags, outlvl + 1)
        init_log.info(
            "Initialization Complete: {}".format(idaeslog.condition(res))
        )

    def _get_performance_contents(self, time_point=0):
        # TODO: make a unit specific stream table
        var_dict = {}
        if hasattr(self, "deltaP"):
            var_dict["Pressure Change"] = self.deltaP[time_point]

        return {"vars": var_dict}

    def get_costing(self, module=None, **kwargs):
        self.costing = Block()
        module.NanoFiltration_costing(self.costing, **kwargs)

    def calculate_scaling_factors(self):
        super().calculate_scaling_factors()

        # TODO: require users to set scaling factor for area or calculate it based on mass transfer and flux
        iscale.set_scaling_factor(self.area, 1e-1)

        # setting scaling factors for variables
        # these variables should have user input, if not there will be a warning
        if iscale.get_scaling_factor(self.area) is None:
            sf = iscale.get_scaling_factor(self.area, default=1, warning=True)
            iscale.set_scaling_factor(self.area, sf)

        # these variables do not typically require user input,
        # will not override if the user does provide the scaling factor
        if iscale.get_scaling_factor(self.A_comp) is None:
            iscale.set_scaling_factor(self.A_comp, 1e11)

        if iscale.get_scaling_factor(self.B_comp) is None:
            iscale.set_scaling_factor(self.B_comp, 1e5)

        if iscale.get_scaling_factor(self.sigma) is None:
            iscale.set_scaling_factor(self.sigma, 1)

        if iscale.get_scaling_factor(self.dens_solvent) is None:
            sf = iscale.get_scaling_factor(self.feed_side.properties_in[0].dens_mass_phase['Liq'])
            iscale.set_scaling_factor(self.dens_solvent, sf)

        for vobj in [self.flux_mass_phase_comp_in, self.flux_mass_phase_comp_out]:
            for (t, p, j), v in vobj.items():
                if iscale.get_scaling_factor(v) is None:
                    comp = self.config.property_package.get_component(j)
                    if comp.is_solvent():  # scaling based on solvent flux equation
                        sf = (iscale.get_scaling_factor(self.A_comp[t, j])
                              * iscale.get_scaling_factor(self.dens_solvent)
                              * iscale.get_scaling_factor(self.feed_side.properties_in[t].pressure))
                        iscale.set_scaling_factor(v, sf)
                    elif comp.is_solute():  # scaling based on solute flux equation
                        sf = (iscale.get_scaling_factor(self.B_comp[t, j])
                              * iscale.get_scaling_factor(self.feed_side.properties_in[t].conc_mass_phase_comp[p, j]))
                        iscale.set_scaling_factor(v, sf)

        for vobj in [self.avg_conc_mass_phase_comp_in, self.avg_conc_mass_phase_comp_out]:
            for (t, p, j), v in vobj.items():
                if iscale.get_scaling_factor(v) is None:
                    sf = iscale.get_scaling_factor(self.feed_side.properties_in[t].conc_mass_phase_comp[p, j])
                    iscale.set_scaling_factor(v, sf)

        for (t, p, j), v in self.feed_side.mass_transfer_term.items():
            if iscale.get_scaling_factor(v) is None:
                sf = iscale.get_scaling_factor(self.feed_side.properties_in[t].get_material_flow_terms(p, j))
                comp = self.config.property_package.get_component(j)
                if comp.is_solute:
                    sf *= 1e2  # solute typically has mass transfer 2 orders magnitude less than flow
                iscale.set_scaling_factor(v, sf)

        for (t, p, j), v in self.mass_transfer_phase_comp.items():
            if iscale.get_scaling_factor(v) is None:
                sf = iscale.get_scaling_factor(self.feed_side.properties_in[t].get_material_flow_terms(p, j))
                comp = self.config.property_package.get_component(j)
                if comp.is_solute:
                    sf *= 1e2  # solute typically has mass transfer 2 orders magnitude less than flow
                iscale.set_scaling_factor(v, sf)

        # TODO: update IDAES control volume to scale mass_transfer and enthalpy_transfer
        for ind, v in self.feed_side.mass_transfer_term.items():
            (t, p, j) = ind
            if iscale.get_scaling_factor(v) is None:
                sf = iscale.get_scaling_factor(self.feed_side.mass_transfer_term[t, p, j])
                iscale.constraint_scaling_transform(self.feed_side.material_balances[t, j], sf)

        for t, v in self.feed_side.enthalpy_transfer.items():
            if iscale.get_scaling_factor(v) is None:
                sf = (iscale.get_scaling_factor(self.feed_side.properties_in[t].enth_flow))
                iscale.set_scaling_factor(v, sf)
                iscale.constraint_scaling_transform(self.feed_side.enthalpy_balances[t], sf)

        # transforming constraints
        for ind, c in self.eq_mass_transfer_term.items():
            sf = iscale.get_scaling_factor(self.mass_transfer_phase_comp[ind])
            iscale.constraint_scaling_transform(c, sf)

        for ind, c in self.eq_permeate_production.items():
            sf = iscale.get_scaling_factor(self.mass_transfer_phase_comp[ind])
            iscale.constraint_scaling_transform(c, sf)

        for ind, c in self.eq_flux_in.items():
            sf = iscale.get_scaling_factor(self.flux_mass_phase_comp_in[ind])
            iscale.constraint_scaling_transform(c, sf)

        for ind, c in self.eq_flux_out.items():
            sf = iscale.get_scaling_factor(self.flux_mass_phase_comp_out[ind])
            iscale.constraint_scaling_transform(c, sf)

        for ind, c in self.eq_avg_conc_in.items():
            sf = iscale.get_scaling_factor(self.avg_conc_mass_phase_comp_in[ind])
            iscale.constraint_scaling_transform(c, sf)

        for ind, c in self.eq_avg_conc_out.items():
            sf = iscale.get_scaling_factor(self.avg_conc_mass_phase_comp_out[ind])
            iscale.constraint_scaling_transform(c, sf)

        for ind, c in self.eq_connect_mass_transfer.items():
            sf = iscale.get_scaling_factor(self.mass_transfer_phase_comp[ind])
            iscale.constraint_scaling_transform(c, sf)

        for ind, c in self.eq_connect_enthalpy_transfer.items():
            sf = iscale.get_scaling_factor(self.feed_side.enthalpy_transfer[ind])
            iscale.constraint_scaling_transform(c, sf)

        for t, c in self.eq_permeate_isothermal.items():
            sf = iscale.get_scaling_factor(self.feed_side.properties_in[t].temperature)
            iscale.constraint_scaling_transform(c, sf)
Пример #13
0
class NanofiltrationData(UnitModelBlockData):
    """
    Zero order nanofiltration model based on specified water flux and ion rejection.
    Default data from Table 9 in Labban et al. (2017) https://doi.org/10.1016/j.memsci.2016.08.062
    """
    CONFIG = ConfigBlock()

    CONFIG.declare(
        "dynamic",
        ConfigValue(domain=In([False]),
                    default=False,
                    description="Dynamic model flag - must be False",
                    doc="""Indicates whether this model will be dynamic or not,
    **default** = False. NF units do not support dynamic
    behavior."""))
    CONFIG.declare(
        "has_holdup",
        ConfigValue(
            default=False,
            domain=In([False]),
            description="Holdup construction flag - must be False",
            doc="""Indicates whether holdup terms should be constructed or not.
    **default** - False. NF units do not have defined volume, thus
    this must be False."""))
    CONFIG.declare(
        "material_balance_type",
        ConfigValue(
            default=MaterialBalanceType.useDefault,
            domain=In(MaterialBalanceType),
            description="Material balance construction flag",
            doc="""Indicates what type of mass balance should be constructed,
    **default** - MaterialBalanceType.useDefault.
    **Valid values:** {
    **MaterialBalanceType.useDefault - refer to property package for default
    balance type
    **MaterialBalanceType.none** - exclude material balances,
    **MaterialBalanceType.componentPhase** - use phase component balances,
    **MaterialBalanceType.componentTotal** - use total component balances,
    **MaterialBalanceType.elementTotal** - use total element balances,
    **MaterialBalanceType.total** - use total material balance.}"""))
    CONFIG.declare(
        "energy_balance_type",
        ConfigValue(
            default=EnergyBalanceType.useDefault,
            domain=In(EnergyBalanceType),
            description="Energy balance construction flag",
            doc="""Indicates what type of energy balance should be constructed,
    **default** - EnergyBalanceType.useDefault.
    **Valid values:** {
    **EnergyBalanceType.useDefault - refer to property package for default
    balance type
    **EnergyBalanceType.none** - exclude energy balances,
    **EnergyBalanceType.enthalpyTotal** - single enthalpy balance for material,
    **EnergyBalanceType.enthalpyPhase** - enthalpy balances for each phase,
    **EnergyBalanceType.energyTotal** - single energy balance for material,
    **EnergyBalanceType.energyPhase** - energy balances for each phase.}"""))
    CONFIG.declare(
        "momentum_balance_type",
        ConfigValue(
            default=MomentumBalanceType.pressureTotal,
            domain=In(MomentumBalanceType),
            description="Momentum balance construction flag",
            doc="""Indicates what type of momentum balance should be constructed,
    **default** - MomentumBalanceType.pressureTotal.
    **Valid values:** {
    **MomentumBalanceType.none** - exclude momentum balances,
    **MomentumBalanceType.pressureTotal** - single pressure balance for material,
    **MomentumBalanceType.pressurePhase** - pressure balances for each phase,
    **MomentumBalanceType.momentumTotal** - single momentum balance for material,
    **MomentumBalanceType.momentumPhase** - momentum balances for each phase.}"""
        ))
    CONFIG.declare(
        "has_pressure_change",
        ConfigValue(
            default=False,
            domain=In([True, False]),
            description="Pressure change term construction flag",
            doc="""Indicates whether terms for pressure change should be
    constructed,
    **default** - False.
    **Valid values:** {
    **True** - include pressure change terms,
    **False** - exclude pressure change terms.}"""))
    CONFIG.declare(
        "property_package",
        ConfigValue(
            default=useDefault,
            domain=is_physical_parameter_block,
            description="Property package to use for control volume",
            doc=
            """Property parameter object used to define property calculations,
    **default** - useDefault.
    **Valid values:** {
    **useDefault** - use default package from parent model or flowsheet,
    **PhysicalParameterObject** - a PhysicalParameterBlock object.}"""))
    CONFIG.declare(
        "property_package_args",
        ConfigBlock(
            implicit=True,
            description="Arguments to use for constructing property packages",
            doc=
            """A ConfigBlock with arguments to be passed to a property block(s)
    and used when constructing these,
    **default** - None.
    **Valid values:** {
    see property package for documentation.}"""))

    def _process_config(self):
        if len(self.config.property_package.solvent_set) > 1:
            raise ConfigurationError(
                "NF model only supports one solvent component,"
                "the provided property package has specified {} solvent components"
                .format(len(self.config.property_package.solvent_set)))

        if len(self.config.property_package.solvent_set) == 0:
            raise ConfigurationError(
                "The NF model was expecting a solvent and did not receive it.")

        if len(self.config.property_package.solute_set) == 0 and len(
                self.config.property_package.ion_set) == 0:
            raise ConfigurationError(
                "The NF model was expecting at least one solute or ion and did not receive any."
            )

    def build(self):
        # Call UnitModel.build to setup dynamics
        super().build()

        self.scaling_factor = Suffix(direction=Suffix.EXPORT)

        units_meta = self.config.property_package.get_metadata(
        ).get_derived_units

        self._process_config()

        if hasattr(self.config.property_package, 'ion_set'):
            solute_set = self.config.property_package.ion_set
        elif hasattr(self.config.property_package, 'solute_set'):
            solute_set = self.config.property_package.solute_set

        solvent_solute_set = self.config.property_package.solvent_set | solute_set

        # Add unit parameters
        self.flux_vol_solvent = Var(self.flowsheet().config.time,
                                    self.config.property_package.solvent_set,
                                    initialize=1.67e-6,
                                    bounds=(1e-8, 1e-4),
                                    units=units_meta('length') *
                                    units_meta('time')**-1,
                                    doc='Solvent volumetric flux')
        self.rejection_phase_comp = Var(
            self.flowsheet().config.time,
            self.config.property_package.phase_list,
            solute_set,
            initialize=0.9,
            bounds=(-1 + 1e-6, 1 - 1e-6),
            units=pyunits.dimensionless,
            doc='Observed solute rejection')
        self.dens_solvent = Param(initialize=1000,
                                  units=units_meta('mass') *
                                  units_meta('length')**-3,
                                  doc='Pure water density')

        # Add unit variables
        self.area = Var(initialize=1,
                        bounds=(1e-8, 1e6),
                        domain=NonNegativeReals,
                        units=units_meta('length')**2,
                        doc='Membrane area')

        def recovery_mass_phase_comp_initialize(b, t, p, j):
            if j in b.config.property_package.solvent_set:
                return 0.8
            elif j in solute_set:
                return 0.1

        def recovery_mass_phase_comp_bounds(b, t, p, j):
            ub = 1 - 1e-6
            if j in b.config.property_package.solvent_set:
                lb = 1e-2
            elif j in solute_set:
                lb = 1e-5
            else:
                lb = 1e-5

            return lb, ub

        self.recovery_mass_phase_comp = Var(
            self.flowsheet().config.time,
            self.config.property_package.phase_list,
            solvent_solute_set,
            initialize=recovery_mass_phase_comp_initialize,
            bounds=recovery_mass_phase_comp_bounds,
            units=pyunits.dimensionless,
            doc='Mass-based component recovery')
        self.recovery_vol_phase = Var(self.flowsheet().config.time,
                                      self.config.property_package.phase_list,
                                      initialize=0.1,
                                      bounds=(1e-2, 1 - 1e-6),
                                      units=pyunits.dimensionless,
                                      doc='Volumetric-based recovery')

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

        self.feed_side.add_state_blocks(has_phase_equilibrium=False)

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

        @self.feed_side.Constraint(
            self.flowsheet().config.time,
            doc='isothermal energy balance for feed_side')
        def eq_isothermal(b, t):
            return b.properties_in[t].temperature == b.properties_out[
                t].temperature

        self.feed_side.add_momentum_balances(
            balance_type=self.config.momentum_balance_type,
            has_pressure_change=self.config.has_pressure_change)

        # Add permeate block
        tmp_dict = dict(**self.config.property_package_args)
        tmp_dict["has_phase_equilibrium"] = False
        tmp_dict["parameters"] = self.config.property_package
        tmp_dict["defined_state"] = False  # permeate block is not an inlet
        self.properties_permeate = self.config.property_package.state_block_class(
            self.flowsheet().config.time,
            doc="Material properties of permeate",
            default=tmp_dict)

        # Add Ports
        self.add_inlet_port(name='inlet', block=self.feed_side)
        self.add_outlet_port(name='retentate', block=self.feed_side)
        self.add_port(name='permeate', block=self.properties_permeate)

        # References for control volume
        # pressure change
        if (self.config.has_pressure_change is True
                and self.config.momentum_balance_type != 'none'):
            self.deltaP = Reference(self.feed_side.deltaP)

        # mass transfer
        self.mass_transfer_phase_comp = Var(
            self.flowsheet().config.time,
            self.config.property_package.phase_list,
            solvent_solute_set,
            initialize=1,
            bounds=(1e-8, 1e6),
            domain=NonNegativeReals,
            units=units_meta('mass') * units_meta('time')**-1,
            doc='Mass transfer to permeate')

        @self.Constraint(self.flowsheet().config.time,
                         self.config.property_package.phase_list,
                         solvent_solute_set,
                         doc="Mass transfer term")
        def eq_mass_transfer_term(b, t, p, j):
            # TODO- come up with better way to handle different locations of mw_comp in property models (generic vs simple ion prop model)
            if b.feed_side.properties_in[0].get_material_flow_basis(
            ) == MaterialFlowBasis.mass:
                return b.mass_transfer_phase_comp[
                    t, p, j] == -b.feed_side.mass_transfer_term[t, p, j]
            elif b.feed_side.properties_in[0].get_material_flow_basis(
            ) == MaterialFlowBasis.molar:
                if hasattr(b.feed_side.properties_in[0].params, 'mw_comp'):
                    mw_comp = b.feed_side.properties_in[0].params.mw_comp[j]
                elif hasattr(b.feed_side.properties_in[0], 'mw_comp'):
                    mw_comp = b.feed_side.properties_in[0].mw_comp[j]
                else:
                    raise ConfigurationError('mw_comp was not found.')
                return b.mass_transfer_phase_comp[t, p, j] == -b.feed_side.mass_transfer_term[t, p, j] \
                       * mw_comp

        # NF performance equations
        @self.Constraint(self.flowsheet().config.time,
                         self.config.property_package.phase_list,
                         self.config.property_package.solvent_set,
                         doc="Solvent mass transfer")
        def eq_solvent_transfer(b, t, p, j):
            if b.feed_side.properties_in[0].get_material_flow_basis(
            ) == MaterialFlowBasis.mass:
                return (b.flux_vol_solvent[t, j] * b.dens_solvent *
                        b.area == -b.feed_side.mass_transfer_term[t, p, j])
            elif b.feed_side.properties_in[0].get_material_flow_basis(
            ) == MaterialFlowBasis.molar:
                #TODO- come up with better way to handle different locations of mw_comp in property models (generic vs simple ion prop model)
                if hasattr(b.feed_side.properties_in[0].params, 'mw_comp'):
                    mw_comp = b.feed_side.properties_in[0].params.mw_comp[j]
                elif hasattr(b.feed_side.properties_in[0], 'mw_comp'):
                    mw_comp = b.feed_side.properties_in[0].mw_comp[j]
                else:
                    raise ConfigurationError('mw_comp was not found.')
                return (b.flux_vol_solvent[t, j] * b.dens_solvent *
                        b.area == -b.feed_side.mass_transfer_term[t, p, j] *
                        mw_comp)

        @self.Constraint(self.flowsheet().config.time,
                         self.config.property_package.phase_list,
                         solvent_solute_set,
                         doc="Permeate production")
        def eq_permeate_production(b, t, p, j):
            return (b.properties_permeate[t].get_material_flow_terms(
                p, j) == -b.feed_side.mass_transfer_term[t, p, j])

        @self.Constraint(self.flowsheet().config.time,
                         self.config.property_package.phase_list,
                         solute_set,
                         doc="Solute rejection")
        def eq_rejection_phase_comp(b, t, p, j):
            return (
                b.properties_permeate[t].conc_mol_phase_comp['Liq', j] ==
                b.feed_side.properties_in[t].conc_mol_phase_comp['Liq', j] *
                (1 - b.rejection_phase_comp[t, p, j]))

        @self.Constraint(self.flowsheet().config.time)
        def eq_recovery_vol_phase(b, t):
            return (b.recovery_vol_phase[
                t, 'Liq'] == b.properties_permeate[t].flow_vol /
                    b.feed_side.properties_in[t].flow_vol)

        @self.Constraint(self.flowsheet().config.time, solvent_solute_set)
        def eq_recovery_mass_phase_comp(b, t, j):
            return (
                b.recovery_mass_phase_comp[t, 'Liq', j] ==
                b.properties_permeate[t].flow_mass_phase_comp['Liq', j] /
                b.feed_side.properties_in[t].flow_mass_phase_comp['Liq', j])

        @self.Constraint(self.flowsheet().config.time,
                         doc="Isothermal assumption for permeate")
        def eq_permeate_isothermal(b, t):
            return b.feed_side.properties_in[t].temperature == \
                   b.properties_permeate[t].temperature

    def initialize_build(blk,
                         state_args=None,
                         outlvl=idaeslog.NOTSET,
                         solver=None,
                         optarg=None):
        """
        General wrapper for pressure changer initialization routines

        Keyword Arguments:
            state_args : a dict of arguments to be passed to the property
                         package(s) to provide an initial state for
                         initialization (see documentation of the specific
                         property package) (default = {}).
            outlvl : sets output level of initialization routine
            optarg : solver options dictionary object (default=None)
            solver : str indicating which solver to use during
                     initialization (default = None)

        Returns: None
        """
        init_log = idaeslog.getInitLogger(blk.name, outlvl, tag="unit")
        solve_log = idaeslog.getSolveLogger(blk.name, outlvl, tag="unit")

        opt = get_solver(solver, optarg)

        # ---------------------------------------------------------------------
        # Initialize holdup block
        flags = blk.feed_side.initialize(
            outlvl=outlvl,
            optarg=optarg,
            solver=solver,
            state_args=state_args,
        )
        init_log.info_high("Initialization Step 1 Complete.")
        # ---------------------------------------------------------------------
        # Initialize permeate
        # Set state_args from inlet state
        if state_args is None:
            state_args = {}
            state_dict = blk.feed_side.properties_in[
                blk.flowsheet().config.time.first()].define_port_members()

            for k in state_dict.keys():
                if state_dict[k].is_indexed():
                    state_args[k] = {}
                    for m in state_dict[k].keys():
                        state_args[k][m] = state_dict[k][m].value
                else:
                    state_args[k] = state_dict[k].value

        blk.properties_permeate.initialize(
            outlvl=outlvl,
            optarg=optarg,
            solver=solver,
            state_args=state_args,
        )
        init_log.info_high("Initialization Step 2 Complete.")

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

        # ---------------------------------------------------------------------
        # Release Inlet state
        blk.feed_side.release_state(flags, outlvl + 1)
        init_log.info("Initialization Complete: {}".format(
            idaeslog.condition(res)))

    def _get_performance_contents(self, time_point=0):
        for k in ('ion_set', 'solute_set'):
            if hasattr(self.config.property_package, k):
                solute_set = getattr(self.config.property_package, k)
                break
        var_dict = {}
        expr_dict = {}
        var_dict["Volumetric Recovery Rate"] = self.recovery_vol_phase[
            time_point, 'Liq']
        var_dict["Solvent Mass Recovery Rate"] = self.recovery_mass_phase_comp[
            time_point, 'Liq', 'H2O']
        var_dict["Membrane Area"] = self.area
        if hasattr(self, "deltaP"):
            var_dict["Pressure Change"] = self.deltaP[time_point]
        if self.feed_side.properties_in[time_point].is_property_constructed(
                'flow_vol'):
            if self.feed_side.properties_in[
                    time_point].flow_vol.is_variable_type():
                obj_dict = var_dict
            elif self.feed_side.properties_in[
                    time_point].flow_vol.is_named_expression_type():
                obj_dict = expr_dict
            else:
                raise Exception(
                    f"{self.feed_side.properties_in[time_point].flow_vol} isn't a variable nor expression"
                )
            obj_dict[
                'Volumetric Flowrate @Inlet'] = self.feed_side.properties_in[
                    time_point].flow_vol
        if self.feed_side.properties_out[time_point].is_property_constructed(
                'flow_vol'):
            if self.feed_side.properties_out[
                    time_point].flow_vol.is_variable_type():
                obj_dict = var_dict
            elif self.feed_side.properties_out[
                    time_point].flow_vol.is_named_expression_type():
                obj_dict = expr_dict
            else:
                raise Exception(
                    f"{self.feed_side.properties_in[time_point].flow_vol} isn't a variable nor expression"
                )
            obj_dict[
                'Volumetric Flowrate @Outlet'] = self.feed_side.properties_out[
                    time_point].flow_vol
        var_dict['Solvent Volumetric Flux'] = self.flux_vol_solvent[time_point,
                                                                    'H2O']
        for j in solute_set:
            var_dict[f'{j} Rejection'] = self.rejection_phase_comp[time_point,
                                                                   'Liq', j]
            if self.feed_side.properties_in[time_point].conc_mol_phase_comp[
                    'Liq', j].is_expression_type():
                obj_dict = expr_dict
            elif self.feed_side.properties_in[time_point].conc_mol_phase_comp[
                    'Liq', j].is_variable_type():
                obj_dict = var_dict
            obj_dict[
                f'{j} Molar Concentration @Inlet'] = self.feed_side.properties_in[
                    time_point].conc_mol_phase_comp['Liq', j]
            obj_dict[
                f'{j} Molar Concentration @Outlet'] = self.feed_side.properties_out[
                    time_point].conc_mol_phase_comp['Liq', j]
            obj_dict[
                f'{j} Molar Concentration @Permeate'] = self.properties_permeate[
                    time_point].conc_mol_phase_comp['Liq', j]

        return {"vars": var_dict, "exprs": expr_dict}

    def _get_stream_table_contents(self, time_point=0):
        return create_stream_table_dataframe(
            {
                "Feed Inlet": self.inlet,
                "Feed Outlet": self.retentate,
                "Permeate Outlet": self.permeate,
            },
            time_point=time_point,
        )

    def get_costing(self, module=None, **kwargs):
        self.costing = Block()
        module.Nanofiltration_costing(self.costing, **kwargs)

    def calculate_scaling_factors(self):
        super().calculate_scaling_factors()

        for k in ('ion_set', 'solute_set'):
            if hasattr(self.config.property_package, k):
                solute_set = getattr(self.config.property_package, k)
                break

        # TODO: require users to set scaling factor for area or calculate it based on mass transfer and flux
        iscale.set_scaling_factor(self.area, 1e-1)

        # setting scaling factors for variables
        # these variables should have user input, if not there will be a warning
        if iscale.get_scaling_factor(self.area) is None:
            sf = iscale.get_scaling_factor(self.area, default=1, warning=True)
            iscale.set_scaling_factor(self.area, sf)

        # these variables do not typically require user input,
        # will not override if the user does provide the scaling factor
        # TODO: this default scaling assumes SI units rather than being based on the property package
        if iscale.get_scaling_factor(self.dens_solvent) is None:
            iscale.set_scaling_factor(self.dens_solvent, 1e-3)

        for t, v in self.flux_vol_solvent.items():
            if iscale.get_scaling_factor(v) is None:
                iscale.set_scaling_factor(v, 1e6)

        for (t, p, j), v in self.rejection_phase_comp.items():
            if iscale.get_scaling_factor(v) is None:
                iscale.set_scaling_factor(v, 1e1)

        for (t, p, j), v in self.mass_transfer_phase_comp.items():
            if iscale.get_scaling_factor(v) is None:
                sf = 10 * iscale.get_scaling_factor(
                    self.feed_side.properties_in[t].get_material_flow_terms(
                        p, j))
                iscale.set_scaling_factor(v, sf)
        if iscale.get_scaling_factor(self.recovery_vol_phase) is None:
            iscale.set_scaling_factor(self.recovery_vol_phase, 1)

        for (t, p, j), v in self.recovery_mass_phase_comp.items():
            if j in self.config.property_package.solvent_set:
                sf = 1
            elif j in solute_set:
                sf = 10
            if iscale.get_scaling_factor(v) is None:
                iscale.set_scaling_factor(v, sf)

        # transforming constraints
        for ind, c in self.feed_side.eq_isothermal.items():
            sf = iscale.get_scaling_factor(
                self.feed_side.properties_in[0].temperature)
            iscale.constraint_scaling_transform(c, sf)

        for ind, c in self.eq_mass_transfer_term.items():
            sf = iscale.get_scaling_factor(self.mass_transfer_phase_comp[ind])
            iscale.constraint_scaling_transform(c, sf)

        for ind, c in self.eq_solvent_transfer.items():
            sf = iscale.get_scaling_factor(self.mass_transfer_phase_comp[ind])
            iscale.constraint_scaling_transform(c, sf)

        for ind, c in self.eq_permeate_production.items():
            sf = iscale.get_scaling_factor(self.mass_transfer_phase_comp[ind])
            iscale.constraint_scaling_transform(c, sf)

        for ind, c in self.eq_rejection_phase_comp.items():
            sf = iscale.get_scaling_factor(self.rejection_phase_comp[ind])
            iscale.constraint_scaling_transform(c, sf)

        for t, c in self.eq_permeate_isothermal.items():
            sf = iscale.get_scaling_factor(
                self.feed_side.properties_in[t].temperature)
            iscale.constraint_scaling_transform(c, sf)
        for t, c in self.eq_recovery_vol_phase.items():
            sf = iscale.get_scaling_factor(self.recovery_vol_phase[t, 'Liq'])
            iscale.constraint_scaling_transform(c, sf)

        for (t, j), c in self.eq_recovery_mass_phase_comp.items():
            sf = iscale.get_scaling_factor(self.recovery_mass_phase_comp[t,
                                                                         'Liq',
                                                                         j])
            iscale.constraint_scaling_transform(c, sf)