Skip to content

Commit 71585a5

Browse files
Merge pull request #25 from TristanHehnen/main
Add instrument readers and Friedman method
2 parents 90aa1c2 + f0fc95f commit 71585a5

21 files changed

Lines changed: 2001 additions & 37 deletions

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,15 @@
22

33
Fundamental algorithms from the field of fire science and fire safety engineering, for computations with Python.
44

5-
Documentation is available here: [![FireSciPy Documentation](https://img.shields.io/badge/docs-online-brightgreen)](https://FireDynamics.github.io/FireSciPy/)
5+
[![FireSciPy Documentation](https://img.shields.io/badge/docs-online-brightgreen)](https://FireDynamics.github.io/FireSciPy/)
6+
[![PyPI version](https://badge.fury.io/py/firescipy.svg)](https://pypi.org/project/firescipy/)
7+
8+
9+
## Installation
10+
11+
```
12+
pip install firescipy
13+
```
614

715
Examples are available in Jupyter notebooks in the [FireSciPy repo on GitHub](https://github.com/FireDynamics/FireSciPy).
816

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,4 @@ functions, and utility modules used in FireSciPy.
3131
reference/pyrolysis
3232
reference/handcalculation
3333
reference/utils
34+
reference/instruments

docs/reference/instruments.rst

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
Instruments Module
2+
==================
3+
4+
This subpackage provides parsers for reading experimental instrument export
5+
files commonly used in fire science and fire safety engineering research.
6+
Supported formats include thermal analysis (STA, DSC, TGA) and calorimetry
7+
(MCC, Cone Calorimeter) instruments.
8+
9+
A generic reader with automatic file type detection is available via
10+
:func:`firescipy.instruments.read_instrument_file`.
11+
12+
13+
Reader
14+
------
15+
16+
.. automodule:: firescipy.instruments.reader
17+
:members:
18+
:undoc-members:
19+
:show-inheritance:
20+
21+
22+
Netzsch STA / DSC / TGA
23+
------------------------
24+
25+
.. automodule:: firescipy.instruments.netzsch_sta
26+
:members:
27+
:undoc-members:
28+
:show-inheritance:
29+
30+
31+
DEATAK MCC
32+
----------
33+
34+
.. automodule:: firescipy.instruments.deatak_mcc
35+
:members:
36+
:undoc-members:
37+
:show-inheritance:
38+
39+
40+
Netzsch Cone Calorimeter
41+
------------------------
42+
43+
.. automodule:: firescipy.instruments.netzsch_cone
44+
:members:
45+
:undoc-members:
46+
:show-inheritance:

docs/tutorials/pyrolysis/mock_kinetics_computation.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -183,12 +183,12 @@ Looping over the above dictionary, a temperature program is created for each nom
183183
t_array = fsp.utils.series_to_numpy(hr_model["Time"])
184184
T_array = fsp.utils.series_to_numpy(hr_model["Temperature"])
185185
# Get overview over temperature resolution
186-
ΔT = temp_model[1] - temp_model[0]
186+
ΔT = T_array[1] - T_array[0]
187187
print(f"Temperature resolution ({hr_label}): ΔT = {ΔT} K")
188188
# Compute conversion for decelerating reaction (n-th order)
189189
t_sol, alpha_sol = fsp.pyrolysis.modeling.solve_kinetics(
190-
t_array=time_model,
191-
T_array=temp_model,
190+
t_array=t_array,
191+
T_array=T_array,
192192
A=A,
193193
E=E,
194194
alpha0=alpha0,

pyproject.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "firescipy"
7-
version = "0.0.6"
7+
version = "0.0.9"
88
description = "FireSciPy: Fundamental algorithms from the field of fire science, for computations with Python."
99
readme = "README.md"
1010
keywords = ["Fire Safety Engineering", "fire", "pyrolysis", "kinetics", "FDS"]
@@ -32,6 +32,12 @@ dependencies = [
3232
[tool.hatch.build.targets.wheel]
3333
packages = ["src/firescipy"]
3434

35+
[project.optional-dependencies]
36+
test = ["pytest>=7.0"]
37+
38+
[tool.pytest.ini_options]
39+
testpaths = ["tests"]
40+
3541
[project.urls]
3642
Repository = "https://github.com/FireDynamics/FireSciPy"
3743
Documentation = "https://firedynamics.github.io/FireSciPy/"

src/firescipy/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from . import pyrolysis
88
from . import constants
99
from . import handcalculation
10+
from . import instruments
1011
from importlib.metadata import PackageNotFoundError, version
1112

1213

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# This Source Code Form is subject to the terms of the Mozilla Public
2+
# License, v. 2.0. If a copy of the MPL was not distributed with this
3+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
5+
from .reader import read_instrument_file, detect_file_type, SUPPORTED_TYPES
6+
from .netzsch_sta import read_netzsch_sta_file
7+
from .deatak_mcc import read_deatak_mcc_file
8+
from .netzsch_cone import read_netzsch_cone_file

src/firescipy/instruments/base.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# This Source Code Form is subject to the terms of the Mozilla Public
2+
# License, v. 2.0. If a copy of the MPL was not distributed with this
3+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
5+
from pathlib import Path
6+
7+
8+
class InstrumentFile:
9+
"""
10+
Generic file loader for laboratory text exports.
11+
12+
Responsibilities
13+
----------------
14+
- read raw bytes
15+
- decode using fallback encodings
16+
- repair known character issues
17+
- expose text and lines
18+
- provide small helpers for line-based inspection
19+
"""
20+
21+
def __init__(
22+
self,
23+
file_path,
24+
encodings=None,
25+
replacements=None,
26+
strip_bom=True,
27+
):
28+
# Store the file path as a Path object for reliable cross-platform handling.
29+
self.file_path = Path(file_path)
30+
31+
# List of encodings to try in order. Many lab instruments export files
32+
# with Windows-specific encodings (cp1252, latin1) rather than UTF-8.
33+
# The first encoding that decodes the file without errors is used.
34+
self.encodings = encodings or [
35+
"utf-8",
36+
"cp1252",
37+
"latin1",
38+
"utf-16",
39+
"utf-16-le",
40+
"utf-16-be",
41+
]
42+
43+
# Character repairs applied after decoding. Some instruments export
44+
# special characters (e.g. degree sign °) using byte sequences that
45+
# do not survive encoding conversion cleanly. These replacements fix
46+
# the most common cases. The order matters: more specific patterns
47+
# (e.g. "°C") must come before broader ones (e.g. "°") to avoid
48+
# partial replacements.
49+
self.replacements = replacements or {
50+
"›": "°",
51+
"›C": "°C", # cp1252: byte › decodes to › before C → °C
52+
"›": "°", # cp1252: byte › decodes to › standalone → °
53+
"�C": "°C", # UTF-8 mojibake for degree-Celsius
54+
"�": "°", # Unicode replacement character U+FFFD → degree sign
55+
}
56+
57+
# If True, strip the Byte Order Mark (BOM) that some editors and
58+
# instruments prepend to UTF-8 or UTF-16 files.
59+
self.strip_bom = strip_bom
60+
61+
# These attributes are populated by read().
62+
self.raw_bytes = None # original file content as bytes
63+
self.text = None # decoded and repaired full text
64+
self.lines = None # text split into individual lines
65+
self.used_encoding = None # whichever encoding succeeded
66+
67+
def read(self):
68+
# Read the entire file as raw bytes first, before any decoding.
69+
self.raw_bytes = self.file_path.read_bytes()
70+
71+
# Try each encoding in order until one succeeds.
72+
self.text, self.used_encoding = self._decode_bytes(self.raw_bytes)
73+
74+
# Fix known character encoding artefacts in the decoded text.
75+
self.text = self._repair_text(self.text)
76+
77+
# Remove a leading BOM character if present (common in UTF-8/16 files).
78+
if self.strip_bom:
79+
self.text = self.text.lstrip("")
80+
81+
# Split into lines for line-by-line processing by the parsers.
82+
self.lines = self.text.splitlines()
83+
return self
84+
85+
def _decode_bytes(self, raw_bytes):
86+
last_error = None
87+
88+
# Try each candidate encoding and return on the first success.
89+
for enc in self.encodings:
90+
try:
91+
return raw_bytes.decode(enc), enc
92+
except UnicodeDecodeError as exc:
93+
last_error = exc
94+
95+
# None of the encodings worked — raise a descriptive error.
96+
raise ValueError(
97+
f"Could not decode file '{self.file_path}' with tried encodings: {self.encodings}"
98+
) from last_error
99+
100+
def _repair_text(self, text):
101+
# Apply each search-and-replace pair from the replacements dict.
102+
for old, new in self.replacements.items():
103+
text = text.replace(old, new)
104+
return text
105+
106+
def preview(self, start=0, stop=10):
107+
# Quick inspection helper: return a slice of lines without printing all.
108+
if self.lines is None:
109+
raise RuntimeError("Call read() first.")
110+
return self.lines[start:stop]
111+
112+
def find_first_line(self, startswith=None, contains=None):
113+
# Search for the first line that matches a prefix or a substring.
114+
# Returns the line index, or None if no match is found.
115+
if self.lines is None:
116+
raise RuntimeError("Call read() first.")
117+
118+
for idx, line in enumerate(self.lines):
119+
if startswith is not None and line.startswith(startswith):
120+
return idx
121+
if contains is not None and contains in line:
122+
return idx
123+
return None

0 commit comments

Comments
 (0)