pyglenn 0.1.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyglenn/__init__.py +30 -0
- pyglenn/builder.py +488 -0
- pyglenn/calculator.py +284 -0
- pyglenn/cli.py +182 -0
- pyglenn/data/__init__.py +1 -0
- pyglenn/data/thermo.db +0 -0
- pyglenn/data/thermo.inp +15638 -0
- pyglenn/database.py +322 -0
- pyglenn-0.1.2.dist-info/METADATA +152 -0
- pyglenn-0.1.2.dist-info/RECORD +14 -0
- pyglenn-0.1.2.dist-info/WHEEL +5 -0
- pyglenn-0.1.2.dist-info/entry_points.txt +2 -0
- pyglenn-0.1.2.dist-info/licenses/LICENSE +21 -0
- pyglenn-0.1.2.dist-info/top_level.txt +1 -0
pyglenn/__init__.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pyglenn — Thermochemical properties calculator.
|
|
3
|
+
|
|
4
|
+
Computes Cp(T), H°(T), S°(T) from NASA polynomial coefficients
|
|
5
|
+
stored in a SQLite database, converted from FORTRAN thermo.inp files.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__version__ = '0.1.2'
|
|
9
|
+
__author__ = 'Dr. Reginaldo G. Leão Jr.'
|
|
10
|
+
|
|
11
|
+
from .builder import ThermoDBBuilder
|
|
12
|
+
from .calculator import (
|
|
13
|
+
DatabaseNotConnectedError,
|
|
14
|
+
SpeciesNotFoundError,
|
|
15
|
+
TemperatureOutOfRangeError,
|
|
16
|
+
ThermoCalcError,
|
|
17
|
+
ThermochemicalCalculator,
|
|
18
|
+
)
|
|
19
|
+
from .database import R, ThermoDBQuery
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
'ThermochemicalCalculator',
|
|
23
|
+
'ThermoDBQuery',
|
|
24
|
+
'ThermoDBBuilder',
|
|
25
|
+
'R',
|
|
26
|
+
'ThermoCalcError',
|
|
27
|
+
'DatabaseNotConnectedError',
|
|
28
|
+
'SpeciesNotFoundError',
|
|
29
|
+
'TemperatureOutOfRangeError',
|
|
30
|
+
]
|
pyglenn/builder.py
ADDED
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Database builder: converts thermo.inp (NASA FORTRAN format) → SQLite3.
|
|
4
|
+
|
|
5
|
+
FORTRAN Record Structure (Appendix C):
|
|
6
|
+
RECORD 1 – Species identification
|
|
7
|
+
RECORD 2 – General information
|
|
8
|
+
RECORD 3 – Temperature interval definition
|
|
9
|
+
RECORD 4 – First 5 polynomial coefficients
|
|
10
|
+
RECORD 5 – Last 2 coefficients + integration constants
|
|
11
|
+
|
|
12
|
+
Records 3–5 repeat for each temperature interval.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
import re
|
|
19
|
+
import sqlite3
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
# Regex: match FORTRAN double-precision scientific notation (e.g. 1.234567890D+05)
|
|
26
|
+
_FORTRAN_D_RE = re.compile(r'\d\.\d{0,9}D[+\-]\d{1,2}', re.IGNORECASE)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ThermoDBBuilder:
|
|
30
|
+
"""Build a SQLite database from a thermo.inp file."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, inp_file: str, db_file: str) -> None:
|
|
33
|
+
self.inp_file: Path = Path(inp_file)
|
|
34
|
+
self.db_file: Path = Path(db_file)
|
|
35
|
+
self.conn: sqlite3.Connection | None = None
|
|
36
|
+
self.cursor: sqlite3.Cursor | None = None
|
|
37
|
+
|
|
38
|
+
# ------------------------------------------------------------------
|
|
39
|
+
# Database lifecycle
|
|
40
|
+
# ------------------------------------------------------------------
|
|
41
|
+
def connect(self) -> None:
|
|
42
|
+
"""Connect to (or create) the SQLite database."""
|
|
43
|
+
self.conn = sqlite3.connect(str(self.db_file))
|
|
44
|
+
self.cursor = self.conn.cursor()
|
|
45
|
+
self.cursor.execute('PRAGMA foreign_keys = ON')
|
|
46
|
+
self.cursor.execute('PRAGMA journal_mode = WAL')
|
|
47
|
+
|
|
48
|
+
def close(self) -> None:
|
|
49
|
+
"""Close the database connection."""
|
|
50
|
+
if self.conn:
|
|
51
|
+
self.conn.commit()
|
|
52
|
+
self.conn.close()
|
|
53
|
+
self.conn = None
|
|
54
|
+
self.cursor = None
|
|
55
|
+
|
|
56
|
+
# ------------------------------------------------------------------
|
|
57
|
+
# Schema
|
|
58
|
+
# ------------------------------------------------------------------
|
|
59
|
+
def create_tables(self) -> None:
|
|
60
|
+
"""Create the normalised table structure."""
|
|
61
|
+
assert self.cursor is not None, 'Database not connected'
|
|
62
|
+
self.cursor.execute("""
|
|
63
|
+
CREATE TABLE IF NOT EXISTS species (
|
|
64
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
65
|
+
name TEXT NOT NULL UNIQUE,
|
|
66
|
+
formula TEXT,
|
|
67
|
+
comments TEXT,
|
|
68
|
+
reference_code TEXT,
|
|
69
|
+
phase TEXT CHECK(phase IN ('gas', 'condensed')),
|
|
70
|
+
molecular_weight REAL,
|
|
71
|
+
heat_of_formation_298K REAL,
|
|
72
|
+
num_intervals INTEGER,
|
|
73
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
74
|
+
)
|
|
75
|
+
""")
|
|
76
|
+
|
|
77
|
+
self.cursor.execute("""
|
|
78
|
+
CREATE TABLE IF NOT EXISTS temperature_intervals (
|
|
79
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
80
|
+
species_id INTEGER NOT NULL,
|
|
81
|
+
interval_number INTEGER NOT NULL,
|
|
82
|
+
temp_min REAL NOT NULL,
|
|
83
|
+
temp_max REAL NOT NULL,
|
|
84
|
+
h_298_to_0 REAL,
|
|
85
|
+
FOREIGN KEY (species_id)
|
|
86
|
+
REFERENCES species(id) ON DELETE CASCADE,
|
|
87
|
+
UNIQUE(species_id, interval_number)
|
|
88
|
+
)
|
|
89
|
+
""")
|
|
90
|
+
|
|
91
|
+
self.cursor.execute("""
|
|
92
|
+
CREATE TABLE IF NOT EXISTS coefficients (
|
|
93
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
94
|
+
interval_id INTEGER NOT NULL UNIQUE,
|
|
95
|
+
a1 REAL, a2 REAL, a3 REAL, a4 REAL, a5 REAL,
|
|
96
|
+
a6 REAL, a7 REAL,
|
|
97
|
+
b1 REAL, b2 REAL,
|
|
98
|
+
FOREIGN KEY (interval_id)
|
|
99
|
+
REFERENCES temperature_intervals(id) ON DELETE CASCADE
|
|
100
|
+
)
|
|
101
|
+
""")
|
|
102
|
+
|
|
103
|
+
assert self.cursor is not None
|
|
104
|
+
self.cursor.execute("""
|
|
105
|
+
CREATE TABLE IF NOT EXISTS file_metadata (
|
|
106
|
+
id INTEGER PRIMARY KEY,
|
|
107
|
+
temp_min_global REAL,
|
|
108
|
+
temp_500_K REAL,
|
|
109
|
+
temp_1500_K REAL,
|
|
110
|
+
temp_max_global REAL,
|
|
111
|
+
reference_date TEXT,
|
|
112
|
+
total_species INTEGER
|
|
113
|
+
)
|
|
114
|
+
""")
|
|
115
|
+
|
|
116
|
+
assert self.conn is not None
|
|
117
|
+
self.conn.commit()
|
|
118
|
+
|
|
119
|
+
# ------------------------------------------------------------------
|
|
120
|
+
# Low-level parsers
|
|
121
|
+
# ------------------------------------------------------------------
|
|
122
|
+
@staticmethod
|
|
123
|
+
def parse_float(value: str) -> float | None:
|
|
124
|
+
"""Parse a FORTRAN-style float ('D' → 'E').
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
value: String possibly in FORTRAN D notation.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Float value or None if parsing fails.
|
|
131
|
+
"""
|
|
132
|
+
if not value or not value.strip():
|
|
133
|
+
return None
|
|
134
|
+
try:
|
|
135
|
+
return float(value.strip().replace('D', 'E').replace('d', 'e'))
|
|
136
|
+
except ValueError:
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
@staticmethod
|
|
140
|
+
def parse_species_record(line: str) -> tuple[str, str]:
|
|
141
|
+
"""Extract species name (cols 1-16) and comments (cols 19-80).
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
line: RECORD 1 line from thermo.inp.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
Tuple of (species_name, comments).
|
|
148
|
+
"""
|
|
149
|
+
name = line[0:16].strip() if len(line) > 16 else line.strip()
|
|
150
|
+
comments = line[18:80].strip() if len(line) > 18 else ''
|
|
151
|
+
return name, comments
|
|
152
|
+
|
|
153
|
+
def parse_general_info_record(self, line: str) -> dict[str, Any]:
|
|
154
|
+
"""Parse RECORD 2 – general information.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
line: RECORD 2 line from thermo.inp.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
Dict with num_intervals, ref_code, phase, molecular_weight,
|
|
161
|
+
heat_of_formation.
|
|
162
|
+
"""
|
|
163
|
+
data: dict[str, Any] = {}
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
num_int_str = line[0:2].strip() if len(line) > 2 else ''
|
|
167
|
+
data['num_intervals'] = int(num_int_str) if num_int_str.isdigit() else 0
|
|
168
|
+
|
|
169
|
+
data['ref_code'] = line[3:9].strip() if len(line) > 9 else ''
|
|
170
|
+
|
|
171
|
+
phase_code = line[50:52].strip() if len(line) > 52 else '0'
|
|
172
|
+
data['phase'] = 'condensed' if phase_code and phase_code != '0' else 'gas'
|
|
173
|
+
|
|
174
|
+
mw_str = line[52:65].strip() if len(line) > 65 else ''
|
|
175
|
+
data['molecular_weight'] = self.parse_float(mw_str)
|
|
176
|
+
|
|
177
|
+
hf_str = line[65:80].strip() if len(line) > 80 else ''
|
|
178
|
+
data['heat_of_formation'] = self.parse_float(hf_str)
|
|
179
|
+
except Exception as e:
|
|
180
|
+
logger.warning('Error parsing RECORD 2: %s', e)
|
|
181
|
+
|
|
182
|
+
return data
|
|
183
|
+
|
|
184
|
+
@staticmethod
|
|
185
|
+
def parse_temp_interval_record(line: str) -> dict[str, Any]:
|
|
186
|
+
"""Parse RECORD 3 – temperature interval.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
line: RECORD 3 line from thermo.inp.
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
Dict with temp_min, temp_max, h_298_to_0.
|
|
193
|
+
"""
|
|
194
|
+
data: dict[str, Any] = {}
|
|
195
|
+
|
|
196
|
+
temp_min_str = line[0:11].strip() if len(line) > 11 else ''
|
|
197
|
+
temp_max_str = line[11:22].strip() if len(line) > 22 else ''
|
|
198
|
+
h298_str = line[65:80].strip() if len(line) > 80 else ''
|
|
199
|
+
|
|
200
|
+
data['temp_min'] = ThermoDBBuilder.parse_float(temp_min_str)
|
|
201
|
+
data['temp_max'] = ThermoDBBuilder.parse_float(temp_max_str)
|
|
202
|
+
data['h_298_to_0'] = ThermoDBBuilder.parse_float(h298_str)
|
|
203
|
+
|
|
204
|
+
return data
|
|
205
|
+
|
|
206
|
+
@staticmethod
|
|
207
|
+
def parse_coefficients_record(lines: list[str]) -> dict[str, Any]:
|
|
208
|
+
"""Parse RECORDS 4-5 – polynomial coefficients.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
lines: Two lines containing a1-a7 and b1-b2.
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
Dict with keys a1-a7, b1, b2.
|
|
215
|
+
"""
|
|
216
|
+
coeffs: dict[str, Any] = {}
|
|
217
|
+
|
|
218
|
+
line4 = lines[0] if len(lines) > 0 else ''
|
|
219
|
+
coeffs['a1'] = ThermoDBBuilder.parse_float(line4[0:16])
|
|
220
|
+
coeffs['a2'] = ThermoDBBuilder.parse_float(line4[16:32])
|
|
221
|
+
coeffs['a3'] = ThermoDBBuilder.parse_float(line4[32:48])
|
|
222
|
+
coeffs['a4'] = ThermoDBBuilder.parse_float(line4[48:64])
|
|
223
|
+
coeffs['a5'] = ThermoDBBuilder.parse_float(line4[64:80])
|
|
224
|
+
|
|
225
|
+
line5 = lines[1] if len(lines) > 1 else ''
|
|
226
|
+
coeffs['a6'] = ThermoDBBuilder.parse_float(line5[0:16])
|
|
227
|
+
coeffs['a7'] = ThermoDBBuilder.parse_float(line5[16:32])
|
|
228
|
+
coeffs['b1'] = ThermoDBBuilder.parse_float(line5[48:64])
|
|
229
|
+
coeffs['b2'] = ThermoDBBuilder.parse_float(line5[64:80])
|
|
230
|
+
|
|
231
|
+
return coeffs
|
|
232
|
+
|
|
233
|
+
# ------------------------------------------------------------------
|
|
234
|
+
# File reading & line-type detection
|
|
235
|
+
# ------------------------------------------------------------------
|
|
236
|
+
def read_thermo_file(self) -> list[str]:
|
|
237
|
+
"""Read thermo.inp, stripping comments and blank lines.
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
List of non-empty, non-comment lines.
|
|
241
|
+
"""
|
|
242
|
+
with open(self.inp_file, encoding='utf-8', errors='ignore') as f:
|
|
243
|
+
lines = f.readlines()
|
|
244
|
+
|
|
245
|
+
return [
|
|
246
|
+
line.rstrip('\n\r')
|
|
247
|
+
for line in lines
|
|
248
|
+
if line.strip() and not line.strip().startswith('!')
|
|
249
|
+
]
|
|
250
|
+
|
|
251
|
+
@staticmethod
|
|
252
|
+
def is_temperature_line(line: str) -> bool:
|
|
253
|
+
"""Detect RECORD 3 (temperature interval).
|
|
254
|
+
|
|
255
|
+
A temperature line has two valid floats in cols 0-11 and 11-22
|
|
256
|
+
where the first is strictly less than the second.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
line: A line from thermo.inp.
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
True if the line appears to be a temperature interval record.
|
|
263
|
+
"""
|
|
264
|
+
if len(line) < 22:
|
|
265
|
+
return False
|
|
266
|
+
try:
|
|
267
|
+
t1 = ThermoDBBuilder.parse_float(line[0:11])
|
|
268
|
+
t2 = ThermoDBBuilder.parse_float(line[11:22])
|
|
269
|
+
return t1 is not None and t2 is not None and t1 < t2
|
|
270
|
+
except Exception:
|
|
271
|
+
return False
|
|
272
|
+
|
|
273
|
+
@staticmethod
|
|
274
|
+
def is_coefficient_line(line: str) -> bool:
|
|
275
|
+
"""Detect coefficient lines containing FORTRAN D notation.
|
|
276
|
+
|
|
277
|
+
Uses regex to match the standard FORTRAN double-precision format
|
|
278
|
+
(e.g. ``1.23456789D+01``), which is more robust than substring
|
|
279
|
+
matching.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
line: A line from thermo.inp.
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
True if the line contains at least one FORTRAN D-format number.
|
|
286
|
+
"""
|
|
287
|
+
return bool(_FORTRAN_D_RE.search(line))
|
|
288
|
+
|
|
289
|
+
# ------------------------------------------------------------------
|
|
290
|
+
# Main parse & load
|
|
291
|
+
# ------------------------------------------------------------------
|
|
292
|
+
def parse_and_load(self) -> None:
|
|
293
|
+
"""Parse the thermo.inp file and populate the database."""
|
|
294
|
+
assert self.cursor is not None, 'Database not connected'
|
|
295
|
+
lines = self.read_thermo_file()
|
|
296
|
+
|
|
297
|
+
if not lines:
|
|
298
|
+
logger.warning('Empty thermo.inp file!')
|
|
299
|
+
return
|
|
300
|
+
|
|
301
|
+
# --- Global metadata (line index 1) ---
|
|
302
|
+
metadata_line = lines[1] if len(lines) > 1 else ''
|
|
303
|
+
parts = metadata_line.split()
|
|
304
|
+
if len(parts) >= 4:
|
|
305
|
+
self.cursor.execute(
|
|
306
|
+
"""
|
|
307
|
+
INSERT INTO file_metadata
|
|
308
|
+
(id, temp_min_global, temp_500_K, temp_1500_K,
|
|
309
|
+
temp_max_global, reference_date)
|
|
310
|
+
VALUES (1, ?, ?, ?, ?, ?)
|
|
311
|
+
""",
|
|
312
|
+
(
|
|
313
|
+
self.parse_float(parts[0]),
|
|
314
|
+
self.parse_float(parts[1]),
|
|
315
|
+
self.parse_float(parts[2]),
|
|
316
|
+
self.parse_float(parts[3]),
|
|
317
|
+
parts[4] if len(parts) > 4 else None,
|
|
318
|
+
),
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
# --- Species loop ---
|
|
322
|
+
i = 2
|
|
323
|
+
species_count = 0
|
|
324
|
+
skipped = 0
|
|
325
|
+
|
|
326
|
+
while i < len(lines):
|
|
327
|
+
try:
|
|
328
|
+
# RECORD 1 – species name
|
|
329
|
+
if i >= len(lines):
|
|
330
|
+
break
|
|
331
|
+
|
|
332
|
+
species_name, comments = self.parse_species_record(lines[i])
|
|
333
|
+
|
|
334
|
+
if (
|
|
335
|
+
not species_name
|
|
336
|
+
or len(species_name.split()) > 1
|
|
337
|
+
or self.is_temperature_line(lines[i])
|
|
338
|
+
):
|
|
339
|
+
i += 1
|
|
340
|
+
skipped += 1
|
|
341
|
+
continue
|
|
342
|
+
|
|
343
|
+
logger.info('Processing species: %s', species_name)
|
|
344
|
+
i += 1
|
|
345
|
+
|
|
346
|
+
# RECORD 2 – general info
|
|
347
|
+
if i >= len(lines):
|
|
348
|
+
break
|
|
349
|
+
general_info = self.parse_general_info_record(lines[i])
|
|
350
|
+
i += 1
|
|
351
|
+
|
|
352
|
+
if general_info.get('num_intervals', 0) <= 0:
|
|
353
|
+
skipped += 1
|
|
354
|
+
continue
|
|
355
|
+
|
|
356
|
+
# Insert species
|
|
357
|
+
try:
|
|
358
|
+
self.cursor.execute(
|
|
359
|
+
"""
|
|
360
|
+
INSERT INTO species
|
|
361
|
+
(name, comments, reference_code, phase,
|
|
362
|
+
molecular_weight, heat_of_formation_298K,
|
|
363
|
+
num_intervals)
|
|
364
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
365
|
+
""",
|
|
366
|
+
(
|
|
367
|
+
species_name,
|
|
368
|
+
comments,
|
|
369
|
+
general_info.get('ref_code'),
|
|
370
|
+
general_info.get('phase'),
|
|
371
|
+
general_info.get('molecular_weight'),
|
|
372
|
+
general_info.get('heat_of_formation'),
|
|
373
|
+
general_info.get('num_intervals'),
|
|
374
|
+
),
|
|
375
|
+
)
|
|
376
|
+
species_id = self.cursor.lastrowid
|
|
377
|
+
species_count += 1
|
|
378
|
+
except sqlite3.IntegrityError:
|
|
379
|
+
self.cursor.execute(
|
|
380
|
+
'SELECT id FROM species WHERE name = ?',
|
|
381
|
+
(species_name,),
|
|
382
|
+
)
|
|
383
|
+
result = self.cursor.fetchone()
|
|
384
|
+
if result:
|
|
385
|
+
species_id = result[0]
|
|
386
|
+
else:
|
|
387
|
+
skipped += 1
|
|
388
|
+
continue
|
|
389
|
+
|
|
390
|
+
# --- Temperature intervals ---
|
|
391
|
+
num_intervals = general_info.get('num_intervals', 0)
|
|
392
|
+
for interval_num in range(num_intervals):
|
|
393
|
+
if i >= len(lines):
|
|
394
|
+
break
|
|
395
|
+
|
|
396
|
+
if not self.is_temperature_line(lines[i]):
|
|
397
|
+
break
|
|
398
|
+
|
|
399
|
+
temp_interval = self.parse_temp_interval_record(lines[i])
|
|
400
|
+
i += 1
|
|
401
|
+
|
|
402
|
+
if i + 1 >= len(lines):
|
|
403
|
+
break
|
|
404
|
+
|
|
405
|
+
if not (
|
|
406
|
+
self.is_coefficient_line(lines[i])
|
|
407
|
+
and self.is_coefficient_line(lines[i + 1])
|
|
408
|
+
):
|
|
409
|
+
break
|
|
410
|
+
|
|
411
|
+
coeffs = self.parse_coefficients_record([lines[i], lines[i + 1]])
|
|
412
|
+
i += 2
|
|
413
|
+
|
|
414
|
+
if (
|
|
415
|
+
temp_interval.get('temp_min') is None
|
|
416
|
+
or temp_interval.get('temp_max') is None
|
|
417
|
+
):
|
|
418
|
+
continue
|
|
419
|
+
|
|
420
|
+
try:
|
|
421
|
+
self.cursor.execute(
|
|
422
|
+
"""
|
|
423
|
+
INSERT INTO temperature_intervals
|
|
424
|
+
(species_id, interval_number, temp_min,
|
|
425
|
+
temp_max, h_298_to_0)
|
|
426
|
+
VALUES (?, ?, ?, ?, ?)
|
|
427
|
+
""",
|
|
428
|
+
(
|
|
429
|
+
species_id,
|
|
430
|
+
interval_num + 1,
|
|
431
|
+
temp_interval.get('temp_min'),
|
|
432
|
+
temp_interval.get('temp_max'),
|
|
433
|
+
temp_interval.get('h_298_to_0'),
|
|
434
|
+
),
|
|
435
|
+
)
|
|
436
|
+
interval_id = self.cursor.lastrowid
|
|
437
|
+
|
|
438
|
+
self.cursor.execute(
|
|
439
|
+
"""
|
|
440
|
+
INSERT INTO coefficients
|
|
441
|
+
(interval_id, a1, a2, a3, a4, a5,
|
|
442
|
+
a6, a7, b1, b2)
|
|
443
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
444
|
+
""",
|
|
445
|
+
(
|
|
446
|
+
interval_id,
|
|
447
|
+
coeffs.get('a1'),
|
|
448
|
+
coeffs.get('a2'),
|
|
449
|
+
coeffs.get('a3'),
|
|
450
|
+
coeffs.get('a4'),
|
|
451
|
+
coeffs.get('a5'),
|
|
452
|
+
coeffs.get('a6'),
|
|
453
|
+
coeffs.get('a7'),
|
|
454
|
+
coeffs.get('b1'),
|
|
455
|
+
coeffs.get('b2'),
|
|
456
|
+
),
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
logger.debug(
|
|
460
|
+
' Interval %d: %sK - %sK',
|
|
461
|
+
interval_num + 1,
|
|
462
|
+
temp_interval.get('temp_min'),
|
|
463
|
+
temp_interval.get('temp_max'),
|
|
464
|
+
)
|
|
465
|
+
except Exception as e:
|
|
466
|
+
logger.warning(
|
|
467
|
+
'Error inserting interval %d: %s',
|
|
468
|
+
interval_num + 1,
|
|
469
|
+
e,
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
except Exception as e:
|
|
473
|
+
logger.warning('Error processing line %d: %s', i, e)
|
|
474
|
+
i += 1
|
|
475
|
+
|
|
476
|
+
# Final metadata update
|
|
477
|
+
assert self.cursor is not None
|
|
478
|
+
self.cursor.execute(
|
|
479
|
+
'UPDATE file_metadata SET total_species = ? WHERE id = 1',
|
|
480
|
+
(species_count,),
|
|
481
|
+
)
|
|
482
|
+
assert self.conn is not None
|
|
483
|
+
self.conn.commit()
|
|
484
|
+
|
|
485
|
+
logger.info('=' * 70)
|
|
486
|
+
logger.info('Total species loaded: %d', species_count)
|
|
487
|
+
logger.info('Skipped lines: %d', skipped)
|
|
488
|
+
logger.info('Database: %s', self.db_file)
|