Skip to content

Commit

Permalink
Separating GAC costing parameters based on type (watertap-org#1209)
Browse files Browse the repository at this point in the history
  • Loading branch information
bknueven authored Nov 23, 2023
1 parent 42faf16 commit d54481b
Show file tree
Hide file tree
Showing 2 changed files with 121 additions and 89 deletions.
190 changes: 111 additions & 79 deletions watertap/costing/unit_models/gac.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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",
)
Expand All @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -200,29 +238,26 @@ 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(
expr=blk.contactor_cost
== 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,
Expand All @@ -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,
)
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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]
)
)

Expand Down
20 changes: 10 additions & 10 deletions watertap/unit_models/tests/test_gac.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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)
)


Expand Down

0 comments on commit d54481b

Please sign in to comment.