solarc-eclipse 0.6.2__tar.gz → 0.7.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 (38) hide show
  1. {solarc_eclipse-0.6.2/solarc_eclipse.egg-info → solarc_eclipse-0.7.0}/PKG-INFO +65 -34
  2. {solarc_eclipse-0.6.2 → solarc_eclipse-0.7.0}/README.md +56 -30
  3. {solarc_eclipse-0.6.2 → solarc_eclipse-0.7.0}/euvst_response/__init__.py +5 -4
  4. {solarc_eclipse-0.6.2 → solarc_eclipse-0.7.0}/euvst_response/analysis.py +66 -27
  5. {solarc_eclipse-0.6.2 → solarc_eclipse-0.7.0}/euvst_response/data_processing.py +17 -10
  6. solarc_eclipse-0.7.0/euvst_response/extern/__init__.py +0 -0
  7. solarc_eclipse-0.7.0/euvst_response/extern/mpfit.py +2388 -0
  8. solarc_eclipse-0.7.0/euvst_response/fitting.py +639 -0
  9. {solarc_eclipse-0.6.2 → solarc_eclipse-0.7.0}/euvst_response/main.py +234 -83
  10. solarc_eclipse-0.7.0/euvst_response/monte_carlo.py +299 -0
  11. {solarc_eclipse-0.6.2 → solarc_eclipse-0.7.0}/euvst_response/radiometric.py +53 -22
  12. {solarc_eclipse-0.6.2 → solarc_eclipse-0.7.0}/euvst_response/synthesis.py +182 -56
  13. {solarc_eclipse-0.6.2 → solarc_eclipse-0.7.0}/euvst_response/utils.py +113 -14
  14. {solarc_eclipse-0.6.2 → solarc_eclipse-0.7.0}/pyproject.toml +9 -3
  15. {solarc_eclipse-0.6.2 → solarc_eclipse-0.7.0}/setup.py +2 -27
  16. {solarc_eclipse-0.6.2 → solarc_eclipse-0.7.0/solarc_eclipse.egg-info}/PKG-INFO +65 -34
  17. {solarc_eclipse-0.6.2 → solarc_eclipse-0.7.0}/solarc_eclipse.egg-info/SOURCES.txt +2 -0
  18. {solarc_eclipse-0.6.2 → solarc_eclipse-0.7.0}/solarc_eclipse.egg-info/requires.txt +8 -0
  19. solarc_eclipse-0.6.2/euvst_response/fitting.py +0 -144
  20. solarc_eclipse-0.6.2/euvst_response/monte_carlo.py +0 -207
  21. {solarc_eclipse-0.6.2 → solarc_eclipse-0.7.0}/LICENSE +0 -0
  22. {solarc_eclipse-0.6.2 → solarc_eclipse-0.7.0}/MANIFEST.in +0 -0
  23. {solarc_eclipse-0.6.2 → solarc_eclipse-0.7.0}/euvst_response/cli.py +0 -0
  24. {solarc_eclipse-0.6.2 → solarc_eclipse-0.7.0}/euvst_response/config.py +0 -0
  25. {solarc_eclipse-0.6.2 → solarc_eclipse-0.7.0}/euvst_response/data/throughput/grating_reflection_efficiency.dat +0 -0
  26. {solarc_eclipse-0.6.2 → solarc_eclipse-0.7.0}/euvst_response/data/throughput/primary_mirror_coating_reflectance.dat +0 -0
  27. {solarc_eclipse-0.6.2 → solarc_eclipse-0.7.0}/euvst_response/data/throughput/source.txt +0 -0
  28. {solarc_eclipse-0.6.2 → solarc_eclipse-0.7.0}/euvst_response/data/throughput/throughput_aluminium_1000_angstrom.dat +0 -0
  29. {solarc_eclipse-0.6.2 → solarc_eclipse-0.7.0}/euvst_response/data/throughput/throughput_aluminium_oxide_1000_angstrom.dat +0 -0
  30. {solarc_eclipse-0.6.2 → solarc_eclipse-0.7.0}/euvst_response/data/throughput/throughput_carbon_1000_angstrom.dat +0 -0
  31. {solarc_eclipse-0.6.2 → solarc_eclipse-0.7.0}/euvst_response/pinhole_diffraction.py +0 -0
  32. {solarc_eclipse-0.6.2 → solarc_eclipse-0.7.0}/euvst_response/psf.py +0 -0
  33. {solarc_eclipse-0.6.2 → solarc_eclipse-0.7.0}/euvst_response/synthesis_cli.py +0 -0
  34. {solarc_eclipse-0.6.2 → solarc_eclipse-0.7.0}/setup.cfg +0 -0
  35. {solarc_eclipse-0.6.2 → solarc_eclipse-0.7.0}/solarc_eclipse.egg-info/dependency_links.txt +0 -0
  36. {solarc_eclipse-0.6.2 → solarc_eclipse-0.7.0}/solarc_eclipse.egg-info/entry_points.txt +0 -0
  37. {solarc_eclipse-0.6.2 → solarc_eclipse-0.7.0}/solarc_eclipse.egg-info/not-zip-safe +0 -0
  38. {solarc_eclipse-0.6.2 → solarc_eclipse-0.7.0}/solarc_eclipse.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: solarc-eclipse
3
- Version: 0.6.2
3
+ Version: 0.7.0
4
4
  Summary: ECLIPSE: Emission Calculation and Line Prediction for SOLAR-C EUVST
5
5
  Home-page: https://github.com/jamesmckevitt/eclipse
6
6
  Author: James McKevitt
@@ -12,11 +12,9 @@ Classifier: Development Status :: 4 - Beta
12
12
  Classifier: Intended Audience :: Science/Research
13
13
  Classifier: Topic :: Scientific/Engineering :: Astronomy
14
14
  Classifier: Programming Language :: Python :: 3
15
- Classifier: Programming Language :: Python :: 3.8
16
- Classifier: Programming Language :: Python :: 3.9
17
15
  Classifier: Programming Language :: Python :: 3.10
18
16
  Classifier: Programming Language :: Python :: 3.11
19
- Requires-Python: >=3.8
17
+ Requires-Python: >=3.10
20
18
  Description-Content-Type: text/markdown
21
19
  License-File: LICENSE
22
20
  Requires-Dist: numpy
@@ -31,6 +29,13 @@ Requires-Dist: dill
31
29
  Requires-Dist: pyyaml
32
30
  Requires-Dist: reproject
33
31
  Requires-Dist: sunpy[all]
32
+ Requires-Dist: dask
33
+ Requires-Dist: psutil
34
+ Requires-Dist: mendeleev
35
+ Requires-Dist: h5py
36
+ Requires-Dist: fiasco
37
+ Provides-Extra: mpi
38
+ Requires-Dist: mpi4py; extra == "mpi"
34
39
  Provides-Extra: dev
35
40
  Requires-Dist: pytest; extra == "dev"
36
41
  Requires-Dist: black; extra == "dev"
@@ -73,7 +78,7 @@ After installation, you can run ECLIPSE from the command line:
73
78
 
74
79
  ```bash
75
80
  # Run synthesis script (convert 3D MHD data to synthetic spectra)
76
- synthesise-spectra --data-dir ./data/atmosphere --goft-file ./data/gofnt.sav --output-dir ./run/input
81
+ synthesise-spectra --data-dir ./data/atmosphere --lines Fe12_195.1190 --output-dir ./run/input
77
82
 
78
83
  # Run instrument response simulation
79
84
  eclipse --config ./run/input/config.yaml
@@ -98,7 +103,7 @@ detector = Detector_SWC()
98
103
  print(f"Telescope collecting area: {telescope.collecting_area:.4f}")
99
104
  print(f"Detector QE (EUV): {detector.qe_euv:.2f}")
100
105
 
101
- # Calculate effective area at Fe XII 195.119 Å
106
+ # Calculate effective area at Fe XII 195.119 Angstrom
102
107
  fe12_wl = 195.119 * u.AA
103
108
  effective_area = telescope.collecting_area * telescope.throughput(fe12_wl) * detector.qe_euv
104
109
 
@@ -141,18 +146,9 @@ combo = get_results_for_combination(results, **{"simulation.expos": 40*u.s, "sim
141
146
 
142
147
  ## Detailed instructions
143
148
 
144
- ### 1. Generate contribution functions for the desired emission lines
149
+ ### 1. Run the line synthesis
145
150
 
146
- Edit `make_gofnt.pro` to specify the desired emission lines and the location of the CHIANTI files. You can use CHIANTI to identify the required lines.
147
-
148
- Run the following command:
149
- ```bash
150
- idl -e "make_goft"
151
- ```
152
-
153
- ### 2. Run the line synthesis
154
-
155
- The synthesis script converts 3D MHD simulation data into synthetic solar spectra. It can be run directly from the command line with extensive configuration options.
151
+ The synthesis script converts 3D MHD simulation data into synthetic solar spectra. Contribution functions G(T, n_e) are computed on-the-fly using [fiasco](https://fiasco.readthedocs.io/) (a Python interface to the CHIANTI atomic database).
156
152
 
157
153
  #### Basic Usage
158
154
 
@@ -160,7 +156,9 @@ The synthesis script converts 3D MHD simulation data into synthetic solar spectr
160
156
  # Example using all available command line options
161
157
  synthesise-spectra \
162
158
  --data-dir ./data/atmosphere \
163
- --goft-file ./data/gofnt.sav \
159
+ --lines Fe12_195.1190 Fe12_195.1790 \
160
+ --abundance sun_coronal_2021_chianti \
161
+ --n-workers 4 \
164
162
  --output-dir ./run/input \
165
163
  --output-name synthesised_spectra.pkl \
166
164
  --temp-file temp/eosT.0270000 \
@@ -180,8 +178,7 @@ synthesise-spectra \
180
178
  --crop-z "0 Mm" "20 Mm" \
181
179
  --downsample 1 \
182
180
  --precision float64 \
183
- --mean-mol-wt 1.29 \
184
- --limit-lines Fe12_195.1190
181
+ --mean-mol-wt 1.29
185
182
 
186
183
  # Show all available options
187
184
  synthesise-spectra --help
@@ -191,10 +188,14 @@ synthesise-spectra --help
191
188
 
192
189
  **Input/Output Paths:**
193
190
  - `--data-dir`: Directory containing simulation data (default: `data/atmosphere`)
194
- - `--goft-file`: Path to CHIANTI G(T,N) save file (default: `./data/gofnt.sav`)
195
191
  - `--output-dir`: Output directory for results (default: `./run/input`)
196
192
  - `--output-name`: Output filename (default: `synthesised_spectra.pkl`)
197
193
 
194
+ **Line and Abundance Selection:**
195
+ - `--lines`: Emission lines to synthesise, e.g., `--lines Fe12_195.1190 Fe12_195.1790` (required)
196
+ - `--abundance`: CHIANTI abundance dataset name (default: `sun_coronal_2021_chianti`)
197
+ - `--n-workers`: Number of parallel workers for the fiasco G(T, n_e) computation. Each distinct ion is computed in a separate process. `0` uses all available CPUs (default: `0`). Set to `1` for serial execution.
198
+
198
199
  **Simulation Files:**
199
200
  - `--temp-file`: Temperature file relative to data-dir (default: `temp/eosT.0270000`)
200
201
  - `--rho-file`: Density file relative to data-dir (default: `rho/result_prim_0.0270000`)
@@ -227,9 +228,6 @@ synthesise-spectra --help
227
228
  - `--precision`: Numerical precision `float32` or `float64` (default: `float64`)
228
229
  - `--mean-mol-wt`: Mean molecular weight (default: `1.29`)
229
230
 
230
- **Line Selection:**
231
- - `--limit-lines`: Limit to specific lines, e.g., `--limit-lines Fe12_195.1190 Fe12_195.1790`
232
-
233
231
  #### Dynamic Mode (Time-varying Atmospheres)
234
232
 
235
233
  For simulating raster scans over evolving atmospheres, use dynamic mode which combines MHD timesteps based on instrument scanning:
@@ -237,7 +235,8 @@ For simulating raster scans over evolving atmospheres, use dynamic mode which co
237
235
  ```bash
238
236
  synthesise-spectra \
239
237
  --data-dir ./data/atmosphere \
240
- --goft-file ./data/gofnt.sav \
238
+ --lines Fe12_195.1190 \
239
+ --abundance sun_coronal_2021_chianti \
241
240
  --output-dir ./run/input \
242
241
  --slit-rest-time "40 s" \
243
242
  --slit-width "0.2 arcsec" \
@@ -275,7 +274,6 @@ The synthesis produces a pickle file containing:
275
274
 
276
275
  - Use `--downsample 2` or `--downsample 4` for initial testing
277
276
  - Use `--precision float32` to reduce memory usage (may affect accuracy)
278
- - Use `--limit-lines` to synthesise only specific lines for development
279
277
  - Use spatial cropping to focus on regions of interest and reduce computation time
280
278
  - Monitor memory usage - full resolution synthesis can require 50+ GB RAM
281
279
  - Side views (`--integration-axis x` or `y`) may require different velocity files
@@ -288,7 +286,7 @@ The synthesis results can be loaded and analyzed using the package API:
288
286
  import euvst_response
289
287
 
290
288
  # Load synthesis results - this sums all line cubes into a single cube
291
- # By default uses Fe XII 195.119 Å as reference for wavelength grid
289
+ # By default uses Fe XII 195.119 Angstrom as reference for wavelength grid
292
290
  cube = euvst_response.load_atmosphere("./run/input/synthesised_spectra.pkl")
293
291
  print(f"Combined cube shape: {cube.data.shape}")
294
292
 
@@ -306,13 +304,7 @@ print(f"Rest wavelength: {fe12_195.meta['rest_wav']}")
306
304
  print(f"Available spectral lines: {list(data['line_cubes'].keys())}")
307
305
  ```
308
306
 
309
- #### Pre-computed Atmospheres
310
-
311
- This step can require a lot of memory at full resolution. A fully synthesised atmosphere using the Cheung et al. (2018) atmosphere (doi:10.1038/s41550-018-0629-3) for the Fe XII 195.119 and 195.179 lines, including 5 background lines from each side, can be downloaded here: https://liveuclac-my.sharepoint.com/:f:/g/personal/ucasjem_ucl_ac_uk/Es-ts6rwXIlInAweGI7hmdMB5BoGqv9uSpIXOvMkzhS3cw?e=54si7R
312
-
313
- **Important:** You can place the synthesised atmosphere file anywhere and specify its location using the `synthesis_file` parameter in your YAML configuration file. The default location is `./run/input/synthesised_spectra.pkl`.
314
-
315
- ### 3. Simulate the instrument response
307
+ ### 2. Simulate the instrument response
316
308
 
317
309
  #### Configuration File
318
310
 
@@ -328,6 +320,7 @@ simulation runs every combination (cartesian product).
328
320
  - `reference_line`: spectral line used as the wavelength-grid reference (default `Fe12_195.1190`)
329
321
  - `n_iter`: number of Monte Carlo iterations
330
322
  - `ncpu`: CPU cores to use (`-1` = all available)
323
+ - `offchip_bin_slit`: off-chip slit binning factor (default `1`)
331
324
  - `pinhole_sizes`, `pinhole_positions`: fixed paired lists for pinhole diffraction tests (SWC only)
332
325
  - `uniform_intensity`, `rest_wavelength`, `thermal_width`: uniform-intensity mode (alternative to synthesis file)
333
326
 
@@ -342,6 +335,7 @@ reference_line: Fe12_195.1190
342
335
  # Global settings (apply to all combinations)
343
336
  n_iter: 500
344
337
  ncpu: -1
338
+ offchip_bin_slit: [1, 2] # sweep no-binning and 2-pixel off-chip binning
345
339
 
346
340
  # Simulation parameters
347
341
  # Any field listed as a list is swept over; all combinations are run.
@@ -380,6 +374,37 @@ detector:
380
374
 
381
375
  For guidance on recommended values, see McKevitt et al. (2025) (in prep.).
382
376
 
377
+ By default, both the DN and photon signals are fitted at every Monte Carlo iteration. To speed up the simulation when only one is needed, use the `fit_signals` option:
378
+
379
+ ```yaml
380
+ fit_signals: dn # Fit only the DN signal
381
+ fit_signals: photon # Fit only the photon signal
382
+ fit_signals: both # Fit both (default)
383
+ ```
384
+
385
+ To fit blended spectral lines with multiple Gaussian components, add a `fitting` block:
386
+
387
+ ```yaml
388
+ fitting:
389
+ primary_component: 0 # index of the component whose velocity is reported
390
+ constrain_positive_intensity: true # reject fits with negative amplitudes
391
+ backend: scipy # optimiser: "scipy" (default) or "mpfit"
392
+ components:
393
+ - wavelength: 195.119 angstrom # component 0: free centre, width, amplitude
394
+ - wavelength: 195.179 angstrom # component 1: centre & width tied to component 0
395
+ tie_center: 0
396
+ tie_width: 0
397
+ ```
398
+
399
+ Each component requires a `wavelength` field giving its rest wavelength.
400
+
401
+ Each entry in `components` corresponds to one Gaussian. Optional per-component keys:
402
+ - `tie_center: <i>`: constrain this component's centre to match component *i*
403
+ - `tie_width: <i>`: constrain this component's line width to match component *i*
404
+ - `amplitude_greater_than: <i>`: constrain amplitude to exceed that of component *i*
405
+
406
+ Omitting the `fitting` block fits a single Gaussian (default behaviour).
407
+
383
408
  If you synthesised data in dynamic mode, your configuration must specify:
384
409
  - Exactly one slit width matching the synthesis slit width
385
410
  - Exactly one exposure time matching the synthesis exposure time
@@ -395,6 +420,12 @@ eclipse --config ./run/input/config.yaml
395
420
  - `--config`: Path to YAML configuration file (required)
396
421
  - `--debug`: Enable debug mode with IPython breakpoints on errors (optional)
397
422
 
423
+ #### Multi-node MPI parallelisation
424
+
425
+ When launched with multiple MPI ranks on a SLURM cluster (via `srun` or `mpirun`, and setting `--ntasks-per-node`), ECLIPSE automatically distributes Monte Carlo iterations across ranks and gathers results on rank 0. No code or configuration changes are needed - MPI is auto-detected at runtime. If `mpi4py` is not installed or only one rank is present, the code falls back to single-process mode.
426
+
427
+ Requirements: `mpi4py` and `intel-mpi` (load with `module load intel-mpi` before launching).
428
+
398
429
  #### Output
399
430
 
400
431
  Results are saved as pickle files in the `run/result/` directory with the same base name as the configuration file. The output includes:
@@ -30,7 +30,7 @@ After installation, you can run ECLIPSE from the command line:
30
30
 
31
31
  ```bash
32
32
  # Run synthesis script (convert 3D MHD data to synthetic spectra)
33
- synthesise-spectra --data-dir ./data/atmosphere --goft-file ./data/gofnt.sav --output-dir ./run/input
33
+ synthesise-spectra --data-dir ./data/atmosphere --lines Fe12_195.1190 --output-dir ./run/input
34
34
 
35
35
  # Run instrument response simulation
36
36
  eclipse --config ./run/input/config.yaml
@@ -55,7 +55,7 @@ detector = Detector_SWC()
55
55
  print(f"Telescope collecting area: {telescope.collecting_area:.4f}")
56
56
  print(f"Detector QE (EUV): {detector.qe_euv:.2f}")
57
57
 
58
- # Calculate effective area at Fe XII 195.119 Å
58
+ # Calculate effective area at Fe XII 195.119 Angstrom
59
59
  fe12_wl = 195.119 * u.AA
60
60
  effective_area = telescope.collecting_area * telescope.throughput(fe12_wl) * detector.qe_euv
61
61
 
@@ -98,18 +98,9 @@ combo = get_results_for_combination(results, **{"simulation.expos": 40*u.s, "sim
98
98
 
99
99
  ## Detailed instructions
100
100
 
101
- ### 1. Generate contribution functions for the desired emission lines
101
+ ### 1. Run the line synthesis
102
102
 
103
- Edit `make_gofnt.pro` to specify the desired emission lines and the location of the CHIANTI files. You can use CHIANTI to identify the required lines.
104
-
105
- Run the following command:
106
- ```bash
107
- idl -e "make_goft"
108
- ```
109
-
110
- ### 2. Run the line synthesis
111
-
112
- The synthesis script converts 3D MHD simulation data into synthetic solar spectra. It can be run directly from the command line with extensive configuration options.
103
+ The synthesis script converts 3D MHD simulation data into synthetic solar spectra. Contribution functions G(T, n_e) are computed on-the-fly using [fiasco](https://fiasco.readthedocs.io/) (a Python interface to the CHIANTI atomic database).
113
104
 
114
105
  #### Basic Usage
115
106
 
@@ -117,7 +108,9 @@ The synthesis script converts 3D MHD simulation data into synthetic solar spectr
117
108
  # Example using all available command line options
118
109
  synthesise-spectra \
119
110
  --data-dir ./data/atmosphere \
120
- --goft-file ./data/gofnt.sav \
111
+ --lines Fe12_195.1190 Fe12_195.1790 \
112
+ --abundance sun_coronal_2021_chianti \
113
+ --n-workers 4 \
121
114
  --output-dir ./run/input \
122
115
  --output-name synthesised_spectra.pkl \
123
116
  --temp-file temp/eosT.0270000 \
@@ -137,8 +130,7 @@ synthesise-spectra \
137
130
  --crop-z "0 Mm" "20 Mm" \
138
131
  --downsample 1 \
139
132
  --precision float64 \
140
- --mean-mol-wt 1.29 \
141
- --limit-lines Fe12_195.1190
133
+ --mean-mol-wt 1.29
142
134
 
143
135
  # Show all available options
144
136
  synthesise-spectra --help
@@ -148,10 +140,14 @@ synthesise-spectra --help
148
140
 
149
141
  **Input/Output Paths:**
150
142
  - `--data-dir`: Directory containing simulation data (default: `data/atmosphere`)
151
- - `--goft-file`: Path to CHIANTI G(T,N) save file (default: `./data/gofnt.sav`)
152
143
  - `--output-dir`: Output directory for results (default: `./run/input`)
153
144
  - `--output-name`: Output filename (default: `synthesised_spectra.pkl`)
154
145
 
146
+ **Line and Abundance Selection:**
147
+ - `--lines`: Emission lines to synthesise, e.g., `--lines Fe12_195.1190 Fe12_195.1790` (required)
148
+ - `--abundance`: CHIANTI abundance dataset name (default: `sun_coronal_2021_chianti`)
149
+ - `--n-workers`: Number of parallel workers for the fiasco G(T, n_e) computation. Each distinct ion is computed in a separate process. `0` uses all available CPUs (default: `0`). Set to `1` for serial execution.
150
+
155
151
  **Simulation Files:**
156
152
  - `--temp-file`: Temperature file relative to data-dir (default: `temp/eosT.0270000`)
157
153
  - `--rho-file`: Density file relative to data-dir (default: `rho/result_prim_0.0270000`)
@@ -184,9 +180,6 @@ synthesise-spectra --help
184
180
  - `--precision`: Numerical precision `float32` or `float64` (default: `float64`)
185
181
  - `--mean-mol-wt`: Mean molecular weight (default: `1.29`)
186
182
 
187
- **Line Selection:**
188
- - `--limit-lines`: Limit to specific lines, e.g., `--limit-lines Fe12_195.1190 Fe12_195.1790`
189
-
190
183
  #### Dynamic Mode (Time-varying Atmospheres)
191
184
 
192
185
  For simulating raster scans over evolving atmospheres, use dynamic mode which combines MHD timesteps based on instrument scanning:
@@ -194,7 +187,8 @@ For simulating raster scans over evolving atmospheres, use dynamic mode which co
194
187
  ```bash
195
188
  synthesise-spectra \
196
189
  --data-dir ./data/atmosphere \
197
- --goft-file ./data/gofnt.sav \
190
+ --lines Fe12_195.1190 \
191
+ --abundance sun_coronal_2021_chianti \
198
192
  --output-dir ./run/input \
199
193
  --slit-rest-time "40 s" \
200
194
  --slit-width "0.2 arcsec" \
@@ -232,7 +226,6 @@ The synthesis produces a pickle file containing:
232
226
 
233
227
  - Use `--downsample 2` or `--downsample 4` for initial testing
234
228
  - Use `--precision float32` to reduce memory usage (may affect accuracy)
235
- - Use `--limit-lines` to synthesise only specific lines for development
236
229
  - Use spatial cropping to focus on regions of interest and reduce computation time
237
230
  - Monitor memory usage - full resolution synthesis can require 50+ GB RAM
238
231
  - Side views (`--integration-axis x` or `y`) may require different velocity files
@@ -245,7 +238,7 @@ The synthesis results can be loaded and analyzed using the package API:
245
238
  import euvst_response
246
239
 
247
240
  # Load synthesis results - this sums all line cubes into a single cube
248
- # By default uses Fe XII 195.119 Å as reference for wavelength grid
241
+ # By default uses Fe XII 195.119 Angstrom as reference for wavelength grid
249
242
  cube = euvst_response.load_atmosphere("./run/input/synthesised_spectra.pkl")
250
243
  print(f"Combined cube shape: {cube.data.shape}")
251
244
 
@@ -263,13 +256,7 @@ print(f"Rest wavelength: {fe12_195.meta['rest_wav']}")
263
256
  print(f"Available spectral lines: {list(data['line_cubes'].keys())}")
264
257
  ```
265
258
 
266
- #### Pre-computed Atmospheres
267
-
268
- This step can require a lot of memory at full resolution. A fully synthesised atmosphere using the Cheung et al. (2018) atmosphere (doi:10.1038/s41550-018-0629-3) for the Fe XII 195.119 and 195.179 lines, including 5 background lines from each side, can be downloaded here: https://liveuclac-my.sharepoint.com/:f:/g/personal/ucasjem_ucl_ac_uk/Es-ts6rwXIlInAweGI7hmdMB5BoGqv9uSpIXOvMkzhS3cw?e=54si7R
269
-
270
- **Important:** You can place the synthesised atmosphere file anywhere and specify its location using the `synthesis_file` parameter in your YAML configuration file. The default location is `./run/input/synthesised_spectra.pkl`.
271
-
272
- ### 3. Simulate the instrument response
259
+ ### 2. Simulate the instrument response
273
260
 
274
261
  #### Configuration File
275
262
 
@@ -285,6 +272,7 @@ simulation runs every combination (cartesian product).
285
272
  - `reference_line`: spectral line used as the wavelength-grid reference (default `Fe12_195.1190`)
286
273
  - `n_iter`: number of Monte Carlo iterations
287
274
  - `ncpu`: CPU cores to use (`-1` = all available)
275
+ - `offchip_bin_slit`: off-chip slit binning factor (default `1`)
288
276
  - `pinhole_sizes`, `pinhole_positions`: fixed paired lists for pinhole diffraction tests (SWC only)
289
277
  - `uniform_intensity`, `rest_wavelength`, `thermal_width`: uniform-intensity mode (alternative to synthesis file)
290
278
 
@@ -299,6 +287,7 @@ reference_line: Fe12_195.1190
299
287
  # Global settings (apply to all combinations)
300
288
  n_iter: 500
301
289
  ncpu: -1
290
+ offchip_bin_slit: [1, 2] # sweep no-binning and 2-pixel off-chip binning
302
291
 
303
292
  # Simulation parameters
304
293
  # Any field listed as a list is swept over; all combinations are run.
@@ -337,6 +326,37 @@ detector:
337
326
 
338
327
  For guidance on recommended values, see McKevitt et al. (2025) (in prep.).
339
328
 
329
+ By default, both the DN and photon signals are fitted at every Monte Carlo iteration. To speed up the simulation when only one is needed, use the `fit_signals` option:
330
+
331
+ ```yaml
332
+ fit_signals: dn # Fit only the DN signal
333
+ fit_signals: photon # Fit only the photon signal
334
+ fit_signals: both # Fit both (default)
335
+ ```
336
+
337
+ To fit blended spectral lines with multiple Gaussian components, add a `fitting` block:
338
+
339
+ ```yaml
340
+ fitting:
341
+ primary_component: 0 # index of the component whose velocity is reported
342
+ constrain_positive_intensity: true # reject fits with negative amplitudes
343
+ backend: scipy # optimiser: "scipy" (default) or "mpfit"
344
+ components:
345
+ - wavelength: 195.119 angstrom # component 0: free centre, width, amplitude
346
+ - wavelength: 195.179 angstrom # component 1: centre & width tied to component 0
347
+ tie_center: 0
348
+ tie_width: 0
349
+ ```
350
+
351
+ Each component requires a `wavelength` field giving its rest wavelength.
352
+
353
+ Each entry in `components` corresponds to one Gaussian. Optional per-component keys:
354
+ - `tie_center: <i>`: constrain this component's centre to match component *i*
355
+ - `tie_width: <i>`: constrain this component's line width to match component *i*
356
+ - `amplitude_greater_than: <i>`: constrain amplitude to exceed that of component *i*
357
+
358
+ Omitting the `fitting` block fits a single Gaussian (default behaviour).
359
+
340
360
  If you synthesised data in dynamic mode, your configuration must specify:
341
361
  - Exactly one slit width matching the synthesis slit width
342
362
  - Exactly one exposure time matching the synthesis exposure time
@@ -352,6 +372,12 @@ eclipse --config ./run/input/config.yaml
352
372
  - `--config`: Path to YAML configuration file (required)
353
373
  - `--debug`: Enable debug mode with IPython breakpoints on errors (optional)
354
374
 
375
+ #### Multi-node MPI parallelisation
376
+
377
+ When launched with multiple MPI ranks on a SLURM cluster (via `srun` or `mpirun`, and setting `--ntasks-per-node`), ECLIPSE automatically distributes Monte Carlo iterations across ranks and gathers results on rank 0. No code or configuration changes are needed - MPI is auto-detected at runtime. If `mpi4py` is not installed or only one rank is present, the code falls back to single-process mode.
378
+
379
+ Requirements: `mpi4py` and `intel-mpi` (load with `module load intel-mpi` before launching).
380
+
355
381
  #### Output
356
382
 
357
383
  Results are saved as pickle files in the `run/result/` directory with the same base name as the configuration file. The output includes:
@@ -4,7 +4,7 @@ ECLIPSE: Emission Calculation and Line Prediction for SOLAR-C EUVST
4
4
  This package provides tools for modeling the performance of the EUV spectrograph EUVST, on SOLAR-C.
5
5
  """
6
6
 
7
- __version__ = "0.6.2"
7
+ __version__ = "0.7.0"
8
8
  __author__ = "James McKevitt"
9
9
  __email__ = "jm2@mssl.ucl.ac.uk"
10
10
 
@@ -13,11 +13,11 @@ from .config import Detector_SWC, Detector_EIS, Telescope_EUVST, Telescope_EIS,
13
13
  from .utils import wl_to_vel, vel_to_wl, angle_to_distance, distance_to_angle
14
14
  from .radiometric import (
15
15
  intensity_to_photons, add_telescope_throughput, photons_to_pixel_counts,
16
- apply_exposure_and_poisson, add_poisson, apply_focusing_optics_psf, to_electrons,
16
+ apply_exposure, sample_photon_arrivals, add_poisson, apply_focusing_optics_psf, to_electrons,
17
17
  to_dn, add_visible_stray_light, add_pinhole_visible_light
18
18
  )
19
19
  from .pinhole_diffraction import apply_euv_pinhole_diffraction, airy_disk_pattern
20
- from .fitting import fit_cube_gauss, velocity_from_fit, width_from_fit, analyse
20
+ from .fitting import fit_cube_gauss, velocity_from_fit, width_from_fit, analyse, FitConfig, FitComponent
21
21
  from .monte_carlo import simulate_once, monte_carlo
22
22
  from .main import main
23
23
  from .data_processing import load_atmosphere, create_uniform_intensity_cube
@@ -36,9 +36,10 @@ __all__ = [
36
36
  "Simulation", "AluminiumFilter",
37
37
  "wl_to_vel", "vel_to_wl", "angle_to_distance", "distance_to_angle",
38
38
  "intensity_to_photons", "add_telescope_throughput", "photons_to_pixel_counts",
39
- "apply_exposure_and_poisson", "add_poisson", "apply_focusing_optics_psf", "to_electrons",
39
+ "apply_exposure", "sample_photon_arrivals", "add_poisson", "apply_focusing_optics_psf", "to_electrons",
40
40
  "to_dn", "add_visible_stray_light", "add_pinhole_visible_light",
41
41
  "fit_cube_gauss", "velocity_from_fit", "width_from_fit", "analyse",
42
+ "FitConfig", "FitComponent",
42
43
  "simulate_once", "monte_carlo",
43
44
  "main",
44
45
  "load_atmosphere",
@@ -119,7 +119,8 @@ def get_parameter_combinations(results: Dict[str, Any]) -> List[Dict]:
119
119
  def analyse_fit_statistics(
120
120
  combination_results: Dict[str, Any],
121
121
  rest_wavelength: u.Quantity,
122
- data_type: str = "dn"
122
+ data_type: str = "dn",
123
+ fit_config=None,
123
124
  ) -> Dict[str, Any]:
124
125
  """
125
126
  Analyze fit statistics to compute velocity and line width statistics.
@@ -132,35 +133,52 @@ def analyse_fit_statistics(
132
133
  Rest wavelength for velocity conversion.
133
134
  data_type : str, optional
134
135
  Either "dn" or "photon" to specify which fit statistics to analyze.
136
+ fit_config : FitConfig, optional
137
+ Multi-component fitting configuration. When provided the primary-
138
+ component indices are used; otherwise indices 1 (centre) and
139
+ 2 (sigma) are assumed (single-component default).
135
140
 
136
141
  Returns
137
142
  -------
138
143
  dict
139
144
  Dictionary containing velocity and width statistics.
140
145
  """
146
+ # Determine parameter indices for the primary component
147
+ if fit_config is not None and not fit_config.is_single:
148
+ idx_center = fit_config.idx_center
149
+ idx_sigma = fit_config.idx_sigma
150
+ else:
151
+ idx_center = 1
152
+ idx_sigma = 2
153
+
141
154
  # Get fit statistics
142
155
  fit_stats_key = f"{data_type}_fit_stats"
143
156
  if fit_stats_key not in combination_results:
144
157
  raise ValueError(f"No {fit_stats_key} found in combination results")
145
158
 
146
159
  fit_stats = combination_results[fit_stats_key]
160
+ if fit_stats is None:
161
+ raise ValueError(
162
+ f"'{data_type}' signal was not fitted for this combination. "
163
+ f"Check the 'fit_signals' setting in your YAML config."
164
+ )
147
165
  fit_truth_data = combination_results["ground_truth"]["fit_truth_data"]
148
166
  fit_truth_units = combination_results["ground_truth"]["fit_truth_units"]
149
167
 
150
168
  # Extract data and units
151
- mean_data = fit_stats["mean_data"] # Shape: (nx, ny, 4)
152
- std_data = fit_stats["std_data"] # Shape: (nx, ny, 4)
153
- units = fit_stats["units"] # List of 4 astropy units
169
+ mean_data = fit_stats["mean_data"] # Shape: (nx, ny, n_params)
170
+ std_data = fit_stats["std_data"] # Shape: (nx, ny, n_params)
171
+ units = fit_stats["units"] # List of n_params astropy units
154
172
 
155
- # Get center statistics (parameter index 1)
156
- center_mean_data = mean_data[..., 1] # (nx, ny) - values only
157
- center_std_data = std_data[..., 1] # (nx, ny) - values only
158
- center_unit = units[1] # wavelength unit
173
+ # Get center statistics for the primary component
174
+ center_mean_data = mean_data[..., idx_center]
175
+ center_std_data = std_data[..., idx_center]
176
+ center_unit = units[idx_center]
159
177
 
160
- # Get width statistics (parameter index 2)
161
- width_mean_data = mean_data[..., 2] # (nx, ny) - values only
162
- width_std_data = std_data[..., 2] # (nx, ny) - values only
163
- width_unit = units[2] # wavelength unit
178
+ # Get width statistics for the primary component
179
+ width_mean_data = mean_data[..., idx_sigma]
180
+ width_std_data = std_data[..., idx_sigma]
181
+ width_unit = units[idx_sigma]
164
182
 
165
183
  # Create quantities
166
184
  center_mean_q = center_mean_data * center_unit
@@ -177,7 +195,7 @@ def analyse_fit_statistics(
177
195
 
178
196
  # Convert to velocities
179
197
  v_mean = centers_to_velocity(center_mean_q, rest_wavelength)
180
- v_true = centers_to_velocity(fit_truth_data[..., 1] * fit_truth_units[1], rest_wavelength)
198
+ v_true = centers_to_velocity(fit_truth_data[..., idx_center] * fit_truth_units[idx_center], rest_wavelength)
181
199
  v_err = v_true - v_mean
182
200
 
183
201
  # Convert center std to velocity std using differential: dv/dlambda = c/lambda
@@ -377,11 +395,12 @@ def summary_table(results: Dict[str, Any]) -> None:
377
395
 
378
396
  def create_sunpy_maps_from_combo(
379
397
  combination_results: Dict[str, Any],
380
- cube_reb,
398
+ cube_reb=None,
381
399
  rest_wavelength: u.Quantity = 195.119 * u.AA,
382
400
  data_type: str = "dn",
383
401
  precision_requirement: u.Quantity = 2.0 * u.km / u.s,
384
- exposure_time_results: List[Dict[str, Any]] | None = None
402
+ exposure_time_results: List[Dict[str, Any]] | None = None,
403
+ fit_config=None,
385
404
  ) -> Dict[str, Any]:
386
405
  """
387
406
  Create SunPy maps from combination results using the new fit statistics structure.
@@ -390,8 +409,9 @@ def create_sunpy_maps_from_combo(
390
409
  ----------
391
410
  combination_results : dict
392
411
  Results for a specific parameter combination from get_results_for_combination().
393
- cube_reb : NDCube
412
+ cube_reb : NDCube, optional
394
413
  NDCube with helioprojective WCS to use for all maps.
414
+ If not provided, the WCS stored in the combination results is used.
395
415
  rest_wavelength : u.Quantity, optional
396
416
  Rest wavelength for velocity conversion (default: 195.119 A for Fe XII).
397
417
  data_type : str, optional
@@ -401,6 +421,9 @@ def create_sunpy_maps_from_combo(
401
421
  exposure_time_results : list of dict, optional
402
422
  List of results from get_results_for_combination() for different exposure times.
403
423
  If provided, will create an exposure time map showing minimum exposure needed.
424
+ fit_config : FitConfig, optional
425
+ Multi-component fitting configuration. When provided, the primary-
426
+ component indices are used to extract centre and width parameters.
404
427
 
405
428
  Returns
406
429
  -------
@@ -426,19 +449,27 @@ def create_sunpy_maps_from_combo(
426
449
  # Extract exposure time from parameters
427
450
  exposure_time = result["parameters"]["simulation.expos"].to_value(u.s)
428
451
  # Create analysis for this exposure
429
- analysis = analyse_fit_statistics(result, rest_wavelength, data_type)
452
+ analysis = analyse_fit_statistics(result, rest_wavelength, data_type, fit_config=fit_config)
430
453
  analysis_per_exp[exposure_time] = analysis
431
454
  else:
432
455
  analysis_per_exp = None
433
456
 
434
- # Extract 2D helioprojective WCS from the cube
435
- wcs_2d = cube_reb.wcs.celestial.swapaxes(0, 1)
457
+ # Extract 2D helioprojective WCS from the cube or stored signal WCS
458
+ if cube_reb is not None:
459
+ wcs_2d = cube_reb.wcs.celestial.swapaxes(0, 1)
460
+ else:
461
+ wcs_2d = combination_results["first_signal_wcs"].celestial.swapaxes(0, 1)
436
462
 
437
463
  # Get the data arrays - now only first iteration is saved
438
464
  first_photon_signal = combination_results["first_photon_signal"] # Shape: (nx, ny, nwave)
439
465
  first_dn_signal = combination_results["first_dn_signal"] # Shape: (nx, ny, nwave)
440
466
  fit_stats_key = f"{data_type}_fit_stats"
441
467
  fit_stats = combination_results[fit_stats_key] # Contains first_fit_data, mean_data, std_data, units
468
+ if fit_stats is None:
469
+ raise ValueError(
470
+ f"'{data_type}' signal was not fitted for this combination. "
471
+ f"Check the 'fit_signals' setting in your YAML config."
472
+ )
442
473
 
443
474
  maps = {}
444
475
 
@@ -457,14 +488,22 @@ def create_sunpy_maps_from_combo(
457
488
  maps['total_dn'] = sunpy.map.Map(total_dn_data.T, wcs_2d)
458
489
  maps['total_dn'].meta['bunit'] = str(total_dn_unit)
459
490
 
491
+ # Determine parameter indices for the primary component
492
+ if fit_config is not None and not fit_config.is_single:
493
+ idx_center = fit_config.idx_center
494
+ idx_sigma = fit_config.idx_sigma
495
+ else:
496
+ idx_center = 1
497
+ idx_sigma = 2
498
+
460
499
  # --- Get velocity and width analysis for this combination ---
461
- analysis = analyse_fit_statistics(combination_results, rest_wavelength, data_type)
500
+ analysis = analyse_fit_statistics(combination_results, rest_wavelength, data_type, fit_config=fit_config)
462
501
 
463
502
  # --- Velocity maps ---
464
- # Velocity from first fit (parameter 1 = center)
465
- first_fit_data = fit_stats["first_fit_data"] # Shape: (nx, ny, 4)
466
- center_first_data = first_fit_data[..., 1] # Extract center parameter
467
- center_first_unit = fit_stats["units"][1] # Get units for center parameter
503
+ # Velocity from first fit (primary component center)
504
+ first_fit_data = fit_stats["first_fit_data"] # Shape: (nx, ny, n_params)
505
+ center_first_data = first_fit_data[..., idx_center]
506
+ center_first_unit = fit_stats["units"][idx_center]
468
507
 
469
508
  def centers_to_velocity(centers_data, centers_unit, lambda0):
470
509
  """Convert wavelength centers to velocities"""
@@ -492,9 +531,9 @@ def create_sunpy_maps_from_combo(
492
531
  maps['velocity_err'].meta['bunit'] = str(analysis["v_err"].unit)
493
532
 
494
533
  # --- Line width maps ---
495
- # Line width from first fit (parameter 2 = width)
496
- width_first_data = first_fit_data[..., 2] # Extract width parameter data
497
- width_first_unit = fit_stats["units"][2] # Get units for width parameter
534
+ # Line width from first fit (primary component sigma)
535
+ width_first_data = first_fit_data[..., idx_sigma]
536
+ width_first_unit = fit_stats["units"][idx_sigma]
498
537
 
499
538
  # Create quantity with proper units
500
539
  width_quantity = width_first_data * width_first_unit