ahuora-builder 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ahuora_builder/__init__.py +0 -0
- ahuora_builder/arc_manager.py +33 -0
- ahuora_builder/build_state.py +57 -0
- ahuora_builder/custom/PIDController.py +494 -0
- ahuora_builder/custom/PySMOModel.py +178 -0
- ahuora_builder/custom/SimpleEffectivenessHX_DH.py +727 -0
- ahuora_builder/custom/__init__.py +0 -0
- ahuora_builder/custom/add_initial_dynamics.py +35 -0
- ahuora_builder/custom/custom_compressor.py +107 -0
- ahuora_builder/custom/custom_cooler.py +33 -0
- ahuora_builder/custom/custom_heat_exchanger.py +183 -0
- ahuora_builder/custom/custom_heat_exchanger_1d.py +258 -0
- ahuora_builder/custom/custom_heater.py +41 -0
- ahuora_builder/custom/custom_pressure_changer.py +34 -0
- ahuora_builder/custom/custom_pump.py +107 -0
- ahuora_builder/custom/custom_separator.py +371 -0
- ahuora_builder/custom/custom_tank.py +133 -0
- ahuora_builder/custom/custom_turbine.py +132 -0
- ahuora_builder/custom/custom_valve.py +300 -0
- ahuora_builder/custom/custom_variable.py +29 -0
- ahuora_builder/custom/direct_steam_injection.py +371 -0
- ahuora_builder/custom/energy/__init__.py +0 -0
- ahuora_builder/custom/energy/acBus.py +280 -0
- ahuora_builder/custom/energy/ac_property_package.py +279 -0
- ahuora_builder/custom/energy/battery.py +170 -0
- ahuora_builder/custom/energy/bus.py +182 -0
- ahuora_builder/custom/energy/energy_mixer.py +195 -0
- ahuora_builder/custom/energy/energy_splitter.py +228 -0
- ahuora_builder/custom/energy/grid.py +173 -0
- ahuora_builder/custom/energy/hydro.py +169 -0
- ahuora_builder/custom/energy/link.py +137 -0
- ahuora_builder/custom/energy/load.py +155 -0
- ahuora_builder/custom/energy/mainDistributionBoard.py +257 -0
- ahuora_builder/custom/energy/power_property_package.py +253 -0
- ahuora_builder/custom/energy/solar.py +176 -0
- ahuora_builder/custom/energy/storage.py +230 -0
- ahuora_builder/custom/energy/storage_wrapper +0 -0
- ahuora_builder/custom/energy/tests/__init__.py +0 -0
- ahuora_builder/custom/energy/tests/test_bus.py +44 -0
- ahuora_builder/custom/energy/tests/test_energy_mixer.py +46 -0
- ahuora_builder/custom/energy/tests/test_mdb.py +49 -0
- ahuora_builder/custom/energy/transformer.py +187 -0
- ahuora_builder/custom/energy/transformer_property_package.py +267 -0
- ahuora_builder/custom/energy/transmissionLine.py +228 -0
- ahuora_builder/custom/energy/wind.py +206 -0
- ahuora_builder/custom/hda_ideal_VLE.py +1341 -0
- ahuora_builder/custom/hda_reaction.py +182 -0
- ahuora_builder/custom/heat_exchanger_1d_wrapper.py +31 -0
- ahuora_builder/custom/integration_block.py +106 -0
- ahuora_builder/custom/inverted.py +81 -0
- ahuora_builder/custom/performance_curves.py +1 -0
- ahuora_builder/custom/reactions/__init__.py +0 -0
- ahuora_builder/custom/reactions/hda_stoich.py +10 -0
- ahuora_builder/custom/simple_separator.py +680 -0
- ahuora_builder/custom/tests/__init__.py +0 -0
- ahuora_builder/custom/tests/test_SimpleEffectivenessHX_DH.py +91 -0
- ahuora_builder/custom/tests/test_custom_tank.py +70 -0
- ahuora_builder/custom/tests/test_direct_steam_injection.py +41 -0
- ahuora_builder/custom/tests/test_simple_separator.py +46 -0
- ahuora_builder/custom/tests/test_waterpipe.py +46 -0
- ahuora_builder/custom/thermal_utility_systems/desuperheater.py +624 -0
- ahuora_builder/custom/thermal_utility_systems/header.py +889 -0
- ahuora_builder/custom/thermal_utility_systems/simple_heat_pump.py +567 -0
- ahuora_builder/custom/thermal_utility_systems/steam_header.py +353 -0
- ahuora_builder/custom/thermal_utility_systems/steam_user.py +944 -0
- ahuora_builder/custom/thermal_utility_systems/temp.py +349 -0
- ahuora_builder/custom/thermal_utility_systems/tests/test_desuperheater.py +142 -0
- ahuora_builder/custom/thermal_utility_systems/tests/test_header.py +998 -0
- ahuora_builder/custom/thermal_utility_systems/tests/test_ntu_hx.py +129 -0
- ahuora_builder/custom/thermal_utility_systems/tests/test_simple_heat_pump.py +120 -0
- ahuora_builder/custom/thermal_utility_systems/tests/test_steam_header.py +703 -0
- ahuora_builder/custom/thermal_utility_systems/tests/test_steam_user.py +277 -0
- ahuora_builder/custom/thermal_utility_systems/tests/test_waterpipe.py +36 -0
- ahuora_builder/custom/thermal_utility_systems/tests/test_willans_turbine.py +253 -0
- ahuora_builder/custom/thermal_utility_systems/willans_turbine.py +804 -0
- ahuora_builder/custom/translator.py +129 -0
- ahuora_builder/custom/updated_pressure_changer.py +1404 -0
- ahuora_builder/custom/valve_wrapper.py +38 -0
- ahuora_builder/custom/water_tank_with_units.py +456 -0
- ahuora_builder/diagnostics/__init__.py +0 -0
- ahuora_builder/diagnostics/infeasibilities.py +40 -0
- ahuora_builder/diagnostics/tests/__init__.py +0 -0
- ahuora_builder/diagnostics/tests/test_infeasibilities.py +28 -0
- ahuora_builder/flowsheet_manager.py +542 -0
- ahuora_builder/flowsheet_manager_type.py +20 -0
- ahuora_builder/generate_python_file.py +440 -0
- ahuora_builder/methods/BlockContext.py +84 -0
- ahuora_builder/methods/__init__.py +0 -0
- ahuora_builder/methods/adapter.py +355 -0
- ahuora_builder/methods/adapter_library.py +549 -0
- ahuora_builder/methods/adapter_methods.py +80 -0
- ahuora_builder/methods/expression_parsing.py +105 -0
- ahuora_builder/methods/load_unit_model.py +147 -0
- ahuora_builder/methods/slice_manipulation.py +7 -0
- ahuora_builder/methods/tests/__init__.py +0 -0
- ahuora_builder/methods/tests/test_expression_parsing.py +15 -0
- ahuora_builder/methods/units_handler.py +129 -0
- ahuora_builder/ml_wizard.py +101 -0
- ahuora_builder/port_manager.py +20 -0
- ahuora_builder/properties_manager.py +44 -0
- ahuora_builder/property_package_manager.py +78 -0
- ahuora_builder/solver.py +38 -0
- ahuora_builder/tear_manager.py +98 -0
- ahuora_builder/tests/__init__.py +0 -0
- ahuora_builder/tests/test_generate_python_file/__init__.py +0 -0
- ahuora_builder/tests/test_generate_python_file/configurations/compressor_generated.py +63 -0
- ahuora_builder/tests/test_generate_python_file/configurations/heat_exchanger_generated.py +70 -0
- ahuora_builder/tests/test_generate_python_file/configurations/pump_generated.py +84 -0
- ahuora_builder/tests/test_generate_python_file/configurations/recycle_generated.py +73 -0
- ahuora_builder/tests/test_generate_python_file/test_generate_python_file.py +108 -0
- ahuora_builder/tests/test_solver/__init__.py +0 -0
- ahuora_builder/tests/test_solver/configurations/BT_PR.json +59 -0
- ahuora_builder/tests/test_solver/configurations/BT_PR_solved.json +59 -0
- ahuora_builder/tests/test_solver/configurations/bus.json +99 -0
- ahuora_builder/tests/test_solver/configurations/bus_solved.json +50 -0
- ahuora_builder/tests/test_solver/configurations/compound_separator.json +377 -0
- ahuora_builder/tests/test_solver/configurations/compound_separator_solved.json +374 -0
- ahuora_builder/tests/test_solver/configurations/compressor.json +38 -0
- ahuora_builder/tests/test_solver/configurations/compressor_solved.json +68 -0
- ahuora_builder/tests/test_solver/configurations/constraints.json +44 -0
- ahuora_builder/tests/test_solver/configurations/constraints_solved.json +59 -0
- ahuora_builder/tests/test_solver/configurations/control.json +39 -0
- ahuora_builder/tests/test_solver/configurations/control_solved.json +68 -0
- ahuora_builder/tests/test_solver/configurations/dynamic_tank.json +733 -0
- ahuora_builder/tests/test_solver/configurations/dynamic_tank_solved.json +846 -0
- ahuora_builder/tests/test_solver/configurations/elimination.json +39 -0
- ahuora_builder/tests/test_solver/configurations/elimination_solved.json +68 -0
- ahuora_builder/tests/test_solver/configurations/expressions.json +68 -0
- ahuora_builder/tests/test_solver/configurations/expressions_solved.json +104 -0
- ahuora_builder/tests/test_solver/configurations/header.json +1192 -0
- ahuora_builder/tests/test_solver/configurations/header_solved.json +761 -0
- ahuora_builder/tests/test_solver/configurations/heat_exchanger.json +63 -0
- ahuora_builder/tests/test_solver/configurations/heat_exchanger_solved.json +104 -0
- ahuora_builder/tests/test_solver/configurations/heat_pump.json +137 -0
- ahuora_builder/tests/test_solver/configurations/heat_pump_solved.json +104 -0
- ahuora_builder/tests/test_solver/configurations/machine_learning.json +2156 -0
- ahuora_builder/tests/test_solver/configurations/machine_learning_solved.json +266 -0
- ahuora_builder/tests/test_solver/configurations/mass_flow_tear.json +77 -0
- ahuora_builder/tests/test_solver/configurations/mass_flow_tear_solved.json +68 -0
- ahuora_builder/tests/test_solver/configurations/milk_heater.json +521 -0
- ahuora_builder/tests/test_solver/configurations/milk_heater_solved.json +311 -0
- ahuora_builder/tests/test_solver/configurations/mixer.json +44 -0
- ahuora_builder/tests/test_solver/configurations/mixer_solved.json +86 -0
- ahuora_builder/tests/test_solver/configurations/optimization.json +62 -0
- ahuora_builder/tests/test_solver/configurations/optimization_solved.json +59 -0
- ahuora_builder/tests/test_solver/configurations/propane_heat_pump.json +167 -0
- ahuora_builder/tests/test_solver/configurations/propane_heat_pump_solved.json +158 -0
- ahuora_builder/tests/test_solver/configurations/propane_recycle.json +141 -0
- ahuora_builder/tests/test_solver/configurations/propane_recycle_solved.json +104 -0
- ahuora_builder/tests/test_solver/configurations/pump.json +64 -0
- ahuora_builder/tests/test_solver/configurations/pump_solved.json +59 -0
- ahuora_builder/tests/test_solver/configurations/pump_unit_conversions.json +63 -0
- ahuora_builder/tests/test_solver/configurations/recycle.json +49 -0
- ahuora_builder/tests/test_solver/configurations/recycle_solved.json +50 -0
- ahuora_builder/tests/test_solver/configurations/sb_vapor_frac.json +29 -0
- ahuora_builder/tests/test_solver/configurations/sb_vapor_frac_solved.json +29 -0
- ahuora_builder/tests/test_solver/configurations/solar.json +67 -0
- ahuora_builder/tests/test_solver/configurations/solar_solved.json +50 -0
- ahuora_builder/tests/test_solver/configurations/vapor_frac_target.json +67 -0
- ahuora_builder/tests/test_solver/configurations/vapor_frac_target_solved.json +68 -0
- ahuora_builder/tests/test_solver/test_solve_models.py +250 -0
- ahuora_builder/timing.py +65 -0
- ahuora_builder/types/__init__.py +1 -0
- ahuora_builder/unit_model_manager.py +48 -0
- ahuora_builder-0.1.0.dist-info/METADATA +14 -0
- ahuora_builder-0.1.0.dist-info/RECORD +167 -0
- ahuora_builder-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,998 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from ahuora_builder.custom.thermal_utility_systems.header import simple_header
|
|
4
|
+
from ahuora_builder.custom.custom_heater import DynamicHeater
|
|
5
|
+
|
|
6
|
+
import pyomo.environ as pyo
|
|
7
|
+
from idaes.core import FlowsheetBlock
|
|
8
|
+
from idaes.core.util.model_statistics import degrees_of_freedom
|
|
9
|
+
from property_packages.build_package import build_package
|
|
10
|
+
from pyomo.network import Arc, SequentialDecomposition
|
|
11
|
+
import idaes.logger as idaeslog
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@pytest.fixture
|
|
15
|
+
def steam_header_base_case():
|
|
16
|
+
def _make_case(num_inlets=2, num_outlets=3):
|
|
17
|
+
# This defines the base case for all tests
|
|
18
|
+
m = pyo.ConcreteModel()
|
|
19
|
+
m.fs = FlowsheetBlock(dynamic=False)
|
|
20
|
+
m.fs.water = build_package("helmholtz", ["water"], ["Liq", "Vap"])
|
|
21
|
+
# Create Customsimple_header with 2 inlets and 3 outlets
|
|
22
|
+
m.fs.header = simple_header(
|
|
23
|
+
property_package=m.fs.water,
|
|
24
|
+
num_inlets=num_inlets,
|
|
25
|
+
num_outlets=num_outlets,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Set inlet conditions for inlet_1
|
|
29
|
+
m.fs.header.inlet_1.flow_mol.fix(1000) # mol/s
|
|
30
|
+
m.fs.header.inlet_1.pressure.fix(201325) # Pa
|
|
31
|
+
m.fs.header.inlet_1.enth_mol.fix(m.fs.water.htpx((300 + 273.15) * pyo.units.K, m.fs.header.inlet_1.pressure[0])) # J/mol
|
|
32
|
+
|
|
33
|
+
return m
|
|
34
|
+
return _make_case
|
|
35
|
+
|
|
36
|
+
def assert_header_solution(
|
|
37
|
+
m,
|
|
38
|
+
expected_total_flow,
|
|
39
|
+
expected_vent_flow,
|
|
40
|
+
expected_makeup_flow,
|
|
41
|
+
expected_liquid_flow,
|
|
42
|
+
has_heat_loss,
|
|
43
|
+
has_pressure_loss,
|
|
44
|
+
):
|
|
45
|
+
eps_rel = 1e-4
|
|
46
|
+
eps_abs = 0.1
|
|
47
|
+
# Check degrees of freedom
|
|
48
|
+
assert degrees_of_freedom(m) == 0
|
|
49
|
+
|
|
50
|
+
# Initialize the model
|
|
51
|
+
m.fs.header.initialize()
|
|
52
|
+
|
|
53
|
+
# Solve the model
|
|
54
|
+
opt = pyo.SolverFactory("ipopt")
|
|
55
|
+
results = opt.solve(m, tee=False)
|
|
56
|
+
|
|
57
|
+
# Check that solve was successful
|
|
58
|
+
assert results.solver.termination_condition == pyo.TerminationCondition.optimal
|
|
59
|
+
|
|
60
|
+
# Verify mass balance: total inlet flow = total vapor outlet flow + liquid outlet flow
|
|
61
|
+
balance_flow = pyo.value(m.fs.header.balance_flow_mol[0])
|
|
62
|
+
makeup_flow = max(-balance_flow, 0)
|
|
63
|
+
total_inlet_flow = sum([pyo.value(inlet_sb[0].flow_mol) for inlet_sb in m.fs.header.inlet_blocks]) + makeup_flow
|
|
64
|
+
total_outlet_flow = sum([pyo.value(outlet_sb[0].flow_mol) for outlet_sb in m.fs.header.outlet_blocks])
|
|
65
|
+
total_outlet_supply = sum([pyo.value(outlet_sb[0].flow_mol) for outlet_sb in m.fs.header._outlet_supply_blocks])
|
|
66
|
+
condensate_flow = pyo.value(m.fs.header.outlet_condensate_state[0].flow_mol)
|
|
67
|
+
vent_flow = pyo.value(m.fs.header.outlet_vent_state[0].flow_mol)
|
|
68
|
+
|
|
69
|
+
assert expected_total_flow == pytest.approx(total_outlet_flow, rel=eps_rel, abs=eps_abs)
|
|
70
|
+
assert expected_total_flow == pytest.approx(total_inlet_flow, rel=eps_rel, abs=eps_abs)
|
|
71
|
+
|
|
72
|
+
assert expected_liquid_flow == pytest.approx(condensate_flow, rel=eps_rel, abs=0.02)
|
|
73
|
+
assert expected_vent_flow == pytest.approx(vent_flow, rel=eps_rel, abs=0.02)
|
|
74
|
+
assert expected_makeup_flow == pytest.approx(makeup_flow, rel=eps_rel, abs=0.02)
|
|
75
|
+
|
|
76
|
+
# Verify pressure drop is applied correctly
|
|
77
|
+
if pyo.value(m.fs.header.inlet_1.pressure[0]) > eps_rel:
|
|
78
|
+
expected_min_inlet_pressure = pyo.value(m.fs.header.inlet_1.pressure[0])
|
|
79
|
+
else:
|
|
80
|
+
expected_min_inlet_pressure = pyo.value(m.fs.header.inlet_1.pressure[0])
|
|
81
|
+
expected_outlet_pressure = expected_min_inlet_pressure - pyo.value(m.fs.header.pressure_loss[0]) # Accounts for pressure drop
|
|
82
|
+
|
|
83
|
+
assert pyo.value(m.fs.header.outlet_1.pressure[0]) == pytest.approx(expected_outlet_pressure, rel=eps_rel, abs=eps_abs)
|
|
84
|
+
assert pyo.value(m.fs.header.outlet_vent.pressure[0]) == pytest.approx(expected_outlet_pressure, rel=eps_rel, abs=eps_abs)
|
|
85
|
+
|
|
86
|
+
assert pyo.value(m.fs.header.outlet_1.pressure[0]) == pytest.approx(pyo.value(m.fs.header.outlet_condensate.pressure[0]), rel=eps_rel, abs=eps_abs)
|
|
87
|
+
assert pyo.value(m.fs.header.outlet_vent.pressure[0]) == pytest.approx(pyo.value(m.fs.header.outlet_condensate.pressure[0]), rel=eps_rel, abs=eps_abs)
|
|
88
|
+
|
|
89
|
+
# Verify molar enthalpy relationships
|
|
90
|
+
if m.fs.header.config.is_liquid_header:
|
|
91
|
+
assert pyo.value(m.fs.header.outlet_1.enth_mol[0]) == pytest.approx(pyo.value(m.fs.header.outlet_condensate.enth_mol[0]), rel=eps_rel, abs=eps_abs)
|
|
92
|
+
else:
|
|
93
|
+
assert pyo.value(m.fs.header.outlet_1.enth_mol[0]) == pytest.approx(pyo.value(m.fs.header.outlet_vent.enth_mol[0]), rel=eps_rel, abs=eps_abs)
|
|
94
|
+
|
|
95
|
+
hV = pyo.value(m.fs.header.outlet_1.enth_mol[0]) # J/mol
|
|
96
|
+
hL = pyo.value(m.fs.header.outlet_condensate.enth_mol[0]) # J/mol
|
|
97
|
+
if pyo.value(m.fs.header.outlet_condensate.flow_mol[0]) > eps_abs:
|
|
98
|
+
assert hV >= hL
|
|
99
|
+
|
|
100
|
+
# Verify cooling effect
|
|
101
|
+
inlet_total_energy = pyo.value(
|
|
102
|
+
sum(
|
|
103
|
+
i[0].flow_mol * i[0].enth_mol
|
|
104
|
+
for i in m.fs.header.inlet_blocks
|
|
105
|
+
)
|
|
106
|
+
*
|
|
107
|
+
(m.fs.header.mixed_state[0].flow_mol + makeup_flow) / m.fs.header.mixed_state[0].flow_mol
|
|
108
|
+
)
|
|
109
|
+
outlet_total_energy = pyo.value(
|
|
110
|
+
sum(
|
|
111
|
+
i[0].flow_mol * i[0].enth_mol
|
|
112
|
+
for i in m.fs.header.outlet_blocks
|
|
113
|
+
)
|
|
114
|
+
+
|
|
115
|
+
m.fs.header.heat_loss[0]
|
|
116
|
+
)
|
|
117
|
+
assert outlet_total_energy == pytest.approx(inlet_total_energy, rel=eps_rel, abs=eps_abs)
|
|
118
|
+
|
|
119
|
+
# Test that ports exist
|
|
120
|
+
assert hasattr(m.fs.header, 'outlet_condensate')
|
|
121
|
+
assert hasattr(m.fs.header, 'inlet_1')
|
|
122
|
+
assert hasattr(m.fs.header, 'outlet_1')
|
|
123
|
+
assert hasattr(m.fs.header, 'outlet_vent')
|
|
124
|
+
|
|
125
|
+
inlet_mass_list = [pyo.value(i[0].flow_mass) for i in m.fs.header.inlet_blocks] + [ pyo.value(makeup_flow * 0.01801528)] # convert mol/s to kg/s
|
|
126
|
+
outlet_mass_list = [pyo.value(i[0].flow_mass) for i in m.fs.header.outlet_blocks]
|
|
127
|
+
# Test all mass flows are non-zero and sum of inlet and outlet masses is approx same
|
|
128
|
+
assert sum(inlet_mass_list) == pytest.approx(sum(outlet_mass_list), rel=eps_rel, abs=eps_abs)
|
|
129
|
+
|
|
130
|
+
min_inlet_pressure = min([pyo.value(i[0].pressure) for i in m.fs.header.inlet_blocks])
|
|
131
|
+
min_outlet_pressure = min([pyo.value(i[0].pressure) for i in m.fs.header.outlet_blocks])
|
|
132
|
+
if total_outlet_flow > eps_abs:
|
|
133
|
+
assert min_outlet_pressure == pytest.approx(min_inlet_pressure - pyo.value(m.fs.header.pressure_loss[0]), rel=eps_rel, abs=eps_abs)
|
|
134
|
+
|
|
135
|
+
# Test minimum input pressure is >= outlet pressure and all pressure are non-zero
|
|
136
|
+
assert min_inlet_pressure >= min_outlet_pressure - eps_abs
|
|
137
|
+
assert min_inlet_pressure > eps_abs
|
|
138
|
+
assert min_outlet_pressure > eps_abs
|
|
139
|
+
|
|
140
|
+
def test_header_with_inlet_greater_than_outlet_flow(steam_header_base_case):
|
|
141
|
+
# Case where there is zero liquid flow and there is more known inlet than outlet flows
|
|
142
|
+
m = steam_header_base_case(num_inlets=1, num_outlets=1)
|
|
143
|
+
|
|
144
|
+
# Set user steam demand
|
|
145
|
+
m.fs.header.outlet_1.flow_mol.fix(800) # mol/s
|
|
146
|
+
# Set cooler specifications
|
|
147
|
+
m.fs.header.heat_loss[0].fix(1000) # W -> heat loss
|
|
148
|
+
m.fs.header.pressure_loss[0].fix(1000) # Pa -> pressure loss
|
|
149
|
+
|
|
150
|
+
assert_header_solution(
|
|
151
|
+
m,
|
|
152
|
+
expected_total_flow=1000,
|
|
153
|
+
expected_vent_flow=200,
|
|
154
|
+
expected_makeup_flow=0,
|
|
155
|
+
expected_liquid_flow=0,
|
|
156
|
+
has_heat_loss=True if pyo.value(m.fs.header.heat_loss[0]) > 0 else False,
|
|
157
|
+
has_pressure_loss=True if pyo.value(m.fs.header.pressure_loss[0]) > 0 else False,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
def test_header_with_condensate_in_inlet_flow(steam_header_base_case):
|
|
161
|
+
# Case where there is zero liquid flow and there is more known inlet than outlet flows
|
|
162
|
+
m = steam_header_base_case(num_inlets=1, num_outlets=1)
|
|
163
|
+
|
|
164
|
+
h_val = pyo.value(m.fs.water.htpx(p=201325 * pyo.units.Pa, x=0.9))
|
|
165
|
+
m.fs.header.inlet_1.enth_mol.fix(h_val) # J/mol
|
|
166
|
+
|
|
167
|
+
# Set user steam demand
|
|
168
|
+
m.fs.header.outlet_1.flow_mol.fix(800) # mol/s
|
|
169
|
+
# Set cooler specifications
|
|
170
|
+
m.fs.header.heat_loss[0].fix(0) # W -> heat loss
|
|
171
|
+
m.fs.header.pressure_loss[0].fix(0) # Pa -> pressure loss
|
|
172
|
+
|
|
173
|
+
assert_header_solution(
|
|
174
|
+
m,
|
|
175
|
+
expected_total_flow=1000,
|
|
176
|
+
expected_vent_flow=100,
|
|
177
|
+
expected_makeup_flow=0,
|
|
178
|
+
expected_liquid_flow=100,
|
|
179
|
+
has_heat_loss=True if pyo.value(m.fs.header.heat_loss[0]) > 0 else False,
|
|
180
|
+
has_pressure_loss=True if pyo.value(m.fs.header.pressure_loss[0]) > 0 else False,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
def test_header_with_inlet_equal_to_outlet_flow(steam_header_base_case):
|
|
184
|
+
# Case where there is zero liquid flow and the header is balanced
|
|
185
|
+
m = steam_header_base_case(num_inlets=1, num_outlets=1)
|
|
186
|
+
# Set user steam demand
|
|
187
|
+
m.fs.header.outlet_1.flow_mol.fix(1000) # mol/s
|
|
188
|
+
# Set cooler specifications
|
|
189
|
+
m.fs.header.heat_loss[0].fix(0) # W -> heat loss
|
|
190
|
+
m.fs.header.pressure_loss[0].fix(0) # Pa -> pressure loss
|
|
191
|
+
|
|
192
|
+
assert_header_solution(
|
|
193
|
+
m,
|
|
194
|
+
expected_total_flow=1000,
|
|
195
|
+
expected_vent_flow=0,
|
|
196
|
+
expected_makeup_flow=0,
|
|
197
|
+
expected_liquid_flow=0,
|
|
198
|
+
has_heat_loss=True if pyo.value(m.fs.header.heat_loss[0]) > 0 else False,
|
|
199
|
+
has_pressure_loss=True if pyo.value(m.fs.header.pressure_loss[0]) > 0 else False,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
def test_header_with_inlet_less_than_outlet_flow(steam_header_base_case):
|
|
203
|
+
# Case where there is zero liquid flow and the header is balanced
|
|
204
|
+
m = steam_header_base_case(num_inlets=1, num_outlets=1)
|
|
205
|
+
# Set user steam demand
|
|
206
|
+
m.fs.header.outlet_1.flow_mol.fix(1200) # mol/s
|
|
207
|
+
# Set cooler specifications
|
|
208
|
+
m.fs.header.heat_loss[0].fix(0) # W -> heat loss
|
|
209
|
+
m.fs.header.pressure_loss[0].fix(0) # Pa -> pressure loss
|
|
210
|
+
|
|
211
|
+
assert_header_solution(
|
|
212
|
+
m,
|
|
213
|
+
expected_total_flow=1200,
|
|
214
|
+
expected_vent_flow=0,
|
|
215
|
+
expected_makeup_flow=200,
|
|
216
|
+
expected_liquid_flow=0,
|
|
217
|
+
has_heat_loss=True if pyo.value(m.fs.header.heat_loss[0]) > 0 else False,
|
|
218
|
+
has_pressure_loss=True if pyo.value(m.fs.header.pressure_loss[0]) > 0 else False,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
def test_header_with_zero_flow(steam_header_base_case):
|
|
222
|
+
# Case where there is zero liquid flow and the header is balanced
|
|
223
|
+
m = steam_header_base_case(num_inlets=1, num_outlets=1)
|
|
224
|
+
m.fs.header.inlet_1.flow_mol.fix(0) # mol/s
|
|
225
|
+
# Set user steam demand
|
|
226
|
+
m.fs.header.outlet_1.flow_mol.fix(0) # mol/s
|
|
227
|
+
# Set cooler specifications
|
|
228
|
+
m.fs.header.heat_loss[0].fix(0) # W -> heat loss
|
|
229
|
+
m.fs.header.pressure_loss[0].fix(0) # Pa -> pressure loss
|
|
230
|
+
|
|
231
|
+
assert_header_solution(
|
|
232
|
+
m,
|
|
233
|
+
expected_total_flow=0,
|
|
234
|
+
expected_vent_flow=0,
|
|
235
|
+
expected_makeup_flow=0,
|
|
236
|
+
expected_liquid_flow=0,
|
|
237
|
+
has_heat_loss=True if pyo.value(m.fs.header.heat_loss[0]) > 0 else False,
|
|
238
|
+
has_pressure_loss=True if pyo.value(m.fs.header.pressure_loss[0]) > 0 else False,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
def test_header_with_inlet_greater_than_three_defined_outlet_flows(steam_header_base_case):
|
|
242
|
+
# Case where there is zero liquid flow and there is more known inlet than outlet flows
|
|
243
|
+
m = steam_header_base_case(num_inlets=1, num_outlets=3)
|
|
244
|
+
|
|
245
|
+
# Set user steam demand
|
|
246
|
+
m.fs.header.outlet_1.flow_mol.fix(500) # mol/s
|
|
247
|
+
m.fs.header.outlet_2.flow_mol.fix(100) # mol/s
|
|
248
|
+
m.fs.header.outlet_3.flow_mol.fix(200) # mol/s
|
|
249
|
+
|
|
250
|
+
# Set cooler specifications
|
|
251
|
+
m.fs.header.heat_loss[0].fix(1000) # W -> heat loss
|
|
252
|
+
m.fs.header.pressure_loss[0].fix(1000) # Pa -> pressure loss
|
|
253
|
+
|
|
254
|
+
assert_header_solution(
|
|
255
|
+
m,
|
|
256
|
+
expected_total_flow=1000,
|
|
257
|
+
expected_vent_flow=200,
|
|
258
|
+
expected_makeup_flow=0,
|
|
259
|
+
expected_liquid_flow=0,
|
|
260
|
+
has_heat_loss=True if pyo.value(m.fs.header.heat_loss[0]) > 0 else False,
|
|
261
|
+
has_pressure_loss=True if pyo.value(m.fs.header.pressure_loss[0]) > 0 else False,
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
def test_header_with_inlet_less_than_two_defined_outlet_flows(steam_header_base_case):
|
|
265
|
+
# Case where there is zero liquid flow and the header is balanced
|
|
266
|
+
m = steam_header_base_case(num_inlets=1, num_outlets=2)
|
|
267
|
+
# Set user steam demand
|
|
268
|
+
m.fs.header.outlet_1.flow_mol.fix(600) # mol/s
|
|
269
|
+
m.fs.header.outlet_2.flow_mol.fix(600) # mol/s
|
|
270
|
+
# Set cooler specifications
|
|
271
|
+
m.fs.header.heat_loss[0].fix(0) # W -> heat loss
|
|
272
|
+
m.fs.header.pressure_loss[0].fix(0) # Pa -> pressure loss
|
|
273
|
+
|
|
274
|
+
assert_header_solution(
|
|
275
|
+
m,
|
|
276
|
+
expected_total_flow=1200,
|
|
277
|
+
expected_vent_flow=0,
|
|
278
|
+
expected_makeup_flow=200,
|
|
279
|
+
expected_liquid_flow=0,
|
|
280
|
+
has_heat_loss=True if pyo.value(m.fs.header.heat_loss[0]) > 0 else False,
|
|
281
|
+
has_pressure_loss=True if pyo.value(m.fs.header.pressure_loss[0]) > 0 else False,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
def test_header_with_two_defined_inlet_less_than_two_defined_outlet_flows(steam_header_base_case):
|
|
285
|
+
# Case where there is zero liquid flow and the header is balanced
|
|
286
|
+
m = steam_header_base_case(num_inlets=2, num_outlets=2)
|
|
287
|
+
m.fs.header.inlet_2.flow_mol.fix(100) # mol/s
|
|
288
|
+
m.fs.header.inlet_2.pressure.fix(221325) # Pa
|
|
289
|
+
m.fs.header.inlet_2.enth_mol.fix(m.fs.water.htpx((310 + 273.15) * pyo.units.K, m.fs.header.inlet_1.pressure[0])) # J/mol
|
|
290
|
+
|
|
291
|
+
# Set user steam demand
|
|
292
|
+
m.fs.header.outlet_1.flow_mol.fix(600) # mol/s
|
|
293
|
+
m.fs.header.outlet_2.flow_mol.fix(600) # mol/s
|
|
294
|
+
# Set cooler specifications
|
|
295
|
+
m.fs.header.heat_loss[0].fix(0) # W -> heat loss
|
|
296
|
+
m.fs.header.pressure_loss[0].fix(0) # Pa -> pressure loss
|
|
297
|
+
|
|
298
|
+
assert_header_solution(
|
|
299
|
+
m,
|
|
300
|
+
expected_total_flow=1200,
|
|
301
|
+
expected_vent_flow=0,
|
|
302
|
+
expected_makeup_flow=100,
|
|
303
|
+
expected_liquid_flow=0,
|
|
304
|
+
has_heat_loss=True if pyo.value(m.fs.header.heat_loss[0]) > 0 else False,
|
|
305
|
+
has_pressure_loss=True if pyo.value(m.fs.header.pressure_loss[0]) > 0 else False,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
def test_sequential_decomposition():
|
|
309
|
+
m = pyo.ConcreteModel()
|
|
310
|
+
m.fs = FlowsheetBlock(dynamic=False)
|
|
311
|
+
m.fs.water = build_package("helmholtz", ["water"], ["Liq", "Vap"])
|
|
312
|
+
|
|
313
|
+
m.fs.heater = DynamicHeater(property_package=m.fs.water)
|
|
314
|
+
m.fs.heater.inlet.flow_mol.fix(100)
|
|
315
|
+
m.fs.heater.inlet.enth_mol.fix(
|
|
316
|
+
m.fs.water.htpx((25 + 273.15) * pyo.units.K, 201325 * pyo.units.Pa)
|
|
317
|
+
) # J/mol
|
|
318
|
+
m.fs.heater.inlet.pressure.fix(101325)
|
|
319
|
+
m.fs.heater.heat_duty.fix(5000)
|
|
320
|
+
|
|
321
|
+
# Create Customsimple_header with 2 inlets and 3 outlets
|
|
322
|
+
m.fs.header = simple_header(
|
|
323
|
+
property_package=m.fs.water,
|
|
324
|
+
num_inlets=1,
|
|
325
|
+
num_outlets=1,
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
# Set user steam demand
|
|
329
|
+
m.fs.header.outlet_1.flow_mol.fix(600) # mol/s
|
|
330
|
+
# Set cooler specifications
|
|
331
|
+
m.fs.header.heat_loss[0].fix(0) # W -> heat loss
|
|
332
|
+
m.fs.header.pressure_loss[0].fix(0) # Pa -> pressure loss
|
|
333
|
+
|
|
334
|
+
m.fs.heater_to_header = Arc(
|
|
335
|
+
source=m.fs.heater.outlet, destination=m.fs.header.inlet_1
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
pyo.TransformationFactory("network.expand_arcs").apply_to(m)
|
|
339
|
+
|
|
340
|
+
assert degrees_of_freedom(m) == 0
|
|
341
|
+
|
|
342
|
+
# create Sequential Decomposition object
|
|
343
|
+
seq = SequentialDecomposition()
|
|
344
|
+
seq.options.select_tear_method = "heuristic"
|
|
345
|
+
seq.options.tear_method = "Wegstein"
|
|
346
|
+
seq.options.iterLim = 1
|
|
347
|
+
|
|
348
|
+
# create computation graph
|
|
349
|
+
G = seq.create_graph(m)
|
|
350
|
+
heuristic_tear_set = seq.tear_set_arcs(G, method="heuristic")
|
|
351
|
+
# get calculation order
|
|
352
|
+
order = seq.calculation_order(G)
|
|
353
|
+
|
|
354
|
+
for o in heuristic_tear_set:
|
|
355
|
+
print(o.name)
|
|
356
|
+
|
|
357
|
+
print("Initialization order:")
|
|
358
|
+
for o in order:
|
|
359
|
+
print(o[0].name)
|
|
360
|
+
|
|
361
|
+
# define unit initialisation function
|
|
362
|
+
def init_unit(unit):
|
|
363
|
+
unit.initialize()
|
|
364
|
+
|
|
365
|
+
# run sequential decomposition
|
|
366
|
+
seq.run(m, init_unit)
|
|
367
|
+
|
|
368
|
+
def test_header_with_inlet_steam_generator():
|
|
369
|
+
m = pyo.ConcreteModel()
|
|
370
|
+
m.fs = FlowsheetBlock(dynamic=False)
|
|
371
|
+
m.fs.water = build_package("helmholtz", ["water"], ["Liq", "Vap"])
|
|
372
|
+
|
|
373
|
+
# Add a heater before the header
|
|
374
|
+
m.fs.boiler = DynamicHeater(
|
|
375
|
+
property_package=m.fs.water,
|
|
376
|
+
has_pressure_change=True,
|
|
377
|
+
)
|
|
378
|
+
# Create Customsimple_header with 2 inlets and 3 outlets
|
|
379
|
+
m.fs.header = simple_header(
|
|
380
|
+
property_package=m.fs.water,
|
|
381
|
+
num_inlets=1,
|
|
382
|
+
num_outlets=1
|
|
383
|
+
)
|
|
384
|
+
# Connect heater outlet to header inlet_1
|
|
385
|
+
m.boiler_to_header_arc = Arc(
|
|
386
|
+
source=m.fs.boiler.outlet, destination=m.fs.header.inlet_1
|
|
387
|
+
)
|
|
388
|
+
pyo.TransformationFactory("network.expand_arcs").apply_to(m)
|
|
389
|
+
|
|
390
|
+
# Set inlet conditions for inlet_1
|
|
391
|
+
m.fs.boiler.inlet.flow_mol.fix(1000) # mol/s
|
|
392
|
+
m.fs.boiler.inlet.pressure.fix(200 * 1000) # Pa
|
|
393
|
+
m.fs.boiler.inlet.enth_mol.fix(
|
|
394
|
+
m.fs.water.htpx(
|
|
395
|
+
(15 + 273.15) * pyo.units.K,
|
|
396
|
+
m.fs.boiler.inlet.pressure[0]
|
|
397
|
+
)
|
|
398
|
+
) # J/mol
|
|
399
|
+
m.fs.boiler.heat_duty.fix(
|
|
400
|
+
50.577 * 1000 * 1000
|
|
401
|
+
)
|
|
402
|
+
m.fs.boiler.deltaP.fix(
|
|
403
|
+
0.0
|
|
404
|
+
)
|
|
405
|
+
m.fs.header.outlet_1.flow_mol.fix(500)
|
|
406
|
+
m.fs.header.heat_loss.fix(
|
|
407
|
+
0.0
|
|
408
|
+
)
|
|
409
|
+
m.fs.header.pressure_loss.fix(
|
|
410
|
+
0.0
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
# Initialize the heater first
|
|
414
|
+
seq = SequentialDecomposition()
|
|
415
|
+
seq.options.select_tear_method = "heuristic"
|
|
416
|
+
seq.options.tear_method = "Wegstein"
|
|
417
|
+
seq.options.iterLim = 1
|
|
418
|
+
|
|
419
|
+
# create computation graph
|
|
420
|
+
G = seq.create_graph(m)
|
|
421
|
+
heuristic_tear_set = seq.tear_set_arcs(G, method="heuristic")
|
|
422
|
+
# get calculation order
|
|
423
|
+
order = seq.calculation_order(G)
|
|
424
|
+
|
|
425
|
+
for o in heuristic_tear_set:
|
|
426
|
+
print(o.name)
|
|
427
|
+
|
|
428
|
+
for o in order:
|
|
429
|
+
print(o[0].name)
|
|
430
|
+
|
|
431
|
+
# define unit initialisation function
|
|
432
|
+
def init_unit(unit):
|
|
433
|
+
unit.initialize()
|
|
434
|
+
|
|
435
|
+
# run sequential decomposition
|
|
436
|
+
seq.run(m, init_unit)
|
|
437
|
+
|
|
438
|
+
dof = degrees_of_freedom(m)
|
|
439
|
+
|
|
440
|
+
opt = pyo.SolverFactory("ipopt_v2")
|
|
441
|
+
results = opt.solve(m, tee=False)
|
|
442
|
+
|
|
443
|
+
assert results.solver.termination_condition == pyo.TerminationCondition.optimal
|
|
444
|
+
|
|
445
|
+
assert_header_solution(
|
|
446
|
+
m=m,
|
|
447
|
+
expected_total_flow=1000,
|
|
448
|
+
expected_vent_flow=500,
|
|
449
|
+
expected_makeup_flow=0,
|
|
450
|
+
expected_liquid_flow=0,
|
|
451
|
+
has_heat_loss=0,
|
|
452
|
+
has_pressure_loss=0,
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
def test_header_with_inlet_steam_generator_with_insufficent_flow():
|
|
456
|
+
m = pyo.ConcreteModel()
|
|
457
|
+
m.fs = FlowsheetBlock(dynamic=False)
|
|
458
|
+
m.fs.water = build_package("helmholtz", ["water"], ["Liq", "Vap"])
|
|
459
|
+
|
|
460
|
+
# Add a heater before the header
|
|
461
|
+
m.fs.boiler = DynamicHeater(
|
|
462
|
+
property_package=m.fs.water,
|
|
463
|
+
has_pressure_change=True,
|
|
464
|
+
)
|
|
465
|
+
# Create Customsimple_header with 2 inlets and 3 outlets
|
|
466
|
+
m.fs.header = simple_header(
|
|
467
|
+
property_package=m.fs.water,
|
|
468
|
+
num_inlets=1,
|
|
469
|
+
num_outlets=1
|
|
470
|
+
)
|
|
471
|
+
# Connect heater outlet to header inlet_1
|
|
472
|
+
m.boiler_to_header_arc = Arc(
|
|
473
|
+
source=m.fs.boiler.outlet, destination=m.fs.header.inlet_1
|
|
474
|
+
)
|
|
475
|
+
pyo.TransformationFactory("network.expand_arcs").apply_to(m)
|
|
476
|
+
|
|
477
|
+
# Set inlet conditions for inlet_1
|
|
478
|
+
m.fs.boiler.inlet.flow_mol.fix(1000) # mol/s
|
|
479
|
+
m.fs.boiler.inlet.pressure.fix(200 * 1000) # Pa
|
|
480
|
+
m.fs.boiler.inlet.enth_mol.fix(
|
|
481
|
+
m.fs.water.htpx(
|
|
482
|
+
(15 + 273.15) * pyo.units.K,
|
|
483
|
+
m.fs.boiler.inlet.pressure[0]
|
|
484
|
+
)
|
|
485
|
+
) # J/mol
|
|
486
|
+
m.fs.boiler.heat_duty.fix(
|
|
487
|
+
50.577 * 1000 * 1000
|
|
488
|
+
)
|
|
489
|
+
m.fs.boiler.deltaP.fix(
|
|
490
|
+
0.0
|
|
491
|
+
)
|
|
492
|
+
m.fs.header.outlet_1.flow_mol.fix(1500)
|
|
493
|
+
m.fs.header.heat_loss.fix(
|
|
494
|
+
0.0
|
|
495
|
+
)
|
|
496
|
+
m.fs.header.pressure_loss.fix(
|
|
497
|
+
0.0
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
# Initialize the heater first
|
|
501
|
+
seq = SequentialDecomposition()
|
|
502
|
+
seq.options.select_tear_method = "heuristic"
|
|
503
|
+
seq.options.tear_method = "Wegstein"
|
|
504
|
+
seq.options.iterLim = 1
|
|
505
|
+
|
|
506
|
+
# create computation graph
|
|
507
|
+
G = seq.create_graph(m)
|
|
508
|
+
heuristic_tear_set = seq.tear_set_arcs(G, method="heuristic")
|
|
509
|
+
# get calculation order
|
|
510
|
+
order = seq.calculation_order(G)
|
|
511
|
+
|
|
512
|
+
for o in heuristic_tear_set:
|
|
513
|
+
print(o.name)
|
|
514
|
+
|
|
515
|
+
for o in order:
|
|
516
|
+
print(o[0].name)
|
|
517
|
+
|
|
518
|
+
# define unit initialisation function
|
|
519
|
+
def init_unit(unit):
|
|
520
|
+
unit.initialize()
|
|
521
|
+
|
|
522
|
+
# run sequential decomposition
|
|
523
|
+
seq.run(m, init_unit)
|
|
524
|
+
|
|
525
|
+
opt = pyo.SolverFactory("ipopt_v2")
|
|
526
|
+
results = opt.solve(m, tee=False)
|
|
527
|
+
|
|
528
|
+
assert results.solver.termination_condition == pyo.TerminationCondition.optimal
|
|
529
|
+
|
|
530
|
+
assert_header_solution(
|
|
531
|
+
m=m,
|
|
532
|
+
expected_total_flow=1500,
|
|
533
|
+
expected_vent_flow=0,
|
|
534
|
+
expected_makeup_flow=500,
|
|
535
|
+
expected_liquid_flow=0,
|
|
536
|
+
has_heat_loss=0,
|
|
537
|
+
has_pressure_loss=0,
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
def test_header_with_inlet_steam_generator_with_losses():
|
|
541
|
+
m = pyo.ConcreteModel()
|
|
542
|
+
m.fs = FlowsheetBlock(dynamic=False)
|
|
543
|
+
m.fs.water = build_package("helmholtz", ["water"], ["Liq", "Vap"])
|
|
544
|
+
|
|
545
|
+
# Add a heater before the header
|
|
546
|
+
m.fs.boiler = DynamicHeater(
|
|
547
|
+
property_package=m.fs.water,
|
|
548
|
+
has_pressure_change=True,
|
|
549
|
+
)
|
|
550
|
+
# Create Customsimple_header with 2 inlets and 3 outlets
|
|
551
|
+
m.fs.header = simple_header(
|
|
552
|
+
property_package=m.fs.water,
|
|
553
|
+
num_inlets=1,
|
|
554
|
+
num_outlets=1
|
|
555
|
+
)
|
|
556
|
+
# Connect heater outlet to header inlet_1
|
|
557
|
+
m.boiler_to_header_arc = Arc(
|
|
558
|
+
source=m.fs.boiler.outlet, destination=m.fs.header.inlet_1
|
|
559
|
+
)
|
|
560
|
+
pyo.TransformationFactory("network.expand_arcs").apply_to(m)
|
|
561
|
+
|
|
562
|
+
# Set inlet conditions for inlet_1
|
|
563
|
+
m.fs.boiler.inlet.flow_mol.fix(1000) # mol/s
|
|
564
|
+
m.fs.boiler.inlet.pressure.fix(200 * 1000) # Pa
|
|
565
|
+
m.fs.boiler.inlet.enth_mol.fix(
|
|
566
|
+
m.fs.water.htpx(
|
|
567
|
+
(15 + 273.15) * pyo.units.K,
|
|
568
|
+
m.fs.boiler.inlet.pressure[0]
|
|
569
|
+
)
|
|
570
|
+
) # J/mol
|
|
571
|
+
m.fs.boiler.heat_duty.fix(
|
|
572
|
+
50.577 * 1000 * 1000
|
|
573
|
+
)
|
|
574
|
+
m.fs.boiler.deltaP.fix(
|
|
575
|
+
0.0
|
|
576
|
+
)
|
|
577
|
+
m.fs.header.outlet_1.flow_mol.fix(500)
|
|
578
|
+
m.fs.header.heat_loss.fix(
|
|
579
|
+
10.0
|
|
580
|
+
)
|
|
581
|
+
m.fs.header.pressure_loss.fix(
|
|
582
|
+
5000.0
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
# Initialize the heater first
|
|
586
|
+
seq = SequentialDecomposition()
|
|
587
|
+
seq.options.select_tear_method = "heuristic"
|
|
588
|
+
seq.options.tear_method = "Wegstein"
|
|
589
|
+
seq.options.iterLim = 1
|
|
590
|
+
|
|
591
|
+
# create computation graph
|
|
592
|
+
G = seq.create_graph(m)
|
|
593
|
+
heuristic_tear_set = seq.tear_set_arcs(G, method="heuristic")
|
|
594
|
+
# get calculation order
|
|
595
|
+
order = seq.calculation_order(G)
|
|
596
|
+
|
|
597
|
+
for o in heuristic_tear_set:
|
|
598
|
+
print(o.name)
|
|
599
|
+
|
|
600
|
+
for o in order:
|
|
601
|
+
print(o[0].name)
|
|
602
|
+
|
|
603
|
+
# define unit initialisation function
|
|
604
|
+
def init_unit(unit):
|
|
605
|
+
unit.initialize()
|
|
606
|
+
|
|
607
|
+
# run sequential decomposition
|
|
608
|
+
seq.run(m, init_unit)
|
|
609
|
+
|
|
610
|
+
opt = pyo.SolverFactory("ipopt_v2")
|
|
611
|
+
results = opt.solve(m, tee=False)
|
|
612
|
+
|
|
613
|
+
assert results.solver.termination_condition == pyo.TerminationCondition.optimal
|
|
614
|
+
|
|
615
|
+
assert_header_solution(
|
|
616
|
+
m=m,
|
|
617
|
+
expected_total_flow=1000,
|
|
618
|
+
expected_vent_flow=500,
|
|
619
|
+
expected_makeup_flow=0,
|
|
620
|
+
expected_liquid_flow=0,
|
|
621
|
+
has_heat_loss=True,
|
|
622
|
+
has_pressure_loss=True,
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
def test_header_with_inlet_steam_generator_with_wet_steam():
|
|
626
|
+
m = pyo.ConcreteModel()
|
|
627
|
+
m.fs = FlowsheetBlock(dynamic=False)
|
|
628
|
+
m.fs.water = build_package("helmholtz", ["water"], ["Liq", "Vap"])
|
|
629
|
+
|
|
630
|
+
# Add a heater before the header
|
|
631
|
+
m.fs.boiler = DynamicHeater(
|
|
632
|
+
property_package=m.fs.water,
|
|
633
|
+
has_pressure_change=True,
|
|
634
|
+
)
|
|
635
|
+
# Create Customsimple_header with 2 inlets and 3 outlets
|
|
636
|
+
m.fs.header = simple_header(
|
|
637
|
+
property_package=m.fs.water,
|
|
638
|
+
num_inlets=1,
|
|
639
|
+
num_outlets=1
|
|
640
|
+
)
|
|
641
|
+
# Connect heater outlet to header inlet_1
|
|
642
|
+
m.boiler_to_header_arc = Arc(
|
|
643
|
+
source=m.fs.boiler.outlet, destination=m.fs.header.inlet_1
|
|
644
|
+
)
|
|
645
|
+
pyo.TransformationFactory("network.expand_arcs").apply_to(m)
|
|
646
|
+
|
|
647
|
+
# Set inlet conditions for inlet_1
|
|
648
|
+
m.fs.boiler.inlet.flow_mol.fix(1000) # mol/s
|
|
649
|
+
m.fs.boiler.inlet.pressure.fix(200 * 1000) # Pa
|
|
650
|
+
m.fs.boiler.inlet.enth_mol.fix(
|
|
651
|
+
m.fs.water.htpx(
|
|
652
|
+
(15 + 273.15) * pyo.units.K,
|
|
653
|
+
m.fs.boiler.inlet.pressure[0]
|
|
654
|
+
)
|
|
655
|
+
) # J/mol
|
|
656
|
+
m.fs.boiler.heat_duty.fix(
|
|
657
|
+
27784964.63
|
|
658
|
+
)
|
|
659
|
+
m.fs.boiler.heat_duty.unfix()
|
|
660
|
+
m.fs.boiler.outlet.enth_mol.fix(
|
|
661
|
+
m.fs.water.htpx(
|
|
662
|
+
x=0.5,
|
|
663
|
+
p=m.fs.boiler.inlet.pressure[0]
|
|
664
|
+
)
|
|
665
|
+
)
|
|
666
|
+
m.fs.boiler.deltaP.fix(
|
|
667
|
+
0.0
|
|
668
|
+
)
|
|
669
|
+
m.fs.header.outlet_1.flow_mol.fix(500)
|
|
670
|
+
m.fs.header.heat_loss.fix(
|
|
671
|
+
0.0
|
|
672
|
+
)
|
|
673
|
+
m.fs.header.pressure_loss.fix(
|
|
674
|
+
0.0
|
|
675
|
+
)
|
|
676
|
+
|
|
677
|
+
# Initialize the heater first
|
|
678
|
+
seq = SequentialDecomposition()
|
|
679
|
+
seq.options.select_tear_method = "heuristic"
|
|
680
|
+
seq.options.tear_method = "Wegstein"
|
|
681
|
+
seq.options.iterLim = 1
|
|
682
|
+
|
|
683
|
+
# create computation graph
|
|
684
|
+
G = seq.create_graph(m)
|
|
685
|
+
heuristic_tear_set = seq.tear_set_arcs(G, method="heuristic")
|
|
686
|
+
# get calculation order
|
|
687
|
+
order = seq.calculation_order(G)
|
|
688
|
+
|
|
689
|
+
for o in heuristic_tear_set:
|
|
690
|
+
print(o.name)
|
|
691
|
+
|
|
692
|
+
for o in order:
|
|
693
|
+
print(o[0].name)
|
|
694
|
+
|
|
695
|
+
# define unit initialisation function
|
|
696
|
+
def init_unit(unit):
|
|
697
|
+
unit.initialize()
|
|
698
|
+
|
|
699
|
+
# run sequential decomposition
|
|
700
|
+
seq.run(m, init_unit)
|
|
701
|
+
|
|
702
|
+
opt = pyo.SolverFactory("ipopt_v2")
|
|
703
|
+
results = opt.solve(m, tee=False)
|
|
704
|
+
|
|
705
|
+
assert results.solver.termination_condition == pyo.TerminationCondition.optimal
|
|
706
|
+
|
|
707
|
+
assert_header_solution(
|
|
708
|
+
m=m,
|
|
709
|
+
expected_total_flow=1000,
|
|
710
|
+
expected_vent_flow=0,
|
|
711
|
+
expected_makeup_flow=0.0,
|
|
712
|
+
expected_liquid_flow=500,
|
|
713
|
+
has_heat_loss=0,
|
|
714
|
+
has_pressure_loss=0,
|
|
715
|
+
)
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
""" TESTS FOR USE AS LIQUID HEADER """
|
|
719
|
+
@pytest.fixture
|
|
720
|
+
def liquid_header_base_case():
|
|
721
|
+
def _make_case(num_inlets=2, num_outlets=3):
|
|
722
|
+
# This defines the base case for all tests
|
|
723
|
+
m = pyo.ConcreteModel()
|
|
724
|
+
m.fs = FlowsheetBlock(dynamic=False)
|
|
725
|
+
m.fs.water = build_package("helmholtz", ["water"], ["Liq", "Vap"])
|
|
726
|
+
|
|
727
|
+
m.fs.header = simple_header(
|
|
728
|
+
property_package=m.fs.water,
|
|
729
|
+
num_inlets=num_inlets,
|
|
730
|
+
num_outlets=num_outlets,
|
|
731
|
+
is_liquid_header=True,
|
|
732
|
+
)
|
|
733
|
+
|
|
734
|
+
# Set inlet conditions for inlet_1
|
|
735
|
+
m.fs.header.inlet_1.flow_mol.fix(1000) # mol/s
|
|
736
|
+
m.fs.header.inlet_1.pressure.fix(201325) # Pa
|
|
737
|
+
m.fs.header.inlet_1.enth_mol.fix(m.fs.water.htpx((50 + 273.15) * pyo.units.K, m.fs.header.inlet_1.pressure[0])) # J/mol
|
|
738
|
+
|
|
739
|
+
return m
|
|
740
|
+
return _make_case
|
|
741
|
+
|
|
742
|
+
def test_liq_header_with_inlet_and_outlet_and_water_boiler(liquid_header_base_case):
|
|
743
|
+
m = liquid_header_base_case(num_inlets=1, num_outlets=1)
|
|
744
|
+
|
|
745
|
+
# Add a heater before the header
|
|
746
|
+
m.fs.boiler = DynamicHeater(
|
|
747
|
+
property_package=m.fs.water,
|
|
748
|
+
has_pressure_change=True,
|
|
749
|
+
)
|
|
750
|
+
# Connect heater outlet to header inlet_1
|
|
751
|
+
m.boiler_to_header_arc = Arc(
|
|
752
|
+
source=m.fs.boiler.outlet, destination=m.fs.header.inlet_1
|
|
753
|
+
)
|
|
754
|
+
pyo.TransformationFactory("network.expand_arcs").apply_to(m)
|
|
755
|
+
|
|
756
|
+
m.fs.header.inlet_1.flow_mol.unfix() # mol/s
|
|
757
|
+
m.fs.header.inlet_1.pressure.unfix() # Pa
|
|
758
|
+
m.fs.header.inlet_1.enth_mol.unfix() # J/mol
|
|
759
|
+
|
|
760
|
+
# Set inlet conditions for inlet_1
|
|
761
|
+
m.fs.boiler.inlet.flow_mol.fix(1000) # mol/s
|
|
762
|
+
m.fs.boiler.inlet.pressure.fix(200 * 1000) # Pa
|
|
763
|
+
m.fs.boiler.inlet.enth_mol.fix(
|
|
764
|
+
m.fs.water.htpx(
|
|
765
|
+
(15 + 273.15) * pyo.units.K,
|
|
766
|
+
m.fs.boiler.inlet.pressure[0]
|
|
767
|
+
)
|
|
768
|
+
) # J/mol
|
|
769
|
+
m.fs.boiler.heat_duty.fix(
|
|
770
|
+
0
|
|
771
|
+
)
|
|
772
|
+
m.fs.boiler.heat_duty.unfix()
|
|
773
|
+
m.fs.boiler.outlet.enth_mol.fix(
|
|
774
|
+
m.fs.water.htpx(
|
|
775
|
+
x=0.1,
|
|
776
|
+
p=m.fs.boiler.inlet.pressure[0]
|
|
777
|
+
)
|
|
778
|
+
)
|
|
779
|
+
m.fs.boiler.deltaP.fix(
|
|
780
|
+
0.0
|
|
781
|
+
)
|
|
782
|
+
m.fs.header.outlet_1.flow_mol.fix(500)
|
|
783
|
+
m.fs.header.heat_loss.fix(
|
|
784
|
+
0.0
|
|
785
|
+
)
|
|
786
|
+
m.fs.header.pressure_loss.fix(
|
|
787
|
+
0.0
|
|
788
|
+
)
|
|
789
|
+
|
|
790
|
+
# Initialize the heater first
|
|
791
|
+
seq = SequentialDecomposition()
|
|
792
|
+
seq.options.select_tear_method = "heuristic"
|
|
793
|
+
seq.options.tear_method = "Wegstein"
|
|
794
|
+
seq.options.iterLim = 1
|
|
795
|
+
|
|
796
|
+
# create computation graph
|
|
797
|
+
G = seq.create_graph(m)
|
|
798
|
+
heuristic_tear_set = seq.tear_set_arcs(G, method="heuristic")
|
|
799
|
+
# get calculation order
|
|
800
|
+
order = seq.calculation_order(G)
|
|
801
|
+
|
|
802
|
+
for o in heuristic_tear_set:
|
|
803
|
+
print(o.name)
|
|
804
|
+
|
|
805
|
+
for o in order:
|
|
806
|
+
print(o[0].name)
|
|
807
|
+
|
|
808
|
+
# define unit initialisation function
|
|
809
|
+
def init_unit(unit):
|
|
810
|
+
unit.initialize()
|
|
811
|
+
|
|
812
|
+
# run sequential decomposition
|
|
813
|
+
seq.run(m, init_unit)
|
|
814
|
+
|
|
815
|
+
opt = pyo.SolverFactory("ipopt_v2")
|
|
816
|
+
results = opt.solve(m, tee=False)
|
|
817
|
+
|
|
818
|
+
assert results.solver.termination_condition == pyo.TerminationCondition.optimal
|
|
819
|
+
|
|
820
|
+
assert_header_solution(
|
|
821
|
+
m=m,
|
|
822
|
+
expected_total_flow=1000,
|
|
823
|
+
expected_vent_flow=100,
|
|
824
|
+
expected_makeup_flow=0.0,
|
|
825
|
+
expected_liquid_flow=400,
|
|
826
|
+
has_heat_loss=0,
|
|
827
|
+
has_pressure_loss=0,
|
|
828
|
+
)
|
|
829
|
+
|
|
830
|
+
def test_liq_header_with_inlet_greater_than_outlet_flow(liquid_header_base_case):
|
|
831
|
+
# Case where there is zero liquid flow and there is more known inlet than outlet flows
|
|
832
|
+
m = liquid_header_base_case(num_inlets=1, num_outlets=1)
|
|
833
|
+
|
|
834
|
+
# Set user steam demand
|
|
835
|
+
m.fs.header.outlet_1.flow_mol.fix(800) # mol/s
|
|
836
|
+
# Set cooler specifications
|
|
837
|
+
m.fs.header.heat_loss[0].fix(5000) # W -> heat loss
|
|
838
|
+
m.fs.header.pressure_loss[0].fix(1000) # Pa -> pressure loss
|
|
839
|
+
|
|
840
|
+
assert_header_solution(
|
|
841
|
+
m,
|
|
842
|
+
expected_total_flow=1000,
|
|
843
|
+
expected_vent_flow=0,
|
|
844
|
+
expected_makeup_flow=0,
|
|
845
|
+
expected_liquid_flow=200,
|
|
846
|
+
has_heat_loss=True if pyo.value(m.fs.header.heat_loss[0]) > 0 else False,
|
|
847
|
+
has_pressure_loss=True if pyo.value(m.fs.header.pressure_loss[0]) > 0 else False,
|
|
848
|
+
)
|
|
849
|
+
|
|
850
|
+
def test_liq_header_with_vapour_in_inlet_flow(liquid_header_base_case):
|
|
851
|
+
# Case where there is zero liquid flow and there is more known inlet than outlet flows
|
|
852
|
+
m = liquid_header_base_case(num_inlets=1, num_outlets=1)
|
|
853
|
+
|
|
854
|
+
h_val = pyo.value(m.fs.water.htpx(p=201325 * pyo.units.Pa, x=0.15))
|
|
855
|
+
m.fs.header.inlet_1.enth_mol.fix(h_val) # J/mol
|
|
856
|
+
|
|
857
|
+
# Set user steam demand
|
|
858
|
+
m.fs.header.outlet_1.flow_mol.fix(800) # mol/s
|
|
859
|
+
# Set cooler specifications
|
|
860
|
+
m.fs.header.heat_loss[0].fix(0) # W -> heat loss
|
|
861
|
+
m.fs.header.pressure_loss[0].fix(0) # Pa -> pressure loss
|
|
862
|
+
|
|
863
|
+
assert_header_solution(
|
|
864
|
+
m,
|
|
865
|
+
expected_total_flow=1000,
|
|
866
|
+
expected_vent_flow=150,
|
|
867
|
+
expected_makeup_flow=0,
|
|
868
|
+
expected_liquid_flow=50,
|
|
869
|
+
has_heat_loss=True if pyo.value(m.fs.header.heat_loss[0]) > 0 else False,
|
|
870
|
+
has_pressure_loss=True if pyo.value(m.fs.header.pressure_loss[0]) > 0 else False,
|
|
871
|
+
)
|
|
872
|
+
|
|
873
|
+
def test_liq_header_with_inlet_equal_to_outlet_flow(liquid_header_base_case):
|
|
874
|
+
# Case where there is zero liquid flow and the header is balanced
|
|
875
|
+
m = liquid_header_base_case(num_inlets=1, num_outlets=1)
|
|
876
|
+
# Set user steam demand
|
|
877
|
+
m.fs.header.outlet_1.flow_mol.fix(1000) # mol/s
|
|
878
|
+
# Set cooler specifications
|
|
879
|
+
m.fs.header.heat_loss[0].fix(0) # W -> heat loss
|
|
880
|
+
m.fs.header.pressure_loss[0].fix(0) # Pa -> pressure loss
|
|
881
|
+
|
|
882
|
+
assert_header_solution(
|
|
883
|
+
m,
|
|
884
|
+
expected_total_flow=1000,
|
|
885
|
+
expected_vent_flow=0,
|
|
886
|
+
expected_makeup_flow=0,
|
|
887
|
+
expected_liquid_flow=0,
|
|
888
|
+
has_heat_loss=True if pyo.value(m.fs.header.heat_loss[0]) > 0 else False,
|
|
889
|
+
has_pressure_loss=True if pyo.value(m.fs.header.pressure_loss[0]) > 0 else False,
|
|
890
|
+
)
|
|
891
|
+
|
|
892
|
+
def test_liq_header_with_inlet_less_than_outlet_flow(liquid_header_base_case):
|
|
893
|
+
# Case where there is zero liquid flow and the header is balanced
|
|
894
|
+
m = liquid_header_base_case(num_inlets=1, num_outlets=1)
|
|
895
|
+
# Set user steam demand
|
|
896
|
+
m.fs.header.outlet_1.flow_mol.fix(1200) # mol/s
|
|
897
|
+
# Set cooler specifications
|
|
898
|
+
m.fs.header.heat_loss[0].fix(0) # W -> heat loss
|
|
899
|
+
m.fs.header.pressure_loss[0].fix(0) # Pa -> pressure loss
|
|
900
|
+
|
|
901
|
+
assert_header_solution(
|
|
902
|
+
m,
|
|
903
|
+
expected_total_flow=1200,
|
|
904
|
+
expected_vent_flow=0,
|
|
905
|
+
expected_makeup_flow=200,
|
|
906
|
+
expected_liquid_flow=0,
|
|
907
|
+
has_heat_loss=True if pyo.value(m.fs.header.heat_loss[0]) > 0 else False,
|
|
908
|
+
has_pressure_loss=True if pyo.value(m.fs.header.pressure_loss[0]) > 0 else False,
|
|
909
|
+
)
|
|
910
|
+
|
|
911
|
+
def test_liq_header_with_zero_flow(liquid_header_base_case):
|
|
912
|
+
# Case where there is zero liquid flow and the header is balanced
|
|
913
|
+
m = liquid_header_base_case(num_inlets=1, num_outlets=1)
|
|
914
|
+
m.fs.header.inlet_1.flow_mol.fix(0) # mol/s
|
|
915
|
+
# Set user steam demand
|
|
916
|
+
m.fs.header.outlet_1.flow_mol.fix(0) # mol/s
|
|
917
|
+
# Set cooler specifications
|
|
918
|
+
m.fs.header.heat_loss[0].fix(0) # W -> heat loss
|
|
919
|
+
m.fs.header.pressure_loss[0].fix(0) # Pa -> pressure loss
|
|
920
|
+
|
|
921
|
+
assert_header_solution(
|
|
922
|
+
m,
|
|
923
|
+
expected_total_flow=0,
|
|
924
|
+
expected_vent_flow=0,
|
|
925
|
+
expected_makeup_flow=0,
|
|
926
|
+
expected_liquid_flow=0,
|
|
927
|
+
has_heat_loss=True if pyo.value(m.fs.header.heat_loss[0]) > 0 else False,
|
|
928
|
+
has_pressure_loss=True if pyo.value(m.fs.header.pressure_loss[0]) > 0 else False,
|
|
929
|
+
)
|
|
930
|
+
|
|
931
|
+
def test_liq_header_with_inlet_greater_than_three_defined_outlet_flows(liquid_header_base_case):
|
|
932
|
+
# Case where there is zero liquid flow and there is more known inlet than outlet flows
|
|
933
|
+
m = liquid_header_base_case(num_inlets=1, num_outlets=3)
|
|
934
|
+
|
|
935
|
+
# Set user steam demand
|
|
936
|
+
m.fs.header.outlet_1.flow_mol.fix(500) # mol/s
|
|
937
|
+
m.fs.header.outlet_2.flow_mol.fix(150) # mol/s
|
|
938
|
+
m.fs.header.outlet_3.flow_mol.fix(200) # mol/s
|
|
939
|
+
|
|
940
|
+
# Set cooler specifications
|
|
941
|
+
m.fs.header.heat_loss[0].fix(10000) # W -> heat loss
|
|
942
|
+
m.fs.header.pressure_loss[0].fix(1000) # Pa -> pressure loss
|
|
943
|
+
|
|
944
|
+
assert_header_solution(
|
|
945
|
+
m,
|
|
946
|
+
expected_total_flow=1000,
|
|
947
|
+
expected_vent_flow=0,
|
|
948
|
+
expected_makeup_flow=0,
|
|
949
|
+
expected_liquid_flow=150,
|
|
950
|
+
has_heat_loss=True if pyo.value(m.fs.header.heat_loss[0]) > 0 else False,
|
|
951
|
+
has_pressure_loss=True if pyo.value(m.fs.header.pressure_loss[0]) > 0 else False,
|
|
952
|
+
)
|
|
953
|
+
|
|
954
|
+
def test_liq_header_with_inlet_less_than_two_defined_outlet_flows(liquid_header_base_case):
|
|
955
|
+
# Case where there is zero liquid flow and the header is balanced
|
|
956
|
+
m = liquid_header_base_case(num_inlets=1, num_outlets=2)
|
|
957
|
+
# Set user steam demand
|
|
958
|
+
m.fs.header.outlet_1.flow_mol.fix(600) # mol/s
|
|
959
|
+
m.fs.header.outlet_2.flow_mol.fix(600) # mol/s
|
|
960
|
+
# Set cooler specifications
|
|
961
|
+
m.fs.header.heat_loss[0].fix(0) # W -> heat loss
|
|
962
|
+
m.fs.header.pressure_loss[0].fix(0) # Pa -> pressure loss
|
|
963
|
+
|
|
964
|
+
assert_header_solution(
|
|
965
|
+
m,
|
|
966
|
+
expected_total_flow=1200,
|
|
967
|
+
expected_vent_flow=0,
|
|
968
|
+
expected_makeup_flow=200,
|
|
969
|
+
expected_liquid_flow=0,
|
|
970
|
+
has_heat_loss=True if pyo.value(m.fs.header.heat_loss[0]) > 0 else False,
|
|
971
|
+
has_pressure_loss=True if pyo.value(m.fs.header.pressure_loss[0]) > 0 else False,
|
|
972
|
+
)
|
|
973
|
+
|
|
974
|
+
def test_header_with_two_defined_inlet_less_than_two_defined_outlet_flows(liquid_header_base_case):
|
|
975
|
+
# Case where there is zero liquid flow and the header is balanced
|
|
976
|
+
m = liquid_header_base_case(num_inlets=2, num_outlets=2)
|
|
977
|
+
m.fs.header.inlet_2.flow_mol.fix(100) # mol/s
|
|
978
|
+
m.fs.header.inlet_2.pressure.fix(221325) # Pa
|
|
979
|
+
m.fs.header.inlet_2.enth_mol.fix(m.fs.water.htpx((90 + 273.15) * pyo.units.K, m.fs.header.inlet_1.pressure[0])) # J/mol
|
|
980
|
+
|
|
981
|
+
# Set user steam demand
|
|
982
|
+
m.fs.header.outlet_1.flow_mol.fix(600) # mol/s
|
|
983
|
+
m.fs.header.outlet_2.flow_mol.fix(600) # mol/s
|
|
984
|
+
# Set cooler specifications
|
|
985
|
+
m.fs.header.heat_loss[0].fix(0) # W -> heat loss
|
|
986
|
+
m.fs.header.pressure_loss[0].fix(0) # Pa -> pressure loss
|
|
987
|
+
|
|
988
|
+
assert_header_solution(
|
|
989
|
+
m,
|
|
990
|
+
expected_total_flow=1200,
|
|
991
|
+
expected_vent_flow=0,
|
|
992
|
+
expected_makeup_flow=100,
|
|
993
|
+
expected_liquid_flow=0,
|
|
994
|
+
has_heat_loss=True if pyo.value(m.fs.header.heat_loss[0]) > 0 else False,
|
|
995
|
+
has_pressure_loss=True if pyo.value(m.fs.header.pressure_loss[0]) > 0 else False,
|
|
996
|
+
)
|
|
997
|
+
|
|
998
|
+
|