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)
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)
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)
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": {}, }
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))
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))
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)
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.")
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))
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)
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)
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)