aspcap 0.2.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 (41) hide show
  1. aspcap-0.2.0/LICENSE +21 -0
  2. aspcap-0.2.0/LICENSE-DOPPLER +21 -0
  3. aspcap-0.2.0/MANIFEST.in +4 -0
  4. aspcap-0.2.0/PKG-INFO +185 -0
  5. aspcap-0.2.0/README.md +147 -0
  6. aspcap-0.2.0/aspcap/__init__.py +8 -0
  7. aspcap-0.2.0/aspcap/abundances.py +73 -0
  8. aspcap-0.2.0/aspcap/cli.py +64 -0
  9. aspcap-0.2.0/aspcap/continuum.py +437 -0
  10. aspcap-0.2.0/aspcap/data/apogee-dr17-mgiant-full-weights.json +1 -0
  11. aspcap-0.2.0/aspcap/data/apogee-dr17-mgiant-full.hdr +15 -0
  12. aspcap-0.2.0/aspcap/data/apogee-dr17-mgiant-full.unf +0 -0
  13. aspcap-0.2.0/aspcap/ferre.py +456 -0
  14. aspcap-0.2.0/aspcap/spectrum.py +551 -0
  15. aspcap-0.2.0/aspcap.egg-info/PKG-INFO +185 -0
  16. aspcap-0.2.0/aspcap.egg-info/SOURCES.txt +39 -0
  17. aspcap-0.2.0/aspcap.egg-info/dependency_links.txt +1 -0
  18. aspcap-0.2.0/aspcap.egg-info/entry_points.txt +2 -0
  19. aspcap-0.2.0/aspcap.egg-info/requires.txt +2 -0
  20. aspcap-0.2.0/aspcap.egg-info/top_level.txt +1 -0
  21. aspcap-0.2.0/docs/conf.py +9 -0
  22. aspcap-0.2.0/docs/continuum.rst +95 -0
  23. aspcap-0.2.0/docs/grid.rst +195 -0
  24. aspcap-0.2.0/docs/index.rst +26 -0
  25. aspcap-0.2.0/docs/inputs.rst +59 -0
  26. aspcap-0.2.0/docs/python-api.rst +57 -0
  27. aspcap-0.2.0/docs/quickstart.rst +46 -0
  28. aspcap-0.2.0/docs/requirements.txt +1 -0
  29. aspcap-0.2.0/docs/spectrum.rst +116 -0
  30. aspcap-0.2.0/examples/apogee-dr17-mgiant-mini.dat +42 -0
  31. aspcap-0.2.0/examples/download_dr17_mgiant.py +112 -0
  32. aspcap-0.2.0/examples/mg.json +6 -0
  33. aspcap-0.2.0/examples/one-star.json +8 -0
  34. aspcap-0.2.0/examples/spectrum_continuum.py +21 -0
  35. aspcap-0.2.0/examples/toy-grid.dat +28 -0
  36. aspcap-0.2.0/examples/toy-spectrum.csv +9 -0
  37. aspcap-0.2.0/pyproject.toml +27 -0
  38. aspcap-0.2.0/setup.cfg +4 -0
  39. aspcap-0.2.0/tests/test_abundances.py +92 -0
  40. aspcap-0.2.0/tests/test_continuum.py +186 -0
  41. aspcap-0.2.0/tests/test_spectrum.py +159 -0
aspcap-0.2.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 David Nidever
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2019 David Nidever
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,4 @@
1
+ include LICENSE-DOPPLER
2
+ include README.md
3
+ recursive-include docs *.rst *.py *.txt
4
+ recursive-include examples *.py *.dat *.csv *.json
aspcap-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,185 @@
1
+ Metadata-Version: 2.4
2
+ Name: aspcap
3
+ Version: 0.2.0
4
+ Summary: Element abundance fits with FERRE
5
+ Author: David Nidever
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 David Nidever
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Documentation, https://aspcap.readthedocs.io/
29
+ Project-URL: Repository, https://github.com/dnidever/aspcap
30
+ Project-URL: Issues, https://github.com/dnidever/aspcap/issues
31
+ Requires-Python: >=3.9
32
+ Description-Content-Type: text/markdown
33
+ License-File: LICENSE
34
+ License-File: LICENSE-DOPPLER
35
+ Requires-Dist: numpy>=1.23
36
+ Requires-Dist: scipy>=1.9
37
+ Dynamic: license-file
38
+
39
+ # aspcap
40
+
41
+ ASPCAP provides multi-order spectral data objects, external continuum
42
+ normalization, and element-abundance fitting with FERRE for HGE.
43
+
44
+ - [Spec1D objects](https://github.com/dnidever/aspcap/blob/main/docs/spectrum.rst): single or multiple orders, unequal chip
45
+ lengths, masks, uncertainties, copying, and native-grid mapping.
46
+ - [External continuum normalization](https://github.com/dnidever/aspcap/blob/main/docs/continuum.rst): polynomial, spline,
47
+ median, percentile, and model-ratio methods with diagnostics.
48
+ - [FERRE abundance fitting](https://github.com/dnidever/aspcap/blob/main/docs/quickstart.rst): fit an element using fixed
49
+ global parameters and a supplied continuum.
50
+
51
+ Install the latest release from PyPI with:
52
+
53
+ ```sh
54
+ python -m pip install --upgrade aspcap
55
+ ```
56
+
57
+ The spectrum and continuum modules do not need a FERRE executable or Doppler
58
+ model files. Install [FERRE](https://github.com/dnidever/ferre) separately when
59
+ running spectral fits: `python -m pip install --upgrade ferre`.
60
+
61
+ ## Try the worked example
62
+
63
+ Install Python 3.9 or later and [FERRE](https://github.com/dnidever/ferre).
64
+ The example was verified with FERRE 5.1.3. From the repository checkout, run:
65
+
66
+ ```sh
67
+ python -m pip install -e .
68
+ aspcap abundances examples/one-star.json --output abundance.csv --pixels pixels.csv
69
+ ```
70
+
71
+ If `ferre` is outside your PATH, add `--ferre /path/to/ferre`. The example
72
+ uses a synthetic spectrum and a toy grid. Expect a raw `ALPHA` coordinate of
73
+ about 0.20 and a reported `[Mg/H]` of about −0.05, using four pixels and fixed
74
+ `METALS = -0.25`. The pixel CSV shows the normalized flux, uncertainty, model,
75
+ and whether each pixel was used. Add `--keep-runs` to inspect FERRE's input
76
+ files after a successful fit. A failed fit keeps its files and reports their
77
+ location.
78
+
79
+ ## Run your own fit
80
+
81
+ 1. Copy [one-star.json](https://github.com/dnidever/aspcap/blob/main/examples/one-star.json) and point it to your inputs.
82
+ Relative paths are resolved from the configuration file. Supply the global
83
+ fit as a dictionary with one starting value per grid label.
84
+ 2. Supply a spectrum CSV with `wavelength,flux,error,continuum` columns. The
85
+ spectrum must already be on the native rest-frame grid in the library's
86
+ wavelength units and air/vacuum convention. Give flux and one-sigma error
87
+ in the same units as the positive global continuum. The wrapper divides
88
+ both by that continuum and holds it fixed during the fit.
89
+ 3. Copy [mg.json](https://github.com/dnidever/aspcap/blob/main/examples/mg.json) and set the species, the one free grid label,
90
+ and one or more inclusive wavelength windows. Overlapping windows still
91
+ give each pixel weight one. Nonfinite flux and nonpositive or nonfinite
92
+ uncertainties get zero weight. Use `add_labels` to list any fixed logarithmic
93
+ coordinates to add to the fitted coordinate to report `[X/H]`. Leave it empty
94
+ if the fitted coordinate is already `[X/H]`. Verify this mapping and the
95
+ wavelength windows against the scientific conventions of your grid.
96
+
97
+ This version supports single-segment, non-PCA FERRE libraries with a positive
98
+ spectral resolution, either formatted ASCII or matching `.hdr` and `.unf`
99
+ files. Point `grid` to the ASCII library or to the binary library's `.hdr`.
100
+ The fit uses linear interpolation. The reported error is conditional on the
101
+ fixed global parameters and continuum. The reduced χ² column comes from FERRE.
102
+
103
+ ## Use from Python
104
+
105
+ ```python
106
+ from aspcap import Grid, fit_abundance
107
+
108
+ grid = Grid.read("examples/toy-grid.dat")
109
+ result, pixels = fit_abundance(
110
+ grid, wavelength, flux, error, continuum,
111
+ {"ALPHA": 0.0, "METALS": -0.25},
112
+ star_id="my-star", species="Mg", label="ALPHA",
113
+ windows=[(15002, 15005)], add_labels=["METALS"],
114
+ )
115
+ ```
116
+
117
+ Pass the four CSV columns as sequences. `result` is a dictionary and `pixels`
118
+ iterates over the diagnostic rows.
119
+
120
+ The FERRE protocol lives in `aspcap/ferre.py`, the abundance logic in
121
+ `aspcap/abundances.py`, and file input and output in `aspcap/cli.py`. Run
122
+ `python -m unittest discover -s tests -v` for the tests. Set
123
+ `ASPCAP_TEST_FERRE=/path/to/ferre` to include the integration test if FERRE
124
+ is outside your PATH.
125
+
126
+ ## Documentation
127
+
128
+ The [user guide](https://github.com/dnidever/aspcap/blob/main/docs/index.rst) includes a quickstart, input reference,
129
+ [Spec1D guide](https://github.com/dnidever/aspcap/blob/main/docs/spectrum.rst), [continuum methods](https://github.com/dnidever/aspcap/blob/main/docs/continuum.rst), and
130
+ Python API. The repository includes Read the Docs build configuration. To
131
+ preview the guide locally after installing ASPCAP:
132
+
133
+ ```sh
134
+ python -m pip install -r docs/requirements.txt
135
+ python -m sphinx -W -b html docs docs/_build/html
136
+ ```
137
+
138
+
139
+ The full-wavelength APOGEE DR17 M-giant test grid and detector weights are
140
+ included in ASPCAP installations.
141
+
142
+ ```sh
143
+ python -m pip install --upgrade aspcap
144
+ ```
145
+
146
+ See the [Grid walkthrough](https://github.com/dnidever/aspcap/blob/main/docs/grid.rst#fit-the-full-apogee-dr17-wavelength-grid)
147
+ for an example using the installed files.
148
+
149
+
150
+ ## Multi-order spectra and external continuum fitting
151
+
152
+ `Spec1D` follows Doppler's `(pixels, orders)` layout, accepts unequal-length
153
+ orders, preserves masks/metadata, and can map detector chips back to a native
154
+ wavelength grid. It imports no Doppler fitting code or model loaders. Existing
155
+ Doppler objects can be used directly with the continuum functions.
156
+
157
+ ```python
158
+ from aspcap import Spec1D, continuum
159
+
160
+ spec = Spec1D(flux_orders, wave=wavelength_orders, err=error_orders)
161
+ result = continuum.fit(spec, method="chebyshev", degree=4)
162
+ normalized = result.apply(spec) # independent copy
163
+ ```
164
+
165
+ Choose the estimator with `continuum.fit(spec, method=...)`:
166
+
167
+ | Method | Useful options | Defaults |
168
+ |---|---|---|
169
+ | `chebyshev` / `polynomial` | `degree`, `weighted`, `clip` | Degree 4, unweighted, no clipping |
170
+ | `spline` | `degree`, `smoothing`, `weighted`, `clip` | Degree 3, noise-based smoothing |
171
+ | `median` | `window`, `mode` | 151 pixels, mirror edges |
172
+ | `percentile` | `percentile`, `bins` | 90th percentile, 20 bins |
173
+ | `model_ratio` | `model`, `window`, `ratio_bounds`, `mode` | Model required; 151 pixels; ratios 0.1–10 |
174
+
175
+ Methods operate independently per order. Use `result.fit_mask`, `result.flags`,
176
+ `result.parameters`, and `result.settings` to inspect the estimate. Invalid
177
+ orders raise an error by default; `on_failure="mask"` returns flagged, masked
178
+ results instead. `result.apply(spec)` returns a copy; `inplace=True` updates the
179
+ input explicitly. All normalization here is outside FERRE. Applying a
180
+ continuum rescales both flux and error; it does not include uncertainty in the
181
+ continuum estimate. See [the spectrum guide](https://github.com/dnidever/aspcap/blob/main/docs/spectrum.rst),
182
+ [the continuum guide](https://github.com/dnidever/aspcap/blob/main/docs/continuum.rst), and the runnable
183
+ [example](https://github.com/dnidever/aspcap/blob/main/examples/spectrum_continuum.py). The core design is adapted from
184
+ [Doppler Spec1D](https://github.com/dnidever/doppler/blob/6e165f87c6d766eff6eaa1fbee2c82be5bb30cc0/doppler/spec1d.py);
185
+ its MIT notice is retained in `LICENSE-DOPPLER`.
aspcap-0.2.0/README.md ADDED
@@ -0,0 +1,147 @@
1
+ # aspcap
2
+
3
+ ASPCAP provides multi-order spectral data objects, external continuum
4
+ normalization, and element-abundance fitting with FERRE for HGE.
5
+
6
+ - [Spec1D objects](https://github.com/dnidever/aspcap/blob/main/docs/spectrum.rst): single or multiple orders, unequal chip
7
+ lengths, masks, uncertainties, copying, and native-grid mapping.
8
+ - [External continuum normalization](https://github.com/dnidever/aspcap/blob/main/docs/continuum.rst): polynomial, spline,
9
+ median, percentile, and model-ratio methods with diagnostics.
10
+ - [FERRE abundance fitting](https://github.com/dnidever/aspcap/blob/main/docs/quickstart.rst): fit an element using fixed
11
+ global parameters and a supplied continuum.
12
+
13
+ Install the latest release from PyPI with:
14
+
15
+ ```sh
16
+ python -m pip install --upgrade aspcap
17
+ ```
18
+
19
+ The spectrum and continuum modules do not need a FERRE executable or Doppler
20
+ model files. Install [FERRE](https://github.com/dnidever/ferre) separately when
21
+ running spectral fits: `python -m pip install --upgrade ferre`.
22
+
23
+ ## Try the worked example
24
+
25
+ Install Python 3.9 or later and [FERRE](https://github.com/dnidever/ferre).
26
+ The example was verified with FERRE 5.1.3. From the repository checkout, run:
27
+
28
+ ```sh
29
+ python -m pip install -e .
30
+ aspcap abundances examples/one-star.json --output abundance.csv --pixels pixels.csv
31
+ ```
32
+
33
+ If `ferre` is outside your PATH, add `--ferre /path/to/ferre`. The example
34
+ uses a synthetic spectrum and a toy grid. Expect a raw `ALPHA` coordinate of
35
+ about 0.20 and a reported `[Mg/H]` of about −0.05, using four pixels and fixed
36
+ `METALS = -0.25`. The pixel CSV shows the normalized flux, uncertainty, model,
37
+ and whether each pixel was used. Add `--keep-runs` to inspect FERRE's input
38
+ files after a successful fit. A failed fit keeps its files and reports their
39
+ location.
40
+
41
+ ## Run your own fit
42
+
43
+ 1. Copy [one-star.json](https://github.com/dnidever/aspcap/blob/main/examples/one-star.json) and point it to your inputs.
44
+ Relative paths are resolved from the configuration file. Supply the global
45
+ fit as a dictionary with one starting value per grid label.
46
+ 2. Supply a spectrum CSV with `wavelength,flux,error,continuum` columns. The
47
+ spectrum must already be on the native rest-frame grid in the library's
48
+ wavelength units and air/vacuum convention. Give flux and one-sigma error
49
+ in the same units as the positive global continuum. The wrapper divides
50
+ both by that continuum and holds it fixed during the fit.
51
+ 3. Copy [mg.json](https://github.com/dnidever/aspcap/blob/main/examples/mg.json) and set the species, the one free grid label,
52
+ and one or more inclusive wavelength windows. Overlapping windows still
53
+ give each pixel weight one. Nonfinite flux and nonpositive or nonfinite
54
+ uncertainties get zero weight. Use `add_labels` to list any fixed logarithmic
55
+ coordinates to add to the fitted coordinate to report `[X/H]`. Leave it empty
56
+ if the fitted coordinate is already `[X/H]`. Verify this mapping and the
57
+ wavelength windows against the scientific conventions of your grid.
58
+
59
+ This version supports single-segment, non-PCA FERRE libraries with a positive
60
+ spectral resolution, either formatted ASCII or matching `.hdr` and `.unf`
61
+ files. Point `grid` to the ASCII library or to the binary library's `.hdr`.
62
+ The fit uses linear interpolation. The reported error is conditional on the
63
+ fixed global parameters and continuum. The reduced χ² column comes from FERRE.
64
+
65
+ ## Use from Python
66
+
67
+ ```python
68
+ from aspcap import Grid, fit_abundance
69
+
70
+ grid = Grid.read("examples/toy-grid.dat")
71
+ result, pixels = fit_abundance(
72
+ grid, wavelength, flux, error, continuum,
73
+ {"ALPHA": 0.0, "METALS": -0.25},
74
+ star_id="my-star", species="Mg", label="ALPHA",
75
+ windows=[(15002, 15005)], add_labels=["METALS"],
76
+ )
77
+ ```
78
+
79
+ Pass the four CSV columns as sequences. `result` is a dictionary and `pixels`
80
+ iterates over the diagnostic rows.
81
+
82
+ The FERRE protocol lives in `aspcap/ferre.py`, the abundance logic in
83
+ `aspcap/abundances.py`, and file input and output in `aspcap/cli.py`. Run
84
+ `python -m unittest discover -s tests -v` for the tests. Set
85
+ `ASPCAP_TEST_FERRE=/path/to/ferre` to include the integration test if FERRE
86
+ is outside your PATH.
87
+
88
+ ## Documentation
89
+
90
+ The [user guide](https://github.com/dnidever/aspcap/blob/main/docs/index.rst) includes a quickstart, input reference,
91
+ [Spec1D guide](https://github.com/dnidever/aspcap/blob/main/docs/spectrum.rst), [continuum methods](https://github.com/dnidever/aspcap/blob/main/docs/continuum.rst), and
92
+ Python API. The repository includes Read the Docs build configuration. To
93
+ preview the guide locally after installing ASPCAP:
94
+
95
+ ```sh
96
+ python -m pip install -r docs/requirements.txt
97
+ python -m sphinx -W -b html docs docs/_build/html
98
+ ```
99
+
100
+
101
+ The full-wavelength APOGEE DR17 M-giant test grid and detector weights are
102
+ included in ASPCAP installations.
103
+
104
+ ```sh
105
+ python -m pip install --upgrade aspcap
106
+ ```
107
+
108
+ See the [Grid walkthrough](https://github.com/dnidever/aspcap/blob/main/docs/grid.rst#fit-the-full-apogee-dr17-wavelength-grid)
109
+ for an example using the installed files.
110
+
111
+
112
+ ## Multi-order spectra and external continuum fitting
113
+
114
+ `Spec1D` follows Doppler's `(pixels, orders)` layout, accepts unequal-length
115
+ orders, preserves masks/metadata, and can map detector chips back to a native
116
+ wavelength grid. It imports no Doppler fitting code or model loaders. Existing
117
+ Doppler objects can be used directly with the continuum functions.
118
+
119
+ ```python
120
+ from aspcap import Spec1D, continuum
121
+
122
+ spec = Spec1D(flux_orders, wave=wavelength_orders, err=error_orders)
123
+ result = continuum.fit(spec, method="chebyshev", degree=4)
124
+ normalized = result.apply(spec) # independent copy
125
+ ```
126
+
127
+ Choose the estimator with `continuum.fit(spec, method=...)`:
128
+
129
+ | Method | Useful options | Defaults |
130
+ |---|---|---|
131
+ | `chebyshev` / `polynomial` | `degree`, `weighted`, `clip` | Degree 4, unweighted, no clipping |
132
+ | `spline` | `degree`, `smoothing`, `weighted`, `clip` | Degree 3, noise-based smoothing |
133
+ | `median` | `window`, `mode` | 151 pixels, mirror edges |
134
+ | `percentile` | `percentile`, `bins` | 90th percentile, 20 bins |
135
+ | `model_ratio` | `model`, `window`, `ratio_bounds`, `mode` | Model required; 151 pixels; ratios 0.1–10 |
136
+
137
+ Methods operate independently per order. Use `result.fit_mask`, `result.flags`,
138
+ `result.parameters`, and `result.settings` to inspect the estimate. Invalid
139
+ orders raise an error by default; `on_failure="mask"` returns flagged, masked
140
+ results instead. `result.apply(spec)` returns a copy; `inplace=True` updates the
141
+ input explicitly. All normalization here is outside FERRE. Applying a
142
+ continuum rescales both flux and error; it does not include uncertainty in the
143
+ continuum estimate. See [the spectrum guide](https://github.com/dnidever/aspcap/blob/main/docs/spectrum.rst),
144
+ [the continuum guide](https://github.com/dnidever/aspcap/blob/main/docs/continuum.rst), and the runnable
145
+ [example](https://github.com/dnidever/aspcap/blob/main/examples/spectrum_continuum.py). The core design is adapted from
146
+ [Doppler Spec1D](https://github.com/dnidever/doppler/blob/6e165f87c6d766eff6eaa1fbee2c82be5bb30cc0/doppler/spec1d.py);
147
+ its MIT notice is retained in `LICENSE-DOPPLER`.
@@ -0,0 +1,8 @@
1
+ """Spectral data, external continuum normalization, and FERRE abundance fits."""
2
+
3
+ from .abundances import fit_abundance
4
+ from .ferre import Grid
5
+ from .spectrum import Spec1D
6
+ from . import continuum
7
+
8
+ __all__ = ["Grid", "fit_abundance", "Spec1D", "continuum"]
@@ -0,0 +1,73 @@
1
+ """Fit an individual species using the continuum and labels of a global fit."""
2
+
3
+ from math import isclose, isfinite
4
+ import numpy as np
5
+ from .ferre import run_ferre
6
+
7
+
8
+ def fit_abundance(grid, wavelength, flux, error, continuum, parameters, *,
9
+ star_id, species, label, windows, add_labels=(),
10
+ executable="ferre", keep_runs=False, run_root=None, mask=None):
11
+ """Return a result row and a model on the native grid.
12
+
13
+ Inputs are in the grid's wavelength units and air/vacuum convention.
14
+ The optional mask uses True for unusable pixels.
15
+ Windows are inclusive (lower, upper) pairs with unit weights. The optional
16
+ add_labels are fixed global-fit coordinates added to report [X/H].
17
+ """
18
+ npix = len(grid.wavelengths)
19
+ badmask = np.zeros(npix, bool) if mask is None else np.asarray(mask, dtype=bool)
20
+ if badmask.shape != (npix,):
21
+ raise ValueError("mask must have one Boolean value per grid pixel (True means bad)")
22
+ if any(len(values) != npix for values in (wavelength, flux, error, continuum)):
23
+ raise ValueError("Spectrum and continuum must have one row per grid pixel")
24
+ if not all(isclose(a, b, rel_tol=0, abs_tol=1e-4 * (grid.wavelengths[1] - grid.wavelengths[0]))
25
+ for a, b in zip(wavelength, grid.wavelengths)):
26
+ raise ValueError("Spectrum wavelengths do not match the native grid")
27
+ if label not in grid.labels:
28
+ raise ValueError(f"Unknown abundance label {label!r}. Available: {grid.labels}")
29
+ if (label in add_labels or len(set(add_labels)) != len(add_labels)
30
+ or any(name not in parameters or name not in grid.labels for name in add_labels)):
31
+ raise ValueError("Abundance conversion labels must be distinct fixed grid labels")
32
+ if not windows or any(not (isfinite(lo) and isfinite(hi) and lo <= hi)
33
+ for lo, hi in windows):
34
+ raise ValueError("Provide one or more finite wavelength windows (lower, upper)")
35
+
36
+ normalized_flux, normalized_error, weights = [], [], []
37
+ for wave, value, sigma, scale, bad in zip(wavelength, flux, error, continuum, badmask):
38
+ if not isfinite(scale) or scale <= 0:
39
+ raise ValueError("Global continuum values must be finite and positive")
40
+ value, sigma = value / scale, sigma / scale
41
+ good = not bad and isfinite(value) and isfinite(sigma) and sigma > 0
42
+ normalized_flux.append(value if good else 1.0)
43
+ normalized_error.append(sigma if good else 1e10)
44
+ weights.append(int(good and any(lo <= wave <= hi for lo, hi in windows)))
45
+ pixels_used = sum(weights)
46
+ if pixels_used < 2:
47
+ raise ValueError(f"{species}: fewer than two usable pixels in the element windows")
48
+
49
+ fit = run_ferre(grid, normalized_flux, normalized_error, parameters, label, weights,
50
+ executable=executable, keep_runs=keep_runs, run_root=run_root)
51
+ axis = grid.labels.index(label)
52
+ raw = fit.parameters[label]
53
+ low, step = grid.lower[axis], grid.step[axis]
54
+ high = low + (grid.counts[axis] - 1) * step
55
+ flags = []
56
+ if min(raw - low, high - raw) <= 0.125 * step:
57
+ flags.append("GRID_EDGE")
58
+ if fit.errors[label] is None:
59
+ flags.append("NO_UNCERTAINTY")
60
+ result = {
61
+ "star_id": star_id,
62
+ "species": species,
63
+ "grid_label": label,
64
+ "raw_value": raw,
65
+ "abundance_scale": f"[{species}/H]",
66
+ "abundance": raw + sum(parameters[name] for name in add_labels),
67
+ "conditional_error": fit.errors[label],
68
+ "ferre_reduced_chi2": fit.reduced_chi2,
69
+ "pixels_used": pixels_used,
70
+ "flags": "|".join(flags),
71
+ }
72
+ pixels = zip(wavelength, normalized_flux, normalized_error, fit.model, weights)
73
+ return result, pixels
@@ -0,0 +1,64 @@
1
+ """Command-line interface for a single HGE abundance fit."""
2
+
3
+ import argparse
4
+ import csv
5
+ import json
6
+ from pathlib import Path
7
+
8
+ from . import Grid, fit_abundance
9
+
10
+
11
+ def _run(args):
12
+ path = args.config.resolve()
13
+ config = json.loads(path.read_text())
14
+ species = json.loads((path.parent / config["species_file"]).read_text())
15
+ grid = Grid.read(path.parent / config["grid"])
16
+ expected_frame = "vacuum" if grid.vacuum else "air"
17
+ if config["wavelength_frame"] != expected_frame:
18
+ raise ValueError(f"Grid uses {expected_frame} wavelengths")
19
+ with (path.parent / config["spectrum"]).open(newline="") as file:
20
+ rows = list(csv.DictReader(file))
21
+ columns = [[float(row[name]) for row in rows]
22
+ for name in ("wavelength", "flux", "error", "continuum")]
23
+ executable = args.ferre or config.get("ferre", "ferre")
24
+ if not args.ferre and "/" in executable:
25
+ executable = path.parent / executable
26
+ result, pixels = fit_abundance(
27
+ grid, *columns, config["parameters"], star_id=config["star_id"],
28
+ species=species["species"], label=species["label"], windows=species["windows"],
29
+ add_labels=species.get("add_labels", ()), executable=executable,
30
+ keep_runs=args.keep_runs, run_root=args.run_root,
31
+ )
32
+ if args.pixels:
33
+ with args.pixels.open("w", newline="") as file:
34
+ writer = csv.writer(file)
35
+ writer.writerow(("wavelength", "normalized_flux", "normalized_error", "model", "weight"))
36
+ writer.writerows(pixels)
37
+ with args.output.open("w", newline="") as file:
38
+ writer = csv.DictWriter(file, fieldnames=result)
39
+ writer.writeheader()
40
+ writer.writerow(result)
41
+ print(f"Wrote {args.output}")
42
+
43
+
44
+ def main():
45
+ parser = argparse.ArgumentParser(prog="aspcap", description=__doc__)
46
+ commands = parser.add_subparsers(dest="command", required=True)
47
+ abundances = commands.add_parser("abundances", help="fit an individual species")
48
+ abundances.add_argument("config", type=Path)
49
+ abundances.add_argument("--ferre", help="FERRE executable name or path")
50
+ abundances.add_argument("--output", type=Path, default=Path("abundance.csv"))
51
+ abundances.add_argument("--pixels", type=Path, help="save normalized data, model, and weights")
52
+ abundances.add_argument("--keep-runs", action="store_true")
53
+ abundances.add_argument("--run-root", type=Path, help="existing directory for FERRE run files")
54
+ args = parser.parse_args()
55
+ try:
56
+ if args.pixels and args.pixels.resolve() == args.output.resolve():
57
+ raise ValueError("--pixels and --output must use different files")
58
+ _run(args)
59
+ except (OSError, ValueError, KeyError, TypeError, RuntimeError) as error:
60
+ parser.exit(1, f"aspcap: {error}\n")
61
+
62
+
63
+ if __name__ == "__main__":
64
+ main()