getframes 2.2.0__tar.gz → 2.3.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.
- {getframes-2.2.0 → getframes-2.3.0}/CHANGELOG.md +36 -1
- {getframes-2.2.0 → getframes-2.3.0}/PKG-INFO +3 -2
- {getframes-2.2.0 → getframes-2.3.0}/README.md +1 -1
- {getframes-2.2.0 → getframes-2.3.0}/pyproject.toml +1 -0
- {getframes-2.2.0 → getframes-2.3.0}/src/getframes/__about__.py +1 -1
- {getframes-2.2.0 → getframes-2.3.0}/src/getframes/frame.py +4 -2
- {getframes-2.2.0 → getframes-2.3.0}/src/getframes/noise.py +1 -11
- {getframes-2.2.0 → getframes-2.3.0}/src/getframes/scene/optics.py +3 -2
- {getframes-2.2.0 → getframes-2.3.0}/src/getframes/scene/psf.py +47 -34
- {getframes-2.2.0 → getframes-2.3.0}/src/getframes/scene/thermal.py +2 -5
- getframes-2.3.0/tests/test_conformance.py +83 -0
- {getframes-2.2.0 → getframes-2.3.0}/tests/test_scene.py +23 -0
- {getframes-2.2.0 → getframes-2.3.0}/.gitignore +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/LICENSE +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/benchmarks/__init__.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/benchmarks/bench_detector_workspace.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/benchmarks/bench_devices.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/benchmarks/bench_fixed_map_dtype.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/benchmarks/detector-workspace-results.json +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/benchmarks/device-results.json +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/benchmarks/device-results.md +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/benchmarks/fixed-map-dtype-results.json +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/benchmarks/render_device_table.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/benchmarks/run.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/examples/01_basic_dark_frame.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/examples/02_custom_camera.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/examples/03_master_dark.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/examples/04_browse_presets.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/examples/05_visualise.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/examples/06_photon_transfer_curve.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/examples/07_star_field_exposure.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/examples/08_ao_limiting_magnitude.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/examples/09_transit_photometry.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/examples/10_detector_realism.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/examples/11_radiometry_and_ir.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/examples/12_ml_dataset.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/examples/13_crowded_field.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/examples/14_keck_lgs_ttf_trade_study.ipynb +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/examples/15_detector_characterization.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/examples/16_detector_showcase.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/examples/README.md +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/examples/_common.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/examples/detector_showcase.webp +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/src/getframes/__init__.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/src/getframes/analysis/__init__.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/src/getframes/analysis/apertures.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/src/getframes/analysis/characterize.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/src/getframes/analysis/nondestructive.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/src/getframes/analysis/ptc.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/src/getframes/backend.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/src/getframes/calibrate.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/src/getframes/camera.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/src/getframes/cli.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/src/getframes/config.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/src/getframes/dataset.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/src/getframes/observation.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/src/getframes/presets/__init__.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/src/getframes/presets/data/__init__.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/src/getframes/presets/data/andor_cb1_0_5mp.toml +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/src/getframes/presets/data/andor_ikon_m934.toml +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/src/getframes/presets/data/andor_ixon_ultra_888.toml +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/src/getframes/presets/data/andor_marana_4_2b_11.toml +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/src/getframes/presets/data/andor_ocam2k.toml +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/src/getframes/presets/data/first_light_imaging_cred_one.toml +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/src/getframes/presets/data/generic_ccd.toml +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/src/getframes/presets/data/generic_cmos.toml +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/src/getframes/presets/data/generic_eapd.toml +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/src/getframes/presets/data/generic_emccd.toml +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/src/getframes/presets/data/generic_scmos.toml +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/src/getframes/presets/data/hamamatsu_orca_fusion.toml +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/src/getframes/presets/data/hamamatsu_orca_quest_2.toml +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/src/getframes/presets/data/leonardo_saphira.toml +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/src/getframes/presets/data/nuvu_hnu_128_omega.toml +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/src/getframes/presets/data/nuvu_hnu_240.toml +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/src/getframes/presets/data/photometrics_prime_95b.toml +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/src/getframes/presets/data/princeton_instruments_kuro_1200b.toml +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/src/getframes/presets/data/qhy530_pro_ii.toml +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/src/getframes/presets/data/scimeasure_little_joe_ccd39.toml +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/src/getframes/presets/data/tucsen_aries_6504_pro.toml +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/src/getframes/presets/data/zwo_asi2600mm.toml +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/src/getframes/py.typed +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/src/getframes/scene/__init__.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/src/getframes/scene/photometry.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/src/getframes/scene/scene.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/src/getframes/scene/sources.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/src/getframes/scene/wcs.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/src/getframes/spectral.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/tests/test_analysis.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/tests/test_benchmarks.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/tests/test_calibrate.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/tests/test_camera.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/tests/test_characterize.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/tests/test_cli.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/tests/test_config.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/tests/test_dataset.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/tests/test_detector.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/tests/test_frame.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/tests/test_gain.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/tests/test_gpu.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/tests/test_noise.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/tests/test_nondestructive_analysis.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/tests/test_observation.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/tests/test_presets.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/tests/test_radiometry.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/tests/test_realism.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/tests/test_scale.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/tests/test_scene_enrich.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/tests/test_signal.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/tests/test_spectral.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/tests/test_validation.py +0 -0
- {getframes-2.2.0 → getframes-2.3.0}/tests/test_workspace.py +0 -0
|
@@ -6,6 +6,40 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [2.3.0] - 2026-10-07
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **`MoffatPSF`, `EllipticalGaussianPSF` and `AiryPSF` no longer put light that
|
|
14
|
+
falls off the frame back onto it.** They normalised their stamp *after*
|
|
15
|
+
clipping it to the frame, so a source on the edge column deposited its full
|
|
16
|
+
flux instead of the part that lands on the detector (a Moffat star centred on
|
|
17
|
+
column 0 kept 100% of its light, where `GaussianPSF` correctly keeps about
|
|
18
|
+
half). The stamp is now normalised before clipping, which follows the AO stack
|
|
19
|
+
convention that light lost at a detector edge is lost, not renormalised
|
|
20
|
+
(aocore CONVENTIONS 3.3). Sources wholly inside the frame are unchanged.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- **Generic primitives now come from `aocore`.** getframes is part of an
|
|
25
|
+
adaptive-optics stack whose shared conventions and primitives live in
|
|
26
|
+
[`aocore`](https://github.com/jacotay7/aocore), now a core dependency
|
|
27
|
+
(`aocore>=0.1.2,<0.2`). `noise.block_sum` is `aocore.block_sum`, re-exported
|
|
28
|
+
so existing imports keep working; it accepts everything it did before and now
|
|
29
|
+
also takes a `(fy, fx)` factor and leading batch axes, and raises a clear
|
|
30
|
+
`ValueError` for a factor below 1 or a shape that does not divide (previously
|
|
31
|
+
a bare reshape error, or a `ZeroDivisionError` for a factor of 0).
|
|
32
|
+
`Frame.binned` sums through it. `AiryPSF` and `Thermal` take their radians per
|
|
33
|
+
arcsecond from `aocore.ARCSEC_TO_RAD`, and `Vignetting` its centred grid from
|
|
34
|
+
`aocore.coordinate_grid`. Every output is bit-for-bit unchanged.
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
|
|
38
|
+
- **Conformance tests** (`tests/test_conformance.py`) run the
|
|
39
|
+
`aocore.conformance` checks that apply to a detector package: every PSF model
|
|
40
|
+
deposits unit flux, a source at `(n - 1) / 2` and the vignetting pattern are
|
|
41
|
+
centred on the optical axis for odd and even windows.
|
|
42
|
+
|
|
9
43
|
## [2.2.0] - 2026-08-24
|
|
10
44
|
|
|
11
45
|
### Added
|
|
@@ -558,7 +592,8 @@ together in 1.0.
|
|
|
558
592
|
- Documentation, runnable examples, and CI (lint, type-check, test matrix, PyPI
|
|
559
593
|
release via Trusted Publishing).
|
|
560
594
|
|
|
561
|
-
[Unreleased]: https://github.com/jacotay7/getframes/compare/2.
|
|
595
|
+
[Unreleased]: https://github.com/jacotay7/getframes/compare/2.3.0...HEAD
|
|
596
|
+
[2.3.0]: https://github.com/jacotay7/getframes/compare/2.2.0...2.3.0
|
|
562
597
|
[2.2.0]: https://github.com/jacotay7/getframes/compare/2.1.1...2.2.0
|
|
563
598
|
[2.1.1]: https://github.com/jacotay7/getframes/compare/2.1.0...2.1.1
|
|
564
599
|
[2.1.0]: https://github.com/jacotay7/getframes/compare/2.0.0...2.1.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: getframes
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: Generate physically realistic synthetic camera frames (CCD/CMOS/EMCCD/eAPD/sCMOS) — dark, bias, flat, and rendered star fields — with auditable noise physics for scientific imaging pipelines.
|
|
5
5
|
Project-URL: Homepage, https://github.com/jacotay7/getframes
|
|
6
6
|
Project-URL: Documentation, https://jacotay7.github.io/getframes/
|
|
@@ -23,6 +23,7 @@ Classifier: Topic :: Scientific/Engineering :: Astronomy
|
|
|
23
23
|
Classifier: Topic :: Scientific/Engineering :: Image Processing
|
|
24
24
|
Classifier: Typing :: Typed
|
|
25
25
|
Requires-Python: >=3.10
|
|
26
|
+
Requires-Dist: aocore<0.2,>=0.1.2
|
|
26
27
|
Requires-Dist: astropy>=5.0
|
|
27
28
|
Requires-Dist: numpy>=1.23
|
|
28
29
|
Requires-Dist: scipy>=1.10
|
|
@@ -72,7 +73,7 @@ switches to CUDA (via CuPy) with a single argument.
|
|
|
72
73
|
## Install
|
|
73
74
|
|
|
74
75
|
```bash
|
|
75
|
-
pip install getframes # CPU (NumPy + SciPy + astropy)
|
|
76
|
+
pip install getframes # CPU (NumPy + SciPy + astropy + aocore)
|
|
76
77
|
pip install 'getframes[gpu]' # + CuPy for CUDA 12.x
|
|
77
78
|
pip install -e '.[dev]' # from a clone, for development
|
|
78
79
|
```
|
|
@@ -27,7 +27,7 @@ switches to CUDA (via CuPy) with a single argument.
|
|
|
27
27
|
## Install
|
|
28
28
|
|
|
29
29
|
```bash
|
|
30
|
-
pip install getframes # CPU (NumPy + SciPy + astropy)
|
|
30
|
+
pip install getframes # CPU (NumPy + SciPy + astropy + aocore)
|
|
31
31
|
pip install 'getframes[gpu]' # + CuPy for CUDA 12.x
|
|
32
32
|
pip install -e '.[dev]' # from a clone, for development
|
|
33
33
|
```
|
|
@@ -7,6 +7,7 @@ from dataclasses import dataclass, field
|
|
|
7
7
|
from typing import TYPE_CHECKING, Any
|
|
8
8
|
|
|
9
9
|
import numpy as np
|
|
10
|
+
from aocore import block_sum
|
|
10
11
|
|
|
11
12
|
from .backend import get_array_module, to_numpy
|
|
12
13
|
|
|
@@ -128,8 +129,9 @@ class Frame:
|
|
|
128
129
|
if factor == 1:
|
|
129
130
|
binned = data.copy()
|
|
130
131
|
else:
|
|
131
|
-
|
|
132
|
-
|
|
132
|
+
binned = block_sum(data, factor)
|
|
133
|
+
if method == "mean":
|
|
134
|
+
binned = binned / (factor * factor)
|
|
133
135
|
metadata = dict(self.metadata)
|
|
134
136
|
metadata["binning"] = int(metadata.get("binning", 1)) * factor
|
|
135
137
|
return Frame(data=binned, metadata=metadata, truth=None)
|
|
@@ -37,6 +37,7 @@ from contextlib import contextmanager
|
|
|
37
37
|
from typing import TYPE_CHECKING, Any, NamedTuple
|
|
38
38
|
|
|
39
39
|
import numpy as np
|
|
40
|
+
from aocore import block_sum
|
|
40
41
|
|
|
41
42
|
from .backend import ArrayBackend, get_backend
|
|
42
43
|
|
|
@@ -1213,17 +1214,6 @@ def frame_electrons(
|
|
|
1213
1214
|
return electrons
|
|
1214
1215
|
|
|
1215
1216
|
|
|
1216
|
-
def block_sum(array: Any, factor: int) -> Any:
|
|
1217
|
-
"""Sum an array into ``factor x factor`` super-pixel blocks (both dims divisible)."""
|
|
1218
|
-
if factor == 1:
|
|
1219
|
-
return array
|
|
1220
|
-
height, width = array.shape
|
|
1221
|
-
binned: NDArray[Any] = array.reshape(height // factor, factor, width // factor, factor).sum(
|
|
1222
|
-
axis=(1, 3)
|
|
1223
|
-
)
|
|
1224
|
-
return binned
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
1217
|
def simulate_frame(
|
|
1228
1218
|
config: CameraConfig,
|
|
1229
1219
|
photon_rate: PhotonRate,
|
|
@@ -9,6 +9,7 @@ from dataclasses import dataclass
|
|
|
9
9
|
from typing import TYPE_CHECKING
|
|
10
10
|
|
|
11
11
|
import numpy as np
|
|
12
|
+
from aocore import coordinate_grid
|
|
12
13
|
from numpy.typing import NDArray
|
|
13
14
|
|
|
14
15
|
from .photometry import Bandpass
|
|
@@ -41,8 +42,8 @@ class Vignetting:
|
|
|
41
42
|
"""Relative illumination in ``[0, 1]`` for a frame of ``(height, width)``."""
|
|
42
43
|
height, width = shape
|
|
43
44
|
cy, cx = (height - 1) / 2.0, (width - 1) / 2.0
|
|
44
|
-
yy, xx =
|
|
45
|
-
r = np.hypot(xx
|
|
45
|
+
yy, xx = coordinate_grid((height, width))
|
|
46
|
+
r = np.hypot(xx, yy)
|
|
46
47
|
r_corner = math.hypot(max(cx, width - 1 - cx), max(cy, height - 1 - cy))
|
|
47
48
|
if r_corner == 0:
|
|
48
49
|
return np.ones(shape, dtype=np.float64)
|
|
@@ -11,9 +11,11 @@ sampled on a stamp and normalised.
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
13
|
import math
|
|
14
|
+
from collections.abc import Callable
|
|
14
15
|
from dataclasses import dataclass
|
|
15
16
|
|
|
16
17
|
import numpy as np
|
|
18
|
+
from aocore import ARCSEC_TO_RAD
|
|
17
19
|
from numpy.typing import NDArray
|
|
18
20
|
from scipy.ndimage import shift as _ndimage_shift
|
|
19
21
|
from scipy.special import erf, j1
|
|
@@ -37,6 +39,33 @@ def _stamp_bounds(
|
|
|
37
39
|
return x0, x1, y0, y1
|
|
38
40
|
|
|
39
41
|
|
|
42
|
+
def _deposit_sampled(
|
|
43
|
+
image: NDArray[np.float64],
|
|
44
|
+
x: float,
|
|
45
|
+
y: float,
|
|
46
|
+
radius: int,
|
|
47
|
+
flux: float,
|
|
48
|
+
profile_at: Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]],
|
|
49
|
+
) -> None:
|
|
50
|
+
"""Add a sampled, normalised stamp, losing whatever falls off the frame.
|
|
51
|
+
|
|
52
|
+
``profile_at(dx, dy)`` evaluates the PSF at column/row offsets from the source.
|
|
53
|
+
The profile is normalised over the *whole* stamp before it is clipped to the
|
|
54
|
+
frame, so light off the edge is lost rather than renormalised back in.
|
|
55
|
+
"""
|
|
56
|
+
x0, x1, y0, y1 = _stamp_bounds(x, y, radius, image.shape)
|
|
57
|
+
if x0 >= x1 or y0 >= y1:
|
|
58
|
+
return # source falls entirely off the frame
|
|
59
|
+
ix, iy = round(x), round(y)
|
|
60
|
+
offsets = np.arange(-radius, radius + 1, dtype=np.float64)
|
|
61
|
+
profile = profile_at(offsets[None, :] + (ix - x), offsets[:, None] + (iy - y))
|
|
62
|
+
total = profile.sum()
|
|
63
|
+
if total > 0:
|
|
64
|
+
sx, sy = x0 - (ix - radius), y0 - (iy - radius)
|
|
65
|
+
stamp = profile[sy : sy + (y1 - y0), sx : sx + (x1 - x0)]
|
|
66
|
+
image[y0:y1, x0:x1] += flux * stamp / total
|
|
67
|
+
|
|
68
|
+
|
|
40
69
|
class PSF:
|
|
41
70
|
"""Base class for point-spread functions."""
|
|
42
71
|
|
|
@@ -193,17 +222,12 @@ class MoffatPSF(PSF):
|
|
|
193
222
|
alpha = fwhm_pix / (2.0 * np.sqrt(2.0 ** (1.0 / self.beta) - 1.0))
|
|
194
223
|
|
|
195
224
|
radius = int(np.ceil(6.0 * alpha)) + 1
|
|
196
|
-
|
|
197
|
-
if x0 >= x1 or y0 >= y1:
|
|
198
|
-
return
|
|
225
|
+
beta = self.beta
|
|
199
226
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
total = profile.sum()
|
|
205
|
-
if total > 0:
|
|
206
|
-
image[y0:y1, x0:x1] += flux * profile / total
|
|
227
|
+
def profile(dx: NDArray[np.float64], dy: NDArray[np.float64]) -> NDArray[np.float64]:
|
|
228
|
+
return np.asarray((1.0 + (dx**2 + dy**2) / alpha**2) ** (-beta))
|
|
229
|
+
|
|
230
|
+
_deposit_sampled(image, x, y, radius, flux, profile)
|
|
207
231
|
|
|
208
232
|
|
|
209
233
|
@dataclass(frozen=True)
|
|
@@ -238,20 +262,15 @@ class EllipticalGaussianPSF(PSF):
|
|
|
238
262
|
raise ValueError("PSF FWHM and plate scale must be positive.")
|
|
239
263
|
|
|
240
264
|
radius = int(np.ceil(5.0 * sigma_major)) + 1
|
|
241
|
-
x0, x1, y0, y1 = _stamp_bounds(x, y, radius, image.shape)
|
|
242
|
-
if x0 >= x1 or y0 >= y1:
|
|
243
|
-
return
|
|
244
|
-
|
|
245
|
-
xs = np.arange(x0, x1) - x
|
|
246
|
-
ys = np.arange(y0, y1) - y
|
|
247
265
|
theta = math.radians(self.position_angle_deg)
|
|
248
266
|
cos_t, sin_t = math.cos(theta), math.sin(theta)
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
267
|
+
|
|
268
|
+
def profile(dx: NDArray[np.float64], dy: NDArray[np.float64]) -> NDArray[np.float64]:
|
|
269
|
+
u = dx * cos_t + dy * sin_t
|
|
270
|
+
v = -dx * sin_t + dy * cos_t
|
|
271
|
+
return np.exp(-0.5 * ((u / sigma_major) ** 2 + (v / sigma_minor) ** 2))
|
|
272
|
+
|
|
273
|
+
_deposit_sampled(image, x, y, radius, flux, profile)
|
|
255
274
|
|
|
256
275
|
|
|
257
276
|
@dataclass(frozen=True)
|
|
@@ -293,23 +312,17 @@ class AiryPSF(PSF):
|
|
|
293
312
|
raise ValueError("AiryPSF obstruction must be in [0, 1).")
|
|
294
313
|
|
|
295
314
|
# Radians per pixel, then the argument scale x = pi D theta / lambda.
|
|
296
|
-
rad_per_pixel = plate_scale_arcsec_per_pixel *
|
|
315
|
+
rad_per_pixel = plate_scale_arcsec_per_pixel * ARCSEC_TO_RAD
|
|
297
316
|
arg_per_pixel = math.pi * self.aperture_diameter_m / self.wavelength_m * rad_per_pixel
|
|
298
317
|
# First null at 1.22 lambda / D; size the stamp to a few Airy rings.
|
|
299
318
|
first_null_pix = 1.22 / (arg_per_pixel / math.pi) if arg_per_pixel > 0 else 1.0
|
|
300
319
|
radius = int(np.ceil(5.0 * first_null_pix)) + 1
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
320
|
+
obstruction = self.obstruction
|
|
321
|
+
|
|
322
|
+
def profile(dx: NDArray[np.float64], dy: NDArray[np.float64]) -> NDArray[np.float64]:
|
|
323
|
+
return _airy_intensity(arg_per_pixel * np.hypot(dx, dy), obstruction)
|
|
304
324
|
|
|
305
|
-
|
|
306
|
-
ys = np.arange(y0, y1) - y
|
|
307
|
-
rr = np.sqrt(xs[None, :] ** 2 + ys[:, None] ** 2)
|
|
308
|
-
arg = arg_per_pixel * rr
|
|
309
|
-
profile = _airy_intensity(arg, self.obstruction)
|
|
310
|
-
total = profile.sum()
|
|
311
|
-
if total > 0:
|
|
312
|
-
image[y0:y1, x0:x1] += flux * profile / total
|
|
325
|
+
_deposit_sampled(image, x, y, radius, flux, profile)
|
|
313
326
|
|
|
314
327
|
|
|
315
328
|
def _airy_intensity(arg: NDArray[np.float64], obstruction: float) -> NDArray[np.float64]:
|
|
@@ -14,11 +14,11 @@ Pure NumPy, no randomness.
|
|
|
14
14
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
|
-
import math
|
|
18
17
|
from dataclasses import dataclass
|
|
19
18
|
from typing import TYPE_CHECKING
|
|
20
19
|
|
|
21
20
|
import numpy as np
|
|
21
|
+
from aocore import ARCSEC_TO_RAD
|
|
22
22
|
from numpy.typing import NDArray
|
|
23
23
|
|
|
24
24
|
from ..spectral import SED
|
|
@@ -34,9 +34,6 @@ _H_PLANCK = 6.62607015e-34 # J s
|
|
|
34
34
|
_C_LIGHT = 2.99792458e8 # m / s
|
|
35
35
|
_K_BOLTZMANN = 1.380649e-23 # J / K
|
|
36
36
|
|
|
37
|
-
# Arcseconds to radians, for the per-pixel solid angle.
|
|
38
|
-
_ARCSEC_TO_RAD = math.pi / (180.0 * 3600.0)
|
|
39
|
-
|
|
40
37
|
|
|
41
38
|
def _photon_radiance(
|
|
42
39
|
wavelength_m: NDArray[np.float64], temperature_k: float
|
|
@@ -96,7 +93,7 @@ class Thermal:
|
|
|
96
93
|
wl_m = resp.wavelength_nm * 1e-9
|
|
97
94
|
integrand = _photon_radiance(wl_m, self.temperature_k) * resp.value
|
|
98
95
|
radiance = float(_trapezoid(integrand, wl_m)) # photons/s/m^2/sr
|
|
99
|
-
omega_sr = (optics.plate_scale_arcsec_per_pixel *
|
|
96
|
+
omega_sr = (optics.plate_scale_arcsec_per_pixel * ARCSEC_TO_RAD) ** 2
|
|
100
97
|
return self.emissivity * radiance * optics.collecting_area_m2 * omega_sr
|
|
101
98
|
|
|
102
99
|
def photon_sed(
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""The AO stack conventions (aocore ``CONVENTIONS.md``) that apply to getframes.
|
|
3
|
+
|
|
4
|
+
getframes owns detectors, not wavefronts: its PSFs are analytic profiles (or a
|
|
5
|
+
user kernel), not images of an OPD map. So the OPD-driven checks (tilt direction,
|
|
6
|
+
slope sign, wind motion, Zernike basis, RMS) do not apply. What does apply is the
|
|
7
|
+
flat-wavefront case: where the optical axis sits in the image plane (rule 1.3) and
|
|
8
|
+
that a normalized point-source image carries unit flux (rule 3.3). The callables
|
|
9
|
+
below take the checks' OPD argument for its shape only.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
import aocore
|
|
17
|
+
import numpy as np
|
|
18
|
+
import pytest
|
|
19
|
+
from aocore import conformance
|
|
20
|
+
|
|
21
|
+
from getframes.scene import (
|
|
22
|
+
AiryPSF,
|
|
23
|
+
ArrayPSF,
|
|
24
|
+
EllipticalGaussianPSF,
|
|
25
|
+
GaussianPSF,
|
|
26
|
+
MoffatPSF,
|
|
27
|
+
Vignetting,
|
|
28
|
+
)
|
|
29
|
+
from getframes.scene.psf import PSF
|
|
30
|
+
|
|
31
|
+
PLATE_SCALE_ARCSEC = 0.01
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _psfs() -> list[PSF]:
|
|
35
|
+
kernel = np.exp(-0.5 * ((np.arange(15.0) - 7.0) / 2.0) ** 2)
|
|
36
|
+
return [
|
|
37
|
+
GaussianPSF(fwhm_arcsec=0.04),
|
|
38
|
+
MoffatPSF(fwhm_arcsec=0.04, beta=3.0),
|
|
39
|
+
EllipticalGaussianPSF(fwhm_major_arcsec=0.05, fwhm_minor_arcsec=0.03),
|
|
40
|
+
# 8 m aperture at 1.6 um: lambda / D = 0.041 arcsec, about 4 pixels.
|
|
41
|
+
AiryPSF(aperture_diameter_m=8.0, wavelength_m=1.6e-6),
|
|
42
|
+
ArrayPSF(kernel=np.outer(kernel, kernel)),
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _on_axis_image(psf: PSF, shape: tuple[int, ...]) -> np.ndarray[Any, Any]:
|
|
47
|
+
"""A unit-flux point source on the optical axis ``(n - 1) / 2`` (rule 1.3)."""
|
|
48
|
+
height, width = shape
|
|
49
|
+
image = np.zeros((height, width))
|
|
50
|
+
psf.add_source(image, (width - 1) / 2.0, (height - 1) / 2.0, 1.0, PLATE_SCALE_ARCSEC)
|
|
51
|
+
return image
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@pytest.mark.parametrize("psf", _psfs(), ids=lambda psf: type(psf).__name__)
|
|
55
|
+
def test_point_source_image_carries_unit_flux(psf: PSF) -> None:
|
|
56
|
+
# Rule 3.3: the window holds all of the light, so the image sums to the flux.
|
|
57
|
+
conformance.check_unit_flux(lambda opd: _on_axis_image(psf, opd.shape), pupil_shape=(96, 96))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@pytest.mark.parametrize("psf", _psfs(), ids=lambda psf: type(psf).__name__)
|
|
61
|
+
@pytest.mark.parametrize("shape", [(64, 64), (65, 65), (64, 65)])
|
|
62
|
+
def test_pixel_coordinates_put_the_axis_on_pixel_centres(psf: PSF, shape: tuple[int, int]) -> None:
|
|
63
|
+
# Rules 1.2-1.3: getframes positions are pixel-centre indices, so a source at
|
|
64
|
+
# ``(n - 1) / 2`` images to the window centre, between pixels for even n.
|
|
65
|
+
conformance.check_image_centring(lambda opd: _on_axis_image(psf, opd.shape), pupil_shape=shape)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@pytest.mark.parametrize("shape", [(64, 64), (65, 65), (64, 65)])
|
|
69
|
+
def test_vignetting_is_centred_on_the_optical_axis(shape: tuple[int, int]) -> None:
|
|
70
|
+
# Rule 1.3: the field-dependent illumination falls off about ``(n - 1) / 2``.
|
|
71
|
+
vignetting = Vignetting(strength=0.4, power=2.0)
|
|
72
|
+
conformance.check_image_centring(
|
|
73
|
+
lambda opd: vignetting.illumination_map(opd.shape), pupil_shape=shape
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_arcsecond_constant_is_the_shared_one() -> None:
|
|
78
|
+
# Rule 2.3: AiryPSF and the thermal background take their radians per
|
|
79
|
+
# arcsecond from aocore rather than a local literal.
|
|
80
|
+
from getframes.scene import psf, thermal
|
|
81
|
+
|
|
82
|
+
assert psf.ARCSEC_TO_RAD is aocore.ARCSEC_TO_RAD
|
|
83
|
+
assert thermal.ARCSEC_TO_RAD is aocore.ARCSEC_TO_RAD
|
|
@@ -61,6 +61,29 @@ def test_psf_conserves_flux(psf, optics):
|
|
|
61
61
|
assert img.sum() == pytest.approx(1000.0, rel=1e-3)
|
|
62
62
|
|
|
63
63
|
|
|
64
|
+
@pytest.mark.parametrize(
|
|
65
|
+
"psf",
|
|
66
|
+
[
|
|
67
|
+
gf.GaussianPSF(fwhm_arcsec=1.2),
|
|
68
|
+
gf.MoffatPSF(fwhm_arcsec=1.2),
|
|
69
|
+
gf.EllipticalGaussianPSF(fwhm_major_arcsec=1.6, fwhm_minor_arcsec=0.8),
|
|
70
|
+
# lambda / D = 2 arcsec, five pixels.
|
|
71
|
+
gf.AiryPSF(aperture_diameter_m=0.1, wavelength_m=1.0e-6),
|
|
72
|
+
],
|
|
73
|
+
ids=lambda psf: type(psf).__name__,
|
|
74
|
+
)
|
|
75
|
+
def test_psf_loses_the_flux_that_falls_off_the_frame(psf):
|
|
76
|
+
# Light beyond the detector edge is lost, never renormalised back in
|
|
77
|
+
# (aocore CONVENTIONS 3.3). A source on column 0 of a frame must deposit
|
|
78
|
+
# exactly the part of a fully captured image that lies at x >= 0.
|
|
79
|
+
full = np.zeros((128, 128))
|
|
80
|
+
psf.add_source(full, x=64.3, y=64.0, flux=1000.0, plate_scale_arcsec_per_pixel=0.4)
|
|
81
|
+
edge = np.zeros((128, 64))
|
|
82
|
+
psf.add_source(edge, x=0.3, y=64.0, flux=1000.0, plate_scale_arcsec_per_pixel=0.4)
|
|
83
|
+
assert edge.sum() == pytest.approx(full[:, 64:].sum(), rel=1e-9)
|
|
84
|
+
assert edge.sum() < 0.75 * full.sum()
|
|
85
|
+
|
|
86
|
+
|
|
64
87
|
def test_psf_peaks_at_source_position(optics):
|
|
65
88
|
img = np.zeros((64, 64))
|
|
66
89
|
gf.GaussianPSF(1.0).add_source(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{getframes-2.2.0 → getframes-2.3.0}/src/getframes/presets/data/first_light_imaging_cred_one.toml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{getframes-2.2.0 → getframes-2.3.0}/src/getframes/presets/data/princeton_instruments_kuro_1200b.toml
RENAMED
|
File without changes
|
|
File without changes
|
{getframes-2.2.0 → getframes-2.3.0}/src/getframes/presets/data/scimeasure_little_joe_ccd39.toml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|