PyDIET 0.9.3__py3-none-any.whl
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.
- pydiet/__init__.py +12 -0
- pydiet/api_client/__init__.py +6 -0
- pydiet/api_client/client.py +57 -0
- pydiet/cmd/__init__.py +9 -0
- pydiet/cmd/start.py +107 -0
- pydiet/data/data_config.toml +242 -0
- pydiet/data/description.txt +1 -0
- pydiet/data/instruments/description.txt +1 -0
- pydiet/data/instruments/megacam/default +0 -0
- pydiet/data/instruments/megacam/description.txt +1 -0
- pydiet/data/instruments/megacam/detector/description.txt +1 -0
- pydiet/data/instruments/megacam/detector/qe/MegaCam_QE.average.fits +0 -0
- pydiet/data/instruments/megacam/detector/qe/description.txt +2 -0
- pydiet/data/instruments/megacam/filters/CaHK.MP9303.fits +0 -0
- pydiet/data/instruments/megacam/filters/Ha.MP9603.fits +0 -0
- pydiet/data/instruments/megacam/filters/HaOFF.MP9604.fits +0 -0
- pydiet/data/instruments/megacam/filters/M4112.MP9403.fits +0 -0
- pydiet/data/instruments/megacam/filters/M4376.MP9404.fits +0 -0
- pydiet/data/instruments/megacam/filters/OIII.MP9501.fits +0 -0
- pydiet/data/instruments/megacam/filters/OIIIOFF.MP9502.fits +0 -0
- pydiet/data/instruments/megacam/filters/description.txt +1 -0
- pydiet/data/instruments/megacam/filters/g.MP9402.fits +0 -0
- pydiet/data/instruments/megacam/filters/gri.MP9605.fits +0 -0
- pydiet/data/instruments/megacam/filters/i.MP9703.fits +0 -0
- pydiet/data/instruments/megacam/filters/r.MP9602.fits +0 -0
- pydiet/data/instruments/megacam/filters/u.MP9302.fits +0 -0
- pydiet/data/instruments/megacam/filters/z.MP9901.fits +0 -0
- pydiet/data/instruments/megacam/optics/description.txt +2 -0
- pydiet/data/instruments/megacam/optics/transmission/MegaPrime_transmission.fits +0 -0
- pydiet/data/instruments/megacam/optics/transmission/description.txt +2 -0
- pydiet/data/instruments/wircam/description.txt +1 -0
- pydiet/data/instruments/wircam/detector/description.txt +1 -0
- pydiet/data/instruments/wircam/detector/qe/WIRCam_QE.average.fits +0 -0
- pydiet/data/instruments/wircam/detector/qe/description.txt +2 -0
- pydiet/data/instruments/wircam/filters/BrG.WC8305.fits +0 -0
- pydiet/data/instruments/wircam/filters/CH4Off.WC8204.fits +0 -0
- pydiet/data/instruments/wircam/filters/CH4On.WC8203.fits +0 -0
- pydiet/data/instruments/wircam/filters/CO.WC8306.fits +0 -0
- pydiet/data/instruments/wircam/filters/H.WC8201.fits +0 -0
- pydiet/data/instruments/wircam/filters/H.WC8202.fits +0 -0
- pydiet/data/instruments/wircam/filters/H2.WC8304.fits +0 -0
- pydiet/data/instruments/wircam/filters/J.WC8101.fits +0 -0
- pydiet/data/instruments/wircam/filters/J.WC8103.fits +0 -0
- pydiet/data/instruments/wircam/filters/Kcont.WC8303.fits +0 -0
- pydiet/data/instruments/wircam/filters/Ks.WC8301.fits +0 -0
- pydiet/data/instruments/wircam/filters/Ks.WC8302.fits +0 -0
- pydiet/data/instruments/wircam/filters/LowOH1.WC8104.fits +0 -0
- pydiet/data/instruments/wircam/filters/LowOH2.WC8102.fits +0 -0
- pydiet/data/instruments/wircam/filters/W.WC8105.fits +0 -0
- pydiet/data/instruments/wircam/filters/Y.WC8002.fits +0 -0
- pydiet/data/instruments/wircam/filters/description.txt +1 -0
- pydiet/data/instruments/wircam/optics/description.txt +2 -0
- pydiet/data/instruments/wircam/optics/transmission/WIRCam_transmission.fits +0 -0
- pydiet/data/instruments/wircam/optics/transmission/description.txt +2 -0
- pydiet/data/sites/description.txt +1 -0
- pydiet/data/sites/mko/default +0 -0
- pydiet/data/sites/mko/description.txt +2 -0
- pydiet/data/sites/mko/emission/MKO_emission.bright.AM1.0.fits +0 -0
- pydiet/data/sites/mko/emission/MKO_emission.bright.AM1.1.fits +0 -0
- pydiet/data/sites/mko/emission/MKO_emission.bright.AM1.2.fits +0 -0
- pydiet/data/sites/mko/emission/MKO_emission.bright.AM1.3.fits +0 -0
- pydiet/data/sites/mko/emission/MKO_emission.bright.AM1.4.fits +0 -0
- pydiet/data/sites/mko/emission/MKO_emission.bright.AM1.5.fits +0 -0
- pydiet/data/sites/mko/emission/MKO_emission.bright.AM1.6.fits +0 -0
- pydiet/data/sites/mko/emission/MKO_emission.bright.AM1.7.fits +0 -0
- pydiet/data/sites/mko/emission/MKO_emission.bright.AM1.8.fits +0 -0
- pydiet/data/sites/mko/emission/MKO_emission.bright.AM1.9.fits +0 -0
- pydiet/data/sites/mko/emission/MKO_emission.bright.AM2.0.fits +0 -0
- pydiet/data/sites/mko/emission/MKO_emission.bright.AM2.5.fits +0 -0
- pydiet/data/sites/mko/emission/MKO_emission.bright.AM3.0.fits +0 -0
- pydiet/data/sites/mko/emission/MKO_emission.dark.AM1.0.fits +0 -0
- pydiet/data/sites/mko/emission/MKO_emission.dark.AM1.1.fits +0 -0
- pydiet/data/sites/mko/emission/MKO_emission.dark.AM1.2.fits +0 -0
- pydiet/data/sites/mko/emission/MKO_emission.dark.AM1.3.fits +0 -0
- pydiet/data/sites/mko/emission/MKO_emission.dark.AM1.4.fits +0 -0
- pydiet/data/sites/mko/emission/MKO_emission.dark.AM1.5.fits +0 -0
- pydiet/data/sites/mko/emission/MKO_emission.dark.AM1.6.fits +0 -0
- pydiet/data/sites/mko/emission/MKO_emission.dark.AM1.7.fits +0 -0
- pydiet/data/sites/mko/emission/MKO_emission.dark.AM1.8.fits +0 -0
- pydiet/data/sites/mko/emission/MKO_emission.dark.AM1.9.fits +0 -0
- pydiet/data/sites/mko/emission/MKO_emission.dark.AM2.0.fits +0 -0
- pydiet/data/sites/mko/emission/MKO_emission.dark.AM2.5.fits +0 -0
- pydiet/data/sites/mko/emission/MKO_emission.dark.AM3.0.fits +0 -0
- pydiet/data/sites/mko/emission/MKO_emission.grey.AM1.0.fits +0 -0
- pydiet/data/sites/mko/emission/MKO_emission.grey.AM1.1.fits +0 -0
- pydiet/data/sites/mko/emission/MKO_emission.grey.AM1.2.fits +0 -0
- pydiet/data/sites/mko/emission/MKO_emission.grey.AM1.3.fits +0 -0
- pydiet/data/sites/mko/emission/MKO_emission.grey.AM1.4.fits +0 -0
- pydiet/data/sites/mko/emission/MKO_emission.grey.AM1.5.fits +0 -0
- pydiet/data/sites/mko/emission/MKO_emission.grey.AM1.6.fits +0 -0
- pydiet/data/sites/mko/emission/MKO_emission.grey.AM1.7.fits +0 -0
- pydiet/data/sites/mko/emission/MKO_emission.grey.AM1.8.fits +0 -0
- pydiet/data/sites/mko/emission/MKO_emission.grey.AM1.9.fits +0 -0
- pydiet/data/sites/mko/emission/MKO_emission.grey.AM2.0.fits +0 -0
- pydiet/data/sites/mko/emission/MKO_emission.grey.AM2.5.fits +0 -0
- pydiet/data/sites/mko/emission/MKO_emission.grey.AM3.0.fits +0 -0
- pydiet/data/sites/mko/emission/description.txt +5 -0
- pydiet/data/sites/mko/transmission/MKO_transmission.AM1.0.fits +0 -0
- pydiet/data/sites/mko/transmission/MKO_transmission.AM1.1.fits +0 -0
- pydiet/data/sites/mko/transmission/MKO_transmission.AM1.2.fits +0 -0
- pydiet/data/sites/mko/transmission/MKO_transmission.AM1.3.fits +0 -0
- pydiet/data/sites/mko/transmission/MKO_transmission.AM1.4.fits +0 -0
- pydiet/data/sites/mko/transmission/MKO_transmission.AM1.5.fits +0 -0
- pydiet/data/sites/mko/transmission/MKO_transmission.AM1.6.fits +0 -0
- pydiet/data/sites/mko/transmission/MKO_transmission.AM1.7.fits +0 -0
- pydiet/data/sites/mko/transmission/MKO_transmission.AM1.8.fits +0 -0
- pydiet/data/sites/mko/transmission/MKO_transmission.AM1.9.fits +0 -0
- pydiet/data/sites/mko/transmission/MKO_transmission.AM2.0.fits +0 -0
- pydiet/data/sites/mko/transmission/MKO_transmission.AM2.5.fits +0 -0
- pydiet/data/sites/mko/transmission/MKO_transmission.AM3.0.fits +0 -0
- pydiet/data/sites/mko/transmission/MKO_transmission.AM3.5.fits +0 -0
- pydiet/data/sites/mko/transmission/MKO_transmission.AM4.0.fits +0 -0
- pydiet/data/sites/mko/transmission/MKO_transmission.AM4.5.fits +0 -0
- pydiet/data/sites/mko/transmission/MKO_transmission.AM5.0.fits +0 -0
- pydiet/data/sites/mko/transmission/description.txt +5 -0
- pydiet/data/telescopes/cfht/default +0 -0
- pydiet/data/telescopes/cfht/description.txt +1 -0
- pydiet/data/telescopes/cfht/emission/description.txt +2 -0
- pydiet/data/telescopes/cfht/transmission/CFHT_M1_transmission.fits +0 -0
- pydiet/data/telescopes/cfht/transmission/description.txt +1 -0
- pydiet/data/telescopes/description.txt +1 -0
- pydiet/package.py +55 -0
- pydiet/py.typed +0 -0
- pydiet/server/__init__.py +9 -0
- pydiet/server/app.py +369 -0
- pydiet/server/config/__init__.py +51 -0
- pydiet/server/config/config.py +330 -0
- pydiet/server/config/fields.py +49 -0
- pydiet/server/config/settings.py +166 -0
- pydiet/server/data.py +31 -0
- pydiet/server/datafiles.py +367 -0
- pydiet/server/image.py +342 -0
- pydiet/server/models/__init__.py +34 -0
- pydiet/server/models/dataconfig.py +195 -0
- pydiet/server/models/default.py +9 -0
- pydiet/server/models/exceptions.py +9 -0
- pydiet/server/models/instrument.py +314 -0
- pydiet/server/models/query.py +172 -0
- pydiet/server/models/response.py +97 -0
- pydiet/server/models/types.py +35 -0
- pydiet/server/photsys.py +71 -0
- pydiet/server/response.py +237 -0
- pydiet/server/types/__init__.py +8 -0
- pydiet/server/types/quantity.py +532 -0
- pydiet/server/types/string.py +318 -0
- pydiet/templates/common/base.html +80 -0
- pydiet/templates/common/plot_filter.html +17 -0
- pydiet/templates/common/privacy.html +132 -0
- pydiet/templates/common/settings.html +23 -0
- pydiet/templates/common/terms.html +101 -0
- pydiet/templates/megacam/etc_form.html +319 -0
- pydiet/templates/megacam/etc_results.html +190 -0
- pydiet/templates/wircam/etc_form.html +319 -0
- pydiet/templates/wircam/etc_results.html +190 -0
- pydiet/web_client/css/style.css +221 -0
- pydiet/web_client/dist/pydiet.js +31 -0
- pydiet/web_client/images/logo.svg +6 -0
- pydiet/web_client/images/megacam/background.jpg +0 -0
- pydiet/web_client/images/megacam/logo.png +0 -0
- pydiet/web_client/images/wircam/background.jpg +0 -0
- pydiet/web_client/images/wircam/logo.png +0 -0
- pydiet/web_client/js/dom.js +51 -0
- pydiet/web_client/js/etc.js +63 -0
- pydiet/web_client/js/fetch.js +49 -0
- pydiet/web_client/js/instrument.js +62 -0
- pydiet/web_client/js/main.js +15 -0
- pydiet/web_client/js/plot.js +88 -0
- pydiet/web_client/js/settings.js +57 -0
- pydiet/web_client/js/theme.js +43 -0
- pydiet/web_client/js/url.js +12 -0
- pydiet/web_client/jsdoc.json +20 -0
- pydiet/web_client/package.json +83 -0
- pydiet-0.9.3.dist-info/METADATA +118 -0
- pydiet-0.9.3.dist-info/RECORD +177 -0
- pydiet-0.9.3.dist-info/WHEEL +4 -0
- pydiet-0.9.3.dist-info/entry_points.txt +5 -0
- pydiet-0.9.3.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Functions that gather data from files.
|
|
3
|
+
"""
|
|
4
|
+
# Copyright CFHT/CNRS/CEA/UParisSaclay
|
|
5
|
+
# Licensed under the MIT licence
|
|
6
|
+
|
|
7
|
+
from os import PathLike, scandir
|
|
8
|
+
from os.path import basename, exists, isabs, join
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
# Manage TOML library for Python versions < 3.11
|
|
12
|
+
import sys
|
|
13
|
+
if sys.version_info >= (3, 11):
|
|
14
|
+
import tomllib
|
|
15
|
+
else:
|
|
16
|
+
import tomli as tomllib
|
|
17
|
+
import warnings
|
|
18
|
+
|
|
19
|
+
from typing import Any, IO, Optional
|
|
20
|
+
from astropy.table import QTable #type: ignore[import-untyped]
|
|
21
|
+
from astropy import units as u #type: ignore[import-untyped]
|
|
22
|
+
from astropy.modeling.models import Const1D #type: ignore[import-untyped]
|
|
23
|
+
from astropy.utils.exceptions import AstropyUserWarning #type: ignore[import-untyped]
|
|
24
|
+
from pydantic import BaseModel, Field
|
|
25
|
+
from specutils import Spectrum #type: ignore[import-untyped]
|
|
26
|
+
from synphot import ( #type: ignore[import-untyped]
|
|
27
|
+
SourceSpectrum,
|
|
28
|
+
SpectralElement,
|
|
29
|
+
ThermalSpectralElement
|
|
30
|
+
) #type: ignore[import-untyped]
|
|
31
|
+
|
|
32
|
+
from .. import package
|
|
33
|
+
from .config import override, settings
|
|
34
|
+
from .models.dataconfig import (
|
|
35
|
+
DataConfigModel,
|
|
36
|
+
DetectorConfigModel,
|
|
37
|
+
EmissionConfigModel,
|
|
38
|
+
FiltersConfigModel,
|
|
39
|
+
OpticsConfigModel,
|
|
40
|
+
TransmissionConfigModel
|
|
41
|
+
)
|
|
42
|
+
from .models.instrument import (
|
|
43
|
+
DetectorModel,
|
|
44
|
+
FiltersModel,
|
|
45
|
+
InstrumentModel,
|
|
46
|
+
OpticsModel,
|
|
47
|
+
SBSEDModel,
|
|
48
|
+
SiteModel,
|
|
49
|
+
TelescopeModel,
|
|
50
|
+
TransmissionModel
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def add_trans(self, other):
|
|
54
|
+
"""Add ``self`` with ``other``."""
|
|
55
|
+
self._validate_other_mul_div(other)
|
|
56
|
+
result = self.__class__(self.model + other.model)
|
|
57
|
+
self._merge_meta(self, other, result)
|
|
58
|
+
return result
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
SpectralElement.__add__ = add_trans
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def get_data_config(data_config: Optional[str] = None) -> DataConfigModel:
|
|
65
|
+
data_config = override("data_config", data_config)
|
|
66
|
+
assert data_config is not None # This is to make mypy happy
|
|
67
|
+
with open(data_config, "rb") as f:
|
|
68
|
+
data = tomllib.load(f)
|
|
69
|
+
assert data is not None # This is to make mypy happy
|
|
70
|
+
# if "path" value is not absolute, assume it is relative to pkg root dir
|
|
71
|
+
if not isabs(data['path']):
|
|
72
|
+
data['path'] = join(package.root_dir, data['path'])
|
|
73
|
+
data_config_model = DataConfigModel.model_validate(data)
|
|
74
|
+
return data_config_model
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def get_data_file(filename: IO[bytes] | PathLike | str):
|
|
78
|
+
return QTable.read(filename)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def get_default(d: dict) -> Any:
|
|
82
|
+
lst = [val for val in d.values() if val.default]
|
|
83
|
+
return lst[0] if len(lst) > 0 else list(d.values())[0]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def get_detector(
|
|
87
|
+
parent_dir: str,
|
|
88
|
+
detector: DetectorConfigModel) -> DetectorModel:
|
|
89
|
+
# Instantiate the model
|
|
90
|
+
transmissions = get_transmissions(
|
|
91
|
+
join(parent_dir, detector.path),
|
|
92
|
+
detector.transmission
|
|
93
|
+
)
|
|
94
|
+
emissions = get_emissions(
|
|
95
|
+
join(parent_dir, detector.path),
|
|
96
|
+
detector.emission
|
|
97
|
+
)
|
|
98
|
+
return DetectorModel(
|
|
99
|
+
gain = detector.gain,
|
|
100
|
+
ron = detector.ron,
|
|
101
|
+
scale = detector.scale,
|
|
102
|
+
transmissions = transmissions,
|
|
103
|
+
emissions = emissions
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def get_emission(
|
|
108
|
+
file: str,
|
|
109
|
+
id: str,
|
|
110
|
+
name: str="",
|
|
111
|
+
description: str="",
|
|
112
|
+
vars: dict[str, float | str]={},
|
|
113
|
+
) -> SBSEDModel:
|
|
114
|
+
data = get_data_file(file)
|
|
115
|
+
wave = u.Quantity(data['WAVELENGTH'])
|
|
116
|
+
sed = u.Quantity(data['PHOTLAM']).to(
|
|
117
|
+
u.Jy / u.arcsec**2,
|
|
118
|
+
equivalencies=u.spectral_density(wave)
|
|
119
|
+
)
|
|
120
|
+
# Instantiate the model
|
|
121
|
+
with warnings.catch_warnings():
|
|
122
|
+
warnings.filterwarnings(
|
|
123
|
+
"ignore",
|
|
124
|
+
message=".*negative flux or throughput.*",
|
|
125
|
+
category=AstropyUserWarning,
|
|
126
|
+
)
|
|
127
|
+
emission = SBSEDModel(
|
|
128
|
+
id = id,
|
|
129
|
+
name = name,
|
|
130
|
+
description = description,
|
|
131
|
+
vars = vars,
|
|
132
|
+
# We drop the surface part as Spectrum does cannot deal with SBs.
|
|
133
|
+
spectral = SourceSpectrum.from_spectrum1d(
|
|
134
|
+
Spectrum(
|
|
135
|
+
spectral_axis = wave,
|
|
136
|
+
flux = sed * u.arcsec**2
|
|
137
|
+
),
|
|
138
|
+
keep_neg=False
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
return emission
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def get_emission_from_transmission(
|
|
145
|
+
transmission: TransmissionModel,
|
|
146
|
+
temperature: u.Quantity['temperature'], #type: ignore[name-defined]
|
|
147
|
+
id: str) -> SBSEDModel:
|
|
148
|
+
flat = SpectralElement(Const1D, amplitude=1.)
|
|
149
|
+
assert transmission.spectral != None # Make mypy happy
|
|
150
|
+
emission = SBSEDModel(
|
|
151
|
+
id = id,
|
|
152
|
+
name = f"{transmission.name} emission",
|
|
153
|
+
description = f"Blackbody emission at {temperature.to(u.K).value:.1f} K",
|
|
154
|
+
# Thermal source spectral flux with Blackbody spectrum over 1 arcsec2
|
|
155
|
+
spectral = ThermalSpectralElement(
|
|
156
|
+
flat + (-1.) * transmission.spectral, # Apply emissivity
|
|
157
|
+
temperature=temperature,
|
|
158
|
+
beam_fill_factor=1.0
|
|
159
|
+
).thermal_source()
|
|
160
|
+
)
|
|
161
|
+
return emission
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
"""
|
|
165
|
+
def get_emission_from_transmission(
|
|
166
|
+
transmission: TransmissionModel,
|
|
167
|
+
temperature: u.Quantity['temperature'], #type: ignore[name-defined]
|
|
168
|
+
area: u.Quantity['area'], #type: ignore[name-defined]
|
|
169
|
+
id: str) -> SBSEDModel:
|
|
170
|
+
# Thermal source spectral flux with Blackbody spectrum over 1 arcsec2
|
|
171
|
+
bb = ThermalSpectralElement(
|
|
172
|
+
BlackBody1D,
|
|
173
|
+
temperature=temperature
|
|
174
|
+
).thermal_source() * area.to(u.m**2).value
|
|
175
|
+
emission = SBSEDModel(
|
|
176
|
+
id = id,
|
|
177
|
+
name = f"{transmission.name} emission",
|
|
178
|
+
description = f"Blackbody emission at {temperature.to(u.K).value:.1f} K",
|
|
179
|
+
# Apply emissivity
|
|
180
|
+
spectral = bb - bb * transmission.spectral
|
|
181
|
+
)
|
|
182
|
+
return emission
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def get_emissions(
|
|
187
|
+
parent_dir: str,
|
|
188
|
+
emission_config: EmissionConfigModel,
|
|
189
|
+
transmissions: dict[str, TransmissionModel] | None = None
|
|
190
|
+
) -> dict[str, SBSEDModel]:
|
|
191
|
+
emissions : dict[str, SBSEDModel] = {}
|
|
192
|
+
for file_config in emission_config.files:
|
|
193
|
+
key = file_config.id if file_config.id != '' else str(len(emissions))
|
|
194
|
+
emissions[key] = get_emission(
|
|
195
|
+
file=join(parent_dir, emission_config.path, file_config.file),
|
|
196
|
+
id=key,
|
|
197
|
+
name=file_config.name,
|
|
198
|
+
description = file_config.description,
|
|
199
|
+
vars = file_config.vars
|
|
200
|
+
)
|
|
201
|
+
# No emission files: we use a blackbody with emissivity from transmission
|
|
202
|
+
if len(emission_config.files) == 0 and transmissions is not None:
|
|
203
|
+
temperatures = emission_config.temperatures
|
|
204
|
+
for t, key in enumerate(transmissions):
|
|
205
|
+
temperature = temperatures[t] if t < len(temperatures) \
|
|
206
|
+
else temperatures[-1]
|
|
207
|
+
emissions[key] = get_emission_from_transmission(
|
|
208
|
+
transmissions[key],
|
|
209
|
+
temperature=temperature,
|
|
210
|
+
id=key
|
|
211
|
+
)
|
|
212
|
+
return emissions
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def get_filters(
|
|
216
|
+
parent_dir: str,
|
|
217
|
+
filters_config: FiltersConfigModel) -> FiltersModel:
|
|
218
|
+
path = join(parent_dir, filters_config.path)
|
|
219
|
+
transmissions = get_transmissions(path, filters_config.transmission)
|
|
220
|
+
# For emissions we may have to use transmission curves
|
|
221
|
+
emissions = get_emissions(
|
|
222
|
+
path,
|
|
223
|
+
filters_config.emission,
|
|
224
|
+
transmissions
|
|
225
|
+
)
|
|
226
|
+
return FiltersModel(
|
|
227
|
+
transmissions=transmissions,
|
|
228
|
+
emissions=emissions
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def get_instruments(
|
|
233
|
+
data_config: DataConfigModel) -> dict:
|
|
234
|
+
# Start by gathering the provided sites and telescopes
|
|
235
|
+
sites = get_sites(data_config)
|
|
236
|
+
telescopes = get_telescopes(data_config)
|
|
237
|
+
instruments = {}
|
|
238
|
+
for instrument in data_config.instruments:
|
|
239
|
+
path = join(data_config.path, instrument.path)
|
|
240
|
+
# Instantiate the model
|
|
241
|
+
instruments[instrument.id] = InstrumentModel(
|
|
242
|
+
id = instrument.id,
|
|
243
|
+
name = instrument.name,
|
|
244
|
+
description = instrument.description,
|
|
245
|
+
wavelength_range = instrument.wavelength_range,
|
|
246
|
+
obstruction_area = instrument.obstruction_area,
|
|
247
|
+
overhead = instrument.overhead,
|
|
248
|
+
optics = get_optics(path, instrument.optics),
|
|
249
|
+
filters = get_filters(path, instrument.filters),
|
|
250
|
+
detector = get_detector(path, instrument.detector),
|
|
251
|
+
telescope = telescopes[instrument.telescope_id],
|
|
252
|
+
site = sites[instrument.site_id],
|
|
253
|
+
default = instrument.default
|
|
254
|
+
)
|
|
255
|
+
return instruments
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def get_optics(
|
|
259
|
+
parent_dir: str,
|
|
260
|
+
optics_config: OpticsConfigModel) -> OpticsModel:
|
|
261
|
+
path = join(parent_dir, optics_config.path)
|
|
262
|
+
transmissions = get_transmissions(path, optics_config.transmission)
|
|
263
|
+
# For emissions we may have to use transmission curves
|
|
264
|
+
emissions = get_emissions(
|
|
265
|
+
path,
|
|
266
|
+
optics_config.emission,
|
|
267
|
+
transmissions
|
|
268
|
+
)
|
|
269
|
+
return OpticsModel(
|
|
270
|
+
transmissions=transmissions,
|
|
271
|
+
emissions=emissions
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def get_sites(data_config: DataConfigModel) -> dict[str, SiteModel]:
|
|
276
|
+
sites = {}
|
|
277
|
+
for site in data_config.sites:
|
|
278
|
+
path = join(data_config.path, site.path)
|
|
279
|
+
# Instantiate the model
|
|
280
|
+
sites[site.id] = SiteModel(
|
|
281
|
+
id = site.id,
|
|
282
|
+
name = site.name,
|
|
283
|
+
description = site.description,
|
|
284
|
+
sky_transmissions = get_transmissions(path, site.transmission),
|
|
285
|
+
sky_emissions = get_emissions(path, site.emission),
|
|
286
|
+
default = site.default
|
|
287
|
+
)
|
|
288
|
+
return sites
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def get_telescopes(data_config: DataConfigModel) -> dict[str, TelescopeModel]:
|
|
292
|
+
telescopes = {}
|
|
293
|
+
for telescope in data_config.telescopes:
|
|
294
|
+
path = join(data_config.path, telescope.path)
|
|
295
|
+
# Instantiate the model
|
|
296
|
+
transmissions = get_transmissions(path, telescope.transmission)
|
|
297
|
+
emissions = get_emissions(path, telescope.emission, transmissions)
|
|
298
|
+
telescopes[telescope.id] = TelescopeModel(
|
|
299
|
+
id = telescope.id,
|
|
300
|
+
name = telescope.name,
|
|
301
|
+
description = telescope.description,
|
|
302
|
+
collecting_area = telescope.collecting_area,
|
|
303
|
+
obstruction_area = telescope.obstruction_area,
|
|
304
|
+
transmissions = transmissions,
|
|
305
|
+
emissions = emissions,
|
|
306
|
+
default = telescope.default
|
|
307
|
+
)
|
|
308
|
+
return telescopes
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def get_transmission(
|
|
312
|
+
file: IO | PathLike | str,
|
|
313
|
+
id: str,
|
|
314
|
+
name: str="",
|
|
315
|
+
description: str="",
|
|
316
|
+
vars: dict[str, float | str]={}
|
|
317
|
+
) -> TransmissionModel:
|
|
318
|
+
data = get_data_file(file)
|
|
319
|
+
# Instantiate the model
|
|
320
|
+
wave = u.Quantity(data['WAVELENGTH'])
|
|
321
|
+
response = u.Quantity(data['THROUGHPUT'])
|
|
322
|
+
with warnings.catch_warnings():
|
|
323
|
+
warnings.filterwarnings(
|
|
324
|
+
"ignore",
|
|
325
|
+
message=".*negative flux or throughput.*",
|
|
326
|
+
category=AstropyUserWarning,
|
|
327
|
+
)
|
|
328
|
+
transmission = TransmissionModel(
|
|
329
|
+
id = id,
|
|
330
|
+
name = name,
|
|
331
|
+
description = description,
|
|
332
|
+
vars = vars,
|
|
333
|
+
# Apply tapering to filters to avoid possible spurious spectral leaks
|
|
334
|
+
spectral = SpectralElement.from_spectrum1d(
|
|
335
|
+
Spectrum(spectral_axis=wave, flux=response),
|
|
336
|
+
keep_neg=False
|
|
337
|
+
).taper()
|
|
338
|
+
)
|
|
339
|
+
return transmission
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def get_transmissions(
|
|
343
|
+
parent_dir: str,
|
|
344
|
+
transmission_config: TransmissionConfigModel) -> dict[str, TransmissionModel]:
|
|
345
|
+
transmissions : dict[str, TransmissionModel] = {}
|
|
346
|
+
for file_config in transmission_config.files:
|
|
347
|
+
key = file_config.id if file_config.id != '' else str(len(transmissions))
|
|
348
|
+
transmissions[key] = get_transmission(
|
|
349
|
+
file=join(parent_dir, transmission_config.path, file_config.file),
|
|
350
|
+
id=file_config.id,
|
|
351
|
+
name=file_config.name,
|
|
352
|
+
description=file_config.description,
|
|
353
|
+
vars=file_config.vars
|
|
354
|
+
)
|
|
355
|
+
return transmissions
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def get_webapi_instruments(instruments: dict[str, InstrumentModel]) -> dict[str, InstrumentModel]:
|
|
359
|
+
winstruments = {}
|
|
360
|
+
for instrument in instruments:
|
|
361
|
+
winstruments[instrument] = instruments[instrument].copy(exclude={
|
|
362
|
+
'site': {'sky_emissions', 'sky_transmissions'},
|
|
363
|
+
'transmissions' : True,
|
|
364
|
+
'emissions_ct' : True
|
|
365
|
+
})
|
|
366
|
+
return winstruments
|
|
367
|
+
|
pydiet/server/image.py
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Image simulation module
|
|
3
|
+
"""
|
|
4
|
+
# Copyright CFHT
|
|
5
|
+
# Licensed under the MIT licence
|
|
6
|
+
|
|
7
|
+
from typing import Literal, Tuple
|
|
8
|
+
|
|
9
|
+
from astropy import units as u #type: ignore[import-untyped]
|
|
10
|
+
from base64 import b64encode
|
|
11
|
+
from io import BytesIO
|
|
12
|
+
from PIL.Image import fromarray
|
|
13
|
+
import numpy as np
|
|
14
|
+
from scipy.optimize import brentq, minimize_scalar #type: ignore[import-untyped]
|
|
15
|
+
from synphot import Observation, SpectralElement #type: ignore[import-untyped]
|
|
16
|
+
|
|
17
|
+
from .models.types import PhotometryID, SourceID
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Image(object):
|
|
22
|
+
"""
|
|
23
|
+
Raster image generation class for computing exposure times and SNRs
|
|
24
|
+
|
|
25
|
+
Examples
|
|
26
|
+
--------
|
|
27
|
+
>>> from astropy import units as u
|
|
28
|
+
|
|
29
|
+
>>> img = Image(
|
|
30
|
+
... source='point_source',
|
|
31
|
+
... psf_fwhm=0.8 * u.arcsec,
|
|
32
|
+
... psf_beta=3.2,
|
|
33
|
+
... pixel=(0.186 * u.arcsec, 0.186 * u.arcsec),
|
|
34
|
+
... rate=42.,
|
|
35
|
+
... bkg_rate=10.,
|
|
36
|
+
... ron=4.,
|
|
37
|
+
... gain=1.65,
|
|
38
|
+
... photometry='model_fitting'
|
|
39
|
+
... )
|
|
40
|
+
|
|
41
|
+
>>> # Compute SNR from exposure time
|
|
42
|
+
>>> print(f"{img.snr(etime=10.):.1f}")
|
|
43
|
+
4.6
|
|
44
|
+
|
|
45
|
+
>>> # Compute Exposure time from SNR
|
|
46
|
+
>>> print(f"{img.etime(snr=10.):.1f}")
|
|
47
|
+
43.0
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
Parameters
|
|
51
|
+
----------
|
|
52
|
+
source: Literal['point_source', 'galaxy', 'extended'], optional
|
|
53
|
+
Source type.
|
|
54
|
+
psf_fwhm: ~astropy.units.Quantity['angle'], optional
|
|
55
|
+
Full Width at Half Maximum of the Point Spread Function.
|
|
56
|
+
psf_beta: float, optional
|
|
57
|
+
Moffat beta parameter of the Point Spread Function.
|
|
58
|
+
sersic_radius: ~astropy.units.Quantity['angle'], optional
|
|
59
|
+
Half-light radius for Sersic galaxy profiles.
|
|
60
|
+
sersic_index: float, optional
|
|
61
|
+
Sersic index for galaxy profiles.
|
|
62
|
+
pixel: ~astropy.units.Quantity['angle'], optional
|
|
63
|
+
Pixel scale on each axis.
|
|
64
|
+
image_size: Tuple[int, int], optional
|
|
65
|
+
Image size in pixels on each axis.
|
|
66
|
+
rate: float, optional
|
|
67
|
+
Number of photons per second.
|
|
68
|
+
bkg_rate: float, optional
|
|
69
|
+
Total background photon rate in photons per second per pixel.
|
|
70
|
+
ron: float, optional
|
|
71
|
+
Detector read out noise standard deviation in electrons.
|
|
72
|
+
gain: float, optional
|
|
73
|
+
Detector conversion factor in e-/ADU.
|
|
74
|
+
full_well: float, optional
|
|
75
|
+
Detector full well in electrons.
|
|
76
|
+
range: int, optional
|
|
77
|
+
Digital range, in analog-to-digital converter steps.
|
|
78
|
+
bias: float, optional
|
|
79
|
+
Detector bias in ADUs.
|
|
80
|
+
photometry: Literal['model_fitting', 'fixed_aperture', 'optimal_aperture', 'large_aperture']
|
|
81
|
+
Photometric measurement type.
|
|
82
|
+
aperture: float, optional
|
|
83
|
+
Aperture diameter in pixels for fixed aperture photometry.
|
|
84
|
+
oversamp: int, optional
|
|
85
|
+
Number of oversampling sub pixels on each axis.
|
|
86
|
+
max_etime: ~astropy.units.Quantity['time'], optional
|
|
87
|
+
Maximum possible exposure time in output
|
|
88
|
+
"""
|
|
89
|
+
def __init__(
|
|
90
|
+
self,
|
|
91
|
+
source: SourceID='point_source',
|
|
92
|
+
psf_fwhm: u.Quantity['angle']=1.*u.arcsec, #type: ignore[name-defined]
|
|
93
|
+
psf_beta: float=3.2,
|
|
94
|
+
sersic_radius: u.Quantity['angle']=1.*u.arcsec, #type: ignore[name-defined]
|
|
95
|
+
sersic_index: float=1.,
|
|
96
|
+
pixel: u.Quantity['angle']=(0.2, 0.2)*u.arcsec, #type: ignore[name-defined]
|
|
97
|
+
image_size: Tuple[int, int]=(64, 64),
|
|
98
|
+
rate: float=1.,
|
|
99
|
+
bkg_rate: float=0.,
|
|
100
|
+
ron: float=0.,
|
|
101
|
+
gain: float=1.,
|
|
102
|
+
full_well: float=1e6,
|
|
103
|
+
range: int=65536,
|
|
104
|
+
bias: float=0.,
|
|
105
|
+
photometry: PhotometryID='model_fitting',
|
|
106
|
+
aperture: float=3.,
|
|
107
|
+
oversamp: int=1,
|
|
108
|
+
max_etime: u.Quantity['time'] = 1e9 * u.s) -> None: #type: ignore[name-defined]
|
|
109
|
+
|
|
110
|
+
self.source = source
|
|
111
|
+
self.pixel = pixel
|
|
112
|
+
self.rate = rate
|
|
113
|
+
self.bkg_rate = bkg_rate
|
|
114
|
+
self.ron = ron
|
|
115
|
+
self.var_rate = rate
|
|
116
|
+
self.var_bkg_rate = bkg_rate
|
|
117
|
+
self.var_ron = ron*ron
|
|
118
|
+
self.gain = gain
|
|
119
|
+
self.oversamp = oversamp
|
|
120
|
+
self.photometry = photometry
|
|
121
|
+
self.saturation = min(range - 1. - bias, full_well / gain)
|
|
122
|
+
self.max_etime = max_etime.to(u.s).value
|
|
123
|
+
|
|
124
|
+
# Create image coordinate rasters
|
|
125
|
+
raster_size = [image_size[0] * oversamp, image_size[1] * oversamp]
|
|
126
|
+
yx = np.mgrid[
|
|
127
|
+
-raster_size[0]//2:raster_size[0] - raster_size[0]//2,
|
|
128
|
+
-raster_size[1]//2:raster_size[1] - raster_size[1]//2
|
|
129
|
+
].astype(np.float32)
|
|
130
|
+
r2 = yx[0]**2 + yx[1]**2
|
|
131
|
+
self.r2 = r2
|
|
132
|
+
|
|
133
|
+
# Create truncation disk
|
|
134
|
+
self.mask_r2 = r2[0, raster_size[1]//2]
|
|
135
|
+
self.mask = r2 <= self.mask_r2
|
|
136
|
+
|
|
137
|
+
self.pixel_area = (pixel[0] * pixel[1]) / oversamp**2 * u.pix**2
|
|
138
|
+
|
|
139
|
+
if source == 'extended':
|
|
140
|
+
self.image = self.extended()
|
|
141
|
+
return
|
|
142
|
+
|
|
143
|
+
# Rasterize the PSF
|
|
144
|
+
if psf_beta <= 1.:
|
|
145
|
+
raise ValueError("Moffat beta must be > 1.")
|
|
146
|
+
|
|
147
|
+
# Compute the square of the alpha parameter from the FWHM
|
|
148
|
+
alpha2 = u.Quantity(psf_fwhm)**2 / (4. * (2.**(1./psf_beta) - 1.)) \
|
|
149
|
+
/ self.pixel_area
|
|
150
|
+
|
|
151
|
+
# Create PSF raster
|
|
152
|
+
moffat = np.power(1. + r2 / alpha2, -psf_beta)
|
|
153
|
+
|
|
154
|
+
# Truncate inside a disk
|
|
155
|
+
moffat *= self.mask
|
|
156
|
+
self.psf = moffat / moffat.sum()
|
|
157
|
+
|
|
158
|
+
# Generate star or galaxy image
|
|
159
|
+
self.image = self.sersic(
|
|
160
|
+
re=sersic_radius,
|
|
161
|
+
n=sersic_index
|
|
162
|
+
) if source == 'galaxy' else self.psf
|
|
163
|
+
|
|
164
|
+
# Create photometry measurement aperture
|
|
165
|
+
if self.photometry != 'model_fitting':
|
|
166
|
+
if self.photometry == 'fixed_aperture':
|
|
167
|
+
# User-provided aperture diameter
|
|
168
|
+
r2max = aperture**2 * u.arcsec**2 / self.pixel_area
|
|
169
|
+
elif self.photometry == 'large_aperture':
|
|
170
|
+
# Aperture enclosing 96% of the flux
|
|
171
|
+
r2max = brentq(
|
|
172
|
+
f = lambda r2: np.sum((self.r2 <= r2) * self.image) - 0.96,
|
|
173
|
+
a=0.,
|
|
174
|
+
b=self.mask_r2,
|
|
175
|
+
xtol=1e-3,
|
|
176
|
+
maxiter=100
|
|
177
|
+
)
|
|
178
|
+
elif self.photometry == 'optimal_aperture':
|
|
179
|
+
r2max = 0.
|
|
180
|
+
self.aperture = r2 < r2max
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def delta_snr2(self, etime: float, snr: float) -> float:
|
|
184
|
+
return self.snr(etime=etime)**2 - snr**2
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def etime(self, snr: float) -> float:
|
|
188
|
+
return brentq(
|
|
189
|
+
f=self.delta_snr2,
|
|
190
|
+
a=0.,
|
|
191
|
+
b=self.etime_max(snr),
|
|
192
|
+
args=(snr),
|
|
193
|
+
xtol=1e-6,
|
|
194
|
+
maxiter=100
|
|
195
|
+
) if self.rate > 0. else self.max_etime
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def etime_bkg_sat(self) -> float:
|
|
199
|
+
return self.saturation / self.bkg_rate if self.bkg_rate > 0. else self.max_etime
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def etime_max(self, snr:float) -> float:
|
|
203
|
+
# Find exposure time range for root finding
|
|
204
|
+
t_high = 1.
|
|
205
|
+
while (tsnr:=self.snr(t_high)) < snr and tsnr < 1.e12:
|
|
206
|
+
t_high *= 2.
|
|
207
|
+
return t_high
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def etime_source_sat(self) -> float:
|
|
211
|
+
return self.saturation / self.max() if self.rate > 0. and self.bkg_rate > 0. \
|
|
212
|
+
else self.max_etime
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def extended(self) -> np.ndarray:
|
|
216
|
+
return self.mask * self.pixel_area.to(u.arcsec**2).value
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def gif(self, etime: float, exposures: int=1, frames: int=10) -> str:
|
|
220
|
+
# Initialize random generator
|
|
221
|
+
rng = np.random.default_rng()
|
|
222
|
+
# Use the PSF as a template image and generate a noiseless image
|
|
223
|
+
noiseless = (self.rate * np.array([self.image] * frames) + self.bkg_rate) * etime
|
|
224
|
+
# Generate Poisson + Gaussian noise realizations
|
|
225
|
+
# We add a 3 sigma offset above the background to prevent negative values
|
|
226
|
+
sigmas = 3.*(self.ron*self.ron + self.bkg_rate * etime)**0.5 \
|
|
227
|
+
/ np.sqrt(exposures)
|
|
228
|
+
offset = sigmas - self.bkg_rate * etime
|
|
229
|
+
nmax = (noiseless.max() + offset + sigmas) / self.gain
|
|
230
|
+
noisy = np.round(
|
|
231
|
+
exposures * (
|
|
232
|
+
rng.poisson(lam=noiseless*exposures) / exposures + rng.normal(
|
|
233
|
+
loc=offset,
|
|
234
|
+
scale=self.ron / np.sqrt(exposures),
|
|
235
|
+
size=noiseless.shape
|
|
236
|
+
)
|
|
237
|
+
) / self.gain
|
|
238
|
+
) / exposures
|
|
239
|
+
# Normalize to a max of 1
|
|
240
|
+
noisy[noisy < 0.] = 0.
|
|
241
|
+
noisy /= nmax
|
|
242
|
+
noisy[noisy > 1.] = 1.
|
|
243
|
+
# Apply sRGB gamma correction and convert to 0...255 unsigned integers
|
|
244
|
+
noisy = (
|
|
245
|
+
np.where(
|
|
246
|
+
noisy <= 0.0031308,
|
|
247
|
+
12.92 * noisy,
|
|
248
|
+
1.055 * np.power(noisy, 1./2.4) - 0.055
|
|
249
|
+
) * 255.
|
|
250
|
+
).astype(np.uint8)
|
|
251
|
+
|
|
252
|
+
# Create image buffer
|
|
253
|
+
buffer = BytesIO()
|
|
254
|
+
# Save GIF to buffer and append the rest of the animation
|
|
255
|
+
fromarray(noisy[0], mode="L").save(
|
|
256
|
+
buffer,
|
|
257
|
+
format='GIF',
|
|
258
|
+
save_all=True,
|
|
259
|
+
append_images=[fromarray(im) for im in noisy[1:]],
|
|
260
|
+
duration=100,
|
|
261
|
+
loop=0
|
|
262
|
+
)
|
|
263
|
+
buffer.seek(0)
|
|
264
|
+
# Encode GIF as base64
|
|
265
|
+
gif_base64 = b64encode(buffer.read()).decode("utf-8")
|
|
266
|
+
return f"data:image/gif;base64,{gif_base64}"
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def max(self) -> float:
|
|
270
|
+
return self.rate * self.image.max() + self.bkg_rate
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def sersic(
|
|
274
|
+
self,
|
|
275
|
+
re: u.Quantity['angle']=1.*u.arcsec, #type: ignore[name-defined]
|
|
276
|
+
n: float=1.) -> np.ndarray:
|
|
277
|
+
# Model validity limits
|
|
278
|
+
if n < 0.36:
|
|
279
|
+
n = 0.36
|
|
280
|
+
# R_e normalization from Ciotti & Bertin 1999
|
|
281
|
+
bn = 2.* n - 0.333333 + 9.8765e-3*n**(-1) + 1.8029e-3 * n**(-2) \
|
|
282
|
+
+ 1.1409e-4 * n**(-3) - 7.151e-5 * n**(-4)
|
|
283
|
+
inv2n = 0.5 / n
|
|
284
|
+
invre2 = (self.pixel_area / re**2).value
|
|
285
|
+
sersic = np.exp(-bn * (np.power(self.r2 * invre2, inv2n) - 1.))
|
|
286
|
+
sersic = np.fft.irfft2(
|
|
287
|
+
np.fft.rfft2(sersic) * np.fft.rfft2(np.fft.fftshift(self.psf))
|
|
288
|
+
) * self.mask
|
|
289
|
+
return sersic / np.sum(sersic)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def snr(self, etime: float=1.) -> float:
|
|
293
|
+
# First treat special case of extended source
|
|
294
|
+
if self.source == 'extended':
|
|
295
|
+
# Compute pixel area in arcsec2
|
|
296
|
+
invarea = 1. / self.pixel_area.to(u.arcsec**2).value
|
|
297
|
+
return self.rate * etime / np.sqrt(
|
|
298
|
+
(self.var_bkg_rate * invarea + self.var_rate) * etime \
|
|
299
|
+
+ self.var_ron * invarea
|
|
300
|
+
)
|
|
301
|
+
# Compute the "noise variance image"
|
|
302
|
+
img2 = self.image**2
|
|
303
|
+
var_tot = self.var_ron + (
|
|
304
|
+
self.var_bkg_rate + self.var_rate * self.image
|
|
305
|
+
) * etime
|
|
306
|
+
if self.photometry == 'optimal_aperture':
|
|
307
|
+
# (Re-)compute optimal aperture
|
|
308
|
+
res = minimize_scalar(
|
|
309
|
+
fun = lambda r2: - self.snr_aper(
|
|
310
|
+
self.rate* etime,
|
|
311
|
+
self.image,
|
|
312
|
+
var_tot,
|
|
313
|
+
self.r2 < r2,
|
|
314
|
+
),
|
|
315
|
+
bounds=(0., self.mask_r2),
|
|
316
|
+
method='bounded'
|
|
317
|
+
)
|
|
318
|
+
# Return SNR at optimal aperture
|
|
319
|
+
return -res.fun
|
|
320
|
+
elif self.photometry == 'model_fitting':
|
|
321
|
+
# Return model-fitting SNR
|
|
322
|
+
return self.rate * etime * np.sqrt(
|
|
323
|
+
np.sum(img2 / var_tot + img2 / (2. * var_tot**2))
|
|
324
|
+
)
|
|
325
|
+
else:
|
|
326
|
+
# Return SNR for a predefined aperture
|
|
327
|
+
return self.snr_aper(
|
|
328
|
+
self.rate * etime,
|
|
329
|
+
self.image,
|
|
330
|
+
var_tot,
|
|
331
|
+
self.aperture
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
def snr_aper(
|
|
335
|
+
self,
|
|
336
|
+
photons: float,
|
|
337
|
+
obj: np.ndarray,
|
|
338
|
+
var: np.ndarray,
|
|
339
|
+
aper: np.ndarray) -> float:
|
|
340
|
+
return photons * np.sum(obj * aper) / np.sqrt(np.sum(var * aper))
|
|
341
|
+
|
|
342
|
+
|