Coverage for backend/idaes_service/solver/custom/energy/storage.py: 25%
76 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 libraries
2from pyomo.environ import (
3 Var,
4 Suffix,
5 units as pyunits,
6)
7from pyomo.common.config import ConfigBlock, ConfigValue, In
8from idaes.core.util.exceptions import ConfigurationError
10# Import IDAES cores
11from idaes.core import (
12 declare_process_block_class,
13 UnitModelBlockData,
14 useDefault,
15)
16from idaes.core.util.config import is_physical_parameter_block
17import idaes.core.util.scaling as iscale
18import idaes.logger as idaeslog
19from pyomo.util.check_units import assert_units_consistent
20from pyomo.environ import value
22# Set up logger
23_log = idaeslog.getLogger(__name__)
26# When using this file the name "Link" is what is imported
27@declare_process_block_class("Storage")
28class StorageData(UnitModelBlockData):
29 """
30 Zero order Link model
31 """
33 # CONFIG are options for the unit model, this simple model only has the mandatory config options
34 CONFIG = ConfigBlock()
36 CONFIG.declare(
37 "dynamic",
38 ConfigValue(
39 domain=In([False]),
40 default=False,
41 description="Dynamic model flag - must be False",
42 doc="""Indicates whether this model will be dynamic or not,
43 **default** = False. The Bus unit does not support dynamic
44 behavior, thus this must be False.""",
45 ),
46 )
47 CONFIG.declare(
48 "has_holdup",
49 ConfigValue(
50 default=False,
51 domain=In([False]),
52 description="Holdup construction flag - must be False",
53 doc="""Indicates whether holdup terms should be constructed or not.
54 **default** - False. The Bus unit does not have defined volume, thus
55 this must be False.""",
56 ),
57 )
58 CONFIG.declare(
59 "property_package",
60 ConfigValue(
61 default=useDefault,
62 domain=is_physical_parameter_block,
63 description="Property package to use for control volume",
64 doc="""Property parameter object used to define property calculations,
65 **default** - useDefault.
66 **Valid values:** {
67 **useDefault** - use default package from parent model or flowsheet,
68 **PhysicalParameterObject** - a PhysicalParameterBlock object.}""",
69 ),
70 )
71 CONFIG.declare(
72 "property_package_args",
73 ConfigBlock(
74 implicit=True,
75 description="Arguments to use for constructing property packages",
76 doc="""A ConfigBlock with arguments to be passed to a property block(s)
77 and used when constructing these,
78 **default** - None.
79 **Valid values:** {
80 see property package for documentation.}""",
81 ),
82 )
84 def build(self):
85 # build always starts by calling super().build()
86 # This triggers a lot of boilerplate in the background for you
87 super().build()
89 # This creates blank scaling factors, which are populated later
90 self.scaling_factor = Suffix(direction=Suffix.EXPORT)
93 # Add state blocks for inlet, outlet, and waste
94 # These include the state variables and any other properties on demand
95 # Add inlet block
96 tmp_dict = dict(**self.config.property_package_args)
97 tmp_dict["parameters"] = self.config.property_package
98 tmp_dict["defined_state"] = True # inlet block is an inlet
99 self.properties_in = self.config.property_package.state_block_class(
100 self.flowsheet().config.time, doc="Material properties of inlet", **tmp_dict
101 )
102 # Add outlet and waste block
103 tmp_dict["defined_state"] = True
104 self.properties_in = self.config.property_package.state_block_class(
105 self.flowsheet().config.time,
106 doc="Material properties of outlet",
107 **tmp_dict
108 )
109 tmp_dict["defined_state"] = False # outlet and waste block is not an inlet
110 self.properties_out = self.config.property_package.state_block_class(
111 self.flowsheet().config.time,
112 doc="Material properties of outlet",
113 **tmp_dict
114 )
117 # Add ports - oftentimes users interact with these rather than the state blocks
118 self.add_port(name="inlet", block=self.properties_in)
119 self.add_port(name="outlet", block=self.properties_out)
121 # Add variables:
122 self.charging_efficiency = Var(
123 self.flowsheet().config.time,
124 initialize=1.0,
125 doc="Charging Efficiency",
126 )
127 self.capacity = Var(self.flowsheet().config.time,
128 initialize=1.0,
129 units=pyunits.kWh,
130 doc="Capacity of the storage",
131 )
132 self.initial_SOC = Var(self.flowsheet().config.time,
133 initialize=0.4,
134 doc="Initial State of Charge",
135 )
136 self.charging_power_in = Var(self.flowsheet().config.time,
137 initialize=1.0,
138 units=pyunits.W,
139 doc="Power in",
140 )
141 self.charging_power_out = Var(self.flowsheet().config.time,
142 initialize=1.0,
143 units=pyunits.W,
144 doc="Power out",
145 )
147 @self.Expression(
148 self.flowsheet().time,
149 doc="updated state of charge",
150 )
151 def updated_SOC(b,t):
152 power_in = self.charging_power_in[t]
153 power_out = self.charging_power_out[t]
154 power_change = (power_in-power_out)/(pyunits.W *1000)
155 capacity_without_unit = self.capacity[t]/pyunits.kWh
156 if t == self.flowsheet().time.first():
157 self.updated_SOC[t] = self.initial_SOC[t] + power_change/capacity_without_unit
158 else:
159 self.updated_SOC[t] = self.updated_SOC[t-1] + power_change/capacity_without_unit
161 return self.updated_SOC[t]
165 @self.Constraint(
166 self.flowsheet().time,
167 doc="Set output power for charging",
168 )
171 @self.Constraint(
172 self.flowsheet().time,
173 doc="Set output power for discharging",
174 )
175 def set_power_out_discharge(b,t):
176 return b.properties_out[t].power == self.charging_power_out[t]
179 @self.Constraint(
180 self.flowsheet().time,
181 doc="Set output power for charging",
182 )
183 def set_power_charge(b,t):
184 return self.charging_power_in[t] == self.properties_in[t].power
187 # Add a constraint to ensure power_change is within a range
188 @self.Constraint(
189 self.flowsheet().time,
190 doc="Ensure power_change is within a specified range",
191 )
192 def power_change_within_range(b, t):
193 power_in = self.charging_power_in[t]
194 power_out = self.charging_power_out[t]
195 power_in_out = (power_in-power_out)/(pyunits.W *1000)
196 # power_in_out = b.properties_out[t].power / (pyunits.W * 1000)
197 remaining_power = power_in_out + self.initial_SOC[t] * self.capacity[t]
198 return remaining_power <= self.capacity[t]
200 @self.Constraint(
201 self.flowsheet().time,
202 doc="Ensure power_change is within capacity",
203 )
204 def power_change_above_zero(b, t):
206 power_in = self.charging_power_in[t]
207 power_out = self.charging_power_out[t]
208 power_in_out = (power_in-power_out)/(pyunits.W *1000)
209 #power_in_out = b.properties_out[t].power / (pyunits.W * 1000)
210 remaining_power = power_in_out + self.initial_SOC[t] * self.capacity[t]
211 return remaining_power >= 0
213 def calculate_scaling_factors(self):
214 super().calculate_scaling_factors()
216 def initialize(blk, *args, **kwargs):
218 for i in blk.properties_in.index_set():
219 if blk.initial_SOC[i].value == 0 and abs(blk.charging_power_out[i].value)> 0 and abs(blk.charging_power_in[i].value) == 0:
220 raise ConfigurationError(
221 "Warning: There is no power to discharge in the battery."
222 )
223 if blk.charging_power_in[i].value > blk.capacity[i].value:
224 pass
225 # raise ConfigurationError(
226 # "Warning: Battery capacity is exceeded!"
227 # )
229 def _get_stream_table_contents(self, time_point=0):
230 pass