Coverage for backend/idaes_service/solver/custom/energy/ac_property_package.py: 41%
80 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-11-06 23:27 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2025-11-06 23:27 +0000
1# Import Pyomo tools
2from pyomo.environ import (
3 Constraint,
4 Var,
5 Param,
6 Expression,
7 Reals,
8 NonNegativeReals,
9 Suffix,
10)
11from pyomo.environ import units as pyunits
12from pyomo.common.config import ConfigBlock, ConfigValue, Bool
14# Import IDAES cores
15from idaes.core import (
16 declare_process_block_class,
17 MaterialFlowBasis,
18 PhysicalParameterBlock,
19 StateBlockData,
20 StateBlock,
21 MaterialBalanceType,
22 EnergyBalanceType,
23)
24from idaes.core.base.components import Component
25from idaes.core.base.phases import LiquidPhase
26from idaes.core.util.initialization import (
27 fix_state_vars,
28 revert_state_vars,
29 solve_indexed_blocks,
30)
31from idaes.core.base.process_base import ProcessBlockData
32from idaes.core.base import property_meta
33from idaes.core.util.model_statistics import (
34 degrees_of_freedom,
35 number_unfixed_variables,
36)
37from idaes.core.util.exceptions import PropertyPackageError
38import idaes.core.util.scaling as iscale
39import idaes.logger as idaeslog
40from idaes.core.solvers import get_solver
42# Set up logger
43_log = idaeslog.getLogger(__name__)
46# STEP 2
48# When using this file the name "acParameterBlock" is what is imported
49@declare_process_block_class("acParameterBlock")
50class acParameterData(PhysicalParameterBlock):
51 CONFIG = ProcessBlockData.CONFIG()
52 CONFIG.declare(
53 "default_arguments",
54 ConfigBlock(
55 implicit=True, description="Default arguments to use with Property Package"
56 ),
57 )
59 def build(self):
60 """
61 Callable method for Block construction.
62 """
63 super(acParameterData, self).build()
65 self._state_block_class = acStateBlock
67 # Variables
68 self.active_power = Var(initialize=0, domain=Reals, units=pyunits.W)
69 self.reactive_power = Var(initialize=0, domain=Reals, units=pyunits.W)
70 self.voltage = Var(initialize=0, domain=Reals, units=pyunits.V)
72 # Default scaling values should be provided so that our tools can ensure the model is well-scaled
73 # Generally scaling factors should be such that if it is multiplied by the variable it will range between 0.01 and 100
74 self.set_default_scaling("active_power", 1e-3)
75 self.set_default_scaling("reactive_power", 1e-3)
76 self.set_default_scaling("voltage", 1e-3)
78 @classmethod
79 def define_metadata(cls, obj):
80 # see https://github.com/watertap-org/watertap/blob/main/tutorials/creating_a_simple_property_model.ipynb
81 obj.add_properties(
82 {
83 "active_power": {"method": None},
84 "reactive_power": {"method": None},
85 "voltage": {"method": None},
86 }
87 )
88 obj.add_default_units(
89 {
90 "time": pyunits.s,
91 "length": pyunits.m,
92 "mass": pyunits.kg,
93 }
94 )
96 def build_state_block(self, *args, **kwargs):
97 """
98 Methods to construct a StateBlock associated with this
99 PhysicalParameterBlock. This will automatically set the parameters
100 construction argument for the StateBlock.
102 Returns:
103 StateBlock
105 """
106 # default = kwargs.pop("default", {})
107 initialize = kwargs.pop("initialize", {})
109 if initialize == {}:
110 kwargs["parameters"] = self
111 else:
112 for i in initialize.keys():
113 initialize[i]["parameters"] = self
115 return self.state_block_class( # pylint: disable=not-callable
116 *args, **kwargs, initialize=initialize
117 )
119 @property
120 def state_block_class(self):
121 if self._state_block_class is not None:
122 return self._state_block_class
123 else:
124 raise AttributeError(
125 "{} has not assigned a StateBlock class to be associated "
126 "with this property package. Please contact the developer of "
127 "the property package.".format(self.name)
128 )
130# STEP 3: State Block
131class _acStateBlock(StateBlock):
132 def initialize(
133 self,
134 state_args=None,
135 state_vars_fixed=False,
136 hold_state=False,
137 outlvl=idaeslog.NOTSET,
138 solver=None,
139 optarg=None,
140 ):
141 """
142 Initialization routine for property package.
143 Keyword Arguments:
144 state_args : Dictionary with initial guesses for the state vars
145 chosen. Note that if this method is triggered
146 through the control volume, and if initial guesses
147 were not provided at the unit model level, the
148 control volume passes the inlet values as initial
149 guess.The keys for the state_args dictionary are:
151 flow_mass_phase_comp : value at which to initialize
152 phase component flows
153 pressure : value at which to initialize pressure
154 temperature : value at which to initialize temperature
156 state_vars_fixed: Flag to denote if state vars have already been
157 fixed.
158 - True - states have already been fixed by the
159 control volume 1D. Control volume 0D
160 does not fix the state vars, so will
161 be False if this state block is used
162 with 0D blocks.
163 - False - states have not been fixed. The state
164 block will deal with fixing/unfixing.
165 hold_state : flag indicating whether the initialization routine
166 should unfix any state variables fixed during
167 initialization (default=False).
168 - True - states variables are not unfixed, and
169 a dict of returned containing flags for
170 which states were fixed during
171 initialization.
172 - False - state variables are unfixed after
173 initialization by calling the
174 release_state method
175 outlvl : sets output level of initialization routine (default=idaeslog.NOTSET)
176 solver : Solver object to use during initialization if None is provided
177 it will use the default solver for IDAES (default = None)
178 optarg : solver options dictionary object (default=None)
179 Returns:
180 If hold_states is True, returns a dict containing flags for
181 which states were fixed during initialization.
182 """
184 # Fix state variables
185 flags = fix_state_vars(self, state_args)
186 # Check that dof = 0 when state variables are fixed
187 for k in self.keys():
188 dof = degrees_of_freedom(self[k])
189 if dof != 0:
190 raise PropertyPackageError(
191 "\nWhile initializing {sb_name}, the degrees of freedom "
192 "are {dof}, when zero is required. \nInitialization assumes "
193 "that the state variables should be fixed and that no other "
194 "variables are fixed. \nIf other properties have a "
195 "predetermined value, use the calculate_state method "
196 "before using initialize to determine the values for "
197 "the state variables and avoid fixing the property variables."
198 "".format(sb_name=self.name, dof=dof)
199 )
201 # If input block, return flags, else release state
202 if state_vars_fixed is False:
203 if hold_state is True:
204 return flags
205 else:
206 self.release_state(flags)
208 def release_state(self, flags, outlvl=idaeslog.NOTSET):
209 """
210 Method to release state variables fixed during initialisation.
212 Keyword Arguments:
213 flags : dict containing information of which state variables
214 were fixed during initialization, and should now be
215 unfixed. This dict is returned by initialize if
216 hold_state=True.
217 outlvl : sets output level of of logging
218 """
219 if flags is None:
220 return
221 # Unfix state variables
222 for attr in flags:
223 if flags[attr] is True:
224 getattr(self, attr).unfix()
225 return
227# STEP 4:
228@declare_process_block_class("acStateBlock", block_class=_acStateBlock)
229class acStateBlockData(StateBlockData):
230 def build(self):
231 """Callable method for Block construction."""
232 super(acStateBlockData, self).build()
234 self.scaling_factor = Suffix(direction=Suffix.EXPORT)
236 self.active_power = Var(
237 initialize=0,
238 domain=Reals,
239 units=pyunits.W,
240 doc="active power flow",
241 )
242 self.reactive_power = Var(
243 initialize=0,
244 domain=Reals,
245 units=pyunits.W,
246 doc="reactive power flow",
247 )
248 self.voltage = Var(
249 initialize=0,
250 domain=Reals,
251 units=pyunits.V,
252 doc="voltage",
253 )
255 # -----------------------------------------------------------------------------
257 def define_state_vars(self):
258 """Define state vars."""
259 return {
260 "active_power": self.active_power,
261 "reactive_power" : self.reactive_power,
262 "voltage": self.voltage
263 }
266 # -----------------------------------------------------------------------------
267 # Scaling methods
268 def calculate_scaling_factors(self):
269 super().calculate_scaling_factors()
270 # This doesn't do anything, but it's a good example of how to get and set scaling factors in relation to each other.
271 sfa = iscale.get_scaling_factor(self.active_power)
272 iscale.set_scaling_factor(self.active_power, sfa)
273 sfr = iscale.get_scaling_factor(self.reactive_power)
274 iscale.set_scaling_factor(self.reactive_power, sfr)
275 sfv = iscale.get_scaling_factor(self.active_power)
276 iscale.set_scaling_factor(self.voltage, sfv)