Coverage for backend/ahuora-builder/src/ahuora_builder/custom/energy/grid.py: 35%

52 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2026-06-23 21:51 +0000

1# Import Pyomo libraries 

2from pyomo.environ import ( 

3 Component, 

4 Var, 

5 Suffix, 

6 value, 

7 units as pyunits, 

8) 

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

10from idaes.core.util.tables import create_stream_table_dataframe 

11from idaes.core.util.exceptions import ConfigurationError 

12# Import IDAES cores 

13from idaes.core import ( 

14 declare_process_block_class, 

15 UnitModelBlockData, 

16 useDefault, 

17) 

18from idaes.core.util.config import is_physical_parameter_block 

19import idaes.core.util.scaling as iscale 

20import idaes.logger as idaeslog 

21 

22# Set up logger 

23_log = idaeslog.getLogger(__name__) 

24 

25 

26# When using this file the name "Grid" is what is imported 

27@declare_process_block_class("Grid") 

28class gridData(UnitModelBlockData): 

29 """ 

30 Zero order Grid 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"] = False # outlet and waste block is not an inlet 

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

105 self.flowsheet().config.time, 

106 doc="Material properties of outlet", 

107 **tmp_dict 

108 ) 

109 

110 # Add ports - oftentimes users interact with these rather than the state blocks 

111 self.add_port(name="inlet", block=self.properties_in) 

112 

113 # Add variables 

114 self.n_capacity = Var(self.flowsheet().config.time, 

115 initialize=1.0, 

116 doc="N Capacity", 

117 units = pyunits.W 

118 ) 

119 self.n_minus_one = Var(self.flowsheet().config.time, 

120 initialize=1.0, 

121 doc="N-1 Capacity", 

122 units = pyunits.W 

123 ) 

124 self.import_export = Var(self.flowsheet().config.time, 

125 initialize=1.0, 

126 doc="Power being import or exported from grid", 

127 units = pyunits.W 

128 ) 

129 # Add constraints 

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

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

132 @self.Constraint( 

133 self.flowsheet().time, 

134 doc="Power usage", 

135 ) 

136 def eq_power_in_balance(b, t): 

137 return ( 

138 self.import_export[t] == b.properties_in[t].power 

139 ) 

140 

141 def calculate_scaling_factors(self): 

142 super().calculate_scaling_factors() 

143 

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

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

146 for i in blk.properties_in.index_set(): 

147 if not blk.properties_out[i].power.fixed: 

148 blk.properties_out[i].power = blk.properties_in[i].power.value 

149 

150 def diagnose(self) -> list[tuple[Component, str]]: 

151 """ 

152 Report common Grid configuration issues that can be shown in the 

153 flowsheet diagnostics panel. 

154 """ 

155 problems = [] 

156 for time in self.flowsheet().time: 

157 power = value(self.import_export[time], exception=False) 

158 capacity = value(self.n_capacity[time], exception=False) 

159 

160 if ( 

161 power is not None 

162 and capacity is not None 

163 and abs(power) > capacity 

164 ): 

165 problems.append( 

166 ( 

167 self.n_capacity[time], 

168 f"Grid power in/out ({power:.2f} W) exceeds Grid " 

169 f"N-Capacity ({capacity:.2f} W). Increase the " 

170 "capacity or reduce the grid power.", 

171 ) 

172 ) 

173 # Avoid reporting this multiple times, so we're just going to early return. 

174 return problems 

175 

176 return problems 

177 

178 def _get_stream_table_contents(self, time_point=0): 

179 """ 

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

181 

182 Developers should overload this as appropriate. 

183 """ 

184 try: 

185 return create_stream_table_dataframe( 

186 {"inlet": self.inlet}, time_point=time_point 

187 ) 

188 except AttributeError: 

189 raise ConfigurationError( 

190 f"Unit model {self.name} does not have the standard Port " 

191 f"names (inlet and outlet). Please contact the unit model " 

192 f"developer to develop a unit specific stream table." 

193 )