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.
@@ -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.
@@ -0,0 +1,4 @@
1
+ include LICENSE
2
+ include README.md
3
+ include CITATION.cff
4
+ recursive-include examples *.py
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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,11 @@
1
+ numpy>=1.20
2
+ scipy>=1.7
3
+ pandas>=1.3
4
+ CoolProp>=6.4
5
+
6
+ [dev]
7
+ pytest>=7.0
8
+ matplotlib>=3.4
9
+
10
+ [plot]
11
+ matplotlib>=3.4
@@ -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)