diff --git a/watertap/costing/unit_models/gac.py b/watertap/costing/unit_models/gac.py index bae1cee31e..60a46223ae 100644 --- a/watertap/costing/unit_models/gac.py +++ b/watertap/costing/unit_models/gac.py @@ -26,7 +26,89 @@ class ContactorType(StrEnum): gravity = "gravity" -def build_gac_cost_param_block(blk): +def cost_gac(blk, contactor_type=ContactorType.pressure): + """ + 3 equation capital cost estimation for GAC systems with: (i), contactor/pressure vessel cost by polynomial as a + function of individual contactor volume; (ii), initial charge of GAC adsorbent cost by exponential as a function of + required mass of GAC adsorbent; and (iii), other process costs (vessels, pipes, instrumentation, and controls) + calculated by power law as a function of total contactor(s) volume. Operating costs calculated as the required + makeup and regeneration of GAC adsorbent. Energy consumption is estimated from that required for booster, backwash, + and residual pumps as a function of total contactor(s) volume. + + Args: + contactor_type: ContactorType Enum indicating whether to cost based on steel pressure vessels or concrete, + default = ContactorType.pressure + """ + + # costing data parameters based on contactor type + if contactor_type == ContactorType.pressure: + cost_gac_pressure(blk) + elif contactor_type == ContactorType.gravity: + cost_gac_gravity(blk) + else: + raise ConfigurationError( + f"{blk.unit_model.name} received invalid argument for contactor_type:" + f" {contactor_type}. Argument must be a member of the ContactorType Enum." + ) + + +def build_gac_cost_param_block_pressure(blk): + _build_gac_cost_param_block(blk, ContactorType.pressure) + + +def build_gac_cost_param_block_gravity(blk): + _build_gac_cost_param_block(blk, ContactorType.gravity) + + +@register_costing_parameter_block( + build_rule=build_gac_cost_param_block_pressure, + parameter_block_name="gac_pressure", +) +def cost_gac_pressure(blk): + """ + 3 equation capital cost estimation for GAC systems with: (i), contactor/pressure vessel cost by polynomial as a + function of individual contactor volume; (ii), initial charge of GAC adsorbent cost by exponential as a function of + required mass of GAC adsorbent; and (iii), other process costs (vessels, pipes, instrumentation, and controls) + calculated by power law as a function of total contactor(s) volume. Operating costs calculated as the required + makeup and regeneration of GAC adsorbent. Energy consumption is estimated from that required for booster, backwash, + and residual pumps as a function of total contactor(s) volume. + + This function costs assuming steel pressure-fed vessels. + """ + parameter_blk = blk.costing_package.gac_pressure + _cost_gac(blk, parameter_blk) + + +@register_costing_parameter_block( + build_rule=build_gac_cost_param_block_gravity, + parameter_block_name="gac_gravity", +) +def cost_gac_gravity(blk): + """ + 3 equation capital cost estimation for GAC systems with: (i), contactor/pressure vessel cost by polynomial as a + function of individual contactor volume; (ii), initial charge of GAC adsorbent cost by exponential as a function of + required mass of GAC adsorbent; and (iii), other process costs (vessels, pipes, instrumentation, and controls) + calculated by power law as a function of total contactor(s) volume. Operating costs calculated as the required + makeup and regeneration of GAC adsorbent. Energy consumption is estimated from that required for booster, backwash, + and residual pumps as a function of total contactor(s) volume. + + This function costs assuming concrete gravity-fed basins. + """ + parameter_blk = blk.costing_package.gac_gravity + _cost_gac(blk, parameter_blk) + + +def _build_gac_cost_param_block(blk, contactor_type): + + adsorbent_unit_cost_coeff_data = {0: 4.58342, 1: -1.25311e-5} + if contactor_type == ContactorType.pressure: + contactor_cost_coeff_data = {0: 10010.9, 1: 2204.95, 2: -15.9378, 3: 0.110592} + other_cost_param_data = {0: 16660.7, 1: 0.552207} + energy_consumption_coeff_data = {0: 8.09926e-4, 1: 8.70577e-4, 2: 0} + elif contactor_type == ContactorType.gravity: + contactor_cost_coeff_data = {0: 75131.3, 1: 735.550, 2: -1.01827, 3: 0.000000} + other_cost_param_data = {0: 38846.9, 1: 0.490571} + energy_consumption_coeff_data = {0: 0.123782, 1: 0.132403, 2: -1.41512e-5} # --------------------------------------------------------------------- # design options @@ -60,28 +142,24 @@ def build_gac_cost_param_block(blk): # --------------------------------------------------------------------- # correlation parameter data - # dummy data is used to initialize, fixed in cost_gac based on the ContactorType # USD_2020 embedded in equation - contactor_cost_coeff_dummy = {0: 10000, 1: 1000, 2: -10, 3: 0.1} blk.contactor_cost_coeff = pyo.Var( - contactor_cost_coeff_dummy.keys(), - initialize=contactor_cost_coeff_dummy, + contactor_cost_coeff_data, + initialize=contactor_cost_coeff_data, units=pyo.units.dimensionless, doc="contactor polynomial cost coefficients", ) # USD_2020 * kg**-1 embedded in equation adsorbent_unit_cost_constraint - adsorbent_unit_cost_coeff_dummy = {0: 1, 1: -1e-5} blk.adsorbent_unit_cost_coeff = pyo.Var( - adsorbent_unit_cost_coeff_dummy.keys(), - initialize=adsorbent_unit_cost_coeff_dummy, + adsorbent_unit_cost_coeff_data, + initialize=adsorbent_unit_cost_coeff_data, units=pyo.units.dimensionless, doc="GAC adsorbent cost exponential function parameters", ) # USD_2020 embedded in equation other_process_cost_constraint - other_cost_param_dummy = {0: 10000, 1: 0.1} blk.other_cost_param = pyo.Var( - other_cost_param_dummy.keys(), - initialize=other_cost_param_dummy, + other_cost_param_data, + initialize=other_cost_param_data, units=pyo.units.dimensionless, doc="other process cost power law parameters", ) @@ -96,20 +174,15 @@ def build_gac_cost_param_block(blk): doc="unit cost to makeup spent GAC adsorbent with fresh adsorbent", ) # kW embedded in equation energy_consumption_constraint - energy_consumption_coeff_dummy = {0: 1e-3, 1: 1e-3, 2: 0} blk.energy_consumption_coeff = pyo.Var( - energy_consumption_coeff_dummy.keys(), - initialize=energy_consumption_coeff_dummy, + energy_consumption_coeff_data, + initialize=energy_consumption_coeff_data, units=pyo.units.dimensionless, doc="energy consumption polynomial coefficients", ) -@register_costing_parameter_block( - build_rule=build_gac_cost_param_block, - parameter_block_name="gac", -) -def cost_gac(blk, contactor_type=ContactorType.pressure): +def _cost_gac(blk, parameter_blk): """ 3 equation capital cost estimation for GAC systems with: (i), contactor/pressure vessel cost by polynomial as a function of individual contactor volume; (ii), initial charge of GAC adsorbent cost by exponential as a function of @@ -119,44 +192,9 @@ def cost_gac(blk, contactor_type=ContactorType.pressure): and residual pumps as a function of total contactor(s) volume. Args: - contactor_type: ContactorType Enum indicating whether to cost based on steel pressure vessels or concrete, - default = ContactorType.pressure + parameter_blk: the block containing the global-level parameters utilized to cost GAC """ - # --------------------------------------------------------------------- - # with ContactorType not assigned in build_gac_cost_param_block and blk.costing_package.gac variables - # fixed when register_costing_parameter_block, refix parameters based on contactor type here - - # costing data parameters based on contactor type - adsorbent_unit_cost_coeff_data = {0: 4.58342, 1: -1.25311e-5} - if contactor_type == ContactorType.pressure: - contactor_cost_coeff_data = {0: 10010.9, 1: 2204.95, 2: -15.9378, 3: 0.110592} - other_cost_param_data = {0: 16660.7, 1: 0.552207} - energy_consumption_coeff_data = {0: 8.09926e-4, 1: 8.70577e-4, 2: 0} - elif contactor_type == ContactorType.gravity: - contactor_cost_coeff_data = {0: 75131.3, 1: 735.550, 2: -1.01827, 3: 0.000000} - other_cost_param_data = {0: 38846.9, 1: 0.490571} - energy_consumption_coeff_data = {0: 0.123782, 1: 0.132403, 2: -1.41512e-5} - else: - raise ConfigurationError( - f"{blk.unit_model.name} received invalid argument for contactor_type:" - f" {contactor_type}. Argument must be a member of the ContactorType Enum." - ) - - # iterable matching coeff_data to vars - gac_cost = blk.costing_package.gac - cost_params = ( - (adsorbent_unit_cost_coeff_data, gac_cost.adsorbent_unit_cost_coeff), - (contactor_cost_coeff_data, gac_cost.contactor_cost_coeff), - (other_cost_param_data, gac_cost.other_cost_param), - (energy_consumption_coeff_data, gac_cost.energy_consumption_coeff), - ) - - # refix variables to appropriate costing parameters - for indexed_data, indexed_var in cost_params: - for index, var in indexed_var.items(): - var.fix(indexed_data[index]) - # --------------------------------------------------------------------- make_capital_cost_var(blk) @@ -200,12 +238,9 @@ def cost_gac(blk, contactor_type=ContactorType.pressure): # intermediate variables to shorten constraint code num_contactors = ( - blk.costing_package.gac.num_contactors_op - + blk.costing_package.gac.num_contactors_redundant - ) - unit_contactor_volume = ( - blk.unit_model.bed_volume / blk.costing_package.gac.num_contactors_op + parameter_blk.num_contactors_op + parameter_blk.num_contactors_redundant ) + unit_contactor_volume = blk.unit_model.bed_volume / parameter_blk.num_contactors_op total_bed_volume = num_contactors * unit_contactor_volume blk.contactor_cost_constraint = pyo.Constraint( @@ -213,16 +248,16 @@ def cost_gac(blk, contactor_type=ContactorType.pressure): == num_contactors * pyo.units.convert( ( - blk.costing_package.gac.contactor_cost_coeff[3] + parameter_blk.contactor_cost_coeff[3] * (pyo.units.m**3) ** -3 * unit_contactor_volume**3 - + blk.costing_package.gac.contactor_cost_coeff[2] + + parameter_blk.contactor_cost_coeff[2] * (pyo.units.m**3) ** -2 * unit_contactor_volume**2 - + blk.costing_package.gac.contactor_cost_coeff[1] + + parameter_blk.contactor_cost_coeff[1] * (pyo.units.m**3) ** -1 * unit_contactor_volume**1 - + blk.costing_package.gac.contactor_cost_coeff[0] + + parameter_blk.contactor_cost_coeff[0] ) * pyo.units.USD_2020, to_units=blk.costing_package.base_currency, @@ -232,7 +267,7 @@ def cost_gac(blk, contactor_type=ContactorType.pressure): blk.bed_mass_gac_ref_constraint = pyo.Constraint( expr=blk.bed_mass_gac_ref == smooth_min( - blk.costing_package.gac.bed_mass_max_ref / pyo.units.kg, + parameter_blk.bed_mass_max_ref / pyo.units.kg, pyo.units.convert(blk.unit_model.bed_mass_gac, to_units=pyo.units.kg) / pyo.units.kg, ) @@ -241,11 +276,11 @@ def cost_gac(blk, contactor_type=ContactorType.pressure): blk.adsorbent_unit_cost_constraint = pyo.Constraint( expr=blk.adsorbent_unit_cost == pyo.units.convert( - blk.costing_package.gac.adsorbent_unit_cost_coeff[0] + parameter_blk.adsorbent_unit_cost_coeff[0] * pyo.exp( blk.bed_mass_gac_ref * pyo.units.kg**-1 - * blk.costing_package.gac.adsorbent_unit_cost_coeff[1] + * parameter_blk.adsorbent_unit_cost_coeff[1] ) * pyo.units.USD_2020 * pyo.units.kg**-1, @@ -260,9 +295,9 @@ def cost_gac(blk, contactor_type=ContactorType.pressure): expr=blk.other_process_cost == pyo.units.convert( ( - blk.costing_package.gac.other_cost_param[0] + parameter_blk.other_cost_param[0] * (total_bed_volume * pyo.units.m**-3) - ** blk.costing_package.gac.other_cost_param[1] + ** parameter_blk.other_cost_param[1] ) * pyo.units.USD_2020, to_units=blk.costing_package.base_currency, @@ -292,8 +327,8 @@ def cost_gac(blk, contactor_type=ContactorType.pressure): expr=blk.gac_regen_cost == pyo.units.convert( ( - blk.costing_package.gac.regen_unit_cost - * (blk.costing_package.gac.regen_frac * blk.unit_model.gac_usage_rate) + parameter_blk.regen_unit_cost + * (parameter_blk.regen_frac * blk.unit_model.gac_usage_rate) ), to_units=blk.costing_package.base_currency / blk.costing_package.base_period, @@ -303,11 +338,8 @@ def cost_gac(blk, contactor_type=ContactorType.pressure): expr=blk.gac_makeup_cost == pyo.units.convert( ( - blk.costing_package.gac.makeup_unit_cost - * ( - (1 - blk.costing_package.gac.regen_frac) - * blk.unit_model.gac_usage_rate - ) + parameter_blk.makeup_unit_cost + * ((1 - parameter_blk.regen_frac) * blk.unit_model.gac_usage_rate) ), to_units=blk.costing_package.base_currency / blk.costing_package.base_period, @@ -321,13 +353,13 @@ def cost_gac(blk, contactor_type=ContactorType.pressure): expr=pyo.units.convert(blk.energy_consumption, to_units=pyo.units.kW) == pyo.units.kW * ( - blk.costing_package.gac.energy_consumption_coeff[2] + parameter_blk.energy_consumption_coeff[2] * total_bed_volume**2 * (pyo.units.m**3) ** -2 - + blk.costing_package.gac.energy_consumption_coeff[1] + + parameter_blk.energy_consumption_coeff[1] * total_bed_volume * (pyo.units.m**3) ** -1 - + blk.costing_package.gac.energy_consumption_coeff[0] + + parameter_blk.energy_consumption_coeff[0] ) ) diff --git a/watertap/unit_models/tests/test_gac.py b/watertap/unit_models/tests/test_gac.py index 80ecbed15e..484fb18649 100644 --- a/watertap/unit_models/tests/test_gac.py +++ b/watertap/unit_models/tests/test_gac.py @@ -399,8 +399,8 @@ def test_robust_costing_pressure(self, gac_frame_robust): cost = mr.fs.unit.costing # Check for known cost solution of default twin alternating contactors - assert pyo.value(mr.fs.costing.gac.num_contactors_op) == 1 - assert pyo.value(mr.fs.costing.gac.num_contactors_redundant) == 1 + assert pyo.value(mr.fs.costing.gac_pressure.num_contactors_op) == 1 + assert pyo.value(mr.fs.costing.gac_pressure.num_contactors_redundant) == 1 assert pytest.approx(56900, rel=1e-3) == pyo.value(cost.contactor_cost) assert pytest.approx(4.359, rel=1e-3) == pyo.value(cost.adsorbent_unit_cost) assert pytest.approx(17450, rel=1e-3) == pyo.value(cost.adsorbent_cost) @@ -437,8 +437,8 @@ def test_robust_costing_gravity(self, gac_frame_robust): cost = mr_grav.fs.unit.costing # Check for known cost solution of default twin alternating contactors - assert pyo.value(mr_grav.fs.costing.gac.num_contactors_op) == 1 - assert pyo.value(mr_grav.fs.costing.gac.num_contactors_redundant) == 1 + assert pyo.value(mr_grav.fs.costing.gac_gravity.num_contactors_op) == 1 + assert pyo.value(mr_grav.fs.costing.gac_gravity.num_contactors_redundant) == 1 assert pytest.approx(163200, rel=1e-3) == pyo.value(cost.contactor_cost) assert pytest.approx(4.359, rel=1e-3) == pyo.value(cost.adsorbent_unit_cost) assert pytest.approx(17450, rel=1e-3) == pyo.value(cost.adsorbent_cost) @@ -461,15 +461,15 @@ def test_robust_costing_modular_contactors(self, gac_frame_robust): ) mr.fs.costing.cost_process() - mr.fs.costing.gac.num_contactors_op.fix(4) - mr.fs.costing.gac.num_contactors_redundant.fix(2) + mr.fs.costing.gac_pressure.num_contactors_op.fix(4) + mr.fs.costing.gac_pressure.num_contactors_redundant.fix(2) results = solver.solve(mr) cost = mr.fs.unit.costing # Check for known cost solution when changing volume scale of vessels in parallel - assert pyo.value(mr.fs.costing.gac.num_contactors_op) == 4 - assert pyo.value(mr.fs.costing.gac.num_contactors_redundant) == 2 + assert pyo.value(mr.fs.costing.gac_pressure.num_contactors_op) == 4 + assert pyo.value(mr.fs.costing.gac_pressure.num_contactors_redundant) == 2 assert pytest.approx(89040, rel=1e-3) == pyo.value(cost.contactor_cost) assert pytest.approx(69690, rel=1e-3) == pyo.value(cost.other_process_cost) assert pytest.approx(176200, rel=1e-3) == pyo.value(cost.capital_cost) @@ -497,10 +497,10 @@ def test_robust_costing_max_gac_ref(self, gac_frame_robust): # Check for bed_mass_gac_cost_ref to be overwritten # if bed_mass_gac is greater than bed_mass_gac_cost_max_ref assert pyo.value(mr.fs.unit.bed_mass_gac) > pyo.value( - mr.fs.costing.gac.bed_mass_max_ref + mr.fs.costing.gac_pressure.bed_mass_max_ref ) assert pyo.value(mr.fs.unit.costing.bed_mass_gac_ref) == ( - pytest.approx(pyo.value(mr.fs.costing.gac.bed_mass_max_ref), 1e-5) + pytest.approx(pyo.value(mr.fs.costing.gac_pressure.bed_mass_max_ref), 1e-5) )