solarc-eclipse 0.7.0__tar.gz → 0.8.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.
- solarc_eclipse-0.8.0/LICENSE +5 -0
- solarc_eclipse-0.8.0/PKG-INFO +75 -0
- solarc_eclipse-0.8.0/README.md +17 -0
- {solarc_eclipse-0.7.0 → solarc_eclipse-0.8.0}/euvst_response/__init__.py +1 -1
- {solarc_eclipse-0.7.0 → solarc_eclipse-0.8.0}/euvst_response/data_processing.py +40 -7
- {solarc_eclipse-0.7.0 → solarc_eclipse-0.8.0}/euvst_response/main.py +11 -5
- {solarc_eclipse-0.7.0 → solarc_eclipse-0.8.0}/euvst_response/monte_carlo.py +63 -12
- {solarc_eclipse-0.7.0 → solarc_eclipse-0.8.0}/euvst_response/radiometric.py +111 -20
- {solarc_eclipse-0.7.0 → solarc_eclipse-0.8.0}/euvst_response/utils.py +5 -0
- {solarc_eclipse-0.7.0 → solarc_eclipse-0.8.0}/pyproject.toml +12 -1
- solarc_eclipse-0.8.0/solarc_eclipse.egg-info/PKG-INFO +75 -0
- {solarc_eclipse-0.7.0 → solarc_eclipse-0.8.0}/solarc_eclipse.egg-info/SOURCES.txt +0 -1
- {solarc_eclipse-0.7.0 → solarc_eclipse-0.8.0}/solarc_eclipse.egg-info/requires.txt +6 -0
- solarc_eclipse-0.7.0/LICENSE +0 -1
- solarc_eclipse-0.7.0/PKG-INFO +0 -443
- solarc_eclipse-0.7.0/README.md +0 -395
- solarc_eclipse-0.7.0/euvst_response/data/throughput/source.txt +0 -3
- solarc_eclipse-0.7.0/solarc_eclipse.egg-info/PKG-INFO +0 -443
- {solarc_eclipse-0.7.0 → solarc_eclipse-0.8.0}/MANIFEST.in +0 -0
- {solarc_eclipse-0.7.0 → solarc_eclipse-0.8.0}/euvst_response/analysis.py +0 -0
- {solarc_eclipse-0.7.0 → solarc_eclipse-0.8.0}/euvst_response/cli.py +0 -0
- {solarc_eclipse-0.7.0 → solarc_eclipse-0.8.0}/euvst_response/config.py +0 -0
- {solarc_eclipse-0.7.0 → solarc_eclipse-0.8.0}/euvst_response/data/throughput/grating_reflection_efficiency.dat +0 -0
- {solarc_eclipse-0.7.0 → solarc_eclipse-0.8.0}/euvst_response/data/throughput/primary_mirror_coating_reflectance.dat +0 -0
- {solarc_eclipse-0.7.0 → solarc_eclipse-0.8.0}/euvst_response/data/throughput/throughput_aluminium_1000_angstrom.dat +0 -0
- {solarc_eclipse-0.7.0 → solarc_eclipse-0.8.0}/euvst_response/data/throughput/throughput_aluminium_oxide_1000_angstrom.dat +0 -0
- {solarc_eclipse-0.7.0 → solarc_eclipse-0.8.0}/euvst_response/data/throughput/throughput_carbon_1000_angstrom.dat +0 -0
- {solarc_eclipse-0.7.0 → solarc_eclipse-0.8.0}/euvst_response/extern/__init__.py +0 -0
- {solarc_eclipse-0.7.0 → solarc_eclipse-0.8.0}/euvst_response/extern/mpfit.py +0 -0
- {solarc_eclipse-0.7.0 → solarc_eclipse-0.8.0}/euvst_response/fitting.py +0 -0
- {solarc_eclipse-0.7.0 → solarc_eclipse-0.8.0}/euvst_response/pinhole_diffraction.py +0 -0
- {solarc_eclipse-0.7.0 → solarc_eclipse-0.8.0}/euvst_response/psf.py +0 -0
- {solarc_eclipse-0.7.0 → solarc_eclipse-0.8.0}/euvst_response/synthesis.py +0 -0
- {solarc_eclipse-0.7.0 → solarc_eclipse-0.8.0}/euvst_response/synthesis_cli.py +0 -0
- {solarc_eclipse-0.7.0 → solarc_eclipse-0.8.0}/setup.cfg +0 -0
- {solarc_eclipse-0.7.0 → solarc_eclipse-0.8.0}/setup.py +0 -0
- {solarc_eclipse-0.7.0 → solarc_eclipse-0.8.0}/solarc_eclipse.egg-info/dependency_links.txt +0 -0
- {solarc_eclipse-0.7.0 → solarc_eclipse-0.8.0}/solarc_eclipse.egg-info/entry_points.txt +0 -0
- {solarc_eclipse-0.7.0 → solarc_eclipse-0.8.0}/solarc_eclipse.egg-info/not-zip-safe +0 -0
- {solarc_eclipse-0.7.0 → solarc_eclipse-0.8.0}/solarc_eclipse.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
Copyright (c) James McKevitt
|
|
2
|
+
|
|
3
|
+
Publishing any results produced by this software, or any work derived from it, requires permission from the author. Contact James McKevitt (jm2@mssl.ucl.ac.uk) before publication.
|
|
4
|
+
|
|
5
|
+
The SOLAR-C SWG are also required to approve the publication of forward modelling results for SOLAR-C/EUVST. Contact James McKevitt (jm2@mssl.ucl.ac.uk) for more information.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: solarc-eclipse
|
|
3
|
+
Version: 0.8.0
|
|
4
|
+
Summary: ECLIPSE: Emission Calculation and Line Prediction for SOLAR-C EUVST
|
|
5
|
+
Home-page: https://github.com/jamesmckevitt/eclipse
|
|
6
|
+
Author: James McKevitt
|
|
7
|
+
Author-email: James McKevitt <jm2@mssl.ucl.ac.uk>
|
|
8
|
+
License: Copyright (c) James McKevitt
|
|
9
|
+
|
|
10
|
+
Publishing any results produced by this software, or any work derived from it, requires permission from the author. Contact James McKevitt (jm2@mssl.ucl.ac.uk) before publication.
|
|
11
|
+
|
|
12
|
+
The SOLAR-C SWG are also required to approve the publication of forward modelling results for SOLAR-C/EUVST. Contact James McKevitt (jm2@mssl.ucl.ac.uk) for more information.
|
|
13
|
+
Project-URL: Homepage, https://github.com/jamesmckevitt/eclipse
|
|
14
|
+
Project-URL: Repository, https://github.com/jamesmckevitt/eclipse
|
|
15
|
+
Project-URL: Documentation, https://solarc-eclipse.readthedocs.io
|
|
16
|
+
Classifier: Development Status :: 4 - Beta
|
|
17
|
+
Classifier: Intended Audience :: Science/Research
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Astronomy
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: numpy
|
|
26
|
+
Requires-Dist: astropy
|
|
27
|
+
Requires-Dist: ndcube
|
|
28
|
+
Requires-Dist: specutils
|
|
29
|
+
Requires-Dist: scipy
|
|
30
|
+
Requires-Dist: matplotlib
|
|
31
|
+
Requires-Dist: joblib
|
|
32
|
+
Requires-Dist: tqdm
|
|
33
|
+
Requires-Dist: dill
|
|
34
|
+
Requires-Dist: pyyaml
|
|
35
|
+
Requires-Dist: reproject
|
|
36
|
+
Requires-Dist: sunpy[all]
|
|
37
|
+
Requires-Dist: dask
|
|
38
|
+
Requires-Dist: psutil
|
|
39
|
+
Requires-Dist: mendeleev
|
|
40
|
+
Requires-Dist: h5py
|
|
41
|
+
Requires-Dist: fiasco
|
|
42
|
+
Provides-Extra: mpi
|
|
43
|
+
Requires-Dist: mpi4py; extra == "mpi"
|
|
44
|
+
Provides-Extra: dev
|
|
45
|
+
Requires-Dist: pytest; extra == "dev"
|
|
46
|
+
Requires-Dist: black; extra == "dev"
|
|
47
|
+
Requires-Dist: flake8; extra == "dev"
|
|
48
|
+
Requires-Dist: mypy; extra == "dev"
|
|
49
|
+
Requires-Dist: ipywidgets; extra == "dev"
|
|
50
|
+
Provides-Extra: docs
|
|
51
|
+
Requires-Dist: mkdocs-material; extra == "docs"
|
|
52
|
+
Requires-Dist: mkdocstrings[python]; extra == "docs"
|
|
53
|
+
Requires-Dist: mkdocs-jupyter; extra == "docs"
|
|
54
|
+
Dynamic: author
|
|
55
|
+
Dynamic: home-page
|
|
56
|
+
Dynamic: license-file
|
|
57
|
+
Dynamic: requires-python
|
|
58
|
+
|
|
59
|
+
# ECLIPSE: Emission Calculation and Line Prediction for SOLAR-C EUVST
|
|
60
|
+
|
|
61
|
+
The ECLIPSE code (Emission Calculation and Line Prediction for SOLAR-C EUVST) is used to forward model the performance of the EUV spectrograph EUVST onboard the SOLAR-C spacecraft.
|
|
62
|
+
|
|
63
|
+
Contact: James McKevitt (jm2@mssl.ucl.ac.uk). See [LICENSE](LICENSE) for usage terms.
|
|
64
|
+
|
|
65
|
+
The instrument response generated by this code will be updated during instrument development, testing, and commissioning.
|
|
66
|
+
|
|
67
|
+
[](https://solarc-eclipse.readthedocs.io/en/latest/)
|
|
68
|
+
[](https://pypi.org/project/solarc-eclipse/)
|
|
69
|
+
[](https://doi.org/10.5281/zenodo.17543844)
|
|
70
|
+
|
|
71
|
+
Full documentation - installation, quick start, the synthesis and instrument response guides, and the analysis tutorial - is at **[solarc-eclipse.readthedocs.io](https://solarc-eclipse.readthedocs.io)**.
|
|
72
|
+
|
|
73
|
+
## Acknowledgements
|
|
74
|
+
|
|
75
|
+
The SOLAR-C/EUVST-SW instrument is an ESA-funded contribution to the JAXA-led SOLAR-C mission. The EUVST-LW (long wavelength) instrument is contributed by NASA. The ECLIPSE code is developed and maintained at Mullard Space Science Laboratory (UCL), and was made using Austrian Super Computing (ASC) infrastructure in collaboration with the University of Vienna.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# ECLIPSE: Emission Calculation and Line Prediction for SOLAR-C EUVST
|
|
2
|
+
|
|
3
|
+
The ECLIPSE code (Emission Calculation and Line Prediction for SOLAR-C EUVST) is used to forward model the performance of the EUV spectrograph EUVST onboard the SOLAR-C spacecraft.
|
|
4
|
+
|
|
5
|
+
Contact: James McKevitt (jm2@mssl.ucl.ac.uk). See [LICENSE](LICENSE) for usage terms.
|
|
6
|
+
|
|
7
|
+
The instrument response generated by this code will be updated during instrument development, testing, and commissioning.
|
|
8
|
+
|
|
9
|
+
[](https://solarc-eclipse.readthedocs.io/en/latest/)
|
|
10
|
+
[](https://pypi.org/project/solarc-eclipse/)
|
|
11
|
+
[](https://doi.org/10.5281/zenodo.17543844)
|
|
12
|
+
|
|
13
|
+
Full documentation - installation, quick start, the synthesis and instrument response guides, and the analysis tutorial - is at **[solarc-eclipse.readthedocs.io](https://solarc-eclipse.readthedocs.io)**.
|
|
14
|
+
|
|
15
|
+
## Acknowledgements
|
|
16
|
+
|
|
17
|
+
The SOLAR-C/EUVST-SW instrument is an ESA-funded contribution to the JAXA-led SOLAR-C mission. The EUVST-LW (long wavelength) instrument is contributed by NASA. The ECLIPSE code is developed and maintained at Mullard Space Science Laboratory (UCL), and was made using Austrian Super Computing (ASC) infrastructure in collaboration with the University of Vienna.
|
|
@@ -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.
|
|
7
|
+
__version__ = "0.8.0"
|
|
8
8
|
__author__ = "James McKevitt"
|
|
9
9
|
__email__ = "jm2@mssl.ucl.ac.uk"
|
|
10
10
|
|
|
@@ -14,7 +14,7 @@ from specutils import Spectrum
|
|
|
14
14
|
from specutils.manipulation import FluxConservingResampler
|
|
15
15
|
from joblib import Parallel, delayed
|
|
16
16
|
from tqdm import tqdm
|
|
17
|
-
from .utils import tqdm_joblib, distance_to_angle
|
|
17
|
+
from .utils import tqdm_joblib, distance_to_angle, _fwhm_to_sigma
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
def _resample_batch(flat_chunk, unit, spectral_world, new_spec_grid, n_spec):
|
|
@@ -331,9 +331,11 @@ def create_uniform_intensity_cube(
|
|
|
331
331
|
det,
|
|
332
332
|
sim,
|
|
333
333
|
n_sigma_extent: float = 8.0,
|
|
334
|
+
n_slit_pixels: int = 1,
|
|
335
|
+
tel=None,
|
|
334
336
|
) -> NDCube:
|
|
335
337
|
"""
|
|
336
|
-
Create a
|
|
338
|
+
Create a 1 x ``n_slit_pixels`` pixel NDCube containing a Gaussian emission line.
|
|
337
339
|
|
|
338
340
|
The cube is built directly at the detector's spectral resolution and
|
|
339
341
|
assigned a helioprojective WCS consistent with the output of
|
|
@@ -354,14 +356,30 @@ def create_uniform_intensity_cube(
|
|
|
354
356
|
Simulation configuration (provides ``slit_width``).
|
|
355
357
|
n_sigma_extent : float, optional
|
|
356
358
|
Number of sigma either side of line centre to include in the
|
|
357
|
-
wavelength grid (default: 8).
|
|
359
|
+
wavelength grid (default: 8). Measured on the width the line will have
|
|
360
|
+
once the spectral PSF has been applied, if *tel* is given.
|
|
361
|
+
n_slit_pixels : int, optional
|
|
362
|
+
Number of (uniform) slit pixels to generate. Set to the
|
|
363
|
+
``offchip_bin_slit`` value so that subsequent ``rebin_slit_offchip``
|
|
364
|
+
sums ``n_slit_pixels`` independent noise realisations into a single
|
|
365
|
+
binned pixel (default: 1).
|
|
366
|
+
tel : Telescope_EUVST or Telescope_EIS, optional
|
|
367
|
+
Telescope configuration. When given, the grid is widened to hold the
|
|
368
|
+
line after spectral PSF broadening, adding the PSF width to the thermal
|
|
369
|
+
width in quadrature. Without this a narrow line gets a grid only a
|
|
370
|
+
couple of pixels wide, and convolving it with a PSF wider than the line
|
|
371
|
+
pushes flux off the ends of the grid. Default None, which sizes the
|
|
372
|
+
grid on the thermal width alone.
|
|
358
373
|
|
|
359
374
|
Returns
|
|
360
375
|
-------
|
|
361
376
|
NDCube
|
|
362
|
-
Shape ``(1,
|
|
377
|
+
Shape ``(1, n_slit_pixels, n_lambda)`` with unit ``erg / (s cm2 sr cm)`` and a
|
|
363
378
|
helioprojective + wavelength WCS.
|
|
364
379
|
"""
|
|
380
|
+
if n_slit_pixels < 1:
|
|
381
|
+
raise ValueError(f"n_slit_pixels must be >= 1, got {n_slit_pixels}")
|
|
382
|
+
|
|
365
383
|
# --- Spectral grid --------------------------------------------------
|
|
366
384
|
lam0 = rest_wavelength.to(u.cm)
|
|
367
385
|
|
|
@@ -371,7 +389,19 @@ def create_uniform_intensity_cube(
|
|
|
371
389
|
# Detector pixel pitch in cm
|
|
372
390
|
dlam = det.wvl_res.to(u.cm / u.pix) * u.pix # strip per-pixel to cm
|
|
373
391
|
|
|
374
|
-
|
|
392
|
+
# The grid has to hold the line as it will be *measured*, not as it leaves
|
|
393
|
+
# the Sun, so add the spectral PSF to the thermal width in quadrature.
|
|
394
|
+
# Widths add that way for Gaussians, and the PSF is often the broader of
|
|
395
|
+
# the two: at the default 20 km/s the line is 0.77 pixels against a PSF of
|
|
396
|
+
# 1.08. Always widening, rather than only when psf is set, keeps the grid
|
|
397
|
+
# independent of a value that is swept and is not known when the cube is
|
|
398
|
+
# built and cached.
|
|
399
|
+
sigma_total = sigma_lam
|
|
400
|
+
if tel is not None:
|
|
401
|
+
sigma_psf = _fwhm_to_sigma(tel.psf_params[1].to(u.pixel).value) * dlam
|
|
402
|
+
sigma_total = np.sqrt(sigma_lam**2 + sigma_psf**2)
|
|
403
|
+
|
|
404
|
+
half_range = n_sigma_extent * sigma_total
|
|
375
405
|
n_pix_half = int(np.ceil((half_range / dlam).decompose().value))
|
|
376
406
|
n_lam = 2 * n_pix_half + 1 # always odd, centred on rest wavelength
|
|
377
407
|
|
|
@@ -384,14 +414,17 @@ def create_uniform_intensity_cube(
|
|
|
384
414
|
u.erg / (u.s * u.cm**2 * u.sr * u.cm)
|
|
385
415
|
)
|
|
386
416
|
profile = A * np.exp(-0.5 * ((lam_grid - lam0) / sigma_lam) ** 2)
|
|
387
|
-
|
|
417
|
+
# Tile the profile along the slit axis. Every slit pixel holds the same
|
|
418
|
+
# intensity, but each is noised independently downstream, which is what
|
|
419
|
+
# rebin_slit_offchip needs in order to sum them.
|
|
420
|
+
data = np.tile(profile.value, (1, n_slit_pixels, 1)) # shape (1, n_slit_pixels, n_lam)
|
|
388
421
|
|
|
389
422
|
# --- WCS (matches reproject_ndcube output format) --------------------
|
|
390
423
|
# Axes: WAVE (cm), HPLT-TAN (arcsec), HPLN-TAN (arcsec)
|
|
391
424
|
wcs = WCS(naxis=3)
|
|
392
425
|
wcs.wcs.ctype = ["WAVE", "HPLT-TAN", "HPLN-TAN"]
|
|
393
426
|
wcs.wcs.cunit = ["cm", "arcsec", "arcsec"]
|
|
394
|
-
wcs.wcs.crpix = [(n_lam + 1) / 2, 1
|
|
427
|
+
wcs.wcs.crpix = [(n_lam + 1) / 2, (n_slit_pixels + 1) / 2, 1.0]
|
|
395
428
|
wcs.wcs.crval = [lam0.value, 0.0, 0.0]
|
|
396
429
|
wcs.wcs.cdelt = [
|
|
397
430
|
dlam.to_value(u.cm),
|
|
@@ -432,14 +432,18 @@ def main() -> None:
|
|
|
432
432
|
DET = Detector_EIS(**all_det) if all_det else Detector_EIS()
|
|
433
433
|
|
|
434
434
|
# Two-level rebinning cache: rebin_atmosphere does not depend on offchip_bin_slit,
|
|
435
|
-
# so cube_reb_cache is keyed by the
|
|
436
|
-
# sweeping multiple binning values at fixed spatial/spectral sampling.
|
|
437
|
-
|
|
435
|
+
# so cube_reb_cache is keyed by the sampling alone to avoid redundant rebin calls
|
|
436
|
+
# when sweeping multiple binning values at fixed spatial/spectral sampling.
|
|
437
|
+
sampling_key = (
|
|
438
438
|
slit_width.to_value(u.arcsec),
|
|
439
439
|
DET.plate_scale_angle.to_value(u.arcsec / u.pixel),
|
|
440
440
|
DET.wvl_res.to_value(u.cm / u.pixel),
|
|
441
441
|
)
|
|
442
|
-
|
|
442
|
+
# In uniform-intensity mode the cube is built with one slit pixel per binning
|
|
443
|
+
# factor, so that rebin_slit_offchip has independent noise realisations to sum.
|
|
444
|
+
# The cube therefore does depend on offchip_bin_slit, and the key must say so.
|
|
445
|
+
cube_reb_key = (*sampling_key, offchip_bin_slit) if uniform_intensity_mode else sampling_key
|
|
446
|
+
rebin_cache_key = (*sampling_key, offchip_bin_slit)
|
|
443
447
|
|
|
444
448
|
if cube_reb_key not in cube_reb_cache:
|
|
445
449
|
print(
|
|
@@ -463,6 +467,8 @@ def main() -> None:
|
|
|
463
467
|
thermal_width=uniform_thermal_width,
|
|
464
468
|
det=DET,
|
|
465
469
|
sim=SIM_rebin,
|
|
470
|
+
n_slit_pixels=offchip_bin_slit,
|
|
471
|
+
tel=TEL,
|
|
466
472
|
)
|
|
467
473
|
else:
|
|
468
474
|
cube_reb_cache[cube_reb_key] = rebin_atmosphere(cube_sim, DET, SIM_rebin)
|
|
@@ -479,7 +485,7 @@ def main() -> None:
|
|
|
479
485
|
# Key by (slit_width_arcsec, offchip_bin_slit) so that sweeps over
|
|
480
486
|
# multiple binning factors at fixed slit width all retain their cubes
|
|
481
487
|
# (a single-key dict would silently keep only the first one).
|
|
482
|
-
cube_reb_dict.setdefault((
|
|
488
|
+
cube_reb_dict.setdefault((sampling_key[0], offchip_bin_slit), cube_reb_binned)
|
|
483
489
|
|
|
484
490
|
cube_reb_binned, fit_truth_data, fit_truth_units = rebin_cache[rebin_cache_key]
|
|
485
491
|
|
|
@@ -18,7 +18,17 @@ from .fitting import fit_cube_gauss
|
|
|
18
18
|
from .utils import angle_to_distance, rebin_slit_offchip, _get_mpi_info
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
def simulate_once(
|
|
21
|
+
def simulate_once(
|
|
22
|
+
I_cube: NDCube,
|
|
23
|
+
t_exp: u.Quantity,
|
|
24
|
+
det,
|
|
25
|
+
tel,
|
|
26
|
+
sim,
|
|
27
|
+
*,
|
|
28
|
+
uniform_mode: bool = False,
|
|
29
|
+
photon_shot_inverse_transform: bool = False,
|
|
30
|
+
dark_current_inverse_transform: bool = False,
|
|
31
|
+
) -> Tuple[NDCube, ...]:
|
|
22
32
|
"""
|
|
23
33
|
Run a single Monte Carlo simulation of the instrument response.
|
|
24
34
|
|
|
@@ -34,6 +44,18 @@ def simulate_once(I_cube: NDCube, t_exp: u.Quantity, det, tel, sim) -> Tuple[NDC
|
|
|
34
44
|
Telescope configuration
|
|
35
45
|
sim : Simulation
|
|
36
46
|
Simulation configuration
|
|
47
|
+
uniform_mode : bool, optional
|
|
48
|
+
If True the input cube is uniform along the slit, so the PSF is
|
|
49
|
+
convolved in the spectral direction only. See
|
|
50
|
+
:func:`~euvst_response.radiometric.apply_focusing_optics_psf`.
|
|
51
|
+
Default False.
|
|
52
|
+
photon_shot_inverse_transform : bool, optional
|
|
53
|
+
Use inverse-transform Poisson sampling for photon shot noise, so that
|
|
54
|
+
common random numbers survive a change in photon flux. Default False.
|
|
55
|
+
dark_current_inverse_transform : bool, optional
|
|
56
|
+
Use inverse-transform Poisson sampling for dark-current shot noise, so
|
|
57
|
+
that common random numbers survive a change in dark-current level.
|
|
58
|
+
Default False.
|
|
37
59
|
|
|
38
60
|
Returns
|
|
39
61
|
-------
|
|
@@ -57,7 +79,9 @@ def simulate_once(I_cube: NDCube, t_exp: u.Quantity, det, tel, sim) -> Tuple[NDC
|
|
|
57
79
|
|
|
58
80
|
# Apply focusing optics PSF (primary mirror + diffraction grating)
|
|
59
81
|
if sim.psf:
|
|
60
|
-
photons_focused = apply_focusing_optics_psf(
|
|
82
|
+
photons_focused = apply_focusing_optics_psf(
|
|
83
|
+
photons_pixels, tel, convolve_spatial=not uniform_mode
|
|
84
|
+
)
|
|
61
85
|
else:
|
|
62
86
|
photons_focused = photons_pixels
|
|
63
87
|
|
|
@@ -68,10 +92,16 @@ def simulate_once(I_cube: NDCube, t_exp: u.Quantity, det, tel, sim) -> Tuple[NDC
|
|
|
68
92
|
photons_euv_pinholes = photons_focused
|
|
69
93
|
|
|
70
94
|
# Sample discrete photon arrivals (photon shot noise)
|
|
71
|
-
photon_arrivals = sample_photon_arrivals(
|
|
95
|
+
photon_arrivals = sample_photon_arrivals(
|
|
96
|
+
photons_euv_pinholes,
|
|
97
|
+
photon_shot_inverse_transform=photon_shot_inverse_transform,
|
|
98
|
+
)
|
|
72
99
|
|
|
73
100
|
# Convert to electrons (detector response: QE, Fano noise, dark current, read noise)
|
|
74
|
-
electrons = to_electrons(
|
|
101
|
+
electrons = to_electrons(
|
|
102
|
+
photon_arrivals, t_exp, det,
|
|
103
|
+
dark_current_inverse_transform=dark_current_inverse_transform,
|
|
104
|
+
)
|
|
75
105
|
|
|
76
106
|
# Add visible stray light (with filter throughput)
|
|
77
107
|
electrons_stray = add_visible_stray_light(electrons, t_exp, det, sim, tel)
|
|
@@ -92,7 +122,10 @@ def simulate_once(I_cube: NDCube, t_exp: u.Quantity, det, tel, sim) -> Tuple[NDC
|
|
|
92
122
|
|
|
93
123
|
def monte_carlo(I_cube: NDCube, t_exp: u.Quantity, det, tel, sim, n_iter: int = 5,
|
|
94
124
|
fit_config=None, offchip_bin_slit: int = 1,
|
|
95
|
-
fit_signals: str = "both", uniform_mode: bool = False
|
|
125
|
+
fit_signals: str = "both", uniform_mode: bool = False,
|
|
126
|
+
*,
|
|
127
|
+
photon_shot_inverse_transform: bool = False,
|
|
128
|
+
dark_current_inverse_transform: bool = False) -> Tuple[NDCube, dict | None, NDCube, dict | None]:
|
|
96
129
|
"""
|
|
97
130
|
Run Monte Carlo simulations and fit results.
|
|
98
131
|
|
|
@@ -121,11 +154,19 @@ def monte_carlo(I_cube: NDCube, t_exp: u.Quantity, det, tel, sim, n_iter: int =
|
|
|
121
154
|
``"photon"``. Fitting is the most expensive step, so
|
|
122
155
|
selecting only the signal of interest roughly halves runtime.
|
|
123
156
|
uniform_mode : bool, optional
|
|
124
|
-
If True the input cube is
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
157
|
+
If True the input cube is taken to be uniform-intensity mode: one scan
|
|
158
|
+
position and ``offchip_bin_slit`` identical slit pixels, which
|
|
159
|
+
*offchip_bin_slit* then sums back down to a single spatial pixel. All
|
|
160
|
+
MC simulations are run first and the resulting spectra are stacked so
|
|
161
|
+
that fitting is parallelised over the n_iter iterations rather than
|
|
162
|
+
over the spatial dimension. Default: False.
|
|
163
|
+
photon_shot_inverse_transform : bool, optional
|
|
164
|
+
Use inverse-transform Poisson sampling for photon shot noise, so that
|
|
165
|
+
common random numbers survive a change in photon flux. Default False.
|
|
166
|
+
dark_current_inverse_transform : bool, optional
|
|
167
|
+
Use inverse-transform Poisson sampling for dark-current shot noise, so
|
|
168
|
+
that common random numbers survive a change in dark-current level.
|
|
169
|
+
Default False.
|
|
129
170
|
|
|
130
171
|
Returns
|
|
131
172
|
-------
|
|
@@ -165,7 +206,12 @@ def monte_carlo(I_cube: NDCube, t_exp: u.Quantity, det, tel, sim, n_iter: int =
|
|
|
165
206
|
disable=not show_progress):
|
|
166
207
|
(intensity_exp, photons_total, photons_throughput, photons_pixels,
|
|
167
208
|
photons_focused, photon_arrivals, electrons, electrons_stray,
|
|
168
|
-
electrons_pinholes, dn) = simulate_once(
|
|
209
|
+
electrons_pinholes, dn) = simulate_once(
|
|
210
|
+
I_cube, t_exp, det, tel, sim,
|
|
211
|
+
uniform_mode=uniform_mode,
|
|
212
|
+
photon_shot_inverse_transform=photon_shot_inverse_transform,
|
|
213
|
+
dark_current_inverse_transform=dark_current_inverse_transform,
|
|
214
|
+
)
|
|
169
215
|
|
|
170
216
|
if i == 0 and rank == 0:
|
|
171
217
|
first_dn_signal = rebin_slit_offchip(dn, offchip_bin_slit)
|
|
@@ -244,7 +290,12 @@ def monte_carlo(I_cube: NDCube, t_exp: u.Quantity, det, tel, sim, n_iter: int =
|
|
|
244
290
|
# Simulate one run
|
|
245
291
|
(intensity_exp, photons_total, photons_throughput, photons_pixels,
|
|
246
292
|
photons_focused, photon_arrivals, electrons, electrons_stray,
|
|
247
|
-
electrons_pinholes, dn) = simulate_once(
|
|
293
|
+
electrons_pinholes, dn) = simulate_once(
|
|
294
|
+
I_cube, t_exp, det, tel, sim,
|
|
295
|
+
uniform_mode=uniform_mode,
|
|
296
|
+
photon_shot_inverse_transform=photon_shot_inverse_transform,
|
|
297
|
+
dark_current_inverse_transform=dark_current_inverse_transform,
|
|
298
|
+
)
|
|
248
299
|
|
|
249
300
|
# Store first iteration signals only on rank 0 (binned, to match fit shapes)
|
|
250
301
|
if i == 0 and rank == 0:
|
|
@@ -7,8 +7,46 @@ import numpy as np
|
|
|
7
7
|
import astropy.units as u
|
|
8
8
|
import astropy.constants as const
|
|
9
9
|
from ndcube import NDCube
|
|
10
|
+
from scipy.ndimage import convolve1d
|
|
10
11
|
from scipy.signal import convolve2d
|
|
11
|
-
from .
|
|
12
|
+
from scipy.stats import poisson
|
|
13
|
+
from .utils import wl_to_vel, vel_to_wl, debug_break, _fwhm_to_sigma
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _poisson_inverse_transform(mean_counts, size=None) -> np.ndarray:
|
|
17
|
+
"""
|
|
18
|
+
Draw Poisson counts by inverse-transform (quantile) sampling.
|
|
19
|
+
|
|
20
|
+
One uniform draw per element is passed through the Poisson inverse-CDF.
|
|
21
|
+
The marginal distribution is identical to ``np.random.poisson``, but this
|
|
22
|
+
consumes a fixed number of RNG draws per element, where the rejection
|
|
23
|
+
sampling in ``np.random.poisson`` consumes a variable number. That keeps
|
|
24
|
+
the random stream synchronised across runs whose only difference is the
|
|
25
|
+
Poisson mean, which is what makes common-random-number variance reduction
|
|
26
|
+
possible.
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
mean_counts : float or np.ndarray
|
|
31
|
+
Poisson mean, either scalar or per element.
|
|
32
|
+
size : tuple of int, optional
|
|
33
|
+
Shape to draw. Defaults to the shape of *mean_counts*.
|
|
34
|
+
|
|
35
|
+
Returns
|
|
36
|
+
-------
|
|
37
|
+
np.ndarray
|
|
38
|
+
Sampled counts as int64.
|
|
39
|
+
"""
|
|
40
|
+
if size is None:
|
|
41
|
+
size = np.shape(mean_counts)
|
|
42
|
+
|
|
43
|
+
u_draw = np.random.random(size=size)
|
|
44
|
+
# np.random.random() draws from [0, 1) and scipy's ppf returns -1 at
|
|
45
|
+
# exactly 0, which would give a negative count. Clamp to the smallest
|
|
46
|
+
# positive double, which leaves the CRN property intact.
|
|
47
|
+
np.maximum(u_draw, np.nextafter(0.0, 1.0), out=u_draw)
|
|
48
|
+
|
|
49
|
+
return poisson.ppf(u_draw, mean_counts).astype(np.int64)
|
|
12
50
|
|
|
13
51
|
|
|
14
52
|
def _vectorized_fano_noise(photon_counts: np.ndarray, rest_wavelength: u.Quantity, det) -> np.ndarray:
|
|
@@ -126,12 +164,12 @@ def photons_to_pixel_counts(ph_flux: NDCube, wl_pitch: u.Quantity, plate_scale:
|
|
|
126
164
|
)
|
|
127
165
|
|
|
128
166
|
|
|
129
|
-
def
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
167
|
+
def apply_focusing_optics_psf(
|
|
168
|
+
signal: NDCube,
|
|
169
|
+
tel,
|
|
170
|
+
*,
|
|
171
|
+
convolve_spatial: bool = True,
|
|
172
|
+
) -> NDCube:
|
|
135
173
|
"""
|
|
136
174
|
Convolve each detector frame (n_slit, n_lambda) of an NDCube with an
|
|
137
175
|
anisotropic 2-D PSF from the focusing optics.
|
|
@@ -147,6 +185,14 @@ def apply_focusing_optics_psf(signal: NDCube, tel) -> NDCube:
|
|
|
147
185
|
tel : Telescope_EUVST or Telescope_EIS
|
|
148
186
|
Telescope configuration containing PSF parameters.
|
|
149
187
|
psf_params = [spatial_fwhm, spectral_fwhm] in pixel units.
|
|
188
|
+
convolve_spatial : bool, optional
|
|
189
|
+
When False, convolve the spectral axis only and leave the slit axis
|
|
190
|
+
alone. This is for a field that is uniform along the slit, where
|
|
191
|
+
convolving a constant with a normalised kernel returns the same
|
|
192
|
+
constant and the spatial pass is an identity operation. Doing it
|
|
193
|
+
anyway would not be harmless: the convolution treats everything
|
|
194
|
+
outside the array as dark, so a uniform field would lose flux off the
|
|
195
|
+
ends of the slit that it really does have. Default True.
|
|
150
196
|
|
|
151
197
|
Returns
|
|
152
198
|
-------
|
|
@@ -160,6 +206,11 @@ def apply_focusing_optics_psf(signal: NDCube, tel) -> NDCube:
|
|
|
160
206
|
psf_type = tel.psf_type.lower()
|
|
161
207
|
psf_params = tel.psf_params
|
|
162
208
|
|
|
209
|
+
if psf_type != "gaussian":
|
|
210
|
+
raise ValueError(
|
|
211
|
+
f"Unsupported PSF type: {psf_type}. Supported: 'gaussian'."
|
|
212
|
+
)
|
|
213
|
+
|
|
163
214
|
if len(psf_params) < 2:
|
|
164
215
|
raise ValueError(
|
|
165
216
|
"psf_params must contain two elements: "
|
|
@@ -182,6 +233,20 @@ def apply_focusing_optics_psf(signal: NDCube, tel) -> NDCube:
|
|
|
182
233
|
if kx % 2 == 0:
|
|
183
234
|
kx += 1
|
|
184
235
|
|
|
236
|
+
if not convolve_spatial:
|
|
237
|
+
# Spectral axis only. Zero padding is correct here: the wavelength
|
|
238
|
+
# grid extends several sigma past the line, so there is no flux at its
|
|
239
|
+
# edges to lose.
|
|
240
|
+
x_1d = np.arange(kx) - kx // 2
|
|
241
|
+
psf_1d = np.exp(-0.5 * (x_1d / sigma_spectral) ** 2)
|
|
242
|
+
psf_1d /= psf_1d.sum()
|
|
243
|
+
return NDCube(
|
|
244
|
+
data=convolve1d(data_in, psf_1d, axis=2, mode="constant", cval=0.0),
|
|
245
|
+
wcs=signal.wcs.deepcopy(),
|
|
246
|
+
unit=unit,
|
|
247
|
+
meta=signal.meta,
|
|
248
|
+
)
|
|
249
|
+
|
|
185
250
|
# Coordinate grids centred at zero
|
|
186
251
|
cy, cx = ky // 2, kx // 2
|
|
187
252
|
y, x = np.mgrid[:ky, :kx]
|
|
@@ -189,13 +254,8 @@ def apply_focusing_optics_psf(signal: NDCube, tel) -> NDCube:
|
|
|
189
254
|
x = (x - cx).astype(float)
|
|
190
255
|
|
|
191
256
|
# Build PSF
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
+ (x / sigma_spectral) ** 2))
|
|
195
|
-
else:
|
|
196
|
-
raise ValueError(
|
|
197
|
-
f"Unsupported PSF type: {psf_type}. Supported: 'gaussian'."
|
|
198
|
-
)
|
|
257
|
+
psf = np.exp(-0.5 * ((y / sigma_spatial) ** 2
|
|
258
|
+
+ (x / sigma_spectral) ** 2))
|
|
199
259
|
|
|
200
260
|
# Normalise
|
|
201
261
|
psf /= psf.sum()
|
|
@@ -213,7 +273,13 @@ def apply_focusing_optics_psf(signal: NDCube, tel) -> NDCube:
|
|
|
213
273
|
)
|
|
214
274
|
|
|
215
275
|
|
|
216
|
-
def to_electrons(
|
|
276
|
+
def to_electrons(
|
|
277
|
+
photon_counts: NDCube,
|
|
278
|
+
t_exp: u.Quantity,
|
|
279
|
+
det,
|
|
280
|
+
*,
|
|
281
|
+
dark_current_inverse_transform: bool = False,
|
|
282
|
+
) -> NDCube:
|
|
217
283
|
"""
|
|
218
284
|
Convert a photon-count NDCube to an electron-count NDCube.
|
|
219
285
|
|
|
@@ -225,6 +291,12 @@ def to_electrons(photon_counts: NDCube, t_exp: u.Quantity, det) -> NDCube:
|
|
|
225
291
|
Exposure time (used for dark current and read noise).
|
|
226
292
|
det : Detector_SWC or Detector_EIS
|
|
227
293
|
Detector description.
|
|
294
|
+
dark_current_inverse_transform : bool, optional
|
|
295
|
+
When True, draw dark-current shot noise with
|
|
296
|
+
:func:`_poisson_inverse_transform` rather than ``np.random.poisson``.
|
|
297
|
+
The distribution is unchanged, but the random stream stays synchronised
|
|
298
|
+
across runs that differ only in dark-current level, which is what
|
|
299
|
+
common-random-number variance reduction needs. Default False.
|
|
228
300
|
|
|
229
301
|
Returns
|
|
230
302
|
-------
|
|
@@ -242,10 +314,16 @@ def to_electrons(photon_counts: NDCube, t_exp: u.Quantity, det) -> NDCube:
|
|
|
242
314
|
|
|
243
315
|
e = electron_counts * (u.electron / u.pixel)
|
|
244
316
|
|
|
245
|
-
# Add dark current with Poisson noise (per pixel)
|
|
317
|
+
# Add dark current with Poisson shot noise (per pixel)
|
|
246
318
|
dark_current_mean = (det.dark_current * t_exp).to(u.electron / u.pixel).value
|
|
247
|
-
|
|
248
|
-
|
|
319
|
+
if dark_current_inverse_transform:
|
|
320
|
+
dark_current_counts = _poisson_inverse_transform(
|
|
321
|
+
dark_current_mean, size=photon_counts.data.shape
|
|
322
|
+
)
|
|
323
|
+
else:
|
|
324
|
+
dark_current_counts = np.random.poisson(dark_current_mean, size=photon_counts.data.shape)
|
|
325
|
+
dark_current_signal = dark_current_counts * (u.electron / u.pixel)
|
|
326
|
+
e += dark_current_signal
|
|
249
327
|
|
|
250
328
|
# Add read noise
|
|
251
329
|
e += np.random.normal(0, det.read_noise_rms.value,
|
|
@@ -317,7 +395,11 @@ def add_poisson(cube: NDCube) -> NDCube:
|
|
|
317
395
|
)
|
|
318
396
|
|
|
319
397
|
|
|
320
|
-
def sample_photon_arrivals(
|
|
398
|
+
def sample_photon_arrivals(
|
|
399
|
+
photon_counts: NDCube,
|
|
400
|
+
*,
|
|
401
|
+
photon_shot_inverse_transform: bool = False,
|
|
402
|
+
) -> NDCube:
|
|
321
403
|
"""
|
|
322
404
|
Sample a discrete Poisson realisation of photon arrivals per pixel.
|
|
323
405
|
|
|
@@ -331,6 +413,12 @@ def sample_photon_arrivals(photon_counts: NDCube) -> NDCube:
|
|
|
331
413
|
----------
|
|
332
414
|
photon_counts : NDCube
|
|
333
415
|
Expected (mean) photon counts per pixel.
|
|
416
|
+
photon_shot_inverse_transform : bool, optional
|
|
417
|
+
When True, draw photon shot noise with
|
|
418
|
+
:func:`_poisson_inverse_transform` rather than ``np.random.poisson``.
|
|
419
|
+
The distribution is unchanged, but the random stream stays synchronised
|
|
420
|
+
across runs that differ only in photon flux, which is what
|
|
421
|
+
common-random-number variance reduction needs. Default False.
|
|
334
422
|
|
|
335
423
|
Returns
|
|
336
424
|
-------
|
|
@@ -344,7 +432,10 @@ def sample_photon_arrivals(photon_counts: NDCube) -> NDCube:
|
|
|
344
432
|
mean_counts = q.to(canonical_units).value
|
|
345
433
|
mean_counts = np.maximum(mean_counts, 0)
|
|
346
434
|
|
|
347
|
-
|
|
435
|
+
if photon_shot_inverse_transform:
|
|
436
|
+
sampled = _poisson_inverse_transform(mean_counts)
|
|
437
|
+
else:
|
|
438
|
+
sampled = np.random.poisson(mean_counts)
|
|
348
439
|
|
|
349
440
|
return NDCube(
|
|
350
441
|
data=sampled.astype(np.int64),
|
|
@@ -457,3 +457,8 @@ def tqdm_joblib(tqdm_object):
|
|
|
457
457
|
finally:
|
|
458
458
|
joblib.parallel.BatchCompletionCallBack = old_callback
|
|
459
459
|
tqdm_object.close()
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def _fwhm_to_sigma(fwhm: float) -> float:
|
|
463
|
+
"""Convert FWHM to Gaussian sigma: sigma = FWHM / (2 * sqrt(2 * ln2))."""
|
|
464
|
+
return fwhm / (2.0 * np.sqrt(2.0 * np.log(2.0)))
|
|
@@ -10,7 +10,7 @@ authors = [
|
|
|
10
10
|
]
|
|
11
11
|
description = "ECLIPSE: Emission Calculation and Line Prediction for SOLAR-C EUVST"
|
|
12
12
|
readme = "README.md"
|
|
13
|
-
license = {
|
|
13
|
+
license = {file = "LICENSE"}
|
|
14
14
|
requires-python = ">=3.10"
|
|
15
15
|
classifiers = [
|
|
16
16
|
"Development Status :: 4 - Beta",
|
|
@@ -49,6 +49,16 @@ dev = [
|
|
|
49
49
|
"black",
|
|
50
50
|
"flake8",
|
|
51
51
|
"mypy",
|
|
52
|
+
# Needed when re-running the tutorial notebook, not when building the docs
|
|
53
|
+
# (mkdocs-jupyter renders the stored outputs without executing anything).
|
|
54
|
+
# Without it tqdm emits a TqdmWarning, which gets saved into the notebook
|
|
55
|
+
# outputs and then shows up as an error block on the docs site.
|
|
56
|
+
"ipywidgets",
|
|
57
|
+
]
|
|
58
|
+
docs = [
|
|
59
|
+
"mkdocs-material",
|
|
60
|
+
"mkdocstrings[python]",
|
|
61
|
+
"mkdocs-jupyter",
|
|
52
62
|
]
|
|
53
63
|
|
|
54
64
|
[project.scripts]
|
|
@@ -60,6 +70,7 @@ synthesise_spectra = "euvst_response.synthesis_cli:main"
|
|
|
60
70
|
[project.urls]
|
|
61
71
|
Homepage = "https://github.com/jamesmckevitt/eclipse"
|
|
62
72
|
Repository = "https://github.com/jamesmckevitt/eclipse"
|
|
73
|
+
Documentation = "https://solarc-eclipse.readthedocs.io"
|
|
63
74
|
|
|
64
75
|
[tool.setuptools.packages.find]
|
|
65
76
|
include = ["euvst_response*"]
|