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

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 

9 

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 

21 

22# Set up logger 

23_log = idaeslog.getLogger(__name__) 

24 

25 

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 """ 

32 

33 # CONFIG are options for the unit model, this simple model only has the mandatory config options 

34 CONFIG = ConfigBlock() 

35 

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 ) 

83 

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() 

88 

89 # This creates blank scaling factors, which are populated later 

90 self.scaling_factor = Suffix(direction=Suffix.EXPORT) 

91 

92 

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 ) 

115 

116 

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) 

120 

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 ) 

146 

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 

160 

161 return self.updated_SOC[t] 

162 

163 

164 

165 @self.Constraint( 

166 self.flowsheet().time, 

167 doc="Set output power for charging", 

168 ) 

169 

170 

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] 

177 

178 

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 

185 

186 

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] 

199 

200 @self.Constraint( 

201 self.flowsheet().time, 

202 doc="Ensure power_change is within capacity", 

203 ) 

204 def power_change_above_zero(b, t): 

205 

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 

212 

213 def calculate_scaling_factors(self): 

214 super().calculate_scaling_factors() 

215 

216 def initialize(blk, *args, **kwargs): 

217 

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 # )  

228 

229 def _get_stream_table_contents(self, time_point=0): 

230 pass