#################################################################################
# WaterTAP Copyright (c) 2020-2026, The Regents of the University of California,
# through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory,
# National Laboratory of the Rockies, and National Energy Technology
# Laboratory (subject to receipt of any required approvals from the U.S. Dept.
# of Energy). All rights reserved.
#
# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and license
# information, respectively. These files are also available online at the URL
# "https://github.com/watertap-org/watertap/"
#################################################################################
"""
This module contains functions needed for the construction of flexible
desalination flowsheet
"""
from idaes.apps.grid_integration import OperationModel, StorageModel
from pyomo.environ import (
Constraint,
Expression,
NonNegativeReals,
Param,
Var,
units as pyunits,
)
from watertap.flowsheets.flex_desal import params as um_params
from watertap.flowsheets.flex_desal import unit_models as um
pyunits.load_definitions_from_strings(["USD = [currency]"])
[docs]def add_operational_cost_expressions(blk, params: um_params.FlexDesalParams):
"""
Adds cost expressions to the flowsheet
"""
time_interval = params.timestep_hours * pyunits.h
# Water revenue
blk.water_revenue = Expression(
expr=(
params.product_water_price
* blk.posttreatment.product_flowrate
* time_interval
),
doc="Revenue generated from product water",
)
# Customer cost
blk.customer_cost = Param(
initialize=0,
units=pyunits.USD,
mutable=True,
doc="Fixed customer cost",
)
# Demand response revenue
blk.demand_response_price = Param(
initialize=0,
units=pyunits.USD / pyunits.kWh,
mutable=True,
doc="Demand-response prices",
)
blk.baseline_power = Param(
initialize=100, units=pyunits.kW, mutable=True, doc="Baseline power requirement"
)
blk.demand_response_revenue = Expression(
expr=blk.demand_response_price
* (blk.baseline_power - blk.power_from_grid)
* time_interval,
doc="Revenue generated from demand response",
)
# Cost of emissions
blk.emissions_intensity = Param(
initialize=0, mutable=True, units=pyunits.kg / pyunits.kWh
)
blk.total_emissions = Expression(
expr=blk.emissions_intensity * blk.power_from_grid * time_interval,
doc="Amount of CO2 released [in kg]",
)
blk.emissions_cost = Expression(
expr=(
blk.total_emissions
* (params.emissions_cost / 907.185) # Conversion factor: $/ton to $/kg
* (pyunits.USD / pyunits.kg)
),
doc="Cost associated with carbon emissions",
)
# Cost of energy
blk.LMP = Param(
initialize=0,
units=pyunits.USD / pyunits.kWh,
mutable=True,
doc="Locational marginal price of electricity [$/kWh]",
)
blk.energy_cost = Expression(
expr=blk.LMP * blk.power_from_grid * time_interval,
doc="Cost of electricity purchased from the grid",
)
# Demand cost parameters
blk.fixed_demand_rate = Param(
initialize=0,
units=pyunits.USD / pyunits.kW,
mutable=True,
doc="Constant demand tariff",
)
blk.variable_demand_rate = Param(
initialize=0,
units=pyunits.USD / pyunits.kW,
mutable=True,
doc="Variable demand tariff",
)
[docs]def build_desal_flowsheet(blk, params: um_params.FlexDesalParams):
"""
Builds a flowsheet instance of the entire desalination process
Parameters
----------
blk : Block
Pyomo Block instance
params : object
Object containing model parameters
"""
# Build units
blk.intake = OperationModel(
model_func=um.intake_operation_model,
model_args={"params": params.intake},
)
blk.bypass_pretreatment_flow = Var(
within=NonNegativeReals,
units=pyunits.m**3 / pyunits.h,
doc="Flowrate bypassed to brine discharge due to pretreatment shutdown",
)
blk.pretreatment = OperationModel(
model_func=um.pretreatment_operation_model,
model_args={"params": params.pretreatment},
)
blk.reverse_osmosis = OperationModel(
model_func=um.reverse_osmosis_operation_model,
model_args={"params": params.ro},
)
blk.posttreatment = OperationModel(
model_func=um.posttreatment_operation_model,
model_args={"params": params},
)
blk.brine_discharge = OperationModel(
model_func=um.brine_discharge_operation_model,
model_args={"params": params},
)
# Flowsheet connections
blk.arc_intake_pretreatment = Constraint(
expr=blk.intake.product_flowrate
== blk.pretreatment.feed_flowrate + blk.bypass_pretreatment_flow,
doc="intake-pretreatment mass balance",
)
blk.suppress_pretreatment_bypass = Constraint(
expr=blk.bypass_pretreatment_flow
<= (1 - blk.pretreatment.op_mode) * params.intake.nominal_flowrate
)
blk.arc_pretreatment_ro = Constraint(
expr=blk.pretreatment.product_flowrate == blk.reverse_osmosis.feed_flowrate,
doc="pretreatment-reverse_osmosis mass balance",
)
blk.arc_ro_posttreatment = Constraint(
expr=blk.reverse_osmosis.product_flowrate == blk.posttreatment.feed_flowrate,
doc="reverse_osmosis-posttreatment mass balance",
)
blk.calculate_brine_discharge = Constraint(
expr=blk.brine_discharge.feed_flowrate
== (
blk.intake.reject_flowrate
+ blk.bypass_pretreatment_flow
+ blk.pretreatment.reject_flowrate
+ blk.reverse_osmosis.reject_flowrate
+ blk.reverse_osmosis.leftover_flow
+ blk.posttreatment.reject_flowrate
),
doc="Computes the total inflow to brine discharge",
)
blk.num_skids_online = Expression(
expr=sum(blk.reverse_osmosis.ro_skid[:].op_mode),
doc="Calculates the number of skids operating at time t",
)
blk.net_power_consumption = Expression(
expr=blk.intake.power_consumption
+ blk.pretreatment.power_consumption
+ blk.reverse_osmosis.power_consumption
+ blk.posttreatment.power_consumption
+ blk.brine_discharge.power_consumption,
doc="Net power consumed from the grid",
)
if params.include_onsite_solar:
blk.power_generation = OperationModel(
model_func=um.power_generation_operation_model,
model_args={"params": params},
)
blk.net_power_consumption += -blk.power_generation.power_utilized
if params.include_battery:
blk.battery = StorageModel(
time_interval=params.timestep_hours,
charge_efficiency=params.battery.efficiency,
max_charge_rate=params.battery.power_capacity,
max_discharge_rate=params.battery.power_capacity,
max_holdup=params.battery.energy_capacity * params.battery.maximum_soc,
min_holdup=params.battery.energy_capacity * params.battery.minimum_soc,
)
blk.net_power_consumption += (
blk.battery.charge_rate - blk.battery.discharge_rate
)
# Power purchased from the grid
blk.power_from_grid = Var(
within=NonNegativeReals,
units=pyunits.kW,
doc="Total power purchased from the grid",
)
blk.overall_power_balance = Constraint(
expr=blk.power_from_grid == blk.net_power_consumption,
doc="Computes the total power purchased from the grid",
)
# Add cost expressions
add_operational_cost_expressions(blk, params)
[docs]def add_delayed_startup_constraints(m):
"""Adds the delayed startup constraints to the model"""
params: um_params.FlexDesalParams = m.params
# "Shutdown" post-treatment unit if RO startup is initiated
@m.Constraint(m.period.index_set())
def posttreatment_unit_commitment(blk, d, t):
indices = [(d, t - i) for i in range(params.ro.startup_delay) if t - i > 0]
return (1 - blk.period[d, t].posttreatment.op_mode) == sum(
blk.period[p].reverse_osmosis.ro_skid[1].startup for p in indices
)
# Brine pump must operate if RO startup is initiated
@m.Constraint(m.period.index_set())
def brine_pump_unit_commitment(blk, d, t):
indices = [(d, t - i) for i in range(params.ro.startup_delay) if t - i > 0]
return blk.period[d, t].brine_discharge.op_mode == sum(
blk.period[p].reverse_osmosis.ro_skid[1].startup for p in indices
)
[docs]def add_demand_and_fixed_costs(m):
"""Adds variables and expressions/constraints for demand and fixed costs"""
params: um_params.FlexDesalParams = m.params
m.fixed_demand_cost = Var(
within=NonNegativeReals,
units=pyunits.USD,
doc="Total fixed demand charge value for the entire time horizon",
)
m.variable_demand_cost = Var(
within=NonNegativeReals,
units=pyunits.USD,
doc="Total variable demand charge value for the entire time horizon",
)
m.fixed_monthly_cost = Var(
within=NonNegativeReals,
units=pyunits.USD,
doc="Total customer cost for the entire time horizon",
)
@m.Constraint(m.period.index_set())
def calculate_fixed_demand_cost(blk, d, t):
return (
blk.fixed_demand_cost
>= blk.period[d, t].fixed_demand_rate
* blk.period[d, t].power_from_grid
* params.num_months
)
@m.Constraint(m.period.index_set())
def calculate_variable_demand_cost(blk, d, t):
return (
blk.variable_demand_cost
>= blk.period[d, t].variable_demand_rate
* blk.period[d, t].power_from_grid
* params.num_months
)
m.calculate_fixed_monthly_cost = Constraint(
expr=m.fixed_monthly_cost == params.fixed_monthly_cost * params.num_months
)
[docs]def add_useful_expressions(m):
"""Defines useful expressions for custom objective functions"""
m.total_water_revenue = Expression(expr=sum(m.period[:, :].water_revenue))
m.total_demand_response_revenue = Expression(
expr=sum(m.period[:, :].demand_response_revenue)
)
m.total_emissions_cost = Expression(expr=sum(m.period[:, :].emissions_cost))
[docs]def constrain_water_production(m, baseline_production: float = None):
"""Constrains the total water production rate"""
params: um_params.FlexDesalParams = m.params
if baseline_production is not None:
m.curtailment_fraction = Param(
initialize=params.curtailment_fraction,
mutable=True,
units=pyunits.dimensionless,
doc="Fraction of water production that is curtailed",
)
m.baseline_production = Param(
initialize=baseline_production,
mutable=True,
units=pyunits.m**3,
doc="Baseline water production",
)
m.water_production_target = Constraint(
expr=m.total_water_production
>= m.baseline_production * (1 - m.curtailment_fraction)
)
elif params.annual_production_AF is not None:
# Convert production rate from acre-ft/year to m^3/year
annual_production_m3 = params.annual_production_AF * 1233.48
m.production_target_abs = Param(
initialize=annual_production_m3 / 365 * params.num_days,
mutable=True,
units=pyunits.m**3,
doc="Absolute water production target",
)
m.water_production_target = Constraint(
expr=m.total_water_production >= m.production_target_abs
)
else:
raise ValueError("Water production targets not specified in params")