Coverage for backend/ahuora-builder/src/ahuora_builder/custom/energy/energy_mixer.py: 82%

61 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2026-07-22 05:22 +0000

1# Import Pyomo libraries 

2from stringprep import in_table_a1 

3from pyomo.environ import ( 

4 Var, 

5 Suffix, 

6 units as pyunits, 

7) 

8from pyomo.common.config import ConfigBlock, ConfigValue, In 

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 

19 

20from idaes.core.util.tables import create_stream_table_dataframe 

21 

22from idaes.core.util.exceptions import ConfigurationError, BurntToast 

23 

24# Set up logger 

25_log = idaeslog.getLogger(__name__) 

26 

27 

28# When using this file the name "EnergyMixer" is what is imported 

29@declare_process_block_class("EnergyMixer") 

30class EnergyMixerData(UnitModelBlockData): 

31 """ 

32 Zero order energy_mixer model 

33 """ 

34 

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

36 CONFIG = ConfigBlock() 

37 

38 CONFIG.declare( 

39 "dynamic", 

40 ConfigValue( 

41 domain=In([False]), 

42 default=False, 

43 description="Dynamic model flag - must be False", 

44 doc="""Indicates whether this model will be dynamic or not, 

45 **default** = False. The Bus unit does not support dynamic 

46 behavior, thus this must be False.""", 

47 ), 

48 ) 

49 CONFIG.declare( 

50 "has_holdup", 

51 ConfigValue( 

52 default=False, 

53 domain=In([False]), 

54 description="Holdup construction flag - must be False", 

55 doc="""Indicates whether holdup terms should be constructed or not. 

56 **default** - False. The Bus unit does not have defined volume, thus 

57 this must be False.""", 

58 ), 

59 ) 

60 CONFIG.declare( 

61 "property_package", 

62 ConfigValue( 

63 default=useDefault, 

64 domain=is_physical_parameter_block, 

65 description="Property package to use for control volume", 

66 doc="""Property parameter object used to define property calculations, 

67 **default** - useDefault. 

68 **Valid values:** { 

69 **useDefault** - use default package from parent model or flowsheet, 

70 **PhysicalParameterObject** - a PhysicalParameterBlock object.}""", 

71 ), 

72 ) 

73 CONFIG.declare( 

74 "property_package_args", 

75 ConfigBlock( 

76 implicit=True, 

77 description="Arguments to use for constructing property packages", 

78 doc="""A ConfigBlock with arguments to be passed to a property block(s) 

79 and used when constructing these, 

80 **default** - None. 

81 **Valid values:** { 

82 see property package for documentation.}""", 

83 ), 

84 ) 

85 CONFIG.declare( 

86 "num_inlets", 

87 ConfigValue( 

88 default=False, 

89 domain=int, 

90 description="Number of inlets to add", 

91 doc="""Number of inlets to add""", 

92 ), 

93 ) 

94 

95 def build(self): 

96 # build always starts by calling super().build() 

97 # This triggers a lot of boilerplate in the background for you 

98 super().build() 

99 

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

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

102 

103 

104 # Defining parameters of state block class 

105 tmp_dict = dict(**self.config.property_package_args) 

106 tmp_dict["parameters"] = self.config.property_package 

107 tmp_dict["defined_state"] = True # inlet block is an inlet 

108 

109 # Add state blocks for inlet, outlet, and waste 

110 # These include the state variables and any other properties on demand 

111 num_inlets = self.config.num_inlets 

112 

113 self.inlet_list = [ "inlet_" + str(i+1) for i in range(num_inlets) ] 

114 

115 

116 self.inlet_blocks = [] 

117 for name in self.inlet_list: 

118 # add properties_inlet_1, properties_inlet2 etc 

119 state_block = self.config.property_package.state_block_class( 

120 self.flowsheet().config.time, doc="inlet power", **tmp_dict 

121 ) 

122 self.inlet_blocks.append(state_block) 

123 # Dynamic equivalent to self.properties_inlet_1 = stateblock 

124 setattr(self,"properties_" + name, state_block) 

125 # also add the port 

126 self.add_port(name=name,block=state_block) 

127 

128 

129 # Add outlet state block 

130 tmp_dict["defined_state"] = False # outlet and waste block is not an inlet 

131 self.properties_out = self.config.property_package.state_block_class( 

132 self.flowsheet().config.time, 

133 doc="Material properties of outlet", 

134 **tmp_dict 

135 ) 

136 

137 

138 # Add outlet port 

139 self.add_port(name="outlet", block=self.properties_out) 

140 

141 #Add variables for capacity and efficiency: 

142 self.efficiency = Var(self.flowsheet().config.time, 

143 initialize=1.0, 

144 doc="Efficiency of the link", 

145 ) 

146 self.capacity = Var( 

147 initialize=1.0, 

148 units = pyunits.W, 

149 doc="Capacity of the link", 

150 ) 

151 

152 

153 # Add constraints 

154 # Usually unit models use a control volume to do the mass, energy, and momentum 

155 # balances, however, they will be explicitly written out in this example 

156 

157 @self.Constraint( 

158 self.flowsheet().time, 

159 doc="Power usage", 

160 ) 

161 def eq_power_balance(b, t): 

162 return ( 

163 sum( 

164 state_block[t].power for state_block in self.inlet_blocks 

165 ) * self.efficiency[t] 

166 == b.properties_out[t].power 

167 ) 

168 

169 def calculate_scaling_factors(self): 

170 super().calculate_scaling_factors() 

171 

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

173 # Just propagate the power from inlet to outlet, good simple method of initialization 

174 for t in blk.flowsheet().time: 

175 power_in = 0 

176 for state_block in blk.inlet_blocks: 

177 power_in += state_block[t].power.value 

178 if not blk.properties_out[t].power.fixed: 178 ↛ 180line 178 didn't jump to line 180 because the condition on line 178 was always true

179 blk.properties_out[t].power = power_in 

180 if(power_in > blk.capacity.value): 180 ↛ 181line 180 didn't jump to line 181 because the condition on line 180 was never true

181 raise BurntToast( 

182 "Danger: Input power exceeds energy mixer capacity. Please either increase capacity or lower input power.".format(blk.name) 

183 ) 

184 def _get_stream_table_contents(self, time_point=0): 

185 """ 

186 Assume unit has standard configuration of 1 inlet and 1 outlet. 

187 

188 Developers should overload this as appropriate. 

189 """ 

190 

191 io_dict = {} 

192 for inlet_name in self.inlet_list: 

193 io_dict[inlet_name] = getattr(self, inlet_name) # get a reference to the port 

194 

195 io_dict["Outlet"] = self.outlet 

196 return create_stream_table_dataframe(io_dict, time_point=time_point) 

197 

198 @staticmethod 

199 def ahuora_metadata(): 

200 from ahuora_unit_ops.json_config import ( 

201 JsonAdapterArgConfig, 

202 JsonFrontendConfig, 

203 JsonGraphicObjectConfig, 

204 JsonIdaesAdapterConfig, 

205 JsonPortAdapterConfig, 

206 JsonPortConfig, 

207 JsonPropertyConfig, 

208 JsonPropertySetGroupConfig, 

209 JsonUnitOpConfig, 

210 ) 

211 

212 return JsonUnitOpConfig( 

213 key='energy_mixer', 

214 objectType='energy_mixer', 

215 enumMember='energy_mixer', 

216 displayType='Energy Mixer', 

217 displayName='Energy Mixer', 

218 categoryPath=['energy', 'power_system'], 

219 ports={ 

220 'inlet': JsonPortConfig( 

221 displayName='Power Inlet', 

222 type='inlet', 

223 streamType='energy_stream', 

224 many=True, 

225 default=2, 

226 minimum=2, 

227 makeStream=True, 

228 streamOffset=0.75, 

229 streamName='PS', 

230 ), 

231 'outlet': JsonPortConfig( 

232 displayName='Power Outlet', 

233 type='outlet', 

234 streamType='energy_stream', 

235 many=False, 

236 default=1, 

237 minimum=1, 

238 makeStream=True, 

239 streamOffset=0.75, 

240 streamName='PS', 

241 ), 

242 }, 

243 propertyPackagePorts={ 

244 '': ['inlet', 'outlet'], 

245 }, 

246 graphicObject=JsonGraphicObjectConfig( 

247 kind='unitop_graphic', 

248 ), 

249 indexSets=[], 

250 properties={ 

251 'efficiency': JsonPropertyConfig( 

252 propertySetGroup='default', 

253 displayName='Efficiency', 

254 indexSets=None, 

255 sumToOne=False, 

256 value=None, 

257 unit=None, 

258 unitType='ratio', 

259 description=None, 

260 type='numeric', 

261 many=False, 

262 default=1, 

263 options={}, 

264 hasTimeIndex=True, 

265 ), 

266 'capacity': JsonPropertyConfig( 

267 propertySetGroup='default', 

268 displayName='Capacity', 

269 indexSets=None, 

270 sumToOne=False, 

271 value=None, 

272 unit=None, 

273 unitType='heatflow', 

274 description=None, 

275 type='numeric', 

276 many=False, 

277 default=1, 

278 options={}, 

279 hasTimeIndex=False, 

280 ), 

281 }, 

282 propertySetGroups={ 

283 'default': JsonPropertySetGroupConfig( 

284 type='stateVars', 

285 displayName='Properties', 

286 stateVars=['efficiency', 'capacity'], 

287 toggle=None, 

288 ), 

289 }, 

290 keyProperties=['efficiency'], 

291 splitterFractionName=None, 

292 idaesAdapter=JsonIdaesAdapterConfig( 

293 constructor='ahuora_builder.custom.energy.energy_mixer.EnergyMixer', 

294 args={ 

295 'property_package': JsonAdapterArgConfig( 

296 kind='constant_schema', 

297 schema='PowerPPArgSchema', 

298 ), 

299 'num_inlets': JsonAdapterArgConfig( 

300 kind='port_count', 

301 port='inlet', 

302 ), 

303 }, 

304 ports=JsonPortAdapterConfig( 

305 kind='mixer', 

306 ), 

307 properties=None, 

308 ), 

309 frontend=JsonFrontendConfig( 

310 showInPanel=True, 

311 variant=None, 

312 ), 

313 )