Coverage for backend/idaes_service/solver/custom/custom_tank.py: 93%

53 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2025-11-06 23:27 +0000

1from idaes.core import declare_process_block_class, MaterialBalanceType 

2from .water_tank_with_units import WaterTankData 

3from .add_initial_dynamics import add_initial_dynamics 

4from pyomo.environ import Reference, Var 

5from pyomo.dae import DerivativeVar 

6from pyomo.environ import units as pyunit 

7from .inverted import add_inverted, initialise_inverted 

8 

9 

10def CustomTank(*args, **kwargs): 

11 # In dynamics mode, we need to use the default material balance type of componentPhase. 

12 # This does balances for liquid and vapor phases separately. This is needed becasue  

13 # accumulation terms are phase specific. 

14 # However, in steady-state, this is not necessary, because we don't have accumulation terms. 

15 # So this would make the system over-defined, as the state block defines the phase equilibrium already. 

16 is_dynamic = kwargs.get("dynamic") 

17 if is_dynamic: 

18 kwargs["material_balance_type"] = MaterialBalanceType.componentPhase 

19 else: 

20 kwargs["material_balance_type"] = MaterialBalanceType.componentTotal 

21 

22 return DynamicTank(*args, **kwargs) 

23 

24 

25@declare_process_block_class("DynamicTank") 

26class DynamicTankData(WaterTankData): 

27 """ 

28 Water tank model with dynamic capabilities. 

29 Some extra properties are added to IDAES's tank model to allow for easier specification of initial conditions. 

30 """ 

31 

32 def build(self, *args, **kwargs): 

33 """ 

34 Build method for the DynamicHeaterData class. 

35 This method initializes the control volume and sets up the model. 

36 """ 

37 

38 super().build(*args, **kwargs) 

39 

40 self.flow_rate_out = Reference(self.outlet.flow_mol) 

41 

42 # add deltaP_inverted as a property 

43 add_inverted(self,"heat_duty") 

44 

45 # Because it's hard to specify the initial conditions directly, 

46 # Create a state block for the initial conditions. 

47 

48 if not self.config.dynamic: 

49 return # There is no need to add these extra properties.  

50 

51 self.initial_block = self.config.property_package.build_state_block( 

52 [0], 

53 defined_state=True, 

54 ) 

55 

56 if len(self.config.property_package.component_list) > 1: 56 ↛ 60line 56 didn't jump to line 60 because the condition on line 56 was never true

57 

58 # We are assuming that the composition does not change at the initial time step. In theory it could, but 

59 # we can worry about that later. 

60 @self.Constraint( 

61 self.config.property_package.component_list, 

62 doc="Initial composition constraint", 

63 ) 

64 def initial_composition_constraint(b, j): 

65 return ( 

66 b.initial_block[0].mole_frac_comp[j] 

67 == b.control_volume.properties_in[0].mole_frac_comp[j] 

68 ) 

69 

70 # The initial temperature, pressure, and flow amount is set by the user. 

71 self.initial_pressure = Var(initialize=101325, units=pyunit.Pa) 

72 @self.Constraint(doc="Initial pressure constraint") 

73 def initial_pressure_constraint(b): 

74 return b.initial_block[0].pressure == b.initial_pressure 

75 

76 self.initial_holdup = Var(initialize=300, units=pyunit.mol) 

77 @self.Constraint(doc="Initial flow constraint") 

78 def initial_holdup_mol_constraint(b): 

79 return ( 

80 b.initial_block[0].flow_mol * pyunit.s == b.initial_holdup 

81 ) # cancel out the seconds as we are using it for holdup not accumulation. 

82 

83 

84 self.initial_level = Var(initialize=300, units=pyunit.m) 

85 @self.Constraint(doc="Initial level constraint") 

86 def initial_level_constraint(b): 

87 return b.initial_holdup == b.tank_cross_sect_area * b.initial_level * b.initial_block[0].dens_mol 

88 

89 self.initial_temperature = Var(initialize=300, units=pyunit.K) 

90 @self.Constraint(doc="Initial temperature constraint") 

91 def initial_temperature_constraint(b): 

92 return b.initial_block[0].temperature == b.initial_temperature 

93 

94 # The temperature, pressure and flow are used to calculate the other properties. 

95 @self.Constraint( 

96 self.config.property_package.phase_list, 

97 self.config.property_package.component_list, 

98 doc="Defining accumulation", 

99 ) 

100 def initial_material_conditions_constraint(b, p, j): 

101 return ( 

102 b.initial_block[0].flow_mol 

103 * b.initial_block[0].mole_frac_phase_comp[p, j] 

104 + b.control_volume.material_accumulation[0, p, j] 

105 == b.control_volume.material_holdup[0, p, j] 

106 ) 

107 

108 @self.Constraint( 

109 self.config.property_package.phase_list, doc="Defining accumulation" 

110 ) 

111 def initial_energy_conditions_constraint(b, p): 

112 return ( 

113 b.initial_block[0].flow_mol 

114 * b.initial_block[0].phase_frac[p] 

115 * b.initial_block[0].enth_mol_phase[p] 

116 + b.control_volume.energy_accumulation[0, p] 

117 == b.control_volume.energy_holdup[0, p] 

118 ) 

119 

120 

121 

122 def initialize(self, *args, **kwargs): 

123 """ 

124 Initialization method for the DynamicTankData class. 

125 This method initializes the control volume and sets up the model. 

126 """ 

127 # Copy initial conditions from inverted properties 

128 initialise_inverted(self,"heat_duty") 

129 

130 if self.config.dynamic: 

131 self.initial_block.initialize() 

132 

133 super().initialize(*args, **kwargs)