pifira 0.1.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.
- pifira-0.1.0/CITATION.cff +29 -0
- pifira-0.1.0/LICENSE +21 -0
- pifira-0.1.0/MANIFEST.in +4 -0
- pifira-0.1.0/PKG-INFO +183 -0
- pifira-0.1.0/README.md +147 -0
- pifira-0.1.0/examples/analyze_localized.py +93 -0
- pifira-0.1.0/pyproject.toml +56 -0
- pifira-0.1.0/setup.cfg +4 -0
- pifira-0.1.0/src/pifira/__init__.py +60 -0
- pifira-0.1.0/src/pifira/effective_area.py +99 -0
- pifira-0.1.0/src/pifira/io.py +74 -0
- pifira-0.1.0/src/pifira/pressure_rise.py +150 -0
- pifira-0.1.0/src/pifira/representative.py +64 -0
- pifira-0.1.0/src/pifira.egg-info/PKG-INFO +183 -0
- pifira-0.1.0/src/pifira.egg-info/SOURCES.txt +17 -0
- pifira-0.1.0/src/pifira.egg-info/dependency_links.txt +1 -0
- pifira-0.1.0/src/pifira.egg-info/requires.txt +11 -0
- pifira-0.1.0/src/pifira.egg-info/top_level.txt +1 -0
- pifira-0.1.0/tests/test_pifira.py +76 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
cff-version: 1.2.0
|
|
2
|
+
message: "If you use this software, please cite it as below."
|
|
3
|
+
title: "pifira: Pressure-based Inverse Fire-heat-flux and Radiative-Area analysis"
|
|
4
|
+
abstract: >-
|
|
5
|
+
A data-agnostic Python toolkit for estimating fire heat input and effective
|
|
6
|
+
flame-contact area for pressurized LPG tanks under localized (non-engulfing)
|
|
7
|
+
flame exposure. It combines the closed-vessel pressure-rise method with a
|
|
8
|
+
radiative (T^4) weighted angular-temperature inversion to recover the
|
|
9
|
+
effective fire-contact area and heat flux, and provides a dimensionless
|
|
10
|
+
framework unifying localized and full-engulfment exposure across tank sizes,
|
|
11
|
+
fill levels and orientations.
|
|
12
|
+
type: software
|
|
13
|
+
authors:
|
|
14
|
+
- given-names: "Woo-gui-yeon"
|
|
15
|
+
family-names: "Lee"
|
|
16
|
+
affiliation: "Korea Gas Safety Corporation (KGS), AI Safety Research Team"
|
|
17
|
+
orcid: "https://orcid.org/0009-0008-8976-5363"
|
|
18
|
+
version: "0.1.0"
|
|
19
|
+
license: MIT
|
|
20
|
+
keywords:
|
|
21
|
+
- LPG
|
|
22
|
+
- fire safety
|
|
23
|
+
- heat flux
|
|
24
|
+
- pressure relief valve
|
|
25
|
+
- closed-vessel pressure-rise method
|
|
26
|
+
- BLEVE
|
|
27
|
+
- inverse method
|
|
28
|
+
- process safety
|
|
29
|
+
repository-code: "https://github.com/lyullee/pifira"
|
pifira-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Woo-gui-yeon Lee, Korea Gas Safety Corporation (KGS), AI Safety Research Team
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
pifira-0.1.0/MANIFEST.in
ADDED
pifira-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pifira
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pressure-based inverse estimation of fire heat flux and effective flame-contact area for pressurized LPG tanks under localized flame exposure
|
|
5
|
+
Author: Woo-gui-yeon Lee
|
|
6
|
+
Maintainer: Woo-gui-yeon Lee
|
|
7
|
+
License: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/lyullee/pifira
|
|
9
|
+
Project-URL: Repository, https://github.com/lyullee/pifira
|
|
10
|
+
Project-URL: Issues, https://github.com/lyullee/pifira/issues
|
|
11
|
+
Keywords: LPG,fire safety,heat flux,pressure relief valve,closed-vessel,pressure-rise method,BLEVE,process safety,thermal engineering,inverse method
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: numpy>=1.20
|
|
27
|
+
Requires-Dist: scipy>=1.7
|
|
28
|
+
Requires-Dist: pandas>=1.3
|
|
29
|
+
Requires-Dist: CoolProp>=6.4
|
|
30
|
+
Provides-Extra: plot
|
|
31
|
+
Requires-Dist: matplotlib>=3.4; extra == "plot"
|
|
32
|
+
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
34
|
+
Requires-Dist: matplotlib>=3.4; extra == "dev"
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
# pifira
|
|
38
|
+
|
|
39
|
+
**P**ressure-based **I**nverse **F**ire-heat-flux and **R**adiative-**A**rea analysis
|
|
40
|
+
|
|
41
|
+
A data-agnostic Python toolkit for estimating **fire heat input** and
|
|
42
|
+
**effective flame-contact area** for pressurized LPG tanks under **localized
|
|
43
|
+
(non-engulfing) flame exposure**.
|
|
44
|
+
|
|
45
|
+
pifira combines the **closed-vessel pressure-rise method** (heat input from a
|
|
46
|
+
measured pressure history) with a **radiative (T?? weighted angular-temperature
|
|
47
|
+
inversion** (effective fire-contact area from surface temperatures), and
|
|
48
|
+
provides a **dimensionless framework** that unifies localized and
|
|
49
|
+
full-engulfment exposure across tank sizes, fill levels and orientations.
|
|
50
|
+
|
|
51
|
+
> **No experimental data are bundled with this package.** All routines operate
|
|
52
|
+
> on user-supplied arrays. See [Data sources](#data-sources) for how to obtain
|
|
53
|
+
> the public validation datasets.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Why
|
|
58
|
+
|
|
59
|
+
Most published fire-engulfment work assumes *uniform, full engulfment* of
|
|
60
|
+
*horizontal* tanks. Small **vertical** LPG tanks under *localized* flame
|
|
61
|
+
exposure are under-studied, and standards (API 521, KGS) do not cover heat-input
|
|
62
|
+
estimation for this case. pifira addresses this by inverting a measured
|
|
63
|
+
pressure rise for heat input and a measured angular temperature distribution
|
|
64
|
+
for the effective contact area ??without assuming full engulfment.
|
|
65
|
+
|
|
66
|
+
## Installation
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pip install pifira
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Dependencies: `numpy`, `scipy`, `pandas`, `CoolProp`. Optional plotting extras:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
pip install "pifira[plot]"
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Quick start
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
import numpy as np
|
|
82
|
+
import pifira
|
|
83
|
+
|
|
84
|
+
# 1. Define the tank (volume, fluid, fill fraction)
|
|
85
|
+
tank = pifira.TankSpec(
|
|
86
|
+
volume=0.619,
|
|
87
|
+
fluid="HEOS::Propane[0.95]&n-Butane[0.05]",
|
|
88
|
+
fill=0.70,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# 2. Heat input from a pressure history (bring your own arrays)
|
|
92
|
+
# t [s], P [bar gauge], closed-vessel window before PRV opens
|
|
93
|
+
Q_W = pifira.heat_input(P1_bar=10.83, P2_bar=16.30, dt=544.5, tank=tank)
|
|
94
|
+
print(Q_W / 1000, "kW")
|
|
95
|
+
|
|
96
|
+
# 3. Effective contact area from angular wall temperatures
|
|
97
|
+
# flame centre at 0 deg; off-flame points at +/-90, 180
|
|
98
|
+
res = pifira.effective_arc(
|
|
99
|
+
angles_deg=[0, 90, 180, -90],
|
|
100
|
+
T_wall_C=[271, 49, 39, 46], # period-averaged wall temps
|
|
101
|
+
T_amb_C=35.0,
|
|
102
|
+
)
|
|
103
|
+
arc_deg = res["arc_deg"] # area-preserving equivalent width
|
|
104
|
+
|
|
105
|
+
h_eff = pifira.effective_height(flame_height_m=0.767, liquid_height_m=0.885)
|
|
106
|
+
A_eff = pifira.effective_area(diameter_m=0.75, arc_deg=arc_deg, height_m=h_eff)
|
|
107
|
+
|
|
108
|
+
# 4. Heat flux
|
|
109
|
+
print(pifira.heat_flux(Q_W, A_eff), "kW/m^2")
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
A full time-varying workflow (moving-window heat input, per-instant effective
|
|
113
|
+
arc, heat-input-weighted representative value) is in
|
|
114
|
+
[`examples/analyze_localized.py`](examples/analyze_localized.py).
|
|
115
|
+
|
|
116
|
+
## Public API
|
|
117
|
+
|
|
118
|
+
| Area | Functions |
|
|
119
|
+
|---|---|
|
|
120
|
+
| Heat input (pressure-rise) | `TankSpec`, `internal_energy`, `liquid_height`, `heat_input`, `heat_input_series` |
|
|
121
|
+
| Effective area (inversion) | `radiative_driving_force`, `effective_arc`, `effective_height`, `effective_area`, `heat_flux` |
|
|
122
|
+
| Representative value | `heat_input_weighted_mean`, `simple_mean` |
|
|
123
|
+
| Dimensionless framework | `coverage_fraction` (?), `normalized_flux` (q\*), `pressurization_number` (?), `orientation_factor` (Ω) |
|
|
124
|
+
| I/O | `load_pressure_csv`, `load_temperature_csv` |
|
|
125
|
+
|
|
126
|
+
## Method summary
|
|
127
|
+
|
|
128
|
+
1. **Closed-vessel pressure-rise.** Before pressure-relief venting, incident
|
|
129
|
+
heat converts entirely into the contents' internal-energy rise:
|
|
130
|
+
`Q = dU/dt = d/dt(m_L u_L + m_V u_V)`. Mass and volume are fixed; the
|
|
131
|
+
liquid/vapour split is re-flashed at each pressure (CoolProp).
|
|
132
|
+
|
|
133
|
+
2. **Radiative (T?? angular inversion.** Radiative flux scales with
|
|
134
|
+
`T_wall????T_amb?? (Stefan-Boltzmann). Angular wall temperatures are fit
|
|
135
|
+
with a Gaussian; the area-preserving equivalent width `??2?)·?` gives the
|
|
136
|
+
effective arc.
|
|
137
|
+
|
|
138
|
+
3. **Axial intersection.** Effective height = min(flame contact height, wetted
|
|
139
|
+
liquid height). Vapour-space wall is excluded.
|
|
140
|
+
|
|
141
|
+
4. **Representative value.** Heat-input-weighted mean `Σ(q·Q)/ΣQ`, since fire
|
|
142
|
+
safety is governed by high-heat-input instants.
|
|
143
|
+
|
|
144
|
+
5. **Dimensionless framework.** ? (coverage), q\* (normalized flux), ?
|
|
145
|
+
(pressurization), Ω (orientation) unify localized/full and vertical/horizontal.
|
|
146
|
+
|
|
147
|
+
## Data sources
|
|
148
|
+
|
|
149
|
+
pifira ships **no data**. The public datasets used to validate the method in
|
|
150
|
+
the associated study are available from their original publications:
|
|
151
|
+
|
|
152
|
+
- **Moodie, K., Billinge, K., Cutler, D.P. (1988).** *Fire Engulfment of LPG
|
|
153
|
+
Storage Tanks.* IChemE Symposium Series No. 93, pp. 87??06. Institution of
|
|
154
|
+
Chemical Engineers. ??pressure history (Fig. 5) and initial conditions /
|
|
155
|
+
heat fluxes (Tables 1??). Obtain from IChemE or a library.
|
|
156
|
+
|
|
157
|
+
- **Birk, A.M., Poirier, D., Davison, C. (2006).** *On the thermal rupture of
|
|
158
|
+
1.9 m³ propane pressure vessels with defects in their thermal protection
|
|
159
|
+
system.* Journal of Loss Prevention in the Process Industries **19**(6),
|
|
160
|
+
582??97. https://doi.org/10.1016/j.jlp.2006.02.002 ??pressure (Fig. 8) and
|
|
161
|
+
lading-temperature stratification (Fig. 10) for the 25% partial-engulfment
|
|
162
|
+
test. Obtain from the publisher.
|
|
163
|
+
|
|
164
|
+
To reproduce the validation, digitize the relevant figures from the original
|
|
165
|
+
papers and pass the values to the pifira API. Digitized values are **not**
|
|
166
|
+
redistributed here.
|
|
167
|
+
|
|
168
|
+
Experimental data from the authors' own fire tests are **not** included and are
|
|
169
|
+
reported separately.
|
|
170
|
+
|
|
171
|
+
## Citing
|
|
172
|
+
|
|
173
|
+
If you use pifira, please cite it via [`CITATION.cff`](CITATION.cff) and the
|
|
174
|
+
archived release DOI (Zenodo). See the repository release page for the DOI.
|
|
175
|
+
|
|
176
|
+
## License
|
|
177
|
+
|
|
178
|
+
MIT ??see [LICENSE](LICENSE).
|
|
179
|
+
|
|
180
|
+
## Author
|
|
181
|
+
|
|
182
|
+
Woo-gui-yeon Lee, Korea Gas Safety Corporation (KGS), AI Safety Research Team
|
|
183
|
+
([ORCID 0009-0008-8976-5363](https://orcid.org/0009-0008-8976-5363)).
|
pifira-0.1.0/README.md
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# pifira
|
|
2
|
+
|
|
3
|
+
**P**ressure-based **I**nverse **F**ire-heat-flux and **R**adiative-**A**rea analysis
|
|
4
|
+
|
|
5
|
+
A data-agnostic Python toolkit for estimating **fire heat input** and
|
|
6
|
+
**effective flame-contact area** for pressurized LPG tanks under **localized
|
|
7
|
+
(non-engulfing) flame exposure**.
|
|
8
|
+
|
|
9
|
+
pifira combines the **closed-vessel pressure-rise method** (heat input from a
|
|
10
|
+
measured pressure history) with a **radiative (T?? weighted angular-temperature
|
|
11
|
+
inversion** (effective fire-contact area from surface temperatures), and
|
|
12
|
+
provides a **dimensionless framework** that unifies localized and
|
|
13
|
+
full-engulfment exposure across tank sizes, fill levels and orientations.
|
|
14
|
+
|
|
15
|
+
> **No experimental data are bundled with this package.** All routines operate
|
|
16
|
+
> on user-supplied arrays. See [Data sources](#data-sources) for how to obtain
|
|
17
|
+
> the public validation datasets.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Why
|
|
22
|
+
|
|
23
|
+
Most published fire-engulfment work assumes *uniform, full engulfment* of
|
|
24
|
+
*horizontal* tanks. Small **vertical** LPG tanks under *localized* flame
|
|
25
|
+
exposure are under-studied, and standards (API 521, KGS) do not cover heat-input
|
|
26
|
+
estimation for this case. pifira addresses this by inverting a measured
|
|
27
|
+
pressure rise for heat input and a measured angular temperature distribution
|
|
28
|
+
for the effective contact area ??without assuming full engulfment.
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install pifira
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Dependencies: `numpy`, `scipy`, `pandas`, `CoolProp`. Optional plotting extras:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install "pifira[plot]"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Quick start
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
import numpy as np
|
|
46
|
+
import pifira
|
|
47
|
+
|
|
48
|
+
# 1. Define the tank (volume, fluid, fill fraction)
|
|
49
|
+
tank = pifira.TankSpec(
|
|
50
|
+
volume=0.619,
|
|
51
|
+
fluid="HEOS::Propane[0.95]&n-Butane[0.05]",
|
|
52
|
+
fill=0.70,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# 2. Heat input from a pressure history (bring your own arrays)
|
|
56
|
+
# t [s], P [bar gauge], closed-vessel window before PRV opens
|
|
57
|
+
Q_W = pifira.heat_input(P1_bar=10.83, P2_bar=16.30, dt=544.5, tank=tank)
|
|
58
|
+
print(Q_W / 1000, "kW")
|
|
59
|
+
|
|
60
|
+
# 3. Effective contact area from angular wall temperatures
|
|
61
|
+
# flame centre at 0 deg; off-flame points at +/-90, 180
|
|
62
|
+
res = pifira.effective_arc(
|
|
63
|
+
angles_deg=[0, 90, 180, -90],
|
|
64
|
+
T_wall_C=[271, 49, 39, 46], # period-averaged wall temps
|
|
65
|
+
T_amb_C=35.0,
|
|
66
|
+
)
|
|
67
|
+
arc_deg = res["arc_deg"] # area-preserving equivalent width
|
|
68
|
+
|
|
69
|
+
h_eff = pifira.effective_height(flame_height_m=0.767, liquid_height_m=0.885)
|
|
70
|
+
A_eff = pifira.effective_area(diameter_m=0.75, arc_deg=arc_deg, height_m=h_eff)
|
|
71
|
+
|
|
72
|
+
# 4. Heat flux
|
|
73
|
+
print(pifira.heat_flux(Q_W, A_eff), "kW/m^2")
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
A full time-varying workflow (moving-window heat input, per-instant effective
|
|
77
|
+
arc, heat-input-weighted representative value) is in
|
|
78
|
+
[`examples/analyze_localized.py`](examples/analyze_localized.py).
|
|
79
|
+
|
|
80
|
+
## Public API
|
|
81
|
+
|
|
82
|
+
| Area | Functions |
|
|
83
|
+
|---|---|
|
|
84
|
+
| Heat input (pressure-rise) | `TankSpec`, `internal_energy`, `liquid_height`, `heat_input`, `heat_input_series` |
|
|
85
|
+
| Effective area (inversion) | `radiative_driving_force`, `effective_arc`, `effective_height`, `effective_area`, `heat_flux` |
|
|
86
|
+
| Representative value | `heat_input_weighted_mean`, `simple_mean` |
|
|
87
|
+
| Dimensionless framework | `coverage_fraction` (?), `normalized_flux` (q\*), `pressurization_number` (?), `orientation_factor` (Ω) |
|
|
88
|
+
| I/O | `load_pressure_csv`, `load_temperature_csv` |
|
|
89
|
+
|
|
90
|
+
## Method summary
|
|
91
|
+
|
|
92
|
+
1. **Closed-vessel pressure-rise.** Before pressure-relief venting, incident
|
|
93
|
+
heat converts entirely into the contents' internal-energy rise:
|
|
94
|
+
`Q = dU/dt = d/dt(m_L u_L + m_V u_V)`. Mass and volume are fixed; the
|
|
95
|
+
liquid/vapour split is re-flashed at each pressure (CoolProp).
|
|
96
|
+
|
|
97
|
+
2. **Radiative (T?? angular inversion.** Radiative flux scales with
|
|
98
|
+
`T_wall????T_amb?? (Stefan-Boltzmann). Angular wall temperatures are fit
|
|
99
|
+
with a Gaussian; the area-preserving equivalent width `??2?)·?` gives the
|
|
100
|
+
effective arc.
|
|
101
|
+
|
|
102
|
+
3. **Axial intersection.** Effective height = min(flame contact height, wetted
|
|
103
|
+
liquid height). Vapour-space wall is excluded.
|
|
104
|
+
|
|
105
|
+
4. **Representative value.** Heat-input-weighted mean `Σ(q·Q)/ΣQ`, since fire
|
|
106
|
+
safety is governed by high-heat-input instants.
|
|
107
|
+
|
|
108
|
+
5. **Dimensionless framework.** ? (coverage), q\* (normalized flux), ?
|
|
109
|
+
(pressurization), Ω (orientation) unify localized/full and vertical/horizontal.
|
|
110
|
+
|
|
111
|
+
## Data sources
|
|
112
|
+
|
|
113
|
+
pifira ships **no data**. The public datasets used to validate the method in
|
|
114
|
+
the associated study are available from their original publications:
|
|
115
|
+
|
|
116
|
+
- **Moodie, K., Billinge, K., Cutler, D.P. (1988).** *Fire Engulfment of LPG
|
|
117
|
+
Storage Tanks.* IChemE Symposium Series No. 93, pp. 87??06. Institution of
|
|
118
|
+
Chemical Engineers. ??pressure history (Fig. 5) and initial conditions /
|
|
119
|
+
heat fluxes (Tables 1??). Obtain from IChemE or a library.
|
|
120
|
+
|
|
121
|
+
- **Birk, A.M., Poirier, D., Davison, C. (2006).** *On the thermal rupture of
|
|
122
|
+
1.9 m³ propane pressure vessels with defects in their thermal protection
|
|
123
|
+
system.* Journal of Loss Prevention in the Process Industries **19**(6),
|
|
124
|
+
582??97. https://doi.org/10.1016/j.jlp.2006.02.002 ??pressure (Fig. 8) and
|
|
125
|
+
lading-temperature stratification (Fig. 10) for the 25% partial-engulfment
|
|
126
|
+
test. Obtain from the publisher.
|
|
127
|
+
|
|
128
|
+
To reproduce the validation, digitize the relevant figures from the original
|
|
129
|
+
papers and pass the values to the pifira API. Digitized values are **not**
|
|
130
|
+
redistributed here.
|
|
131
|
+
|
|
132
|
+
Experimental data from the authors' own fire tests are **not** included and are
|
|
133
|
+
reported separately.
|
|
134
|
+
|
|
135
|
+
## Citing
|
|
136
|
+
|
|
137
|
+
If you use pifira, please cite it via [`CITATION.cff`](CITATION.cff) and the
|
|
138
|
+
archived release DOI (Zenodo). See the repository release page for the DOI.
|
|
139
|
+
|
|
140
|
+
## License
|
|
141
|
+
|
|
142
|
+
MIT ??see [LICENSE](LICENSE).
|
|
143
|
+
|
|
144
|
+
## Author
|
|
145
|
+
|
|
146
|
+
Woo-gui-yeon Lee, Korea Gas Safety Corporation (KGS), AI Safety Research Team
|
|
147
|
+
([ORCID 0009-0008-8976-5363](https://orcid.org/0009-0008-8976-5363)).
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Example: full localized-exposure heat-flux workflow with pifira.
|
|
5
|
+
|
|
6
|
+
This reproduces the time-varying analysis:
|
|
7
|
+
moving-window heat input x per-instant radiative-weighted effective arc
|
|
8
|
+
x axial intersection -> heat-input-weighted representative flux.
|
|
9
|
+
|
|
10
|
+
No data are bundled. Provide your own pressure/temperature CSVs via the
|
|
11
|
+
command line:
|
|
12
|
+
|
|
13
|
+
python analyze_localized.py PRESSURE.csv TEMPERATURE.csv
|
|
14
|
+
|
|
15
|
+
Pressure CSV : columns [idx, datetime, seconds, pressure_bar]
|
|
16
|
+
Temperature : columns [idx, datetime, seconds, CH1..CH10]
|
|
17
|
+
with the flame-facing sensors identified by *temperature*
|
|
18
|
+
(hottest = flame side), not by any label.
|
|
19
|
+
|
|
20
|
+
Sensor angular layout used here (edit to match your rig):
|
|
21
|
+
0 deg = flame centre (hottest circumferential sensor)
|
|
22
|
+
+90 deg = top (12 o'clock)
|
|
23
|
+
180 deg = rear (opposite the flame)
|
|
24
|
+
-90 deg = bottom (6 o'clock) <-- note: bottom, not a side sensor
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import sys
|
|
28
|
+
import numpy as np
|
|
29
|
+
|
|
30
|
+
import pifira
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ----- rig-specific configuration (edit for your setup) -------------------
|
|
34
|
+
DIAMETER_M = 0.75
|
|
35
|
+
VOLUME_M3 = 0.619
|
|
36
|
+
FLUID = "HEOS::Propane[0.95]&n-Butane[0.05]"
|
|
37
|
+
FILL = 0.70
|
|
38
|
+
CYL_LENGTH_M = 1.150
|
|
39
|
+
|
|
40
|
+
T_START, T_END = 205.5, 750.0 # closed-vessel window [s]
|
|
41
|
+
T_AMB_C = 35.0
|
|
42
|
+
FLAME_HEIGHT_M = CYL_LENGTH_M * (2 / 3) # observed flame contact height
|
|
43
|
+
|
|
44
|
+
# circumferential angle -> channel name (assign from your rig geometry)
|
|
45
|
+
CIRC = {0: "CH3", 90: "CH1", 180: "CH7", -90: "CH5"}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def main(pressure_csv, temperature_csv):
|
|
49
|
+
A_sec = np.pi / 4 * DIAMETER_M**2
|
|
50
|
+
V_head = np.pi / 24 * DIAMETER_M**3
|
|
51
|
+
|
|
52
|
+
t_p, P = pifira.load_pressure_csv(pressure_csv)
|
|
53
|
+
t_t, dft = pifira.load_temperature_csv(temperature_csv)
|
|
54
|
+
|
|
55
|
+
tank = pifira.TankSpec(volume=VOLUME_M3, fluid=FLUID, fill=FILL)
|
|
56
|
+
m_tot = tank.initial_mass(P[np.searchsorted(t_p, T_START)])
|
|
57
|
+
|
|
58
|
+
_, Q = pifira.heat_input_series(t_p, P, tank, window=60, m_tot=m_tot)
|
|
59
|
+
|
|
60
|
+
i0, i1 = np.searchsorted(t_p, T_START), np.searchsorted(t_p, T_END)
|
|
61
|
+
ql, Ql, widths = [], [], []
|
|
62
|
+
for i in range(i0, i1, 4):
|
|
63
|
+
Qi = Q[i]
|
|
64
|
+
if not np.isfinite(Qi):
|
|
65
|
+
continue
|
|
66
|
+
idx = np.searchsorted(t_t, t_p[i])
|
|
67
|
+
angles = list(CIRC.keys())
|
|
68
|
+
temps = [dft[CIRC[a]].iloc[idx] for a in angles]
|
|
69
|
+
try:
|
|
70
|
+
arc = pifira.effective_arc(angles, temps, T_amb_C=T_AMB_C)["arc_deg"]
|
|
71
|
+
except Exception:
|
|
72
|
+
continue
|
|
73
|
+
h_liq = pifira.liquid_height(P[i], m_tot, tank, A_sec, V_head)
|
|
74
|
+
h_eff = pifira.effective_height(FLAME_HEIGHT_M, h_liq)
|
|
75
|
+
A = pifira.effective_area(DIAMETER_M, arc, h_eff)
|
|
76
|
+
ql.append(pifira.heat_flux(Qi, A))
|
|
77
|
+
Ql.append(Qi)
|
|
78
|
+
widths.append(arc)
|
|
79
|
+
|
|
80
|
+
q_rep = pifira.heat_input_weighted_mean(ql, Ql)
|
|
81
|
+
print(f"total mass : {m_tot:.1f} kg")
|
|
82
|
+
print(f"effective arc (var): {min(widths):.0f}-{max(widths):.0f} deg "
|
|
83
|
+
f"(mean {np.mean(widths):.0f})")
|
|
84
|
+
print(f"heat-input weighted: {q_rep:.1f} kW/m^2 <- representative")
|
|
85
|
+
print(f"simple mean : {pifira.simple_mean(ql):.1f} kW/m^2")
|
|
86
|
+
print(f"instantaneous max : {max(ql):.1f} kW/m^2")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
if __name__ == "__main__":
|
|
90
|
+
if len(sys.argv) != 3:
|
|
91
|
+
print(__doc__)
|
|
92
|
+
sys.exit(1)
|
|
93
|
+
main(sys.argv[1], sys.argv[2])
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=64", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pifira"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Pressure-based inverse estimation of fire heat flux and effective flame-contact area for pressurized LPG tanks under localized flame exposure"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Woo-gui-yeon Lee" },
|
|
14
|
+
]
|
|
15
|
+
maintainers = [
|
|
16
|
+
{ name = "Woo-gui-yeon Lee" },
|
|
17
|
+
]
|
|
18
|
+
keywords = [
|
|
19
|
+
"LPG", "fire safety", "heat flux", "pressure relief valve",
|
|
20
|
+
"closed-vessel", "pressure-rise method", "BLEVE", "process safety",
|
|
21
|
+
"thermal engineering", "inverse method",
|
|
22
|
+
]
|
|
23
|
+
classifiers = [
|
|
24
|
+
"Development Status :: 4 - Beta",
|
|
25
|
+
"Intended Audience :: Science/Research",
|
|
26
|
+
"License :: OSI Approved :: MIT License",
|
|
27
|
+
"Operating System :: OS Independent",
|
|
28
|
+
"Programming Language :: Python :: 3",
|
|
29
|
+
"Programming Language :: Python :: 3.9",
|
|
30
|
+
"Programming Language :: Python :: 3.10",
|
|
31
|
+
"Programming Language :: Python :: 3.11",
|
|
32
|
+
"Programming Language :: Python :: 3.12",
|
|
33
|
+
"Topic :: Scientific/Engineering",
|
|
34
|
+
"Topic :: Scientific/Engineering :: Physics",
|
|
35
|
+
]
|
|
36
|
+
dependencies = [
|
|
37
|
+
"numpy>=1.20",
|
|
38
|
+
"scipy>=1.7",
|
|
39
|
+
"pandas>=1.3",
|
|
40
|
+
"CoolProp>=6.4",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[project.optional-dependencies]
|
|
44
|
+
plot = ["matplotlib>=3.4"]
|
|
45
|
+
dev = ["pytest>=7.0", "matplotlib>=3.4"]
|
|
46
|
+
|
|
47
|
+
[project.urls]
|
|
48
|
+
Homepage = "https://github.com/lyullee/pifira"
|
|
49
|
+
Repository = "https://github.com/lyullee/pifira"
|
|
50
|
+
Issues = "https://github.com/lyullee/pifira/issues"
|
|
51
|
+
|
|
52
|
+
[tool.setuptools.packages.find]
|
|
53
|
+
where = ["src"]
|
|
54
|
+
|
|
55
|
+
[tool.pytest.ini_options]
|
|
56
|
+
testpaths = ["tests"]
|
pifira-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pifira -- Pressure-based Inverse Fire-heat-flux and Radiative-Area analysis
|
|
3
|
+
==========================================================================
|
|
4
|
+
|
|
5
|
+
A data-agnostic toolkit for estimating fire heat input and effective
|
|
6
|
+
flame-contact area for pressurized LPG tanks under localized (non-engulfing)
|
|
7
|
+
flame exposure, using the closed-vessel pressure-rise method combined with a
|
|
8
|
+
radiative (T^4) weighted angular-temperature inversion.
|
|
9
|
+
|
|
10
|
+
No experimental data are bundled. Supply your own pressure/temperature series;
|
|
11
|
+
see the ``examples`` directory for the analysis workflow.
|
|
12
|
+
|
|
13
|
+
Public API
|
|
14
|
+
----------
|
|
15
|
+
Pressure-rise (heat input):
|
|
16
|
+
TankSpec, internal_energy, liquid_height, heat_input, heat_input_series
|
|
17
|
+
Effective area (inversion):
|
|
18
|
+
radiative_driving_force, effective_arc, effective_height,
|
|
19
|
+
effective_area, heat_flux
|
|
20
|
+
Representative value & dimensionless framework:
|
|
21
|
+
heat_input_weighted_mean, simple_mean, coverage_fraction,
|
|
22
|
+
normalized_flux, pressurization_number, orientation_factor
|
|
23
|
+
I/O:
|
|
24
|
+
load_pressure_csv, load_temperature_csv
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from .pressure_rise import (
|
|
28
|
+
TankSpec,
|
|
29
|
+
internal_energy,
|
|
30
|
+
liquid_height,
|
|
31
|
+
heat_input,
|
|
32
|
+
heat_input_series,
|
|
33
|
+
)
|
|
34
|
+
from .effective_area import (
|
|
35
|
+
radiative_driving_force,
|
|
36
|
+
effective_arc,
|
|
37
|
+
effective_height,
|
|
38
|
+
effective_area,
|
|
39
|
+
heat_flux,
|
|
40
|
+
)
|
|
41
|
+
from .representative import (
|
|
42
|
+
heat_input_weighted_mean,
|
|
43
|
+
simple_mean,
|
|
44
|
+
coverage_fraction,
|
|
45
|
+
normalized_flux,
|
|
46
|
+
pressurization_number,
|
|
47
|
+
orientation_factor,
|
|
48
|
+
)
|
|
49
|
+
from .io import load_pressure_csv, load_temperature_csv
|
|
50
|
+
|
|
51
|
+
__version__ = "0.1.0"
|
|
52
|
+
|
|
53
|
+
__all__ = [
|
|
54
|
+
"TankSpec", "internal_energy", "liquid_height", "heat_input",
|
|
55
|
+
"heat_input_series", "radiative_driving_force", "effective_arc",
|
|
56
|
+
"effective_height", "effective_area", "heat_flux",
|
|
57
|
+
"heat_input_weighted_mean", "simple_mean", "coverage_fraction",
|
|
58
|
+
"normalized_flux", "pressurization_number", "orientation_factor",
|
|
59
|
+
"load_pressure_csv", "load_temperature_csv",
|
|
60
|
+
]
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pifira.effective_area
|
|
3
|
+
=====================
|
|
4
|
+
Inverse estimation of the effective fire-contact area from surface-temperature
|
|
5
|
+
measurements under localized (non-engulfing) flame exposure.
|
|
6
|
+
|
|
7
|
+
Two directions are treated independently:
|
|
8
|
+
|
|
9
|
+
* Circumferential -- wall temperatures at several angular positions are
|
|
10
|
+
weighted by their radiative driving force (Stefan-Boltzmann, T^4) and fit
|
|
11
|
+
with a Gaussian. The area-preserving equivalent width sqrt(2*pi)*sigma gives
|
|
12
|
+
the effective arc.
|
|
13
|
+
* Axial -- the flame-contact height intersected with the wetted (liquid) part
|
|
14
|
+
of the wall; vapour-space wall above the liquid level is excluded because it
|
|
15
|
+
contributes negligibly to pressurization.
|
|
16
|
+
|
|
17
|
+
The effective contact area is the product of the effective arc length and the
|
|
18
|
+
effective height. All routines are data-agnostic.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import numpy as np
|
|
24
|
+
from scipy.optimize import curve_fit
|
|
25
|
+
|
|
26
|
+
KELVIN = 273.15
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _gaussian(theta, amp, sigma):
|
|
30
|
+
return amp * np.exp(-theta**2 / (2.0 * sigma**2))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def radiative_driving_force(T_wall_C, T_amb_C=35.0):
|
|
34
|
+
"""Radiative driving force (T_wall^4 - T_amb^4) with temperatures in degC.
|
|
35
|
+
|
|
36
|
+
Returns the value in K^4. This is the physically correct weight for a
|
|
37
|
+
radiation-dominated fire, rather than the plain temperature rise.
|
|
38
|
+
"""
|
|
39
|
+
return (np.asarray(T_wall_C) + KELVIN) ** 4 - (T_amb_C + KELVIN) ** 4
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def effective_arc(angles_deg, T_wall_C, T_amb_C=35.0, sigma0=40.0):
|
|
43
|
+
"""Effective circumferential arc [deg] from angular wall temperatures.
|
|
44
|
+
|
|
45
|
+
Parameters
|
|
46
|
+
----------
|
|
47
|
+
angles_deg : array-like
|
|
48
|
+
Circumferential angles of the sensors [deg], flame centre at 0.
|
|
49
|
+
Include the flame-facing point (0 deg) and off-flame points
|
|
50
|
+
(e.g. +/-90, 180). At least three distinct angles are needed.
|
|
51
|
+
T_wall_C : array-like
|
|
52
|
+
Wall temperatures at those angles [degC] (period-averaged or
|
|
53
|
+
instantaneous).
|
|
54
|
+
T_amb_C : float
|
|
55
|
+
Ambient temperature [degC].
|
|
56
|
+
sigma0 : float
|
|
57
|
+
Initial guess for the Gaussian standard deviation [deg].
|
|
58
|
+
|
|
59
|
+
Returns
|
|
60
|
+
-------
|
|
61
|
+
dict with keys:
|
|
62
|
+
``sigma`` : fitted standard deviation [deg]
|
|
63
|
+
``arc_deg`` : area-preserving equivalent width sqrt(2*pi)*sigma [deg]
|
|
64
|
+
``fwhm_deg`` : full width at half maximum 2.355*sigma [deg]
|
|
65
|
+
"""
|
|
66
|
+
ang = np.asarray(angles_deg, float)
|
|
67
|
+
drive = radiative_driving_force(T_wall_C, T_amb_C)
|
|
68
|
+
if np.max(drive) <= 0:
|
|
69
|
+
raise ValueError("Non-positive radiative driving force; check inputs.")
|
|
70
|
+
popt, _ = curve_fit(_gaussian, ang, drive,
|
|
71
|
+
p0=[float(np.max(drive)), sigma0], maxfev=10000)
|
|
72
|
+
sigma = abs(popt[1])
|
|
73
|
+
return {
|
|
74
|
+
"sigma": sigma,
|
|
75
|
+
"arc_deg": np.sqrt(2.0 * np.pi) * sigma,
|
|
76
|
+
"fwhm_deg": 2.355 * sigma,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def effective_height(flame_height_m, liquid_height_m):
|
|
81
|
+
"""Effective axial height [m] = min(flame contact, wetted liquid).
|
|
82
|
+
|
|
83
|
+
Vapour-space wall above the liquid level is excluded.
|
|
84
|
+
"""
|
|
85
|
+
return min(flame_height_m, liquid_height_m)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def effective_area(diameter_m, arc_deg, height_m):
|
|
89
|
+
"""Effective fire-contact area [m^2].
|
|
90
|
+
|
|
91
|
+
A_eff = pi * D * (arc/360) * height
|
|
92
|
+
"""
|
|
93
|
+
arc_length = np.pi * diameter_m * (arc_deg / 360.0)
|
|
94
|
+
return arc_length * height_m
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def heat_flux(Q_W, area_m2):
|
|
98
|
+
"""Heat flux [kW/m^2] from heat-input rate [W] and area [m^2]."""
|
|
99
|
+
return Q_W / area_m2 / 1000.0
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pifira.io
|
|
3
|
+
=========
|
|
4
|
+
Lightweight loaders for user-supplied CSV files.
|
|
5
|
+
|
|
6
|
+
No experimental data are bundled with pifira. These helpers parse the two
|
|
7
|
+
common file shapes produced by GL840-type dataloggers, but any pipeline that
|
|
8
|
+
yields ``(time, pressure)`` and ``(time, temperatures)`` arrays works with the
|
|
9
|
+
rest of the library. Bring your own data.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from io import StringIO
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
import pandas as pd
|
|
18
|
+
|
|
19
|
+
_ENCODINGS = ("utf-8-sig", "cp949", "euc-kr", "utf-8", "latin-1")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _read_text(path):
|
|
23
|
+
for enc in _ENCODINGS:
|
|
24
|
+
try:
|
|
25
|
+
return open(path, encoding=enc).read().replace("\r", "")
|
|
26
|
+
except (UnicodeDecodeError, UnicodeError):
|
|
27
|
+
continue
|
|
28
|
+
return open(path, encoding="utf-8", errors="ignore").read().replace("\r", "")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def load_pressure_csv(path, time_col=2, value_col=3, header_scan=True):
|
|
32
|
+
"""Load a pressure CSV -> (time_s, pressure).
|
|
33
|
+
|
|
34
|
+
By default expects columns [index, datetime, seconds, pressure]; the data
|
|
35
|
+
body is detected as the first row whose first field is numeric.
|
|
36
|
+
|
|
37
|
+
Parameters
|
|
38
|
+
----------
|
|
39
|
+
path : str
|
|
40
|
+
time_col, value_col : int
|
|
41
|
+
Column indices for time [s] and pressure.
|
|
42
|
+
header_scan : bool
|
|
43
|
+
If True, auto-detect the first numeric data row.
|
|
44
|
+
"""
|
|
45
|
+
lines = _read_text(path).split("\n")
|
|
46
|
+
if header_scan:
|
|
47
|
+
start = next(i for i, ln in enumerate(lines)
|
|
48
|
+
if ln.split(",")[0].strip().replace(".", "", 1).isdigit())
|
|
49
|
+
else:
|
|
50
|
+
start = 0
|
|
51
|
+
df = pd.read_csv(StringIO("\n".join(lines[start:])), header=None,
|
|
52
|
+
engine="python")
|
|
53
|
+
t = pd.to_numeric(df.iloc[:, time_col], errors="coerce").values
|
|
54
|
+
v = pd.to_numeric(df.iloc[:, value_col], errors="coerce").values
|
|
55
|
+
ok = ~(np.isnan(t) | np.isnan(v))
|
|
56
|
+
return t[ok], v[ok]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def load_temperature_csv(path, n_channels=10, data_start_row=2):
|
|
60
|
+
"""Load a multi-channel temperature CSV -> (time_s, DataFrame[CH1..CHn]).
|
|
61
|
+
|
|
62
|
+
Expects columns [index, datetime, seconds, CH1..CHn, ...]. Channel labels
|
|
63
|
+
in the header (e.g. ``CH3(front-centre)``) are ignored here; positions are
|
|
64
|
+
assigned by the caller.
|
|
65
|
+
"""
|
|
66
|
+
lines = _read_text(path).split("\n")
|
|
67
|
+
names = ["no", "dt", "sec"] + [f"CH{i}" for i in range(1, n_channels + 1)]
|
|
68
|
+
df = pd.read_csv(StringIO("\n".join(lines[data_start_row:])),
|
|
69
|
+
header=None, names=names, engine="python")
|
|
70
|
+
df = df[pd.to_numeric(df["no"], errors="coerce").notna()].reset_index(drop=True)
|
|
71
|
+
for ch in [f"CH{i}" for i in range(1, n_channels + 1)]:
|
|
72
|
+
df[ch] = pd.to_numeric(df[ch], errors="coerce")
|
|
73
|
+
t = pd.to_numeric(df["sec"], errors="coerce").values
|
|
74
|
+
return t, df
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pifira.pressure_rise
|
|
3
|
+
====================
|
|
4
|
+
Closed-vessel pressure-rise method for fire heat-input estimation.
|
|
5
|
+
|
|
6
|
+
The core physics: for a sealed vessel (constant mass and volume) exposed to
|
|
7
|
+
fire before any pressure-relief venting, the incident heat is converted
|
|
8
|
+
entirely into the internal-energy rise of the two-phase contents:
|
|
9
|
+
|
|
10
|
+
Q = dU/dt = d/dt ( m_L u_L + m_V u_V )
|
|
11
|
+
|
|
12
|
+
By tracking total mass and internal energy at each pressure and calling a
|
|
13
|
+
two-phase equilibrium flash, the heat-input rate Q is recovered from a
|
|
14
|
+
measured pressure history alone.
|
|
15
|
+
|
|
16
|
+
This module is data-agnostic: the caller supplies pressure-time arrays and
|
|
17
|
+
tank/fluid parameters. No experimental data are bundled.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from typing import Callable
|
|
24
|
+
|
|
25
|
+
import numpy as np
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
from CoolProp.CoolProp import PropsSI
|
|
29
|
+
except ImportError as exc: # pragma: no cover
|
|
30
|
+
raise ImportError(
|
|
31
|
+
"pifira requires CoolProp. Install with: pip install CoolProp"
|
|
32
|
+
) from exc
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
P_ATM_BAR = 1.01325
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class TankSpec:
|
|
40
|
+
"""Geometry and fluid specification for a pressurized tank.
|
|
41
|
+
|
|
42
|
+
Parameters
|
|
43
|
+
----------
|
|
44
|
+
volume : float
|
|
45
|
+
Internal volume [m^3].
|
|
46
|
+
fluid : str
|
|
47
|
+
CoolProp fluid string, e.g. ``"HEOS::Propane"`` or a mixture like
|
|
48
|
+
``"HEOS::Propane[0.95]&n-Butane[0.05]"``.
|
|
49
|
+
fill : float
|
|
50
|
+
Initial liquid fill fraction by volume (0-1).
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
volume: float
|
|
54
|
+
fluid: str = "HEOS::Propane"
|
|
55
|
+
fill: float = 0.70
|
|
56
|
+
|
|
57
|
+
def initial_mass(self, P_bar_gauge: float) -> float:
|
|
58
|
+
"""Total contents mass [kg] at a given initial gauge pressure."""
|
|
59
|
+
p = (P_bar_gauge + P_ATM_BAR) * 1e5
|
|
60
|
+
VL = self.fill * self.volume
|
|
61
|
+
VV = (1.0 - self.fill) * self.volume
|
|
62
|
+
rho_L = PropsSI("D", "P", p, "Q", 0, self.fluid)
|
|
63
|
+
rho_V = PropsSI("D", "P", p, "Q", 1, self.fluid)
|
|
64
|
+
return VL * rho_L + VV * rho_V
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def internal_energy(P_bar_gauge: float, m_tot: float, tank: TankSpec) -> float:
|
|
68
|
+
"""Two-phase internal energy U [J] under closed-vessel constraint.
|
|
69
|
+
|
|
70
|
+
The liquid/vapour split is redistributed at each pressure so that total
|
|
71
|
+
mass ``m_tot`` and total volume ``tank.volume`` are conserved.
|
|
72
|
+
"""
|
|
73
|
+
p = (P_bar_gauge + P_ATM_BAR) * 1e5
|
|
74
|
+
rho_L = PropsSI("D", "P", p, "Q", 0, tank.fluid)
|
|
75
|
+
rho_V = PropsSI("D", "P", p, "Q", 1, tank.fluid)
|
|
76
|
+
u_L = PropsSI("U", "P", p, "Q", 0, tank.fluid)
|
|
77
|
+
u_V = PropsSI("U", "P", p, "Q", 1, tank.fluid)
|
|
78
|
+
V_L = (m_tot - tank.volume * rho_V) / (rho_L - rho_V)
|
|
79
|
+
return V_L * rho_L * u_L + (tank.volume - V_L) * rho_V * u_V
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def liquid_height(P_bar_gauge: float, m_tot: float, tank: TankSpec,
|
|
83
|
+
cross_section: float, head_volume: float = 0.0) -> float:
|
|
84
|
+
"""Dynamic liquid height [m] in the cylindrical section.
|
|
85
|
+
|
|
86
|
+
Parameters
|
|
87
|
+
----------
|
|
88
|
+
cross_section : float
|
|
89
|
+
Horizontal cross-sectional area of the cylinder [m^2].
|
|
90
|
+
head_volume : float
|
|
91
|
+
Volume of the bottom head below the cylinder [m^3] (default 0).
|
|
92
|
+
"""
|
|
93
|
+
p = (P_bar_gauge + P_ATM_BAR) * 1e5
|
|
94
|
+
rho_L = PropsSI("D", "P", p, "Q", 0, tank.fluid)
|
|
95
|
+
rho_V = PropsSI("D", "P", p, "Q", 1, tank.fluid)
|
|
96
|
+
V_L = (m_tot - tank.volume * rho_V) / (rho_L - rho_V)
|
|
97
|
+
return max(0.0, (V_L - head_volume) / cross_section)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def heat_input(P1_bar: float, P2_bar: float, dt: float,
|
|
101
|
+
tank: TankSpec, m_tot: float | None = None) -> float:
|
|
102
|
+
"""Average heat-input rate Q [W] over a closed-vessel window.
|
|
103
|
+
|
|
104
|
+
Q = (U(P2) - U(P1)) / dt, with mass fixed at its initial value.
|
|
105
|
+
|
|
106
|
+
Parameters
|
|
107
|
+
----------
|
|
108
|
+
P1_bar, P2_bar : float
|
|
109
|
+
Gauge pressures at the start and end of the window [bar].
|
|
110
|
+
dt : float
|
|
111
|
+
Duration of the window [s].
|
|
112
|
+
tank : TankSpec
|
|
113
|
+
m_tot : float, optional
|
|
114
|
+
Total mass [kg]. If omitted, computed from ``P1_bar``.
|
|
115
|
+
"""
|
|
116
|
+
if m_tot is None:
|
|
117
|
+
m_tot = tank.initial_mass(P1_bar)
|
|
118
|
+
dU = internal_energy(P2_bar, m_tot, tank) - internal_energy(P1_bar, m_tot, tank)
|
|
119
|
+
return dU / dt
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def heat_input_series(t: np.ndarray, P_bar: np.ndarray, tank: TankSpec,
|
|
123
|
+
window: int = 60, m_tot: float | None = None
|
|
124
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
125
|
+
"""Instantaneous heat-input rate Q(t) [W] via a moving-window slope.
|
|
126
|
+
|
|
127
|
+
A local linear fit of pressure over +/- ``window`` samples gives dP/dt,
|
|
128
|
+
which is converted to dU/dt through the equation of state.
|
|
129
|
+
|
|
130
|
+
Returns
|
|
131
|
+
-------
|
|
132
|
+
t_out, Q_out : np.ndarray
|
|
133
|
+
Times and instantaneous heat-input rates (NaN where the window
|
|
134
|
+
does not fit).
|
|
135
|
+
"""
|
|
136
|
+
t = np.asarray(t, float)
|
|
137
|
+
P_bar = np.asarray(P_bar, float)
|
|
138
|
+
if m_tot is None:
|
|
139
|
+
m_tot = tank.initial_mass(P_bar[0])
|
|
140
|
+
|
|
141
|
+
Q = np.full(len(P_bar), np.nan)
|
|
142
|
+
for i in range(len(P_bar)):
|
|
143
|
+
if i < window or i + window >= len(P_bar):
|
|
144
|
+
continue
|
|
145
|
+
a, b = np.polyfit(t[i - window:i + window], P_bar[i - window:i + window], 1)
|
|
146
|
+
Pc = a * t[i] + b
|
|
147
|
+
dUdP = (internal_energy(Pc + 0.05, m_tot, tank)
|
|
148
|
+
- internal_energy(Pc - 0.05, m_tot, tank)) / 0.1
|
|
149
|
+
Q[i] = dUdP * a
|
|
150
|
+
return t, Q
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pifira.representative
|
|
3
|
+
=====================
|
|
4
|
+
Representative heat-flux estimators and the dimensionless framework.
|
|
5
|
+
|
|
6
|
+
For a time-varying heat flux q(t) driven by a time-varying heat input Q(t),
|
|
7
|
+
the physically meaningful design value weights instants by how much heat
|
|
8
|
+
actually entered the vessel (a heat-input-weighted mean), because fire safety
|
|
9
|
+
is governed by the high-heat-input instants.
|
|
10
|
+
|
|
11
|
+
The dimensionless framework expresses localized-exposure heat flux in a form
|
|
12
|
+
independent of tank size, fill level and orientation.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
|
|
19
|
+
Q_ENGULF_DEFAULT = 100.0 # reference full-engulfment heat flux [kW/m^2]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def heat_input_weighted_mean(q, Q):
|
|
23
|
+
"""Heat-input-weighted mean flux: sum(q*Q) / sum(Q).
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
q : array-like
|
|
28
|
+
Instantaneous heat flux [kW/m^2].
|
|
29
|
+
Q : array-like
|
|
30
|
+
Instantaneous heat-input rate [W] (weights).
|
|
31
|
+
"""
|
|
32
|
+
q = np.asarray(q, float)
|
|
33
|
+
Q = np.asarray(Q, float)
|
|
34
|
+
mask = np.isfinite(q) & np.isfinite(Q)
|
|
35
|
+
return float(np.sum(q[mask] * Q[mask]) / np.sum(Q[mask]))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def simple_mean(q):
|
|
39
|
+
"""Plain time-average of instantaneous flux [kW/m^2]."""
|
|
40
|
+
q = np.asarray(q, float)
|
|
41
|
+
return float(np.nanmean(q))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# --------------------------------------------------------------------------
|
|
45
|
+
# Dimensionless framework
|
|
46
|
+
# --------------------------------------------------------------------------
|
|
47
|
+
def coverage_fraction(A_eff, A_wetted):
|
|
48
|
+
"""phi = A_eff / A_wetted (fire coverage fraction; 1 = full engulfment)."""
|
|
49
|
+
return A_eff / A_wetted
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def normalized_flux(q_local, q_engulf=Q_ENGULF_DEFAULT):
|
|
53
|
+
"""q* = q_local / q_engulf (dimensionless heat flux)."""
|
|
54
|
+
return q_local / q_engulf
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def pressurization_number(dPdt, volume, Q_engulf_W):
|
|
58
|
+
"""Pi = (dP/dt * V) / Q_engulf (dimensionless pressurization rate)."""
|
|
59
|
+
return (dPdt * volume) / Q_engulf_W
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def orientation_factor(liquid_height_m, diameter_m):
|
|
63
|
+
"""Omega = h_liq / D (orientation factor; vertical > 1, horizontal < 1)."""
|
|
64
|
+
return liquid_height_m / diameter_m
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pifira
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pressure-based inverse estimation of fire heat flux and effective flame-contact area for pressurized LPG tanks under localized flame exposure
|
|
5
|
+
Author: Woo-gui-yeon Lee
|
|
6
|
+
Maintainer: Woo-gui-yeon Lee
|
|
7
|
+
License: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/lyullee/pifira
|
|
9
|
+
Project-URL: Repository, https://github.com/lyullee/pifira
|
|
10
|
+
Project-URL: Issues, https://github.com/lyullee/pifira/issues
|
|
11
|
+
Keywords: LPG,fire safety,heat flux,pressure relief valve,closed-vessel,pressure-rise method,BLEVE,process safety,thermal engineering,inverse method
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: numpy>=1.20
|
|
27
|
+
Requires-Dist: scipy>=1.7
|
|
28
|
+
Requires-Dist: pandas>=1.3
|
|
29
|
+
Requires-Dist: CoolProp>=6.4
|
|
30
|
+
Provides-Extra: plot
|
|
31
|
+
Requires-Dist: matplotlib>=3.4; extra == "plot"
|
|
32
|
+
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
34
|
+
Requires-Dist: matplotlib>=3.4; extra == "dev"
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
# pifira
|
|
38
|
+
|
|
39
|
+
**P**ressure-based **I**nverse **F**ire-heat-flux and **R**adiative-**A**rea analysis
|
|
40
|
+
|
|
41
|
+
A data-agnostic Python toolkit for estimating **fire heat input** and
|
|
42
|
+
**effective flame-contact area** for pressurized LPG tanks under **localized
|
|
43
|
+
(non-engulfing) flame exposure**.
|
|
44
|
+
|
|
45
|
+
pifira combines the **closed-vessel pressure-rise method** (heat input from a
|
|
46
|
+
measured pressure history) with a **radiative (T?? weighted angular-temperature
|
|
47
|
+
inversion** (effective fire-contact area from surface temperatures), and
|
|
48
|
+
provides a **dimensionless framework** that unifies localized and
|
|
49
|
+
full-engulfment exposure across tank sizes, fill levels and orientations.
|
|
50
|
+
|
|
51
|
+
> **No experimental data are bundled with this package.** All routines operate
|
|
52
|
+
> on user-supplied arrays. See [Data sources](#data-sources) for how to obtain
|
|
53
|
+
> the public validation datasets.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Why
|
|
58
|
+
|
|
59
|
+
Most published fire-engulfment work assumes *uniform, full engulfment* of
|
|
60
|
+
*horizontal* tanks. Small **vertical** LPG tanks under *localized* flame
|
|
61
|
+
exposure are under-studied, and standards (API 521, KGS) do not cover heat-input
|
|
62
|
+
estimation for this case. pifira addresses this by inverting a measured
|
|
63
|
+
pressure rise for heat input and a measured angular temperature distribution
|
|
64
|
+
for the effective contact area ??without assuming full engulfment.
|
|
65
|
+
|
|
66
|
+
## Installation
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pip install pifira
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Dependencies: `numpy`, `scipy`, `pandas`, `CoolProp`. Optional plotting extras:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
pip install "pifira[plot]"
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Quick start
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
import numpy as np
|
|
82
|
+
import pifira
|
|
83
|
+
|
|
84
|
+
# 1. Define the tank (volume, fluid, fill fraction)
|
|
85
|
+
tank = pifira.TankSpec(
|
|
86
|
+
volume=0.619,
|
|
87
|
+
fluid="HEOS::Propane[0.95]&n-Butane[0.05]",
|
|
88
|
+
fill=0.70,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# 2. Heat input from a pressure history (bring your own arrays)
|
|
92
|
+
# t [s], P [bar gauge], closed-vessel window before PRV opens
|
|
93
|
+
Q_W = pifira.heat_input(P1_bar=10.83, P2_bar=16.30, dt=544.5, tank=tank)
|
|
94
|
+
print(Q_W / 1000, "kW")
|
|
95
|
+
|
|
96
|
+
# 3. Effective contact area from angular wall temperatures
|
|
97
|
+
# flame centre at 0 deg; off-flame points at +/-90, 180
|
|
98
|
+
res = pifira.effective_arc(
|
|
99
|
+
angles_deg=[0, 90, 180, -90],
|
|
100
|
+
T_wall_C=[271, 49, 39, 46], # period-averaged wall temps
|
|
101
|
+
T_amb_C=35.0,
|
|
102
|
+
)
|
|
103
|
+
arc_deg = res["arc_deg"] # area-preserving equivalent width
|
|
104
|
+
|
|
105
|
+
h_eff = pifira.effective_height(flame_height_m=0.767, liquid_height_m=0.885)
|
|
106
|
+
A_eff = pifira.effective_area(diameter_m=0.75, arc_deg=arc_deg, height_m=h_eff)
|
|
107
|
+
|
|
108
|
+
# 4. Heat flux
|
|
109
|
+
print(pifira.heat_flux(Q_W, A_eff), "kW/m^2")
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
A full time-varying workflow (moving-window heat input, per-instant effective
|
|
113
|
+
arc, heat-input-weighted representative value) is in
|
|
114
|
+
[`examples/analyze_localized.py`](examples/analyze_localized.py).
|
|
115
|
+
|
|
116
|
+
## Public API
|
|
117
|
+
|
|
118
|
+
| Area | Functions |
|
|
119
|
+
|---|---|
|
|
120
|
+
| Heat input (pressure-rise) | `TankSpec`, `internal_energy`, `liquid_height`, `heat_input`, `heat_input_series` |
|
|
121
|
+
| Effective area (inversion) | `radiative_driving_force`, `effective_arc`, `effective_height`, `effective_area`, `heat_flux` |
|
|
122
|
+
| Representative value | `heat_input_weighted_mean`, `simple_mean` |
|
|
123
|
+
| Dimensionless framework | `coverage_fraction` (?), `normalized_flux` (q\*), `pressurization_number` (?), `orientation_factor` (Ω) |
|
|
124
|
+
| I/O | `load_pressure_csv`, `load_temperature_csv` |
|
|
125
|
+
|
|
126
|
+
## Method summary
|
|
127
|
+
|
|
128
|
+
1. **Closed-vessel pressure-rise.** Before pressure-relief venting, incident
|
|
129
|
+
heat converts entirely into the contents' internal-energy rise:
|
|
130
|
+
`Q = dU/dt = d/dt(m_L u_L + m_V u_V)`. Mass and volume are fixed; the
|
|
131
|
+
liquid/vapour split is re-flashed at each pressure (CoolProp).
|
|
132
|
+
|
|
133
|
+
2. **Radiative (T?? angular inversion.** Radiative flux scales with
|
|
134
|
+
`T_wall????T_amb?? (Stefan-Boltzmann). Angular wall temperatures are fit
|
|
135
|
+
with a Gaussian; the area-preserving equivalent width `??2?)·?` gives the
|
|
136
|
+
effective arc.
|
|
137
|
+
|
|
138
|
+
3. **Axial intersection.** Effective height = min(flame contact height, wetted
|
|
139
|
+
liquid height). Vapour-space wall is excluded.
|
|
140
|
+
|
|
141
|
+
4. **Representative value.** Heat-input-weighted mean `Σ(q·Q)/ΣQ`, since fire
|
|
142
|
+
safety is governed by high-heat-input instants.
|
|
143
|
+
|
|
144
|
+
5. **Dimensionless framework.** ? (coverage), q\* (normalized flux), ?
|
|
145
|
+
(pressurization), Ω (orientation) unify localized/full and vertical/horizontal.
|
|
146
|
+
|
|
147
|
+
## Data sources
|
|
148
|
+
|
|
149
|
+
pifira ships **no data**. The public datasets used to validate the method in
|
|
150
|
+
the associated study are available from their original publications:
|
|
151
|
+
|
|
152
|
+
- **Moodie, K., Billinge, K., Cutler, D.P. (1988).** *Fire Engulfment of LPG
|
|
153
|
+
Storage Tanks.* IChemE Symposium Series No. 93, pp. 87??06. Institution of
|
|
154
|
+
Chemical Engineers. ??pressure history (Fig. 5) and initial conditions /
|
|
155
|
+
heat fluxes (Tables 1??). Obtain from IChemE or a library.
|
|
156
|
+
|
|
157
|
+
- **Birk, A.M., Poirier, D., Davison, C. (2006).** *On the thermal rupture of
|
|
158
|
+
1.9 m³ propane pressure vessels with defects in their thermal protection
|
|
159
|
+
system.* Journal of Loss Prevention in the Process Industries **19**(6),
|
|
160
|
+
582??97. https://doi.org/10.1016/j.jlp.2006.02.002 ??pressure (Fig. 8) and
|
|
161
|
+
lading-temperature stratification (Fig. 10) for the 25% partial-engulfment
|
|
162
|
+
test. Obtain from the publisher.
|
|
163
|
+
|
|
164
|
+
To reproduce the validation, digitize the relevant figures from the original
|
|
165
|
+
papers and pass the values to the pifira API. Digitized values are **not**
|
|
166
|
+
redistributed here.
|
|
167
|
+
|
|
168
|
+
Experimental data from the authors' own fire tests are **not** included and are
|
|
169
|
+
reported separately.
|
|
170
|
+
|
|
171
|
+
## Citing
|
|
172
|
+
|
|
173
|
+
If you use pifira, please cite it via [`CITATION.cff`](CITATION.cff) and the
|
|
174
|
+
archived release DOI (Zenodo). See the repository release page for the DOI.
|
|
175
|
+
|
|
176
|
+
## License
|
|
177
|
+
|
|
178
|
+
MIT ??see [LICENSE](LICENSE).
|
|
179
|
+
|
|
180
|
+
## Author
|
|
181
|
+
|
|
182
|
+
Woo-gui-yeon Lee, Korea Gas Safety Corporation (KGS), AI Safety Research Team
|
|
183
|
+
([ORCID 0009-0008-8976-5363](https://orcid.org/0009-0008-8976-5363)).
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
CITATION.cff
|
|
2
|
+
LICENSE
|
|
3
|
+
MANIFEST.in
|
|
4
|
+
README.md
|
|
5
|
+
pyproject.toml
|
|
6
|
+
examples/analyze_localized.py
|
|
7
|
+
src/pifira/__init__.py
|
|
8
|
+
src/pifira/effective_area.py
|
|
9
|
+
src/pifira/io.py
|
|
10
|
+
src/pifira/pressure_rise.py
|
|
11
|
+
src/pifira/representative.py
|
|
12
|
+
src/pifira.egg-info/PKG-INFO
|
|
13
|
+
src/pifira.egg-info/SOURCES.txt
|
|
14
|
+
src/pifira.egg-info/dependency_links.txt
|
|
15
|
+
src/pifira.egg-info/requires.txt
|
|
16
|
+
src/pifira.egg-info/top_level.txt
|
|
17
|
+
tests/test_pifira.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pifira
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for pifira. These are data-free: they check physical consistency and
|
|
3
|
+
known analytic properties, not any bundled experimental dataset.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
import pifira
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_tankspec_mass_positive():
|
|
13
|
+
tank = pifira.TankSpec(volume=0.619, fluid="HEOS::Propane", fill=0.70)
|
|
14
|
+
m = tank.initial_mass(10.0)
|
|
15
|
+
assert m > 0
|
|
16
|
+
# 70% of 0.619 m^3 of liquid propane is order 200-300 kg
|
|
17
|
+
assert 150 < m < 350
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_heat_input_positive_for_rising_pressure():
|
|
21
|
+
tank = pifira.TankSpec(volume=0.619, fluid="HEOS::Propane", fill=0.70)
|
|
22
|
+
Q = pifira.heat_input(P1_bar=10.0, P2_bar=15.0, dt=500.0, tank=tank)
|
|
23
|
+
assert Q > 0 # heating raises pressure -> positive heat input
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_internal_energy_monotonic_in_pressure():
|
|
27
|
+
tank = pifira.TankSpec(volume=0.619, fluid="HEOS::Propane", fill=0.70)
|
|
28
|
+
m = tank.initial_mass(10.0)
|
|
29
|
+
U1 = pifira.internal_energy(10.0, m, tank)
|
|
30
|
+
U2 = pifira.internal_energy(15.0, m, tank)
|
|
31
|
+
assert U2 > U1
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_radiative_driving_force_t4():
|
|
35
|
+
# doubling absolute temperature raises driving force ~16x (minus ambient)
|
|
36
|
+
d1 = pifira.radiative_driving_force(0.0, T_amb_C=-273.15) # 273.15 K
|
|
37
|
+
d2 = pifira.radiative_driving_force(273.15, T_amb_C=-273.15) # 546.3 K
|
|
38
|
+
assert d2 / d1 == pytest.approx(16.0, rel=0.05)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_effective_arc_symmetric_gaussian():
|
|
42
|
+
# a symmetric bell centred at 0 should recover a finite equivalent width
|
|
43
|
+
angles = [0, 90, 180, -90]
|
|
44
|
+
temps = [271, 49, 39, 46]
|
|
45
|
+
res = pifira.effective_arc(angles, temps, T_amb_C=35.0)
|
|
46
|
+
assert res["sigma"] > 0
|
|
47
|
+
assert res["arc_deg"] == pytest.approx(np.sqrt(2 * np.pi) * res["sigma"])
|
|
48
|
+
assert res["fwhm_deg"] == pytest.approx(2.355 * res["sigma"])
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_effective_height_takes_minimum():
|
|
52
|
+
assert pifira.effective_height(0.767, 0.885) == 0.767
|
|
53
|
+
assert pifira.effective_height(1.2, 0.885) == 0.885
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_effective_area_and_flux():
|
|
57
|
+
A = pifira.effective_area(diameter_m=0.75, arc_deg=90.0, height_m=0.767)
|
|
58
|
+
assert A > 0
|
|
59
|
+
q = pifira.heat_flux(16800.0, A) # 16.8 kW over A
|
|
60
|
+
assert q > 0
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_weighted_mean_biases_toward_high_Q():
|
|
64
|
+
q = [30.0, 70.0]
|
|
65
|
+
Q_equal = [1.0, 1.0]
|
|
66
|
+
Q_high = [1.0, 9.0]
|
|
67
|
+
m_eq = pifira.heat_input_weighted_mean(q, Q_equal)
|
|
68
|
+
m_hi = pifira.heat_input_weighted_mean(q, Q_high)
|
|
69
|
+
assert m_eq == pytest.approx(50.0)
|
|
70
|
+
assert m_hi > m_eq # weighting toward the high-flux instant
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_dimensionless_definitions():
|
|
74
|
+
assert pifira.coverage_fraction(0.5, 2.0) == 0.25
|
|
75
|
+
assert pifira.normalized_flux(50.0, q_engulf=100.0) == 0.5
|
|
76
|
+
assert pifira.orientation_factor(0.89, 0.75) == pytest.approx(1.187, rel=1e-3)
|