diskmelts 2.1__tar.gz → 2.1.2__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 (67) hide show
  1. {diskmelts-2.1/src/diskmelts.egg-info → diskmelts-2.1.2}/PKG-INFO +51 -61
  2. {diskmelts-2.1 → diskmelts-2.1.2}/README.md +50 -60
  3. {diskmelts-2.1 → diskmelts-2.1.2}/pyproject.toml +1 -1
  4. {diskmelts-2.1 → diskmelts-2.1.2}/src/diskmelts/fitting.py +27 -12
  5. diskmelts-2.1.2/src/diskmelts/models/Trained_model/C3H4_single_v2/net_C3H4_forward.pt +0 -0
  6. diskmelts-2.1.2/src/diskmelts/models/Trained_model/C6H6_single_v2/net_C6H6_forward.pt +0 -0
  7. {diskmelts-2.1 → diskmelts-2.1.2}/src/diskmelts/plotting.py +6 -1
  8. {diskmelts-2.1 → diskmelts-2.1.2}/src/diskmelts/v2_fitting.py +4 -3
  9. {diskmelts-2.1 → diskmelts-2.1.2}/src/diskmelts/v2_grid_fitting.py +18 -14
  10. {diskmelts-2.1 → diskmelts-2.1.2/src/diskmelts.egg-info}/PKG-INFO +51 -61
  11. {diskmelts-2.1 → diskmelts-2.1.2}/src/diskmelts.egg-info/SOURCES.txt +2 -0
  12. {diskmelts-2.1 → diskmelts-2.1.2}/tests/test_dev_v2_fitting_grids.py +63 -4
  13. {diskmelts-2.1 → diskmelts-2.1.2}/tests/test_io.py +25 -0
  14. {diskmelts-2.1 → diskmelts-2.1.2}/tests/test_shared_area_fitting.py +38 -0
  15. {diskmelts-2.1 → diskmelts-2.1.2}/tests/test_v2_package_assets.py +19 -0
  16. {diskmelts-2.1 → diskmelts-2.1.2}/tests/test_workflows.py +17 -4
  17. {diskmelts-2.1 → diskmelts-2.1.2}/LICENSE +0 -0
  18. {diskmelts-2.1 → diskmelts-2.1.2}/setup.cfg +0 -0
  19. {diskmelts-2.1 → diskmelts-2.1.2}/src/diskmelts/__init__.py +0 -0
  20. {diskmelts-2.1 → diskmelts-2.1.2}/src/diskmelts/consub/__init__.py +0 -0
  21. {diskmelts-2.1 → diskmelts-2.1.2}/src/diskmelts/consub/_api.py +0 -0
  22. {diskmelts-2.1 → diskmelts-2.1.2}/src/diskmelts/consub/_core.py +0 -0
  23. {diskmelts-2.1 → diskmelts-2.1.2}/src/diskmelts/consub/cont_ranges.csv +0 -0
  24. {diskmelts-2.1 → diskmelts-2.1.2}/src/diskmelts/consub/cont_ranges_PPan.csv +0 -0
  25. {diskmelts-2.1 → diskmelts-2.1.2}/src/diskmelts/consub/regions/carbon_extended.csv +0 -0
  26. {diskmelts-2.1 → diskmelts-2.1.2}/src/diskmelts/consub/regions/carbon_extended.json +0 -0
  27. {diskmelts-2.1 → diskmelts-2.1.2}/src/diskmelts/consub/regions/carbon_rich.csv +0 -0
  28. {diskmelts-2.1 → diskmelts-2.1.2}/src/diskmelts/consub/regions/carbon_rich.json +0 -0
  29. {diskmelts-2.1 → diskmelts-2.1.2}/src/diskmelts/consub/regions/ttauri.csv +0 -0
  30. {diskmelts-2.1 → diskmelts-2.1.2}/src/diskmelts/consub/regions/ttauri.json +0 -0
  31. {diskmelts-2.1 → diskmelts-2.1.2}/src/diskmelts/consub/regions/water.csv +0 -0
  32. {diskmelts-2.1 → diskmelts-2.1.2}/src/diskmelts/consub/regions/water.json +0 -0
  33. {diskmelts-2.1 → diskmelts-2.1.2}/src/diskmelts/models/Trained_model/13C12CH2_single_v2/net_13C12CH2_forward.pt +0 -0
  34. {diskmelts-2.1 → diskmelts-2.1.2}/src/diskmelts/models/Trained_model/13CO2_single_v2/net_13CO2_forward.pt +0 -0
  35. {diskmelts-2.1 → diskmelts-2.1.2}/src/diskmelts/models/Trained_model/C2H2_single_v2/net_C2H2_forward.pt +0 -0
  36. {diskmelts-2.1 → diskmelts-2.1.2}/src/diskmelts/models/Trained_model/C2H4_single_v2/net_C2H4_forward.pt +0 -0
  37. {diskmelts-2.1 → diskmelts-2.1.2}/src/diskmelts/models/Trained_model/C2H6_single_v2/net_C2H6_forward.pt +0 -0
  38. {diskmelts-2.1 → diskmelts-2.1.2}/src/diskmelts/models/Trained_model/C2HD_single_v2/net_C2HD_forward.pt +0 -0
  39. {diskmelts-2.1 → diskmelts-2.1.2}/src/diskmelts/models/Trained_model/C4H2_single_v2/net_C4H2_forward.pt +0 -0
  40. {diskmelts-2.1 → diskmelts-2.1.2}/src/diskmelts/models/Trained_model/CH4_single_v2/net_CH4_forward.pt +0 -0
  41. {diskmelts-2.1 → diskmelts-2.1.2}/src/diskmelts/models/Trained_model/CO17O_single_v2/net_CO17O_forward.pt +0 -0
  42. {diskmelts-2.1 → diskmelts-2.1.2}/src/diskmelts/models/Trained_model/CO18O_single_v2/net_CO18O_forward.pt +0 -0
  43. {diskmelts-2.1 → diskmelts-2.1.2}/src/diskmelts/models/Trained_model/CO2_single_v2/net_CO2_forward.pt +0 -0
  44. {diskmelts-2.1 → diskmelts-2.1.2}/src/diskmelts/models/Trained_model/H2O_wavelength_bank_v2_fixed/H2O_model_bank.json +0 -0
  45. {diskmelts-2.1 → diskmelts-2.1.2}/src/diskmelts/models/Trained_model/H2O_wavelength_bank_v2_fixed/net_H2O_w13p5to19p2_n13to19_t100to1400.pt +0 -0
  46. {diskmelts-2.1 → diskmelts-2.1.2}/src/diskmelts/models/Trained_model/H2O_wavelength_bank_v2_fixed/net_H2O_w19to25_n13to19_t100to1400.pt +0 -0
  47. {diskmelts-2.1 → diskmelts-2.1.2}/src/diskmelts/models/Trained_model/H2O_wavelength_bank_v2_fixed/net_H2O_w4p9to7p4_n13to19_t100to1400.pt +0 -0
  48. {diskmelts-2.1 → diskmelts-2.1.2}/src/diskmelts/models/Trained_model/H2O_wavelength_bank_v2_fixed/net_H2O_w7p2to11p2_n13to19_t100to1400.pt +0 -0
  49. {diskmelts-2.1 → diskmelts-2.1.2}/src/diskmelts/models/Trained_model/H2O_wavelength_bank_v2_fixed/net_H2O_w9p9to15p1_n13to19_t100to1400.pt +0 -0
  50. {diskmelts-2.1 → diskmelts-2.1.2}/src/diskmelts/models/Trained_model/HC3N_single_v2/net_HC3N_forward.pt +0 -0
  51. {diskmelts-2.1 → diskmelts-2.1.2}/src/diskmelts/models/Trained_model/HCN_single_v2/net_HCN_forward.pt +0 -0
  52. {diskmelts-2.1 → diskmelts-2.1.2}/src/diskmelts/trainmodel.py +0 -0
  53. {diskmelts-2.1 → diskmelts-2.1.2}/src/diskmelts/v2_paper_comparison.py +0 -0
  54. {diskmelts-2.1 → diskmelts-2.1.2}/src/diskmelts/v2_read_plot.py +0 -0
  55. {diskmelts-2.1 → diskmelts-2.1.2}/src/diskmelts/validation.py +0 -0
  56. {diskmelts-2.1 → diskmelts-2.1.2}/src/diskmelts.egg-info/dependency_links.txt +0 -0
  57. {diskmelts-2.1 → diskmelts-2.1.2}/src/diskmelts.egg-info/requires.txt +0 -0
  58. {diskmelts-2.1 → diskmelts-2.1.2}/src/diskmelts.egg-info/top_level.txt +0 -0
  59. {diskmelts-2.1 → diskmelts-2.1.2}/tests/test_consub_package.py +0 -0
  60. {diskmelts-2.1 → diskmelts-2.1.2}/tests/test_detection.py +0 -0
  61. {diskmelts-2.1 → diskmelts-2.1.2}/tests/test_dev_v2_fitting_distance.py +0 -0
  62. {diskmelts-2.1 → diskmelts-2.1.2}/tests/test_dev_v2_read_plot.py +0 -0
  63. {diskmelts-2.1 → diskmelts-2.1.2}/tests/test_imports.py +0 -0
  64. {diskmelts-2.1 → diskmelts-2.1.2}/tests/test_mlp.py +0 -0
  65. {diskmelts-2.1 → diskmelts-2.1.2}/tests/test_model_bank.py +0 -0
  66. {diskmelts-2.1 → diskmelts-2.1.2}/tests/test_smoke.py +0 -0
  67. {diskmelts-2.1 → diskmelts-2.1.2}/tests/test_water_training.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: diskmelts
3
- Version: 2.1
3
+ Version: 2.1.2
4
4
  Summary: Machine-learning Enabled Line fitting Tool for Spectra of protoplanetary disks
5
5
  Author: Chengyan Xie, Dingshan Deng
6
6
  License-Expression: MIT
@@ -49,58 +49,30 @@ density (`logN`), and emitting area scaling (`A`) from observed spectra.
49
49
 
50
50
  ## Installation
51
51
 
52
- Requires Python 3.9 or newer. Once the package is published to PyPI, install
53
- it from any directory without cloning this repository:
52
+ DiskMELTS is available on [PyPI](https://pypi.org/project/diskmelts/).
53
+ The C6H6 and C3H4 checkpoints are included starting with version 2.1.2.
54
+ With Python 3.9 or newer, install it from any directory. These commands use
55
+ `python` in an activated environment; the
56
+ [installation guide](https://diskmelts.readthedocs.io/en/latest/installation.html)
57
+ shows how to create one on macOS, Linux, Windows, or Conda:
54
58
 
55
59
  ```bash
56
60
  python -m pip install diskmelts
57
- # Add [io] for FITS input, or [io,notebooks] for JupyterLab.
61
+ python -c "import diskmelts; print('DiskMELTS is ready')"
58
62
  ```
59
63
 
60
- Until the first PyPI release, install directly from GitHub without a clone:
64
+ Optional features can be installed in the same way:
61
65
 
62
66
  ```bash
63
- python -m pip install "diskmelts[io] @ git+https://github.com/wddlx/DiskMELTS.git"
67
+ python -m pip install "diskmelts[io]" # FITS input
68
+ python -m pip install "diskmelts[io,notebooks]" # FITS input and JupyterLab
64
69
  ```
65
70
 
66
- The package includes the production checkpoints and continuum reference tables.
67
- Full slab grids for training and direct-grid fitting are separate downloads;
68
- pass their local path as `grid_root`. See the
69
- [installation guide](https://diskmelts.readthedocs.io/en/latest/installation.html)
70
- for details.
71
-
72
- ### Development checkout (macOS, Linux, Windows)
73
-
74
- From the repository root, use a native Python 3.10 or 3.11 interpreter to
75
- create a virtual environment. For FITS input and the example notebooks:
76
-
77
- ```bash
78
- python3.11 -m venv .venv
79
- .venv/bin/python -m pip install -e ".[io,notebooks]"
80
- .venv/bin/python -c "import diskmelts; print(diskmelts.__name__)"
81
- ```
82
-
83
- Use `python3.10` if that is the native Python available on your machine.
84
- For core fitting and CSV/array inputs only, replace the install command with
85
- `.venv/bin/python -m pip install -e .`. See the
86
- [installation guide](https://diskmelts.readthedocs.io/en/latest/installation.html) for Windows commands.
87
-
88
- ### Conda or Mamba
89
-
90
- The YAML files use Conda only for Python and pip; pip installs the package and
91
- its declared dependencies. Choose one from the repository root:
92
-
93
- | File | Contents |
94
- | --- | --- |
95
- | `environment.yaml` | Core package |
96
- | `environment-notebooks.yaml` | Core package, FITS support, JupyterLab |
97
- | `environment-dev.yaml` | Notebook environment, tests, documentation tools |
98
-
99
- For the notebook setup, run `mamba env create --file environment-notebooks.yaml`
100
- then `conda activate diskmelts-notebooks`. `conda env create --file` works with
101
- the same files when using a current Conda release. On Apple Silicon, use a
102
- native `osx-arm64` installation such as Miniforge; an older Intel Conda can
103
- still spend a long time solving even these smaller files.
71
+ The pip package includes the production checkpoints and continuum reference
72
+ tables. Full slab grids for training and direct-grid fitting are separate
73
+ downloads; pass their local path as `grid_root`.
74
+ Example notebooks and spectra can be downloaded from the repository when
75
+ needed; they are not installed by pip.
104
76
 
105
77
  ## Runtime dependencies
106
78
 
@@ -158,34 +130,44 @@ special regions, FITS input, saving CSVs, and uncertainty diagnostics.
158
130
 
159
131
  ## Quick fitting example
160
132
 
133
+ Use a continuum-subtracted CSV with a header and `wave,flux` as its first two
134
+ columns, with wavelength in µm and flux in Jy. For a runnable example without
135
+ cloning the repository, download the
136
+ [J16120505 spectrum](https://github.com/wddlx/DiskMELTS/blob/main/Realobs_data/Consub_data/j16120505_v9.0_contsub_RVcorr.csv)
137
+ as `j16120505_contsub.csv`, or run:
138
+
139
+ ```bash
140
+ curl -L -o j16120505_contsub.csv https://raw.githubusercontent.com/wddlx/DiskMELTS/main/Realobs_data/Consub_data/j16120505_v9.0_contsub_RVcorr.csv
141
+ ```
142
+
161
143
  ```python
162
- from diskmelts import load_fitting_models, load_observed_spectrum, fit_stage
144
+ from diskmelts import fit_stage, load_fitting_models, load_observed_spectrum
163
145
 
164
- pretrained = load_fitting_models(['H2O'])
146
+ models = load_fitting_models('C2H2')
165
147
 
166
148
  obs_wav, obs_flux = load_observed_spectrum(
167
- 'Realobs_data/Consub_data/j16120505_v9.0_contsub_RVcorr.csv'
149
+ 'j16120505_contsub.csv'
168
150
  )
169
151
 
170
152
  fit = fit_stage(
171
153
  obs_wav,
172
154
  obs_flux,
173
- molecule='H2O',
174
- wavelength_range=(11.0, 19.0),
175
- pretrained=pretrained,
176
- fit_ranges=[(11.0, 12.0), (16.5, 18.5)],
155
+ molecule='C2H2',
156
+ wavelength_range=(13.0, 15.0),
157
+ pretrained=models,
158
+ fit_ranges=(13.5, 14.1),
177
159
  distance_pc=122.5,
178
- n_samples=20000,
179
- n_refine=32,
180
- n_top=20,
181
- sigma=0.001,
160
+ n_samples=1024,
161
+ n_refine=4,
162
+ n_top=4,
182
163
  )
183
164
 
184
- print(fit['params'])
165
+ print(fit['params']['C2H2'])
185
166
  ```
186
167
 
187
168
  You can also read spectra with your own code. DiskMELTS only requires
188
- one-dimensional `obs_wav` and `obs_flux` arrays.
169
+ one-dimensional `obs_wav` and `obs_flux` arrays. Replace the sample CSV with
170
+ your own file to fit your source; update `distance_pc` and fit intervals.
189
171
 
190
172
  For a complete staged fit-and-subtract workflow from a checkout, edit and run:
191
173
 
@@ -215,9 +197,9 @@ plotting.
215
197
 
216
198
  ## Training and validation data
217
199
 
218
- `Model_grids/` and `Pretrain_grid/` are intentionally ignored by Git because
219
- they contain the full local training data. The generated checkpoint directories
220
- under the top-level `Trained_model/` are also optional; the package bundles
200
+ Generated data under `Model_grids/` and `Pretrain_grid/` are intentionally
201
+ ignored by Git because they contain the full local training data. Generated
202
+ checkpoint directories under the top-level `Trained_model/` are also optional; the package bundles
221
203
  only the production checkpoints needed for v2 fitting. To run
222
204
  `examples/dev_v2_pt_validation.py` or
223
205
  `notebooks/Example_Training_Validation.ipynb`, place the complete model grids
@@ -228,7 +210,8 @@ The maintained v2 workflow uses a hybrid design after restricting the expanded
228
210
  IRIS grids to the intended physical domain. H2O uses five overlapping
229
211
  wavelength checkpoints covering 4.9–25.0 µm, all sharing `T=100–1400 K` and
230
212
  `logN=13–19`. Primary carbon-bearing and rarer molecules use one checkpoint
231
- each, the same temperature range, and `logN=13–19.5`.
213
+ each and `logN=13–19.5`; C6H6 is limited to `T=100–675 K`, while the others
214
+ use `T=100–1400 K`.
232
215
  The new C2HD checkpoint spans 11.5–20.5 µm to include its 19 µm band;
233
216
  CO17O and CO18O span 13–18 µm. The updated 13C12CH2 checkpoint keeps
234
217
  11.5–17 µm. To retrain these four grids, run:
@@ -237,6 +220,13 @@ CO17O and CO18O span 13–18 µm. The updated 13C12CH2 checkpoint keeps
237
220
  DISKMELTS_MOLECULES=13C12CH2,C2HD,CO17O,CO18O python examples/dev_v2_pt_validation.py
238
221
  ```
239
222
 
223
+ C6H6 and C3H4 also have bundled fitting checkpoints. The C6H6 checkpoint
224
+ covers 13–17 µm and 100–675 K; C3H4 covers 15–18 µm and 100–1400 K. Load
225
+ either by name with `load_fitting_models`. Direct grid fitting
226
+ uses the same names with local `Model_grids/C6H6/` and `Model_grids/C3H4/`
227
+ files. See the [training guide](https://diskmelts.readthedocs.io/en/latest/training.html)
228
+ for peak audits and retraining commands.
229
+
240
230
  Five to ten percent of complete `(T, logN)` grid spectra are excluded before
241
231
  PCA or neural-network training. They are used to select the PCA dimension and
242
232
  to validate spectral reconstruction and full parameter fitting. The
@@ -10,58 +10,30 @@ density (`logN`), and emitting area scaling (`A`) from observed spectra.
10
10
 
11
11
  ## Installation
12
12
 
13
- Requires Python 3.9 or newer. Once the package is published to PyPI, install
14
- it from any directory without cloning this repository:
13
+ DiskMELTS is available on [PyPI](https://pypi.org/project/diskmelts/).
14
+ The C6H6 and C3H4 checkpoints are included starting with version 2.1.2.
15
+ With Python 3.9 or newer, install it from any directory. These commands use
16
+ `python` in an activated environment; the
17
+ [installation guide](https://diskmelts.readthedocs.io/en/latest/installation.html)
18
+ shows how to create one on macOS, Linux, Windows, or Conda:
15
19
 
16
20
  ```bash
17
21
  python -m pip install diskmelts
18
- # Add [io] for FITS input, or [io,notebooks] for JupyterLab.
22
+ python -c "import diskmelts; print('DiskMELTS is ready')"
19
23
  ```
20
24
 
21
- Until the first PyPI release, install directly from GitHub without a clone:
25
+ Optional features can be installed in the same way:
22
26
 
23
27
  ```bash
24
- python -m pip install "diskmelts[io] @ git+https://github.com/wddlx/DiskMELTS.git"
28
+ python -m pip install "diskmelts[io]" # FITS input
29
+ python -m pip install "diskmelts[io,notebooks]" # FITS input and JupyterLab
25
30
  ```
26
31
 
27
- The package includes the production checkpoints and continuum reference tables.
28
- Full slab grids for training and direct-grid fitting are separate downloads;
29
- pass their local path as `grid_root`. See the
30
- [installation guide](https://diskmelts.readthedocs.io/en/latest/installation.html)
31
- for details.
32
-
33
- ### Development checkout (macOS, Linux, Windows)
34
-
35
- From the repository root, use a native Python 3.10 or 3.11 interpreter to
36
- create a virtual environment. For FITS input and the example notebooks:
37
-
38
- ```bash
39
- python3.11 -m venv .venv
40
- .venv/bin/python -m pip install -e ".[io,notebooks]"
41
- .venv/bin/python -c "import diskmelts; print(diskmelts.__name__)"
42
- ```
43
-
44
- Use `python3.10` if that is the native Python available on your machine.
45
- For core fitting and CSV/array inputs only, replace the install command with
46
- `.venv/bin/python -m pip install -e .`. See the
47
- [installation guide](https://diskmelts.readthedocs.io/en/latest/installation.html) for Windows commands.
48
-
49
- ### Conda or Mamba
50
-
51
- The YAML files use Conda only for Python and pip; pip installs the package and
52
- its declared dependencies. Choose one from the repository root:
53
-
54
- | File | Contents |
55
- | --- | --- |
56
- | `environment.yaml` | Core package |
57
- | `environment-notebooks.yaml` | Core package, FITS support, JupyterLab |
58
- | `environment-dev.yaml` | Notebook environment, tests, documentation tools |
59
-
60
- For the notebook setup, run `mamba env create --file environment-notebooks.yaml`
61
- then `conda activate diskmelts-notebooks`. `conda env create --file` works with
62
- the same files when using a current Conda release. On Apple Silicon, use a
63
- native `osx-arm64` installation such as Miniforge; an older Intel Conda can
64
- still spend a long time solving even these smaller files.
32
+ The pip package includes the production checkpoints and continuum reference
33
+ tables. Full slab grids for training and direct-grid fitting are separate
34
+ downloads; pass their local path as `grid_root`.
35
+ Example notebooks and spectra can be downloaded from the repository when
36
+ needed; they are not installed by pip.
65
37
 
66
38
  ## Runtime dependencies
67
39
 
@@ -119,34 +91,44 @@ special regions, FITS input, saving CSVs, and uncertainty diagnostics.
119
91
 
120
92
  ## Quick fitting example
121
93
 
94
+ Use a continuum-subtracted CSV with a header and `wave,flux` as its first two
95
+ columns, with wavelength in µm and flux in Jy. For a runnable example without
96
+ cloning the repository, download the
97
+ [J16120505 spectrum](https://github.com/wddlx/DiskMELTS/blob/main/Realobs_data/Consub_data/j16120505_v9.0_contsub_RVcorr.csv)
98
+ as `j16120505_contsub.csv`, or run:
99
+
100
+ ```bash
101
+ curl -L -o j16120505_contsub.csv https://raw.githubusercontent.com/wddlx/DiskMELTS/main/Realobs_data/Consub_data/j16120505_v9.0_contsub_RVcorr.csv
102
+ ```
103
+
122
104
  ```python
123
- from diskmelts import load_fitting_models, load_observed_spectrum, fit_stage
105
+ from diskmelts import fit_stage, load_fitting_models, load_observed_spectrum
124
106
 
125
- pretrained = load_fitting_models(['H2O'])
107
+ models = load_fitting_models('C2H2')
126
108
 
127
109
  obs_wav, obs_flux = load_observed_spectrum(
128
- 'Realobs_data/Consub_data/j16120505_v9.0_contsub_RVcorr.csv'
110
+ 'j16120505_contsub.csv'
129
111
  )
130
112
 
131
113
  fit = fit_stage(
132
114
  obs_wav,
133
115
  obs_flux,
134
- molecule='H2O',
135
- wavelength_range=(11.0, 19.0),
136
- pretrained=pretrained,
137
- fit_ranges=[(11.0, 12.0), (16.5, 18.5)],
116
+ molecule='C2H2',
117
+ wavelength_range=(13.0, 15.0),
118
+ pretrained=models,
119
+ fit_ranges=(13.5, 14.1),
138
120
  distance_pc=122.5,
139
- n_samples=20000,
140
- n_refine=32,
141
- n_top=20,
142
- sigma=0.001,
121
+ n_samples=1024,
122
+ n_refine=4,
123
+ n_top=4,
143
124
  )
144
125
 
145
- print(fit['params'])
126
+ print(fit['params']['C2H2'])
146
127
  ```
147
128
 
148
129
  You can also read spectra with your own code. DiskMELTS only requires
149
- one-dimensional `obs_wav` and `obs_flux` arrays.
130
+ one-dimensional `obs_wav` and `obs_flux` arrays. Replace the sample CSV with
131
+ your own file to fit your source; update `distance_pc` and fit intervals.
150
132
 
151
133
  For a complete staged fit-and-subtract workflow from a checkout, edit and run:
152
134
 
@@ -176,9 +158,9 @@ plotting.
176
158
 
177
159
  ## Training and validation data
178
160
 
179
- `Model_grids/` and `Pretrain_grid/` are intentionally ignored by Git because
180
- they contain the full local training data. The generated checkpoint directories
181
- under the top-level `Trained_model/` are also optional; the package bundles
161
+ Generated data under `Model_grids/` and `Pretrain_grid/` are intentionally
162
+ ignored by Git because they contain the full local training data. Generated
163
+ checkpoint directories under the top-level `Trained_model/` are also optional; the package bundles
182
164
  only the production checkpoints needed for v2 fitting. To run
183
165
  `examples/dev_v2_pt_validation.py` or
184
166
  `notebooks/Example_Training_Validation.ipynb`, place the complete model grids
@@ -189,7 +171,8 @@ The maintained v2 workflow uses a hybrid design after restricting the expanded
189
171
  IRIS grids to the intended physical domain. H2O uses five overlapping
190
172
  wavelength checkpoints covering 4.9–25.0 µm, all sharing `T=100–1400 K` and
191
173
  `logN=13–19`. Primary carbon-bearing and rarer molecules use one checkpoint
192
- each, the same temperature range, and `logN=13–19.5`.
174
+ each and `logN=13–19.5`; C6H6 is limited to `T=100–675 K`, while the others
175
+ use `T=100–1400 K`.
193
176
  The new C2HD checkpoint spans 11.5–20.5 µm to include its 19 µm band;
194
177
  CO17O and CO18O span 13–18 µm. The updated 13C12CH2 checkpoint keeps
195
178
  11.5–17 µm. To retrain these four grids, run:
@@ -198,6 +181,13 @@ CO17O and CO18O span 13–18 µm. The updated 13C12CH2 checkpoint keeps
198
181
  DISKMELTS_MOLECULES=13C12CH2,C2HD,CO17O,CO18O python examples/dev_v2_pt_validation.py
199
182
  ```
200
183
 
184
+ C6H6 and C3H4 also have bundled fitting checkpoints. The C6H6 checkpoint
185
+ covers 13–17 µm and 100–675 K; C3H4 covers 15–18 µm and 100–1400 K. Load
186
+ either by name with `load_fitting_models`. Direct grid fitting
187
+ uses the same names with local `Model_grids/C6H6/` and `Model_grids/C3H4/`
188
+ files. See the [training guide](https://diskmelts.readthedocs.io/en/latest/training.html)
189
+ for peak audits and retraining commands.
190
+
201
191
  Five to ten percent of complete `(T, logN)` grid spectra are excluded before
202
192
  PCA or neural-network training. They are used to select the PCA dimension and
203
193
  to validate spectral reconstruction and full parameter fitting. The
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "diskmelts"
7
- version = "2.1"
7
+ version = "2.1.2"
8
8
  description = "Machine-learning Enabled Line fitting Tool for Spectra of protoplanetary disks"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -519,6 +519,9 @@ def _build_components(molecules, h2o_components=1, component_names=None):
519
519
  raise ValueError('component_names must match the number of fitted components')
520
520
  for comp, label in zip(components, component_names):
521
521
  comp['label'] = label
522
+ labels = [comp['label'] for comp in components]
523
+ if len(labels) != len(set(labels)):
524
+ raise ValueError('component labels must be unique; name repeated molecules with component_names')
522
525
  return components
523
526
 
524
527
 
@@ -697,8 +700,9 @@ def fit_molecules(
697
700
  molecule names or component labels.
698
701
  logN_bounds (tuple or dict or None): default (14, 19). A dict can use
699
702
  molecule names or component labels.
700
- loga_bounds (tuple or None): bounds on log10(A). None means A >= 0
701
- with no upper bound; otherwise bounded least squares is used.
703
+ loga_bounds (tuple, dict, or None): bounds on log10(A), optionally
704
+ keyed by component label or molecule. None means A >= 0 with no
705
+ upper bound. Shared-area groups use the intersection of bounds.
702
706
  shared_area_groups (list of sequences or None): component labels whose
703
707
  emitting areas must be equal, e.g. [('C2H2', '13C12CH2')]. Each
704
708
  component may appear in at most one group. Unlisted components
@@ -914,10 +918,22 @@ def fit_molecules(
914
918
  )
915
919
  return physical
916
920
 
917
- if loga_bounds is None:
918
- A_bounds = (0.0, np.inf)
919
- else:
920
- A_bounds = (10.0 ** float(loga_bounds[0]), 10.0 ** float(loga_bounds[1]))
921
+ component_area_bounds = []
922
+ for comp in components:
923
+ pair = _bounds_for(comp, loga_bounds, None)
924
+ if pair is None:
925
+ component_area_bounds.append((0.0, np.inf))
926
+ continue
927
+ if (len(pair) != 2 or not np.all(np.isfinite(pair)) or
928
+ pair[0] >= pair[1] or not 0 < 10.0 ** pair[0] < 10.0 ** pair[1]):
929
+ raise ValueError(f'{comp["label"]} needs finite increasing loga_bounds')
930
+ component_area_bounds.append((10.0 ** pair[0], 10.0 ** pair[1]))
931
+ area_lower = np.array([max(component_area_bounds[i][0] for i in members)
932
+ for members in area_group_members])
933
+ area_upper = np.array([min(component_area_bounds[i][1] for i in members)
934
+ for members in area_group_members])
935
+ if np.any(area_lower >= area_upper):
936
+ raise ValueError('shared area group has no overlap in loga_bounds')
921
937
 
922
938
  def _matrix_for(params):
923
939
  cols = []
@@ -939,12 +955,11 @@ def fit_molecules(
939
955
  flux_scale = max(float(np.max(np.abs(obs_fit))),
940
956
  float(np.max(np.abs(M_group))), np.finfo(float).tiny)
941
957
  M_scaled, obs_scaled = M_group / flux_scale, obs_fit / flux_scale
942
- if np.isfinite(A_bounds[1]):
958
+ if np.any(area_lower > 0) or np.any(np.isfinite(area_upper)):
943
959
  res = scipy.optimize.lsq_linear(
944
960
  M_scaled,
945
961
  obs_scaled,
946
- bounds=(np.full(len(area_group_members), A_bounds[0]),
947
- np.full(len(area_group_members), A_bounds[1])),
962
+ bounds=(area_lower, area_upper),
948
963
  )
949
964
  A_group = np.asarray(res.x, dtype=np.float64)
950
965
  else:
@@ -1332,14 +1347,14 @@ def save_fitted_comparison(csv_path, source_name, all_fits):
1332
1347
 
1333
1348
  def _v(comp, key):
1334
1349
  if comp not in all_params:
1335
- return ''
1350
+ return np.nan
1336
1351
  return round(all_params[comp][key], 4)
1337
1352
 
1338
1353
  def _e(comp, key, which):
1339
1354
  if comp not in all_unc:
1340
- return ''
1355
+ return np.nan
1341
1356
  v = all_unc[comp].get(key, {}).get(which, float('nan'))
1342
- return round(v, 4) if np.isfinite(v) else ''
1357
+ return round(v, 4) if np.isfinite(v) else np.nan
1343
1358
 
1344
1359
  row = {
1345
1360
  'Source': source_name,
@@ -43,6 +43,8 @@ _MOL_LABEL = {
43
43
  'C2HD': r'C$_2$HD',
44
44
  'CO17O': r'C$^{16}$O$^{17}$O',
45
45
  'CO18O': r'C$^{16}$O$^{18}$O',
46
+ 'C6H6': r'C$_6$H$_6$',
47
+ 'C3H4': r'C$_3$H$_4$',
46
48
  }
47
49
 
48
50
  _MOL_COLOR = {
@@ -64,6 +66,8 @@ _MOL_COLOR = {
64
66
  'C2HD': 'teal',
65
67
  'CO17O': 'slateblue',
66
68
  'CO18O': 'goldenrod',
69
+ 'C6H6': 'darkorange',
70
+ 'C3H4': 'purple',
67
71
  }
68
72
 
69
73
  # ---------------------------------------------------------------------------
@@ -270,7 +274,8 @@ def plot_fit(
270
274
  ax_bot.text(0.99, 0.92, rf'$\chi^2_{{\rm red}} = {chi2_red:.2f}$',
271
275
  transform=ax_bot.transAxes, ha='right', va='top', fontsize=9)
272
276
 
273
- plt.tight_layout()
277
+ fig.subplots_adjust(left=0.11, right=0.98, bottom=0.10, top=0.92,
278
+ hspace=0.08)
274
279
  if show:
275
280
  plt.show()
276
281
  if save_path is not None:
@@ -34,7 +34,8 @@ CARBON_RANGES = {
34
34
  'HC3N': (13.0, 18.0), 'C2H6': (9.0, 15.0),
35
35
  'C2H4': (8.0, 14.0), 'CH4': (5.0, 9.0),
36
36
  'C2HD': (11.5, 20.5), 'CO17O': (13.0, 18.0),
37
- 'CO18O': (13.0, 18.0),
37
+ 'CO18O': (13.0, 18.0), 'C6H6': (13.0, 17.0),
38
+ 'C3H4': (15.0, 18.0),
38
39
  }
39
40
  MODEL_SPECS = {
40
41
  'H2O': {
@@ -49,9 +50,9 @@ MODEL_SPECS = {
49
50
  mol: {
50
51
  'path': f'Trained_model/{mol}_single_v2/net_{mol}_forward.pt',
51
52
  'wavelength_ranges': (wav_range,),
52
- 'T_bounds': (100.0, 1400.0),
53
+ 'T_bounds': (100.0, 675.0) if mol == 'C6H6' else (100.0, 1400.0),
53
54
  'logN_bounds': (13.0, 19.5),
54
- 'n_pca': 30,
55
+ 'n_pca': 75 if mol == 'C3H4' else 30,
55
56
  'hidden': (64, 128, 64),
56
57
  }
57
58
  for mol, wav_range in CARBON_RANGES.items()
@@ -58,8 +58,8 @@ def _stage_ranges(fit_ranges, components):
58
58
 
59
59
  def _components(molecule, h2o_components, component_names):
60
60
  molecules = [molecule] if isinstance(molecule, str) else list(molecule)
61
- if not molecules or len(molecules) != len(set(molecules)):
62
- raise ValueError('molecule must contain distinct molecular names')
61
+ if not molecules:
62
+ raise ValueError('molecule must contain at least one molecular name')
63
63
  if h2o_components not in (1, 2):
64
64
  raise ValueError('h2o_components must be 1 or 2')
65
65
  parts = []
@@ -281,10 +281,6 @@ def fit_grid_stage(obs_wav, obs_flux, molecule, wavelength_range, fit_ranges, *,
281
281
  noise_scale = float(sigma) * to_reference if sigma is not None else max(
282
282
  float(np.std(target)), np.finfo(float).tiny,
283
283
  )
284
- lo_A, hi_A = 10.0 ** float(loga_bounds[0]), 10.0 ** float(loga_bounds[1])
285
- if not 0 < lo_A < hi_A:
286
- raise ValueError('loga_bounds must be increasing and finite')
287
-
288
284
  if n_refine < 1:
289
285
  raise ValueError('n_refine must be positive')
290
286
 
@@ -301,6 +297,14 @@ def fit_grid_stage(obs_wav, obs_flux, molecule, wavelength_range, fit_ranges, *,
301
297
  bounds = [(bound_for(T_bounds, part, (100.0, 1400.0)),
302
298
  bound_for(logN_bounds, part, default_logN(part)))
303
299
  for part in parts]
300
+ area_bounds = [bound_for(loga_bounds, part, (-2.0, 2.0))
301
+ for part in parts]
302
+ if any(len(pair) != 2 or not np.all(np.isfinite(pair)) or
303
+ pair[0] >= pair[1] or not 0 < 10.0 ** pair[0] < 10.0 ** pair[1]
304
+ for pair in area_bounds):
305
+ raise ValueError('loga_bounds must be increasing and finite')
306
+ area_lower = np.array([10.0 ** pair[0] for pair in area_bounds])
307
+ area_upper = np.array([10.0 ** pair[1] for pair in area_bounds])
304
308
  molecules = list(dict.fromkeys(part['model_mol'] for part in parts))
305
309
  libraries = {}
306
310
  for mol in molecules:
@@ -330,7 +334,7 @@ def fit_grid_stage(obs_wav, obs_flux, molecule, wavelength_range, fit_ranges, *,
330
334
  def profile_amplitudes(nonlin):
331
335
  if (ordered_water and
332
336
  nonlin[2 * water_indices[0]] > nonlin[2 * water_indices[1]]):
333
- return np.inf, np.full(n_parts, lo_A)
337
+ return np.inf, area_lower.copy()
334
338
  matrix = templates(nonlin)
335
339
  scale = max(float(np.max(np.abs(target))),
336
340
  float(np.max(np.abs(matrix))), np.finfo(float).tiny)
@@ -338,12 +342,12 @@ def fit_grid_stage(obs_wav, obs_flux, molecule, wavelength_range, fit_ranges, *,
338
342
  column = matrix[:, 0] / scale
339
343
  denom = float(np.dot(column, column))
340
344
  amplitude = (float(np.clip(np.dot(column, target / scale) / denom,
341
- lo_A, hi_A)) if denom > 0 else lo_A)
345
+ area_lower[0], area_upper[0]))
346
+ if denom > 0 else area_lower[0])
342
347
  amplitudes = np.array([amplitude])
343
348
  else:
344
349
  solution = lsq_linear(matrix / scale, target / scale,
345
- bounds=(np.full(n_parts, lo_A),
346
- np.full(n_parts, hi_A)))
350
+ bounds=(area_lower, area_upper))
347
351
  amplitudes = solution.x
348
352
  residual = matrix @ amplitudes - target
349
353
  return float(np.dot(residual, residual)), amplitudes
@@ -358,8 +362,8 @@ def fit_grid_stage(obs_wav, obs_flux, molecule, wavelength_range, fit_ranges, *,
358
362
  key=lambda x: profile_amplitudes(x)[0])
359
363
  if not candidates:
360
364
  raise RuntimeError('grid search found no feasible component ordering')
361
- solution_bounds = [entry for pair in bounds
362
- for entry in (pair[0], pair[1], loga_bounds)]
365
+ solution_bounds = [entry for k, pair in enumerate(bounds)
366
+ for entry in (pair[0], pair[1], area_bounds[k])]
363
367
  lower = np.array([item[0] for item in solution_bounds], dtype=float)
364
368
  upper = np.array([item[1] for item in solution_bounds], dtype=float)
365
369
  scales = np.array([
@@ -445,8 +449,8 @@ def fit_grid_stage(obs_wav, obs_flux, molecule, wavelength_range, fit_ranges, *,
445
449
  np.isclose(T, bounds[k][0][1], rtol=0, atol=1e-5)),
446
450
  'logN_at_bound': bool(np.isclose(logN, bounds[k][1][0], rtol=0, atol=1e-6) or
447
451
  np.isclose(logN, bounds[k][1][1], rtol=0, atol=1e-6)),
448
- 'log10A_at_bound': bool(np.isclose(logA, loga_bounds[0], rtol=0, atol=1e-6) or
449
- np.isclose(logA, loga_bounds[1], rtol=0, atol=1e-6)),
452
+ 'log10A_at_bound': bool(np.isclose(logA, area_bounds[k][0], rtol=0, atol=1e-6) or
453
+ np.isclose(logA, area_bounds[k][1], rtol=0, atol=1e-6)),
450
454
  }
451
455
  print(f' Grid fit: {len(parts)} components, weighted SSE={best_loss:.4g}')
452
456
  return {
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: diskmelts
3
- Version: 2.1
3
+ Version: 2.1.2
4
4
  Summary: Machine-learning Enabled Line fitting Tool for Spectra of protoplanetary disks
5
5
  Author: Chengyan Xie, Dingshan Deng
6
6
  License-Expression: MIT
@@ -49,58 +49,30 @@ density (`logN`), and emitting area scaling (`A`) from observed spectra.
49
49
 
50
50
  ## Installation
51
51
 
52
- Requires Python 3.9 or newer. Once the package is published to PyPI, install
53
- it from any directory without cloning this repository:
52
+ DiskMELTS is available on [PyPI](https://pypi.org/project/diskmelts/).
53
+ The C6H6 and C3H4 checkpoints are included starting with version 2.1.2.
54
+ With Python 3.9 or newer, install it from any directory. These commands use
55
+ `python` in an activated environment; the
56
+ [installation guide](https://diskmelts.readthedocs.io/en/latest/installation.html)
57
+ shows how to create one on macOS, Linux, Windows, or Conda:
54
58
 
55
59
  ```bash
56
60
  python -m pip install diskmelts
57
- # Add [io] for FITS input, or [io,notebooks] for JupyterLab.
61
+ python -c "import diskmelts; print('DiskMELTS is ready')"
58
62
  ```
59
63
 
60
- Until the first PyPI release, install directly from GitHub without a clone:
64
+ Optional features can be installed in the same way:
61
65
 
62
66
  ```bash
63
- python -m pip install "diskmelts[io] @ git+https://github.com/wddlx/DiskMELTS.git"
67
+ python -m pip install "diskmelts[io]" # FITS input
68
+ python -m pip install "diskmelts[io,notebooks]" # FITS input and JupyterLab
64
69
  ```
65
70
 
66
- The package includes the production checkpoints and continuum reference tables.
67
- Full slab grids for training and direct-grid fitting are separate downloads;
68
- pass their local path as `grid_root`. See the
69
- [installation guide](https://diskmelts.readthedocs.io/en/latest/installation.html)
70
- for details.
71
-
72
- ### Development checkout (macOS, Linux, Windows)
73
-
74
- From the repository root, use a native Python 3.10 or 3.11 interpreter to
75
- create a virtual environment. For FITS input and the example notebooks:
76
-
77
- ```bash
78
- python3.11 -m venv .venv
79
- .venv/bin/python -m pip install -e ".[io,notebooks]"
80
- .venv/bin/python -c "import diskmelts; print(diskmelts.__name__)"
81
- ```
82
-
83
- Use `python3.10` if that is the native Python available on your machine.
84
- For core fitting and CSV/array inputs only, replace the install command with
85
- `.venv/bin/python -m pip install -e .`. See the
86
- [installation guide](https://diskmelts.readthedocs.io/en/latest/installation.html) for Windows commands.
87
-
88
- ### Conda or Mamba
89
-
90
- The YAML files use Conda only for Python and pip; pip installs the package and
91
- its declared dependencies. Choose one from the repository root:
92
-
93
- | File | Contents |
94
- | --- | --- |
95
- | `environment.yaml` | Core package |
96
- | `environment-notebooks.yaml` | Core package, FITS support, JupyterLab |
97
- | `environment-dev.yaml` | Notebook environment, tests, documentation tools |
98
-
99
- For the notebook setup, run `mamba env create --file environment-notebooks.yaml`
100
- then `conda activate diskmelts-notebooks`. `conda env create --file` works with
101
- the same files when using a current Conda release. On Apple Silicon, use a
102
- native `osx-arm64` installation such as Miniforge; an older Intel Conda can
103
- still spend a long time solving even these smaller files.
71
+ The pip package includes the production checkpoints and continuum reference
72
+ tables. Full slab grids for training and direct-grid fitting are separate
73
+ downloads; pass their local path as `grid_root`.
74
+ Example notebooks and spectra can be downloaded from the repository when
75
+ needed; they are not installed by pip.
104
76
 
105
77
  ## Runtime dependencies
106
78
 
@@ -158,34 +130,44 @@ special regions, FITS input, saving CSVs, and uncertainty diagnostics.
158
130
 
159
131
  ## Quick fitting example
160
132
 
133
+ Use a continuum-subtracted CSV with a header and `wave,flux` as its first two
134
+ columns, with wavelength in µm and flux in Jy. For a runnable example without
135
+ cloning the repository, download the
136
+ [J16120505 spectrum](https://github.com/wddlx/DiskMELTS/blob/main/Realobs_data/Consub_data/j16120505_v9.0_contsub_RVcorr.csv)
137
+ as `j16120505_contsub.csv`, or run:
138
+
139
+ ```bash
140
+ curl -L -o j16120505_contsub.csv https://raw.githubusercontent.com/wddlx/DiskMELTS/main/Realobs_data/Consub_data/j16120505_v9.0_contsub_RVcorr.csv
141
+ ```
142
+
161
143
  ```python
162
- from diskmelts import load_fitting_models, load_observed_spectrum, fit_stage
144
+ from diskmelts import fit_stage, load_fitting_models, load_observed_spectrum
163
145
 
164
- pretrained = load_fitting_models(['H2O'])
146
+ models = load_fitting_models('C2H2')
165
147
 
166
148
  obs_wav, obs_flux = load_observed_spectrum(
167
- 'Realobs_data/Consub_data/j16120505_v9.0_contsub_RVcorr.csv'
149
+ 'j16120505_contsub.csv'
168
150
  )
169
151
 
170
152
  fit = fit_stage(
171
153
  obs_wav,
172
154
  obs_flux,
173
- molecule='H2O',
174
- wavelength_range=(11.0, 19.0),
175
- pretrained=pretrained,
176
- fit_ranges=[(11.0, 12.0), (16.5, 18.5)],
155
+ molecule='C2H2',
156
+ wavelength_range=(13.0, 15.0),
157
+ pretrained=models,
158
+ fit_ranges=(13.5, 14.1),
177
159
  distance_pc=122.5,
178
- n_samples=20000,
179
- n_refine=32,
180
- n_top=20,
181
- sigma=0.001,
160
+ n_samples=1024,
161
+ n_refine=4,
162
+ n_top=4,
182
163
  )
183
164
 
184
- print(fit['params'])
165
+ print(fit['params']['C2H2'])
185
166
  ```
186
167
 
187
168
  You can also read spectra with your own code. DiskMELTS only requires
188
- one-dimensional `obs_wav` and `obs_flux` arrays.
169
+ one-dimensional `obs_wav` and `obs_flux` arrays. Replace the sample CSV with
170
+ your own file to fit your source; update `distance_pc` and fit intervals.
189
171
 
190
172
  For a complete staged fit-and-subtract workflow from a checkout, edit and run:
191
173
 
@@ -215,9 +197,9 @@ plotting.
215
197
 
216
198
  ## Training and validation data
217
199
 
218
- `Model_grids/` and `Pretrain_grid/` are intentionally ignored by Git because
219
- they contain the full local training data. The generated checkpoint directories
220
- under the top-level `Trained_model/` are also optional; the package bundles
200
+ Generated data under `Model_grids/` and `Pretrain_grid/` are intentionally
201
+ ignored by Git because they contain the full local training data. Generated
202
+ checkpoint directories under the top-level `Trained_model/` are also optional; the package bundles
221
203
  only the production checkpoints needed for v2 fitting. To run
222
204
  `examples/dev_v2_pt_validation.py` or
223
205
  `notebooks/Example_Training_Validation.ipynb`, place the complete model grids
@@ -228,7 +210,8 @@ The maintained v2 workflow uses a hybrid design after restricting the expanded
228
210
  IRIS grids to the intended physical domain. H2O uses five overlapping
229
211
  wavelength checkpoints covering 4.9–25.0 µm, all sharing `T=100–1400 K` and
230
212
  `logN=13–19`. Primary carbon-bearing and rarer molecules use one checkpoint
231
- each, the same temperature range, and `logN=13–19.5`.
213
+ each and `logN=13–19.5`; C6H6 is limited to `T=100–675 K`, while the others
214
+ use `T=100–1400 K`.
232
215
  The new C2HD checkpoint spans 11.5–20.5 µm to include its 19 µm band;
233
216
  CO17O and CO18O span 13–18 µm. The updated 13C12CH2 checkpoint keeps
234
217
  11.5–17 µm. To retrain these four grids, run:
@@ -237,6 +220,13 @@ CO17O and CO18O span 13–18 µm. The updated 13C12CH2 checkpoint keeps
237
220
  DISKMELTS_MOLECULES=13C12CH2,C2HD,CO17O,CO18O python examples/dev_v2_pt_validation.py
238
221
  ```
239
222
 
223
+ C6H6 and C3H4 also have bundled fitting checkpoints. The C6H6 checkpoint
224
+ covers 13–17 µm and 100–675 K; C3H4 covers 15–18 µm and 100–1400 K. Load
225
+ either by name with `load_fitting_models`. Direct grid fitting
226
+ uses the same names with local `Model_grids/C6H6/` and `Model_grids/C3H4/`
227
+ files. See the [training guide](https://diskmelts.readthedocs.io/en/latest/training.html)
228
+ for peak audits and retraining commands.
229
+
240
230
  Five to ten percent of complete `(T, logN)` grid spectra are excluded before
241
231
  PCA or neural-network training. They are used to select the PCA dimension and
242
232
  to validate spectral reconstruction and full parameter fitting. The
@@ -34,7 +34,9 @@ src/diskmelts/models/Trained_model/C2H2_single_v2/net_C2H2_forward.pt
34
34
  src/diskmelts/models/Trained_model/C2H4_single_v2/net_C2H4_forward.pt
35
35
  src/diskmelts/models/Trained_model/C2H6_single_v2/net_C2H6_forward.pt
36
36
  src/diskmelts/models/Trained_model/C2HD_single_v2/net_C2HD_forward.pt
37
+ src/diskmelts/models/Trained_model/C3H4_single_v2/net_C3H4_forward.pt
37
38
  src/diskmelts/models/Trained_model/C4H2_single_v2/net_C4H2_forward.pt
39
+ src/diskmelts/models/Trained_model/C6H6_single_v2/net_C6H6_forward.pt
38
40
  src/diskmelts/models/Trained_model/CH4_single_v2/net_CH4_forward.pt
39
41
  src/diskmelts/models/Trained_model/CO17O_single_v2/net_CO17O_forward.pt
40
42
  src/diskmelts/models/Trained_model/CO18O_single_v2/net_CO18O_forward.pt
@@ -14,12 +14,13 @@ from diskmelts.v2_grid_fitting import (
14
14
  )
15
15
 
16
16
 
17
- def _small_grid(tmp_path):
18
- grid_dir = tmp_path / 'grids' / 'C2H2'
17
+ def _small_grid(tmp_path, molecule='C2H2', wavelength_range=(11.0, 13.0)):
18
+ grid_dir = tmp_path / 'grids' / molecule
19
19
  grid_dir.mkdir(parents=True)
20
- wav = np.linspace(11.0, 13.0, 60)
20
+ lo, hi = wavelength_range
21
+ wav = np.linspace(lo, hi, 60)
21
22
  basis = np.stack([np.exp(-((wav - center) / 0.09) ** 2)
22
- for center in (11.3, 12.0, 12.7)])
23
+ for center in (lo + 0.3, (lo + hi) / 2, hi - 0.3)])
23
24
 
24
25
  def spectrum(T, logN):
25
26
  return 0.01 * (basis[0] + (1 + (T - 100) / 25) * basis[1]
@@ -33,6 +34,25 @@ def _small_grid(tmp_path):
33
34
  return wav, spectrum
34
35
 
35
36
 
37
+ @pytest.mark.parametrize('molecule,wavelength_range', [
38
+ ('C6H6', (13.0, 17.0)),
39
+ ('C3H4', (15.0, 18.0)),
40
+ ])
41
+ def test_new_molecules_fit_from_direct_grids(tmp_path, molecule,
42
+ wavelength_range):
43
+ wav, spectrum = _small_grid(tmp_path, molecule, wavelength_range)
44
+ observed = spectrum(113.0, 13.07)
45
+ fit = fit_grid_stage(
46
+ wav, observed, molecule, wavelength_range, wavelength_range,
47
+ grid_root=tmp_path / 'grids', T_bounds=(100, 150),
48
+ logN_bounds=(13, 13.25), sigma=1e-4,
49
+ maxiter=20, popsize=5, n_refine=2,
50
+ )
51
+ assert molecule in fit['params']
52
+ assert np.isfinite(fit['model_flux']).all()
53
+ assert np.linalg.norm(fit['residual']) < np.linalg.norm(observed)
54
+
55
+
36
56
  def test_grid_fit_and_saved_errors(tmp_path):
37
57
  wav, spectrum = _small_grid(tmp_path)
38
58
  distance_pc = 177.0
@@ -114,3 +134,42 @@ def test_grid_stage_rejects_unapplied_isotope_constraints(tmp_path):
114
134
  output_dir=tmp_path / 'results', detection_screening=False,
115
135
  )
116
136
  assert not (tmp_path / 'results').exists()
137
+
138
+
139
+ def test_repeated_molecule_has_independent_component_bounds(tmp_path):
140
+ wav, spectrum = _small_grid(tmp_path)
141
+ observed = 0.7 * spectrum(110, 13.04) + 1.4 * spectrum(140, 13.20)
142
+ stage = {
143
+ 'name': 'acetylene', 'mol': ['C2H2', 'C2H2'],
144
+ 'component_names': ['C2H2_thin', 'C2H2_thick'],
145
+ 'wavelength_range': (11, 13), 'fit_ranges': (11, 13),
146
+ 'T_bounds': (100, 150),
147
+ 'logN_bounds': {'C2H2_thin': (13.0, 13.1),
148
+ 'C2H2_thick': (13.15, 13.25)},
149
+ 'loga_bounds': {'C2H2_thin': (-1.0, 0.0),
150
+ 'C2H2_thick': (0.0, 1.0)},
151
+ }
152
+ input_path = tmp_path / 'observation.csv'
153
+ pd.DataFrame({'wave': wav, 'flux': observed}).to_csv(input_path, index=False)
154
+ result = fit_observation_grids(
155
+ input_path, [stage], grid_root=tmp_path / 'grids',
156
+ output_dir=tmp_path / 'results', sigma_noise=1e-4,
157
+ detection_screening=False, maxiter=12, popsize=5, n_refine=2,
158
+ )
159
+ params = result['fits']['acetylene']['params']
160
+ assert set(params) == {'C2H2_thin', 'C2H2_thick'}
161
+ assert 13.0 <= params['C2H2_thin']['logN'] <= 13.1
162
+ assert 13.15 <= params['C2H2_thick']['logN'] <= 13.25
163
+ assert 0.1 <= params['C2H2_thin']['A'] <= 1.0
164
+ assert 1.0 <= params['C2H2_thick']['A'] <= 10.0
165
+ spectrum_csv = pd.read_csv(result['products']['acetylene']['spectrum'])
166
+ assert {'C2H2_thin_model', 'C2H2_thick_model'} <= set(spectrum_csv.columns)
167
+
168
+
169
+ def test_repeated_grid_molecule_requires_unique_labels(tmp_path):
170
+ wav, spectrum = _small_grid(tmp_path)
171
+ with pytest.raises(ValueError, match='component labels must be unique'):
172
+ fit_grid_stage(
173
+ wav, spectrum(125, 13.125), ['C2H2', 'C2H2'],
174
+ (11, 13), (11, 13), grid_root=tmp_path / 'grids',
175
+ )
@@ -1,5 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import warnings
4
+
3
5
  import numpy as np
4
6
  import pandas as pd
5
7
  import pytest
@@ -53,3 +55,26 @@ def test_save_running_spectrum(tmp_path):
53
55
  assert list(df.columns) == ["wave", "residual", "cumulative_model"]
54
56
  assert len(df) == 50
55
57
  assert df["wave"].iloc[0] == pytest.approx(11.0)
58
+
59
+
60
+ def test_save_fitted_comparison_updates_missing_numeric_fields(tmp_path):
61
+ pytest.importorskip("torch")
62
+ from diskmelts import save_fitted_comparison
63
+
64
+ path = tmp_path / "comparison.csv"
65
+ water = {"water": {"params": {
66
+ "H2O_warm": {"logN": 17.0, "T": 400.0, "log10A": 1.0},
67
+ }}}
68
+ carbon = {"carbon": {"params": {
69
+ "C2H2": {"logN": 18.0, "T": 600.0, "log10A": 2.0},
70
+ }}}
71
+ save_fitted_comparison(path, "source", water)
72
+ with warnings.catch_warnings():
73
+ warnings.simplefilter("error", FutureWarning)
74
+ save_fitted_comparison(path, "source", carbon)
75
+
76
+ df = pd.read_csv(path)
77
+ assert len(df) == 1
78
+ assert pd.isna(df.loc[0, "H2O_N_warm"])
79
+ assert pd.isna(df.loc[0, "C2H2_N_err_low"])
80
+ assert df.loc[0, "C2H2_N"] == pytest.approx(18.0)
@@ -8,6 +8,44 @@ pytest.importorskip('torch')
8
8
  from diskmelts import fitting
9
9
 
10
10
 
11
+ def test_repeated_model_molecule_uses_component_bounds(monkeypatch):
12
+ wave = np.linspace(13.0, 14.0, 41)
13
+ bases = (np.exp(-((wave - 13.3) / 0.1) ** 2),
14
+ np.exp(-((wave - 13.7) / 0.1) ** 2))
15
+
16
+ def fake_flux(model, T, logN, A, obs_wav):
17
+ shape = bases[0] + (logN - 14.0) * bases[1]
18
+ return A * np.interp(obs_wav, wave, shape)
19
+
20
+ monkeypatch.setattr(fitting, '_mol_flux_on_wav', fake_flux)
21
+ observed = fake_flux(None, 300, 14.5, 0.8, wave) + fake_flux(None, 500, 16.5, 1.5, wave)
22
+ fit = fitting.fit_molecules(
23
+ wave, observed, ['C2H2', 'C2H2'], {'C2H2': {'mol': 'C2H2'}},
24
+ (13.0, 14.0), component_names=['C2H2_thin', 'C2H2_thick'],
25
+ T_bounds=(200, 600),
26
+ logN_bounds={'C2H2_thin': (14.0, 15.0),
27
+ 'C2H2_thick': (16.0, 17.0)},
28
+ loga_bounds={'C2H2_thin': (-1.0, 0.0),
29
+ 'C2H2_thick': (0.0, 1.0)},
30
+ n_samples=16, n_refine=2, n_top=2, seed=7, verbose=False,
31
+ )
32
+ params = fit['params']
33
+ assert set(params) == {'C2H2_thin', 'C2H2_thick'}
34
+ assert 14.0 <= params['C2H2_thin']['logN'] <= 15.0
35
+ assert 16.0 <= params['C2H2_thick']['logN'] <= 17.0
36
+ assert 0.1 <= params['C2H2_thin']['A'] <= 1.0
37
+ assert 1.0 <= params['C2H2_thick']['A'] <= 10.0
38
+
39
+
40
+ def test_repeated_model_molecule_requires_unique_labels():
41
+ with pytest.raises(ValueError, match='component labels must be unique'):
42
+ fitting.fit_molecules(
43
+ np.array([13.0, 14.0]), np.array([1.0, 1.0]),
44
+ ['C2H2', 'C2H2'], {'C2H2': {}}, (13.0, 14.0),
45
+ verbose=False,
46
+ )
47
+
48
+
11
49
  def test_shared_area_groups_use_one_amplitude_in_joint_fit(monkeypatch):
12
50
  wave = np.linspace(13.0, 16.0, 80)
13
51
  profiles = {
@@ -23,3 +23,22 @@ def test_bundled_model_fit_without_grids(tmp_path, monkeypatch):
23
23
  assert fit['params']['C2H2']['logN'] == pytest.approx(16, abs=0.02)
24
24
  with pytest.raises(ValueError, match='grid_root is required'):
25
25
  fit_grid_stage(wave, flux, 'C2H2', (13.5, 14.1), (13.5, 14.1))
26
+
27
+
28
+ @pytest.mark.parametrize('molecule,window', [
29
+ ('C6H6', (14.5, 15.2)),
30
+ ('C3H4', (15.68, 15.85)),
31
+ ])
32
+ def test_new_bundled_molecules_fit_without_grids(tmp_path, monkeypatch,
33
+ molecule, window):
34
+ monkeypatch.chdir(tmp_path)
35
+ models = load_fitting_models(molecule)
36
+ wave = np.linspace(*window, 100)
37
+ flux = generate_spectrum(500, 16, 1.0, models[molecule], obs_wav=wave)
38
+ fit = fit_stage(
39
+ wave, flux, molecule, window, window, models,
40
+ n_samples=64, n_refine=2, n_top=2, seed=4,
41
+ )
42
+ assert molecule in fit['params']
43
+ assert np.isfinite(fit['model_flux']).all()
44
+ assert np.linalg.norm(fit['residual']) < np.linalg.norm(flux)
@@ -30,6 +30,14 @@ def test_notebook_code_cells_compile(path):
30
30
  compile("".join(cell["source"]), f"{path.name}:cell-{index}", "exec")
31
31
 
32
32
 
33
+ @pytest.mark.parametrize("path", [FITTING_NOTEBOOK, TRAINING_NOTEBOOK])
34
+ def test_notebook_rejects_incompatible_sklearn(path, monkeypatch):
35
+ sklearn = pytest.importorskip("sklearn")
36
+ monkeypatch.setattr(sklearn, "__version__", "0.0")
37
+ with pytest.raises(RuntimeError, match="require scikit-learn 1.8.0"):
38
+ exec(_code_cell(_notebook(path), 1), {})
39
+
40
+
33
41
  @pytest.mark.parametrize("start_dir", [ROOT, ROOT / "notebooks"])
34
42
  def test_consub_notebook_reads_public_usco_csv(monkeypatch, start_dir):
35
43
  raw_dir = ROOT / "Realobs_data" / "Raw_data"
@@ -46,6 +54,9 @@ def test_consub_notebook_reads_public_usco_csv(monkeypatch, start_dir):
46
54
  @pytest.mark.parametrize("start_dir", [ROOT, ROOT / "notebooks"])
47
55
  def test_fitting_notebook_resolves_committed_assets(monkeypatch, start_dir):
48
56
  pytest.importorskip("torch")
57
+ sklearn = pytest.importorskip("sklearn")
58
+ # This test checks paths and stage settings; the version guard is tested above.
59
+ monkeypatch.setattr(sklearn, "__version__", "1.8.0")
49
60
  notebook = _notebook(FITTING_NOTEBOOK)
50
61
  namespace = {}
51
62
 
@@ -65,6 +76,8 @@ def test_fitting_notebook_resolves_committed_assets(monkeypatch, start_dir):
65
76
  @pytest.mark.parametrize("start_dir", [ROOT, ROOT / "notebooks"])
66
77
  def test_training_notebook_resolves_local_data_locations(monkeypatch, start_dir):
67
78
  pytest.importorskip("torch")
79
+ sklearn = pytest.importorskip("sklearn")
80
+ monkeypatch.setattr(sklearn, "__version__", "1.8.0")
68
81
  notebook = _notebook(TRAINING_NOTEBOOK)
69
82
  namespace = {}
70
83
 
@@ -83,11 +96,11 @@ def test_production_checkpoint_files_are_bundled():
83
96
  from diskmelts import MODEL_SPECS
84
97
  from diskmelts.v2_fitting import MODEL_ROOT
85
98
 
86
- assert len(MODEL_SPECS) == 14
99
+ assert len(MODEL_SPECS) == 16
87
100
  for molecule, spec in MODEL_SPECS.items():
88
101
  path = MODEL_ROOT / spec['path']
89
102
  assert path.is_file(), f'missing bundled checkpoint for {molecule}: {path}'
90
- assert len(list(MODEL_ROOT.rglob('*.pt'))) == 18
103
+ assert len(list(MODEL_ROOT.rglob('*.pt'))) == 20
91
104
 
92
105
 
93
106
  def test_new_grid_checkpoints_load_and_generate_finite_spectra():
@@ -95,7 +108,7 @@ def test_new_grid_checkpoints_load_and_generate_finite_spectra():
95
108
  from diskmelts import load_fitting_models
96
109
  from diskmelts.fitting import generate_spectrum
97
110
 
98
- molecules = ('13C12CH2', 'C2HD', 'CO17O', 'CO18O')
111
+ molecules = ('13C12CH2', 'C2HD', 'CO17O', 'CO18O', 'C6H6', 'C3H4')
99
112
  models = load_fitting_models(molecules)
100
113
  for molecule in molecules:
101
114
  flux = generate_spectrum(400., 16., 1., models[molecule])
@@ -111,7 +124,7 @@ def test_gitignore_separates_fitting_assets_from_training_data():
111
124
  if line.strip() and not line.lstrip().startswith("#")
112
125
  }
113
126
 
114
- for pattern in ('/Model_grids/', '/Pretrain_grid/', '/Trained_model/',
127
+ for pattern in ('/Model_grids/*', '/Pretrain_grid/', '/Trained_model/',
115
128
  '/0-Consub/',
116
129
  '/docs/_build/', '/realobs_results/', '/figures/*'):
117
130
  assert pattern in patterns
File without changes
File without changes
File without changes
File without changes
File without changes