ThermoSim 3.2.0__tar.gz → 3.3.0__tar.gz
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.
- thermosim-3.3.0/CHANGELOG.md +436 -0
- {thermosim-3.2.0 → thermosim-3.3.0}/LICENSE +1 -1
- {thermosim-3.2.0 → thermosim-3.3.0}/MANIFEST.in +1 -1
- thermosim-3.3.0/PKG-INFO +185 -0
- thermosim-3.3.0/README.md +151 -0
- thermosim-3.3.0/ThermoSim/T66.json +69 -0
- {thermosim-3.2.0 → thermosim-3.3.0}/ThermoSim/__init__.py +1 -1
- {thermosim-3.2.0 → thermosim-3.3.0}/ThermoSim/analysis.py +58 -19
- {thermosim-3.2.0 → thermosim-3.3.0}/ThermoSim/heat_exchangers.py +959 -364
- {thermosim-3.2.0 → thermosim-3.3.0}/ThermoSim/model.py +355 -118
- {thermosim-3.2.0 → thermosim-3.3.0}/ThermoSim/plotting.py +599 -236
- {thermosim-3.2.0 → thermosim-3.3.0}/ThermoSim/simple_components.py +95 -57
- {thermosim-3.2.0 → thermosim-3.3.0}/ThermoSim/state.py +159 -37
- {thermosim-3.2.0 → thermosim-3.3.0}/ThermoSim/turbomachinery.py +56 -12
- thermosim-3.3.0/ThermoSim.egg-info/PKG-INFO +185 -0
- {thermosim-3.2.0 → thermosim-3.3.0}/ThermoSim.egg-info/SOURCES.txt +1 -2
- {thermosim-3.2.0 → thermosim-3.3.0}/ThermoSim.egg-info/requires.txt +1 -0
- {thermosim-3.2.0 → thermosim-3.3.0}/pyproject.toml +11 -5
- thermosim-3.2.0/CHANGELOG.md +0 -97
- thermosim-3.2.0/PKG-INFO +0 -165
- thermosim-3.2.0/README.md +0 -127
- thermosim-3.2.0/ThermoSim/heat_exchangers_old.py +0 -1293
- thermosim-3.2.0/ThermoSim.egg-info/PKG-INFO +0 -165
- thermosim-3.2.0/setup.py +0 -60
- {thermosim-3.2.0 → thermosim-3.3.0}/ThermoSim/base_component.py +0 -0
- {thermosim-3.2.0 → thermosim-3.3.0}/ThermoSim/config.py +0 -0
- {thermosim-3.2.0 → thermosim-3.3.0}/ThermoSim.egg-info/dependency_links.txt +0 -0
- {thermosim-3.2.0 → thermosim-3.3.0}/ThermoSim.egg-info/top_level.txt +0 -0
- {thermosim-3.2.0 → thermosim-3.3.0}/setup.cfg +0 -0
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
|
|
2
|
+
# Changelog
|
|
3
|
+
## [3.3.0] - 2026-08-29
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **`Model.Solve()` uses a staged default solve order instead of one flat
|
|
7
|
+
priority pass.** Previously every iteration attempted pass-through
|
|
8
|
+
components, then junctions, then `SimpleHEX`, then other heat exchangers
|
|
9
|
+
sorted by hot-inlet temperature, all in a single pass — so a harder heat
|
|
10
|
+
exchanger could be attempted in the same round as an easier one still
|
|
11
|
+
waiting on upstream data, and heat exchangers were interleaved with a
|
|
12
|
+
still-settling simple network rather than solved after it stabilised.
|
|
13
|
+
|
|
14
|
+
The default order is now staged: pass-through and junction components
|
|
15
|
+
(Turbine, Pump, Compressor, Pipe, Expansion Valve, Splitter, Separator,
|
|
16
|
+
Mixer) are round-robined together to convergence; then `SimpleHEX`,
|
|
17
|
+
round-robined to convergence; then pinch-type heat exchangers
|
|
18
|
+
(`double_pipe`, `Condenser`, `Evaporator`), solved in ascending order of
|
|
19
|
+
*current* unknown-count — every 1-unknown exchanger fully converged
|
|
20
|
+
before any 2-unknown one is attempted, then 3-unknown — with the
|
|
21
|
+
unknown-count re-evaluated every pass, since resolving one exchanger can
|
|
22
|
+
drop another's tier; then everything else, round-robined to convergence.
|
|
23
|
+
The whole sequence repeats every `Solve()` iteration. `custom_order` is
|
|
24
|
+
unchanged.
|
|
25
|
+
|
|
26
|
+
`Model.Solve()` had no test coverage before this change — every existing
|
|
27
|
+
test and example bypasses it via `Calculate=True` in hand-ordered
|
|
28
|
+
construction. Added `tests/test_solve.py`, including a check that a
|
|
29
|
+
cycle built in a deliberately scrambled order converges to the same
|
|
30
|
+
state as the same cycle built in correct dependency order.
|
|
31
|
+
|
|
32
|
+
Also fixed a latent bug this exposed: `HeatExchanger._identify_unknowns()`
|
|
33
|
+
read `self.Hot_In`/`Hot_Out`/`Cold_In`/`Cold_Out`, which were previously
|
|
34
|
+
only ever set inside `Cal()` — calling it on a HEX before its first
|
|
35
|
+
`Cal()` (which the new tiering logic needs to do) raised `AttributeError`
|
|
36
|
+
instead of correctly reporting "everything unknown". Fixed by
|
|
37
|
+
initialising those attributes to `None` in `HeatExchanger.__init__`.
|
|
38
|
+
|
|
39
|
+
- **`CyclePlotter.plot_Ts_diagram` / `plot_Ph_diagram` / `plot_hs_diagram`
|
|
40
|
+
can now plot several cycles on one diagram**, e.g. a cascade system's
|
|
41
|
+
two loops:
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
plotter.plot_Ph_diagram([
|
|
45
|
+
['8', '9', '1', '2', '3', '4'],
|
|
46
|
+
['3b', '5', '6', '7'],
|
|
47
|
+
])
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`loop_points` accepts either a flat point-name list (unchanged single-
|
|
51
|
+
cycle behaviour) or a list of such lists. Each loop is drawn in its own
|
|
52
|
+
color with its own legend entry, and gets its own saturation dome —
|
|
53
|
+
auto-detected from its own first point, not a single global fluid — so a
|
|
54
|
+
cascade's mismatched refrigerants each get a correctly labelled dome.
|
|
55
|
+
Loops don't need to be point-disjoint: a cascade condenser's two sides
|
|
56
|
+
are free to share a point name if that's how the model is built. CSV
|
|
57
|
+
export tags every row with a `Loop` column.
|
|
58
|
+
|
|
59
|
+
Added two new optional parameters, `loop_labels` and `loop_colors`
|
|
60
|
+
(one entry per loop, `None` falls back to the auto value), for the case
|
|
61
|
+
where several loops share a fluid — e.g. a dual-pressure ORC's two
|
|
62
|
+
stages — where the auto legend label only differs by number
|
|
63
|
+
(`'Loop 1 (R245fa)'` / `'Loop 2 (R245fa)'`) and doesn't say which loop is
|
|
64
|
+
which:
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
plotter.plot_Ph_diagram(
|
|
68
|
+
[['h1', 'h2', 'h3', 'h4'], ['l1', 'l2', 'l3', 'l4']],
|
|
69
|
+
loop_labels=['High-Pressure Loop', 'Low-Pressure Loop'],
|
|
70
|
+
loop_colors=['crimson', 'navy'],
|
|
71
|
+
)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`CyclePlotter` had no test coverage before this change. Added
|
|
75
|
+
`tests/test_plotting.py`, covering single-loop backward compatibility,
|
|
76
|
+
multi-fluid cascade rendering, same-fluid dual-pressure labeling, and
|
|
77
|
+
the `loop_labels`/`loop_colors` overrides.
|
|
78
|
+
|
|
79
|
+
### Fixed
|
|
80
|
+
- **The discretised pinch no longer depends on `div_N`.** `_build_profile`
|
|
81
|
+
(used by both the solver's pinch constraint and `plot_hex_profile`) marched
|
|
82
|
+
the exchanger in `div_N` equal-enthalpy steps. For a phase-changing stream
|
|
83
|
+
the true pinch sits exactly at a saturation crossing — onset of boiling,
|
|
84
|
+
superheating, condensation or subcooling — and a uniform grid only landed
|
|
85
|
+
on it by chance. The reported minimum approach converged from above as
|
|
86
|
+
`div_N` increased instead of being exact at any resolution (documented as
|
|
87
|
+
a known limitation in 3.2.4, see [Troubleshooting](https://github.com/Nouman090/ThermoSim/wiki/Troubleshooting)).
|
|
88
|
+
|
|
89
|
+
ThermoSim now computes the exact enthalpy of every such crossing on both
|
|
90
|
+
streams (via CoolProp's saturated-liquid/-vapour enthalpy at the stream's
|
|
91
|
+
pressure) and inserts it into the profile in addition to the uniform
|
|
92
|
+
`div_N` grid, so the reported pinch is exact at `div_N=10` and `div_N=5000`
|
|
93
|
+
alike. Verified on a water/water double-pipe exchanger where the hot
|
|
94
|
+
stream condenses across the unit: `min_dT` was resolution-dependent before
|
|
95
|
+
the fix (20.000 K at `div_N=10` drifting toward 19.028 K by `div_N=5000`,
|
|
96
|
+
never settling) and is now exactly 19.024401 K at every `div_N` tested,
|
|
97
|
+
10 through 5000.
|
|
98
|
+
|
|
99
|
+
This changes solved results for exchangers where a stream crosses its
|
|
100
|
+
saturation dome and the pinch constraint resolves an unknown (mass flow,
|
|
101
|
+
enthalpy). The worked example in [Heat Exchanger](https://github.com/Nouman090/ThermoSim/wiki/Heat-Exchanger)
|
|
102
|
+
moves `mc` from 13.521 to 13.553 kg/s; `Q` is unaffected there because it
|
|
103
|
+
comes directly from the fully-specified hot side, not from the pinch.
|
|
104
|
+
Fluids with no two-phase envelope at the operating pressure (incompressible
|
|
105
|
+
fluids, supercritical pressure) are unaffected.
|
|
106
|
+
|
|
107
|
+
## [3.2.4] - 2026-08-20
|
|
108
|
+
|
|
109
|
+
### Added
|
|
110
|
+
- **Full-profile pinch validation.** `HeatExchanger` now checks the minimum
|
|
111
|
+
approach along the whole exchanger after every solve, not only at the two
|
|
112
|
+
ends, and reports a violation with `min_dT` stored on the component.
|
|
113
|
+
|
|
114
|
+
The previous check compared only the terminal temperature differences.
|
|
115
|
+
That is sufficient for a monotonic single-phase exchanger, but when
|
|
116
|
+
either stream changes phase the closest approach sits *inside* the unit —
|
|
117
|
+
typically where a boiling stream reaches saturation — and both ends can
|
|
118
|
+
be comfortably clear while the profiles cross in the middle.
|
|
119
|
+
|
|
120
|
+
The underlying asymmetry was that the pinch constraint only binds where
|
|
121
|
+
it is doing work. With two or three unknowns the solver drives
|
|
122
|
+
`min(dT) - PPT` to zero, so the constraint holds by construction. With
|
|
123
|
+
zero or one unknown the energy balance closes the problem on its own and
|
|
124
|
+
`PPT` never enters the calculation, so the exchanger reported convergence
|
|
125
|
+
whatever the profile did.
|
|
126
|
+
|
|
127
|
+
A combined-cycle HRSG built that way returned 53.87 % thermal efficiency
|
|
128
|
+
with every component solved and a minimum approach of **-12.9 K**: the
|
|
129
|
+
temperature profiles crossed and the design was not realisable. It also
|
|
130
|
+
scored higher than the feasible alternative (51.98 % at +15.9 K), because
|
|
131
|
+
relaxing a constraint that is not enforced always improves the objective.
|
|
132
|
+
In a parameter sweep the infeasible design is therefore selected
|
|
133
|
+
preferentially.
|
|
134
|
+
|
|
135
|
+
- `strict_pinch` argument on `HeatExchanger` (default `False`). A violated
|
|
136
|
+
pinch warns by default, since an infeasible point in a sweep is a
|
|
137
|
+
statement about the design rather than an error in the input. Set `True`
|
|
138
|
+
to raise instead.
|
|
139
|
+
- `min_dT` attribute on `HeatExchanger`, so the pinch can be tested
|
|
140
|
+
programmatically rather than read off a plot.
|
|
141
|
+
|
|
142
|
+
## [3.2.3] - 2026-08-18
|
|
143
|
+
|
|
144
|
+
### Added
|
|
145
|
+
- **Gas working fluids in pinch-constrained heat exchangers.** Bracket
|
|
146
|
+
bounds now respect the melting line `Tmelt(p)` obtained from
|
|
147
|
+
`AbstractState.melting_line()`, rather than `PropsSI('TMIN', fluid)`
|
|
148
|
+
alone.
|
|
149
|
+
|
|
150
|
+
`TMIN` is a single number, but the melting line varies with pressure and
|
|
151
|
+
can rise above it — for air, 59.75 K against 59.93 K at 10 bar — at which
|
|
152
|
+
point CoolProp refuses the evaluation. Every two- and three-unknown pinch
|
|
153
|
+
problem therefore failed for `Air`, `Nitrogen`, `CO2` and `Oxygen`, i.e.
|
|
154
|
+
for every gas-turbine, HRSG and recuperator model. Water is the opposite
|
|
155
|
+
case: its melting line falls with pressure, so taking the maximum of the
|
|
156
|
+
two handles both directions. Fluids without a fitted melting line, and
|
|
157
|
+
CO2 below its triple-point pressure, fall back to `TMIN`.
|
|
158
|
+
|
|
159
|
+
- `Condition` column and `condition` argument on `Model.Point_print()`.
|
|
160
|
+
Since `Q` became strictly numeric in 3.2.2, single-phase rows showed a
|
|
161
|
+
bare `NaN`; `Condition` carries the readable label without putting a
|
|
162
|
+
string back into a numeric column. The full label, with its margin from
|
|
163
|
+
saturation, remains in `phase`.
|
|
164
|
+
|
|
165
|
+
- **Real process paths in cycle diagrams.** `plot_Ts_diagram`,
|
|
166
|
+
`plot_Ph_diagram` and `plot_hs_diagram` gained `process_path=True`
|
|
167
|
+
(default) and `path_N=60`. Isobaric segments now show their saturation
|
|
168
|
+
crossings, so boiler and condenser legs render as sub-cooled → saturated
|
|
169
|
+
liquid → saturated vapour → superheated with sharp kinks and a flat
|
|
170
|
+
two-phase section; throttling stays isenthalpic; and expansion or
|
|
171
|
+
compression follows a constant-efficiency condition line.
|
|
172
|
+
|
|
173
|
+
Interpolating `(P, H)` linearly across a turbine makes entropy *fall*
|
|
174
|
+
mid-expansion (6.661 → 6.090 → 7.265 kJ/kg·K), which no adiabatic machine
|
|
175
|
+
does; the condition line gives a monotonic 6.661 → 6.709 → 7.265. The
|
|
176
|
+
efficiency is derived from the two endpoints rather than read from
|
|
177
|
+
`n_isen`, so the path stays correct when the outlet state was supplied
|
|
178
|
+
directly. Segments that cannot be traced fall back to a straight line
|
|
179
|
+
silently; `process_path=False` restores the previous polygon.
|
|
180
|
+
|
|
181
|
+
- Validation suite in `validation/`, covering nine worked examples from
|
|
182
|
+
Moran & Shapiro 5th ed. (SI) and the Chena Hot Springs geothermal ORC.
|
|
183
|
+
- Continuous integration on GitHub Actions across Python 3.9–3.13.
|
|
184
|
+
- `CONTRIBUTING.md` and `CODE_OF_CONDUCT.md`.
|
|
185
|
+
|
|
186
|
+
### Changed
|
|
187
|
+
- **Effectiveness uses the rigorous enthalpy-based definition.**
|
|
188
|
+
`Q_max = min[mh(h_hi - h_h(T_ci)), mc(h_c(T_hi) - h_ci)]`, of which the
|
|
189
|
+
familiar `Cmin·dT` form is the constant-`cp` simplification. For air
|
|
190
|
+
between 453 K and 1138 K — the span of a typical regenerator — `cp` rises
|
|
191
|
+
about 19 %, and that assumption cost 6.4 kJ/kg on the regenerator cold
|
|
192
|
+
outlet of Moran Example 9.11, moving the cycle efficiency deviation from
|
|
193
|
+
0.85 % to 0.09 %. The enthalpy form was already used for two-phase
|
|
194
|
+
inlets, so this also removes an internal inconsistency.
|
|
195
|
+
|
|
196
|
+
- **Root-finding brackets floored at the counter-flow limit.** No state in
|
|
197
|
+
a counter-flow exchanger is colder than the cold inlet, so the bracket
|
|
198
|
+
need not reach the fluid's absolute validity floor. With the tighter
|
|
199
|
+
bracket `brentq` converges directly instead of falling back to
|
|
200
|
+
differential evolution: **62× faster** on gas cases (252 s → 4.1 s), with
|
|
201
|
+
identical duties and a slightly more accurate pinch.
|
|
202
|
+
|
|
203
|
+
- The `PPT` end-check tolerance is now relative, `max(1e-2, 1e-3·PPT)`. The
|
|
204
|
+
pinch is located by a root-finder with a relative tolerance, so a fixed
|
|
205
|
+
absolute allowance rejected good solutions on large pinches — a 30 K
|
|
206
|
+
target converging to 29.986 K is 0.05 % out, i.e. numerical noise, yet
|
|
207
|
+
was reported as a physical violation.
|
|
208
|
+
|
|
209
|
+
- `requires-python` raised to `>=3.9`, with per-version classifiers, to
|
|
210
|
+
match what is actually tested.
|
|
211
|
+
- `setup.py` removed; `pyproject.toml` is the single source of truth. The
|
|
212
|
+
two had drifted to different version strings.
|
|
213
|
+
|
|
214
|
+
### Fixed
|
|
215
|
+
- `ThermodynamicModel.add_point()` now returns the created `Prop`. It
|
|
216
|
+
registered the point but returned `None`, so the documented
|
|
217
|
+
`inlet = Model.add_point(...)` capture pattern raised `AttributeError`.
|
|
218
|
+
- `Pipe` distinguishes an explicitly lossless line from an unspecified one.
|
|
219
|
+
`Pressure_drop` and `Temperature_drop` default to `None` and are tested
|
|
220
|
+
with `is not None`; they previously defaulted to `0` and were tested with
|
|
221
|
+
`!= 0`, so `Pipe(..., Pressure_drop=0)` raised
|
|
222
|
+
`Need at least 2 of (P_in, P_out, Pressure_drop)`.
|
|
223
|
+
- `Turbine`, `Pump` and `Compressor` raise on a mass-flow mismatch between
|
|
224
|
+
their ports, via the shared `_resolve_mass_flowrate` helper. Previously
|
|
225
|
+
the mismatch was silent: work was computed from the inlet and the outlet
|
|
226
|
+
left unchanged.
|
|
227
|
+
- Defaulted mass flows are no longer propagated into the model. The 1 kg/s
|
|
228
|
+
fallback is a guess, and writing it to a shared state point caused a
|
|
229
|
+
later component that resolved the real rate to see a spurious mismatch.
|
|
230
|
+
- A lower enthalpy bound subtracted its margin instead of adding it,
|
|
231
|
+
placing the bound below the valid region.
|
|
232
|
+
## [3.2.2] - 2026-08-07
|
|
233
|
+
|
|
234
|
+
### Fixed — packaging
|
|
235
|
+
- `tabulate` added to the declared dependencies. It is imported at module
|
|
236
|
+
scope in `model.py`, so a clean `pip install ThermoSim` followed by
|
|
237
|
+
`import ThermoSim` previously raised `ModuleNotFoundError`.
|
|
238
|
+
- Placeholder author name/email replaced in `pyproject.toml`.
|
|
239
|
+
- `heat_exchangers_old.py` (dead, never imported) removed from the package.
|
|
240
|
+
- `.gitignore` added; it excludes private keys, `.env` files and build output.
|
|
241
|
+
- `README.md` no longer contains unresolved Git merge-conflict markers.
|
|
242
|
+
|
|
243
|
+
### Fixed — state points (`Prop`)
|
|
244
|
+
- `Q` is now ALWAYS numeric (0-1) or `None`. The human-readable phase label
|
|
245
|
+
moved to the new `Prop.phase` attribute. Previously `_classify_phase()`
|
|
246
|
+
overwrote `Q` with strings like `"Superheated (46.97 K)"`, which crashed
|
|
247
|
+
`Separator` and made any arithmetic on `Q` unsafe.
|
|
248
|
+
- Assigning the FIRST property to an under-defined point no longer raises
|
|
249
|
+
`ValueError: not enough values to unpack`; a solve is attempted only once
|
|
250
|
+
two independent inputs are present.
|
|
251
|
+
- Therminol-66 works again. `T66.json` was missing the `entropy` block that
|
|
252
|
+
`_calc_therminol66()` required, so every Therminol-66 state raised
|
|
253
|
+
`KeyError: 'entropy'`. Entropy is now integrated analytically from the Cp
|
|
254
|
+
polynomial (s = ∫Cp/T dT, which contains a log term and therefore cannot
|
|
255
|
+
be a polynomial). Whichever of (T, H) the user supplies is preserved
|
|
256
|
+
rather than round-tripped through the independent inverse fit, the fitted
|
|
257
|
+
temperature range is range-checked, and `'T66'` is accepted as an alias.
|
|
258
|
+
|
|
259
|
+
### Fixed — turbomachinery
|
|
260
|
+
- `Turbine`, `Compressor` and `Pump` write the resolved mass flow back to
|
|
261
|
+
their ports again. Since 3.1.1 the flow was kept local and left to the
|
|
262
|
+
propagation graph, which is only built by `Solve()` — so the documented
|
|
263
|
+
`Calculate=True` workflow left every downstream point at
|
|
264
|
+
`Mass_flowrate=None` and the next heat exchanger could not solve.
|
|
265
|
+
- Mass-flow resolution uses an explicit `is not None` test, so a legitimate
|
|
266
|
+
0 kg/s is no longer treated as missing.
|
|
267
|
+
|
|
268
|
+
### Fixed — heat exchangers
|
|
269
|
+
- Mass flows are resolved BEFORE the SimpleHEX branch. Previously that branch
|
|
270
|
+
returned early, so `Hot/Cold_Mass_flowrate` were still `None` and the
|
|
271
|
+
solver took the wrong path.
|
|
272
|
+
- `_solve_simple_hex()` rewritten: each side is now closed independently
|
|
273
|
+
against a shared duty, so a two-sided unit no longer solves only one side;
|
|
274
|
+
a fully-determined unit is idempotent (calling `Cal()` twice used to raise
|
|
275
|
+
"insufficient inputs", which made every `Solve()` iteration report phantom
|
|
276
|
+
failures); and an over-specified `Q` is checked rather than ignored.
|
|
277
|
+
- The NTU-effectiveness solver is reachable. It was the last `elif` in a
|
|
278
|
+
chain that had already consumed n = 0, 1, 2 and 3, so the `effectiveness`
|
|
279
|
+
argument silently did nothing.
|
|
280
|
+
- A failed 3-unknown solve no longer corrupts the model: the anchor is
|
|
281
|
+
validated against the solvable-case table first and rolled back on failure,
|
|
282
|
+
and both valid anchors are tried before giving up.
|
|
283
|
+
- Anchor rules no longer compare `None` temperatures (`TypeError`), and the
|
|
284
|
+
`H_co → H_hi` guard tests the correct temperature pair; it previously
|
|
285
|
+
reused the `H_ho → H_ci` condition verbatim.
|
|
286
|
+
- The `{H_co, mc}` retry passes the narrowed bracket instead of re-using the
|
|
287
|
+
bracket that had just failed, and cold-side enthalpies are no longer
|
|
288
|
+
bounded by hot-side enthalpies (meaningless across different fluids).
|
|
289
|
+
- `exclude_idx` masking now covers the final profile index, which is exactly
|
|
290
|
+
the anchored point it was meant to exclude.
|
|
291
|
+
- `_compute_outputs()` uses `is not None` instead of truthiness, so a valid
|
|
292
|
+
H = 0 or Q = 0 no longer silently skips the duty and UA calculations.
|
|
293
|
+
- Exergy destruction is computed per connected leg, so one-sided units report
|
|
294
|
+
a value instead of falling back to "Not Calculated" and printing to stdout.
|
|
295
|
+
- Solvable-case counts in the module docstring corrected to 10 (2-unknown)
|
|
296
|
+
and 8 (3-unknown), matching the logic table and the CHANGELOG. The list is
|
|
297
|
+
now a single constant, `HeatExchanger._SOLVABLE_2_UNKNOWN`.
|
|
298
|
+
- `HeatExchanger` and `TES` call `super().__init__()`; `TES` reuses the
|
|
299
|
+
shared mass-flow resolver instead of comparing floats with `==`.
|
|
300
|
+
|
|
301
|
+
### Fixed — model
|
|
302
|
+
- `ModelSummary()` skips unsolved components with a warning instead of
|
|
303
|
+
raising `TypeError` on `Q_in += None`.
|
|
304
|
+
- `Efficiency` is `nan` rather than the string `"Not Applicable"` for
|
|
305
|
+
refrigeration cycles, and `COP_R` / `COP_HP` are exposed as attributes.
|
|
306
|
+
- `Solve()` convergence uses a relative tolerance (new `tol` argument). The
|
|
307
|
+
old absolute 1e-6 J/kg was ~3e-13 relative on typical enthalpies.
|
|
308
|
+
- `Solve()` and the flow network are quiet unless `verbose=True`.
|
|
309
|
+
- `custom_order` defaults to `None` instead of a mutable list.
|
|
310
|
+
- `Point_print()` no longer advertises private attributes.
|
|
311
|
+
|
|
312
|
+
### Fixed — sensitivity analysis
|
|
313
|
+
- `compute_sensitivity_indices()` returned a meaningless `sensitivity_index`.
|
|
314
|
+
It normalised BOTH axes to [0, 1] before taking the gradient, so any
|
|
315
|
+
monotonic near-linear sweep gave ~1.0000 regardless of the parameter — the
|
|
316
|
+
metric could not rank parameters against each other, which is the one thing
|
|
317
|
+
the wiki instructs users to do with it. It is now the mean **elasticity**,
|
|
318
|
+
S = (dY/dX)·(X/Y): dimensionless, so parameters in kelvin and pascals stay
|
|
319
|
+
comparable. Validation on a simple Rankine cycle now gives eta_t → 1.01
|
|
320
|
+
(cycle efficiency is nearly proportional to turbine efficiency, as
|
|
321
|
+
expected), T1 → 0.27, P_cond → 0.07. The raw slope is also returned, as
|
|
322
|
+
the new `mean_gradient` key.
|
|
323
|
+
- `ModelSummary()` accepts `verbose=True` (default). `SensitivityAnalyzer`
|
|
324
|
+
calls it with `verbose=False`, so a sweep no longer prints a full
|
|
325
|
+
energy + exergy table for every single evaluation point.
|
|
326
|
+
|
|
327
|
+
### Fixed — components, plotting, analysis
|
|
328
|
+
- `Splitter` treats a known inlet flow as authoritative instead of averaging
|
|
329
|
+
it with stale outlet flows. Re-running a splitter with refined fractions
|
|
330
|
+
(as the regenerative Rankine example does) previously broke mass
|
|
331
|
+
conservation downstream.
|
|
332
|
+
- Every component `__str__` returns "not yet solved" instead of raising.
|
|
333
|
+
- `plot_energy_summary()` no longer raises `NameError` on `Compressor` in its
|
|
334
|
+
import-failure fallback, and tolerates unsolved components.
|
|
335
|
+
- `'Water'` removed from `INCOMPRESSIBLE_FLUIDS`; it is an ordinary
|
|
336
|
+
compressible CoolProp fluid and was silently losing its saturation dome.
|
|
337
|
+
- `plot_hex_profile()` reports a clear error instead of dividing by `None`.
|
|
338
|
+
- `compute_sensitivity_indices()` guards against a zero-width parameter range.
|
|
339
|
+
- Bare `except:` replaced with `except Exception:` throughout (22 sites).
|
|
340
|
+
- Corrected `thermocycle.*` import paths in docstrings to `ThermoSim.*`.
|
|
341
|
+
|
|
342
|
+
## [3.2.0] - 2026-04-20
|
|
343
|
+
- Solver is now more robust.
|
|
344
|
+
- User can give the order of solving components.
|
|
345
|
+
- Now HEX can solve 10 cases with 2 unknows and 8 cases with 3 unknwons
|
|
346
|
+
- Mass flowrate now can automatically propgate through the branches.
|
|
347
|
+
|
|
348
|
+
## [3.1.1] - 2026-03-31
|
|
349
|
+
- Fix the bug in testing.
|
|
350
|
+
## [3.1.0] - 2026-03-30
|
|
351
|
+
- Energy-balance consistency check when all four states are known.
|
|
352
|
+
- Mass-flow written back after PPT/brentq solvers (was silently discarded).
|
|
353
|
+
- Upfront problem classifier (_classify_problem) catches zero-info,
|
|
354
|
+
both-m̊-unknown, and over-specified inputs before any computation.
|
|
355
|
+
- Silent PPT mutation (self.PPT -= 0.001) replaced with a proper
|
|
356
|
+
ValueError so pinch violations are never hidden.
|
|
357
|
+
- Phase-change guard in _solve_effectiveness(); falls back to
|
|
358
|
+
enthalpy-based Q when either stream is two-phase (Cp is ill-defined).
|
|
359
|
+
- TES over-specified branch added: CI.H, CO.H, and Capacity all known
|
|
360
|
+
→ consistency check instead of silent fall-through.
|
|
361
|
+
- warnings.warn(RuntimeWarning) replaces bare print() in solver errors;
|
|
362
|
+
Solution_Status set False and Cal() returns cleanly on non-fatal failures.
|
|
363
|
+
- SimpleHEX two-sided consistency path: when both cold and hot sides are
|
|
364
|
+
fully given, Q from each side is compared and averaged if consistent.
|
|
365
|
+
- _resolve_side() raises immediately when both mass-flows are None
|
|
366
|
+
(was a silent None that propagated to arithmetic errors later).
|
|
367
|
+
- exergy sign and base-class helper preserved from v1.x bugfix baseline.
|
|
368
|
+
- All three Cal() methods now handle a "both-known" branch that validates
|
|
369
|
+
isentropic consistency and emits a warning when the supplied enthalpy
|
|
370
|
+
pair violates the declared efficiency.
|
|
371
|
+
- Pump.Cal() Incompressible branch now handles reverse (In.H is None)
|
|
372
|
+
and both-known sub-cases with proper guards.
|
|
373
|
+
- fsolve calls use physically-motivated initial guesses (rather than
|
|
374
|
+
the outlet enthalpy) and check convergence with full_output=True.
|
|
375
|
+
- Guard added in the Incompressible path for In.D being None.
|
|
376
|
+
- Duplicate 'from scipy.optimize import fsolve' inside Compressor.Cal()
|
|
377
|
+
removed.
|
|
378
|
+
- All three __str__ methods guard against Solution_Status=False (i.e.
|
|
379
|
+
Cal() was never called) so printing an unsolved component does not
|
|
380
|
+
raise AttributeError on self.In / self.Out.
|
|
381
|
+
|
|
382
|
+
- CSV Export Functionality
|
|
383
|
+
|
|
384
|
+
Added CSV export capability to all plotting methods
|
|
385
|
+
|
|
386
|
+
- save_csv parameter (default: False) - enables CSV export
|
|
387
|
+
- csv_path parameter (optional) - custom file path, auto-generates if not provided
|
|
388
|
+
- Auto-generated filenames include timestamp (e.g., Ts_diagram_20240115_143022.csv)
|
|
389
|
+
New utility methods:
|
|
390
|
+
|
|
391
|
+
- export_all_points() - Export all thermodynamic state points to CSV
|
|
392
|
+
- export_all_components() - Export all component performance data to CSV
|
|
393
|
+
- _generate_csv_filename() - Auto-generate timestamped filenames
|
|
394
|
+
- _save_dataframe_to_csv() - Centralized CSV saving with error handling
|
|
395
|
+
Enhanced Data Structure
|
|
396
|
+
|
|
397
|
+
CSV files now include:
|
|
398
|
+
- Original SI units (J/kg, Pa, K)
|
|
399
|
+
- Converted engineering units (kJ/kg, bar, °C)
|
|
400
|
+
- Point/component identification
|
|
401
|
+
- Calculated derivatives (percentages, ratios)
|
|
402
|
+
|
|
403
|
+
## [3.0.0] - 2026-03-25
|
|
404
|
+
|
|
405
|
+
### Added
|
|
406
|
+
- Modular package structure (`ThermoSIm/` package with separate files)
|
|
407
|
+
- `Compressor` component for gas cycles (e.g., Brayton)
|
|
408
|
+
- `CyclePlotter` class with T-s, P-h, h-s diagrams, saturation domes
|
|
409
|
+
- Exergy destruction bar chart and pie chart
|
|
410
|
+
- Heat exchanger temperature profile plots
|
|
411
|
+
- Energy flow summary visualisation
|
|
412
|
+
- `SensitivityAnalyzer` with single sweep, double sweep, and multi-output
|
|
413
|
+
- `save_model()` and `load_model()` for JSON serialisation
|
|
414
|
+
- Abstract `Component` base class with shared helpers
|
|
415
|
+
- 48+ unit tests (pytest)
|
|
416
|
+
- `.gitignore`, `README.md`, `CHANGELOG.md`, `setup.py`
|
|
417
|
+
|
|
418
|
+
### Fixed
|
|
419
|
+
- `__init__` was misspelled as `init` (constructor never called)
|
|
420
|
+
- Exergy sign error in HeatExchanger and TES: was `Out_hot - Out_cold`, now `Out_hot + Out_cold`
|
|
421
|
+
- `ModelSummary()` crashed on second call (DataFrame append issue)
|
|
422
|
+
- Type checking used fragile string comparison instead of `isinstance()`
|
|
423
|
+
- `Solve()` silently swallowed all errors
|
|
424
|
+
- Multiple typos in error messages and `__str__` methods
|
|
425
|
+
|
|
426
|
+
### Changed
|
|
427
|
+
- Dead state stored in `config.py` module instead of class variable
|
|
428
|
+
- Mass-flow resolution logic extracted to base class
|
|
429
|
+
- Visualisation completely separated from computation
|
|
430
|
+
- Iterative solver now checks convergence
|
|
431
|
+
|
|
432
|
+
## [2.0.0] - Previous version
|
|
433
|
+
- Single-file module
|
|
434
|
+
|
|
435
|
+
## [1.0.0] - Initial release
|
|
436
|
+
- Basic functionality
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c)
|
|
3
|
+
Copyright (c) 2025 [Md. Waheduzzaman Basunia Nouman]
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
thermosim-3.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ThermoSim
|
|
3
|
+
Version: 3.3.0
|
|
4
|
+
Summary: A Python package for thermodynamic cycle modelling and analysis
|
|
5
|
+
Author-email: "Md. Waheduzzaman Basunia Nouman" <md.waheduzzaman.nouman@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Nouman090/ThermoSim
|
|
8
|
+
Project-URL: Repository, https://github.com/Nouman090/ThermoSim
|
|
9
|
+
Project-URL: Issues, https://github.com/Nouman090/ThermoSim/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/Nouman090/ThermoSim/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: thermodynamics,rankine,brayton,ORC,exergy,CoolProp
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
17
|
+
Requires-Python: >=3.9
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: CoolProp
|
|
21
|
+
Requires-Dist: scipy
|
|
22
|
+
Requires-Dist: matplotlib
|
|
23
|
+
Requires-Dist: numpy
|
|
24
|
+
Requires-Dist: pandas
|
|
25
|
+
Requires-Dist: tabulate
|
|
26
|
+
Provides-Extra: optimization
|
|
27
|
+
Requires-Dist: pymoo; extra == "optimization"
|
|
28
|
+
Provides-Extra: test
|
|
29
|
+
Requires-Dist: pytest; extra == "test"
|
|
30
|
+
Provides-Extra: all
|
|
31
|
+
Requires-Dist: pymoo; extra == "all"
|
|
32
|
+
Requires-Dist: pytest; extra == "all"
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
[](https://doi.org/10.5281/zenodo.22016326)
|
|
36
|
+

|
|
37
|
+
# ThermoSim
|
|
38
|
+
|
|
39
|
+

|
|
40
|
+
|
|
41
|
+
**ThermoSim** is a Python package for modelling and analysing thermodynamic
|
|
42
|
+
power cycles, built on [CoolProp](http://www.coolprop.org/) for fluid
|
|
43
|
+
properties.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Features
|
|
48
|
+
|
|
49
|
+
- **State-point calculation** from any two independent properties
|
|
50
|
+
(P, T, H, S, Q, D) for water, refrigerants, air and every other CoolProp
|
|
51
|
+
fluid, plus a built-in Therminol-66 correlation
|
|
52
|
+
- **Components**: Turbine, Pump, Compressor, Heat Exchanger, TES, Mixer,
|
|
53
|
+
Splitter, Separator, Expansion Valve, Pipe, Source, Sink
|
|
54
|
+
- **Pinch analysis** for heat exchangers, with a generalised solver that
|
|
55
|
+
closes up to three unknowns from
|
|
56
|
+
`{H_hi, H_ho, H_ci, H_co, mh, mc}`
|
|
57
|
+
- **Automatic mass-flow propagation** through branches, splitters and mixers
|
|
58
|
+
- **Exergy analysis** against a configurable dead state
|
|
59
|
+
- **Cycle plots**: T-s, P-h and h-s diagrams with saturation domes, heat
|
|
60
|
+
exchanger temperature profiles, exergy bar/pie charts, energy summaries
|
|
61
|
+
- **Sensitivity analysis**: single-parameter, multi-output and 2D contour
|
|
62
|
+
sweeps, with CSV export
|
|
63
|
+
- **Save / load** model state to JSON
|
|
64
|
+
- **57 unit tests** (pytest)
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Installation
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pip install ThermoSim
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
To install from source:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
git clone https://github.com/Nouman090/ThermoSim.git
|
|
78
|
+
cd ThermoSim
|
|
79
|
+
pip install -e .
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Optional extras: `pip install "ThermoSim[all]"` adds `pymoo` (optimisation)
|
|
83
|
+
and `pytest` (testing).
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Quick example
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from ThermoSim import ThermodynamicModel, Turbine, Pump, HeatExchanger
|
|
91
|
+
from ThermoSim.plotting import CyclePlotter
|
|
92
|
+
|
|
93
|
+
Model = ThermodynamicModel()
|
|
94
|
+
Model.set_dead_state()
|
|
95
|
+
|
|
96
|
+
Model.add_point('water', '1', P=8e6, T=753.15, Mass_flowrate=1)
|
|
97
|
+
Model.add_point('water', '2', P=0.008e6)
|
|
98
|
+
Model.add_point('water', '3', P=0.008e6, Q=0)
|
|
99
|
+
Model.add_point('water', '4', P=8e6)
|
|
100
|
+
|
|
101
|
+
Turbine(Model, 'Turbine', '1', '2', n_isen=0.85, Calculate=True)
|
|
102
|
+
HeatExchanger(Model, 'Condenser', PPT=5, HEX_type='SimpleHEX',
|
|
103
|
+
HeatAdded=False, Hot_In_state='2', Hot_Out_state='3',
|
|
104
|
+
Cold_In_state=None, Cold_Out_state=None, Calculate=True)
|
|
105
|
+
Pump(Model, 'Pump', '3', '4', n_isen=1.0, Calculate=True)
|
|
106
|
+
HeatExchanger(Model, 'Boiler', PPT=5, HEX_type='SimpleHEX',
|
|
107
|
+
HeatAdded=True, Hot_In_state=None, Hot_Out_state=None,
|
|
108
|
+
Cold_In_state='4', Cold_Out_state='1', Calculate=True)
|
|
109
|
+
|
|
110
|
+
print(Model)
|
|
111
|
+
|
|
112
|
+
plotter = CyclePlotter(Model)
|
|
113
|
+
plotter.plot_Ts_diagram(['1', '2', '3', '4', '1'])
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
For cycles with recycle loops or branches, build the components without
|
|
117
|
+
`Calculate=True` and let the iterative solver handle the ordering:
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
Model.Solve(verbose=True)
|
|
121
|
+
Model.ModelSummary()
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Running the tests
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
pytest tests/ -v
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Who this is for
|
|
135
|
+
|
|
136
|
+
- **Engineering students** studying thermodynamics, energy systems and heat
|
|
137
|
+
transfer, who want to simulate real energy systems rather than only work
|
|
138
|
+
through closed-form textbook problems.
|
|
139
|
+
- **Researchers** modelling, optimising and analysing thermodynamic and
|
|
140
|
+
energy-efficiency problems.
|
|
141
|
+
- **Energy system designers** working on power plants, heat exchanger
|
|
142
|
+
networks, refrigeration cycles and renewable energy systems.
|
|
143
|
+
|
|
144
|
+
### Typical applications
|
|
145
|
+
|
|
146
|
+
1. **Heat exchanger design and optimisation** — double-pipe units,
|
|
147
|
+
evaporators and condensers, with pinch-point analysis to trade off
|
|
148
|
+
thermal effectiveness against surface area.
|
|
149
|
+
2. **Pumps, turbines and compressors** — work output, isentropic efficiency
|
|
150
|
+
and energy transfer for power generation, refrigeration and HVAC.
|
|
151
|
+
3. **Energy efficiency analysis** — exergy destruction accounting to locate
|
|
152
|
+
where useful work is actually being lost.
|
|
153
|
+
4. **Thermodynamic cycle simulation** — Rankine, regenerative Rankine,
|
|
154
|
+
Brayton, ORC and refrigeration cycles.
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Resources
|
|
159
|
+
|
|
160
|
+
- [**PyPI package**](https://pypi.org/project/ThermoSim/)
|
|
161
|
+
- [**Source code**](https://github.com/Nouman090/ThermoSim)
|
|
162
|
+
- [**Report an issue**](https://github.com/Nouman090/ThermoSim/issues)
|
|
163
|
+
- [**Wiki**](https://github.com/Nouman090/ThermoSim/wiki)
|
|
164
|
+
- [**Changelog**](https://github.com/Nouman090/ThermoSim/blob/main/CHANGELOG.md)
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Contributing
|
|
169
|
+
|
|
170
|
+
Contributions are welcome. Fork the repository and open a pull request; for
|
|
171
|
+
major changes, please open an issue to discuss it first.
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## License
|
|
176
|
+
|
|
177
|
+
MIT — see [LICENSE](https://github.com/Nouman090/ThermoSim/blob/main/LICENSE).
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Acknowledgements
|
|
182
|
+
|
|
183
|
+
Created and maintained by
|
|
184
|
+
[Md. Waheduzzaman Nouman](https://github.com/Nouman090), for educational and
|
|
185
|
+
research use.
|