diskmelts 2.1__tar.gz → 2.1.1__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 (65) hide show
  1. {diskmelts-2.1/src/diskmelts.egg-info → diskmelts-2.1.1}/PKG-INFO +38 -57
  2. {diskmelts-2.1 → diskmelts-2.1.1}/README.md +37 -56
  3. {diskmelts-2.1 → diskmelts-2.1.1}/pyproject.toml +1 -1
  4. {diskmelts-2.1 → diskmelts-2.1.1}/src/diskmelts/fitting.py +24 -9
  5. {diskmelts-2.1 → diskmelts-2.1.1}/src/diskmelts/plotting.py +2 -1
  6. {diskmelts-2.1 → diskmelts-2.1.1}/src/diskmelts/v2_grid_fitting.py +18 -14
  7. {diskmelts-2.1 → diskmelts-2.1.1/src/diskmelts.egg-info}/PKG-INFO +38 -57
  8. {diskmelts-2.1 → diskmelts-2.1.1}/tests/test_dev_v2_fitting_grids.py +39 -0
  9. {diskmelts-2.1 → diskmelts-2.1.1}/tests/test_shared_area_fitting.py +38 -0
  10. {diskmelts-2.1 → diskmelts-2.1.1}/LICENSE +0 -0
  11. {diskmelts-2.1 → diskmelts-2.1.1}/setup.cfg +0 -0
  12. {diskmelts-2.1 → diskmelts-2.1.1}/src/diskmelts/__init__.py +0 -0
  13. {diskmelts-2.1 → diskmelts-2.1.1}/src/diskmelts/consub/__init__.py +0 -0
  14. {diskmelts-2.1 → diskmelts-2.1.1}/src/diskmelts/consub/_api.py +0 -0
  15. {diskmelts-2.1 → diskmelts-2.1.1}/src/diskmelts/consub/_core.py +0 -0
  16. {diskmelts-2.1 → diskmelts-2.1.1}/src/diskmelts/consub/cont_ranges.csv +0 -0
  17. {diskmelts-2.1 → diskmelts-2.1.1}/src/diskmelts/consub/cont_ranges_PPan.csv +0 -0
  18. {diskmelts-2.1 → diskmelts-2.1.1}/src/diskmelts/consub/regions/carbon_extended.csv +0 -0
  19. {diskmelts-2.1 → diskmelts-2.1.1}/src/diskmelts/consub/regions/carbon_extended.json +0 -0
  20. {diskmelts-2.1 → diskmelts-2.1.1}/src/diskmelts/consub/regions/carbon_rich.csv +0 -0
  21. {diskmelts-2.1 → diskmelts-2.1.1}/src/diskmelts/consub/regions/carbon_rich.json +0 -0
  22. {diskmelts-2.1 → diskmelts-2.1.1}/src/diskmelts/consub/regions/ttauri.csv +0 -0
  23. {diskmelts-2.1 → diskmelts-2.1.1}/src/diskmelts/consub/regions/ttauri.json +0 -0
  24. {diskmelts-2.1 → diskmelts-2.1.1}/src/diskmelts/consub/regions/water.csv +0 -0
  25. {diskmelts-2.1 → diskmelts-2.1.1}/src/diskmelts/consub/regions/water.json +0 -0
  26. {diskmelts-2.1 → diskmelts-2.1.1}/src/diskmelts/models/Trained_model/13C12CH2_single_v2/net_13C12CH2_forward.pt +0 -0
  27. {diskmelts-2.1 → diskmelts-2.1.1}/src/diskmelts/models/Trained_model/13CO2_single_v2/net_13CO2_forward.pt +0 -0
  28. {diskmelts-2.1 → diskmelts-2.1.1}/src/diskmelts/models/Trained_model/C2H2_single_v2/net_C2H2_forward.pt +0 -0
  29. {diskmelts-2.1 → diskmelts-2.1.1}/src/diskmelts/models/Trained_model/C2H4_single_v2/net_C2H4_forward.pt +0 -0
  30. {diskmelts-2.1 → diskmelts-2.1.1}/src/diskmelts/models/Trained_model/C2H6_single_v2/net_C2H6_forward.pt +0 -0
  31. {diskmelts-2.1 → diskmelts-2.1.1}/src/diskmelts/models/Trained_model/C2HD_single_v2/net_C2HD_forward.pt +0 -0
  32. {diskmelts-2.1 → diskmelts-2.1.1}/src/diskmelts/models/Trained_model/C4H2_single_v2/net_C4H2_forward.pt +0 -0
  33. {diskmelts-2.1 → diskmelts-2.1.1}/src/diskmelts/models/Trained_model/CH4_single_v2/net_CH4_forward.pt +0 -0
  34. {diskmelts-2.1 → diskmelts-2.1.1}/src/diskmelts/models/Trained_model/CO17O_single_v2/net_CO17O_forward.pt +0 -0
  35. {diskmelts-2.1 → diskmelts-2.1.1}/src/diskmelts/models/Trained_model/CO18O_single_v2/net_CO18O_forward.pt +0 -0
  36. {diskmelts-2.1 → diskmelts-2.1.1}/src/diskmelts/models/Trained_model/CO2_single_v2/net_CO2_forward.pt +0 -0
  37. {diskmelts-2.1 → diskmelts-2.1.1}/src/diskmelts/models/Trained_model/H2O_wavelength_bank_v2_fixed/H2O_model_bank.json +0 -0
  38. {diskmelts-2.1 → diskmelts-2.1.1}/src/diskmelts/models/Trained_model/H2O_wavelength_bank_v2_fixed/net_H2O_w13p5to19p2_n13to19_t100to1400.pt +0 -0
  39. {diskmelts-2.1 → diskmelts-2.1.1}/src/diskmelts/models/Trained_model/H2O_wavelength_bank_v2_fixed/net_H2O_w19to25_n13to19_t100to1400.pt +0 -0
  40. {diskmelts-2.1 → diskmelts-2.1.1}/src/diskmelts/models/Trained_model/H2O_wavelength_bank_v2_fixed/net_H2O_w4p9to7p4_n13to19_t100to1400.pt +0 -0
  41. {diskmelts-2.1 → diskmelts-2.1.1}/src/diskmelts/models/Trained_model/H2O_wavelength_bank_v2_fixed/net_H2O_w7p2to11p2_n13to19_t100to1400.pt +0 -0
  42. {diskmelts-2.1 → diskmelts-2.1.1}/src/diskmelts/models/Trained_model/H2O_wavelength_bank_v2_fixed/net_H2O_w9p9to15p1_n13to19_t100to1400.pt +0 -0
  43. {diskmelts-2.1 → diskmelts-2.1.1}/src/diskmelts/models/Trained_model/HC3N_single_v2/net_HC3N_forward.pt +0 -0
  44. {diskmelts-2.1 → diskmelts-2.1.1}/src/diskmelts/models/Trained_model/HCN_single_v2/net_HCN_forward.pt +0 -0
  45. {diskmelts-2.1 → diskmelts-2.1.1}/src/diskmelts/trainmodel.py +0 -0
  46. {diskmelts-2.1 → diskmelts-2.1.1}/src/diskmelts/v2_fitting.py +0 -0
  47. {diskmelts-2.1 → diskmelts-2.1.1}/src/diskmelts/v2_paper_comparison.py +0 -0
  48. {diskmelts-2.1 → diskmelts-2.1.1}/src/diskmelts/v2_read_plot.py +0 -0
  49. {diskmelts-2.1 → diskmelts-2.1.1}/src/diskmelts/validation.py +0 -0
  50. {diskmelts-2.1 → diskmelts-2.1.1}/src/diskmelts.egg-info/SOURCES.txt +0 -0
  51. {diskmelts-2.1 → diskmelts-2.1.1}/src/diskmelts.egg-info/dependency_links.txt +0 -0
  52. {diskmelts-2.1 → diskmelts-2.1.1}/src/diskmelts.egg-info/requires.txt +0 -0
  53. {diskmelts-2.1 → diskmelts-2.1.1}/src/diskmelts.egg-info/top_level.txt +0 -0
  54. {diskmelts-2.1 → diskmelts-2.1.1}/tests/test_consub_package.py +0 -0
  55. {diskmelts-2.1 → diskmelts-2.1.1}/tests/test_detection.py +0 -0
  56. {diskmelts-2.1 → diskmelts-2.1.1}/tests/test_dev_v2_fitting_distance.py +0 -0
  57. {diskmelts-2.1 → diskmelts-2.1.1}/tests/test_dev_v2_read_plot.py +0 -0
  58. {diskmelts-2.1 → diskmelts-2.1.1}/tests/test_imports.py +0 -0
  59. {diskmelts-2.1 → diskmelts-2.1.1}/tests/test_io.py +0 -0
  60. {diskmelts-2.1 → diskmelts-2.1.1}/tests/test_mlp.py +0 -0
  61. {diskmelts-2.1 → diskmelts-2.1.1}/tests/test_model_bank.py +0 -0
  62. {diskmelts-2.1 → diskmelts-2.1.1}/tests/test_smoke.py +0 -0
  63. {diskmelts-2.1 → diskmelts-2.1.1}/tests/test_v2_package_assets.py +0 -0
  64. {diskmelts-2.1 → diskmelts-2.1.1}/tests/test_water_training.py +0 -0
  65. {diskmelts-2.1 → diskmelts-2.1.1}/tests/test_workflows.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.1
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,29 @@ 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 2.1.1 is available on [PyPI](https://pypi.org/project/diskmelts/).
53
+ With Python 3.9 or newer, install it from any directory. These commands use
54
+ `python` in an activated environment; the
55
+ [installation guide](https://diskmelts.readthedocs.io/en/latest/installation.html)
56
+ shows how to create one on macOS, Linux, Windows, or Conda:
54
57
 
55
58
  ```bash
56
59
  python -m pip install diskmelts
57
- # Add [io] for FITS input, or [io,notebooks] for JupyterLab.
58
- ```
59
-
60
- Until the first PyPI release, install directly from GitHub without a clone:
61
-
62
- ```bash
63
- python -m pip install "diskmelts[io] @ git+https://github.com/wddlx/DiskMELTS.git"
60
+ python -c "import diskmelts; print('DiskMELTS is ready')"
64
61
  ```
65
62
 
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:
63
+ Optional features can be installed in the same way:
76
64
 
77
65
  ```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__)"
66
+ python -m pip install "diskmelts[io]" # FITS input
67
+ python -m pip install "diskmelts[io,notebooks]" # FITS input and JupyterLab
81
68
  ```
82
69
 
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.
70
+ The pip package includes the production checkpoints and continuum reference
71
+ tables. Full slab grids for training and direct-grid fitting are separate
72
+ downloads; pass their local path as `grid_root`.
73
+ Example notebooks and spectra can be downloaded from the repository when
74
+ needed; they are not installed by pip.
104
75
 
105
76
  ## Runtime dependencies
106
77
 
@@ -158,34 +129,44 @@ special regions, FITS input, saving CSVs, and uncertainty diagnostics.
158
129
 
159
130
  ## Quick fitting example
160
131
 
132
+ Use a continuum-subtracted CSV with a header and `wave,flux` as its first two
133
+ columns, with wavelength in µm and flux in Jy. For a runnable example without
134
+ cloning the repository, download the
135
+ [J16120505 spectrum](https://github.com/wddlx/DiskMELTS/blob/main/Realobs_data/Consub_data/j16120505_v9.0_contsub_RVcorr.csv)
136
+ as `j16120505_contsub.csv`, or run:
137
+
138
+ ```bash
139
+ curl -L -o j16120505_contsub.csv https://raw.githubusercontent.com/wddlx/DiskMELTS/main/Realobs_data/Consub_data/j16120505_v9.0_contsub_RVcorr.csv
140
+ ```
141
+
161
142
  ```python
162
- from diskmelts import load_fitting_models, load_observed_spectrum, fit_stage
143
+ from diskmelts import fit_stage, load_fitting_models, load_observed_spectrum
163
144
 
164
- pretrained = load_fitting_models(['H2O'])
145
+ models = load_fitting_models('C2H2')
165
146
 
166
147
  obs_wav, obs_flux = load_observed_spectrum(
167
- 'Realobs_data/Consub_data/j16120505_v9.0_contsub_RVcorr.csv'
148
+ 'j16120505_contsub.csv'
168
149
  )
169
150
 
170
151
  fit = fit_stage(
171
152
  obs_wav,
172
153
  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)],
154
+ molecule='C2H2',
155
+ wavelength_range=(13.0, 15.0),
156
+ pretrained=models,
157
+ fit_ranges=(13.5, 14.1),
177
158
  distance_pc=122.5,
178
- n_samples=20000,
179
- n_refine=32,
180
- n_top=20,
181
- sigma=0.001,
159
+ n_samples=1024,
160
+ n_refine=4,
161
+ n_top=4,
182
162
  )
183
163
 
184
- print(fit['params'])
164
+ print(fit['params']['C2H2'])
185
165
  ```
186
166
 
187
167
  You can also read spectra with your own code. DiskMELTS only requires
188
- one-dimensional `obs_wav` and `obs_flux` arrays.
168
+ one-dimensional `obs_wav` and `obs_flux` arrays. Replace the sample CSV with
169
+ your own file to fit your source; update `distance_pc` and fit intervals.
189
170
 
190
171
  For a complete staged fit-and-subtract workflow from a checkout, edit and run:
191
172
 
@@ -10,58 +10,29 @@ 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 2.1.1 is available on [PyPI](https://pypi.org/project/diskmelts/).
14
+ With Python 3.9 or newer, install it from any directory. These commands use
15
+ `python` in an activated environment; the
16
+ [installation guide](https://diskmelts.readthedocs.io/en/latest/installation.html)
17
+ shows how to create one on macOS, Linux, Windows, or Conda:
15
18
 
16
19
  ```bash
17
20
  python -m pip install diskmelts
18
- # Add [io] for FITS input, or [io,notebooks] for JupyterLab.
19
- ```
20
-
21
- Until the first PyPI release, install directly from GitHub without a clone:
22
-
23
- ```bash
24
- python -m pip install "diskmelts[io] @ git+https://github.com/wddlx/DiskMELTS.git"
21
+ python -c "import diskmelts; print('DiskMELTS is ready')"
25
22
  ```
26
23
 
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:
24
+ Optional features can be installed in the same way:
37
25
 
38
26
  ```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__)"
27
+ python -m pip install "diskmelts[io]" # FITS input
28
+ python -m pip install "diskmelts[io,notebooks]" # FITS input and JupyterLab
42
29
  ```
43
30
 
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.
31
+ The pip package includes the production checkpoints and continuum reference
32
+ tables. Full slab grids for training and direct-grid fitting are separate
33
+ downloads; pass their local path as `grid_root`.
34
+ Example notebooks and spectra can be downloaded from the repository when
35
+ needed; they are not installed by pip.
65
36
 
66
37
  ## Runtime dependencies
67
38
 
@@ -119,34 +90,44 @@ special regions, FITS input, saving CSVs, and uncertainty diagnostics.
119
90
 
120
91
  ## Quick fitting example
121
92
 
93
+ Use a continuum-subtracted CSV with a header and `wave,flux` as its first two
94
+ columns, with wavelength in µm and flux in Jy. For a runnable example without
95
+ cloning the repository, download the
96
+ [J16120505 spectrum](https://github.com/wddlx/DiskMELTS/blob/main/Realobs_data/Consub_data/j16120505_v9.0_contsub_RVcorr.csv)
97
+ as `j16120505_contsub.csv`, or run:
98
+
99
+ ```bash
100
+ curl -L -o j16120505_contsub.csv https://raw.githubusercontent.com/wddlx/DiskMELTS/main/Realobs_data/Consub_data/j16120505_v9.0_contsub_RVcorr.csv
101
+ ```
102
+
122
103
  ```python
123
- from diskmelts import load_fitting_models, load_observed_spectrum, fit_stage
104
+ from diskmelts import fit_stage, load_fitting_models, load_observed_spectrum
124
105
 
125
- pretrained = load_fitting_models(['H2O'])
106
+ models = load_fitting_models('C2H2')
126
107
 
127
108
  obs_wav, obs_flux = load_observed_spectrum(
128
- 'Realobs_data/Consub_data/j16120505_v9.0_contsub_RVcorr.csv'
109
+ 'j16120505_contsub.csv'
129
110
  )
130
111
 
131
112
  fit = fit_stage(
132
113
  obs_wav,
133
114
  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)],
115
+ molecule='C2H2',
116
+ wavelength_range=(13.0, 15.0),
117
+ pretrained=models,
118
+ fit_ranges=(13.5, 14.1),
138
119
  distance_pc=122.5,
139
- n_samples=20000,
140
- n_refine=32,
141
- n_top=20,
142
- sigma=0.001,
120
+ n_samples=1024,
121
+ n_refine=4,
122
+ n_top=4,
143
123
  )
144
124
 
145
- print(fit['params'])
125
+ print(fit['params']['C2H2'])
146
126
  ```
147
127
 
148
128
  You can also read spectra with your own code. DiskMELTS only requires
149
- one-dimensional `obs_wav` and `obs_flux` arrays.
129
+ one-dimensional `obs_wav` and `obs_flux` arrays. Replace the sample CSV with
130
+ your own file to fit your source; update `distance_pc` and fit intervals.
150
131
 
151
132
  For a complete staged fit-and-subtract workflow from a checkout, edit and run:
152
133
 
@@ -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.1"
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:
@@ -270,7 +270,8 @@ def plot_fit(
270
270
  ax_bot.text(0.99, 0.92, rf'$\chi^2_{{\rm red}} = {chi2_red:.2f}$',
271
271
  transform=ax_bot.transAxes, ha='right', va='top', fontsize=9)
272
272
 
273
- plt.tight_layout()
273
+ fig.subplots_adjust(left=0.11, right=0.98, bottom=0.10, top=0.92,
274
+ hspace=0.08)
274
275
  if show:
275
276
  plt.show()
276
277
  if save_path is not None:
@@ -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.1
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,29 @@ 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 2.1.1 is available on [PyPI](https://pypi.org/project/diskmelts/).
53
+ With Python 3.9 or newer, install it from any directory. These commands use
54
+ `python` in an activated environment; the
55
+ [installation guide](https://diskmelts.readthedocs.io/en/latest/installation.html)
56
+ shows how to create one on macOS, Linux, Windows, or Conda:
54
57
 
55
58
  ```bash
56
59
  python -m pip install diskmelts
57
- # Add [io] for FITS input, or [io,notebooks] for JupyterLab.
58
- ```
59
-
60
- Until the first PyPI release, install directly from GitHub without a clone:
61
-
62
- ```bash
63
- python -m pip install "diskmelts[io] @ git+https://github.com/wddlx/DiskMELTS.git"
60
+ python -c "import diskmelts; print('DiskMELTS is ready')"
64
61
  ```
65
62
 
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:
63
+ Optional features can be installed in the same way:
76
64
 
77
65
  ```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__)"
66
+ python -m pip install "diskmelts[io]" # FITS input
67
+ python -m pip install "diskmelts[io,notebooks]" # FITS input and JupyterLab
81
68
  ```
82
69
 
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.
70
+ The pip package includes the production checkpoints and continuum reference
71
+ tables. Full slab grids for training and direct-grid fitting are separate
72
+ downloads; pass their local path as `grid_root`.
73
+ Example notebooks and spectra can be downloaded from the repository when
74
+ needed; they are not installed by pip.
104
75
 
105
76
  ## Runtime dependencies
106
77
 
@@ -158,34 +129,44 @@ special regions, FITS input, saving CSVs, and uncertainty diagnostics.
158
129
 
159
130
  ## Quick fitting example
160
131
 
132
+ Use a continuum-subtracted CSV with a header and `wave,flux` as its first two
133
+ columns, with wavelength in µm and flux in Jy. For a runnable example without
134
+ cloning the repository, download the
135
+ [J16120505 spectrum](https://github.com/wddlx/DiskMELTS/blob/main/Realobs_data/Consub_data/j16120505_v9.0_contsub_RVcorr.csv)
136
+ as `j16120505_contsub.csv`, or run:
137
+
138
+ ```bash
139
+ curl -L -o j16120505_contsub.csv https://raw.githubusercontent.com/wddlx/DiskMELTS/main/Realobs_data/Consub_data/j16120505_v9.0_contsub_RVcorr.csv
140
+ ```
141
+
161
142
  ```python
162
- from diskmelts import load_fitting_models, load_observed_spectrum, fit_stage
143
+ from diskmelts import fit_stage, load_fitting_models, load_observed_spectrum
163
144
 
164
- pretrained = load_fitting_models(['H2O'])
145
+ models = load_fitting_models('C2H2')
165
146
 
166
147
  obs_wav, obs_flux = load_observed_spectrum(
167
- 'Realobs_data/Consub_data/j16120505_v9.0_contsub_RVcorr.csv'
148
+ 'j16120505_contsub.csv'
168
149
  )
169
150
 
170
151
  fit = fit_stage(
171
152
  obs_wav,
172
153
  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)],
154
+ molecule='C2H2',
155
+ wavelength_range=(13.0, 15.0),
156
+ pretrained=models,
157
+ fit_ranges=(13.5, 14.1),
177
158
  distance_pc=122.5,
178
- n_samples=20000,
179
- n_refine=32,
180
- n_top=20,
181
- sigma=0.001,
159
+ n_samples=1024,
160
+ n_refine=4,
161
+ n_top=4,
182
162
  )
183
163
 
184
- print(fit['params'])
164
+ print(fit['params']['C2H2'])
185
165
  ```
186
166
 
187
167
  You can also read spectra with your own code. DiskMELTS only requires
188
- one-dimensional `obs_wav` and `obs_flux` arrays.
168
+ one-dimensional `obs_wav` and `obs_flux` arrays. Replace the sample CSV with
169
+ your own file to fit your source; update `distance_pc` and fit intervals.
189
170
 
190
171
  For a complete staged fit-and-subtract workflow from a checkout, edit and run:
191
172
 
@@ -114,3 +114,42 @@ def test_grid_stage_rejects_unapplied_isotope_constraints(tmp_path):
114
114
  output_dir=tmp_path / 'results', detection_screening=False,
115
115
  )
116
116
  assert not (tmp_path / 'results').exists()
117
+
118
+
119
+ def test_repeated_molecule_has_independent_component_bounds(tmp_path):
120
+ wav, spectrum = _small_grid(tmp_path)
121
+ observed = 0.7 * spectrum(110, 13.04) + 1.4 * spectrum(140, 13.20)
122
+ stage = {
123
+ 'name': 'acetylene', 'mol': ['C2H2', 'C2H2'],
124
+ 'component_names': ['C2H2_thin', 'C2H2_thick'],
125
+ 'wavelength_range': (11, 13), 'fit_ranges': (11, 13),
126
+ 'T_bounds': (100, 150),
127
+ 'logN_bounds': {'C2H2_thin': (13.0, 13.1),
128
+ 'C2H2_thick': (13.15, 13.25)},
129
+ 'loga_bounds': {'C2H2_thin': (-1.0, 0.0),
130
+ 'C2H2_thick': (0.0, 1.0)},
131
+ }
132
+ input_path = tmp_path / 'observation.csv'
133
+ pd.DataFrame({'wave': wav, 'flux': observed}).to_csv(input_path, index=False)
134
+ result = fit_observation_grids(
135
+ input_path, [stage], grid_root=tmp_path / 'grids',
136
+ output_dir=tmp_path / 'results', sigma_noise=1e-4,
137
+ detection_screening=False, maxiter=12, popsize=5, n_refine=2,
138
+ )
139
+ params = result['fits']['acetylene']['params']
140
+ assert set(params) == {'C2H2_thin', 'C2H2_thick'}
141
+ assert 13.0 <= params['C2H2_thin']['logN'] <= 13.1
142
+ assert 13.15 <= params['C2H2_thick']['logN'] <= 13.25
143
+ assert 0.1 <= params['C2H2_thin']['A'] <= 1.0
144
+ assert 1.0 <= params['C2H2_thick']['A'] <= 10.0
145
+ spectrum_csv = pd.read_csv(result['products']['acetylene']['spectrum'])
146
+ assert {'C2H2_thin_model', 'C2H2_thick_model'} <= set(spectrum_csv.columns)
147
+
148
+
149
+ def test_repeated_grid_molecule_requires_unique_labels(tmp_path):
150
+ wav, spectrum = _small_grid(tmp_path)
151
+ with pytest.raises(ValueError, match='component labels must be unique'):
152
+ fit_grid_stage(
153
+ wav, spectrum(125, 13.125), ['C2H2', 'C2H2'],
154
+ (11, 13), (11, 13), grid_root=tmp_path / 'grids',
155
+ )
@@ -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 = {
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes