Source code for watertap.costing.multiple_choice_costing_block

#################################################################################
# WaterTAP Copyright (c) 2020-2024, The Regents of the University of California,
# through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory,
# National Renewable Energy Laboratory, 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/"
#################################################################################

import pyomo.environ as pyo
from pyomo.common.config import ConfigBlock, ConfigValue
from pyomo.util.calc_var_value import calculate_variable_from_constraint
from idaes.core import declare_process_block_class, ProcessBlockData, UnitModelBlockData
from idaes.core.base.process_base import ProcessBaseBlock
from idaes.core.util.misc import add_object_reference
from idaes.core.base.costing_base import (
    UnitModelCostingBlockData,
    UnitModelCostingBlock,
    assert_flowsheet_costing_block,
    DefaultCostingComponents,
)
import idaes.logger as idaeslog

_log = idaeslog.getLogger(__name__)


[docs]@declare_process_block_class("MultiUnitModelCostingBlock") class MultiUnitModelCostingBlockData(UnitModelCostingBlockData, UnitModelCostingBlock): """ Class for constructing several costing blocks on the same unit model and then allowing for choice between them """ CONFIG = ConfigBlock() CONFIG.declare( "flowsheet_costing_block", ConfigValue( domain=assert_flowsheet_costing_block, doc="Reference to associated FlowsheetCostingBlock to use.", ), ) CONFIG.declare( "costing_blocks", ConfigValue( domain=dict, doc="Costing blocks to use for unit. Should be a dictionary whose keys " "are the names of the block and whose values are either the costing " "method used to construct the block, or another dictionary whose keys " "are `costing_method`, which has the value of the costing method, and " "optionally `costing_method_arguments`, which has the keyword arguments " "for the costing method specified.", ), ) CONFIG.declare( "initial_costing_block", ConfigValue( doc="Costing block to be initially active", default=None, ), )
[docs] def build(self): ProcessBlockData.build(self) # Alias flowsheet costing block reference fcb = self.config.flowsheet_costing_block # Get reference to unit model unit_model = self.parent_block() # Check that parent is an instance of a UnitModelBlockData if UnitModelBlockData not in unit_model.__class__.__mro__: raise TypeError( f"{self.name} - parent object ({unit_model.name}) is not an " f"instance of a UnitModelBlockData object. " "UnitModelCostingBlocks can only be added to UnitModelBlocks." ) # Check to see if unit model already has costing for b in unit_model.component_objects(pyo.Block, descend_into=False): if b is not self and isinstance( b, (UnitModelCostingBlock, MultiUnitModelCostingBlock) ): # Block already has costing, clean up and raise exception raise RuntimeError( f"Unit model {unit_model.name} already has a costing block" f" registered: {b.name}. Each unit may only have a single " "UnitModelCostingBlock associated with it." ) # Add block to unit model initialization order unit_model._initialization_order.append(self) # Register unit model with this costing package fcb._registered_unit_costing.append(self) # Assign object references for costing package and unit model add_object_reference(self, "costing_package", fcb) add_object_reference(self, "unit_model", unit_model) self.costing_blocks = ProcessBaseBlock(self.config.costing_blocks) self.costing_block_selector = pyo.Param( self.config.costing_blocks, domain=pyo.Boolean, default=0, mutable=True ) if self.config.initial_costing_block is None: for k in self.config.costing_blocks: self.costing_block_selector[k].set_value(1) break else: self.costing_block_selector[self.config.initial_costing_block].set_value(1) # Get costing method if not provided for k, val in self.config.costing_blocks.items(): # if we get a callable, just use it if callable(val): method = val kwds = {} # else we'll assume it's a dictionary with keys # `costing_method` and potentially `costing_method_arguments`. # We raise an error if we find something else. else: for l in val: if l not in ("costing_method", "costing_method_arguments"): raise RuntimeError( f"Unrecognized key {l} for costing block {k}." ) try: method = val["costing_method"] except KeyError: raise KeyError( f"Must specify a `costing_method` key for costing " f"block {k}." ) kwds = val.get("costing_method_arguments", {}) blk = self.costing_blocks[k] # Assign object references for costing package and unit model add_object_reference(blk, "costing_package", fcb) add_object_reference(blk, "unit_model", unit_model) # Call unit costing method method(blk, **kwds) # Check that costs are Vars and have lower bound of 0 cost_vars = DefaultCostingComponents for v in cost_vars: try: cvar = getattr(blk, v) if not isinstance(cvar, pyo.Var): raise TypeError( f"{unit_model.name} {v} component must be a Var. " "Please check the costing package you are using to " "ensure that all costing components are declared as " "variables." ) elif cvar.lb is None or cvar.lb < 0: _log.warning( f"{unit_model.name} {v} component has a lower bound " "less than zero. Be aware that this may result in " "negative costs during optimization." ) except AttributeError: pass # Now we need to tie them all together cost_vars = list(DefaultCostingComponents) + ["direct_capital_cost"] for vname in cost_vars: for blk in self.costing_blocks.values(): if hasattr(blk, vname): break else: # no break continue expr = 0.0 for name, blk in self.costing_blocks.items(): cvar = blk.component(vname) if cvar is None: continue expr += self.costing_block_selector[name] * cvar self.add_component(vname, pyo.Expression(expr=expr))
[docs] def select_costing_block(self, costing_block_name): """ Set the active costing block """ # zero out everything else self.costing_block_selector[:].set_value(0) self.costing_block_selector[costing_block_name].set_value(1)
[docs] def initialize(self, *args, **kwargs): """ Initialize all costing blocks for easy switching between """ # TODO: Implement an initialization method # TODO: Need to have a general purpose method (block triangularisation?) # TODO: Should also allow registering custom methods # Vars and Constraints for blk in self.costing_blocks.values(): for c in DefaultCostingComponents: if hasattr(blk, c): var = getattr(blk, c) cons = getattr(blk, f"{c}_constraint") calculate_variable_from_constraint(var, cons)