pymolfit 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.
Files changed (71) hide show
  1. pymolfit-0.1.0/PKG-INFO +753 -0
  2. pymolfit-0.1.0/README.md +718 -0
  3. pymolfit-0.1.0/THIRD_PARTY_NOTICES.md +40 -0
  4. pymolfit-0.1.0/pyproject.toml +74 -0
  5. pymolfit-0.1.0/setup.cfg +4 -0
  6. pymolfit-0.1.0/src/pymolfit/__init__.py +241 -0
  7. pymolfit-0.1.0/src/pymolfit/_version.py +1 -0
  8. pymolfit-0.1.0/src/pymolfit/aer_data.py +928 -0
  9. pymolfit-0.1.0/src/pymolfit/atmosphere.py +1987 -0
  10. pymolfit-0.1.0/src/pymolfit/cli.py +750 -0
  11. pymolfit-0.1.0/src/pymolfit/components.py +1915 -0
  12. pymolfit-0.1.0/src/pymolfit/continuum.py +1664 -0
  13. pymolfit-0.1.0/src/pymolfit/data/lblrtm_v12_11_co2_continuum.npz +0 -0
  14. pymolfit-0.1.0/src/pymolfit/data/lblrtm_v12_11_h2o_continuum.npz +0 -0
  15. pymolfit-0.1.0/src/pymolfit/data/lblrtm_v12_11_n2_fundamental.npz +0 -0
  16. pymolfit-0.1.0/src/pymolfit/data/lblrtm_v12_11_o2_continuum.npz +0 -0
  17. pymolfit-0.1.0/src/pymolfit/data/lblrtm_v12_11_tips.npz +0 -0
  18. pymolfit-0.1.0/src/pymolfit/data/profiles/gdas/GDAS_t0_s1.fits +0 -0
  19. pymolfit-0.1.0/src/pymolfit/data/profiles/gdas/GDAS_t0_s2.fits +0 -0
  20. pymolfit-0.1.0/src/pymolfit/data/profiles/gdas/GDAS_t0_s3.fits +0 -0
  21. pymolfit-0.1.0/src/pymolfit/data/profiles/gdas/GDAS_t0_s4.fits +0 -0
  22. pymolfit-0.1.0/src/pymolfit/data/profiles/gdas/GDAS_t0_s5.fits +0 -0
  23. pymolfit-0.1.0/src/pymolfit/data/profiles/gdas/GDAS_t0_s6.fits +0 -0
  24. pymolfit-0.1.0/src/pymolfit/data/profiles/mipas/equ.fits +57 -0
  25. pymolfit-0.1.0/src/pymolfit/data/profiles/mipas/std.fits +2 -1
  26. pymolfit-0.1.0/src/pymolfit/data/profiles/mipas/tro.fits +8 -0
  27. pymolfit-0.1.0/src/pymolfit/diagnostics.py +66 -0
  28. pymolfit-0.1.0/src/pymolfit/fit.py +2396 -0
  29. pymolfit-0.1.0/src/pymolfit/gdas.py +297 -0
  30. pymolfit-0.1.0/src/pymolfit/hitran.py +629 -0
  31. pymolfit-0.1.0/src/pymolfit/io.py +294 -0
  32. pymolfit-0.1.0/src/pymolfit/line_data.py +561 -0
  33. pymolfit-0.1.0/src/pymolfit/linelist.py +575 -0
  34. pymolfit-0.1.0/src/pymolfit/model.py +1141 -0
  35. pymolfit-0.1.0/src/pymolfit/partition.py +457 -0
  36. pymolfit-0.1.0/src/pymolfit/physics.py +1499 -0
  37. pymolfit-0.1.0/src/pymolfit/plotting.py +44 -0
  38. pymolfit-0.1.0/src/pymolfit/provenance.py +163 -0
  39. pymolfit-0.1.0/src/pymolfit/radiative_transfer.py +250 -0
  40. pymolfit-0.1.0/src/pymolfit/spectrum.py +249 -0
  41. pymolfit-0.1.0/src/pymolfit/systematics.py +413 -0
  42. pymolfit-0.1.0/src/pymolfit/validation.py +490 -0
  43. pymolfit-0.1.0/src/pymolfit/workflow.py +1391 -0
  44. pymolfit-0.1.0/src/pymolfit.egg-info/PKG-INFO +753 -0
  45. pymolfit-0.1.0/src/pymolfit.egg-info/SOURCES.txt +69 -0
  46. pymolfit-0.1.0/src/pymolfit.egg-info/dependency_links.txt +1 -0
  47. pymolfit-0.1.0/src/pymolfit.egg-info/entry_points.txt +2 -0
  48. pymolfit-0.1.0/src/pymolfit.egg-info/requires.txt +12 -0
  49. pymolfit-0.1.0/src/pymolfit.egg-info/top_level.txt +1 -0
  50. pymolfit-0.1.0/tests/test_aer_data.py +283 -0
  51. pymolfit-0.1.0/tests/test_atmosphere.py +467 -0
  52. pymolfit-0.1.0/tests/test_cli.py +237 -0
  53. pymolfit-0.1.0/tests/test_components.py +158 -0
  54. pymolfit-0.1.0/tests/test_continuum.py +436 -0
  55. pymolfit-0.1.0/tests/test_external_absorption.py +303 -0
  56. pymolfit-0.1.0/tests/test_fit.py +208 -0
  57. pymolfit-0.1.0/tests/test_fit_controls.py +499 -0
  58. pymolfit-0.1.0/tests/test_gdas.py +71 -0
  59. pymolfit-0.1.0/tests/test_io.py +167 -0
  60. pymolfit-0.1.0/tests/test_lblrtm_audit.py +628 -0
  61. pymolfit-0.1.0/tests/test_line_data.py +174 -0
  62. pymolfit-0.1.0/tests/test_model.py +386 -0
  63. pymolfit-0.1.0/tests/test_molecfit_reference_data.py +35 -0
  64. pymolfit-0.1.0/tests/test_physics_hitran.py +1047 -0
  65. pymolfit-0.1.0/tests/test_physics_parity.py +445 -0
  66. pymolfit-0.1.0/tests/test_release_tools.py +142 -0
  67. pymolfit-0.1.0/tests/test_spectrum.py +38 -0
  68. pymolfit-0.1.0/tests/test_systematics.py +122 -0
  69. pymolfit-0.1.0/tests/test_validation.py +137 -0
  70. pymolfit-0.1.0/tests/test_validation_review.py +198 -0
  71. pymolfit-0.1.0/tests/test_workflow.py +399 -0
@@ -0,0 +1,753 @@
1
+ Metadata-Version: 2.4
2
+ Name: pymolfit
3
+ Version: 0.1.0
4
+ Summary: Generic pure-Python telluric fitting and correction with source-audited radiative-transfer physics.
5
+ Author: Nikolas Valsamidis
6
+ License: Scientific/research use; see THIRD_PARTY_NOTICES.md for bundled AER data terms
7
+ Project-URL: Homepage, https://github.com/nikolasvalsamidis97/PyMolFit
8
+ Project-URL: Issues, https://github.com/nikolasvalsamidis97/PyMolFit/issues
9
+ Project-URL: Repository, https://github.com/nikolasvalsamidis97/PyMolFit.git
10
+ Keywords: astronomy,radiative transfer,spectroscopy,telluric correction
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Scientific/Engineering :: Astronomy
21
+ Classifier: Topic :: Scientific/Engineering :: Physics
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/markdown
24
+ License-File: THIRD_PARTY_NOTICES.md
25
+ Requires-Dist: numpy>=1.23
26
+ Requires-Dist: scipy>=1.9
27
+ Requires-Dist: astropy>=5.0
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest>=7.0; extra == "dev"
30
+ Provides-Extra: examples
31
+ Requires-Dist: matplotlib>=3.7; extra == "examples"
32
+ Provides-Extra: plot
33
+ Requires-Dist: matplotlib>=3.7; extra == "plot"
34
+ Dynamic: license-file
35
+
36
+ # PyMolFit
37
+
38
+ PyMolFit is a pure-Python generic telluric correction package inspired by ESO
39
+ Molecfit. The goal is to fit and remove telluric absorption from spectra
40
+ without requiring the user to know about ESO recipe files, EsoRex-style
41
+ workflow files, shell paths, or compiled C/Fortran executables.
42
+
43
+ The current release is a science-ready candidate pending independent blind
44
+ review; it is not claimed to be a bit-identical drop-in replacement for
45
+ Molecfit/LBLRTM. Automated validation covers synthetic recovery, uncertainty
46
+ coverage, failure behavior, and real HARPS, UVES, X-shooter, and CRIRES+
47
+ spectra, plus independent Keck/HIRES and Keck/KPF spectra.
48
+ The current version implements generic spectrum input/output, a vectorized
49
+ molecular-line absorption model, a self-contained HITRAN `.par` reader,
50
+ HITRAN/TIPS partition-sum tables, HITRAN isotopologue metadata and abundance
51
+ scaling, source-backed LBLRTM H2O, CO2, N2, and O2 continua, optional external
52
+ HITRAN CIA tables, LBLRTM `contnm.f90` Rayleigh scattering, layer-by-layer
53
+ radiative transfer, Molecfit-inspired line-window and LSF controls, fitting,
54
+ correction, diagnostics, and comparison tools. Scientific workflows use the
55
+ official versioned AER 3.9 catalogue. PyMolFit downloads it from its immutable
56
+ Zenodo release, verifies it, and caches it automatically;
57
+ the small built-in synthetic list is retained only for tests and examples.
58
+
59
+ ## Why This Shape
60
+
61
+ Molecfit is built from several responsibilities:
62
+
63
+ - read spectra and wavelength/fit ranges
64
+ - create an atmospheric transmission model from molecular line data and an
65
+ atmospheric profile
66
+ - convolve the model with the instrumental profile
67
+ - fit continuum, wavelength/resolution adjustments, and molecule abundances
68
+ - divide the observed spectrum by the fitted telluric transmission
69
+
70
+ PyMolFit keeps the same conceptual split, but implements the first usable
71
+ layer in Python:
72
+
73
+ - `Spectrum`: generic 1D spectrum container
74
+ - `LineList`: molecular line centers, strengths, widths, and species labels
75
+ - `transmission_model`: vectorized optical-depth and transmission model
76
+ - `AbsorptionComponent`: plugin-style interface for physical opacity sources
77
+ - `HitranLineAbsorption`: HITRAN line-by-line component
78
+ - `IsotopologueMetadata`: HITRAN global/local isotopologue IDs, abundances,
79
+ masses, and q-file names
80
+ - `PartitionTable`: tabulated HITRAN/TIPS partition sums
81
+ - `physical_transmission_model`: self-contained HITRAN line-by-line model
82
+ - `MTCKDH2OContinuum`: vectorized MT_CKD H2O self/foreign continuum model
83
+ - `H2OContinuumAbsorption`: MT_CKD H2O continuum component
84
+ - `CO2ContinuumAbsorption`: CO2 continuum component from external tables
85
+ - `O2CIAAbsorption` / `N2CIAAbsorption`: HITRAN CIA-file components
86
+ - `RayleighScatteringAbsorption`: LBLRTM `contnm.f90` Rayleigh component
87
+ - `N2RototranslationalContinuumAbsorption`: LBLRTM N2 pure-rotation continuum
88
+ - `fit_tellurics`: nonlinear least-squares fit of molecule scales and continuum
89
+ - `correct_spectrum`: divides flux by the fitted transmission
90
+
91
+ ## Install For Development
92
+
93
+ ```bash
94
+ python -m pip install -e ".[dev]"
95
+ ```
96
+
97
+ For diagnostic plots:
98
+
99
+ ```bash
100
+ python -m pip install -e ".[dev,plot]"
101
+ ```
102
+
103
+ For a normal release install, the intended interface is simply:
104
+
105
+ ```bash
106
+ python -m pip install pymolfit
107
+ pymolfit fit spectrum.fits corrected.ecsv
108
+ ```
109
+
110
+ The wheel does not contain the 759 MiB uncompressed AER catalogue. On first
111
+ scientific use PyMolFit downloads the official AER 3.9 archive from
112
+ [Zenodo record 18881607](https://doi.org/10.5281/zenodo.18881607), extracts its
113
+ line catalogue, broadening tables, and line-coupling table, verifies fixed
114
+ SHA-256 hashes, and stores them under `~/.cache/pymolfit/aer`. No HITRAN
115
+ account or API key is needed.
116
+ The data can be installed ahead of time and inspected explicitly:
117
+
118
+ ```bash
119
+ pymolfit install-aer
120
+ pymolfit aer-status
121
+ ```
122
+
123
+ Set `PYMOLFIT_AER_URL` only to use another location containing the exact
124
+ `aer_v_3.9.tgz` artifact. The extracted scientific payload must still match
125
+ the hashes pinned from the official Zenodo release.
126
+
127
+ ## Python Example
128
+
129
+ ```python
130
+ from pymolfit import LineList, Spectrum, fit_tellurics
131
+
132
+ spectrum = Spectrum(wavelength=wavelength_micron, flux=flux)
133
+ result = fit_tellurics(spectrum, line_list=LineList.demo_near_ir())
134
+
135
+ corrected = result.corrected
136
+ ```
137
+
138
+ For normal scripting, the higher-level workflow is shorter:
139
+
140
+ ```python
141
+ from pymolfit import correct_file
142
+
143
+ result = correct_file(
144
+ "spectrum.fits",
145
+ "corrected.txt",
146
+ wavelength_unit="nm",
147
+ hitran_species=("H2O", "CO2", "CH4"),
148
+ atmosphere_mode="mipas_gdas",
149
+ # alternatively: atmosphere_table="profile.ecsv",
150
+ pwv_mm=2.0,
151
+ solve_continuum_linear=True,
152
+ fit_wavelength_polynomial=True,
153
+ wavelength_polynomial_order=1,
154
+ fit_lsf_sigma=True,
155
+ estimate_uncertainties=True,
156
+ product_path="fit_product.ecsv",
157
+ )
158
+ ```
159
+
160
+ With no `line_list`, `line_list_path`, or `hitran_par`, this workflow selects
161
+ the required wavelength window from the managed AER catalogue and caches the
162
+ result as a provenance-bearing ECSV table. Pass `aer_catalog=None` (Python) or
163
+ `--no-auto-aer` (CLI) only when intentionally running a continuum-only model
164
+ or supplying another opacity source.
165
+
166
+ `solve_continuum_linear=True` removes the linear continuum coefficients from
167
+ the nonlinear optimizer and is recommended for spectra with large absolute
168
+ flux values. With `estimate_uncertainties=True`, the result includes a
169
+ linearized parameter covariance, molecular-scale standard errors,
170
+ transmission uncertainty, and corrected-flux uncertainty. These uncertainties
171
+ are conditional on the supplied atmosphere, line data, masks, continuum
172
+ family, and LSF model. Use `fit_tellurics_with_systematics` to refit named
173
+ alternative model configurations and add their transmission spread to the
174
+ corrected-flux uncertainty; no universal atmosphere/LSF ensemble is assumed.
175
+ Shared multi-segment fits propagate the shared covariance into every corrected
176
+ segment. Rank-deficient fits report `NaN` covariance/output uncertainty rather
177
+ than false precision. Full product tables retain input, fit-pixel, and
178
+ corrected masks; parameter-bound flags; and machine-readable provenance. The
179
+ provenance hashes the modeled spectrum, source file, full and selected line
180
+ lists, atmospheric layers, component data, and fit configuration, and records
181
+ the numerical library versions. Wavelength correction can be a constant shift, one polynomial
182
+ shared over the full wavelength span, or independent per-segment polynomials
183
+ in the lower-level multi-segment API. Covariance rank is evaluated after
184
+ Jacobian column scaling, so the result is invariant to parameter units.
185
+
186
+ ## Science-readiness status
187
+
188
+ PyMolFit has an automated cross-band, cross-instrument validation campaign,
189
+ but it does not currently pass its full scientific acceptance gate. See
190
+ [`docs/science_readiness_validation.md`](docs/science_readiness_validation.md)
191
+ for the checklist, public-data provenance, numerical thresholds, current
192
+ caveats, and reproducible evidence files. Do not describe version `0.1.0` as a
193
+ validated drop-in replacement for Molecfit.
194
+
195
+ ## Physical HITRAN Example
196
+
197
+ This path does not call Molecfit, LBLRTM, LNFL, or HAPI at model-evaluation
198
+ time. It parses a HITRAN `.par` file and calculates Voigt line absorption in Python. If you provide
199
+ HITRAN isotopologue metadata and TIPS q files, line intensities use tabulated
200
+ partition sums instead of the fallback power-law approximation.
201
+
202
+ ```python
203
+ from pymolfit import (
204
+ AtmosphereProfile,
205
+ FitConfig,
206
+ IsotopologueMetadata,
207
+ LineList,
208
+ PartitionTable,
209
+ Spectrum,
210
+ fit_tellurics,
211
+ )
212
+
213
+ line_list = LineList.from_hitran_par(
214
+ "hitran_h2o_lband.par",
215
+ wavenumber_min=4200,
216
+ wavenumber_max=4400,
217
+ )
218
+ isotopologues = IsotopologueMetadata.from_hitran_iso_meta_html("hitran_iso_meta.html")
219
+ line_list = line_list.with_isotopologue_metadata(isotopologues)
220
+ partition = PartitionTable.from_hitran_q_directory("hitran_q", isotopologues)
221
+
222
+ atmosphere = AtmosphereProfile.single_layer(
223
+ pressure_atm=0.75,
224
+ temperature_k=280,
225
+ mixing_ratios={"H2O": 0.001, "CO2": 4.2e-4, "CH4": 1.9e-6},
226
+ )
227
+
228
+ spectrum = Spectrum(wavelength=wavelength_micron, flux=flux)
229
+ result = fit_tellurics(
230
+ spectrum,
231
+ line_list=line_list,
232
+ config=FitConfig(
233
+ atmosphere=atmosphere,
234
+ airmass=1.2,
235
+ partition_table=partition,
236
+ continuum_order=2,
237
+ ),
238
+ )
239
+ ```
240
+
241
+ The same physical path is available through `correct_arrays` and `correct_file`.
242
+ See `examples/physical_hitran_demo.py` for a fully self-contained synthetic
243
+ HITRAN-style example.
244
+
245
+ A more realistic built-in atmosphere is also available:
246
+
247
+ ```python
248
+ atmosphere = AtmosphereProfile.standard_midlatitude()
249
+ atmosphere = atmosphere.with_pwv_mm(2.0)
250
+ ```
251
+
252
+ The default high-level physical workflow now uses a Molecfit-style
253
+ MIPAS+GDAS atmosphere:
254
+
255
+ ```python
256
+ from pymolfit import AtmosphereProfile
257
+
258
+ atmosphere = AtmosphereProfile.from_mipas_gdas(
259
+ observation_time="2022-01-02T05:17:35",
260
+ observatory_altitude_m=2635.0,
261
+ airmass=1.2,
262
+ )
263
+ ```
264
+
265
+ For FITS inputs, `correct_file(..., atmosphere_mode="mipas_gdas")` reads common
266
+ FITS, ESO, and legacy Keck header keywords for observation time, airmass, site
267
+ altitude, longitude, latitude, pressure, temperature, and relative humidity.
268
+ When coordinates are absent, named Paranal, La Silla, and Keck observatories
269
+ are resolved through the packaged site registry; explicit FITS coordinates
270
+ always take precedence and the resolution source is retained in provenance.
271
+ An unknown site without complete coordinates now raises an error instead of
272
+ silently using Paranal. Supply `observatory_latitude_deg`,
273
+ `observatory_longitude_deg`, and `observatory_altitude_m`, or explicitly opt in
274
+ to the legacy fallback with `allow_default_observatory=True`. Array and plain
275
+ text inputs using MIPAS/GDAS require the same explicit geometry because they do
276
+ not carry a FITS site header.
277
+ Known barycentric and heliocentric wavelength products are moved back to the
278
+ observatory frame before telluric fitting, with the applied velocity and frame
279
+ retained in the product provenance. By default
280
+ `gdas_mode="auto"` tries to download the exact ESO GDAS tarball for the
281
+ rounded observatory coordinates, interpolates the bracketing 3-hour profiles to
282
+ the observation time, and caches both the tarball and the interpolated FITS
283
+ profile under `~/.cache/pymolfit/gdas`. If the exact profile is unavailable,
284
+ PyMolFit falls back to the same bundled six two-month average GDAS profiles
285
+ that Molecfit ships, merged with the packaged MIPAS climatology.
286
+
287
+ GDAS source control:
288
+
289
+ ```python
290
+ result = correct_file(
291
+ "spectrum.fits",
292
+ "corrected.txt",
293
+ hitran_par="hitran_lband.par",
294
+ atmosphere_mode="mipas_gdas",
295
+ gdas_mode="auto", # exact download/cache, then monthly average fallback
296
+ # gdas_mode="online", # require exact download/cache
297
+ # gdas_mode="cache", # require exact cached tarball/profile, no download
298
+ # gdas_mode="average", # force bundled monthly averages
299
+ )
300
+ ```
301
+
302
+ `with_pwv_mm()` scales the vertical H2O column. For manual `fit_tellurics`
303
+ calls, keep the atmosphere vertical and pass the observation airmass through
304
+ `FitConfig`. For `correct_arrays`, `correct_file`, and the CLI, pass `airmass`
305
+ there; internally built atmospheres are slanted once before fitting. If you
306
+ deliberately need pre-slanted layer paths, `standard_midlatitude` also accepts
307
+ `airmass` and uses spherical-shell geometry for its layer path lengths.
308
+
309
+ ## Model-systematic sensitivity
310
+
311
+ The optimizer covariance is conditional on one chosen physical and
312
+ instrumental model. To measure sensitivity to defensible alternatives, refit
313
+ the same pixels under named configurations:
314
+
315
+ ```python
316
+ from dataclasses import replace
317
+ from pymolfit import FitConfig, fit_tellurics_with_systematics
318
+
319
+ baseline = FitConfig(
320
+ atmosphere=atmosphere,
321
+ species=("H2O", "O2"),
322
+ lsf_sigma_pixels=1.2,
323
+ estimate_uncertainties=True,
324
+ )
325
+ variants = {
326
+ "warmer_profile": replace(
327
+ baseline,
328
+ atmosphere=atmosphere.perturbed(temperature_offset_k=2.0),
329
+ ),
330
+ "broader_lsf": replace(baseline, lsf_sigma_pixels=1.3),
331
+ }
332
+ systematics = fit_tellurics_with_systematics(
333
+ spectrum,
334
+ line_list,
335
+ baseline,
336
+ variants,
337
+ )
338
+ systematics.write("fit_with_systematics.ecsv")
339
+ ```
340
+
341
+ The result stores every variant transmission, the RMS systematic uncertainty,
342
+ the full envelope, and a correction whose uncertainty combines the fitted
343
+ statistical term and ensemble spread in quadrature. Perturbation sizes must
344
+ come from site, atmospheric-product, or instrument-calibration uncertainties;
345
+ they are not fitted to make one observed spectrum resemble Molecfit.
346
+
347
+ ## Blocked Cross-Validation
348
+
349
+ Use blocked out-of-fold prediction to check whether a fitted configuration
350
+ generalizes to pixels that were not used by the optimizer:
351
+
352
+ ```python
353
+ from dataclasses import replace
354
+ from pymolfit import cross_validate_telluric_segments
355
+
356
+ cross_validation = cross_validate_telluric_segments(
357
+ spectra,
358
+ line_list=line_list,
359
+ config=replace(config, estimate_uncertainties=False),
360
+ block_size=64,
361
+ n_folds=2,
362
+ )
363
+ cross_validation.write("cross_validation")
364
+ print(cross_validation.metrics)
365
+ ```
366
+
367
+ The split uses alternating contiguous pixel blocks and does not inspect flux
368
+ residuals, fitted transmission, or a reference correction. Every valid pixel
369
+ is predicted once by a fit that did not consume that pixel. Reported metrics
370
+ separate complete prediction coverage from correction coverage lost to
371
+ saturated transmission, and compare held-out telluric residuals against the
372
+ continuum-only prediction.
373
+
374
+ ## MT_CKD H2O Continuum
375
+
376
+ PyMolFit can add the LBLRTM/MT_CKD water-vapor continuum when you provide the
377
+ AER `absco-ref_wv-mt-ckd.nc` coefficient file. The implementation reads the
378
+ reference self and foreign continuum coefficients, applies density scaling,
379
+ self-continuum temperature scaling, the radiation term, and the same cubic
380
+ interpolation formula as the reference MT_CKD H2O routine.
381
+
382
+ Python:
383
+
384
+ ```python
385
+ from pymolfit import correct_file
386
+
387
+ result = correct_file(
388
+ "spectrum.fits",
389
+ "corrected.txt",
390
+ hitran_par="hitran_h2o.par",
391
+ h2o_continuum="absco-ref_wv-mt-ckd.nc",
392
+ atmosphere_mode="standard",
393
+ pwv_mm=2.0,
394
+ )
395
+ ```
396
+
397
+ Continuum-only modelling is also supported if you are testing broad H2O
398
+ continuum effects without a line list:
399
+
400
+ ```python
401
+ result = correct_file(
402
+ "spectrum.fits",
403
+ "corrected.txt",
404
+ h2o_continuum="absco-ref_wv-mt-ckd.nc",
405
+ atmosphere_mode="standard",
406
+ pwv_mm=2.0,
407
+ )
408
+ ```
409
+
410
+ Command line:
411
+
412
+ ```bash
413
+ pymolfit fit spectrum.txt corrected.txt \
414
+ --hitran-par hitran_h2o.par \
415
+ --mtckd-h2o absco-ref_wv-mt-ckd.nc \
416
+ --atmosphere standard \
417
+ --pwv-mm 2.0
418
+ ```
419
+
420
+ Use `--mtckd-h2o-foreign-closure` to use the optional foreign-closure
421
+ coefficient column from MT_CKD 4.2+ files.
422
+
423
+ ## TIPS And Line Wings
424
+
425
+ HITRAN line intensities are weighted by natural terrestrial isotopologue
426
+ abundances. PyMolFit keeps that as the default. To model a non-terrestrial
427
+ isotopic mixture, attach `IsotopologueMetadata` and pass absolute abundance
428
+ overrides; PyMolFit applies `override / natural_abundance` internally.
429
+
430
+ ```python
431
+ line_list = line_list.with_isotopologue_metadata(
432
+ isotopologues,
433
+ abundance_overrides={(1, 1): 0.95, (1, 2): 0.05},
434
+ )
435
+ ```
436
+
437
+ The default line shape is an untruncated Voigt profile. For an LBLRTM-style
438
+ calculation window, use `line_wing_mode="lblrtm_dynamic"`. This ports the
439
+ LBLRTM `AVRAT`/`ALFV`/`ALFMAX`/`HWF3` per-line window sizing from
440
+ `oprop_voigt.f90`; HITRAN line selection also expands automatically for the
441
+ largest dynamic window implied by the wavelength grid. For simpler fixed-window
442
+ experiments, use `line_cutoff_cm`. Neither mode makes the whole calculation
443
+ bit-identical to LBLRTM by itself, but both avoid letting isolated Voigt wings
444
+ contribute indefinitely.
445
+
446
+ For experiments that should follow LBLRTM's tabulated Voigt subfunction and
447
+ panel interpolation machinery more closely, use `line_wing_mode="lblrtm_panel"`.
448
+ This builds the 0-4, 0-16, and 0-64 effective-halfwidth subfunction tables from
449
+ the LBLRTM source formula using SciPy's Faddeeva function, then applies the
450
+ source `PANEL` four-to-one interpolation coefficients. It is an expert parity
451
+ option and not guaranteed to be faster or better on every spectrum. The older
452
+ `line_wing_mode="lblrtm_table"` mode keeps only the tabulated subfunction
453
+ decomposition without panel accumulation.
454
+
455
+ ```python
456
+ config = FitConfig(
457
+ atmosphere=atmosphere,
458
+ partition_table=partition,
459
+ line_wing_mode="lblrtm_dynamic",
460
+ )
461
+ ```
462
+
463
+ The same controls are available on the CLI:
464
+
465
+ ```bash
466
+ pymolfit fit spectrum.txt corrected.txt \
467
+ --hitran-par hitran_lband.par \
468
+ --partition-table tips_lband.ecsv \
469
+ --line-wing-mode lblrtm_dynamic
470
+ ```
471
+
472
+ For Molecfit-style instrumental broadening, PyMolFit supports the synthetic
473
+ box/Gaussian/Lorentzian kernel controls. Molecfit's optional `kern_mode`
474
+ Voigt-approximation path is exposed as `lsf_molecfit_voigt=True` or
475
+ `--lsf-molecfit-voigt`.
476
+
477
+ Physical high-level runs default to the audited LBLRTM path: packaged
478
+ LBLRTM-12.11 TIPS tables, packaged MT_CKD/LBLRTM H2O and CO2 continuum data,
479
+ the `lblrtm_panel` line accumulator, an oversampled internal grid,
480
+ Molecfit overlap rebinning before LSF convolution, separate Gaussian and
481
+ Lorentzian kernels, and the source kernel support of 3 FWHM. Use
482
+ `line_wing_mode="lblrtm_dynamic"` for a faster approximation, or explicitly
483
+ pass `h2o_continuum="none"` / `co2_continuum="none"` to disable continua.
484
+
485
+ ## Rayleigh, N2, CO2, And CIA
486
+
487
+ PyMolFit also supports external continuum and collision-induced absorption
488
+ tables, plus source-backed `contnm.f90` branches:
489
+
490
+ - `rayleigh=True` / `--rayleigh` enables the LBLRTM Rayleigh formula for
491
+ wavenumbers above 820 cm-1.
492
+ - `n2_continuum=True` / `--n2-continuum` enables the LBLRTM N2
493
+ rototranslational, fundamental, and first-overtone continua.
494
+ - `o2_continuum=True` / `--o2-continuum` enables the LBLRTM O2 fundamental,
495
+ 1.27 micron, 9100--11000 cm-1, A-band, and visible continua.
496
+ - `co2_continuum=` reads an Astropy-readable table with `wavenumber_cm`,
497
+ optional `temperature_k`, and `coefficient` columns.
498
+ - `o2_cia=` and `n2_cia=` read HITRAN `.cia` files. HITRAN CIA coefficients
499
+ are in cm5 molecule-2 and are scaled by the two collision-partner number
500
+ densities and path length in each atmosphere layer.
501
+
502
+ Python:
503
+
504
+ ```python
505
+ result = correct_file(
506
+ "spectrum.fits",
507
+ "corrected.txt",
508
+ hitran_par="hitran.par",
509
+ h2o_continuum="absco-ref_wv-mt-ckd.nc",
510
+ co2_continuum="co2_continuum.ecsv",
511
+ rayleigh=True,
512
+ n2_continuum=True,
513
+ o2_continuum=True,
514
+ atmosphere_mode="standard",
515
+ )
516
+ ```
517
+
518
+ Command line:
519
+
520
+ ```bash
521
+ pymolfit fit spectrum.txt corrected.txt \
522
+ --hitran-par hitran.par \
523
+ --mtckd-h2o absco-ref_wv-mt-ckd.nc \
524
+ --co2-continuum co2_continuum.ecsv \
525
+ --rayleigh \
526
+ --n2-continuum \
527
+ --o2-continuum \
528
+ --atmosphere standard
529
+ ```
530
+
531
+ The source-backed N2/O2 continua and overlapping HITRAN CIA tables are
532
+ alternative representations. PyMolFit rejects combinations such as
533
+ `n2_continuum=True` with an N2-N2 CIA table instead of silently double-counting
534
+ the same collision-induced absorption.
535
+
536
+ The CO2 continuum remains table-driven rather than hardcoded from LBLRTM block
537
+ data. This keeps the package lightweight and lets users update or swap
538
+ continuum datasets without changing PyMolFit code.
539
+
540
+ ## Component Architecture
541
+
542
+ The physical backend is now built from absorption components. Each component
543
+ returns species names and optical-depth basis rows on the requested wavelength
544
+ grid. Components with the same species are summed before fitting, so H2O line
545
+ absorption and H2O continuum share one fitted H2O scale factor.
546
+
547
+ ```python
548
+ from pymolfit import (
549
+ AtmosphereProfile,
550
+ CO2ContinuumAbsorption,
551
+ FitConfig,
552
+ H2OContinuumAbsorption,
553
+ HitranLineAbsorption,
554
+ LineList,
555
+ MTCKDH2OContinuum,
556
+ N2RototranslationalContinuumAbsorption,
557
+ O2CIAAbsorption,
558
+ Spectrum,
559
+ TabulatedContinuum,
560
+ fit_tellurics,
561
+ )
562
+
563
+ line_list = LineList.from_hitran_par("hitran_lband.par")
564
+ h2o_continuum = MTCKDH2OContinuum.from_netcdf("absco-ref_wv-mt-ckd.nc")
565
+ co2_continuum = TabulatedContinuum.from_table("co2_continuum.ecsv")
566
+ atmosphere = AtmosphereProfile.standard_midlatitude().with_pwv_mm(2.0)
567
+
568
+ components = (
569
+ HitranLineAbsorption(line_list),
570
+ H2OContinuumAbsorption(h2o_continuum),
571
+ CO2ContinuumAbsorption(co2_continuum),
572
+ N2RototranslationalContinuumAbsorption(),
573
+ # O2CIAAbsorption(HitranCIATable.from_hitran_cia("O2-O2_2011.cia")),
574
+ )
575
+
576
+ result = fit_tellurics(
577
+ Spectrum(wavelength=wavelength_micron, flux=flux),
578
+ line_list=LineList.empty_hitran(),
579
+ config=FitConfig(
580
+ atmosphere=atmosphere,
581
+ airmass=1.2,
582
+ components=components,
583
+ continuum_order=2,
584
+ ),
585
+ )
586
+ ```
587
+
588
+ The old high-level arguments, such as `hitran_par=` and `h2o_continuum=`, still
589
+ work. They now build this component set internally.
590
+
591
+ ## Command Line Example
592
+
593
+ PyMolFit can load several common 1D spectrum formats:
594
+
595
+ - whitespace numeric text: wavelength, flux, optional uncertainty
596
+ - CSV numeric text
597
+ - ECSV or Astropy-readable tables with names like `wave`, `wavelength`,
598
+ `flux`, `err`, or `uncertainty`
599
+ - FITS binary tables with inferred wavelength/flux/error columns
600
+ - gzip-compressed `.fits.gz`/`.fit.gz` tables and images
601
+ - 1D FITS image spectra with linear `CRVAL1`/`CDELT1` wavelength WCS
602
+
603
+ For a non-scientific format smoke test with the synthetic demo line list:
604
+
605
+ ```bash
606
+ pymolfit fit spectrum.txt corrected.txt --demo-lines
607
+ ```
608
+
609
+ For a FITS table or 1D FITS image, supply real line data:
610
+
611
+ ```bash
612
+ pymolfit fit spectrum.fits corrected.txt \
613
+ --wavelength-unit nm \
614
+ --hitran-par hitran_window.par
615
+ ```
616
+
617
+ PyMolFit deliberately refuses to run a line fit without `--hitran-par`,
618
+ `--line-list`, an explicit continuum-only component, or `--demo-lines`. The
619
+ packaged demo list is synthetic and must not be used for scientific spectra.
620
+
621
+ With a HITRAN `.par` file:
622
+
623
+ ```bash
624
+ pymolfit fit spectrum.txt corrected.txt \
625
+ --hitran-par hitran_h2o_lband.par \
626
+ --mtckd-h2o absco-ref_wv-mt-ckd.nc \
627
+ --hitran-species H2O \
628
+ --hitran-min-strength 1e-28 \
629
+ --airmass 1.2 \
630
+ --atmosphere standard \
631
+ --pwv-mm 2.0 \
632
+ --partition-table partition_sums.ecsv \
633
+ --mixing-ratio H2O=0.001
634
+ ```
635
+
636
+ The optional partition table should be readable by Astropy and contain:
637
+ `mol_id`, `iso_id`, `temperature_k`, and `q`. If a molecule/isotopologue is
638
+ not present in the table, PyMolFit falls back to its approximate internal
639
+ partition scaling for that line.
640
+
641
+ Instead of the built-in atmosphere, a profile table can be supplied:
642
+
643
+ ```bash
644
+ pymolfit fit spectrum.txt corrected.txt \
645
+ --hitran-par hitran_lband.par \
646
+ --atmosphere-table profile.ecsv
647
+ ```
648
+
649
+ The atmosphere table should be Astropy-readable and contain pressure,
650
+ temperature, path length, and volume mixing ratio columns. Supported pressure
651
+ columns are `pressure_atm`, `pressure_hpa`, `pressure_mbar`, or `pressure_pa`.
652
+ Supported path columns are `path_length_m`, `thickness_m`, or `dz_m`. Mixing
653
+ ratio columns should be named like `mix_H2O`, `mix_CO2`, or `vmr_H2O`.
654
+
655
+ To cache a filtered HITRAN file as a reusable PyMolFit line-list table:
656
+
657
+ ```bash
658
+ pymolfit convert-hitran hitran_lband.par h2o_lband.ecsv \
659
+ --species H2O \
660
+ --wavenumber-min 4200 \
661
+ --wavenumber-max 4400 \
662
+ --min-strength 1e-28 \
663
+ --max-lines 50000
664
+ ```
665
+
666
+ PyMolFit can optionally acquire a newer or custom wavelength window from
667
+ HITRAN's authenticated
668
+ API v2 without installing HAPI. Create an API key in your HITRAN account, keep
669
+ it in the environment, and request all molecules needed by the fit:
670
+
671
+ ```bash
672
+ export HITRAN_API_KEY='your-private-key'
673
+ pymolfit fetch-hitran \
674
+ --species H2O --species CO2 --species CH4 \
675
+ --wavelength-min 2.29 --wavelength-max 2.36
676
+ ```
677
+
678
+ The command prints paths to a standard `.par` file, a directly reusable ECSV
679
+ line table, and a JSON manifest. They are cached under
680
+ `~/.cache/pymolfit/hitran` (or `PYMOLFIT_HITRAN_CACHE`) and include the exact
681
+ query, database edition reported by the service, line coverage, isotopologue
682
+ metadata, and SHA-256 hashes. The API key is never persisted. A verified cache
683
+ hit works offline. Existing licensed files can be imported through the same
684
+ validation/provenance path:
685
+
686
+ ```bash
687
+ pymolfit cache-hitran downloaded.par \
688
+ --species O2 --wavelength-min 0.75 --wavelength-max 0.78
689
+ ```
690
+
691
+ Use the printed ECSV path as `--line-list`. Direct HITRAN API data remain
692
+ subject to HITRAN's terms and citation policy. This optional route is separate
693
+ from the official AER 3.9 catalogue used by the default workflow.
694
+
695
+ For a release checkout, exercise the real authenticated path once with a
696
+ fixed, narrow O2 request and write a credential-free validation receipt:
697
+
698
+ ```bash
699
+ export HITRAN_API_KEY='your-private-key'
700
+ PYTHONPATH=src python local_tests/validate_authenticated_hitran.py
701
+ unset HITRAN_API_KEY
702
+ ```
703
+
704
+ This forces a network request rather than accepting a cache hit. The receipt
705
+ contains request, database-edition, source-code, and artifact hashes, but no
706
+ API key or downloaded line records. The science-readiness campaign accepts it
707
+ only while its client-source hash matches the current checkout.
708
+
709
+ With common expert controls:
710
+
711
+ ```bash
712
+ pymolfit fit spectrum_nm.txt corrected.txt \
713
+ --wavelength-unit nm \
714
+ --hitran-par hitran_lband.par \
715
+ --airmass 1.3 \
716
+ --fit-wavelength-polynomial \
717
+ --wavelength-polynomial-order 1 \
718
+ --wavelength-shift-bounds -0.0003 0.0003 \
719
+ --fit-lsf-sigma \
720
+ --lsf-molecfit-voigt \
721
+ --fit-range 2.318:2.322 \
722
+ --fit-range 2.330:2.334 \
723
+ --exclude-range 2.3205:2.3212 \
724
+ --loss soft_l1 \
725
+ --product fit_product.ecsv \
726
+ --plot fit_diagnostic.png
727
+ ```
728
+
729
+ The main output is the corrected spectrum. The optional product table contains
730
+ the observed flux, fitted model, continuum, transmission, corrected flux,
731
+ input/fit/corrected masks, propagated uncertainties, fitted parameters,
732
+ covariance rank, parameters at active bounds, and exact model/input provenance.
733
+
734
+ To compare a corrected PyMolFit spectrum with a reference product, such as a
735
+ Molecfit-corrected spectrum:
736
+
737
+ ```bash
738
+ pymolfit compare corrected.txt reference_corrected.txt --normalize
739
+ ```
740
+
741
+ ## Remaining Release Gates
742
+
743
+ 1. Complete the independent blind packet and a reviewer-selected held-out
744
+ spectrum with an experienced telluric-correction user. Send only
745
+ `local_tests/science_readiness/results/independent_review_packet.zip`; its
746
+ manifest is checked automatically and the private answer key is excluded.
747
+ 2. Resolve or scientifically explain the retained direct-transmission warnings
748
+ across X-shooter and CRIRES+ without observation-specific calibration.
749
+
750
+ The production and validation workflows both use the checksum-pinned official
751
+ AER 3.9 catalogue from Zenodo. Authenticated HITRAN validation remains an
752
+ optional check of the direct-API feature, not a requirement for normal
753
+ scientific use.