From 57aaca9355094b81961fbb98d87a4aedf66db085 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Mon, 18 Sep 2023 16:25:11 -0600 Subject: [PATCH 01/27] + single_use resin option, - bed_capacity_param --- watertap/unit_models/ion_exchange_0D.py | 482 +++++++++++------------- 1 file changed, 229 insertions(+), 253 deletions(-) diff --git a/watertap/unit_models/ion_exchange_0D.py b/watertap/unit_models/ion_exchange_0D.py index 7349d2876f..254f5d47ba 100644 --- a/watertap/unit_models/ion_exchange_0D.py +++ b/watertap/unit_models/ion_exchange_0D.py @@ -101,6 +101,7 @@ class RegenerantChem(StrEnum): H2SO4 = "H2SO4" NaCl = "NaCl" MeOH = "MeOH" + single_use = "single_use" none = "none" @@ -301,22 +302,8 @@ def build(self): prop_in = self.process_flow.properties_in[0] - 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 - - self.regeneration_stream = self.config.property_package.state_block_class( - self.flowsheet().config.time, - doc="Material properties of regeneration stream", - **tmp_dict, - ) - - regen = self.regeneration_stream[0] - self.add_inlet_port(name="inlet", block=self.process_flow) self.add_outlet_port(name="outlet", block=self.process_flow) - self.add_outlet_port(name="regen", block=self.regeneration_stream) # ==========PARAMETERS========== @@ -377,31 +364,11 @@ def build(self): doc="Sherwood equation exponent C", ) - # Bed expansion is calculated as a fraction of the bed_depth - # These coefficients are used to calculate that fraction (bed_expansion_frac) as a function of backwash rate (bw_rate, m/hr) - # bed_expansion_frac = bed_expansion_A + bed_expansion_B * bw_rate + bed_expansion_C * bw_rate**2 - # Default is for strong-base type I acrylic anion exchanger resin (A-850, Purolite), @20C - # Data extracted from MWH Chap 16, Figure 16-15 and fit with Excel - - self.bed_expansion_frac_A = Param( - initialize=-1.23e-2, + self.number_columns_redund = Param( + initialize=1, mutable=True, units=pyunits.dimensionless, - doc="Bed expansion fraction eq intercept", - ) - - self.bed_expansion_frac_B = Param( - initialize=1.02e-1, - mutable=True, - units=pyunits.hr / pyunits.m, - doc="Bed expansion fraction equation B parameter", - ) - - self.bed_expansion_frac_C = Param( - initialize=-1.35e-3, - mutable=True, - units=pyunits.hr**2 / pyunits.m**2, - doc="Bed expansion fraction equation C parameter", + doc="Number of redundant columns for ion exchange process", ) # Pressure drop (psi/m of resin bed depth) is a function of loading rate (vel_bed) in m/hr @@ -437,48 +404,70 @@ def build(self): doc="Pump efficiency", ) - # Rinse, Regen, Backwashing params + if self.config.regenerant != RegenerantChem.single_use: - self.t_regen = Param( - initialize=1800, - mutable=True, - units=pyunits.s, - doc="Regeneration time", - ) + # Bed expansion is calculated as a fraction of the bed_depth + # These coefficients are used to calculate that fraction (bed_expansion_frac) as a function of backwash rate (bw_rate, m/hr) + # bed_expansion_frac = bed_expansion_A + bed_expansion_B * bw_rate + bed_expansion_C * bw_rate**2 + # Default is for strong-base type I acrylic anion exchanger resin (A-850, Purolite), @20C + # Data extracted from MWH Chap 16, Figure 16-15 and fit with Excel - self.rinse_bv = Param( - initialize=5, - mutable=True, - doc="Number of bed volumes for rinse step", - ) + self.bed_expansion_frac_A = Param( + initialize=-1.23e-2, + mutable=True, + units=pyunits.dimensionless, + doc="Bed expansion fraction eq intercept", + ) - self.bw_rate = Param( - initialize=5, - mutable=True, - units=pyunits.m / pyunits.hour, - doc="Backwash loading rate [m/hr]", - ) + self.bed_expansion_frac_B = Param( + initialize=1.02e-1, + mutable=True, + units=pyunits.hr / pyunits.m, + doc="Bed expansion fraction equation B parameter", + ) - self.t_bw = Param( - initialize=600, - mutable=True, - units=pyunits.s, - doc="Backwash time", - ) + self.bed_expansion_frac_C = Param( + initialize=-1.35e-3, + mutable=True, + units=pyunits.hr**2 / pyunits.m**2, + doc="Bed expansion fraction equation C parameter", + ) + # Rinse, Regen, Backwashing params - self.service_to_regen_flow_ratio = Param( - initialize=3, - mutable=True, - units=pyunits.dimensionless, - doc="Ratio of service flow rate to regeneration flow rate", - ) + self.t_regen = Param( + initialize=1800, + mutable=True, + units=pyunits.s, + doc="Regeneration time", + ) + + self.rinse_bv = Param( + initialize=5, + mutable=True, + doc="Number of bed volumes for rinse step", + ) + + self.bw_rate = Param( + initialize=5, + mutable=True, + units=pyunits.m / pyunits.hour, + doc="Backwash loading rate [m/hr]", + ) + + self.t_bw = Param( + initialize=600, + mutable=True, + units=pyunits.s, + doc="Backwash time", + ) + + self.service_to_regen_flow_ratio = Param( + initialize=3, + mutable=True, + units=pyunits.dimensionless, + doc="Ratio of service flow rate to regeneration flow rate", + ) - self.number_columns_redund = Param( - initialize=1, - mutable=True, - units=pyunits.dimensionless, - doc="Number of redundant columns for ion exchange process", - ) # ==========VARIABLES========== # COMMON TO LANGMUIR + FREUNDLICH @@ -503,13 +492,6 @@ def build(self): doc="Resin surface area per volume", ) - self.regen_dose = Var( - initialize=300, - units=pyunits.kg / pyunits.m**3, - bounds=(0, None), - doc="Regenerant dose required for regeneration per volume of resin [kg regenerant/m3 resin]", - ) - self.c_norm = Var( self.target_ion_set, initialize=0.5, @@ -792,13 +774,6 @@ def build(self): doc="Bed volumes of feed at 50 percent breakthrough", ) - self.bed_capacity_param = Var( - initialize=1, - bounds=(0, None), - units=pyunits.dimensionless, - doc="Bed capacity fitting parameter for Clark model (A)", - ) - self.kinetic_param = Var( initialize=1e-5, bounds=(0, None), @@ -808,97 +783,127 @@ def build(self): # ==========EXPRESSIONS========== - @self.Expression(doc="Bed expansion fraction from backwashing") - def bed_expansion_frac(b): + @self.Expression(doc="Pressure drop") + def pressure_drop(b): + vel_bed = pyunits.convert(b.vel_bed, to_units=pyunits.m / pyunits.hr) return ( - b.bed_expansion_frac_A - + b.bed_expansion_frac_B * b.bw_rate - + b.bed_expansion_frac_C * b.bw_rate**2 - ) # for 20C - - @self.Expression(doc="Bed expansion from backwashing") - def bed_expansion_h(b): - return b.bed_expansion_frac * b.bed_depth + b.p_drop_A + b.p_drop_B * vel_bed + b.p_drop_C * vel_bed**2 + ) * b.bed_depth # for 20C; @self.Expression(doc="Total bed volume") def bed_vol(b): return b.bed_vol_tot / b.number_columns - @self.Expression(doc="Backwashing flow rate") - def bw_flow(b): - return ( - pyunits.convert(b.bw_rate, to_units=pyunits.m / pyunits.s) - * (b.bed_vol / b.bed_depth) - * b.number_columns + if self.config.regenerant != RegenerantChem.single_use: + + 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 + + self.regeneration_stream = self.config.property_package.state_block_class( + self.flowsheet().config.time, + doc="Material properties of regeneration stream", + **tmp_dict, ) - @self.Expression(doc="Rinse flow rate") - def rinse_flow(b): - return b.vel_bed * (b.bed_vol / b.bed_depth) * b.number_columns + regen = self.regeneration_stream[0] - @self.Expression(doc="Rinse time") - def t_rinse(b): - return b.ebct * b.rinse_bv + self.add_outlet_port(name="regen", block=self.regeneration_stream) - @self.Expression(doc="Waste time") - def t_waste(b): - return b.t_regen + b.t_bw + b.t_rinse + @self.Expression(doc="Backwashing flow rate") + def bw_flow(b): + return ( + pyunits.convert(b.bw_rate, to_units=pyunits.m / pyunits.s) + * (b.bed_vol / b.bed_depth) + * b.number_columns + ) - @self.Expression(doc="Cycle time") - def t_cycle(b): - return b.t_breakthru + b.t_waste + @self.Expression(doc="Bed expansion fraction from backwashing") + def bed_expansion_frac(b): + return ( + b.bed_expansion_frac_A + + b.bed_expansion_frac_B * b.bw_rate + + b.bed_expansion_frac_C * b.bw_rate**2 + ) # for 20C - @self.Expression(doc="Volume per column") - def col_vol_per(b): - return b.col_height * (b.bed_vol / b.bed_depth) + @self.Expression(doc="Bed expansion from backwashing") + def bed_expansion_h(b): + return b.bed_expansion_frac * b.bed_depth - @self.Expression(doc="Total column volume required") - def col_vol_tot(b): - return b.number_columns * b.col_vol_per + @self.Expression(doc="Rinse flow rate") + def rinse_flow(b): + return b.vel_bed * (b.bed_vol / b.bed_depth) * b.number_columns - @self.Expression( - doc="Bed volumes at breakthrough", - ) - def bv_calc(b): - return (b.vel_bed * b.t_breakthru) / b.bed_depth + @self.Expression(doc="Rinse time") + def t_rinse(b): + return b.ebct * b.rinse_bv - @self.Expression(doc="Regen tank volume") - def regen_tank_vol(b): - return ( - prop_in.flow_vol_phase["Liq"] / b.service_to_regen_flow_ratio - ) * b.t_regen + @self.Expression(doc="Waste time") + def t_waste(b): + return b.t_regen + b.t_bw + b.t_rinse - @self.Expression(doc="Pressure drop") - def pressure_drop(b): - vel_bed = pyunits.convert(b.vel_bed, to_units=pyunits.m / pyunits.hr) - return ( - b.p_drop_A + b.p_drop_B * vel_bed + b.p_drop_C * vel_bed**2 - ) * b.bed_depth # for 20C; + @self.Expression(doc="Cycle time") + def t_cycle(b): + return b.t_breakthru + b.t_waste - @self.Expression(doc="Backwash pump power") - def bw_pump_power(b): - return pyunits.convert( - (b.pressure_drop * b.bw_flow) / b.pump_efficiency, - to_units=pyunits.kilowatts, + @self.Expression(doc="Regen tank volume") + def regen_tank_vol(b): + return ( + prop_in.flow_vol_phase["Liq"] / b.service_to_regen_flow_ratio + ) * b.t_regen + + @self.Expression(doc="Backwash pump power") + def bw_pump_power(b): + return pyunits.convert( + (b.pressure_drop * b.bw_flow) / b.pump_efficiency, + to_units=pyunits.kilowatts, + ) + + @self.Expression(doc="Rinse pump power") + def rinse_pump_power(b): + return pyunits.convert( + (b.pressure_drop * b.rinse_flow) / b.pump_efficiency, + to_units=pyunits.kilowatts, + ) + + @self.Expression(doc="Rinse pump power") + def regen_pump_power(b): + return pyunits.convert( + ( + b.pressure_drop + * ( + prop_in.flow_vol_phase["Liq"] + / b.service_to_regen_flow_ratio + ) + ) + / b.pump_efficiency, + to_units=pyunits.kilowatts, + ) + + @self.Constraint( + self.target_ion_set, doc="Mass transfer for regeneration stream" ) + def eq_mass_transfer_regen(b, j): + return ( + regen.get_material_flow_terms("Liq", j) + == -b.process_flow.mass_transfer_term[0, "Liq", j] + ) - @self.Expression(doc="Rinse pump power") - def rinse_pump_power(b): - return pyunits.convert( - (b.pressure_drop * b.rinse_flow) / b.pump_efficiency, - to_units=pyunits.kilowatts, + @self.Constraint( + doc="Isothermal assumption for regen stream", ) + def eq_isothermal_regen_stream(b): + return prop_in.temperature == regen.temperature - @self.Expression(doc="Rinse pump power") - def regen_pump_power(b): - return pyunits.convert( - ( - b.pressure_drop - * (prop_in.flow_vol_phase["Liq"] / b.service_to_regen_flow_ratio) - ) - / b.pump_efficiency, - to_units=pyunits.kilowatts, + @self.Constraint( + doc="Isobaric assumption for regen stream", ) + def eq_isobaric_regen_stream(b): + return prop_in.pressure == regen.pressure + + for j in inerts: + self.regeneration_stream[0].get_material_flow_terms("Liq", j).fix(0) @self.Expression(doc="Main pump power") def main_pump_power(b): @@ -907,6 +912,20 @@ def main_pump_power(b): to_units=pyunits.kilowatts, ) + @self.Expression(doc="Volume per column") + def col_vol_per(b): + return b.col_height * (b.bed_vol / b.bed_depth) + + @self.Expression(doc="Total column volume required") + def col_vol_tot(b): + return b.number_columns * b.col_vol_per + + @self.Expression( + doc="Bed volumes at breakthrough", + ) + def bv_calc(b): + return (b.vel_bed * b.t_breakthru) / b.bed_depth + if self.config.isotherm == IsothermType.langmuir: @self.Expression(doc="Left hand side of constant pattern sol'n") @@ -938,54 +957,10 @@ def HTU(b, j): * b.rate_coeff[j] ) - if self.config.isotherm == IsothermType.freundlich: - - @self.Expression( - self.target_ion_set, doc="Removed total mass of ion at resin exhaustion" - ) # Croll et al (2023), Eq.16 - def mass_removed_total(b, j): - return (prop_in.flow_mass_phase_comp["Liq", j] / b.kinetic_param) * log( - b.bed_capacity_param + 1 - ) - - @self.Expression( - self.target_ion_set, doc="Freundlich base coeff estimation" - ) - def freundlich_k(b, j): - mass_bed = pyunits.convert( - b.bed_vol_tot * b.resin_bulk_dens, to_units=pyunits.kg - ) - return b.mass_removed_total[j] / ( - mass_bed - * prop_in.conc_mass_phase_comp["Liq", j] ** (1 / b.freundlich_n) - ) - # ==========CONSTRAINTS========== - @self.Constraint( - self.target_ion_set, doc="Mass transfer for regeneration stream" - ) - def eq_mass_transfer_regen(b, j): - return ( - regen.get_material_flow_terms("Liq", j) - == -b.process_flow.mass_transfer_term[0, "Liq", j] - ) - - @self.Constraint( - doc="Isothermal assumption for regen stream", - ) - def eq_isothermal_regen_stream(b): - return prop_in.temperature == regen.temperature - - @self.Constraint( - doc="Isobaric assumption for regen stream", - ) - def eq_isobaric_regen_stream(b): - return prop_in.pressure == regen.pressure - for j in inerts: self.process_flow.mass_transfer_term[:, "Liq", j].fix(0) - self.regeneration_stream[0].get_material_flow_terms("Liq", j).fix(0) # =========== DIMENSIONLESS =========== @@ -1030,7 +1005,7 @@ def eq_resin_surf_per_vol(b): @self.Constraint(doc="Empty bed contact time") def eq_ebct(b): - return b.ebct == b.bed_depth / b.vel_bed + return b.ebct * b.vel_bed == b.bed_depth @self.Constraint(doc="Contact time") def eq_t_contact(b): @@ -1051,10 +1026,16 @@ def eq_bed_flow(b): @self.Constraint(doc="Column height") def eq_col_height(b): - return ( - b.col_height - == b.bed_depth + b.distributor_h + b.underdrain_h + b.bed_expansion_h - ) + if self.config.regenerant != RegenerantChem.single_use: + return ( + b.col_height + == b.bed_depth + + b.distributor_h + + b.underdrain_h + + b.bed_expansion_h + ) + else: + return b.col_height == b.bed_depth + b.distributor_h + b.underdrain_h @self.Constraint(doc="Bed design") def eq_bed_design(b): @@ -1179,8 +1160,6 @@ def eq_bv(b): self.target_ion_set, doc="Clark equation with fundamental constants" ) # Croll et al (2023), Eq.9 def eq_clark_1(b, j): - c0 = prop_in.conc_mass_phase_comp["Liq", j] - cb = b.c_breakthru[j] denom = ( 1 + (2 ** (b.freundlich_n - 1) - 1) @@ -1192,20 +1171,8 @@ def eq_clark_1(b, j): * (b.bv_50 - b.bv) ) ) ** (1 / (b.freundlich_n - 1)) - return c0 == denom * cb - - @self.Constraint( - self.target_ion_set, doc="Clark equation for fitting" - ) # Croll et al (2023), Eq.12 - def eq_clark_2(b, j): - c0 = prop_in.conc_mass_phase_comp["Liq", j] - cb = b.c_breakthru[j] - denom = ( - 1 - + b.bed_capacity_param - * exp((-b.kinetic_param * b.bed_depth * b.bv) / b.vel_bed) - ) ** (1 / (b.freundlich_n - 1)) - return c0 == denom * cb + # return c0 == denom * cb + return denom * b.c_norm[j] == 1 @self.Constraint( self.target_ion_set, @@ -1217,16 +1184,24 @@ def eq_c_traps(b, j, k): (b.c_norm[j] - b.c_trap_min) / (b.num_traps - 1) ) - # b.ebct == b.bed_depth / b.vel_bed @self.Constraint( self.trap_index, doc="Breakthru time calc for trapezoids", ) def eq_tb_traps(b, k): - x = 1 / b.c_traps[k] - return b.tb_traps[k] == (1 / -b.kinetic_param) * log( - (x ** (b.freundlich_n - 1) - 1) / b.bed_capacity_param - ) + bv_traps = (b.tb_traps[k] * b.vel_bed) / b.bed_depth + denom = ( + 1 + + (2 ** (b.freundlich_n - 1) - 1) + * exp( + ( + (b.mass_transfer_coeff * b.bed_depth * (b.freundlich_n - 1)) + / (b.bv_50 * b.vel_bed) + ) + * (b.bv_50 - bv_traps) + ) + ) ** (1 / (b.freundlich_n - 1)) + return denom * b.c_traps[k] == 1 @self.Constraint(self.trap_index, doc="Area of trapezoids") def eq_traps(b, k): @@ -1285,6 +1260,7 @@ def initialize_build( hold_state=True, ) init_log.info("Initialization Step 1a Complete.") + # --------------------------------------------------------------------- # Initialize other state blocks # Set state_args from inlet state @@ -1318,16 +1294,18 @@ def initialize_build( ) init_log.info("Initialization Step 1b Complete.") - state_args_regen = deepcopy(state_args) + if self.config.regenerant != RegenerantChem.single_use: - self.regeneration_stream.initialize( - outlvl=outlvl, - optarg=optarg, - solver=solver, - state_args=state_args_regen, - ) + state_args_regen = deepcopy(state_args) + + self.regeneration_stream.initialize( + outlvl=outlvl, + optarg=optarg, + solver=solver, + state_args=state_args_regen, + ) - init_log.info("Initialization Step 1c Complete.") + init_log.info("Initialization Step 1c Complete.") # Solve unit with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: @@ -1419,9 +1397,6 @@ def calculate_scaling_factors(self): if iscale.get_scaling_factor(self.vel_inter) is None: iscale.set_scaling_factor(self.vel_inter, 1e3) - if iscale.get_scaling_factor(self.regen_dose) is None: - iscale.set_scaling_factor(self.regen_dose, 1e-2) - # unique scaling for isotherm type if isotherm == IsothermType.langmuir: if iscale.get_scaling_factor(self.resin_max_capacity) is None: @@ -1459,9 +1434,6 @@ def calculate_scaling_factors(self): if iscale.get_scaling_factor(self.kinetic_param) is None: iscale.set_scaling_factor(self.kinetic_param, 1e7) - if iscale.get_scaling_factor(self.bed_capacity_param) is None: - iscale.set_scaling_factor(self.bed_capacity_param, 0.1) - if iscale.get_scaling_factor(self.mass_transfer_coeff) is None: iscale.set_scaling_factor(self.mass_transfer_coeff, 1e4) @@ -1503,9 +1475,6 @@ def calculate_scaling_factors(self): iscale.constraint_scaling_transform(c, sf) if isotherm == IsothermType.freundlich: - for ind, c in self.eq_clark_2.items(): - if iscale.get_scaling_factor(c) is None: - iscale.constraint_scaling_transform(c, 1e6) for ind, c in self.eq_clark_1.items(): if iscale.get_scaling_factor(c) is None: @@ -1520,14 +1489,23 @@ def calculate_scaling_factors(self): iscale.constraint_scaling_transform(c, sf) def _get_stream_table_contents(self, time_point=0): - return create_stream_table_dataframe( - { - "Feed Inlet": self.inlet, - "Liquid Outlet": self.outlet, - "Regen Outlet": self.regen, - }, - time_point=time_point, - ) + if self.config.regenerant != RegenerantChem.single_use: + return create_stream_table_dataframe( + { + "Feed Inlet": self.inlet, + "Liquid Outlet": self.outlet, + "Regen Outlet": self.regen, + }, + time_point=time_point, + ) + else: + return create_stream_table_dataframe( + { + "Feed Inlet": self.inlet, + "Liquid Outlet": self.outlet, + }, + time_point=time_point, + ) def _get_performance_contents(self, time_point=0): @@ -1540,7 +1518,6 @@ def _get_performance_contents(self, time_point=0): var_dict[f"Relative Breakthrough Conc. [{target_ion}]"] = self.c_norm[ target_ion ] - var_dict["Regen Dose"] = self.regen_dose var_dict["Number Columns"] = self.number_columns var_dict["Bed Volume Total"] = self.bed_vol_tot var_dict["Bed Depth"] = self.bed_depth @@ -1574,7 +1551,6 @@ def _get_performance_contents(self, time_point=0): var_dict[f"BV at 50% Breakthrough"] = self.bv_50 var_dict[f"Freundlich n"] = self.freundlich_n var_dict[f"Clark Mass Transfer Coeff."] = self.mass_transfer_coeff - var_dict[f"Clark Bed Capacity Param."] = self.bed_capacity_param var_dict[f"Clark Kinetic Param."] = self.kinetic_param return {"vars": var_dict} From de400c5fe2f9305b58c8a2e7133254a6f43a8a90 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Mon, 18 Sep 2023 16:25:33 -0600 Subject: [PATCH 02/27] update costing relationships, +single-use approach --- watertap/costing/units/ion_exchange.py | 298 ++++++++++++++----------- 1 file changed, 172 insertions(+), 126 deletions(-) diff --git a/watertap/costing/units/ion_exchange.py b/watertap/costing/units/ion_exchange.py index 860f4718ae..a875243fc6 100644 --- a/watertap/costing/units/ion_exchange.py +++ b/watertap/costing/units/ion_exchange.py @@ -108,69 +108,45 @@ def build_ion_exhange_cost_param_block(blk): units=pyo.units.USD_2020 / pyo.units.ft**3, doc="Cation exchange resin cost per cubic ft. Assumes strong acid polystyrenic gel-type. From EPA-WBS cost model.", ) - # Ion exchange pressure vessels costed with 3rd order polynomial: - # pv_cost = A * col_vol^3 + B * col_vol^2 + C * col_vol + intercept + # Ion exchange pressure vessels costed with power equation, col_vol in gallons: + # pressure_vessel_cost = A * col_vol ** b - blk.vessel_intercept = pyo.Var( - initialize=10010.86, - units=pyo.units.USD_2020, - doc="Ion exchange pressure vessel cost equation - intercept, Carbon steel w/ plastic internals", - ) blk.vessel_A_coeff = pyo.Var( - initialize=6e-9, - units=pyo.units.USD_2020 / pyo.units.gal**3, - doc="Ion exchange pressure vessel cost equation - A coeff., Carbon steel w/ plastic internals", - ) - blk.vessel_B_coeff = pyo.Var( - initialize=-2.284e-4, - units=pyo.units.USD_2020 / pyo.units.gal**2, - doc="Ion exchange pressure vessel cost equation - B coeff., Carbon steel w/ plastic internals", + initialize=1596.499333, + units=pyo.units.USD_2020, + doc="Ion exchange pressure vessel cost equation - A coeff., Carbon steel w/ stainless steel internals", ) - blk.vessel_C_coeff = pyo.Var( - initialize=8.3472, - units=pyo.units.USD_2020 / pyo.units.gal, - doc="Ion exchange pressure vessel cost equation - C coeff., Carbon steel w/ plastic internals", + blk.vessel_b_coeff = pyo.Var( + initialize=0.459496809, + units=pyo.units.dimensionless, + doc="Ion exchange pressure vessel cost equation - b coeff., Carbon steel w/ stainless steel internals", ) - # Ion exchange backwash/rinse tank costed with 3rd order polynomial: - # pv_cost = A * tank_vol^3 + B * tank_vol^2 + C * tank_vol + intercept + + # Ion exchange backwash/rinse tank costed with power equation, tank_vol in gallons: + # bw_tank_cost = A * tank_vol ** b blk.backwash_tank_A_coeff = pyo.Var( - initialize=1e-9, - units=pyo.units.USD_2020 / pyo.units.gal**3, - doc="Ion exchange backwash tank cost equation - A coeff., Fiberglass tank", - ) - blk.backwash_tank_B_coeff = pyo.Var( - initialize=-5.8587e-05, - units=pyo.units.USD_2020 / pyo.units.gal**2, - doc="Ion exchange backwash tank cost equation - B coeff., Fiberglass tank", - ) - blk.backwash_tank_C_coeff = pyo.Var( - initialize=2.2911, - units=pyo.units.USD_2020 / pyo.units.gal, - doc="Ion exchange backwash tank cost equation - C coeff., Fiberglass tank", - ) - blk.backwash_tank_intercept = pyo.Var( - initialize=4717.255, + initialize=308.9371309, units=pyo.units.USD_2020, - doc="Ion exchange backwash tank cost equation - intercept, Fiberglass tank", + doc="Ion exchange backwash tank cost equation - A coeff., Steel tank", ) - # Ion exchange regeneration solution tank costed with 2nd order polynomial: - # regen_tank_cost = A * tank_vol^2 + B * tank_vol + intercept - - blk.regen_tank_intercept = pyo.Var( - initialize=4408.327, - units=pyo.units.USD_2020, - doc="Ion exchange regen tank cost equation - intercept. Stainless steel", + blk.backwash_tank_b_coeff = pyo.Var( + initialize=0.501467571, + units=pyo.units.dimensionless, + doc="Ion exchange backwash tank cost equation - b coeff., Steel tank", ) + # Ion exchange regeneration solution tank costed with power equation, tank_vol in gallons: + # regen_tank_cost = A * tank_vol ** b + blk.regen_tank_A_coeff = pyo.Var( - initialize=-3.258e-5, - units=pyo.units.USD_2020 / pyo.units.gal**2, + initialize=57.02158923, + units=pyo.units.USD_2020, doc="Ion exchange regen tank cost equation - A coeff. Stainless steel", ) - blk.regen_tank_B_coeff = pyo.Var( - initialize=3.846, - units=pyo.units.USD_2020 / pyo.units.gal, - doc="Ion exchange regen tank cost equation - B coeff. Stainless steel", + blk.regen_tank_b_coeff = pyo.Var( + initialize=0.729325391, + units=pyo.units.dimensionless, + doc="Ion exchange regen tank cost equation - b coeff. Stainless steel", ) blk.annual_resin_replacement_factor = pyo.Var( initialize=0.05, @@ -239,19 +215,14 @@ def cost_ion_exchange(blk): blk.unit_model.bed_vol * blk.unit_model.resin_bulk_dens, to_units=pyo.units.ton, ) - bw_tank_vol = pyo.units.convert( - ( - blk.unit_model.bw_flow * blk.unit_model.t_bw - + blk.unit_model.rinse_flow * blk.unit_model.t_rinse - ), - to_units=pyo.units.gal, - ) - regen_tank_vol = pyo.units.convert( - blk.unit_model.regen_tank_vol, - to_units=pyo.units.gal, - ) - ix_type = blk.unit_model.ion_exchange_type + ix_type = blk.unit_model.ion_exchange_type + blk.regen_dose = pyo.Param( + initialize=300, + units=pyo.units.kg / pyo.units.m**3, + mutable=True, + doc="Regenerant dose required for regeneration per volume of resin [kg regenerant/m3 resin]", + ) blk.capital_cost_vessel = pyo.Var( initialize=1e5, domain=pyo.NonNegativeReals, @@ -282,7 +253,7 @@ def cost_ion_exchange(blk): units=blk.costing_package.base_currency / blk.costing_package.base_period, doc="Operating cost for hazardous waste disposal", ) - blk.regen_soln_flow = pyo.Var( + blk.vol_flow_regen_soln = pyo.Var( initialize=1, bounds=(0, None), units=pyo.units.kg / pyo.units.year, @@ -304,10 +275,10 @@ def cost_ion_exchange(blk): blk.capital_cost_vessel_constraint = pyo.Constraint( expr=blk.capital_cost_vessel == pyo.units.convert( - ion_exchange_params.vessel_intercept - + ion_exchange_params.vessel_A_coeff * col_vol_gal**3 - + ion_exchange_params.vessel_B_coeff * col_vol_gal**2 - + ion_exchange_params.vessel_C_coeff * col_vol_gal, + ( + ion_exchange_params.vessel_A_coeff + * (col_vol_gal / pyo.units.gallon) ** ion_exchange_params.vessel_b_coeff + ), to_units=blk.costing_package.base_currency, ) ) @@ -317,25 +288,64 @@ def cost_ion_exchange(blk): resin_cost * bed_vol_ft3, to_units=blk.costing_package.base_currency ) ) - blk.capital_cost_backwash_tank_constraint = pyo.Constraint( - expr=blk.capital_cost_backwash_tank - == pyo.units.convert( - ion_exchange_params.backwash_tank_intercept - + ion_exchange_params.backwash_tank_A_coeff * bw_tank_vol**3 - + ion_exchange_params.backwash_tank_B_coeff * bw_tank_vol**2 - + ion_exchange_params.backwash_tank_C_coeff * bw_tank_vol, - to_units=blk.costing_package.base_currency, + if blk.unit_model.config.regenerant == "single_use": + blk.capital_cost_backwash_tank.fix(0) + blk.capital_cost_regen_tank.fix(0) + blk.vol_flow_regen_soln.fix(0) + blk.vol_flow_resin = pyo.Var( + initialize=1e5, + bounds=(0, None), + units=pyo.units.m**3 / blk.costing_package.base_period, + doc="Volumetric flow of resin per cycle", # assumes you are only replacing the operational columns ) - ) - blk.capital_cost_regen_tank_constraint = pyo.Constraint( - expr=blk.capital_cost_regen_tank - == pyo.units.convert( - ion_exchange_params.regen_tank_intercept - + ion_exchange_params.regen_tank_A_coeff * regen_tank_vol**2 - + ion_exchange_params.regen_tank_B_coeff * regen_tank_vol, - to_units=blk.costing_package.base_currency, + blk.operating_cost_single_use_resin = pyo.Var( + initialize=1e5, + bounds=(0, None), + units=blk.costing_package.base_currency / blk.costing_package.base_period, + doc="Operating cost for using single-use resin (i.e., no regeneration)", + ) + + blk.vol_flow_resin_constraint = pyo.Constraint( + expr=blk.vol_flow_resin + == pyo.units.convert( + blk.unit_model.bed_vol_tot / blk.unit_model.t_breakthru, + to_units=pyo.units.m**3 / blk.costing_package.base_period, + ) + ) + blk.mass_flow_resin = pyo.units.convert( + blk.vol_flow_resin * blk.unit_model.resin_bulk_dens, + to_units=pyo.units.ton / blk.costing_package.base_period, + ) + else: + bw_tank_vol = pyo.units.convert( + ( + blk.unit_model.bw_flow * blk.unit_model.t_bw + + blk.unit_model.rinse_flow * blk.unit_model.t_rinse + ), + to_units=pyo.units.gal, + ) + regen_tank_vol = pyo.units.convert( + blk.unit_model.regen_tank_vol, + to_units=pyo.units.gal, + ) + blk.capital_cost_backwash_tank_constraint = pyo.Constraint( + expr=blk.capital_cost_backwash_tank + == pyo.units.convert( + ion_exchange_params.backwash_tank_A_coeff + * (bw_tank_vol / pyo.units.gallon) + ** ion_exchange_params.backwash_tank_b_coeff, + to_units=blk.costing_package.base_currency, + ) + ) + blk.capital_cost_regen_tank_constraint = pyo.Constraint( + expr=blk.capital_cost_regen_tank + == pyo.units.convert( + ion_exchange_params.regen_tank_A_coeff + * (regen_tank_vol / pyo.units.gallon) + ** ion_exchange_params.regen_tank_b_coeff, + to_units=blk.costing_package.base_currency, + ) ) - ) blk.capital_cost_constraint = pyo.Constraint( expr=blk.capital_cost == pyo.units.convert( @@ -350,65 +360,101 @@ def cost_ion_exchange(blk): ) if blk.unit_model.config.hazardous_waste: blk.regen_dens = 1000 * pyo.units.kg / pyo.units.m**3 - blk.regen_soln_vol_flow = pyo.units.convert( - blk.regen_soln_flow / blk.regen_dens, - to_units=pyo.units.gal / pyo.units.year, + + if blk.unit_model.config.regenerant == "single_use": + blk.operating_cost_hazardous_constraint = pyo.Constraint( + expr=blk.operating_cost_hazardous + == pyo.units.convert( + (blk.mass_flow_resin * ion_exchange_params.hazardous_resin_disposal) + + ion_exchange_params.hazardous_min_cost, + to_units=blk.costing_package.base_currency + / blk.costing_package.base_period, + ) + ) + else: + blk.operating_cost_hazardous_constraint = pyo.Constraint( + expr=blk.operating_cost_hazardous + == pyo.units.convert( + ( + +bed_mass_ton + * tot_num_col + * ion_exchange_params.hazardous_resin_disposal + ) + * ion_exchange_params.annual_resin_replacement_factor + + pyo.units.convert( + blk.vol_flow_regen_soln / blk.regen_dens, + to_units=pyo.units.gal / pyo.units.year, + ) + * ion_exchange_params.hazardous_regen_disposal + + ion_exchange_params.hazardous_min_cost, + to_units=blk.costing_package.base_currency + / blk.costing_package.base_period, + ) + ) + else: + blk.operating_cost_hazardous.fix(0) + if blk.unit_model.config.regenerant == "single_use": + + blk.operating_cost_single_use_resin_constraint = pyo.Constraint( + expr=blk.operating_cost_single_use_resin + == pyo.units.convert( + blk.vol_flow_resin * resin_cost, + to_units=blk.costing_package.base_currency + / blk.costing_package.base_period, + ) + ) + + blk.fixed_operating_cost_constraint = pyo.Constraint( + expr=blk.fixed_operating_cost + == blk.operating_cost_single_use_resin + blk.operating_cost_hazardous ) - blk.operating_cost_hazardous_constraint = pyo.Constraint( - expr=blk.operating_cost_hazardous + + else: + blk.fixed_operating_cost_constraint = pyo.Constraint( + expr=blk.fixed_operating_cost == pyo.units.convert( ( - +bed_mass_ton - * tot_num_col - * ion_exchange_params.hazardous_resin_disposal - ) - * ion_exchange_params.annual_resin_replacement_factor - + blk.regen_soln_vol_flow * ion_exchange_params.hazardous_regen_disposal - + ion_exchange_params.hazardous_min_cost, + ( + bed_vol_ft3 + * tot_num_col + * ion_exchange_params.annual_resin_replacement_factor + * resin_cost + ) + ), to_units=blk.costing_package.base_currency / blk.costing_package.base_period, ) + + blk.operating_cost_hazardous ) - else: - blk.operating_cost_hazardous.fix(0) - blk.fixed_operating_cost_constraint = pyo.Constraint( - expr=blk.fixed_operating_cost - == pyo.units.convert( - ( + + blk.vol_flow_regen_soln_constraint = pyo.Constraint( + expr=blk.vol_flow_regen_soln + == pyo.units.convert( ( - bed_vol_ft3 - * tot_num_col - * ion_exchange_params.annual_resin_replacement_factor - * resin_cost + (blk.regen_dose * blk.unit_model.bed_vol * tot_num_col) + / (blk.unit_model.t_cycle) ) - ), - to_units=blk.costing_package.base_currency - / blk.costing_package.base_period, + / ion_exchange_params.regen_recycle, + to_units=pyo.units.kg / pyo.units.year, + ) ) - + blk.operating_cost_hazardous - ) - blk.regen_soln_flow_constr = pyo.Constraint( - expr=blk.regen_soln_flow - == pyo.units.convert( - ( - (blk.unit_model.regen_dose * blk.unit_model.bed_vol * tot_num_col) - / (blk.unit_model.t_cycle) - ) - / ion_exchange_params.regen_recycle, - to_units=pyo.units.kg / pyo.units.year, + blk.costing_package.cost_flow( + blk.vol_flow_regen_soln, blk.unit_model.config.regenerant ) - ) - blk.total_pumping_power_constr = pyo.Constraint( - expr=blk.total_pumping_power - == ( + if blk.unit_model.config.regenerant == "single_use": + power_expr = blk.unit_model.main_pump_power + else: + power_expr = ( blk.unit_model.main_pump_power + blk.unit_model.regen_pump_power + blk.unit_model.bw_pump_power + blk.unit_model.rinse_pump_power ) + + blk.total_pumping_power_constr = pyo.Constraint( + expr=blk.total_pumping_power == power_expr ) - blk.costing_package.cost_flow(blk.regen_soln_flow, blk.unit_model.config.regenerant) blk.costing_package.cost_flow(blk.total_pumping_power, "electricity") From 11f853dfb87c3fc1e0f00234a822a65ab5e8035d Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Mon, 18 Sep 2023 16:26:09 -0600 Subject: [PATCH 03/27] update test file costing, new model, +single-use --- .../unit_models/tests/test_ion_exchange_0D.py | 361 ++++++++++++++++-- 1 file changed, 323 insertions(+), 38 deletions(-) diff --git a/watertap/unit_models/tests/test_ion_exchange_0D.py b/watertap/unit_models/tests/test_ion_exchange_0D.py index 11a38cdb4d..7211090957 100644 --- a/watertap/unit_models/tests/test_ion_exchange_0D.py +++ b/watertap/unit_models/tests/test_ion_exchange_0D.py @@ -94,7 +94,6 @@ def IX_lang(self): ix.resin_diam.fix() ix.resin_bulk_dens.fix() ix.bed_porosity.fix() - ix.regen_dose.fix() return m @@ -169,7 +168,6 @@ def test_default_build(self, IX_lang): "resin_diam", "resin_bulk_dens", "resin_surf_per_vol", - "regen_dose", "c_norm", "col_height_to_diam_ratio", "bed_vol_tot", @@ -206,9 +204,9 @@ def test_default_build(self, IX_lang): assert isinstance(var, Var) # test statistics - assert number_variables(m) == 69 + assert number_variables(m) == 68 assert number_total_constraints(m) == 42 - assert number_unused_variables(m) == 12 + assert number_unused_variables(m) == 11 @pytest.mark.unit def test_dof(self, IX_lang): @@ -254,6 +252,7 @@ def test_solution(self, IX_lang): "resin_max_capacity": 3, "resin_eq_capacity": 1.5547810762853227, "resin_unused_capacity": 1.4452189237146773, + # "regen_dose": 300, "resin_diam": 0.0007, "resin_bulk_dens": 0.7, "langmuir": 0.9, @@ -281,7 +280,6 @@ def test_solution(self, IX_lang): "N_Pe_particle": 0.10782790064157834, "N_Pe_bed": 261.86775870097597, "c_norm": 0.4919290557789296, - "regen_dose": 300, } for v, val in results_dict.items(): @@ -307,17 +305,17 @@ def test_costing(self, IX_lang): results = solver.solve(m, tee=True) assert_optimal_termination(results) - assert pytest.approx(8894349.86900, rel=1e-3) == value( + assert pytest.approx(3294284.78759, rel=1e-3) == value( m.fs.costing.aggregate_capital_cost ) - assert pytest.approx(2498819.7327, rel=1e-3) == value( + assert pytest.approx(2162801.2301, rel=1e-3) == value( m.fs.costing.total_operating_cost ) - assert pytest.approx(17788699.7380, rel=1e-3) == value( + assert pytest.approx(6588569.57518, rel=1e-3) == value( m.fs.costing.total_capital_cost ) - assert pytest.approx(0.30125629, rel=1e-3) == value(m.fs.costing.LCOW) - assert pytest.approx(0.0572452, rel=1e-3) == value( + assert pytest.approx(0.19871527, rel=1e-3) == value(m.fs.costing.LCOW) + assert pytest.approx(0.05723051, rel=1e-3) == value( m.fs.costing.specific_energy_consumption ) @@ -362,7 +360,6 @@ def IX_fr(self): ix.bv_50.fix(20000) ix.bv.fix(18000) ix.resin_bulk_dens.fix(0.72) - ix.regen_dose.fix() ix.bed_porosity.fix() ix.vel_bed.fix(6.15e-3) ix.resin_diam.fix(6.75e-4) @@ -394,6 +391,7 @@ def test_config(self, IX_fr): @pytest.mark.unit def test_default_build(self, IX_fr): + m = IX_fr ix = m.fs.ix # test ports and variables @@ -420,19 +418,19 @@ def test_default_build(self, IX_fr): "Sh_exp_A", "Sh_exp_B", "Sh_exp_C", - "bed_expansion_frac_A", - "bed_expansion_frac_B", - "bed_expansion_frac_C", + "number_columns_redund", "p_drop_A", "p_drop_B", "p_drop_C", "pump_efficiency", + "bed_expansion_frac_A", + "bed_expansion_frac_B", + "bed_expansion_frac_C", "t_regen", "rinse_bv", "bw_rate", "t_bw", "service_to_regen_flow_ratio", - "number_columns_redund", "c_trap_min", ] @@ -445,7 +443,6 @@ def test_default_build(self, IX_fr): "resin_diam", "resin_bulk_dens", "resin_surf_per_vol", - "regen_dose", "c_norm", "bed_vol_tot", "bed_depth", @@ -474,7 +471,6 @@ def test_default_build(self, IX_fr): "mass_transfer_coeff", "bv", "bv_50", - "bed_capacity_param", "kinetic_param", ] @@ -484,9 +480,9 @@ def test_default_build(self, IX_fr): assert isinstance(var, Var) # test statistics - assert number_variables(m) == 82 - assert number_total_constraints(m) == 52 - assert number_unused_variables(m) == 13 + assert number_variables(m) == 80 + assert number_total_constraints(m) == 51 + assert number_unused_variables(m) == 12 @pytest.mark.unit def test_dof(self, IX_fr): @@ -533,7 +529,7 @@ def test_solution(self, IX_fr): "resin_diam": 0.0006749999999999999, "resin_bulk_dens": 0.72, "resin_surf_per_vol": 4444.444444444445, - "regen_dose": 300, + # "regen_dose": 300, "c_norm": {"Cl_-": 0.25}, "bed_vol_tot": 120.00000000000001, "bed_depth": 1.476, @@ -582,7 +578,7 @@ def test_solution(self, IX_fr): "mass_transfer_coeff": 0.159346300525143, "bv": 18000, "bv_50": 20000, - "bed_capacity_param": 311.9325370632754, + # "bed_capacity_param": 311.9325370632754, "kinetic_param": 1.5934630052514297e-06, } @@ -611,21 +607,315 @@ def test_costing(self, IX_fr): results = solver.solve(m, tee=True) assert_optimal_termination(results) - assert pytest.approx(9701947.4187, rel=1e-3) == value( + assert pytest.approx(4220876.29698, rel=1e-3) == value( m.fs.costing.aggregate_capital_cost ) - assert pytest.approx(1448862.0602, rel=1e-3) == value( + assert pytest.approx(1119986.6162, rel=1e-3) == value( m.fs.costing.total_operating_cost ) - assert pytest.approx(19403894.837, rel=1e-3) == value( + assert pytest.approx(8441752.59397, rel=1e-3) == value( m.fs.costing.total_capital_cost ) - assert pytest.approx(0.238664, rel=1e-3) == value(m.fs.costing.LCOW) + assert pytest.approx(0.1383122, rel=1e-3) == value(m.fs.costing.LCOW) assert pytest.approx(0.04382530, rel=1e-3) == value( m.fs.costing.specific_energy_consumption ) +class TestIonExchangeSingleUse: + @pytest.fixture(scope="class") + def IX_single_use(self): + + target_ion = "Cl_-" + + ion_props = { + "solute_list": [target_ion], + "diffusivity_data": {("Liq", target_ion): 1e-9}, + "mw_data": {"H2O": 0.018, target_ion: 35.45e-3}, + "charge": {target_ion: -1}, + } + + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + m.fs.properties = MCASParameterBlock(**ion_props) + ix_config = { + "property_package": m.fs.properties, + "target_ion": target_ion, + "isotherm": "freundlich", + "regenerant": "single_use", + "hazardous_waste": True, + } + m.fs.ix = ix = IonExchange0D(**ix_config) + + # c0 = pyunits.convert(c0, to_units=pyunits.kg / pyunits.m**3) + ix.process_flow.properties_in.calculate_state( + var_args={ + ("flow_vol_phase", "Liq"): 0.5, + ("conc_mass_phase_comp", ("Liq", target_ion)): 1e-6, + ("pressure", None): 101325, + ("temperature", None): 298, + }, + hold_state=True, + ) + + ix.freundlich_n.fix(1.2) + ix.bv_50.fix(20000) + ix.bv.fix(18000) + ix.resin_bulk_dens.fix(0.72) + ix.bed_porosity.fix() + ix.vel_bed.fix(6.15e-3) + ix.resin_diam.fix(6.75e-4) + ix.number_columns.fix(18) + ix.service_flow_rate.fix(15) + ix.c_norm.fix(0.25) + + return m + + @pytest.mark.unit + def test_config(self, IX_single_use): + m = IX_single_use + + assert len(m.fs.ix.config) == 11 + + assert not m.fs.ix.config.dynamic + assert not m.fs.ix.config.has_holdup + assert m.fs.ix.config.property_package is m.fs.properties + assert m.fs.ix.config.hazardous_waste + assert m.fs.ix.config.regenerant is RegenerantChem.single_use + assert isinstance(m.fs.ix.ion_exchange_type, IonExchangeType) + assert m.fs.ix.ion_exchange_type is IonExchangeType.anion + assert isinstance(m.fs.ix.config.isotherm, IsothermType) + assert m.fs.ix.config.isotherm is IsothermType.freundlich + assert isinstance(m.fs.ix.config.energy_balance_type, EnergyBalanceType) + assert m.fs.ix.config.energy_balance_type is EnergyBalanceType.none + assert isinstance(m.fs.ix.config.momentum_balance_type, MomentumBalanceType) + assert m.fs.ix.config.momentum_balance_type is MomentumBalanceType.pressureTotal + + @pytest.mark.unit + def test_default_build(self, IX_single_use): + m = IX_single_use + ix = m.fs.ix + # test ports and variables + + port_lst = ["inlet", "outlet"] + port_vars_lst = ["flow_mol_phase_comp", "pressure", "temperature"] + for port_str in port_lst: + assert hasattr(ix, port_str) + port = getattr(ix, port_str) + assert len(port.vars) == 3 + assert isinstance(port, Port) + for var_str in port_vars_lst: + assert hasattr(port, var_str) + var = getattr(port, var_str) + assert isinstance(var, Var) + + assert not hasattr(ix, "regen") + assert not hasattr(ix, "regeneration_stream") + + # test unit objects + ix_params = [ + "underdrain_h", + "distributor_h", + "Pe_p_A", + "Pe_p_exp", + "Sh_A", + "Sh_exp_A", + "Sh_exp_B", + "Sh_exp_C", + "number_columns_redund", + "p_drop_A", + "p_drop_B", + "p_drop_C", + "pump_efficiency", + "c_trap_min", + ] + + for p in ix_params: + assert hasattr(ix, p) + param = getattr(ix, p) + assert isinstance(param, Param) + + ix_vars = [ + "resin_diam", + "resin_bulk_dens", + "resin_surf_per_vol", + "c_norm", + "bed_vol_tot", + "bed_depth", + "bed_porosity", + "col_height", + "col_diam", + "col_height_to_diam_ratio", + "number_columns", + "t_breakthru", + "t_contact", + "ebct", + "vel_bed", + "vel_inter", + "service_flow_rate", + "N_Re", + "N_Sc", + "N_Sh", + "N_Pe_particle", + "N_Pe_bed", + "c_traps", + "tb_traps", + "traps", + "c_norm_avg", + "c_breakthru", + "freundlich_n", + "mass_transfer_coeff", + "bv", + "bv_50", + "kinetic_param", + ] + + for v in ix_vars: + assert hasattr(ix, v) + var = getattr(ix, v) + assert isinstance(var, Var) + + # test statistics + assert number_variables(m) == 76 + assert number_total_constraints(m) == 48 + assert number_unused_variables(m) == 11 + + @pytest.mark.unit + def test_dof(self, IX_single_use): + m = IX_single_use + check_dof(m, fail_flag=True) + + @pytest.mark.unit + def test_calculate_scaling(self, IX_single_use): + m = IX_single_use + m.fs.properties.set_default_scaling( + "flow_mol_phase_comp", 1e-4, index=("Liq", "H2O") + ) + m.fs.properties.set_default_scaling( + "flow_mol_phase_comp", 1e6, index=("Liq", "Cl_-") + ) + calculate_scaling_factors(m) + + # check that all variables have scaling factors + unscaled_var_list = list(unscaled_variables_generator(m)) + assert len(unscaled_var_list) == 0 + + @pytest.mark.requires_idaes_solver + @pytest.mark.component + def test_initialize(self, IX_single_use): + m = IX_single_use + initialization_tester(m, unit=m.fs.ix, outlvl=idaeslog.DEBUG) + + @pytest.mark.requires_idaes_solver + @pytest.mark.component + def test_solve(self, IX_single_use): + m = IX_single_use + results = solver.solve(m, tee=True) + assert_units_consistent(m) + # Check for optimal solution + assert_optimal_termination(results) + + @pytest.mark.requires_idaes_solver + @pytest.mark.component + def test_solution(self, IX_single_use): + m = IX_single_use + ix = m.fs.ix + target_ion = ix.config.target_ion + results_dict = { + "resin_diam": 0.000675, + "resin_bulk_dens": 0.72, + "resin_surf_per_vol": 4444.444444444444, + "c_norm": {"Cl_-": 0.25}, + "bed_vol_tot": 120.00000000000001, + "bed_depth": 1.476, + "bed_porosity": 0.5, + "col_height": 2.476, + "col_diam": 2.3980942681530144, + "col_height_to_diam_ratio": 1.0324865176826379, + "number_columns": 18, + "t_breakthru": 4320000.000000001, + "t_contact": 120.0, + "ebct": 240.0, + "vel_bed": 0.00615, + "vel_inter": 0.0123, + "service_flow_rate": 15, + "N_Re": 4.15125, + "N_Sc": {"Cl_-": 1000.0}, + "N_Sh": {"Cl_-": 24.083093218519274}, + "N_Pe_particle": 0.09901383248136636, + "N_Pe_bed": 216.5102470259211, + "c_traps": { + 0: 0, + 1: 0.01, + 2: 0.06999999999999999, + 3: 0.13, + 4: 0.19, + 5: 0.25, + }, + "tb_traps": { + 0: 0, + 1: 3344557.580567036, + 2: 3825939.149532492, + 3: 4034117.3864088715, + 4: 4188551.1118896306, + 5: 4320000.000000001, + }, + "traps": { + 1: 0.003871015718248884, + 2: 0.004457236749680146, + 3: 0.004818940668434715, + 4: 0.005719767610398469, + 5: 0.0066941563389540295, + }, + "c_norm_avg": {"Cl_-": 0.025561117085716244}, + "c_breakthru": {"Cl_-": 2.5000000020167303e-07}, + "freundlich_n": 1.2, + "mass_transfer_coeff": 0.15934630052514298, + "bv": 18000, + "bv_50": 20000, + "kinetic_param": 1.5934630052514293e-06, + } + + for k, v in results_dict.items(): + var = getattr(ix, k) + if isinstance(v, dict): + for i, u in v.items(): + assert pytest.approx(u, rel=1e-3) == value(var[i]) + else: + assert pytest.approx(v, rel=1e-3) == value(var) + + @pytest.mark.component + def test_costing(self, IX_single_use): + m = IX_single_use + ix = m.fs.ix + + m.fs.costing = WaterTAPCosting() + ix.costing = UnitModelCostingBlock(flowsheet_costing_block=m.fs.costing) + m.fs.costing.cost_process() + m.fs.costing.add_LCOW(ix.process_flow.properties_out[0].flow_vol_phase["Liq"]) + m.fs.costing.add_specific_energy_consumption( + ix.process_flow.properties_out[0].flow_vol_phase["Liq"] + ) + ix.initialize() + + results = solver.solve(m, tee=True) + assert_optimal_termination(results) + + assert pytest.approx(3521557.0901, rel=1e-3) == value( + m.fs.costing.aggregate_capital_cost + ) + assert pytest.approx(6895468.46934, rel=1e-3) == value( + m.fs.costing.total_operating_cost + ) + assert pytest.approx(7043114.18037, rel=1e-3) == value( + m.fs.costing.total_capital_cost + ) + assert pytest.approx(0.535161094, rel=1e-3) == value(m.fs.costing.LCOW) + assert pytest.approx(0.01712042, rel=1e-3) == value( + m.fs.costing.specific_energy_consumption + ) + + class TestIonExchangeInert: @pytest.fixture(scope="class") def IX_inert(self): @@ -665,7 +955,6 @@ def IX_inert(self): ix.bv_50.fix(20000) ix.bv.fix(18000) ix.resin_bulk_dens.fix(0.72) - ix.regen_dose.fix() ix.bed_porosity.fix() ix.vel_bed.fix(6.15e-3) ix.resin_diam.fix(6.75e-4) @@ -748,7 +1037,6 @@ def test_default_build(self, IX_inert): "resin_diam", "resin_bulk_dens", "resin_surf_per_vol", - "regen_dose", "c_norm", "bed_vol_tot", "bed_depth", @@ -777,7 +1065,6 @@ def test_default_build(self, IX_inert): "mass_transfer_coeff", "bv", "bv_50", - "bed_capacity_param", "kinetic_param", ] @@ -787,9 +1074,9 @@ def test_default_build(self, IX_inert): assert isinstance(var, Var) # test statistics - assert number_variables(m) == 90 - assert number_total_constraints(m) == 56 - assert number_unused_variables(m) == 15 + assert number_variables(m) == 88 + assert number_total_constraints(m) == 55 + assert number_unused_variables(m) == 14 @pytest.mark.unit def test_dof(self, IX_inert): @@ -832,7 +1119,6 @@ def test_solution(self, IX_inert): "resin_diam": 0.0006749999999999999, "resin_bulk_dens": 0.72, "resin_surf_per_vol": 4444.444444444445, - "regen_dose": 300, "c_norm": {"Cl_-": 0.25}, "bed_vol_tot": 120.0, "bed_depth": 1.4759999999999998, @@ -881,7 +1167,6 @@ def test_solution(self, IX_inert): "mass_transfer_coeff": 0.159346300525143, "bv": 18000, "bv_50": 20000, - "bed_capacity_param": 311.93253706327357, "kinetic_param": 1.59346300525143e-06, } @@ -910,16 +1195,16 @@ def test_costing(self, IX_inert): results = solver.solve(m, tee=True) assert_optimal_termination(results) - assert pytest.approx(9701947.4187, rel=1e-3) == value( + assert pytest.approx(4220876.29698, rel=1e-3) == value( m.fs.costing.aggregate_capital_cost ) - assert pytest.approx(695243.5958, rel=1e-3) == value( + assert pytest.approx(366368.1517, rel=1e-3) == value( m.fs.costing.total_operating_cost ) - assert pytest.approx(19403894.837, rel=1e-3) == value( + assert pytest.approx(8441752.59397, rel=1e-3) == value( m.fs.costing.total_capital_cost ) - assert pytest.approx(0.18559, rel=1e-3) == value(m.fs.costing.LCOW) + assert pytest.approx(0.085244, rel=1e-3) == value(m.fs.costing.LCOW) assert pytest.approx(0.04382530, rel=1e-3) == value( m.fs.costing.specific_energy_consumption ) From 6ecffe2d377f2cb116c23aa6abc4f948b9b74ffd Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Mon, 18 Sep 2023 17:50:17 -0600 Subject: [PATCH 04/27] update IX docs --- .../unit_models/ion_exchange_0D.rst | 89 ++++++++++++------- watertap/costing/units/ion_exchange.py | 11 ++- 2 files changed, 66 insertions(+), 34 deletions(-) diff --git a/docs/technical_reference/unit_models/ion_exchange_0D.rst b/docs/technical_reference/unit_models/ion_exchange_0D.rst index 8d8cabcf44..9d1f8a188c 100644 --- a/docs/technical_reference/unit_models/ion_exchange_0D.rst +++ b/docs/technical_reference/unit_models/ion_exchange_0D.rst @@ -34,6 +34,9 @@ At this time, the mass transfer zone is approaching the end of the ion exchange and the regeneration cycle can begin. Fundamental to this model is the assumption that the isotherm between the solute and the resin is favorable, and thus the mass transfer zone is shallow. +.. note:: + If using ``single-use`` configuration for ``regenerant``, the backwashing, regeneration, and rinsing steps are not modeled and all associated costs for these steps are zero. + Isotherm Configurations ^^^^^^^^^^^^^^^^^^^^^^^ @@ -80,7 +83,7 @@ The model provides three ports (Pyomo notation in parenthesis): * Inlet port (inlet) * Outlet port (outlet) -* Regeneration port (regen) +* Regeneration port (regen, only if not using ``single_use`` resin configuration) Sets ---- @@ -136,7 +139,6 @@ The ion exchange model includes many variables, parameters, and expressions that "Resin bulk density", ":math:`\rho_{b}`", "``resin_bulk_dens``", "None", ":math:`\text{kg/L}`" "Resin surface area per volume", ":math:`a_{s}`", "``resin_surf_per_vol``", "None", ":math:`\text{m}^{-1}`" "Bed porosity", ":math:`\epsilon`", "``bed_porosity``", "None", ":math:`\text{dimensionless}`" - "Regenerant dose per volume of resin", ":math:`C_{regen}`", "``regen_dose``", "None", ":math:`\text{kg/}\text{m}^3`" "Number of cycles before regenerant disposal", ":math:`N_{regen}`", "``regen_recycle``", "None", ":math:`\text{dimensionless}`" "Relative breakthrough concentration at breakthrough time ", ":math:`X`", "``c_norm``", "``target_ion_set``", ":math:`\text{dimensionless}`" "Breakthrough time", ":math:`t_{break}`", "``t_breakthru``", "None", ":math:`\text{s}`" @@ -207,7 +209,6 @@ If ``isotherm`` is set to ``freundlich``, the model includes the following compo **Variables** "Freundlich isotherm exponent for resin/ion system", ":math:`n`", "``freundlich_n``", "None", ":math:`\text{dimensionless}`" - "Bed capacity parameter", ":math:`A`", "``bed_capacity_param``", None, ":math:`\text{dimensionless}`" "Bed volumes at breakthrough", ":math:`BV`", "``bv``", "None", ":math:`\text{dimensionless}`" "Bed volumes at 50% influent conc.", ":math:`BV_{50}`", "``bv_50``", "None", ":math:`\text{dimensionless}`" "Kinetic fitting parameter", ":math:`r`", "``kinetic_param``", "None", ":math:`\text{dimensionless}`" @@ -235,7 +236,6 @@ For either model configuration, the user can fix the following variables: * ``service_flow_rate`` (alternatively, ``vel_bed``) * ``bed_depth`` * ``number_columns`` -* ``regen_dose`` Langmuir DOF @@ -256,7 +256,7 @@ If ``isotherm`` is set to ``freundlich``, the additional variables to fix are: * ``freundlich_n`` * ``bv`` * ``c_norm`` -* one of ``bv_50``, ``kinetic_param``, ``mass_transfer_coeff``, or ``bed_capacity_param`` as determined from Clark model equations +* one of ``bv_50``, ``kinetic_param``, or ``mass_transfer_coeff`` as determined from Clark model equations @@ -346,7 +346,6 @@ Equations and Relationships "Breakthrough concentration", ":math:`X = \frac{C_b}{C_0}`" "Bed volumes at breakthrough concentration", ":math:`BV = \frac{t_{break} u_{bed}}{Z}`" "Clark equation with fundamental constants", ":math:`X = \frac{1}{\bigg(1 + (2^{n - 1} - 1)\text{exp}\bigg[\frac{k_T Z (n - 1)}{BV_{50} u_{bed}} (BV_{50} - BV)\bigg]\bigg)^{\frac{1}{n-1}}}`" - "Clark equation for fitting", ":math:`X = \frac{1}{A \text{exp}\big[\frac{-r Z}{u_{bed}} BV\big]^{\frac{1}{n-1}}}`" "Mass transfer coefficient from Clark equation", ":math:`k_T = \frac{r BV_{50}}{n - 1}`" "Evenly spaced c_norm for trapezoids", ":math:`X_{trap,k} = X_{trap,min} + (k - 1) \frac{X - X_{trap,min}}{n_{trap} - 1}`" "Breakthru time calculation for trapezoids", ":math:`t_{trap,k} = - \log{\frac{X_{trap,k}^{n-1}-1}{A}} / k_T`" @@ -364,43 +363,40 @@ The following is a list of variables and/or parameters that are created when app :header: "Description", "Symbol", "Variable Name", "Default Value", "Units", "Notes" "Anion exchange resin cost", ":math:`c_{res}`", "``anion_exchange_resin_cost``", "205", ":math:`\text{\$/}\text{ft}^{3}`", "Assumes strong base polystyrenic gel-type Type II. From EPA-WBS cost model." - "Cation exchange resin cost", ":math:`c_{res}`", "``cation_exchange_resin_cost``", "205", ":math:`\text{\$/}\text{ft}^{3}`", "Assumes strong acid polystyrenic gel-type. From EPA-WBS cost model." - "Ion exchange column cost equation intercept", ":math:`C_{col,int}`", "``vessel_intercept``", "10010.86", ":math:`\text{\$}`", "Carbon steel w/ plastic internals. From EPA-WBS cost model." - "Ion exchange column cost equation A coeff", ":math:`C_{col,A}`", "``vessel_A_coeff``", "6e-9", ":math:`\text{\$/}\text{gal}^{3}`", "Carbon steel w/ plastic internals. From EPA-WBS cost model." - "Ion exchange column cost equation B coeff", ":math:`C_{col,B}`", "``vessel_B_coeff``", "-2.284e-4", ":math:`\text{\$/}\text{gal}^{2}`", "Carbon steel w/ plastic internals. From EPA-WBS cost model." - "Ion exchange column cost equation C coeff", ":math:`C_{col,C}`", "``vessel_C_coeff``", "8.3472", ":math:`\text{\$/}\text{gal}`", "Carbon steel w/ plastic internals. From EPA-WBS cost model." - "Backwash/rinse tank cost equation intercept", ":math:`C_{bw,int}`", "``backwash_tank_intercept``", "4717.255", ":math:`\text{\$}`", "Fiberglass tank. From EPA-WBS cost model." - "Backwash/rinse tank cost equation A coeff", ":math:`C_{bw,A}`", "``backwash_tank_A_coeff``", "1e-9", ":math:`\text{\$/}\text{gal}^{3}`", "Fiberglass tank. From EPA-WBS cost model." - "Backwash/rinse tank cost equation B coeff", ":math:`C_{bw,B}`", "``backwash_tank_B_coeff``", "-5.8587e-05", ":math:`\text{\$/}\text{gal}^{2}`", "Fiberglass tank. From EPA-WBS cost model." - "Backwash/rinse tank cost equation C coeff", ":math:`C_{bw,C}`", "``backwash_tank_C_coeff``", "2.2911", ":math:`\text{\$/}\text{gal}`", "Fiberglass tank. From EPA-WBS cost model." - "Regeneration solution tank cost equation intercept", ":math:`C_{regen,int}`", "``regen_tank_intercept``", "4408.327", ":math:`\text{\$}`", "Stainless steel tank. From EPA-WBS cost model." - "Regeneration solution tank cost equation A coeff", ":math:`C_{regen,A}`", "``regen_tank_A_coeff``", "-3.258e-5", ":math:`\text{\$/}\text{gal}^{2}`", "Stainless steel tank. From EPA-WBS cost model." - "Regeneration solution tank cost equation B coeff", ":math:`C_{regen,B}`", "``regen_tank_B_coeff``", "3.846", ":math:`\text{\$/}\text{gal}`", "Stainless steel tank. From EPA-WBS cost model." + "Cation exchange resin cost", ":math:`c_{res}`", "``cation_exchange_resin_cost``", "153", ":math:`\text{\$/}\text{ft}^{3}`", "Assumes strong acid polystyrenic gel-type. From EPA-WBS cost model." + "Regenerant dose per volume of resin", ":math:`D_{regen}`", "``regen_dose``", "300", ":math:`\text{kg/}\text{m}^3`", "Mass of regenerant chemical per cubic meter of resin volume" + "Ion exchange column cost equation A coeff", ":math:`C_{col,A}`", "``vessel_A_coeff``", "1596.499", ":math:`\text{\$}`", "Carbon steel w/ stainless steel internals. From EPA-WBS cost model." + "Ion exchange column cost equation B coeff", ":math:`C_{col,b}`", "``vessel_b_coeff``", "0.459496", ":math:`\text{dimensionless}`", "Carbon steel w/ stainless steel internals. From EPA-WBS cost model." + "Backwash/rinse tank cost equation A coeff", ":math:`C_{bw,A}`", "``backwash_tank_A_coeff``", "308.9371", ":math:`\text{\$}`", "Steel tank. From EPA-WBS cost model." + "Backwash/rinse tank cost equation B coeff", ":math:`C_{bw,b}`", "``backwash_tank_b_coeff``", "0.501467", ":math:`\text{dimensionless}`", "Steel tank. From EPA-WBS cost model." + "Regeneration solution tank cost equation A coeff", ":math:`C_{regen,A}`", "``regen_tank_A_coeff``", "57.02158", ":math:`\text{\$}`", "Stainless steel tank. From EPA-WBS cost model." + "Regeneration solution tank cost equation B coeff", ":math:`C_{regen,b}`", "``regen_tank_b_coeff``", "0.729325", ":math:`\text{dimensionless}`", "Stainless steel tank. From EPA-WBS cost model." "Fraction of resin replaced per year", ":math:`f_{res}`", "``annual_resin_replacement_factor``", "0.05", ":math:`\text{yr}^{-1}`", "Estimated 4-5% per year. From EPA-WBS cost model." "Minimum hazardous waste disposal cost", ":math:`f_{haz,min}`", "``hazardous_min_cost``", "3240", ":math:`\text{\$/}\text{yr}`", "Minimum cost per hazardous waste shipment. From EPA-WBS cost model." "Unit cost for hazardous waste resin disposal", ":math:`f_{haz,res}`", "``hazardous_resin_disposal``", "347.10", ":math:`\text{\$/}\text{ton}`", "From EPA-WBS cost model." "Unit cost for hazardous waste regeneration solution disposal", ":math:`f_{haz,regen}`", "``hazardous_regen_disposal``", "3.64", ":math:`\text{\$/}\text{gal}`", "From EPA-WBS cost model." "Number of cycles the regenerant can be reused before disposal", ":math:`f_{recycle}`", "``regen_recycle``", "1", ":math:`\text{dimensionless}`", "Can optionally be set by the user to investigate more efficient regen regimes." - "Costing factor to account for total installed cost installation of equipment", ":math:`f_{TIC}`", "``total_installed_cost_factor``", "1.65", ":math:`\text{dimensionless}`", "" + "Costing factor to account for total installed cost installation of equipment", ":math:`f_{TIC}`", "``total_installed_cost_factor``", "1.65", ":math:`\text{dimensionless}`", "Costing factor to account for total installed cost of equipment" "Unit cost of NaCl", ":math:`c_{regen}`", "``costing.nacl``", "0.09", ":math:`\text{\$/}\text{kg}`", "Assumes solid NaCl. From CatCost v 1.0.4" "Unit cost of HCl", ":math:`c_{regen}`", "``costing.hcl``", "0.17", ":math:`\text{\$/}\text{kg}`", "Assumes 37% solution HCl. From CatCost v 1.0.4" "Unit cost of NaOH", ":math:`c_{regen}`", "``costing.naoh``", "0.59", ":math:`\text{\$/}\text{kg}`", "Assumes 30% solution NaOH. From iDST" "Unit cost of Methanol (MeOH)", ":math:`c_{regen}`", "``costing.meoh``", "3.395", ":math:`\text{\$/}\text{kg}`", "Assumes 100% pure MeOH. From ICIS" - + Capital Cost Calculations ^^^^^^^^^^^^^^^^^^^^^^^^^ -Capital costs for ion exchange in the ``watertap_costing_package`` are the summation of the total cost of the resin, columns, backwashing tank, and regeneration solution tank: +Capital costs for ion exchange in the ``watertap_costing_package`` are the summation of the +total cost of the resin, columns, backwashing tank, and regeneration solution tank: Resin is costed based on the total volume of resin required for the system, where :math:`c_{res}` is the cost per volume of resin (either cation or anion exchange resin): .. math:: C_{resin} = V_{res,tot} c_{res} -Vessel cost as a function of volume was fit to a polynomial regression of the following form to determine capital cost of each column: +Vessel cost as a function of volume was fit to a power function to determine capital cost of each column: .. math:: - C_{col} = C_{col,A} V_{col}^3 + C_{col,B} V_{col}^2 + C_{col,C} V_{col} + C_{col,int} + C_{col} = C_{col,A} V_{col}^{C_{col,b}} The backwashing tank is assumed to include backwash and rinsing volumes. The total volume of this tank is: @@ -408,22 +404,25 @@ The backwashing tank is assumed to include backwash and rinsing volumes. The tot .. math:: V_{bw} = Q_{bw} t_{bw} + Q_{rinse} t_{rinse} -Backwashing tank cost as a function of volume was fit to a polynomial regression of the following form to determine capital cost of the backwashing tank: +Backwashing tank cost as a function of volume was fit to a power function to determine capital cost of the backwashing tank: .. math:: - C_{bw} = C_{bw,A} V_{bw}^3 + C_{bw,B} V_{bw}^2 + C_{bw,C} V_{bw} + C_{bw,int} + C_{bw} = C_{bw,A} V_{bw}^{C_{bw,b}} -Regeneration tank cost as a function of volume was fit to a polynomial regression of the following form the determine capital cost of the regeneration tank: +Regeneration tank cost as a function of volume was fit to a power function to determine capital cost of the regeneration tank: .. math:: - C_{regen} = C_{regen,A} V_{regen}^2 + C_{regen,B} V_{regen} + C_{regen,int} + C_{regen} = C_{regen,A} V_{regen}^{C_{regen,b}} And the total capital cost for the ion exchange system is the summation of these: .. math:: C_{tot} = ((C_{resin} + C_{col}) (n_{op} + n_{red}) + C_{bw} + C_{regen}) f_{TIC} -A total installed cost (:math:`f_{TIC}`) factor of 1.65 is applied to account for installation costs. +A total installed cost (:math:`f_{TIC}`) factor of 1.65 is applied to account for installation costs. + +.. note:: + If using ``single_use`` option for ``regenerant`` configuration keyword, the capital for the backwashing/rinse tank and regeneration tank are zero. Operating Cost Calculations ^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -440,10 +439,10 @@ optional model configuration keyword ``regenerant``. Costing data is available f * MeOH If the user does not provide a value for this option, the model defaults to a NaCl regeneration solution. The dose of regenerant needed -is set by the model variable ``regen_dose`` in kg regenerant per cubic meter of resin volume. The mass flow of regenerant solution [kg/yr] is: +is set by the parameter ``regen_dose`` in kg regenerant per cubic meter of resin volume. The mass flow of regenerant solution [kg/yr] is: .. math:: - \dot{m}_{regen} = \frac{C_{regen} V_{res} (n_{op} + n_{red})}{t_{cycle} f_{recycle}} + \dot{m}_{regen} = \frac{D_{regen} V_{res} (n_{op} + n_{red})}{t_{cycle} f_{recycle}} Annual resin replacement cost is: @@ -454,7 +453,7 @@ If the spent resin and regenerant contains hazardous material, the user designat disposal costs are calculated as a function of the annual mass of resin replaced and regenerant consumed: .. math:: - C_{op,haz} = f_{haz,min} + M_{res} (n_{op} + n_{red}) f_{haz,res} + \dot{v}_{regen} f_{haz,regen} + C_{op,haz} = f_{haz,min} + \bigg( M_{res} (n_{op} + n_{red}) f_{res} \bigg) f_{haz,res} + \dot{v}_{regen} f_{haz,regen} Where :math:`M_{res}` is the resin mass for a single bed and :math:`\dot{v}_{regen}` is the volumetric flow of regenerant solution. If ``hazardous_waste`` is set to ``False``, :math:`C_{op,haz} = 0` @@ -464,6 +463,34 @@ The total energy consumed by the unit is the summation of the power required for .. math:: P_{tot} = P_{main} + P_{bw} + P_{regen} + P_{rinse} +If the user chooses ``single_use`` for the ``regenerant`` configuration keyword, there is no cost for regeneration solution: + +.. math:: + \dot{m}_{regen} = \dot{v}_{regen} = 0 + +Instead, the model assumes the entire volume of resin for the operational columns is replaced at the end of each service cycle by calculating the +volumetric "flow" of resin: + +.. math:: + \dot{v}_{resin} = \frac{V_{res, tot}}{t_{break}} + +And then operational cost of replacing the entire bed is: + +.. math:: + C_{op,res} = \dot{v}_{resin} c_{res} + +If ``hazardous_waste`` is set to ``True``, the hazardous waste disposal costs are: + +.. math:: + C_{op,haz} = f_{haz,min} + ( \dot{v}_{resin} \rho_{b} n_{op}) f_{haz,res} + +Otherwise, :math:`C_{op,haz} = 0` as before. + +Lastly, the total energy consumed by the unit for ``single_use`` configuration is only for the main booster pump: + +.. math:: + P_{tot} = P_{main} + References ---------- diff --git a/watertap/costing/units/ion_exchange.py b/watertap/costing/units/ion_exchange.py index a875243fc6..6e63e2b133 100644 --- a/watertap/costing/units/ion_exchange.py +++ b/watertap/costing/units/ion_exchange.py @@ -359,7 +359,12 @@ def cost_ion_exchange(blk): ) ) if blk.unit_model.config.hazardous_waste: - blk.regen_dens = 1000 * pyo.units.kg / pyo.units.m**3 + blk.regen_soln_dens = pyo.Param( + initialize=1000, + units=pyo.units.kg / pyo.units.m**3, + mutable=True, + doc="Density of regeneration solution", + ) if blk.unit_model.config.regenerant == "single_use": blk.operating_cost_hazardous_constraint = pyo.Constraint( @@ -376,13 +381,13 @@ def cost_ion_exchange(blk): expr=blk.operating_cost_hazardous == pyo.units.convert( ( - +bed_mass_ton + bed_mass_ton * tot_num_col * ion_exchange_params.hazardous_resin_disposal ) * ion_exchange_params.annual_resin_replacement_factor + pyo.units.convert( - blk.vol_flow_regen_soln / blk.regen_dens, + blk.vol_flow_regen_soln / blk.regen_soln_dens, to_units=pyo.units.gal / pyo.units.year, ) * ion_exchange_params.hazardous_regen_disposal From ae47530901041097294685d53c7080f6b791dfb4 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Mon, 18 Sep 2023 18:45:57 -0600 Subject: [PATCH 05/27] apply changes to IX demo file and test --- .../ion_exchange/ion_exchange_demo.py | 1 - .../tests/test_ion_exchange_demo.py | 22 +++++++++---------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/watertap/examples/flowsheets/ion_exchange/ion_exchange_demo.py b/watertap/examples/flowsheets/ion_exchange/ion_exchange_demo.py index 16a5ea4237..5e6b3edadb 100644 --- a/watertap/examples/flowsheets/ion_exchange/ion_exchange_demo.py +++ b/watertap/examples/flowsheets/ion_exchange/ion_exchange_demo.py @@ -199,7 +199,6 @@ def set_operating_conditions(m, flow_in=0.05, conc_mass_in=0.1, solver=None): ix.resin_bulk_dens.fix() ix.bed_porosity.fix() ix.dimensionless_time.fix() - ix.regen_dose.fix() def initialize_system(m): diff --git a/watertap/examples/flowsheets/ion_exchange/tests/test_ion_exchange_demo.py b/watertap/examples/flowsheets/ion_exchange/tests/test_ion_exchange_demo.py index 8683c85a88..07d6d747e1 100644 --- a/watertap/examples/flowsheets/ion_exchange/tests/test_ion_exchange_demo.py +++ b/watertap/examples/flowsheets/ion_exchange/tests/test_ion_exchange_demo.py @@ -151,7 +151,7 @@ def test_specific_operating_conditions(self, ix_0D): assert value(m.fs.costing.specific_energy_consumption) == pytest.approx( 0.057245, rel=1e-3 ) - assert value(m.fs.costing.LCOW) == pytest.approx(0.222437, rel=1e-3) + assert value(m.fs.costing.LCOW) == pytest.approx(0.265142128, rel=1e-3) @pytest.mark.component def test_optimization(self, ix_0D): @@ -189,33 +189,33 @@ def test_optimization(self, ix_0D): results = solver.solve(m) assert_optimal_termination(results) assert degrees_of_freedom(m) == 0 - assert value(m.fs.ion_exchange.number_columns) == 6 - assert value(m.fs.ion_exchange.bed_depth) == pytest.approx(1.61147, rel=1e-3) + assert value(m.fs.ion_exchange.number_columns) == 4 + assert value(m.fs.ion_exchange.bed_depth) == pytest.approx(1.88055, rel=1e-3) assert value(m.fs.ion_exchange.t_breakthru) == pytest.approx( - 133404.2583, rel=1e-3 + 133635.328, rel=1e-3 ) assert value(m.fs.ion_exchange.dimensionless_time) == pytest.approx( 1.33210077, rel=1e-3 ) assert value(m.fs.costing.specific_energy_consumption) == pytest.approx( - 0.051706, rel=1e-3 + 0.06949, rel=1e-3 ) - assert value(m.fs.costing.LCOW) == pytest.approx(0.145645, rel=1e-3) + assert value(m.fs.costing.LCOW) == pytest.approx(0.18896, rel=1e-3) @pytest.mark.unit def test_main_fun(self): m = ixf.main() assert degrees_of_freedom(m) == 0 - assert value(m.fs.ion_exchange.number_columns) == 6 - assert value(m.fs.ion_exchange.bed_depth) == pytest.approx(1.61147, rel=1e-3) + assert value(m.fs.ion_exchange.number_columns) == 4 + assert value(m.fs.ion_exchange.bed_depth) == pytest.approx(1.88055, rel=1e-3) assert value(m.fs.ion_exchange.t_breakthru) == pytest.approx( - 133404.2583, rel=1e-3 + 133635.328, rel=1e-3 ) assert value(m.fs.ion_exchange.dimensionless_time) == pytest.approx( 1.33210077, rel=1e-3 ) assert value(m.fs.costing.specific_energy_consumption) == pytest.approx( - 0.051706, rel=1e-3 + 0.06949, rel=1e-3 ) - assert value(m.fs.costing.LCOW) == pytest.approx(0.145645, rel=1e-3) + assert value(m.fs.costing.LCOW) == pytest.approx(0.18896, rel=1e-3) From 7739ccca31a99d242a16f2f4fe8cb105cd9da5ac Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Tue, 10 Oct 2023 11:12:43 -0600 Subject: [PATCH 06/27] reformulate eq_clark --- watertap/unit_models/ion_exchange_0D.py | 55 +++++++++---------- .../unit_models/tests/test_ion_exchange_0D.py | 2 - 2 files changed, 26 insertions(+), 31 deletions(-) diff --git a/watertap/unit_models/ion_exchange_0D.py b/watertap/unit_models/ion_exchange_0D.py index 4e62337775..d805b80cbf 100644 --- a/watertap/unit_models/ion_exchange_0D.py +++ b/watertap/unit_models/ion_exchange_0D.py @@ -103,7 +103,6 @@ class RegenerantChem(StrEnum): NaCl = "NaCl" MeOH = "MeOH" single_use = "single_use" - none = "none" class IsothermType(StrEnum): @@ -1160,20 +1159,17 @@ def eq_bv(b): @self.Constraint( self.target_ion_set, doc="Clark equation with fundamental constants" ) # Croll et al (2023), Eq.9 - def eq_clark_1(b, j): - denom = ( - 1 - + (2 ** (b.freundlich_n - 1) - 1) - * exp( - ( - (b.mass_transfer_coeff * b.bed_depth * (b.freundlich_n - 1)) - / (b.bv_50 * b.vel_bed) - ) - * (b.bv_50 - b.bv) - ) - ) ** (1 / (b.freundlich_n - 1)) - # return c0 == denom * cb - return denom * b.c_norm[j] == 1 + def eq_clark(b, j): + left_side = ( + (b.mass_transfer_coeff * b.bed_depth * (b.freundlich_n - 1)) + / (b.bv_50 * b.vel_bed) + ) * (b.bv_50 - b.bv) + + right_side = log( + ((1 / b.c_norm[j]) ** (b.freundlich_n - 1) - 1) + / (2 ** (b.freundlich_n - 1) - 1) + ) + return left_side - right_side == 0 @self.Constraint( self.target_ion_set, @@ -1191,18 +1187,16 @@ def eq_c_traps(b, j, k): ) def eq_tb_traps(b, k): bv_traps = (b.tb_traps[k] * b.vel_bed) / b.bed_depth - denom = ( - 1 - + (2 ** (b.freundlich_n - 1) - 1) - * exp( - ( - (b.mass_transfer_coeff * b.bed_depth * (b.freundlich_n - 1)) - / (b.bv_50 * b.vel_bed) - ) - * (b.bv_50 - bv_traps) - ) - ) ** (1 / (b.freundlich_n - 1)) - return denom * b.c_traps[k] == 1 + left_side = ( + (b.mass_transfer_coeff * b.bed_depth * (b.freundlich_n - 1)) + / (b.bv_50 * b.vel_bed) + ) * (b.bv_50 - bv_traps) + + right_side = log( + ((1 / b.c_traps[k]) ** (b.freundlich_n - 1) - 1) + / (2 ** (b.freundlich_n - 1) - 1) + ) + return left_side - right_side == 0 @self.Constraint(self.trap_index, doc="Area of trapezoids") def eq_traps(b, k): @@ -1436,11 +1430,14 @@ def calculate_scaling_factors(self): iscale.set_scaling_factor(self.kinetic_param, 1e7) if iscale.get_scaling_factor(self.mass_transfer_coeff) is None: - iscale.set_scaling_factor(self.mass_transfer_coeff, 1e4) + iscale.set_scaling_factor(self.mass_transfer_coeff, 10) if iscale.get_scaling_factor(self.bv_50) is None: iscale.set_scaling_factor(self.bv_50, 1e-5) + if iscale.get_scaling_factor(self.bv) is None: + iscale.set_scaling_factor(self.bv, 1e-5) + if iscale.get_scaling_factor(self.tb_traps) is None: sf = iscale.get_scaling_factor(self.t_breakthru) iscale.set_scaling_factor(self.tb_traps, sf) @@ -1477,7 +1474,7 @@ def calculate_scaling_factors(self): if isotherm == IsothermType.freundlich: - for ind, c in self.eq_clark_1.items(): + for ind, c in self.eq_clark.items(): if iscale.get_scaling_factor(c) is None: iscale.constraint_scaling_transform(c, 1e6) diff --git a/watertap/unit_models/tests/test_ion_exchange_0D.py b/watertap/unit_models/tests/test_ion_exchange_0D.py index 7211090957..3d69257875 100644 --- a/watertap/unit_models/tests/test_ion_exchange_0D.py +++ b/watertap/unit_models/tests/test_ion_exchange_0D.py @@ -252,7 +252,6 @@ def test_solution(self, IX_lang): "resin_max_capacity": 3, "resin_eq_capacity": 1.5547810762853227, "resin_unused_capacity": 1.4452189237146773, - # "regen_dose": 300, "resin_diam": 0.0007, "resin_bulk_dens": 0.7, "langmuir": 0.9, @@ -529,7 +528,6 @@ def test_solution(self, IX_fr): "resin_diam": 0.0006749999999999999, "resin_bulk_dens": 0.72, "resin_surf_per_vol": 4444.444444444445, - # "regen_dose": 300, "c_norm": {"Cl_-": 0.25}, "bed_vol_tot": 120.00000000000001, "bed_depth": 1.476, From 326d35677abd294730a3fe35f001f54111335625 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Tue, 10 Oct 2023 11:29:41 -0600 Subject: [PATCH 07/27] vol_flow_regen_soln to flow_mass_regen_soln --- watertap/costing/unit_models/ion_exchange.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/watertap/costing/unit_models/ion_exchange.py b/watertap/costing/unit_models/ion_exchange.py index 75b13cc03c..4efd3a0a2c 100644 --- a/watertap/costing/unit_models/ion_exchange.py +++ b/watertap/costing/unit_models/ion_exchange.py @@ -253,7 +253,7 @@ def cost_ion_exchange(blk): units=blk.costing_package.base_currency / blk.costing_package.base_period, doc="Operating cost for hazardous waste disposal", ) - blk.vol_flow_regen_soln = pyo.Var( + blk.flow_mass_regen_soln = pyo.Var( initialize=1, bounds=(0, None), units=pyo.units.kg / pyo.units.year, @@ -291,7 +291,7 @@ def cost_ion_exchange(blk): if blk.unit_model.config.regenerant == "single_use": blk.capital_cost_backwash_tank.fix(0) blk.capital_cost_regen_tank.fix(0) - blk.vol_flow_regen_soln.fix(0) + blk.flow_mass_regen_soln.fix(0) blk.vol_flow_resin = pyo.Var( initialize=1e5, bounds=(0, None), @@ -387,7 +387,7 @@ def cost_ion_exchange(blk): ) * ion_exchange_params.annual_resin_replacement_factor + pyo.units.convert( - blk.vol_flow_regen_soln / blk.regen_soln_dens, + blk.flow_mass_regen_soln / blk.regen_soln_dens, to_units=pyo.units.gal / pyo.units.year, ) * ion_exchange_params.hazardous_regen_disposal @@ -432,8 +432,8 @@ def cost_ion_exchange(blk): + blk.operating_cost_hazardous ) - blk.vol_flow_regen_soln_constraint = pyo.Constraint( - expr=blk.vol_flow_regen_soln + blk.flow_mass_regen_soln_constraint = pyo.Constraint( + expr=blk.flow_mass_regen_soln == pyo.units.convert( ( (blk.regen_dose * blk.unit_model.bed_vol * tot_num_col) @@ -445,7 +445,7 @@ def cost_ion_exchange(blk): ) blk.costing_package.cost_flow( - blk.vol_flow_regen_soln, blk.unit_model.config.regenerant + blk.flow_mass_regen_soln, blk.unit_model.config.regenerant ) if blk.unit_model.config.regenerant == "single_use": From b80fe87d3197a435b24aab9e7a336a25bc659208 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Tue, 10 Oct 2023 11:30:24 -0600 Subject: [PATCH 08/27] modify bed_expansion_h expr for single_use --- watertap/unit_models/ion_exchange_0D.py | 25 ++++++++----------- .../unit_models/tests/test_ion_exchange_0D.py | 1 - 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/watertap/unit_models/ion_exchange_0D.py b/watertap/unit_models/ion_exchange_0D.py index d805b80cbf..3bc6248f38 100644 --- a/watertap/unit_models/ion_exchange_0D.py +++ b/watertap/unit_models/ion_exchange_0D.py @@ -827,10 +827,6 @@ def bed_expansion_frac(b): + b.bed_expansion_frac_C * b.bw_rate**2 ) # for 20C - @self.Expression(doc="Bed expansion from backwashing") - def bed_expansion_h(b): - return b.bed_expansion_frac * b.bed_depth - @self.Expression(doc="Rinse flow rate") def rinse_flow(b): return b.vel_bed * (b.bed_vol / b.bed_depth) * b.number_columns @@ -905,6 +901,13 @@ def eq_isobaric_regen_stream(b): for j in inerts: self.regeneration_stream[0].get_material_flow_terms("Liq", j).fix(0) + @self.Expression(doc="Bed expansion from backwashing") + def bed_expansion_h(b): + if self.config.regenerant == RegenerantChem.single_use: + return 0 * pyunits.m + else: + return b.bed_expansion_frac * b.bed_depth + @self.Expression(doc="Main pump power") def main_pump_power(b): return pyunits.convert( @@ -1026,16 +1029,10 @@ def eq_bed_flow(b): @self.Constraint(doc="Column height") def eq_col_height(b): - if self.config.regenerant != RegenerantChem.single_use: - return ( - b.col_height - == b.bed_depth - + b.distributor_h - + b.underdrain_h - + b.bed_expansion_h - ) - else: - return b.col_height == b.bed_depth + b.distributor_h + b.underdrain_h + return ( + b.col_height + == b.bed_depth + b.distributor_h + b.underdrain_h + b.bed_expansion_h + ) @self.Constraint(doc="Bed design") def eq_bed_design(b): diff --git a/watertap/unit_models/tests/test_ion_exchange_0D.py b/watertap/unit_models/tests/test_ion_exchange_0D.py index 3d69257875..fa5a131793 100644 --- a/watertap/unit_models/tests/test_ion_exchange_0D.py +++ b/watertap/unit_models/tests/test_ion_exchange_0D.py @@ -576,7 +576,6 @@ def test_solution(self, IX_fr): "mass_transfer_coeff": 0.159346300525143, "bv": 18000, "bv_50": 20000, - # "bed_capacity_param": 311.9325370632754, "kinetic_param": 1.5934630052514297e-06, } From 7d42351070545302210ca3ed5ec13509fa10850f Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Tue, 10 Oct 2023 12:15:43 -0600 Subject: [PATCH 09/27] rename, add some Expressions, clean up --- watertap/costing/unit_models/ion_exchange.py | 71 +++++++++++--------- 1 file changed, 38 insertions(+), 33 deletions(-) diff --git a/watertap/costing/unit_models/ion_exchange.py b/watertap/costing/unit_models/ion_exchange.py index 4efd3a0a2c..4f28d3fe66 100644 --- a/watertap/costing/unit_models/ion_exchange.py +++ b/watertap/costing/unit_models/ion_exchange.py @@ -211,12 +211,14 @@ def cost_ion_exchange(blk): tot_num_col = blk.unit_model.number_columns + blk.unit_model.number_columns_redund col_vol_gal = pyo.units.convert(blk.unit_model.col_vol_per, to_units=pyo.units.gal) bed_vol_ft3 = pyo.units.convert(blk.unit_model.bed_vol, to_units=pyo.units.ft**3) - bed_mass_ton = pyo.units.convert( - blk.unit_model.bed_vol * blk.unit_model.resin_bulk_dens, - to_units=pyo.units.ton, - ) ix_type = blk.unit_model.ion_exchange_type + blk.regen_soln_dens = pyo.Param( + initialize=1000, + units=pyo.units.kg / pyo.units.m**3, + mutable=True, + doc="Density of regeneration solution", + ) blk.regen_dose = pyo.Param( initialize=300, units=pyo.units.kg / pyo.units.m**3, @@ -255,13 +257,13 @@ def cost_ion_exchange(blk): ) blk.flow_mass_regen_soln = pyo.Var( initialize=1, - bounds=(0, None), + domain=pyo.NonNegativeReals, units=pyo.units.kg / pyo.units.year, doc="Regeneration solution flow", ) blk.total_pumping_power = pyo.Var( initialize=1, - bounds=(0, None), + domain=pyo.NonNegativeReals, units=pyo.units.kilowatt, doc="Total pumping power required", ) @@ -292,47 +294,52 @@ def cost_ion_exchange(blk): blk.capital_cost_backwash_tank.fix(0) blk.capital_cost_regen_tank.fix(0) blk.flow_mass_regen_soln.fix(0) - blk.vol_flow_resin = pyo.Var( + blk.flow_vol_resin = pyo.Var( initialize=1e5, bounds=(0, None), units=pyo.units.m**3 / blk.costing_package.base_period, - doc="Volumetric flow of resin per cycle", # assumes you are only replacing the operational columns + doc="Volumetric flow of resin per cycle", # assumes you are only replacing the operational columns, t_cycle = t_breakthru ) - blk.operating_cost_single_use_resin = pyo.Var( + blk.single_use_resin_replacement_cost = pyo.Var( initialize=1e5, bounds=(0, None), units=blk.costing_package.base_currency / blk.costing_package.base_period, doc="Operating cost for using single-use resin (i.e., no regeneration)", ) - blk.vol_flow_resin_constraint = pyo.Constraint( - expr=blk.vol_flow_resin + blk.flow_vol_resin_constraint = pyo.Constraint( + expr=blk.flow_vol_resin == pyo.units.convert( blk.unit_model.bed_vol_tot / blk.unit_model.t_breakthru, to_units=pyo.units.m**3 / blk.costing_package.base_period, ) ) blk.mass_flow_resin = pyo.units.convert( - blk.vol_flow_resin * blk.unit_model.resin_bulk_dens, + blk.flow_vol_resin * blk.unit_model.resin_bulk_dens, to_units=pyo.units.ton / blk.costing_package.base_period, ) else: - bw_tank_vol = pyo.units.convert( - ( - blk.unit_model.bw_flow * blk.unit_model.t_bw - + blk.unit_model.rinse_flow * blk.unit_model.t_rinse - ), - to_units=pyo.units.gal, + blk.backwash_tank_vol = pyo.Expression( + expr=pyo.units.convert( + ( + blk.unit_model.bw_flow * blk.unit_model.t_bw + + blk.unit_model.rinse_flow * blk.unit_model.t_rinse + ), + to_units=pyo.units.gal, + ) ) - regen_tank_vol = pyo.units.convert( - blk.unit_model.regen_tank_vol, - to_units=pyo.units.gal, + + blk.regeneration_tank_vol = pyo.Expression( + expr=pyo.units.convert( + blk.unit_model.regen_tank_vol, + to_units=pyo.units.gal, + ) ) blk.capital_cost_backwash_tank_constraint = pyo.Constraint( expr=blk.capital_cost_backwash_tank == pyo.units.convert( ion_exchange_params.backwash_tank_A_coeff - * (bw_tank_vol / pyo.units.gallon) + * (blk.backwash_tank_vol / pyo.units.gallon) ** ion_exchange_params.backwash_tank_b_coeff, to_units=blk.costing_package.base_currency, ) @@ -341,7 +348,7 @@ def cost_ion_exchange(blk): expr=blk.capital_cost_regen_tank == pyo.units.convert( ion_exchange_params.regen_tank_A_coeff - * (regen_tank_vol / pyo.units.gallon) + * (blk.regeneration_tank_vol / pyo.units.gallon) ** ion_exchange_params.regen_tank_b_coeff, to_units=blk.costing_package.base_currency, ) @@ -359,12 +366,6 @@ def cost_ion_exchange(blk): ) ) if blk.unit_model.config.hazardous_waste: - blk.regen_soln_dens = pyo.Param( - initialize=1000, - units=pyo.units.kg / pyo.units.m**3, - mutable=True, - doc="Density of regeneration solution", - ) if blk.unit_model.config.regenerant == "single_use": blk.operating_cost_hazardous_constraint = pyo.Constraint( @@ -377,6 +378,10 @@ def cost_ion_exchange(blk): ) ) else: + bed_mass_ton = pyo.units.convert( + blk.unit_model.bed_vol * blk.unit_model.resin_bulk_dens, + to_units=pyo.units.ton, + ) blk.operating_cost_hazardous_constraint = pyo.Constraint( expr=blk.operating_cost_hazardous == pyo.units.convert( @@ -400,10 +405,10 @@ def cost_ion_exchange(blk): blk.operating_cost_hazardous.fix(0) if blk.unit_model.config.regenerant == "single_use": - blk.operating_cost_single_use_resin_constraint = pyo.Constraint( - expr=blk.operating_cost_single_use_resin + blk.single_use_resin_replacement_cost_constraint = pyo.Constraint( + expr=blk.single_use_resin_replacement_cost == pyo.units.convert( - blk.vol_flow_resin * resin_cost, + blk.flow_vol_resin * resin_cost, to_units=blk.costing_package.base_currency / blk.costing_package.base_period, ) @@ -411,7 +416,7 @@ def cost_ion_exchange(blk): blk.fixed_operating_cost_constraint = pyo.Constraint( expr=blk.fixed_operating_cost - == blk.operating_cost_single_use_resin + blk.operating_cost_hazardous + == blk.single_use_resin_replacement_cost + blk.operating_cost_hazardous ) else: From 496ecbacf72bd91ddba8d151a40de4802e403128 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Wed, 11 Oct 2023 11:38:29 -0400 Subject: [PATCH 10/27] remove unused exp import --- watertap/unit_models/ion_exchange_0D.py | 1 - 1 file changed, 1 deletion(-) diff --git a/watertap/unit_models/ion_exchange_0D.py b/watertap/unit_models/ion_exchange_0D.py index 3bc6248f38..921a5af147 100644 --- a/watertap/unit_models/ion_exchange_0D.py +++ b/watertap/unit_models/ion_exchange_0D.py @@ -20,7 +20,6 @@ Param, Suffix, log, - exp, units as pyunits, ) from pyomo.common.config import ConfigBlock, ConfigValue, In From 23a20aa4c1084258b7f882ee1efba32baf2a7d95 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Fri, 27 Oct 2023 11:49:10 -0600 Subject: [PATCH 11/27] t_contact to Expression --- watertap/unit_models/ion_exchange_0D.py | 35 +++++++------------ .../unit_models/tests/test_ion_exchange_0D.py | 32 ++++++++--------- 2 files changed, 28 insertions(+), 39 deletions(-) diff --git a/watertap/unit_models/ion_exchange_0D.py b/watertap/unit_models/ion_exchange_0D.py index 921a5af147..060a2f4b6e 100644 --- a/watertap/unit_models/ion_exchange_0D.py +++ b/watertap/unit_models/ion_exchange_0D.py @@ -551,13 +551,6 @@ def build(self): doc="Breakthrough time", ) - self.t_contact = Var( - initialize=120, - bounds=(100, None), - units=pyunits.s, - doc="Resin contact time", - ) - self.ebct = Var( initialize=520, bounds=(90, None), @@ -921,15 +914,19 @@ def col_vol_per(b): @self.Expression(doc="Total column volume required") def col_vol_tot(b): return b.number_columns * b.col_vol_per - - @self.Expression( - doc="Bed volumes at breakthrough", - ) - def bv_calc(b): - return (b.vel_bed * b.t_breakthru) / b.bed_depth + + @self.Expression(doc="Contact time") + def t_contact(b): + return b.ebct * b.bed_porosity if self.config.isotherm == IsothermType.langmuir: + @self.Expression( + doc="Bed volumes at breakthrough", + ) + def bv_calc(b): + return (b.vel_bed * b.t_breakthru) / b.bed_depth + @self.Expression(doc="Left hand side of constant pattern sol'n") def lh(b): return b.num_transfer_units * (b.dimensionless_time - 1) @@ -1009,10 +1006,6 @@ def eq_resin_surf_per_vol(b): def eq_ebct(b): return b.ebct * b.vel_bed == b.bed_depth - @self.Constraint(doc="Contact time") - def eq_t_contact(b): - return b.t_contact == b.ebct * b.bed_porosity - @self.Constraint(doc="Service flow rate") def eq_service_flow_rate(b): return b.service_flow_rate * b.bed_vol_tot == pyunits.convert( @@ -1102,7 +1095,7 @@ def eq_dimensionless_time( b, ): # Eqs. 16-120, 16-129, Perry's; Eq. 4.136, Inglezakis + Poulopoulos return b.dimensionless_time * b.partition_ratio == ( - (b.vel_inter * b.t_breakthru * b.bed_porosity) / b.bed_depth + ((b.vel_bed / b.bed_porosity) * b.t_breakthru * b.bed_porosity) / b.bed_depth - b.bed_porosity ) @@ -1379,9 +1372,6 @@ def calculate_scaling_factors(self): if iscale.get_scaling_factor(self.ebct) is None: iscale.set_scaling_factor(self.ebct, 1e-2) - if iscale.get_scaling_factor(self.t_contact) is None: - iscale.set_scaling_factor(self.t_contact, 1e-2) - if iscale.get_scaling_factor(self.vel_bed) is None: iscale.set_scaling_factor(self.vel_bed, 1e3) @@ -1472,7 +1462,7 @@ def calculate_scaling_factors(self): for ind, c in self.eq_clark.items(): if iscale.get_scaling_factor(c) is None: - iscale.constraint_scaling_transform(c, 1e6) + iscale.constraint_scaling_transform(c, 1e-2) for ind, c in self.eq_traps.items(): if iscale.get_scaling_factor(c) is None: @@ -1508,7 +1498,6 @@ def _get_performance_contents(self, time_point=0): var_dict = {} var_dict["Breakthrough Time"] = self.t_breakthru var_dict["EBCT"] = self.ebct - var_dict["Contact Time"] = self.t_contact var_dict[f"Relative Breakthrough Conc. [{target_ion}]"] = self.c_norm[ target_ion ] diff --git a/watertap/unit_models/tests/test_ion_exchange_0D.py b/watertap/unit_models/tests/test_ion_exchange_0D.py index fa5a131793..3b945c8000 100644 --- a/watertap/unit_models/tests/test_ion_exchange_0D.py +++ b/watertap/unit_models/tests/test_ion_exchange_0D.py @@ -177,7 +177,7 @@ def test_default_build(self, IX_lang): "col_diam", "number_columns", "t_breakthru", - "t_contact", + # "t_contact", "ebct", "vel_bed", "vel_inter", @@ -204,8 +204,8 @@ def test_default_build(self, IX_lang): assert isinstance(var, Var) # test statistics - assert number_variables(m) == 68 - assert number_total_constraints(m) == 42 + assert number_variables(m) == 67 + assert number_total_constraints(m) == 41 assert number_unused_variables(m) == 11 @pytest.mark.unit @@ -268,7 +268,7 @@ def test_solution(self, IX_lang): "partition_ratio": 217.66935067994518, "fluid_mass_transfer_coeff": 3.456092786557271e-05, "t_breakthru": 52360.64416318684, - "t_contact": 120.0, + # "t_contact": 120.0, "mass_removed": 65300.80520398353, "vel_bed": 0.007083333333333333, "vel_inter": 0.014166666666666666, @@ -451,7 +451,7 @@ def test_default_build(self, IX_fr): "col_height_to_diam_ratio", "number_columns", "t_breakthru", - "t_contact", + # "t_contact", "ebct", "vel_bed", "vel_inter", @@ -479,8 +479,8 @@ def test_default_build(self, IX_fr): assert isinstance(var, Var) # test statistics - assert number_variables(m) == 80 - assert number_total_constraints(m) == 51 + assert number_variables(m) == 79 + assert number_total_constraints(m) == 50 assert number_unused_variables(m) == 12 @pytest.mark.unit @@ -537,7 +537,7 @@ def test_solution(self, IX_fr): "col_height_to_diam_ratio": 1.242662400172933, "number_columns": 16, "t_breakthru": 4320000.0, - "t_contact": 120.00000000000001, + # "t_contact": 120.00000000000001, "ebct": 240.00000000000003, "vel_bed": 0.006149999999999999, "vel_inter": 0.012299999999999998, @@ -745,7 +745,7 @@ def test_default_build(self, IX_single_use): "col_height_to_diam_ratio", "number_columns", "t_breakthru", - "t_contact", + # "t_contact", "ebct", "vel_bed", "vel_inter", @@ -773,8 +773,8 @@ def test_default_build(self, IX_single_use): assert isinstance(var, Var) # test statistics - assert number_variables(m) == 76 - assert number_total_constraints(m) == 48 + assert number_variables(m) == 75 + assert number_total_constraints(m) == 47 assert number_unused_variables(m) == 11 @pytest.mark.unit @@ -831,7 +831,7 @@ def test_solution(self, IX_single_use): "col_height_to_diam_ratio": 1.0324865176826379, "number_columns": 18, "t_breakthru": 4320000.000000001, - "t_contact": 120.0, + # "t_contact": 120.0, "ebct": 240.0, "vel_bed": 0.00615, "vel_inter": 0.0123, @@ -1043,7 +1043,7 @@ def test_default_build(self, IX_inert): "col_height_to_diam_ratio", "number_columns", "t_breakthru", - "t_contact", + # "t_contact", "ebct", "vel_bed", "vel_inter", @@ -1071,8 +1071,8 @@ def test_default_build(self, IX_inert): assert isinstance(var, Var) # test statistics - assert number_variables(m) == 88 - assert number_total_constraints(m) == 55 + assert number_variables(m) == 87 + assert number_total_constraints(m) == 54 assert number_unused_variables(m) == 14 @pytest.mark.unit @@ -1125,7 +1125,7 @@ def test_solution(self, IX_inert): "col_height_to_diam_ratio": 1.242662400172933, "number_columns": 16, "t_breakthru": 4320000.0, - "t_contact": 120.0, + # "t_contact": 120.0, "ebct": 240.0, "vel_bed": 0.006149999999999999, "vel_inter": 0.012299999999999998, From 8308af831dc833b69d4638bca757f68f526e44fe Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Fri, 27 Oct 2023 12:15:45 -0600 Subject: [PATCH 12/27] add back regeneration_stream for single_use config --- watertap/unit_models/ion_exchange_0D.py | 83 +++++++++---------- .../unit_models/tests/test_ion_exchange_0D.py | 17 +--- 2 files changed, 40 insertions(+), 60 deletions(-) diff --git a/watertap/unit_models/ion_exchange_0D.py b/watertap/unit_models/ion_exchange_0D.py index 060a2f4b6e..6d72add0d9 100644 --- a/watertap/unit_models/ion_exchange_0D.py +++ b/watertap/unit_models/ion_exchange_0D.py @@ -304,6 +304,21 @@ def build(self): self.add_inlet_port(name="inlet", block=self.process_flow) self.add_outlet_port(name="outlet", block=self.process_flow) + 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 + + self.regeneration_stream = self.config.property_package.state_block_class( + self.flowsheet().config.time, + doc="Material properties of regeneration stream", + **tmp_dict, + ) + + regen = self.regeneration_stream[0] + + self.add_outlet_port(name="regen", block=self.regeneration_stream) + # ==========PARAMETERS========== self.underdrain_h = Param( @@ -788,21 +803,6 @@ def bed_vol(b): if self.config.regenerant != RegenerantChem.single_use: - 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 - - self.regeneration_stream = self.config.property_package.state_block_class( - self.flowsheet().config.time, - doc="Material properties of regeneration stream", - **tmp_dict, - ) - - regen = self.regeneration_stream[0] - - self.add_outlet_port(name="regen", block=self.regeneration_stream) - @self.Expression(doc="Backwashing flow rate") def bw_flow(b): return ( @@ -890,8 +890,8 @@ def eq_isothermal_regen_stream(b): def eq_isobaric_regen_stream(b): return prop_in.pressure == regen.pressure - for j in inerts: - self.regeneration_stream[0].get_material_flow_terms("Liq", j).fix(0) + for j in inerts: + self.regeneration_stream[0].get_material_flow_terms("Liq", j).fix(0) @self.Expression(doc="Bed expansion from backwashing") def bed_expansion_h(b): @@ -914,7 +914,7 @@ def col_vol_per(b): @self.Expression(doc="Total column volume required") def col_vol_tot(b): return b.number_columns * b.col_vol_per - + @self.Expression(doc="Contact time") def t_contact(b): return b.ebct * b.bed_porosity @@ -1095,7 +1095,8 @@ def eq_dimensionless_time( b, ): # Eqs. 16-120, 16-129, Perry's; Eq. 4.136, Inglezakis + Poulopoulos return b.dimensionless_time * b.partition_ratio == ( - ((b.vel_bed / b.bed_porosity) * b.t_breakthru * b.bed_porosity) / b.bed_depth + ((b.vel_bed / b.bed_porosity) * b.t_breakthru * b.bed_porosity) + / b.bed_depth - b.bed_porosity ) @@ -1278,18 +1279,16 @@ def initialize_build( ) init_log.info("Initialization Step 1b Complete.") - if self.config.regenerant != RegenerantChem.single_use: - - state_args_regen = deepcopy(state_args) + state_args_regen = deepcopy(state_args) - self.regeneration_stream.initialize( - outlvl=outlvl, - optarg=optarg, - solver=solver, - state_args=state_args_regen, - ) + self.regeneration_stream.initialize( + outlvl=outlvl, + optarg=optarg, + solver=solver, + state_args=state_args_regen, + ) - init_log.info("Initialization Step 1c Complete.") + init_log.info("Initialization Step 1c Complete.") # Solve unit with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: @@ -1473,23 +1472,15 @@ def calculate_scaling_factors(self): iscale.constraint_scaling_transform(c, sf) def _get_stream_table_contents(self, time_point=0): - if self.config.regenerant != RegenerantChem.single_use: - return create_stream_table_dataframe( - { - "Feed Inlet": self.inlet, - "Liquid Outlet": self.outlet, - "Regen Outlet": self.regen, - }, - time_point=time_point, - ) - else: - return create_stream_table_dataframe( - { - "Feed Inlet": self.inlet, - "Liquid Outlet": self.outlet, - }, - time_point=time_point, - ) + + return create_stream_table_dataframe( + { + "Feed Inlet": self.inlet, + "Liquid Outlet": self.outlet, + "Regen Outlet": self.regen, + }, + time_point=time_point, + ) def _get_performance_contents(self, time_point=0): diff --git a/watertap/unit_models/tests/test_ion_exchange_0D.py b/watertap/unit_models/tests/test_ion_exchange_0D.py index 3b945c8000..15a7d2b17b 100644 --- a/watertap/unit_models/tests/test_ion_exchange_0D.py +++ b/watertap/unit_models/tests/test_ion_exchange_0D.py @@ -177,7 +177,6 @@ def test_default_build(self, IX_lang): "col_diam", "number_columns", "t_breakthru", - # "t_contact", "ebct", "vel_bed", "vel_inter", @@ -268,7 +267,6 @@ def test_solution(self, IX_lang): "partition_ratio": 217.66935067994518, "fluid_mass_transfer_coeff": 3.456092786557271e-05, "t_breakthru": 52360.64416318684, - # "t_contact": 120.0, "mass_removed": 65300.80520398353, "vel_bed": 0.007083333333333333, "vel_inter": 0.014166666666666666, @@ -451,7 +449,6 @@ def test_default_build(self, IX_fr): "col_height_to_diam_ratio", "number_columns", "t_breakthru", - # "t_contact", "ebct", "vel_bed", "vel_inter", @@ -537,7 +534,6 @@ def test_solution(self, IX_fr): "col_height_to_diam_ratio": 1.242662400172933, "number_columns": 16, "t_breakthru": 4320000.0, - # "t_contact": 120.00000000000001, "ebct": 240.00000000000003, "vel_bed": 0.006149999999999999, "vel_inter": 0.012299999999999998, @@ -694,7 +690,7 @@ def test_default_build(self, IX_single_use): ix = m.fs.ix # test ports and variables - port_lst = ["inlet", "outlet"] + port_lst = ["inlet", "outlet", "regen"] port_vars_lst = ["flow_mol_phase_comp", "pressure", "temperature"] for port_str in port_lst: assert hasattr(ix, port_str) @@ -706,9 +702,6 @@ def test_default_build(self, IX_single_use): var = getattr(port, var_str) assert isinstance(var, Var) - assert not hasattr(ix, "regen") - assert not hasattr(ix, "regeneration_stream") - # test unit objects ix_params = [ "underdrain_h", @@ -745,7 +738,6 @@ def test_default_build(self, IX_single_use): "col_height_to_diam_ratio", "number_columns", "t_breakthru", - # "t_contact", "ebct", "vel_bed", "vel_inter", @@ -773,9 +765,9 @@ def test_default_build(self, IX_single_use): assert isinstance(var, Var) # test statistics - assert number_variables(m) == 75 + assert number_variables(m) == 79 assert number_total_constraints(m) == 47 - assert number_unused_variables(m) == 11 + assert number_unused_variables(m) == 15 @pytest.mark.unit def test_dof(self, IX_single_use): @@ -831,7 +823,6 @@ def test_solution(self, IX_single_use): "col_height_to_diam_ratio": 1.0324865176826379, "number_columns": 18, "t_breakthru": 4320000.000000001, - # "t_contact": 120.0, "ebct": 240.0, "vel_bed": 0.00615, "vel_inter": 0.0123, @@ -1043,7 +1034,6 @@ def test_default_build(self, IX_inert): "col_height_to_diam_ratio", "number_columns", "t_breakthru", - # "t_contact", "ebct", "vel_bed", "vel_inter", @@ -1125,7 +1115,6 @@ def test_solution(self, IX_inert): "col_height_to_diam_ratio": 1.242662400172933, "number_columns": 16, "t_breakthru": 4320000.0, - # "t_contact": 120.0, "ebct": 240.0, "vel_bed": 0.006149999999999999, "vel_inter": 0.012299999999999998, From e673cf912ac92e7e3e0e99174bf231705e0ffe0a Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Fri, 27 Oct 2023 12:35:12 -0600 Subject: [PATCH 13/27] vel_inter to Expression --- watertap/unit_models/ion_exchange_0D.py | 27 +++++++------------ .../unit_models/tests/test_ion_exchange_0D.py | 24 ++++++----------- 2 files changed, 17 insertions(+), 34 deletions(-) diff --git a/watertap/unit_models/ion_exchange_0D.py b/watertap/unit_models/ion_exchange_0D.py index 6d72add0d9..796938b08a 100644 --- a/watertap/unit_models/ion_exchange_0D.py +++ b/watertap/unit_models/ion_exchange_0D.py @@ -582,12 +582,6 @@ def build(self): doc="Superficial velocity through bed", ) - self.vel_inter = Var( - initialize=0.01, - units=pyunits.m / pyunits.s, - doc="Interstitial velocity through bed", - ) - self.service_flow_rate = Var( initialize=10, bounds=(1, 40), @@ -801,6 +795,8 @@ def pressure_drop(b): def bed_vol(b): return b.bed_vol_tot / b.number_columns + + if self.config.regenerant != RegenerantChem.single_use: @self.Expression(doc="Backwashing flow rate") @@ -914,10 +910,14 @@ def col_vol_per(b): @self.Expression(doc="Total column volume required") def col_vol_tot(b): return b.number_columns * b.col_vol_per - + @self.Expression(doc="Contact time") def t_contact(b): return b.ebct * b.bed_porosity + + @self.Expression(doc="Interstitial velocity") + def vel_inter(b): + return b.vel_bed / b.bed_porosity if self.config.isotherm == IsothermType.langmuir: @@ -994,9 +994,7 @@ def eq_Pe_p(b): # Eq. 3.313, Inglezakis + Poulopoulos, for downflow # =========== RESIN & COLUMN =========== - @self.Constraint(doc="Interstitial velocity") - def eq_vel_inter(b): - return b.vel_inter == b.vel_bed / b.bed_porosity + @self.Constraint(doc="Resin bead surface area per volume") def eq_resin_surf_per_vol(b): @@ -1095,7 +1093,7 @@ def eq_dimensionless_time( b, ): # Eqs. 16-120, 16-129, Perry's; Eq. 4.136, Inglezakis + Poulopoulos return b.dimensionless_time * b.partition_ratio == ( - ((b.vel_bed / b.bed_porosity) * b.t_breakthru * b.bed_porosity) + (b.vel_inter * b.t_breakthru * b.bed_porosity) / b.bed_depth - b.bed_porosity ) @@ -1374,9 +1372,6 @@ def calculate_scaling_factors(self): if iscale.get_scaling_factor(self.vel_bed) is None: iscale.set_scaling_factor(self.vel_bed, 1e3) - if iscale.get_scaling_factor(self.vel_inter) is None: - iscale.set_scaling_factor(self.vel_inter, 1e3) - # unique scaling for isotherm type if isotherm == IsothermType.langmuir: if iscale.get_scaling_factor(self.resin_max_capacity) is None: @@ -1467,10 +1462,6 @@ def calculate_scaling_factors(self): if iscale.get_scaling_factor(c) is None: iscale.constraint_scaling_transform(c, 1e2) - for ind, c in self.eq_vel_inter.items(): - sf = iscale.get_scaling_factor(self.vel_inter) - iscale.constraint_scaling_transform(c, sf) - def _get_stream_table_contents(self, time_point=0): return create_stream_table_dataframe( diff --git a/watertap/unit_models/tests/test_ion_exchange_0D.py b/watertap/unit_models/tests/test_ion_exchange_0D.py index 15a7d2b17b..fe23d64a20 100644 --- a/watertap/unit_models/tests/test_ion_exchange_0D.py +++ b/watertap/unit_models/tests/test_ion_exchange_0D.py @@ -179,7 +179,6 @@ def test_default_build(self, IX_lang): "t_breakthru", "ebct", "vel_bed", - "vel_inter", "service_flow_rate", "N_Re", "N_Sc", @@ -203,8 +202,8 @@ def test_default_build(self, IX_lang): assert isinstance(var, Var) # test statistics - assert number_variables(m) == 67 - assert number_total_constraints(m) == 41 + assert number_variables(m) == 66 + assert number_total_constraints(m) == 40 assert number_unused_variables(m) == 11 @pytest.mark.unit @@ -269,7 +268,6 @@ def test_solution(self, IX_lang): "t_breakthru": 52360.64416318684, "mass_removed": 65300.80520398353, "vel_bed": 0.007083333333333333, - "vel_inter": 0.014166666666666666, "service_flow_rate": 15, "N_Re": 4.958333333333333, "N_Sc": 1086.9565217391305, @@ -451,7 +449,6 @@ def test_default_build(self, IX_fr): "t_breakthru", "ebct", "vel_bed", - "vel_inter", "service_flow_rate", "N_Re", "N_Sc", @@ -476,8 +473,8 @@ def test_default_build(self, IX_fr): assert isinstance(var, Var) # test statistics - assert number_variables(m) == 79 - assert number_total_constraints(m) == 50 + assert number_variables(m) == 78 + assert number_total_constraints(m) == 49 assert number_unused_variables(m) == 12 @pytest.mark.unit @@ -536,7 +533,6 @@ def test_solution(self, IX_fr): "t_breakthru": 4320000.0, "ebct": 240.00000000000003, "vel_bed": 0.006149999999999999, - "vel_inter": 0.012299999999999998, "service_flow_rate": 15, "N_Re": 4.151249999999999, "N_Sc": {"Cl_-": 999.9999999999998}, @@ -740,7 +736,6 @@ def test_default_build(self, IX_single_use): "t_breakthru", "ebct", "vel_bed", - "vel_inter", "service_flow_rate", "N_Re", "N_Sc", @@ -765,8 +760,8 @@ def test_default_build(self, IX_single_use): assert isinstance(var, Var) # test statistics - assert number_variables(m) == 79 - assert number_total_constraints(m) == 47 + assert number_variables(m) == 78 + assert number_total_constraints(m) == 46 assert number_unused_variables(m) == 15 @pytest.mark.unit @@ -825,7 +820,6 @@ def test_solution(self, IX_single_use): "t_breakthru": 4320000.000000001, "ebct": 240.0, "vel_bed": 0.00615, - "vel_inter": 0.0123, "service_flow_rate": 15, "N_Re": 4.15125, "N_Sc": {"Cl_-": 1000.0}, @@ -1036,7 +1030,6 @@ def test_default_build(self, IX_inert): "t_breakthru", "ebct", "vel_bed", - "vel_inter", "service_flow_rate", "N_Re", "N_Sc", @@ -1061,8 +1054,8 @@ def test_default_build(self, IX_inert): assert isinstance(var, Var) # test statistics - assert number_variables(m) == 87 - assert number_total_constraints(m) == 54 + assert number_variables(m) == 86 + assert number_total_constraints(m) == 53 assert number_unused_variables(m) == 14 @pytest.mark.unit @@ -1117,7 +1110,6 @@ def test_solution(self, IX_inert): "t_breakthru": 4320000.0, "ebct": 240.0, "vel_bed": 0.006149999999999999, - "vel_inter": 0.012299999999999998, "service_flow_rate": 15, "N_Re": 4.151249999999999, "N_Sc": {"Cl_-": 999.9999999999998}, From aeb1a4885cdffc83e6a7f136d90115b030bfe5d1 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Fri, 27 Oct 2023 12:37:23 -0600 Subject: [PATCH 14/27] black --- watertap/unit_models/ion_exchange_0D.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/watertap/unit_models/ion_exchange_0D.py b/watertap/unit_models/ion_exchange_0D.py index 796938b08a..a4b4ce76d3 100644 --- a/watertap/unit_models/ion_exchange_0D.py +++ b/watertap/unit_models/ion_exchange_0D.py @@ -795,8 +795,6 @@ def pressure_drop(b): def bed_vol(b): return b.bed_vol_tot / b.number_columns - - if self.config.regenerant != RegenerantChem.single_use: @self.Expression(doc="Backwashing flow rate") @@ -910,11 +908,11 @@ def col_vol_per(b): @self.Expression(doc="Total column volume required") def col_vol_tot(b): return b.number_columns * b.col_vol_per - + @self.Expression(doc="Contact time") def t_contact(b): return b.ebct * b.bed_porosity - + @self.Expression(doc="Interstitial velocity") def vel_inter(b): return b.vel_bed / b.bed_porosity @@ -994,8 +992,6 @@ def eq_Pe_p(b): # Eq. 3.313, Inglezakis + Poulopoulos, for downflow # =========== RESIN & COLUMN =========== - - @self.Constraint(doc="Resin bead surface area per volume") def eq_resin_surf_per_vol(b): return b.resin_surf_per_vol == (6 * (1 - b.bed_porosity)) / b.resin_diam @@ -1093,8 +1089,7 @@ def eq_dimensionless_time( b, ): # Eqs. 16-120, 16-129, Perry's; Eq. 4.136, Inglezakis + Poulopoulos return b.dimensionless_time * b.partition_ratio == ( - (b.vel_inter * b.t_breakthru * b.bed_porosity) - / b.bed_depth + (b.vel_inter * b.t_breakthru * b.bed_porosity) / b.bed_depth - b.bed_porosity ) From 29182416437f6c41c32b0a60f70f94c5e6f3e936 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Fri, 27 Oct 2023 12:43:46 -0600 Subject: [PATCH 15/27] remove commented code --- watertap/unit_models/tests/test_ion_exchange_0D.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/watertap/unit_models/tests/test_ion_exchange_0D.py b/watertap/unit_models/tests/test_ion_exchange_0D.py index fe23d64a20..cc96472051 100644 --- a/watertap/unit_models/tests/test_ion_exchange_0D.py +++ b/watertap/unit_models/tests/test_ion_exchange_0D.py @@ -340,7 +340,6 @@ def IX_fr(self): } m.fs.ix = ix = IonExchange0D(**ix_config) - # c0 = pyunits.convert(c0, to_units=pyunits.kg / pyunits.m**3) ix.process_flow.properties_in.calculate_state( var_args={ ("flow_vol_phase", "Liq"): 0.5, @@ -636,7 +635,6 @@ def IX_single_use(self): } m.fs.ix = ix = IonExchange0D(**ix_config) - # c0 = pyunits.convert(c0, to_units=pyunits.kg / pyunits.m**3) ix.process_flow.properties_in.calculate_state( var_args={ ("flow_vol_phase", "Liq"): 0.5, From 2505d7ca3f6bbf1e7b05b3d24b878a92b0ff6182 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Wed, 1 Nov 2023 13:19:12 -0400 Subject: [PATCH 16/27] isobaric + isothermic assumptions for regen stream --- watertap/unit_models/ion_exchange_0D.py | 50 +++++++++---------- .../unit_models/tests/test_ion_exchange_0D.py | 4 +- 2 files changed, 26 insertions(+), 28 deletions(-) diff --git a/watertap/unit_models/ion_exchange_0D.py b/watertap/unit_models/ion_exchange_0D.py index a4b4ce76d3..4d7dac0984 100644 --- a/watertap/unit_models/ion_exchange_0D.py +++ b/watertap/unit_models/ion_exchange_0D.py @@ -319,6 +319,10 @@ def build(self): self.add_outlet_port(name="regen", block=self.regeneration_stream) + for j in inerts: + self.process_flow.mass_transfer_term[:, "Liq", j].fix(0) + regen.get_material_flow_terms("Liq", j).fix(0) + # ==========PARAMETERS========== self.underdrain_h = Param( @@ -849,7 +853,7 @@ def rinse_pump_power(b): to_units=pyunits.kilowatts, ) - @self.Expression(doc="Rinse pump power") + @self.Expression(doc="Regen pump power") def regen_pump_power(b): return pyunits.convert( ( @@ -863,29 +867,26 @@ def regen_pump_power(b): to_units=pyunits.kilowatts, ) - @self.Constraint( - self.target_ion_set, doc="Mass transfer for regeneration stream" - ) - def eq_mass_transfer_regen(b, j): - return ( - regen.get_material_flow_terms("Liq", j) - == -b.process_flow.mass_transfer_term[0, "Liq", j] - ) - - @self.Constraint( - doc="Isothermal assumption for regen stream", + @self.Constraint( + self.target_ion_set, doc="Mass transfer for regeneration stream" + ) + def eq_mass_transfer_regen(b, j): + return ( + regen.get_material_flow_terms("Liq", j) + == -b.process_flow.mass_transfer_term[0, "Liq", j] ) - def eq_isothermal_regen_stream(b): - return prop_in.temperature == regen.temperature - @self.Constraint( - doc="Isobaric assumption for regen stream", - ) - def eq_isobaric_regen_stream(b): - return prop_in.pressure == regen.pressure + @self.Constraint( + doc="Isothermal assumption for regen stream", + ) + def eq_isothermal_regen_stream(b): + return prop_in.temperature == regen.temperature - for j in inerts: - self.regeneration_stream[0].get_material_flow_terms("Liq", j).fix(0) + @self.Constraint( + doc="Isobaric assumption for regen stream", + ) + def eq_isobaric_regen_stream(b): + return prop_in.pressure == regen.pressure @self.Expression(doc="Bed expansion from backwashing") def bed_expansion_h(b): @@ -956,9 +957,6 @@ def HTU(b, j): # ==========CONSTRAINTS========== - for j in inerts: - self.process_flow.mass_transfer_term[:, "Liq", j].fix(0) - # =========== DIMENSIONLESS =========== @self.Constraint(doc="Reynolds number") @@ -1030,7 +1028,7 @@ def eq_bed_design(b): def eq_col_height_to_diam_ratio(b): return b.col_height_to_diam_ratio * b.col_diam == b.col_height - # =========== MASS BALANCE =========== + # =========== ISOTHERM SPECIFIC =========== if self.config.isotherm == IsothermType.langmuir: @@ -1259,7 +1257,7 @@ def initialize_build( state_args_out = deepcopy(state_args) for p, j in self.process_flow.properties_out.phase_component_set: - if j == self.config.target_ion: + if j in self.target_ion_set: state_args_out["flow_mol_phase_comp"][(p, j)] = ( state_args["flow_mol_phase_comp"][(p, j)] * 1e-3 ) diff --git a/watertap/unit_models/tests/test_ion_exchange_0D.py b/watertap/unit_models/tests/test_ion_exchange_0D.py index cc96472051..eae847baae 100644 --- a/watertap/unit_models/tests/test_ion_exchange_0D.py +++ b/watertap/unit_models/tests/test_ion_exchange_0D.py @@ -759,8 +759,8 @@ def test_default_build(self, IX_single_use): # test statistics assert number_variables(m) == 78 - assert number_total_constraints(m) == 46 - assert number_unused_variables(m) == 15 + assert number_total_constraints(m) == 49 + assert number_unused_variables(m) == 12 @pytest.mark.unit def test_dof(self, IX_single_use): From 60828f2a2dd0b0153ec7994828a033abc13939b6 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Wed, 8 Nov 2023 10:42:48 -0700 Subject: [PATCH 17/27] add back bw and rinse for single_use --- watertap/costing/unit_models/ion_exchange.py | 1 - watertap/unit_models/ion_exchange_0D.py | 208 +++++++++--------- .../unit_models/tests/test_ion_exchange_0D.py | 38 ++-- 3 files changed, 126 insertions(+), 121 deletions(-) diff --git a/watertap/costing/unit_models/ion_exchange.py b/watertap/costing/unit_models/ion_exchange.py index 4f28d3fe66..b706d195a7 100644 --- a/watertap/costing/unit_models/ion_exchange.py +++ b/watertap/costing/unit_models/ion_exchange.py @@ -291,7 +291,6 @@ def cost_ion_exchange(blk): ) ) if blk.unit_model.config.regenerant == "single_use": - blk.capital_cost_backwash_tank.fix(0) blk.capital_cost_regen_tank.fix(0) blk.flow_mass_regen_soln.fix(0) blk.flow_vol_resin = pyo.Var( diff --git a/watertap/unit_models/ion_exchange_0D.py b/watertap/unit_models/ion_exchange_0D.py index 4d7dac0984..2a6d5bce06 100644 --- a/watertap/unit_models/ion_exchange_0D.py +++ b/watertap/unit_models/ion_exchange_0D.py @@ -424,34 +424,6 @@ def build(self): if self.config.regenerant != RegenerantChem.single_use: - # Bed expansion is calculated as a fraction of the bed_depth - # These coefficients are used to calculate that fraction (bed_expansion_frac) as a function of backwash rate (bw_rate, m/hr) - # bed_expansion_frac = bed_expansion_A + bed_expansion_B * bw_rate + bed_expansion_C * bw_rate**2 - # Default is for strong-base type I acrylic anion exchanger resin (A-850, Purolite), @20C - # Data extracted from MWH Chap 16, Figure 16-15 and fit with Excel - - self.bed_expansion_frac_A = Param( - initialize=-1.23e-2, - mutable=True, - units=pyunits.dimensionless, - doc="Bed expansion fraction eq intercept", - ) - - self.bed_expansion_frac_B = Param( - initialize=1.02e-1, - mutable=True, - units=pyunits.hr / pyunits.m, - doc="Bed expansion fraction equation B parameter", - ) - - self.bed_expansion_frac_C = Param( - initialize=-1.35e-3, - mutable=True, - units=pyunits.hr**2 / pyunits.m**2, - doc="Bed expansion fraction equation C parameter", - ) - # Rinse, Regen, Backwashing params - self.t_regen = Param( initialize=1800, mutable=True, @@ -459,26 +431,6 @@ def build(self): doc="Regeneration time", ) - self.rinse_bv = Param( - initialize=5, - mutable=True, - doc="Number of bed volumes for rinse step", - ) - - self.bw_rate = Param( - initialize=5, - mutable=True, - units=pyunits.m / pyunits.hour, - doc="Backwash loading rate [m/hr]", - ) - - self.t_bw = Param( - initialize=600, - mutable=True, - units=pyunits.s, - doc="Backwash time", - ) - self.service_to_regen_flow_ratio = Param( initialize=3, mutable=True, @@ -486,6 +438,54 @@ def build(self): doc="Ratio of service flow rate to regeneration flow rate", ) + # Bed expansion is calculated as a fraction of the bed_depth + # These coefficients are used to calculate that fraction (bed_expansion_frac) as a function of backwash rate (bw_rate, m/hr) + # bed_expansion_frac = bed_expansion_A + bed_expansion_B * bw_rate + bed_expansion_C * bw_rate**2 + # Default is for strong-base type I acrylic anion exchanger resin (A-850, Purolite), @20C + # Data extracted from MWH Chap 16, Figure 16-15 and fit with Excel + + self.bed_expansion_frac_A = Param( + initialize=-1.23e-2, + mutable=True, + units=pyunits.dimensionless, + doc="Bed expansion fraction eq intercept", + ) + + self.bed_expansion_frac_B = Param( + initialize=1.02e-1, + mutable=True, + units=pyunits.hr / pyunits.m, + doc="Bed expansion fraction equation B parameter", + ) + + self.bed_expansion_frac_C = Param( + initialize=-1.35e-3, + mutable=True, + units=pyunits.hr**2 / pyunits.m**2, + doc="Bed expansion fraction equation C parameter", + ) + # Rinse, Regen, Backwashing params + + self.rinse_bv = Param( + initialize=5, + mutable=True, + doc="Number of bed volumes for rinse step", + ) + + self.bw_rate = Param( + initialize=5, + mutable=True, + units=pyunits.m / pyunits.hour, + doc="Backwash loading rate [m/hr]", + ) + + self.t_bw = Param( + initialize=600, + mutable=True, + units=pyunits.s, + doc="Backwash time", + ) + # ==========VARIABLES========== # COMMON TO LANGMUIR + FREUNDLICH @@ -799,60 +799,24 @@ def pressure_drop(b): def bed_vol(b): return b.bed_vol_tot / b.number_columns - if self.config.regenerant != RegenerantChem.single_use: + @self.Expression(doc="Rinse time") + def t_rinse(b): + return b.ebct * b.rinse_bv - @self.Expression(doc="Backwashing flow rate") - def bw_flow(b): - return ( - pyunits.convert(b.bw_rate, to_units=pyunits.m / pyunits.s) - * (b.bed_vol / b.bed_depth) - * b.number_columns - ) + if self.config.regenerant == RegenerantChem.single_use: - @self.Expression(doc="Bed expansion fraction from backwashing") - def bed_expansion_frac(b): - return ( - b.bed_expansion_frac_A - + b.bed_expansion_frac_B * b.bw_rate - + b.bed_expansion_frac_C * b.bw_rate**2 - ) # for 20C + @self.Expression(doc="Waste time") + def t_waste(b): + return b.t_bw + b.t_rinse - @self.Expression(doc="Rinse flow rate") - def rinse_flow(b): - return b.vel_bed * (b.bed_vol / b.bed_depth) * b.number_columns + if self.config.regenerant != RegenerantChem.single_use: - @self.Expression(doc="Rinse time") - def t_rinse(b): - return b.ebct * b.rinse_bv + # If resin is not single use, add regeneration @self.Expression(doc="Waste time") def t_waste(b): return b.t_regen + b.t_bw + b.t_rinse - @self.Expression(doc="Cycle time") - def t_cycle(b): - return b.t_breakthru + b.t_waste - - @self.Expression(doc="Regen tank volume") - def regen_tank_vol(b): - return ( - prop_in.flow_vol_phase["Liq"] / b.service_to_regen_flow_ratio - ) * b.t_regen - - @self.Expression(doc="Backwash pump power") - def bw_pump_power(b): - return pyunits.convert( - (b.pressure_drop * b.bw_flow) / b.pump_efficiency, - to_units=pyunits.kilowatts, - ) - - @self.Expression(doc="Rinse pump power") - def rinse_pump_power(b): - return pyunits.convert( - (b.pressure_drop * b.rinse_flow) / b.pump_efficiency, - to_units=pyunits.kilowatts, - ) - @self.Expression(doc="Regen pump power") def regen_pump_power(b): return pyunits.convert( @@ -867,6 +831,50 @@ def regen_pump_power(b): to_units=pyunits.kilowatts, ) + @self.Expression(doc="Regen tank volume") + def regen_tank_vol(b): + return ( + prop_in.flow_vol_phase["Liq"] / b.service_to_regen_flow_ratio + ) * b.t_regen + + @self.Expression(doc="Backwashing flow rate") + def bw_flow(b): + return ( + pyunits.convert(b.bw_rate, to_units=pyunits.m / pyunits.s) + * (b.bed_vol / b.bed_depth) + * b.number_columns + ) + + @self.Expression(doc="Bed expansion fraction from backwashing") + def bed_expansion_frac(b): + return ( + b.bed_expansion_frac_A + + b.bed_expansion_frac_B * b.bw_rate + + b.bed_expansion_frac_C * b.bw_rate**2 + ) # for 20C + + @self.Expression(doc="Rinse flow rate") + def rinse_flow(b): + return b.vel_bed * (b.bed_vol / b.bed_depth) * b.number_columns + + @self.Expression(doc="Cycle time") + def t_cycle(b): + return b.t_breakthru + b.t_waste + + @self.Expression(doc="Backwash pump power") + def bw_pump_power(b): + return pyunits.convert( + (b.pressure_drop * b.bw_flow) / b.pump_efficiency, + to_units=pyunits.kilowatts, + ) + + @self.Expression(doc="Rinse pump power") + def rinse_pump_power(b): + return pyunits.convert( + (b.pressure_drop * b.rinse_flow) / b.pump_efficiency, + to_units=pyunits.kilowatts, + ) + @self.Constraint( self.target_ion_set, doc="Mass transfer for regeneration stream" ) @@ -890,10 +898,7 @@ def eq_isobaric_regen_stream(b): @self.Expression(doc="Bed expansion from backwashing") def bed_expansion_h(b): - if self.config.regenerant == RegenerantChem.single_use: - return 0 * pyunits.m - else: - return b.bed_expansion_frac * b.bed_depth + return b.bed_expansion_frac * b.bed_depth @self.Expression(doc="Main pump power") def main_pump_power(b): @@ -1118,12 +1123,9 @@ def eq_constant_pattern_soln( if self.config.isotherm == IsothermType.freundlich: - @self.Constraint(self.target_ion_set, doc="Breakthrough concentration") - def eq_c_breakthru(b, j): - return ( - b.c_norm[j] - == b.c_breakthru[j] / prop_in.conc_mass_phase_comp["Liq", j] - ) + @self.Expression(self.target_ion_set, doc="Breakthrough concentration") + def c_breakthru(b, j): + return b.c_norm[j] * prop_in.conc_mass_phase_comp["Liq", j] @self.Constraint( doc="Mass transfer coefficient from Clark equation (kT)", diff --git a/watertap/unit_models/tests/test_ion_exchange_0D.py b/watertap/unit_models/tests/test_ion_exchange_0D.py index eae847baae..a1c2880a4e 100644 --- a/watertap/unit_models/tests/test_ion_exchange_0D.py +++ b/watertap/unit_models/tests/test_ion_exchange_0D.py @@ -403,6 +403,7 @@ def test_default_build(self, IX_fr): assert isinstance(var, Var) # test unit objects + ix_params = [ "underdrain_h", "distributor_h", @@ -458,7 +459,6 @@ def test_default_build(self, IX_fr): "tb_traps", "traps", "c_norm_avg", - "c_breakthru", "freundlich_n", "mass_transfer_coeff", "bv", @@ -472,8 +472,8 @@ def test_default_build(self, IX_fr): assert isinstance(var, Var) # test statistics - assert number_variables(m) == 78 - assert number_total_constraints(m) == 49 + assert number_variables(m) == 77 + assert number_total_constraints(m) == 48 assert number_unused_variables(m) == 12 @pytest.mark.unit @@ -516,7 +516,6 @@ def test_solve(self, IX_fr): def test_solution(self, IX_fr): m = IX_fr ix = m.fs.ix - target_ion = ix.config.target_ion results_dict = { "resin_diam": 0.0006749999999999999, "resin_bulk_dens": 0.72, @@ -568,6 +567,7 @@ def test_solution(self, IX_fr): "bv": 18000, "bv_50": 20000, "kinetic_param": 1.5934630052514297e-06, + "t_waste": 3600, } for k, v in results_dict.items(): @@ -711,6 +711,12 @@ def test_default_build(self, IX_single_use): "p_drop_B", "p_drop_C", "pump_efficiency", + "bed_expansion_frac_A", + "bed_expansion_frac_B", + "bed_expansion_frac_C", + "rinse_bv", + "bw_rate", + "t_bw", "c_trap_min", ] @@ -744,7 +750,6 @@ def test_default_build(self, IX_single_use): "tb_traps", "traps", "c_norm_avg", - "c_breakthru", "freundlich_n", "mass_transfer_coeff", "bv", @@ -758,8 +763,8 @@ def test_default_build(self, IX_single_use): assert isinstance(var, Var) # test statistics - assert number_variables(m) == 78 - assert number_total_constraints(m) == 49 + assert number_variables(m) == 77 + assert number_total_constraints(m) == 48 assert number_unused_variables(m) == 12 @pytest.mark.unit @@ -802,7 +807,6 @@ def test_solve(self, IX_single_use): def test_solution(self, IX_single_use): m = IX_single_use ix = m.fs.ix - target_ion = ix.config.target_ion results_dict = { "resin_diam": 0.000675, "resin_bulk_dens": 0.72, @@ -811,9 +815,9 @@ def test_solution(self, IX_single_use): "bed_vol_tot": 120.00000000000001, "bed_depth": 1.476, "bed_porosity": 0.5, - "col_height": 2.476, + "col_height": 3.16, "col_diam": 2.3980942681530144, - "col_height_to_diam_ratio": 1.0324865176826379, + "col_height_to_diam_ratio": 1.318, "number_columns": 18, "t_breakthru": 4320000.000000001, "ebct": 240.0, @@ -854,6 +858,7 @@ def test_solution(self, IX_single_use): "bv": 18000, "bv_50": 20000, "kinetic_param": 1.5934630052514293e-06, + "t_waste": 1800, } for k, v in results_dict.items(): @@ -881,16 +886,16 @@ def test_costing(self, IX_single_use): results = solver.solve(m, tee=True) assert_optimal_termination(results) - assert pytest.approx(3521557.0901, rel=1e-3) == value( + assert pytest.approx(3820012.32, rel=1e-3) == value( m.fs.costing.aggregate_capital_cost ) - assert pytest.approx(6895468.46934, rel=1e-3) == value( + assert pytest.approx(6913375.783, rel=1e-3) == value( m.fs.costing.total_operating_cost ) - assert pytest.approx(7043114.18037, rel=1e-3) == value( + assert pytest.approx(7640024.640, rel=1e-3) == value( m.fs.costing.total_capital_cost ) - assert pytest.approx(0.535161094, rel=1e-3) == value(m.fs.costing.LCOW) + assert pytest.approx(0.540625, rel=1e-3) == value(m.fs.costing.LCOW) assert pytest.approx(0.01712042, rel=1e-3) == value( m.fs.costing.specific_energy_consumption ) @@ -1038,7 +1043,6 @@ def test_default_build(self, IX_inert): "tb_traps", "traps", "c_norm_avg", - "c_breakthru", "freundlich_n", "mass_transfer_coeff", "bv", @@ -1052,8 +1056,8 @@ def test_default_build(self, IX_inert): assert isinstance(var, Var) # test statistics - assert number_variables(m) == 86 - assert number_total_constraints(m) == 53 + assert number_variables(m) == 85 + assert number_total_constraints(m) == 52 assert number_unused_variables(m) == 14 @pytest.mark.unit From f835f9fdd2325e1d9256b6abd6f60ed8eddae8e8 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Wed, 8 Nov 2023 10:55:47 -0700 Subject: [PATCH 18/27] update IX docs --- docs/technical_reference/unit_models/ion_exchange_0D.rst | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/technical_reference/unit_models/ion_exchange_0D.rst b/docs/technical_reference/unit_models/ion_exchange_0D.rst index 9d1f8a188c..31d5d54b1a 100644 --- a/docs/technical_reference/unit_models/ion_exchange_0D.rst +++ b/docs/technical_reference/unit_models/ion_exchange_0D.rst @@ -211,9 +211,7 @@ If ``isotherm`` is set to ``freundlich``, the model includes the following compo "Freundlich isotherm exponent for resin/ion system", ":math:`n`", "``freundlich_n``", "None", ":math:`\text{dimensionless}`" "Bed volumes at breakthrough", ":math:`BV`", "``bv``", "None", ":math:`\text{dimensionless}`" "Bed volumes at 50% influent conc.", ":math:`BV_{50}`", "``bv_50``", "None", ":math:`\text{dimensionless}`" - "Kinetic fitting parameter", ":math:`r`", "``kinetic_param``", "None", ":math:`\text{dimensionless}`" "Mass transfer coefficient", ":math:`k_T`", "``mass_transfer_coeff``", "None", ":math:`\text{s}^{-1}`" - "Concentration at breakthrough", ":math:`C_{b}`", "``c_breakthru``", "``target_ion_set``", ":math:`\text{kg/}\text{m}^3`" "Average relative breakthrough concentration at breakthrough time", ":math:`X_{avg}`", "``c_norm_avg``", "None", ":math:`\text{dimensionless}`" "Relative breakthrough conc. for trapezoids", ":math:`X_{trap,k}`", "``c_traps``", "``k``", ":math:`\text{dimensionless}`" "Breakthrough times for trapezoids", ":math:`t_{trap,k}`", "``tb_traps``", "``k``", ":math:`\text{s}`" @@ -256,7 +254,7 @@ If ``isotherm`` is set to ``freundlich``, the additional variables to fix are: * ``freundlich_n`` * ``bv`` * ``c_norm`` -* one of ``bv_50``, ``kinetic_param``, or ``mass_transfer_coeff`` as determined from Clark model equations +* one of ``bv_50`` or ``mass_transfer_coeff`` as determined from Clark model equations @@ -346,7 +344,6 @@ Equations and Relationships "Breakthrough concentration", ":math:`X = \frac{C_b}{C_0}`" "Bed volumes at breakthrough concentration", ":math:`BV = \frac{t_{break} u_{bed}}{Z}`" "Clark equation with fundamental constants", ":math:`X = \frac{1}{\bigg(1 + (2^{n - 1} - 1)\text{exp}\bigg[\frac{k_T Z (n - 1)}{BV_{50} u_{bed}} (BV_{50} - BV)\bigg]\bigg)^{\frac{1}{n-1}}}`" - "Mass transfer coefficient from Clark equation", ":math:`k_T = \frac{r BV_{50}}{n - 1}`" "Evenly spaced c_norm for trapezoids", ":math:`X_{trap,k} = X_{trap,min} + (k - 1) \frac{X - X_{trap,min}}{n_{trap} - 1}`" "Breakthru time calculation for trapezoids", ":math:`t_{trap,k} = - \log{\frac{X_{trap,k}^{n-1}-1}{A}} / k_T`" "Area of trapezoids", ":math:`A_{trap,k} = \frac{t_{trap,k} - t_{trap,k - 1}}{t_{trap,n_{trap}}} \frac{X_{trap,k} + X_{trap,k - 1}}{2}`" From 7d8ebfdbce81ae1a1c9ea00643aada9954a9e6be Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Wed, 8 Nov 2023 10:56:28 -0700 Subject: [PATCH 19/27] remove kinetic_param from model --- watertap/unit_models/ion_exchange_0D.py | 19 --------------- .../unit_models/tests/test_ion_exchange_0D.py | 24 +++++++++---------- 2 files changed, 12 insertions(+), 31 deletions(-) diff --git a/watertap/unit_models/ion_exchange_0D.py b/watertap/unit_models/ion_exchange_0D.py index 2a6d5bce06..ecb5de0422 100644 --- a/watertap/unit_models/ion_exchange_0D.py +++ b/watertap/unit_models/ion_exchange_0D.py @@ -779,13 +779,6 @@ def build(self): doc="Bed volumes of feed at 50 percent breakthrough", ) - self.kinetic_param = Var( - initialize=1e-5, - bounds=(0, None), - units=pyunits.s**-1, - doc="Kinetic fitting parameter for Clark model (r)", - ) - # ==========EXPRESSIONS========== @self.Expression(doc="Pressure drop") @@ -1127,14 +1120,6 @@ def eq_constant_pattern_soln( def c_breakthru(b, j): return b.c_norm[j] * prop_in.conc_mass_phase_comp["Liq", j] - @self.Constraint( - doc="Mass transfer coefficient from Clark equation (kT)", - ) # Croll et al (2023), Eq.19 - def eq_mass_transfer_coeff(b): - return b.mass_transfer_coeff * (b.freundlich_n - 1) == ( - b.kinetic_param * b.bv_50 - ) - @self.Constraint(doc="Bed volumes at breakthrough") def eq_bv(b): return b.t_breakthru * b.vel_bed == b.bv * b.bed_depth @@ -1401,9 +1386,6 @@ def calculate_scaling_factors(self): if iscale.get_scaling_factor(self.c_breakthru) is None: iscale.set_scaling_factor(self.c_breakthru, 1e4) - if iscale.get_scaling_factor(self.kinetic_param) is None: - iscale.set_scaling_factor(self.kinetic_param, 1e7) - if iscale.get_scaling_factor(self.mass_transfer_coeff) is None: iscale.set_scaling_factor(self.mass_transfer_coeff, 10) @@ -1511,7 +1493,6 @@ def _get_performance_contents(self, time_point=0): var_dict[f"BV at 50% Breakthrough"] = self.bv_50 var_dict[f"Freundlich n"] = self.freundlich_n var_dict[f"Clark Mass Transfer Coeff."] = self.mass_transfer_coeff - var_dict[f"Clark Kinetic Param."] = self.kinetic_param return {"vars": var_dict} diff --git a/watertap/unit_models/tests/test_ion_exchange_0D.py b/watertap/unit_models/tests/test_ion_exchange_0D.py index a1c2880a4e..fb5f144187 100644 --- a/watertap/unit_models/tests/test_ion_exchange_0D.py +++ b/watertap/unit_models/tests/test_ion_exchange_0D.py @@ -463,7 +463,7 @@ def test_default_build(self, IX_fr): "mass_transfer_coeff", "bv", "bv_50", - "kinetic_param", + # "kinetic_param", ] for v in ix_vars: @@ -472,8 +472,8 @@ def test_default_build(self, IX_fr): assert isinstance(var, Var) # test statistics - assert number_variables(m) == 77 - assert number_total_constraints(m) == 48 + assert number_variables(m) == 76 + assert number_total_constraints(m) == 47 assert number_unused_variables(m) == 12 @pytest.mark.unit @@ -566,7 +566,7 @@ def test_solution(self, IX_fr): "mass_transfer_coeff": 0.159346300525143, "bv": 18000, "bv_50": 20000, - "kinetic_param": 1.5934630052514297e-06, + # "kinetic_param": 1.5934630052514297e-06, "t_waste": 3600, } @@ -754,7 +754,7 @@ def test_default_build(self, IX_single_use): "mass_transfer_coeff", "bv", "bv_50", - "kinetic_param", + # "kinetic_param", ] for v in ix_vars: @@ -763,8 +763,8 @@ def test_default_build(self, IX_single_use): assert isinstance(var, Var) # test statistics - assert number_variables(m) == 77 - assert number_total_constraints(m) == 48 + assert number_variables(m) == 76 + assert number_total_constraints(m) == 47 assert number_unused_variables(m) == 12 @pytest.mark.unit @@ -857,7 +857,7 @@ def test_solution(self, IX_single_use): "mass_transfer_coeff": 0.15934630052514298, "bv": 18000, "bv_50": 20000, - "kinetic_param": 1.5934630052514293e-06, + # "kinetic_param": 1.5934630052514293e-06, "t_waste": 1800, } @@ -1047,7 +1047,7 @@ def test_default_build(self, IX_inert): "mass_transfer_coeff", "bv", "bv_50", - "kinetic_param", + # "kinetic_param", ] for v in ix_vars: @@ -1056,8 +1056,8 @@ def test_default_build(self, IX_inert): assert isinstance(var, Var) # test statistics - assert number_variables(m) == 85 - assert number_total_constraints(m) == 52 + assert number_variables(m) == 84 + assert number_total_constraints(m) == 51 assert number_unused_variables(m) == 14 @pytest.mark.unit @@ -1147,7 +1147,7 @@ def test_solution(self, IX_inert): "mass_transfer_coeff": 0.159346300525143, "bv": 18000, "bv_50": 20000, - "kinetic_param": 1.59346300525143e-06, + # "kinetic_param": 1.59346300525143e-06, } for k, v in results_dict.items(): From 7761afed97697bf0e43665b0403991b11fc36c3d Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Wed, 8 Nov 2023 11:02:26 -0700 Subject: [PATCH 20/27] redo t_waste Expression to get around pylint fail --- watertap/unit_models/ion_exchange_0D.py | 42 +++++++++---------- .../unit_models/tests/test_ion_exchange_0D.py | 2 + 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/watertap/unit_models/ion_exchange_0D.py b/watertap/unit_models/ion_exchange_0D.py index ecb5de0422..d26c5b5145 100644 --- a/watertap/unit_models/ion_exchange_0D.py +++ b/watertap/unit_models/ion_exchange_0D.py @@ -422,21 +422,19 @@ def build(self): doc="Pump efficiency", ) - if self.config.regenerant != RegenerantChem.single_use: - - self.t_regen = Param( - initialize=1800, - mutable=True, - units=pyunits.s, - doc="Regeneration time", - ) + self.t_regen = Param( + initialize=1800, + mutable=True, + units=pyunits.s, + doc="Regeneration time", + ) - self.service_to_regen_flow_ratio = Param( - initialize=3, - mutable=True, - units=pyunits.dimensionless, - doc="Ratio of service flow rate to regeneration flow rate", - ) + self.service_to_regen_flow_ratio = Param( + initialize=3, + mutable=True, + units=pyunits.dimensionless, + doc="Ratio of service flow rate to regeneration flow rate", + ) # Bed expansion is calculated as a fraction of the bed_depth # These coefficients are used to calculate that fraction (bed_expansion_frac) as a function of backwash rate (bw_rate, m/hr) @@ -796,20 +794,18 @@ def bed_vol(b): def t_rinse(b): return b.ebct * b.rinse_bv + @self.Expression(doc="Waste time") + def t_waste(b): + return b.t_regen + b.t_bw + b.t_rinse + if self.config.regenerant == RegenerantChem.single_use: - - @self.Expression(doc="Waste time") - def t_waste(b): - return b.t_bw + b.t_rinse + self.t_regen.set_value(0) + self.service_to_regen_flow_ratio.set_value(0) if self.config.regenerant != RegenerantChem.single_use: # If resin is not single use, add regeneration - - @self.Expression(doc="Waste time") - def t_waste(b): - return b.t_regen + b.t_bw + b.t_rinse - + @self.Expression(doc="Regen pump power") def regen_pump_power(b): return pyunits.convert( diff --git a/watertap/unit_models/tests/test_ion_exchange_0D.py b/watertap/unit_models/tests/test_ion_exchange_0D.py index fb5f144187..a85cdda3b1 100644 --- a/watertap/unit_models/tests/test_ion_exchange_0D.py +++ b/watertap/unit_models/tests/test_ion_exchange_0D.py @@ -717,6 +717,8 @@ def test_default_build(self, IX_single_use): "rinse_bv", "bw_rate", "t_bw", + "t_regen", + "service_to_regen_flow_ratio", "c_trap_min", ] From 1e865a2d69e856d1705ae9e6fbecb4afb55e6f64 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Wed, 8 Nov 2023 11:04:06 -0700 Subject: [PATCH 21/27] black --- watertap/unit_models/ion_exchange_0D.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/watertap/unit_models/ion_exchange_0D.py b/watertap/unit_models/ion_exchange_0D.py index d26c5b5145..79c875bdef 100644 --- a/watertap/unit_models/ion_exchange_0D.py +++ b/watertap/unit_models/ion_exchange_0D.py @@ -797,7 +797,7 @@ def t_rinse(b): @self.Expression(doc="Waste time") def t_waste(b): return b.t_regen + b.t_bw + b.t_rinse - + if self.config.regenerant == RegenerantChem.single_use: self.t_regen.set_value(0) self.service_to_regen_flow_ratio.set_value(0) @@ -805,7 +805,7 @@ def t_waste(b): if self.config.regenerant != RegenerantChem.single_use: # If resin is not single use, add regeneration - + @self.Expression(doc="Regen pump power") def regen_pump_power(b): return pyunits.convert( From 2e21d3d603cea0ce27319c21ff537d0fe5c63c75 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Wed, 8 Nov 2023 11:19:11 -0700 Subject: [PATCH 22/27] modify power consumed eq in IX docs --- docs/technical_reference/unit_models/ion_exchange_0D.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/technical_reference/unit_models/ion_exchange_0D.rst b/docs/technical_reference/unit_models/ion_exchange_0D.rst index 31d5d54b1a..55b9ddaf8d 100644 --- a/docs/technical_reference/unit_models/ion_exchange_0D.rst +++ b/docs/technical_reference/unit_models/ion_exchange_0D.rst @@ -83,7 +83,7 @@ The model provides three ports (Pyomo notation in parenthesis): * Inlet port (inlet) * Outlet port (outlet) -* Regeneration port (regen, only if not using ``single_use`` resin configuration) +* Regeneration port (regen) Sets ---- @@ -419,7 +419,7 @@ And the total capital cost for the ion exchange system is the summation of these A total installed cost (:math:`f_{TIC}`) factor of 1.65 is applied to account for installation costs. .. note:: - If using ``single_use`` option for ``regenerant`` configuration keyword, the capital for the backwashing/rinse tank and regeneration tank are zero. + If using ``single_use`` option for ``regenerant`` configuration keyword, the capital for the regeneration tank is zero. Operating Cost Calculations ^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -483,10 +483,10 @@ If ``hazardous_waste`` is set to ``True``, the hazardous waste disposal costs ar Otherwise, :math:`C_{op,haz} = 0` as before. -Lastly, the total energy consumed by the unit for ``single_use`` configuration is only for the main booster pump: +Lastly, the total energy consumed by the unit for ``single_use`` configuration includes the booster pump, backwashing pump, and rinsing pump: .. math:: - P_{tot} = P_{main} + P_{tot} = P_{main} + P_{bw} + P_{rinse} References ---------- From eee7293cb1385b7dfdf801d84c18663ca61f9838 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Thu, 16 Nov 2023 15:22:26 -0700 Subject: [PATCH 23/27] remove c_breakthru Var --- watertap/unit_models/ion_exchange_0D.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/watertap/unit_models/ion_exchange_0D.py b/watertap/unit_models/ion_exchange_0D.py index 79c875bdef..c790548d47 100644 --- a/watertap/unit_models/ion_exchange_0D.py +++ b/watertap/unit_models/ion_exchange_0D.py @@ -741,14 +741,6 @@ def build(self): doc="Sum of trapezoid areas", ) - self.c_breakthru = Var( - self.target_ion_set, - initialize=0.5, - bounds=(0, None), - units=pyunits.kg / pyunits.m**3, - doc="Breakthrough concentration of target ion", - ) - self.freundlich_n = Var( initialize=1.5, bounds=(0, None), @@ -1379,9 +1371,6 @@ def calculate_scaling_factors(self): if iscale.get_scaling_factor(self.freundlich_n) is None: iscale.set_scaling_factor(self.freundlich_n, 0.1) - if iscale.get_scaling_factor(self.c_breakthru) is None: - iscale.set_scaling_factor(self.c_breakthru, 1e4) - if iscale.get_scaling_factor(self.mass_transfer_coeff) is None: iscale.set_scaling_factor(self.mass_transfer_coeff, 10) @@ -1482,9 +1471,6 @@ def _get_performance_contents(self, time_point=0): f"Fluid Mass Transfer Coeff. [{target_ion}]" ] = self.fluid_mass_transfer_coeff[target_ion] elif self.config.isotherm == IsothermType.freundlich: - var_dict[f"Breakthrough Conc. [{target_ion}]"] = self.c_breakthru[ - target_ion - ] var_dict[f"BV at Breakthrough"] = self.bv var_dict[f"BV at 50% Breakthrough"] = self.bv_50 var_dict[f"Freundlich n"] = self.freundlich_n From d74375843c81ee08e2ca7a1056d885e0b191dae8 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Thu, 14 Dec 2023 12:48:48 -0700 Subject: [PATCH 24/27] update IX_0D test file; clean IX costing --- watertap/costing/unit_models/ion_exchange.py | 1 - .../unit_models/tests/test_ion_exchange_0D.py | 717 +++++++++++------- 2 files changed, 436 insertions(+), 282 deletions(-) diff --git a/watertap/costing/unit_models/ion_exchange.py b/watertap/costing/unit_models/ion_exchange.py index 2eef6db619..d4c74574d3 100644 --- a/watertap/costing/unit_models/ion_exchange.py +++ b/watertap/costing/unit_models/ion_exchange.py @@ -348,7 +348,6 @@ def cost_ion_exchange(blk): ) ) - ) blk.costing_package.add_cost_factor(blk, "TIC") blk.capital_cost_constraint = pyo.Constraint( expr=blk.capital_cost diff --git a/watertap/unit_models/tests/test_ion_exchange_0D.py b/watertap/unit_models/tests/test_ion_exchange_0D.py index 7b5b2c72f1..eae510e0ab 100644 --- a/watertap/unit_models/tests/test_ion_exchange_0D.py +++ b/watertap/unit_models/tests/test_ion_exchange_0D.py @@ -85,6 +85,10 @@ def IX_lang(self): hold_state=True, ) + ix.process_flow.properties_in[0].flow_mass_phase_comp[...] + ix.process_flow.properties_out[0].flow_mass_phase_comp[...] + ix.regeneration_stream[0].flow_mass_phase_comp[...] + ix.service_flow_rate.fix(15) ix.langmuir[target_ion].fix(0.9) ix.resin_max_capacity.fix(3) @@ -94,7 +98,6 @@ def IX_lang(self): ix.resin_diam.fix() ix.resin_bulk_dens.fix() ix.bed_porosity.fix() - ix.regen_dose.fix() return m @@ -137,8 +140,6 @@ def test_default_build(self, IX_lang): # test unit objects ix_params = [ - "underdrain_h", - "distributor_h", "Pe_p_A", "Pe_p_exp", "Sh_A", @@ -148,16 +149,18 @@ def test_default_build(self, IX_lang): "bed_expansion_frac_A", "bed_expansion_frac_B", "bed_expansion_frac_C", + "bw_rate", + "distributor_h", + "number_columns_redund", "p_drop_A", "p_drop_B", "p_drop_C", "pump_efficiency", - "t_regen", "rinse_bv", - "bw_rate", - "t_bw", "service_to_regen_flow_ratio", - "number_columns_redund", + "t_bw", + "t_regen", + "underdrain_h", ] for p in ix_params: @@ -166,38 +169,35 @@ def test_default_build(self, IX_lang): assert isinstance(param, Param) ix_vars = [ - "resin_diam", - "resin_bulk_dens", - "resin_surf_per_vol", - "regen_dose", - "c_norm", - "col_height_to_diam_ratio", - "bed_vol_tot", + "N_Pe_bed", + "N_Pe_particle", + "N_Re", + "N_Sc", + "N_Sh", "bed_depth", "bed_porosity", - "col_height", + "bed_vol_tot", + "c_norm", "col_diam", - "number_columns", - "t_breakthru", - "t_contact", + "col_height", + "col_height_to_diam_ratio", + "dimensionless_time", "ebct", - "vel_bed", - "vel_inter", - "service_flow_rate", - "N_Re", - "N_Sc", - "N_Sh", - "N_Pe_particle", - "N_Pe_bed", - "resin_max_capacity", - "resin_eq_capacity", - "resin_unused_capacity", + "fluid_mass_transfer_coeff", "langmuir", "mass_removed", "num_transfer_units", - "dimensionless_time", + "number_columns", "partition_ratio", - "fluid_mass_transfer_coeff", + "resin_bulk_dens", + "resin_diam", + "resin_eq_capacity", + "resin_max_capacity", + "resin_surf_per_vol", + "resin_unused_capacity", + "service_flow_rate", + "t_breakthru", + "vel_bed", ] for v in ix_vars: @@ -206,9 +206,9 @@ def test_default_build(self, IX_lang): assert isinstance(var, Var) # test statistics - assert number_variables(m) == 69 - assert number_total_constraints(m) == 42 - assert number_unused_variables(m) == 12 + assert number_variables(m) == 70 + assert number_total_constraints(m) == 44 + assert number_unused_variables(m) == 10 @pytest.mark.unit def test_dof(self, IX_lang): @@ -244,52 +244,100 @@ def test_solve(self, IX_lang): # Check for optimal solution assert_optimal_termination(results) + @pytest.mark.requires_idaes_solver @pytest.mark.component - def test_solution(self, IX_lang): + def test_mass_balance(self, IX_lang): m = IX_lang + ix = m.fs.ix - target_ion = ix.config.target_ion + target = ix.config.target_ion + pf = ix.process_flow + prop_in = pf.properties_in[0] + prop_out = pf.properties_out[0] + regen = ix.regeneration_stream[0] + + assert value(prop_in.flow_mass_phase_comp["Liq", target]) == pytest.approx( + value(prop_out.flow_mass_phase_comp["Liq", target]) + + value(regen.flow_mass_phase_comp["Liq", target]), + rel=1e-3, + ) + + assert value(prop_in.flow_mass_phase_comp["Liq", "H2O"]) == pytest.approx( + value(prop_out.flow_mass_phase_comp["Liq", "H2O"]), + rel=1e-3, + ) + assert -1 * value(pf.mass_transfer_term[0, "Liq", target]) == pytest.approx( + value(regen.flow_mol_phase_comp["Liq", target]), rel=1e-3 + ) + + @pytest.mark.component + def test_solution(self, IX_lang): + m = IX_lang + + # results for all Var and Expressions on unit model results_dict = { - "resin_max_capacity": 3, - "resin_eq_capacity": 1.5547810762853227, - "resin_unused_capacity": 1.4452189237146773, "resin_diam": 0.0007, "resin_bulk_dens": 0.7, - "langmuir": 0.9, - "num_transfer_units": 35.54838294744622, - "dimensionless_time": 1, - "resin_surf_per_vol": 4285.714285714286, - "col_height_to_diam_ratio": 1.0408526790314099, - "bed_vol_tot": 120.00000000000003, + "resin_surf_per_vol": 4285.714285714285, + "c_norm": {"Ca_2+": 0.4919290557789296}, + "bed_vol_tot": 120.00000000000001, "bed_depth": 1.7, "bed_porosity": 0.5, "col_height": 3.488715, - "col_diam": 3.3517855795370646, + "col_diam": 3.351785579537064, + "col_height_to_diam_ratio": 1.0408526790314099, "number_columns": 8, - "partition_ratio": 217.66935067994518, - "fluid_mass_transfer_coeff": 3.456092786557271e-05, - "t_breakthru": 52360.64416318684, - "t_contact": 120.0, - "mass_removed": 65300.80520398353, + "t_breakthru": 52360.64416318655, + "ebct": 240.0, "vel_bed": 0.007083333333333333, - "vel_inter": 0.014166666666666666, "service_flow_rate": 15, "N_Re": 4.958333333333333, - "N_Sc": 1086.9565217391305, - "N_Sh": 26.29635815858793, + "N_Sc": {"Ca_2+": 1086.9565217391307}, + "N_Sh": {"Ca_2+": 26.29635815858793}, "N_Pe_particle": 0.10782790064157834, - "N_Pe_bed": 261.86775870097597, - "c_norm": 0.4919290557789296, - "regen_dose": 300, + "N_Pe_bed": 261.8677587009759, + "resin_max_capacity": 3, + "resin_eq_capacity": 1.5547810762853225, + "resin_unused_capacity": 1.4452189237146778, + "langmuir": {"Ca_2+": 0.9}, + "mass_removed": {"Ca_2+": 65300.80520398353}, + "num_transfer_units": 35.54838294744621, + "dimensionless_time": 1, + "partition_ratio": 217.66935067994396, + "fluid_mass_transfer_coeff": {"Ca_2+": 3.4560927865572706e-05}, + "pressure_drop": 9.450141899999998, + "bed_vol": 15.000000000000002, + "t_rinse": 1200.0, + "t_waste": 3600.0, + "regen_pump_power": 13.574257247187694, + "regen_tank_vol": 300.00000000000006, + "bw_flow": 0.09803921568627452, + "bed_expansion_frac": 0.46395000000000003, + "rinse_flow": 0.5, + "t_cycle": 55960.64416318655, + "bw_pump_power": 7.984857204228056, + "rinse_pump_power": 40.72277174156308, + "bed_expansion_h": 0.788715, + "main_pump_power": 40.722771741563086, + "col_vol_per": 30.782779411764707, + "col_vol_tot": 246.26223529411766, + "t_contact": 120.0, + "vel_inter": 0.014166666666666666, + "bv_calc": 218.16935067994396, + "lh": 0.0, + "separation_factor": {"Ca_2+": 1.1111111111111112}, + "rate_coeff": {"Ca_2+": 0.00021159751754432275}, + "HTU": {"Ca_2+": 0.04782214714276131}, } - for v, val in results_dict.items(): - var = getattr(ix, v) - if var.is_indexed(): - assert pytest.approx(val, rel=1e-3) == value(var[target_ion]) + for v, r in results_dict.items(): + ixv = getattr(m.fs.ix, v) + if ixv.is_indexed(): + for i, s in r.items(): + assert pytest.approx(s, rel=1e-3) == value(ixv[i]) else: - assert pytest.approx(val, rel=1e-3) == value(var) + assert pytest.approx(r, rel=1e-3) == value(ixv) @pytest.mark.component def test_costing(self, IX_lang): @@ -301,25 +349,58 @@ def test_costing(self, IX_lang): m.fs.costing.cost_process() m.fs.costing.add_LCOW(ix.process_flow.properties_out[0].flow_vol_phase["Liq"]) m.fs.costing.add_specific_energy_consumption( - ix.process_flow.properties_out[0].flow_vol_phase["Liq"] + ix.process_flow.properties_out[0].flow_vol_phase["Liq"], name="SEC" ) results = solver.solve(m, tee=True) assert_optimal_termination(results) - assert pytest.approx(2.0 * 8894349.86900 / 1.65, rel=1e-3) == value( - m.fs.costing.aggregate_capital_cost - ) - assert pytest.approx(2288575.0472, rel=1e-3) == value( - m.fs.costing.total_operating_cost - ) - assert pytest.approx(17788699.7380 / 1.65, rel=1e-3) == value( - m.fs.costing.total_capital_cost - ) - assert pytest.approx(0.2370983, rel=1e-3) == value(m.fs.costing.LCOW) - assert pytest.approx(0.0572452, rel=1e-3) == value( - m.fs.costing.specific_energy_consumption - ) + sys_cost_results = { + "aggregate_capital_cost": 3993072.4698084667, + "aggregate_fixed_operating_cost": 36893.31436153293, + "aggregate_variable_operating_cost": 0.0, + "aggregate_flow_electricity": 103.00465793454192, + "aggregate_flow_NaCl": 22838957.969693653, + "aggregate_flow_costs": { + "electricity": 63205.71820179364, + "NaCl": 2079295.2023431766, + }, + "total_capital_cost": 3993072.4698084667, + "total_operating_cost": 2084936.31694626, + "LCOW": 0.17495285, + "SEC": 0.0572305198, + } + + for v, r in sys_cost_results.items(): + mv = getattr(m.fs.costing, v) + if mv.is_indexed(): + for i, s in r.items(): + assert pytest.approx(s, rel=1e-3) == value(mv[i]) + else: + assert pytest.approx(r, rel=1e-3) == value(mv) + + ix_cost_results = { + "capital_cost": 3993072.4698084667, + "fixed_operating_cost": 36893.31436153293, + "capital_cost_vessel": 101131.8811552528, + "capital_cost_resin": 81985.14302562873, + "capital_cost_regen_tank": 215778.261765144, + "capital_cost_backwash_tank": 132704.75551115556, + "operating_cost_hazardous": 0, + "flow_mass_regen_soln": 22838957.969693653, + "total_pumping_power": 103.00465793454192, + "backwash_tank_vol": 174042.7639065449, + "regeneration_tank_vol": 79251.61570744455, + "direct_capital_cost": 1996536.2349042334, + } + + for v, r in ix_cost_results.items(): + mv = getattr(m.fs.ix.costing, v) + if mv.is_indexed(): + for i, s in r.items(): + assert pytest.approx(s, rel=1e-3) == value(mv[i]) + else: + assert pytest.approx(r, rel=1e-3) == value(mv) class TestIonExchangeFreundlich: @@ -347,7 +428,6 @@ def IX_fr(self): } m.fs.ix = ix = IonExchange0D(**ix_config) - # c0 = pyunits.convert(c0, to_units=pyunits.kg / pyunits.m**3) ix.process_flow.properties_in.calculate_state( var_args={ ("flow_vol_phase", "Liq"): 0.5, @@ -358,11 +438,14 @@ def IX_fr(self): hold_state=True, ) + ix.process_flow.properties_in[0].flow_mass_phase_comp[...] + ix.process_flow.properties_out[0].flow_mass_phase_comp[...] + ix.regeneration_stream[0].flow_mass_phase_comp[...] + ix.freundlich_n.fix(1.2) ix.bv_50.fix(20000) ix.bv.fix(18000) ix.resin_bulk_dens.fix(0.72) - ix.regen_dose.fix() ix.bed_porosity.fix() ix.vel_bed.fix(6.15e-3) ix.resin_diam.fix(6.75e-4) @@ -393,7 +476,7 @@ def test_config(self, IX_fr): assert m.fs.ix.config.momentum_balance_type is MomentumBalanceType.pressureTotal @pytest.mark.unit - def test_default_build(self, IX_fr): + def test_fr_build(self, IX_fr): m = IX_fr ix = m.fs.ix # test ports and variables @@ -412,8 +495,6 @@ def test_default_build(self, IX_fr): # test unit objects ix_params = [ - "underdrain_h", - "distributor_h", "Pe_p_A", "Pe_p_exp", "Sh_A", @@ -423,17 +504,19 @@ def test_default_build(self, IX_fr): "bed_expansion_frac_A", "bed_expansion_frac_B", "bed_expansion_frac_C", + "bw_rate", + "c_trap_min", + "distributor_h", + "number_columns_redund", "p_drop_A", "p_drop_B", "p_drop_C", "pump_efficiency", - "t_regen", "rinse_bv", - "bw_rate", - "t_bw", "service_to_regen_flow_ratio", - "number_columns_redund", - "c_trap_min", + "t_bw", + "t_regen", + "underdrain_h", ] for p in ix_params: @@ -442,40 +525,34 @@ def test_default_build(self, IX_fr): assert isinstance(param, Param) ix_vars = [ - "resin_diam", - "resin_bulk_dens", - "resin_surf_per_vol", - "regen_dose", - "c_norm", - "bed_vol_tot", + "N_Pe_bed", + "N_Pe_particle", + "N_Re", + "N_Sc", + "N_Sh", "bed_depth", "bed_porosity", - "col_height", + "bed_vol_tot", + "bv", + "bv_50", + "c_norm", + "c_norm_avg", + "c_traps", "col_diam", + "col_height", "col_height_to_diam_ratio", - "number_columns", - "t_breakthru", - "t_contact", "ebct", - "vel_bed", - "vel_inter", + "freundlich_n", + "mass_transfer_coeff", + "number_columns", + "resin_bulk_dens", + "resin_diam", + "resin_surf_per_vol", "service_flow_rate", - "N_Re", - "N_Sc", - "N_Sh", - "N_Pe_particle", - "N_Pe_bed", - "c_traps", + "t_breakthru", "tb_traps", "traps", - "c_norm_avg", - "c_breakthru", - "freundlich_n", - "mass_transfer_coeff", - "bv", - "bv_50", - "bed_capacity_param", - "kinetic_param", + "vel_bed", ] for v in ix_vars: @@ -484,9 +561,9 @@ def test_default_build(self, IX_fr): assert isinstance(var, Var) # test statistics - assert number_variables(m) == 82 - assert number_total_constraints(m) == 52 - assert number_unused_variables(m) == 13 + assert number_variables(m) == 80 + assert number_total_constraints(m) == 51 + assert number_unused_variables(m) == 11 @pytest.mark.unit def test_dof(self, IX_fr): @@ -525,77 +602,34 @@ def test_solve(self, IX_fr): @pytest.mark.requires_idaes_solver @pytest.mark.component - def test_solution(self, IX_fr): + def test_mass_balance(self, IX_fr): m = IX_fr + ix = m.fs.ix - target_ion = ix.config.target_ion - results_dict = { - "resin_diam": 0.0006749999999999999, - "resin_bulk_dens": 0.72, - "resin_surf_per_vol": 4444.444444444445, - "regen_dose": 300, - "c_norm": {"Cl_-": 0.25}, - "bed_vol_tot": 120.00000000000001, - "bed_depth": 1.476, - "bed_porosity": 0.5, - "col_height": 3.1607902, - "col_diam": 2.5435630784033814, - "col_height_to_diam_ratio": 1.242662400172933, - "number_columns": 16, - "t_breakthru": 4320000.0, - "t_contact": 120.00000000000001, - "ebct": 240.00000000000003, - "vel_bed": 0.006149999999999999, - "vel_inter": 0.012299999999999998, - "service_flow_rate": 15, - "N_Re": 4.151249999999999, - "N_Sc": {"Cl_-": 999.9999999999998}, - "N_Sh": {"Cl_-": 24.083093218519274}, - "N_Pe_particle": 0.09901383248136636, - "N_Pe_bed": 216.51024702592113, - "c_traps": { - 0: 0, - 1: 0.01, - 2: 0.06999999999999999, - 3: 0.13, - 4: 0.19, - 5: 0.25, - }, - "tb_traps": { - 0: 0, - 1: 3344557.580567041, - 2: 3825939.1495324974, - 3: 4034117.3864088757, - 4: 4188551.111889635, - 5: 4320000.000000004, - }, - "traps": { - 1: 0.003871015718248886, - 2: 0.0044572367496801485, - 3: 0.004818940668434689, - 4: 0.005719767610398479, - 5: 0.00669415633895397, - }, - "c_norm_avg": {"Cl_-": 0.02556111708571618}, - "c_breakthru": {"Cl_-": 2.500000002016729e-07}, - "freundlich_n": 1.2, - "mass_transfer_coeff": 0.159346300525143, - "bv": 18000, - "bv_50": 20000, - "bed_capacity_param": 311.9325370632754, - "kinetic_param": 1.5934630052514297e-06, - } + target = ix.config.target_ion + pf = ix.process_flow + prop_in = pf.properties_in[0] + prop_out = pf.properties_out[0] + regen = ix.regeneration_stream[0] + + assert value(prop_in.flow_mass_phase_comp["Liq", target]) == pytest.approx( + value(prop_out.flow_mass_phase_comp["Liq", target]) + + value(regen.flow_mass_phase_comp["Liq", target]), + rel=1e-3, + ) - for k, v in results_dict.items(): - var = getattr(ix, k) - if isinstance(v, dict): - for i, u in v.items(): - assert pytest.approx(u, rel=1e-3) == value(var[i]) - else: - assert pytest.approx(v, rel=1e-3) == value(var) + assert value(prop_in.flow_mass_phase_comp["Liq", "H2O"]) == pytest.approx( + value(prop_out.flow_mass_phase_comp["Liq", "H2O"]), + rel=1e-3, + ) + + assert -1 * value(pf.mass_transfer_term[0, "Liq", target]) == pytest.approx( + value(regen.flow_mol_phase_comp["Liq", target]), rel=1e-3 + ) + @pytest.mark.requires_idaes_solver @pytest.mark.component - def test_costing(self, IX_fr): + def test_solution(self, IX_fr): m = IX_fr ix = m.fs.ix @@ -604,26 +638,64 @@ def test_costing(self, IX_fr): m.fs.costing.cost_process() m.fs.costing.add_LCOW(ix.process_flow.properties_out[0].flow_vol_phase["Liq"]) m.fs.costing.add_specific_energy_consumption( - ix.process_flow.properties_out[0].flow_vol_phase["Liq"] + ix.process_flow.properties_out[0].flow_vol_phase["Liq"], name="SEC" ) ix.initialize() results = solver.solve(m, tee=True) assert_optimal_termination(results) - assert pytest.approx(2.0 * 9701947.4187 / 1.65, rel=1e-3) == value( - m.fs.costing.aggregate_capital_cost - ) - assert pytest.approx(1219532.1263, rel=1e-3) == value( - m.fs.costing.total_operating_cost - ) - assert pytest.approx(19403894.837 / 1.65, rel=1e-3) == value( - m.fs.costing.total_capital_cost - ) - assert pytest.approx(0.168688, rel=1e-3) == value(m.fs.costing.LCOW) - assert pytest.approx(0.04382530, rel=1e-3) == value( - m.fs.costing.specific_energy_consumption - ) + sys_cost_results = { + "aggregate_capital_cost": 5116213.693319044, + "aggregate_fixed_operating_cost": 323306.0679291797, + "aggregate_variable_operating_cost": 0.0, + "aggregate_flow_electricity": 78.86531537032893, + "aggregate_flow_NaOH": 279183.59700249793, + "aggregate_flow_costs": { + "electricity": 48393.334817541254, + "NaOH": 555415.5212892868, + }, + "total_capital_cost": 5116213.693319044, + "total_operating_cost": 1020220.4492248963, + "aggregate_direct_capital_cost": 2558106.8466595225, + "maintenance_labor_chemical_operating_cost": 153486.4107995713, + "total_fixed_operating_cost": 476792.478728751, + "total_variable_operating_cost": 543427.9704961453, + "total_annualized_cost": 1531841.8185568007, + "LCOW": 0.10786919580206682, + "SEC": 0.04381406413732131, + } + + for v, r in sys_cost_results.items(): + mv = getattr(m.fs.costing, v) + if mv.is_indexed(): + for i, s in r.items(): + assert pytest.approx(s, rel=1e-3) == value(mv[i]) + else: + assert pytest.approx(r, rel=1e-3) == value(mv) + + ix_cost_results = { + "capital_cost": 5116213.693319045, + "fixed_operating_cost": 323306.0679291797, + "capital_cost_vessel": 75000.3204403362, + "capital_cost_resin": 54924.687321091136, + "capital_cost_regen_tank": 215778.261765144, + "capital_cost_backwash_tank": 133603.4529501137, + "operating_cost_hazardous": 276620.08370625216, + "flow_mass_regen_soln": 279183.597002498, + "total_pumping_power": 78.86531537032894, + "backwash_tank_vol": 176401.06694050893, + "regeneration_tank_vol": 79251.61570744455, + "direct_capital_cost": 2558106.8466595225, + } + + for v, r in ix_cost_results.items(): + mv = getattr(m.fs.ix.costing, v) + if mv.is_indexed(): + for i, s in r.items(): + assert pytest.approx(s, rel=1e-3) == value(mv[i]) + else: + assert pytest.approx(r, rel=1e-3) == value(mv) class TestIonExchangeInert: @@ -646,6 +718,7 @@ def IX_inert(self): ix_config = { "property_package": m.fs.properties, "target_ion": target_ion, + "regenerant": "single_use", "isotherm": "freundlich", } m.fs.ix = ix = IonExchange0D(**ix_config) @@ -661,11 +734,14 @@ def IX_inert(self): hold_state=True, ) + ix.process_flow.properties_in[0].flow_mass_phase_comp[...] + ix.process_flow.properties_out[0].flow_mass_phase_comp[...] + ix.regeneration_stream[0].flow_mass_phase_comp[...] + ix.freundlich_n.fix(1.2) ix.bv_50.fix(20000) ix.bv.fix(18000) ix.resin_bulk_dens.fix(0.72) - ix.regen_dose.fix() ix.bed_porosity.fix() ix.vel_bed.fix(6.15e-3) ix.resin_diam.fix(6.75e-4) @@ -685,7 +761,7 @@ def test_config(self, IX_inert): assert not m.fs.ix.config.has_holdup assert m.fs.ix.config.property_package is m.fs.properties assert not m.fs.ix.config.hazardous_waste - assert m.fs.ix.config.regenerant is RegenerantChem.NaCl + assert m.fs.ix.config.regenerant is RegenerantChem.single_use assert isinstance(m.fs.ix.ion_exchange_type, IonExchangeType) assert m.fs.ix.ion_exchange_type is IonExchangeType.anion assert isinstance(m.fs.ix.config.isotherm, IsothermType) @@ -696,7 +772,7 @@ def test_config(self, IX_inert): assert m.fs.ix.config.momentum_balance_type is MomentumBalanceType.pressureTotal @pytest.mark.unit - def test_default_build(self, IX_inert): + def test_inert_build(self, IX_inert): m = IX_inert ix = m.fs.ix # test ports and variables @@ -715,8 +791,6 @@ def test_default_build(self, IX_inert): # test unit objects ix_params = [ - "underdrain_h", - "distributor_h", "Pe_p_A", "Pe_p_exp", "Sh_A", @@ -726,17 +800,19 @@ def test_default_build(self, IX_inert): "bed_expansion_frac_A", "bed_expansion_frac_B", "bed_expansion_frac_C", + "bw_rate", + "c_trap_min", + "distributor_h", + "number_columns_redund", "p_drop_A", "p_drop_B", "p_drop_C", "pump_efficiency", - "t_regen", "rinse_bv", - "bw_rate", - "t_bw", "service_to_regen_flow_ratio", - "number_columns_redund", - "c_trap_min", + "t_bw", + "t_regen", + "underdrain_h", ] for p in ix_params: @@ -745,40 +821,34 @@ def test_default_build(self, IX_inert): assert isinstance(param, Param) ix_vars = [ - "resin_diam", - "resin_bulk_dens", - "resin_surf_per_vol", - "regen_dose", - "c_norm", - "bed_vol_tot", + "N_Pe_bed", + "N_Pe_particle", + "N_Re", + "N_Sc", + "N_Sh", "bed_depth", "bed_porosity", - "col_height", + "bed_vol_tot", + "bv", + "bv_50", + "c_norm", + "c_norm_avg", + "c_traps", "col_diam", + "col_height", "col_height_to_diam_ratio", - "number_columns", - "t_breakthru", - "t_contact", "ebct", - "vel_bed", - "vel_inter", + "freundlich_n", + "mass_transfer_coeff", + "number_columns", + "resin_bulk_dens", + "resin_diam", + "resin_surf_per_vol", "service_flow_rate", - "N_Re", - "N_Sc", - "N_Sh", - "N_Pe_particle", - "N_Pe_bed", - "c_traps", + "t_breakthru", "tb_traps", "traps", - "c_norm_avg", - "c_breakthru", - "freundlich_n", - "mass_transfer_coeff", - "bv", - "bv_50", - "bed_capacity_param", - "kinetic_param", + "vel_bed", ] for v in ix_vars: @@ -788,8 +858,8 @@ def test_default_build(self, IX_inert): # test statistics assert number_variables(m) == 90 - assert number_total_constraints(m) == 56 - assert number_unused_variables(m) == 15 + assert number_total_constraints(m) == 57 + assert number_unused_variables(m) == 12 @pytest.mark.unit def test_dof(self, IX_inert): @@ -825,31 +895,62 @@ def test_solve(self, IX_inert): assert_optimal_termination(results) @pytest.mark.component - def test_solution(self, IX_inert): + def test_mass_balance(self, IX_inert): m = IX_inert + ix = m.fs.ix + target = ix.config.target_ion + inert = "Ca_2+" + pf = ix.process_flow + prop_in = pf.properties_in[0] + prop_out = pf.properties_out[0] + regen = ix.regeneration_stream[0] + + assert value(prop_in.flow_mass_phase_comp["Liq", target]) == pytest.approx( + value(prop_out.flow_mass_phase_comp["Liq", target]) + + value(regen.flow_mass_phase_comp["Liq", target]), + rel=1e-3, + ) + + assert value(prop_in.flow_mass_phase_comp["Liq", "H2O"]) == pytest.approx( + value(prop_out.flow_mass_phase_comp["Liq", "H2O"]), + rel=1e-3, + ) + + assert -1 * value(pf.mass_transfer_term[0, "Liq", target]) == pytest.approx( + value(regen.flow_mol_phase_comp["Liq", target]), rel=1e-3 + ) + + assert value(prop_in.flow_mass_phase_comp["Liq", inert]) == pytest.approx( + value(prop_out.flow_mass_phase_comp["Liq", inert]), + rel=1e-3, + ) + + assert value(pf.mass_transfer_term[0, "Liq", inert]) == 0 + + @pytest.mark.component + def test_solution(self, IX_inert): + m = IX_inert + results_dict = { - "resin_diam": 0.0006749999999999999, + "resin_diam": 0.000675, "resin_bulk_dens": 0.72, - "resin_surf_per_vol": 4444.444444444445, - "regen_dose": 300, + "resin_surf_per_vol": 4444.444444444444, "c_norm": {"Cl_-": 0.25}, - "bed_vol_tot": 120.0, - "bed_depth": 1.4759999999999998, + "bed_vol_tot": 119.99999999999997, + "bed_depth": 1.476, "bed_porosity": 0.5, - "col_height": 3.1607901999999997, - "col_diam": 2.543563078403381, - "col_height_to_diam_ratio": 1.242662400172933, + "col_height": 3.1607902, + "col_diam": 2.5435630784033805, + "col_height_to_diam_ratio": 1.2426624001729332, "number_columns": 16, "t_breakthru": 4320000.0, - "t_contact": 120.0, "ebct": 240.0, - "vel_bed": 0.006149999999999999, - "vel_inter": 0.012299999999999998, + "vel_bed": 0.00615, "service_flow_rate": 15, - "N_Re": 4.151249999999999, + "N_Re": 4.15125, "N_Sc": {"Cl_-": 999.9999999999998}, - "N_Sh": {"Cl_-": 24.083093218519274}, + "N_Sh": {"Cl_-": 24.083093218519267}, "N_Pe_particle": 0.09901383248136636, "N_Pe_bed": 216.5102470259211, "c_traps": { @@ -862,39 +963,54 @@ def test_solution(self, IX_inert): }, "tb_traps": { 0: 0, - 1: 3344557.5805670363, - 2: 3825939.149532493, - 3: 4034117.3864088715, - 4: 4188551.1118896315, - 5: 4320000.000000001, + 1: 3344557.580567035, + 2: 3825939.149532492, + 3: 4034117.3864088706, + 4: 4188551.1118896296, + 5: 4320000.0, }, "traps": { - 1: 0.0038710157182488838, + 1: 0.0038710157182488833, 2: 0.004457236749680154, - 3: 0.004818940668434693, - 4: 0.005719767610398498, - 5: 0.0066941563389539965, + 3: 0.004818940668434703, + 4: 0.005719767610398483, + 5: 0.006694156338954019, }, - "c_norm_avg": {"Cl_-": 0.025561117085716227}, - "c_breakthru": {"Cl_-": 2.5000000016147893e-07}, + "c_norm_avg": {"Cl_-": 0.02556111708571624}, "freundlich_n": 1.2, - "mass_transfer_coeff": 0.159346300525143, + "mass_transfer_coeff": 0.15934630052514293, "bv": 18000, "bv_50": 20000, - "bed_capacity_param": 311.93253706327357, - "kinetic_param": 1.59346300525143e-06, + "pressure_drop": 7.151350934188799, + "bed_vol": 7.499999999999998, + "t_rinse": 1200.0, + "t_waste": 1800.0, + "bw_flow": 0.1129177958446251, + "bed_expansion_frac": 0.46395000000000003, + "rinse_flow": 0.4999999999999999, + "t_cycle": 4321800.0, + "bw_pump_power": 6.959523064801349, + "rinse_pump_power": 30.816768130940375, + "bed_expansion_h": 0.6847902, + "main_pump_power": 30.81676813094038, + "col_vol_per": 16.060925813008126, + "col_vol_tot": 256.97481300813, + "t_contact": 120.0, + "vel_inter": 0.0123, + "c_breakthru": {"Cl_-": 2.500000001615544e-07}, } - for k, v in results_dict.items(): - var = getattr(ix, k) - if isinstance(v, dict): - for i, u in v.items(): - assert pytest.approx(u, rel=1e-3) == value(var[i]) + for v, r in results_dict.items(): + ixv = getattr(m.fs.ix, v) + if ixv.is_indexed(): + for i, s in r.items(): + assert pytest.approx(s, rel=1e-3) == value(ixv[i]) else: - assert pytest.approx(v, rel=1e-3) == value(var) + assert pytest.approx(r, rel=1e-3) == value(ixv) @pytest.mark.component def test_costing(self, IX_inert): + m = IX_inert ix = m.fs.ix @@ -903,23 +1019,62 @@ def test_costing(self, IX_inert): m.fs.costing.cost_process() m.fs.costing.add_LCOW(ix.process_flow.properties_out[0].flow_vol_phase["Liq"]) m.fs.costing.add_specific_energy_consumption( - ix.process_flow.properties_out[0].flow_vol_phase["Liq"] + ix.process_flow.properties_out[0].flow_vol_phase["Liq"], name="SEC" ) ix.initialize() results = solver.solve(m, tee=True) assert_optimal_termination(results) - assert pytest.approx(2.0 * 9701947.4187 / 1.65, rel=1e-3) == value( - m.fs.costing.aggregate_capital_cost - ) - assert pytest.approx(465913.6619, rel=1e-3) == value( - m.fs.costing.total_operating_cost - ) - assert pytest.approx(19403894.837 / 1.65, rel=1e-3) == value( - m.fs.costing.total_capital_cost - ) - assert pytest.approx(0.115619, rel=1e-3) == value(m.fs.costing.LCOW) - assert pytest.approx(0.04382530, rel=1e-3) == value( - m.fs.costing.specific_energy_consumption - ) \ No newline at end of file + sys_cost_results = { + "aggregate_capital_cost": 4485122.812315232, + "aggregate_fixed_operating_cost": 6419597.45408913, + "aggregate_variable_operating_cost": 0.0, + "aggregate_flow_electricity": 30.81676813094038, + "aggregate_flow_costs": {"electricity": 18909.78526050764}, + "total_capital_cost": 4485122.812315232, + "total_operating_cost": 6571169.945193044, + "capital_recovery_factor": 0.1, + "HCl_cost": 0.4594594594594595, + "NaOH_cost": 1.9666666666666666, + "MeOH_cost": 3.395, + "NaCl_cost": 0.09, + "aggregate_direct_capital_cost": 2242561.406157616, + "maintenance_labor_chemical_operating_cost": 134553.68436945695, + "total_fixed_operating_cost": 6554151.138458587, + "total_variable_operating_cost": 17018.806734456877, + "total_annualized_cost": 7019682.226424567, + "LCOW": 0.4943117934094987, + "SEC": 0.017120426756094133, + } + + for v, r in sys_cost_results.items(): + mv = getattr(m.fs.costing, v) + if mv.is_indexed(): + for i, s in r.items(): + assert pytest.approx(s, rel=1e-3) == value(mv[i]) + else: + assert pytest.approx(r, rel=1e-3) == value(mv) + + ix_cost_results = { + "capital_cost": 4485122.812315232, + "fixed_operating_cost": 6419597.45408913, + "capital_cost_vessel": 75000.32044033613, + "capital_cost_resin": 54924.68732109111, + "capital_cost_regen_tank": 0, + "capital_cost_backwash_tank": 33836.27421335332, + "operating_cost_hazardous": 0, + "flow_mass_regen_soln": 0, + "total_pumping_power": 30.81676813094038, + "flow_vol_resin": 876.5999999999999, + "single_use_resin_replacement_cost": 6419597.45408913, + "direct_capital_cost": 2242561.406157616, + } + + for v, r in ix_cost_results.items(): + mv = getattr(m.fs.ix.costing, v) + if mv.is_indexed(): + for i, s in r.items(): + assert pytest.approx(s, rel=1e-3) == value(mv[i]) + else: + assert pytest.approx(r, rel=1e-3) == value(mv) From 2e7a8e144a8ffc82a9b66dccd96ac9162bd529d7 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Thu, 14 Dec 2023 13:21:49 -0700 Subject: [PATCH 25/27] update IX_demo test file --- .../tests/test_ion_exchange_demo.py | 263 +++++++++++++++--- 1 file changed, 227 insertions(+), 36 deletions(-) diff --git a/watertap/examples/flowsheets/ion_exchange/tests/test_ion_exchange_demo.py b/watertap/examples/flowsheets/ion_exchange/tests/test_ion_exchange_demo.py index 20ed1ab203..62f89df49e 100644 --- a/watertap/examples/flowsheets/ion_exchange/tests/test_ion_exchange_demo.py +++ b/watertap/examples/flowsheets/ion_exchange/tests/test_ion_exchange_demo.py @@ -111,6 +111,7 @@ def test_build_model(self, ix_0D): @pytest.mark.component def test_specific_operating_conditions(self, ix_0D): + m = ix_0D ixf.set_operating_conditions(m) ixf.initialize_system(m) @@ -138,20 +139,97 @@ def test_specific_operating_conditions(self, ix_0D): ) ) == pytest.approx(0.000211438, rel=1e-3) - assert value(m.fs.ion_exchange.number_columns) == pytest.approx(4, rel=1e-3) - assert value(m.fs.ion_exchange.bed_depth) == pytest.approx(1.7, rel=1e-3) - assert value(m.fs.ion_exchange.t_breakthru) == pytest.approx( - 56759.75759, rel=1e-3 - ) - assert value(m.fs.ion_exchange.partition_ratio) == pytest.approx( - 235.99899, rel=1e-3 - ) + results_dict = { + "resin_diam": 0.0007, + "resin_bulk_dens": 0.7, + "resin_surf_per_vol": 4285.714285714285, + "c_norm": {"Ca_2+": 0.4730788932825343}, + "bed_vol_tot": 11.999999999999998, + "bed_depth": 1.7, + "bed_porosity": 0.5, + "col_height": 3.488715, + "col_diam": 1.4989640803696807, + "col_height_to_diam_ratio": 2.327417344877002, + "number_columns": 4, + "t_breakthru": 56759.757564984655, + "ebct": 240.0, + "vel_bed": 0.007083333333333333, + "service_flow_rate": 15, + "N_Re": 4.958333333333333, + "N_Sc": {"Ca_2+": 1086.9565217391305}, + "N_Sh": {"Ca_2+": 26.296358158587932}, + "N_Pe_particle": 0.1078279006415783, + "N_Pe_bed": 261.86775870097586, + "resin_max_capacity": 3, + "resin_eq_capacity": 1.685707070386448, + "resin_unused_capacity": 1.3142929296135517, + "langmuir": {"Ca_2+": 0.7}, + "mass_removed": {"Ca_2+": 7079.96969562308}, + "num_transfer_units": 35.54838294744621, + "dimensionless_time": 1, + "partition_ratio": 235.99898985410272, + "fluid_mass_transfer_coeff": {"Ca_2+": 3.4560927865572706e-05}, + "pressure_drop": 9.450141899999998, + "bed_vol": 2.9999999999999996, + "t_rinse": 1200.0, + "t_waste": 3600.0, + "regen_pump_power": 1.357425724718769, + "regen_tank_vol": 29.999999999999996, + "bw_flow": 0.009803921568627449, + "bed_expansion_frac": 0.46395000000000003, + "rinse_flow": 0.04999999999999999, + "t_cycle": 60359.757564984655, + "bw_pump_power": 0.7984857204228053, + "rinse_pump_power": 4.072277174156307, + "bed_expansion_h": 0.788715, + "main_pump_power": 4.072277174156307, + "col_vol_per": 6.15655588235294, + "col_vol_tot": 24.62622352941176, + "t_contact": 120.0, + "vel_inter": 0.014166666666666666, + "bv_calc": 236.49898985410272, + "lh": 0.0, + "separation_factor": {"Ca_2+": 1.4285714285714286}, + "rate_coeff": {"Ca_2+": 0.00021159751754432275}, + "HTU": {"Ca_2+": 0.04782214714276131}, + } + for v, r in results_dict.items(): + ixv = getattr(m.fs.ion_exchange, v) + if ixv.is_indexed(): + for i, s in r.items(): + assert pytest.approx(s, rel=1e-3) == value(ixv[i]) + else: + assert pytest.approx(r, rel=1e-3) == value(ixv) - assert value(m.fs.costing.specific_energy_consumption) == pytest.approx( - 0.057245, rel=1e-3 - ) + sys_cost_results = { + "aggregate_capital_cost": 810841.861208609, + "aggregate_fixed_operating_cost": 4099.257151281435, + "aggregate_variable_operating_cost": 0.0, + "aggregate_flow_electricity": 10.30046579345419, + "aggregate_flow_NaCl": 2352713.2269726195, + "aggregate_flow_costs": { + "electricity": 6320.5718201793625, + "NaCl": 214194.76894808252, + }, + "total_capital_cost": 810841.861208609, + "total_operating_cost": 226888.31967897541, + "aggregate_direct_capital_cost": 405420.9306043045, + "maintenance_labor_chemical_operating_cost": 24325.255836258268, + "total_fixed_operating_cost": 28424.512987539703, + "total_variable_operating_cost": 198463.8066914357, + "total_annualized_cost": 307972.5057998363, + "annual_water_production": 1419950.2910321492, + "LCOW": 0.21688963884501467, + "specific_energy_consumption": 0.05723052091619846, + } - assert value(m.fs.costing.LCOW) == pytest.approx(0.191008, rel=1e-3) + for v, r in sys_cost_results.items(): + mv = getattr(m.fs.costing, v) + if mv.is_indexed(): + for i, s in r.items(): + assert pytest.approx(s, rel=1e-3) == value(mv[i]) + else: + assert pytest.approx(r, rel=1e-3) == value(mv) @pytest.mark.component def test_optimization(self, ix_0D): @@ -190,34 +268,147 @@ def test_optimization(self, ix_0D): assert_optimal_termination(results) assert degrees_of_freedom(m) == 0 - assert value(m.fs.ion_exchange.number_columns) == 7 - assert value(m.fs.ion_exchange.bed_depth) == pytest.approx(1.38976, rel=1e-3) - assert value(m.fs.ion_exchange.t_breakthru) == pytest.approx( - 133134.2829, rel=1e-3 - ) - assert value(m.fs.ion_exchange.dimensionless_time) == pytest.approx( - 1.33210077, rel=1e-3 - ) - assert value(m.fs.costing.specific_energy_consumption) == pytest.approx( - 0.039173, rel=1e-3 - ) - assert value(m.fs.costing.LCOW) == pytest.approx(0.112747, rel=1e-3) + results_dict = { + "resin_diam": 0.0007, + "resin_bulk_dens": 0.7, + "resin_surf_per_vol": 4285.714285714285, + "c_norm": {"Ca_2+": 0.9897743909187179}, + "bed_vol_tot": 11.999999999999998, + "bed_depth": 1.6386683694597846, + "bed_porosity": 0.5, + "col_height": 3.3989285594706518, + "col_diam": 1.3655737026706347, + "col_height_to_diam_ratio": 2.489011430379571, + "number_columns": 5, + "t_breakthru": 133431.67855270393, + "ebct": 240.0, + "vel_bed": 0.006827784872749103, + "service_flow_rate": 15, + "N_Re": 4.779449410924372, + "N_Sc": {"Ca_2+": 1086.9565217391305}, + "N_Sh": {"Ca_2+": 25.969879651128718}, + "N_Pe_particle": 0.1059427840517767, + "N_Pe_bed": 248.00727028307858, + "resin_max_capacity": 3, + "resin_eq_capacity": 2.978460143483587, + "resin_unused_capacity": 0.02153985651641334, + "langmuir": {"Ca_2+": 0.7}, + "mass_removed": {"Ca_2+": 12509.532602631058}, + "num_transfer_units": 35.10703730797482, + "dimensionless_time": 1.332100914432498, + "partition_ratio": 416.9844200877022, + "fluid_mass_transfer_coeff": {"Ca_2+": 3.413184182719774e-05}, + "pressure_drop": 8.785890093905437, + "bed_vol": 2.3999999999999995, + "t_rinse": 1200.0, + "t_waste": 3600.0, + "regen_pump_power": 1.2620120792068785, + "regen_tank_vol": 29.999999999999996, + "bw_flow": 0.010170860057646149, + "bed_expansion_frac": 0.46395000000000003, + "rinse_flow": 0.04999999999999999, + "t_cycle": 137031.67855270393, + "bw_pump_power": 0.7701448949203327, + "rinse_pump_power": 3.7860362376206353, + "bed_expansion_h": 0.7602601900108671, + "main_pump_power": 3.7860362376206367, + "col_vol_per": 4.978083848301045, + "col_vol_tot": 24.890419241505228, + "t_contact": 120.0, + "vel_inter": 0.013655569745498206, + "bv_calc": 555.9653273029331, + "lh": 11.65907919299426, + "separation_factor": {"Ca_2+": 1.4285714285714286}, + "rate_coeff": {"Ca_2+": 0.00020897046016651682}, + "HTU": {"Ca_2+": 0.04667635024524126}, + } + for v, r in results_dict.items(): + ixv = getattr(m.fs.ion_exchange, v) + if ixv.is_indexed(): + for i, s in r.items(): + assert pytest.approx(s, rel=1e-3) == value(ixv[i]) + else: + assert pytest.approx(r, rel=1e-3) == value(ixv) + + sys_cost_results = { + "aggregate_capital_cost": 847088.1118126865, + "aggregate_fixed_operating_cost": 3935.286865230177, + "aggregate_variable_operating_cost": 0.0, + "aggregate_flow_electricity": 9.604229449368482, + "aggregate_flow_NaCl": 994870.9191908962, + "aggregate_flow_costs": { + "electricity": 5893.347274721491, + "NaCl": 90574.63707273173, + }, + "total_capital_cost": 847088.1118126865, + "total_operating_cost": 116169.11613231867, + "aggregate_direct_capital_cost": 423544.0559063433, + "maintenance_labor_chemical_operating_cost": 25412.643354380594, + "total_fixed_operating_cost": 29347.93021961077, + "total_variable_operating_cost": 86821.18591270791, + "total_annualized_cost": 200877.92731358734, + "annual_water_production": 1419985.4904372608, + "LCOW": 0.14146477458141515, + "specific_energy_consumption": 0.05336083243675618, + } + + for v, r in sys_cost_results.items(): + mv = getattr(m.fs.costing, v) + if mv.is_indexed(): + for i, s in r.items(): + assert pytest.approx(s, rel=1e-3) == value(mv[i]) + else: + assert pytest.approx(r, rel=1e-3) == value(mv) @pytest.mark.unit def test_main_fun(self): m = ixf.main() assert degrees_of_freedom(m) == 0 - assert value(m.fs.ion_exchange.number_columns) == 7 - assert value(m.fs.ion_exchange.bed_depth) == pytest.approx(1.38976, rel=1e-3) - assert value(m.fs.ion_exchange.t_breakthru) == pytest.approx( - 133134.2829, rel=1e-3 - ) - assert value(m.fs.ion_exchange.dimensionless_time) == pytest.approx( - 1.33210077, rel=1e-3 - ) - assert value(m.fs.costing.specific_energy_consumption) == pytest.approx( - 0.039173, rel=1e-3 - ) - assert value(m.fs.costing.LCOW) == pytest.approx(0.112747, rel=1e-3) + results_dict = { + "c_norm": {"Ca_2+": 0.9897743909187181}, + "bed_vol_tot": 11.999999999999998, + "bed_depth": 1.638668369459785, + "number_columns": 5, + "t_breakthru": 133431.67855270393, + "ebct": 239.99999999999994, + "resin_eq_capacity": 2.978460143483587, + "mass_removed": {"Ca_2+": 12509.53260263106}, + "num_transfer_units": 35.107037307974814, + "dimensionless_time": 1.3321009144324982, + "partition_ratio": 416.98442008770223, + "fluid_mass_transfer_coeff": {"Ca_2+": 3.413184182719774e-05}, + "bv_calc": 555.9653273029331, + "lh": 11.659079192994266, + "separation_factor": {"Ca_2+": 1.4285714285714286}, + "rate_coeff": {"Ca_2+": 0.00020897046016651682}, + "HTU": {"Ca_2+": 0.04667635024524128}, + } + + for v, r in results_dict.items(): + ixv = getattr(m.fs.ion_exchange, v) + if ixv.is_indexed(): + for i, s in r.items(): + assert pytest.approx(s, rel=1e-3) == value(ixv[i]) + else: + assert pytest.approx(r, rel=1e-3) == value(ixv) + + sys_cost_results = { + "aggregate_capital_cost": 847088.1118126865, + "aggregate_flow_electricity": 9.60422944936849, + "aggregate_flow_NaCl": 994870.9191908962, + "total_capital_cost": 847088.1118126865, + "total_operating_cost": 116169.11613231867, + "total_annualized_cost": 200877.92731358734, + "annual_water_production": 1419985.4904372608, + "LCOW": 0.14146477458141515, + "specific_energy_consumption": 0.05336083243675621, + } + for v, r in sys_cost_results.items(): + mv = getattr(m.fs.costing, v) + if mv.is_indexed(): + for i, s in r.items(): + assert pytest.approx(s, rel=1e-3) == value(mv[i]) + else: + assert pytest.approx(r, rel=1e-3) == value(mv) From e9abd9c4356c659736137196c5f1057331e5dc26 Mon Sep 17 00:00:00 2001 From: kurbansitterley Date: Thu, 14 Dec 2023 15:02:08 -0700 Subject: [PATCH 26/27] sig figs in test files --- .../tests/test_ion_exchange_demo.py | 270 ++++++++--------- .../unit_models/tests/test_ion_exchange_0D.py | 283 +++++++++--------- 2 files changed, 274 insertions(+), 279 deletions(-) diff --git a/watertap/examples/flowsheets/ion_exchange/tests/test_ion_exchange_demo.py b/watertap/examples/flowsheets/ion_exchange/tests/test_ion_exchange_demo.py index 62f89df49e..d3c2e96f57 100644 --- a/watertap/examples/flowsheets/ion_exchange/tests/test_ion_exchange_demo.py +++ b/watertap/examples/flowsheets/ion_exchange/tests/test_ion_exchange_demo.py @@ -142,56 +142,56 @@ def test_specific_operating_conditions(self, ix_0D): results_dict = { "resin_diam": 0.0007, "resin_bulk_dens": 0.7, - "resin_surf_per_vol": 4285.714285714285, - "c_norm": {"Ca_2+": 0.4730788932825343}, - "bed_vol_tot": 11.999999999999998, + "resin_surf_per_vol": 4285.714, + "c_norm": {"Ca_2+": 0.47307}, + "bed_vol_tot": 11.999, "bed_depth": 1.7, "bed_porosity": 0.5, - "col_height": 3.488715, - "col_diam": 1.4989640803696807, - "col_height_to_diam_ratio": 2.327417344877002, + "col_height": 3.488, + "col_diam": 1.498964, + "col_height_to_diam_ratio": 2.3274, "number_columns": 4, - "t_breakthru": 56759.757564984655, + "t_breakthru": 56759.7575, "ebct": 240.0, - "vel_bed": 0.007083333333333333, + "vel_bed": 0.00708, "service_flow_rate": 15, - "N_Re": 4.958333333333333, - "N_Sc": {"Ca_2+": 1086.9565217391305}, - "N_Sh": {"Ca_2+": 26.296358158587932}, - "N_Pe_particle": 0.1078279006415783, - "N_Pe_bed": 261.86775870097586, + "N_Re": 4.9583, + "N_Sc": {"Ca_2+": 1086.956}, + "N_Sh": {"Ca_2+": 26.296358}, + "N_Pe_particle": 0.1078, + "N_Pe_bed": 261.8677, "resin_max_capacity": 3, - "resin_eq_capacity": 1.685707070386448, - "resin_unused_capacity": 1.3142929296135517, + "resin_eq_capacity": 1.6857, + "resin_unused_capacity": 1.3142, "langmuir": {"Ca_2+": 0.7}, - "mass_removed": {"Ca_2+": 7079.96969562308}, - "num_transfer_units": 35.54838294744621, + "mass_removed": {"Ca_2+": 7079.9696}, + "num_transfer_units": 35.54838, "dimensionless_time": 1, - "partition_ratio": 235.99898985410272, - "fluid_mass_transfer_coeff": {"Ca_2+": 3.4560927865572706e-05}, - "pressure_drop": 9.450141899999998, - "bed_vol": 2.9999999999999996, + "partition_ratio": 235.998, + "fluid_mass_transfer_coeff": {"Ca_2+": 3.4560e-05}, + "pressure_drop": 9.4501, + "bed_vol": 3, "t_rinse": 1200.0, "t_waste": 3600.0, - "regen_pump_power": 1.357425724718769, - "regen_tank_vol": 29.999999999999996, - "bw_flow": 0.009803921568627449, - "bed_expansion_frac": 0.46395000000000003, - "rinse_flow": 0.04999999999999999, - "t_cycle": 60359.757564984655, - "bw_pump_power": 0.7984857204228053, - "rinse_pump_power": 4.072277174156307, + "regen_pump_power": 1.3574, + "regen_tank_vol": 29.9999, + "bw_flow": 0.00980392, + "bed_expansion_frac": 0.463950, + "rinse_flow": 0.04999, + "t_cycle": 60359.75756, + "bw_pump_power": 0.798485720, + "rinse_pump_power": 4.072277, "bed_expansion_h": 0.788715, - "main_pump_power": 4.072277174156307, - "col_vol_per": 6.15655588235294, - "col_vol_tot": 24.62622352941176, + "main_pump_power": 4.072277, + "col_vol_per": 6.15655, + "col_vol_tot": 24.6262, "t_contact": 120.0, - "vel_inter": 0.014166666666666666, - "bv_calc": 236.49898985410272, + "vel_inter": 0.01416, + "bv_calc": 236.4989, "lh": 0.0, - "separation_factor": {"Ca_2+": 1.4285714285714286}, - "rate_coeff": {"Ca_2+": 0.00021159751754432275}, - "HTU": {"Ca_2+": 0.04782214714276131}, + "separation_factor": {"Ca_2+": 1.428571}, + "rate_coeff": {"Ca_2+": 0.000211597}, + "HTU": {"Ca_2+": 0.047822}, } for v, r in results_dict.items(): ixv = getattr(m.fs.ion_exchange, v) @@ -202,25 +202,25 @@ def test_specific_operating_conditions(self, ix_0D): assert pytest.approx(r, rel=1e-3) == value(ixv) sys_cost_results = { - "aggregate_capital_cost": 810841.861208609, - "aggregate_fixed_operating_cost": 4099.257151281435, + "aggregate_capital_cost": 810841.861208, + "aggregate_fixed_operating_cost": 4099.25715, "aggregate_variable_operating_cost": 0.0, - "aggregate_flow_electricity": 10.30046579345419, - "aggregate_flow_NaCl": 2352713.2269726195, + "aggregate_flow_electricity": 10.300, + "aggregate_flow_NaCl": 2352713.226, "aggregate_flow_costs": { - "electricity": 6320.5718201793625, - "NaCl": 214194.76894808252, + "electricity": 6320.57182, + "NaCl": 214194.7689, }, - "total_capital_cost": 810841.861208609, - "total_operating_cost": 226888.31967897541, - "aggregate_direct_capital_cost": 405420.9306043045, - "maintenance_labor_chemical_operating_cost": 24325.255836258268, - "total_fixed_operating_cost": 28424.512987539703, - "total_variable_operating_cost": 198463.8066914357, - "total_annualized_cost": 307972.5057998363, - "annual_water_production": 1419950.2910321492, - "LCOW": 0.21688963884501467, - "specific_energy_consumption": 0.05723052091619846, + "total_capital_cost": 810841.8612, + "total_operating_cost": 226888.3196, + "aggregate_direct_capital_cost": 405420.9305, + "maintenance_labor_chemical_operating_cost": 24325.25583, + "total_fixed_operating_cost": 28424.5129, + "total_variable_operating_cost": 198463.8066, + "total_annualized_cost": 307972.50579, + "annual_water_production": 1419950.29103, + "LCOW": 0.21688, + "specific_energy_consumption": 0.057231, } for v, r in sys_cost_results.items(): @@ -271,56 +271,56 @@ def test_optimization(self, ix_0D): results_dict = { "resin_diam": 0.0007, "resin_bulk_dens": 0.7, - "resin_surf_per_vol": 4285.714285714285, - "c_norm": {"Ca_2+": 0.9897743909187179}, - "bed_vol_tot": 11.999999999999998, - "bed_depth": 1.6386683694597846, + "resin_surf_per_vol": 4285.71428, + "c_norm": {"Ca_2+": 0.989774}, + "bed_vol_tot": 12, + "bed_depth": 1.63866, "bed_porosity": 0.5, - "col_height": 3.3989285594706518, - "col_diam": 1.3655737026706347, - "col_height_to_diam_ratio": 2.489011430379571, + "col_height": 3.398, + "col_diam": 1.3655, + "col_height_to_diam_ratio": 2.4890, "number_columns": 5, - "t_breakthru": 133431.67855270393, + "t_breakthru": 133431.6785, "ebct": 240.0, - "vel_bed": 0.006827784872749103, + "vel_bed": 0.0068277, "service_flow_rate": 15, - "N_Re": 4.779449410924372, - "N_Sc": {"Ca_2+": 1086.9565217391305}, - "N_Sh": {"Ca_2+": 25.969879651128718}, - "N_Pe_particle": 0.1059427840517767, - "N_Pe_bed": 248.00727028307858, + "N_Re": 4.77944, + "N_Sc": {"Ca_2+": 1086.9565}, + "N_Sh": {"Ca_2+": 25.96987}, + "N_Pe_particle": 0.105942, + "N_Pe_bed": 248.007270, "resin_max_capacity": 3, - "resin_eq_capacity": 2.978460143483587, - "resin_unused_capacity": 0.02153985651641334, + "resin_eq_capacity": 2.97846, + "resin_unused_capacity": 0.0215398, "langmuir": {"Ca_2+": 0.7}, - "mass_removed": {"Ca_2+": 12509.532602631058}, - "num_transfer_units": 35.10703730797482, - "dimensionless_time": 1.332100914432498, - "partition_ratio": 416.9844200877022, - "fluid_mass_transfer_coeff": {"Ca_2+": 3.413184182719774e-05}, - "pressure_drop": 8.785890093905437, - "bed_vol": 2.3999999999999995, + "mass_removed": {"Ca_2+": 12509.53260}, + "num_transfer_units": 35.10703, + "dimensionless_time": 1.33210, + "partition_ratio": 416.98442, + "fluid_mass_transfer_coeff": {"Ca_2+": 3.41318e-05}, + "pressure_drop": 8.78589, + "bed_vol": 2.39999, "t_rinse": 1200.0, "t_waste": 3600.0, - "regen_pump_power": 1.2620120792068785, - "regen_tank_vol": 29.999999999999996, - "bw_flow": 0.010170860057646149, - "bed_expansion_frac": 0.46395000000000003, - "rinse_flow": 0.04999999999999999, - "t_cycle": 137031.67855270393, - "bw_pump_power": 0.7701448949203327, - "rinse_pump_power": 3.7860362376206353, - "bed_expansion_h": 0.7602601900108671, - "main_pump_power": 3.7860362376206367, - "col_vol_per": 4.978083848301045, - "col_vol_tot": 24.890419241505228, + "regen_pump_power": 1.262012, + "regen_tank_vol": 29.9999, + "bw_flow": 0.010170, + "bed_expansion_frac": 0.46395, + "rinse_flow": 0.05, + "t_cycle": 137031.6785, + "bw_pump_power": 0.77014, + "rinse_pump_power": 3.7860, + "bed_expansion_h": 0.7602, + "main_pump_power": 3.7860, + "col_vol_per": 4.9780, + "col_vol_tot": 24.8904, "t_contact": 120.0, - "vel_inter": 0.013655569745498206, - "bv_calc": 555.9653273029331, - "lh": 11.65907919299426, - "separation_factor": {"Ca_2+": 1.4285714285714286}, - "rate_coeff": {"Ca_2+": 0.00020897046016651682}, - "HTU": {"Ca_2+": 0.04667635024524126}, + "vel_inter": 0.01365, + "bv_calc": 555.96532, + "lh": 11.6591, + "separation_factor": {"Ca_2+": 1.428571}, + "rate_coeff": {"Ca_2+": 0.00020897}, + "HTU": {"Ca_2+": 0.046676}, } for v, r in results_dict.items(): @@ -332,25 +332,25 @@ def test_optimization(self, ix_0D): assert pytest.approx(r, rel=1e-3) == value(ixv) sys_cost_results = { - "aggregate_capital_cost": 847088.1118126865, - "aggregate_fixed_operating_cost": 3935.286865230177, + "aggregate_capital_cost": 847088.1118, + "aggregate_fixed_operating_cost": 3935.286, "aggregate_variable_operating_cost": 0.0, - "aggregate_flow_electricity": 9.604229449368482, - "aggregate_flow_NaCl": 994870.9191908962, + "aggregate_flow_electricity": 9.60422, + "aggregate_flow_NaCl": 994870.919, "aggregate_flow_costs": { - "electricity": 5893.347274721491, - "NaCl": 90574.63707273173, + "electricity": 5893.3472, + "NaCl": 90574.6370, }, - "total_capital_cost": 847088.1118126865, - "total_operating_cost": 116169.11613231867, - "aggregate_direct_capital_cost": 423544.0559063433, - "maintenance_labor_chemical_operating_cost": 25412.643354380594, - "total_fixed_operating_cost": 29347.93021961077, - "total_variable_operating_cost": 86821.18591270791, - "total_annualized_cost": 200877.92731358734, - "annual_water_production": 1419985.4904372608, - "LCOW": 0.14146477458141515, - "specific_energy_consumption": 0.05336083243675618, + "total_capital_cost": 847088.111, + "total_operating_cost": 116169.116137, + "aggregate_direct_capital_cost": 423544.0559, + "maintenance_labor_chemical_operating_cost": 25412.6433, + "total_fixed_operating_cost": 29347.93021, + "total_variable_operating_cost": 86821.18591, + "total_annualized_cost": 200877.9273, + "annual_water_production": 1419985.4904, + "LCOW": 0.14146, + "specific_energy_consumption": 0.05336, } for v, r in sys_cost_results.items(): @@ -367,23 +367,23 @@ def test_main_fun(self): assert degrees_of_freedom(m) == 0 results_dict = { - "c_norm": {"Ca_2+": 0.9897743909187181}, - "bed_vol_tot": 11.999999999999998, - "bed_depth": 1.638668369459785, + "c_norm": {"Ca_2+": 0.989774}, + "bed_vol_tot": 12, + "bed_depth": 1.63866, "number_columns": 5, - "t_breakthru": 133431.67855270393, - "ebct": 239.99999999999994, - "resin_eq_capacity": 2.978460143483587, - "mass_removed": {"Ca_2+": 12509.53260263106}, - "num_transfer_units": 35.107037307974814, - "dimensionless_time": 1.3321009144324982, - "partition_ratio": 416.98442008770223, - "fluid_mass_transfer_coeff": {"Ca_2+": 3.413184182719774e-05}, - "bv_calc": 555.9653273029331, - "lh": 11.659079192994266, - "separation_factor": {"Ca_2+": 1.4285714285714286}, - "rate_coeff": {"Ca_2+": 0.00020897046016651682}, - "HTU": {"Ca_2+": 0.04667635024524128}, + "t_breakthru": 133431.678, + "ebct": 240, + "resin_eq_capacity": 2.9784, + "mass_removed": {"Ca_2+": 12509.532}, + "num_transfer_units": 35.1070, + "dimensionless_time": 1.3321, + "partition_ratio": 416.984, + "fluid_mass_transfer_coeff": {"Ca_2+": 3.413e-05}, + "bv_calc": 555.96, + "lh": 11.6590, + "separation_factor": {"Ca_2+": 1.42857}, + "rate_coeff": {"Ca_2+": 0.00020897}, + "HTU": {"Ca_2+": 0.04667}, } for v, r in results_dict.items(): @@ -395,15 +395,15 @@ def test_main_fun(self): assert pytest.approx(r, rel=1e-3) == value(ixv) sys_cost_results = { - "aggregate_capital_cost": 847088.1118126865, - "aggregate_flow_electricity": 9.60422944936849, - "aggregate_flow_NaCl": 994870.9191908962, - "total_capital_cost": 847088.1118126865, - "total_operating_cost": 116169.11613231867, - "total_annualized_cost": 200877.92731358734, - "annual_water_production": 1419985.4904372608, - "LCOW": 0.14146477458141515, - "specific_energy_consumption": 0.05336083243675621, + "aggregate_capital_cost": 847088.11, + "aggregate_flow_electricity": 9.604, + "aggregate_flow_NaCl": 994870.919, + "total_capital_cost": 847088.1118, + "total_operating_cost": 116169.116, + "total_annualized_cost": 200877.927, + "annual_water_production": 1419985.4904, + "LCOW": 0.14145, + "specific_energy_consumption": 0.05336, } for v, r in sys_cost_results.items(): mv = getattr(m.fs.costing, v) diff --git a/watertap/unit_models/tests/test_ion_exchange_0D.py b/watertap/unit_models/tests/test_ion_exchange_0D.py index eae510e0ab..8984bf3d22 100644 --- a/watertap/unit_models/tests/test_ion_exchange_0D.py +++ b/watertap/unit_models/tests/test_ion_exchange_0D.py @@ -279,56 +279,56 @@ def test_solution(self, IX_lang): results_dict = { "resin_diam": 0.0007, "resin_bulk_dens": 0.7, - "resin_surf_per_vol": 4285.714285714285, - "c_norm": {"Ca_2+": 0.4919290557789296}, - "bed_vol_tot": 120.00000000000001, + "resin_surf_per_vol": 4285.714, + "c_norm": {"Ca_2+": 0.4919}, + "bed_vol_tot": 120.0, "bed_depth": 1.7, "bed_porosity": 0.5, - "col_height": 3.488715, - "col_diam": 3.351785579537064, - "col_height_to_diam_ratio": 1.0408526790314099, + "col_height": 3.488, + "col_diam": 3.351, + "col_height_to_diam_ratio": 1.0408, "number_columns": 8, - "t_breakthru": 52360.64416318655, + "t_breakthru": 52360.644, "ebct": 240.0, - "vel_bed": 0.007083333333333333, + "vel_bed": 0.007083, "service_flow_rate": 15, - "N_Re": 4.958333333333333, - "N_Sc": {"Ca_2+": 1086.9565217391307}, - "N_Sh": {"Ca_2+": 26.29635815858793}, - "N_Pe_particle": 0.10782790064157834, - "N_Pe_bed": 261.8677587009759, + "N_Re": 4.958, + "N_Sc": {"Ca_2+": 1086.9565}, + "N_Sh": {"Ca_2+": 26.296}, + "N_Pe_particle": 0.10782, + "N_Pe_bed": 261.8677, "resin_max_capacity": 3, - "resin_eq_capacity": 1.5547810762853225, - "resin_unused_capacity": 1.4452189237146778, + "resin_eq_capacity": 1.554, + "resin_unused_capacity": 1.445, "langmuir": {"Ca_2+": 0.9}, - "mass_removed": {"Ca_2+": 65300.80520398353}, - "num_transfer_units": 35.54838294744621, + "mass_removed": {"Ca_2+": 65300.8052}, + "num_transfer_units": 35.5483, "dimensionless_time": 1, - "partition_ratio": 217.66935067994396, - "fluid_mass_transfer_coeff": {"Ca_2+": 3.4560927865572706e-05}, - "pressure_drop": 9.450141899999998, - "bed_vol": 15.000000000000002, + "partition_ratio": 217.669, + "fluid_mass_transfer_coeff": {"Ca_2+": 3.45609e-05}, + "pressure_drop": 9.450, + "bed_vol": 15., "t_rinse": 1200.0, "t_waste": 3600.0, - "regen_pump_power": 13.574257247187694, - "regen_tank_vol": 300.00000000000006, - "bw_flow": 0.09803921568627452, - "bed_expansion_frac": 0.46395000000000003, + "regen_pump_power": 13.574, + "regen_tank_vol": 300.0, + "bw_flow": 0.09803, + "bed_expansion_frac": 0.46395, "rinse_flow": 0.5, - "t_cycle": 55960.64416318655, - "bw_pump_power": 7.984857204228056, - "rinse_pump_power": 40.72277174156308, - "bed_expansion_h": 0.788715, - "main_pump_power": 40.722771741563086, - "col_vol_per": 30.782779411764707, - "col_vol_tot": 246.26223529411766, + "t_cycle": 55960.6441, + "bw_pump_power": 7.984, + "rinse_pump_power": 40.722, + "bed_expansion_h": 0.788, + "main_pump_power": 40.722, + "col_vol_per": 30.7827, + "col_vol_tot": 246.2622, "t_contact": 120.0, - "vel_inter": 0.014166666666666666, - "bv_calc": 218.16935067994396, + "vel_inter": 0.01416, + "bv_calc": 218.169, "lh": 0.0, - "separation_factor": {"Ca_2+": 1.1111111111111112}, - "rate_coeff": {"Ca_2+": 0.00021159751754432275}, - "HTU": {"Ca_2+": 0.04782214714276131}, + "separation_factor": {"Ca_2+": 1.11111}, + "rate_coeff": {"Ca_2+": 0.00021159}, + "HTU": {"Ca_2+": 0.04782}, } for v, r in results_dict.items(): @@ -356,19 +356,19 @@ def test_costing(self, IX_lang): assert_optimal_termination(results) sys_cost_results = { - "aggregate_capital_cost": 3993072.4698084667, - "aggregate_fixed_operating_cost": 36893.31436153293, + "aggregate_capital_cost": 3993072.469, + "aggregate_fixed_operating_cost": 36893.314, "aggregate_variable_operating_cost": 0.0, - "aggregate_flow_electricity": 103.00465793454192, - "aggregate_flow_NaCl": 22838957.969693653, + "aggregate_flow_electricity": 103.00, + "aggregate_flow_NaCl": 22838957.969, "aggregate_flow_costs": { - "electricity": 63205.71820179364, - "NaCl": 2079295.2023431766, + "electricity": 63205.718, + "NaCl": 2079295.202, }, - "total_capital_cost": 3993072.4698084667, - "total_operating_cost": 2084936.31694626, - "LCOW": 0.17495285, - "SEC": 0.0572305198, + "total_capital_cost": 3993072.4698, + "total_operating_cost": 2084936.317, + "LCOW": 0.17495, + "SEC": 0.0572, } for v, r in sys_cost_results.items(): @@ -380,18 +380,18 @@ def test_costing(self, IX_lang): assert pytest.approx(r, rel=1e-3) == value(mv) ix_cost_results = { - "capital_cost": 3993072.4698084667, - "fixed_operating_cost": 36893.31436153293, - "capital_cost_vessel": 101131.8811552528, - "capital_cost_resin": 81985.14302562873, - "capital_cost_regen_tank": 215778.261765144, - "capital_cost_backwash_tank": 132704.75551115556, + "capital_cost": 3993072.4698, + "fixed_operating_cost": 36893.314, + "capital_cost_vessel": 101131.881, + "capital_cost_resin": 81985.1430, + "capital_cost_regen_tank": 215778.261, + "capital_cost_backwash_tank": 132704.7555, "operating_cost_hazardous": 0, - "flow_mass_regen_soln": 22838957.969693653, - "total_pumping_power": 103.00465793454192, - "backwash_tank_vol": 174042.7639065449, - "regeneration_tank_vol": 79251.61570744455, - "direct_capital_cost": 1996536.2349042334, + "flow_mass_regen_soln": 22838957.969, + "total_pumping_power": 103.00465, + "backwash_tank_vol": 174042.7639, + "regeneration_tank_vol": 79251.61570, + "direct_capital_cost": 1996536.234, } for v, r in ix_cost_results.items(): @@ -646,24 +646,24 @@ def test_solution(self, IX_fr): assert_optimal_termination(results) sys_cost_results = { - "aggregate_capital_cost": 5116213.693319044, - "aggregate_fixed_operating_cost": 323306.0679291797, + "aggregate_capital_cost": 5116213.693, + "aggregate_fixed_operating_cost": 323306.067, "aggregate_variable_operating_cost": 0.0, - "aggregate_flow_electricity": 78.86531537032893, - "aggregate_flow_NaOH": 279183.59700249793, + "aggregate_flow_electricity": 78.865, + "aggregate_flow_NaOH": 279183.597, "aggregate_flow_costs": { - "electricity": 48393.334817541254, - "NaOH": 555415.5212892868, + "electricity": 48393.3348, + "NaOH": 555415.521, }, - "total_capital_cost": 5116213.693319044, - "total_operating_cost": 1020220.4492248963, - "aggregate_direct_capital_cost": 2558106.8466595225, - "maintenance_labor_chemical_operating_cost": 153486.4107995713, - "total_fixed_operating_cost": 476792.478728751, - "total_variable_operating_cost": 543427.9704961453, - "total_annualized_cost": 1531841.8185568007, - "LCOW": 0.10786919580206682, - "SEC": 0.04381406413732131, + "total_capital_cost": 5116213.693, + "total_operating_cost": 1020220.449, + "aggregate_direct_capital_cost": 2558106.846, + "maintenance_labor_chemical_operating_cost": 153486.410, + "total_fixed_operating_cost": 476792.478, + "total_variable_operating_cost": 543427.970, + "total_annualized_cost": 1531841.818, + "LCOW": 0.1078691, + "SEC": 0.043814, } for v, r in sys_cost_results.items(): @@ -675,18 +675,18 @@ def test_solution(self, IX_fr): assert pytest.approx(r, rel=1e-3) == value(mv) ix_cost_results = { - "capital_cost": 5116213.693319045, - "fixed_operating_cost": 323306.0679291797, - "capital_cost_vessel": 75000.3204403362, - "capital_cost_resin": 54924.687321091136, - "capital_cost_regen_tank": 215778.261765144, - "capital_cost_backwash_tank": 133603.4529501137, - "operating_cost_hazardous": 276620.08370625216, - "flow_mass_regen_soln": 279183.597002498, - "total_pumping_power": 78.86531537032894, - "backwash_tank_vol": 176401.06694050893, - "regeneration_tank_vol": 79251.61570744455, - "direct_capital_cost": 2558106.8466595225, + "capital_cost": 5116213.6933, + "fixed_operating_cost": 323306.0679, + "capital_cost_vessel": 75000.3204, + "capital_cost_resin": 54924.687, + "capital_cost_regen_tank": 215778.261, + "capital_cost_backwash_tank": 133603.4529, + "operating_cost_hazardous": 276620.0837, + "flow_mass_regen_soln": 279183.597, + "total_pumping_power": 78.865, + "backwash_tank_vol": 176401.0669, + "regeneration_tank_vol": 79251.615, + "direct_capital_cost": 2558106.846, } for v, r in ix_cost_results.items(): @@ -935,69 +935,69 @@ def test_solution(self, IX_inert): results_dict = { "resin_diam": 0.000675, "resin_bulk_dens": 0.72, - "resin_surf_per_vol": 4444.444444444444, + "resin_surf_per_vol": 4444.44, "c_norm": {"Cl_-": 0.25}, - "bed_vol_tot": 119.99999999999997, + "bed_vol_tot": 120, "bed_depth": 1.476, "bed_porosity": 0.5, "col_height": 3.1607902, - "col_diam": 2.5435630784033805, - "col_height_to_diam_ratio": 1.2426624001729332, + "col_diam": 2.5435, + "col_height_to_diam_ratio": 1.2426, "number_columns": 16, "t_breakthru": 4320000.0, "ebct": 240.0, "vel_bed": 0.00615, "service_flow_rate": 15, "N_Re": 4.15125, - "N_Sc": {"Cl_-": 999.9999999999998}, - "N_Sh": {"Cl_-": 24.083093218519267}, - "N_Pe_particle": 0.09901383248136636, - "N_Pe_bed": 216.5102470259211, + "N_Sc": {"Cl_-": 1000}, + "N_Sh": {"Cl_-": 24.0830}, + "N_Pe_particle": 0.0990, + "N_Pe_bed": 216.5102, "c_traps": { 0: 0, 1: 0.01, - 2: 0.06999999999999999, + 2: 0.07, 3: 0.13, 4: 0.19, 5: 0.25, }, "tb_traps": { 0: 0, - 1: 3344557.580567035, - 2: 3825939.149532492, - 3: 4034117.3864088706, - 4: 4188551.1118896296, + 1: 3344557.580, + 2: 3825939.149, + 3: 4034117.386, + 4: 4188551.111, 5: 4320000.0, }, "traps": { - 1: 0.0038710157182488833, - 2: 0.004457236749680154, - 3: 0.004818940668434703, - 4: 0.005719767610398483, - 5: 0.006694156338954019, + 1: 0.00387, + 2: 0.00445724, + 3: 0.00481894, + 4: 0.00571976, + 5: 0.00669, }, - "c_norm_avg": {"Cl_-": 0.02556111708571624}, + "c_norm_avg": {"Cl_-": 0.02556}, "freundlich_n": 1.2, - "mass_transfer_coeff": 0.15934630052514293, + "mass_transfer_coeff": 0.1593, "bv": 18000, "bv_50": 20000, - "pressure_drop": 7.151350934188799, - "bed_vol": 7.499999999999998, + "pressure_drop": 7.1513, + "bed_vol": 7.5, "t_rinse": 1200.0, "t_waste": 1800.0, - "bw_flow": 0.1129177958446251, - "bed_expansion_frac": 0.46395000000000003, - "rinse_flow": 0.4999999999999999, + "bw_flow": 0.1129, + "bed_expansion_frac": 0.4639, + "rinse_flow": 0.5, "t_cycle": 4321800.0, - "bw_pump_power": 6.959523064801349, - "rinse_pump_power": 30.816768130940375, - "bed_expansion_h": 0.6847902, - "main_pump_power": 30.81676813094038, - "col_vol_per": 16.060925813008126, - "col_vol_tot": 256.97481300813, + "bw_pump_power": 6.9595, + "rinse_pump_power": 30.816, + "bed_expansion_h": 0.68479, + "main_pump_power": 30.816, + "col_vol_per": 16.060, + "col_vol_tot": 256.97, "t_contact": 120.0, "vel_inter": 0.0123, - "c_breakthru": {"Cl_-": 2.500000001615544e-07}, + "c_breakthru": {"Cl_-": 2.50e-07}, } for v, r in results_dict.items(): @@ -1027,25 +1027,20 @@ def test_costing(self, IX_inert): assert_optimal_termination(results) sys_cost_results = { - "aggregate_capital_cost": 4485122.812315232, - "aggregate_fixed_operating_cost": 6419597.45408913, + "aggregate_capital_cost": 4485122.81, + "aggregate_fixed_operating_cost": 6419597.45, "aggregate_variable_operating_cost": 0.0, - "aggregate_flow_electricity": 30.81676813094038, - "aggregate_flow_costs": {"electricity": 18909.78526050764}, - "total_capital_cost": 4485122.812315232, - "total_operating_cost": 6571169.945193044, - "capital_recovery_factor": 0.1, - "HCl_cost": 0.4594594594594595, - "NaOH_cost": 1.9666666666666666, - "MeOH_cost": 3.395, - "NaCl_cost": 0.09, - "aggregate_direct_capital_cost": 2242561.406157616, - "maintenance_labor_chemical_operating_cost": 134553.68436945695, - "total_fixed_operating_cost": 6554151.138458587, - "total_variable_operating_cost": 17018.806734456877, - "total_annualized_cost": 7019682.226424567, - "LCOW": 0.4943117934094987, - "SEC": 0.017120426756094133, + "aggregate_flow_electricity": 30.82, + "aggregate_flow_costs": {"electricity": 18909.78}, + "total_capital_cost": 4485122.81, + "total_operating_cost": 6571169.94, + "aggregate_direct_capital_cost": 2242561.40, + "maintenance_labor_chemical_operating_cost": 134553.68, + "total_fixed_operating_cost": 6554151.13, + "total_variable_operating_cost": 17018.80, + "total_annualized_cost": 7019682.22, + "LCOW": 0.49431179, + "SEC": 0.017120, } for v, r in sys_cost_results.items(): @@ -1057,18 +1052,18 @@ def test_costing(self, IX_inert): assert pytest.approx(r, rel=1e-3) == value(mv) ix_cost_results = { - "capital_cost": 4485122.812315232, - "fixed_operating_cost": 6419597.45408913, - "capital_cost_vessel": 75000.32044033613, - "capital_cost_resin": 54924.68732109111, + "capital_cost": 4485122.81, + "fixed_operating_cost": 6419597.45, + "capital_cost_vessel": 75000.32, + "capital_cost_resin": 54924.68, "capital_cost_regen_tank": 0, - "capital_cost_backwash_tank": 33836.27421335332, + "capital_cost_backwash_tank": 33836.27, "operating_cost_hazardous": 0, "flow_mass_regen_soln": 0, - "total_pumping_power": 30.81676813094038, - "flow_vol_resin": 876.5999999999999, - "single_use_resin_replacement_cost": 6419597.45408913, - "direct_capital_cost": 2242561.406157616, + "total_pumping_power": 30.81, + "flow_vol_resin": 876.599, + "single_use_resin_replacement_cost": 6419597.454, + "direct_capital_cost": 2242561.40, } for v, r in ix_cost_results.items(): From 0f7558d1f42f1f45eeb20b9adbbaff4933fab2bf Mon Sep 17 00:00:00 2001 From: Ludovico Bianchi Date: Thu, 14 Dec 2023 17:32:11 -0600 Subject: [PATCH 27/27] Run Black --- watertap/unit_models/tests/test_ion_exchange_0D.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/watertap/unit_models/tests/test_ion_exchange_0D.py b/watertap/unit_models/tests/test_ion_exchange_0D.py index 8984bf3d22..518befae5b 100644 --- a/watertap/unit_models/tests/test_ion_exchange_0D.py +++ b/watertap/unit_models/tests/test_ion_exchange_0D.py @@ -307,7 +307,7 @@ def test_solution(self, IX_lang): "partition_ratio": 217.669, "fluid_mass_transfer_coeff": {"Ca_2+": 3.45609e-05}, "pressure_drop": 9.450, - "bed_vol": 15., + "bed_vol": 15.0, "t_rinse": 1200.0, "t_waste": 3600.0, "regen_pump_power": 13.574,