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,172 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Query models
|
|
3
|
+
"""
|
|
4
|
+
# Copyright CFHT/CNRS/CEA/UParisSaclay
|
|
5
|
+
# Licensed under the MIT licence
|
|
6
|
+
|
|
7
|
+
from typing import Literal
|
|
8
|
+
from pydantic import (
|
|
9
|
+
BaseModel,
|
|
10
|
+
Field,
|
|
11
|
+
PydanticUserError,
|
|
12
|
+
ValidationInfo,
|
|
13
|
+
field_validator
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from .default import default_filter, default_instrument, filters, instruments
|
|
17
|
+
from .exceptions import ETCValidationError
|
|
18
|
+
from .types import (
|
|
19
|
+
ComputeID,
|
|
20
|
+
FilterID,
|
|
21
|
+
InstrumentID,
|
|
22
|
+
PhotometryID,
|
|
23
|
+
PhotSysID,
|
|
24
|
+
SkyID,
|
|
25
|
+
SourceID,
|
|
26
|
+
StackingID
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
class ETCQueryModel(BaseModel):
|
|
30
|
+
|
|
31
|
+
instrument: InstrumentID = Field(
|
|
32
|
+
default=default_instrument.id,
|
|
33
|
+
description="Instrument ID"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
airmass: float = Field(
|
|
37
|
+
default=1.2,
|
|
38
|
+
ge=1.,
|
|
39
|
+
description="Observation airmass"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
aperture: float = Field(
|
|
43
|
+
default=3.,
|
|
44
|
+
gt=0.,
|
|
45
|
+
le=15.,
|
|
46
|
+
description="Photometric aperture diameter [\"]"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
brightness: float = Field(
|
|
50
|
+
default=20.,
|
|
51
|
+
ge=-100.,
|
|
52
|
+
le=1000.,
|
|
53
|
+
description="Source brightness"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
compute: ComputeID = Field(
|
|
57
|
+
default='etime',
|
|
58
|
+
description="Computation type"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
etime: float = Field(
|
|
62
|
+
default=20.,
|
|
63
|
+
ge=0.,
|
|
64
|
+
le=1e30,
|
|
65
|
+
description="Exposure time [s]"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
exposures: int = Field(
|
|
69
|
+
default=1,
|
|
70
|
+
ge=1,
|
|
71
|
+
le=1000000,
|
|
72
|
+
description="Number of exposures"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
filter: FilterID = Field(
|
|
76
|
+
default=default_filter,
|
|
77
|
+
description="Instrument filter"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
photometry: PhotometryID = Field(
|
|
81
|
+
default='model_fitting',
|
|
82
|
+
description="Photometry type"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
seeing: float = Field(
|
|
86
|
+
default=0.7,
|
|
87
|
+
ge=0.1,
|
|
88
|
+
le=100.
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
sky: SkyID = Field(
|
|
92
|
+
default='dark',
|
|
93
|
+
description="Sky setting"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
sky_brightness: float = Field(
|
|
97
|
+
default=22.,
|
|
98
|
+
ge=-100.,
|
|
99
|
+
le=1000.,
|
|
100
|
+
description="Sky surface brightness"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
sersic_radius: float = Field(
|
|
104
|
+
default=1.,
|
|
105
|
+
gt=0.,
|
|
106
|
+
le=10.,
|
|
107
|
+
description="Sérsic effective radius [\"]"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
sersic_index: float = Field(
|
|
111
|
+
default=1.,
|
|
112
|
+
ge=0.3,
|
|
113
|
+
le=10.,
|
|
114
|
+
description="Sérsic index"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
sky_unit: PhotSysID = Field(
|
|
118
|
+
default='abmag',
|
|
119
|
+
description="Sky background photometric system"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
snr: float = Field(
|
|
123
|
+
default=10.,
|
|
124
|
+
gt=0.,
|
|
125
|
+
description="Required source Signal-to-Noise Ratio"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
source: SourceID = Field(
|
|
129
|
+
default='point_source',
|
|
130
|
+
description="Source type"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
stacking: StackingID = Field(
|
|
134
|
+
default='median',
|
|
135
|
+
description="Stacking method"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
transparency: float = Field(
|
|
139
|
+
default=1.,
|
|
140
|
+
gt=0.,
|
|
141
|
+
le=1.,
|
|
142
|
+
description="Sky transparency"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
unit: PhotSysID = Field(
|
|
146
|
+
default='abmag',
|
|
147
|
+
description="Photometric system"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
@field_validator('filter')
|
|
151
|
+
def validate_filter(cls, f: str, info: ValidationInfo) -> str:
|
|
152
|
+
"""
|
|
153
|
+
Kind of emulate Enum validation and errors.
|
|
154
|
+
"""
|
|
155
|
+
instrument = info.data['instrument']
|
|
156
|
+
fids = list(instruments[instrument].filters.transmissions) + ['upload']
|
|
157
|
+
if f not in fids:
|
|
158
|
+
expected = f"'{fids[0]}'" + \
|
|
159
|
+
(
|
|
160
|
+
"".join(f", '{fid}'" for fid in fids[:-1]) \
|
|
161
|
+
if len(fids) > 2 else ""
|
|
162
|
+
) + (
|
|
163
|
+
f" or '{fids[-1]}'" if len(fids) > 1 else ""
|
|
164
|
+
)
|
|
165
|
+
raise ETCValidationError({
|
|
166
|
+
"type": "enum",
|
|
167
|
+
"loc": ("query", "filter"),
|
|
168
|
+
"input": str(f),
|
|
169
|
+
"expected": expected
|
|
170
|
+
})
|
|
171
|
+
return f
|
|
172
|
+
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Response models
|
|
3
|
+
"""
|
|
4
|
+
# Copyright CFHT/CNRS/CEA/UParisSaclay
|
|
5
|
+
# Licensed under the MIT licence
|
|
6
|
+
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from astropy import units as u #type: ignore[import-untyped]
|
|
10
|
+
from pydantic import BaseModel, Field, Json
|
|
11
|
+
|
|
12
|
+
from ..types import AnnotatedQuantity
|
|
13
|
+
from .types import ComputeID, FilterID, InstrumentID
|
|
14
|
+
from .instrument import TransmissionModel
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ETCResponseModel(BaseModel):
|
|
18
|
+
|
|
19
|
+
instrument: str
|
|
20
|
+
|
|
21
|
+
filter: str
|
|
22
|
+
|
|
23
|
+
compute: ComputeID
|
|
24
|
+
|
|
25
|
+
zp: float=Field(
|
|
26
|
+
default=0.,
|
|
27
|
+
ge=-100.,
|
|
28
|
+
le=100.,
|
|
29
|
+
description="Estimate magnitude zero-point"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
snr: float=Field(
|
|
33
|
+
default=10.,
|
|
34
|
+
ge=0.,
|
|
35
|
+
lt=1e30,
|
|
36
|
+
description="Estimated source Signal-to-Noise Ratio"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
etime: float=Field(
|
|
40
|
+
default=1.,
|
|
41
|
+
ge=0.,
|
|
42
|
+
lt=1e30,
|
|
43
|
+
description="Estimated exposure time"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
etime_skysat: float=Field(
|
|
47
|
+
default=0.,
|
|
48
|
+
ge=0.,
|
|
49
|
+
lt=1e30,
|
|
50
|
+
description="Estimated exposure time for sky background saturation"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
etime_sourcesat: float=Field(
|
|
54
|
+
default=0.,
|
|
55
|
+
ge=0.,
|
|
56
|
+
lt=1e30,
|
|
57
|
+
description="Estimated exposure time for source saturation"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
ttime: float=Field(
|
|
61
|
+
default=1.,
|
|
62
|
+
ge=0.,
|
|
63
|
+
lt=1e30,
|
|
64
|
+
description="Estimated total time"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
sky_mag: float=Field(
|
|
68
|
+
default=99.,
|
|
69
|
+
ge=-100.,
|
|
70
|
+
le=100.,
|
|
71
|
+
description="Estimated sky background in mag/arcsec2"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
lambda_pivot: float=Field(
|
|
75
|
+
default=0.,
|
|
76
|
+
ge=0.,
|
|
77
|
+
lt=1e12,
|
|
78
|
+
description="Pivot wavelength of the full filter response in nm"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
bandwidth_rect: float=Field(
|
|
82
|
+
default=0.,
|
|
83
|
+
ge=0.,
|
|
84
|
+
lt=1e12,
|
|
85
|
+
description="Equivalent rectangular bandwidth of the full filter response in nm"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
cutout: Optional[str]=Field(
|
|
89
|
+
default=None,
|
|
90
|
+
description="GIF animation of the source"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
filter_transmission: Optional[Json] = None
|
|
94
|
+
|
|
95
|
+
atmosphere_transmission: Optional[Json] = None
|
|
96
|
+
|
|
97
|
+
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom types for PyDIET data models
|
|
3
|
+
"""
|
|
4
|
+
# Copyright CFHT/CNRS/CEA/UParisSaclay
|
|
5
|
+
# Licensed under the MIT licence
|
|
6
|
+
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from typing import Literal
|
|
9
|
+
|
|
10
|
+
from .default import filters, instruments
|
|
11
|
+
|
|
12
|
+
ComputeID = Literal['etime', 'snr']
|
|
13
|
+
|
|
14
|
+
FilterID = Enum( # type: ignore[misc]
|
|
15
|
+
"FilterID",
|
|
16
|
+
{tag : tag for tag in filters.keys()} | {'upload' : 'upload'},
|
|
17
|
+
type=str
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
InstrumentID = Enum( # type: ignore[misc]
|
|
21
|
+
"InstrumentID",
|
|
22
|
+
{tag : tag for tag in instruments.keys()},
|
|
23
|
+
type=str
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
SkyID = Literal['dark', 'grey', 'bright', 'specify']
|
|
27
|
+
|
|
28
|
+
PhotometryID = Literal['model_fitting', 'fixed_aperture', 'optimal_aperture', 'large_aperture']
|
|
29
|
+
|
|
30
|
+
PhotSysID = Literal['abmag', 'vegamag', 'fmegajy', 'fmujy', 'flux', 'photons']
|
|
31
|
+
|
|
32
|
+
SourceID = Literal['point_source', 'galaxy', 'extended']
|
|
33
|
+
|
|
34
|
+
StackingID = Literal['average', 'median']
|
|
35
|
+
|
pydiet/server/photsys.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Photometric system conversion module
|
|
3
|
+
"""
|
|
4
|
+
# Copyright CFHT
|
|
5
|
+
# Licensed under the MIT licence
|
|
6
|
+
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from astropy import units as u #type: ignore[import-untyped]
|
|
10
|
+
|
|
11
|
+
from .models.types import PhotSysID
|
|
12
|
+
|
|
13
|
+
from .data import ab_spectrum, vega_spectrum
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PhotSys(object):
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
id: PhotSysID = 'abmag',
|
|
20
|
+
wavelength: Optional[u.Quantity[u.nm]] = None,
|
|
21
|
+
dwavelength: Optional[u.Quantity[u.nm]] = None):
|
|
22
|
+
|
|
23
|
+
self.eps = 1e-30
|
|
24
|
+
self.id = id
|
|
25
|
+
if id == 'abmag':
|
|
26
|
+
self.spectrum = ab_spectrum
|
|
27
|
+
self.photon_rate = self._rate_from_mag
|
|
28
|
+
elif id == 'vegamag':
|
|
29
|
+
self.spectrum = vega_spectrum
|
|
30
|
+
self.photon_rate = self._rate_from_mag #type: ignore[assignment]
|
|
31
|
+
elif id == 'fmegajy':
|
|
32
|
+
self.spectrum = ab_spectrum
|
|
33
|
+
self.photon_rate = self._rate_from_fmegajy #type: ignore[assignment]
|
|
34
|
+
elif id == 'fmujy':
|
|
35
|
+
self.spectrum = ab_spectrum
|
|
36
|
+
self.photon_rate = self._rate_from_fmujy #type: ignore[assignment]
|
|
37
|
+
elif id == 'flux':
|
|
38
|
+
self.spectrum = ab_spectrum
|
|
39
|
+
self.photon_rate = self._rate_from_flux #type: ignore[assignment]
|
|
40
|
+
if wavelength is None:
|
|
41
|
+
raise ValueError("wavelength is required when id == 'flux'")
|
|
42
|
+
if dwavelength is None:
|
|
43
|
+
raise ValueError("dwavelength is required when id == 'flux'")
|
|
44
|
+
self.wavelength = wavelength.to_value(u.nm)
|
|
45
|
+
self.dwavelength = dwavelength.to_value(u.nm)
|
|
46
|
+
else:
|
|
47
|
+
self.spectrum = None
|
|
48
|
+
self.photon_rate = self._rate_from_photons #type: ignore[assignment]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _rate_from_mag(self, mag: float) -> float:
|
|
52
|
+
return 10.**(-0.4 * mag) if mag < 40. else 1e-16
|
|
53
|
+
|
|
54
|
+
def _rate_from_fmegajy(self, fnu: float) -> float:
|
|
55
|
+
return 2.754e+02 * (fnu if fnu >= self.eps else self.eps)
|
|
56
|
+
|
|
57
|
+
def _rate_from_fmujy(self, fnu: float) -> float:
|
|
58
|
+
return 2.754e-10 * (fnu if fnu >= self.eps else self.eps)
|
|
59
|
+
|
|
60
|
+
def _rate_from_flux(self, flux: float) -> float:
|
|
61
|
+
if self.dwavelength < self.eps:
|
|
62
|
+
self.dwavelength = self.eps
|
|
63
|
+
# 10**(0.4*48.6) / 3e17 (in nm/s) = 91.808
|
|
64
|
+
return 91.808 * self.wavelength ** 2 / self.dwavelength \
|
|
65
|
+
* (flux if flux >= self.eps else self.eps) * 1e-15
|
|
66
|
+
|
|
67
|
+
def _rate_from_photons(self, photons: float) -> float:
|
|
68
|
+
return photons if photons >= self.eps else self.eps
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Computation module
|
|
3
|
+
"""
|
|
4
|
+
# Copyright CFHT
|
|
5
|
+
# Licensed under the MIT licence
|
|
6
|
+
|
|
7
|
+
from math import pi, sqrt
|
|
8
|
+
from os import PathLike
|
|
9
|
+
from typing import IO, Optional
|
|
10
|
+
|
|
11
|
+
from astropy import units as u #type: ignore[import-untyped]
|
|
12
|
+
from pydantic import BaseModel, Field
|
|
13
|
+
from synphot import Observation, SpectralElement #type: ignore[import-untyped]
|
|
14
|
+
|
|
15
|
+
from .image import Image
|
|
16
|
+
from .models import (
|
|
17
|
+
ETCQueryModel,
|
|
18
|
+
ETCResponseModel,
|
|
19
|
+
FiltersModel,
|
|
20
|
+
SBSEDModel,
|
|
21
|
+
SEDModel,
|
|
22
|
+
TransmissionModel,
|
|
23
|
+
spectral_to_arrays
|
|
24
|
+
)
|
|
25
|
+
from .models.types import SkyID
|
|
26
|
+
from .data import instruments
|
|
27
|
+
from .datafiles import (
|
|
28
|
+
get_emission_from_transmission,
|
|
29
|
+
get_transmission
|
|
30
|
+
)
|
|
31
|
+
from .photsys import PhotSys
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def spectrum_from_airmass(
|
|
35
|
+
models: dict[str, SBSEDModel | SEDModel | TransmissionModel],
|
|
36
|
+
sky: SkyID|None = None,
|
|
37
|
+
am: float = 1.) -> SpectralElement:
|
|
38
|
+
# Build a dictionary of emission or transmission spectra
|
|
39
|
+
am_spectra = {
|
|
40
|
+
model.vars['am'] : model.spectral #type: ignore[index]
|
|
41
|
+
for model in models.values()
|
|
42
|
+
if sky is None or model.vars['sky'] == sky #type: ignore[index]
|
|
43
|
+
}
|
|
44
|
+
ams = sorted(list(am_spectra.keys()))
|
|
45
|
+
# bracket the requested airmass for interpolation
|
|
46
|
+
aml = float(ams[0])
|
|
47
|
+
amp = float(ams[-1])
|
|
48
|
+
for aa in ams:
|
|
49
|
+
a = float(aa)
|
|
50
|
+
if a >= am:
|
|
51
|
+
amp = a
|
|
52
|
+
break
|
|
53
|
+
else:
|
|
54
|
+
aml = a
|
|
55
|
+
# Linear interpolation
|
|
56
|
+
fac = (am - aml) / (amp - aml) if am < amp else 1.
|
|
57
|
+
return am_spectra[aml] * (1. - fac) + am_spectra[amp] * fac #type: ignore[operator]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def get_response(
|
|
61
|
+
q: ETCQueryModel,
|
|
62
|
+
filter: Optional[IO[bytes] | PathLike | str]=None,
|
|
63
|
+
ui: bool=False) -> ETCResponseModel:
|
|
64
|
+
if filter is not None:
|
|
65
|
+
# Read the uploaded filter transmission file
|
|
66
|
+
filter_transmission = get_transmission(filter, id='upload')
|
|
67
|
+
# Compute filter emission based on transmission (and dummy temperature/area)
|
|
68
|
+
emission = get_emission_from_transmission(
|
|
69
|
+
filter_transmission,
|
|
70
|
+
temperature=273. * u.K,
|
|
71
|
+
id='upload'
|
|
72
|
+
)
|
|
73
|
+
# Make a copy of the instrument while adding the uploaded filter
|
|
74
|
+
filters = instruments[q.instrument].filters
|
|
75
|
+
instrument = instruments[q.instrument].model_copy(
|
|
76
|
+
update={
|
|
77
|
+
'filters': FiltersModel(
|
|
78
|
+
transmissions = filters.transmissions | {
|
|
79
|
+
'upload' : filter_transmission
|
|
80
|
+
},
|
|
81
|
+
emissions = filters.emissions | {'upload' : emission}
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
# Update composite transmissions and emission curves
|
|
86
|
+
instrument._update_transmissions()
|
|
87
|
+
else:
|
|
88
|
+
instrument = instruments[q.instrument]
|
|
89
|
+
|
|
90
|
+
telescope = instrument.telescope
|
|
91
|
+
detector = instrument.detector
|
|
92
|
+
instrument_transmission = instrument.transmissions[q.filter]
|
|
93
|
+
|
|
94
|
+
# Multiply Total instrument transmission with atmospheric transmission
|
|
95
|
+
atmosphere_spec = spectrum_from_airmass(
|
|
96
|
+
instrument.site.sky_transmissions,
|
|
97
|
+
am=q.airmass
|
|
98
|
+
) * q.transparency
|
|
99
|
+
full_spec = instrument_transmission.spectral * atmosphere_spec
|
|
100
|
+
# Compute effective collecting area, compensating for possible obstruction
|
|
101
|
+
area = instrument.telescope.collecting_area - instrument.obstruction_area
|
|
102
|
+
|
|
103
|
+
# Make virtual observation
|
|
104
|
+
gain = detector.gain.value
|
|
105
|
+
tpeak = full_spec.tpeak()
|
|
106
|
+
lambda_pivot = full_spec.pivot()
|
|
107
|
+
dlambda_rect = full_spec.rectwidth()
|
|
108
|
+
# Actual source
|
|
109
|
+
photsys = PhotSys(q.unit, lambda_pivot, dlambda_rect)
|
|
110
|
+
if tpeak > 0.:
|
|
111
|
+
if photsys.spectrum is None:
|
|
112
|
+
ct_ref = photsys.photon_rate(q.brightness) / gain * u.ct / u.s
|
|
113
|
+
else:
|
|
114
|
+
observation = Observation(photsys.spectrum, full_spec)
|
|
115
|
+
# Compute ref source count rate to get effective zero-point
|
|
116
|
+
ct_ref = observation.countrate(area=area, binned=False) / gain
|
|
117
|
+
zp = u.Magnitude(1. * u.ct / u.s) - u.Magnitude(ct_ref)
|
|
118
|
+
else:
|
|
119
|
+
ct_ref = 0. * u.ct / u.s
|
|
120
|
+
zp = -100. * u.mag
|
|
121
|
+
# Compute total number of reference source electrons (photons per second)
|
|
122
|
+
photon_rate = (ct_ref * gain).value * photsys.photon_rate(q.brightness)
|
|
123
|
+
|
|
124
|
+
# Sky background
|
|
125
|
+
# Atmospheric emission
|
|
126
|
+
if q.sky == 'specify':
|
|
127
|
+
sky_photsys = PhotSys(q.sky_unit, lambda_pivot, dlambda_rect)
|
|
128
|
+
sky_spectrum = sky_photsys.spectrum
|
|
129
|
+
sky_photon_rate = sky_photsys.photon_rate(q.sky_brightness)
|
|
130
|
+
if sky_spectrum is not None:
|
|
131
|
+
# If in Jy per str, convert to arcsec2
|
|
132
|
+
if q.sky_unit == 'fmegajy':
|
|
133
|
+
sky_photon_rate *= 2.350e-11
|
|
134
|
+
sky_spectrum *= sky_photon_rate
|
|
135
|
+
else:
|
|
136
|
+
sky_spectrum = spectrum_from_airmass(
|
|
137
|
+
instrument.site.sky_emissions,
|
|
138
|
+
sky=q.sky,
|
|
139
|
+
am=q.airmass
|
|
140
|
+
)
|
|
141
|
+
if sky_spectrum is not None:
|
|
142
|
+
# Compute background count rate to get background surface brightness
|
|
143
|
+
if tpeak > 0.:
|
|
144
|
+
bkg_observation = Observation(
|
|
145
|
+
sky_spectrum,
|
|
146
|
+
instrument_transmission.spectral,
|
|
147
|
+
force='extrap'
|
|
148
|
+
)
|
|
149
|
+
# Add instrumental thermal background
|
|
150
|
+
ct_bkgsb = (
|
|
151
|
+
bkg_observation.countrate(area=area, binned=False) \
|
|
152
|
+
+ instrument.emissions_ct[q.filter] * u.ct / u.s
|
|
153
|
+
) / gain
|
|
154
|
+
mag_bkgsb = zp + u.Magnitude(ct_bkgsb)
|
|
155
|
+
else:
|
|
156
|
+
ct_bkgsb = 0. * u.ct / u.s
|
|
157
|
+
mag_bkgsb = 100. * u.mag
|
|
158
|
+
if mag_bkgsb.value < -100. :
|
|
159
|
+
mag_bkgsb = u.Quantity('100 mag')
|
|
160
|
+
if mag_bkgsb.value > 100.:
|
|
161
|
+
mag_bkgsb = u.Quantity('100 mag')
|
|
162
|
+
|
|
163
|
+
# Compute number of background electrons per pixel per second
|
|
164
|
+
# We explicitely assume that counts are per arcsec2
|
|
165
|
+
bkg_rate = (ct_bkgsb * gain * (
|
|
166
|
+
detector.scale[0].to(u.arcsec / u.pix)
|
|
167
|
+
* detector.scale[1].to(u.arcsec / u.pix)
|
|
168
|
+
)).value
|
|
169
|
+
else:
|
|
170
|
+
# Counts directly provided by user
|
|
171
|
+
bkg_rate = sky_photon_rate
|
|
172
|
+
|
|
173
|
+
# Instantiate image model
|
|
174
|
+
img = Image(
|
|
175
|
+
source=q.source,
|
|
176
|
+
psf_fwhm=q.seeing * u.arcsec,
|
|
177
|
+
psf_beta=3.2,
|
|
178
|
+
sersic_radius=q.sersic_radius,
|
|
179
|
+
sersic_index=q.sersic_index,
|
|
180
|
+
pixel=detector.scale,
|
|
181
|
+
rate=photon_rate,
|
|
182
|
+
bkg_rate=bkg_rate,
|
|
183
|
+
# Use RON 'counts' instead of electrons for compatibility with synphot
|
|
184
|
+
ron=detector.ron.to_value('electron'),
|
|
185
|
+
gain=gain,
|
|
186
|
+
photometry=q.photometry,
|
|
187
|
+
aperture=q.aperture
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
sexposures = sqrt(q.exposures * 2. / pi) if \
|
|
191
|
+
q.stacking == 'median' and q.exposures > 2 else sqrt(q.exposures)
|
|
192
|
+
|
|
193
|
+
if q.compute == 'etime':
|
|
194
|
+
snr = q.snr / sexposures
|
|
195
|
+
# Compute exposure time (solution to a second degree equation) in s
|
|
196
|
+
etime = img.etime(snr)
|
|
197
|
+
else:
|
|
198
|
+
etime = q.etime
|
|
199
|
+
snr = img.snr(etime)
|
|
200
|
+
|
|
201
|
+
if ui:
|
|
202
|
+
full_wave, full_response = spectral_to_arrays(full_spec)
|
|
203
|
+
atmosphere_wave, atmosphere_response = spectral_to_arrays(atmosphere_spec)
|
|
204
|
+
|
|
205
|
+
return ETCResponseModel(
|
|
206
|
+
instrument = instrument.name,
|
|
207
|
+
filter = instrument_transmission.name,
|
|
208
|
+
compute = q.compute,
|
|
209
|
+
zp = zp.value,
|
|
210
|
+
snr = snr * sexposures,
|
|
211
|
+
etime = etime,
|
|
212
|
+
etime_skysat = img.etime_bkg_sat(),
|
|
213
|
+
etime_sourcesat = img.etime_source_sat(),
|
|
214
|
+
ttime = q.exposures * (etime + instrument.overhead.to_value(u.s)),
|
|
215
|
+
sky_mag = mag_bkgsb.value,
|
|
216
|
+
lambda_pivot = lambda_pivot.to_value(u.nm),
|
|
217
|
+
bandwidth_rect = dlambda_rect.to_value(u.nm),
|
|
218
|
+
cutout = img.gif(etime, exposures=q.exposures) if ui else None,
|
|
219
|
+
filter_transmission = TransmissionModel(
|
|
220
|
+
id = instrument_transmission.id,
|
|
221
|
+
name = instrument_transmission.name,
|
|
222
|
+
wave_range = instrument.wavelength_range,
|
|
223
|
+
wave = full_wave,
|
|
224
|
+
response = full_response
|
|
225
|
+
).model_dump_json(
|
|
226
|
+
exclude={'spectral'}
|
|
227
|
+
) if ui else None,
|
|
228
|
+
atmosphere_transmission = TransmissionModel(
|
|
229
|
+
id = 'atmosphere',
|
|
230
|
+
name = "Atmosphere",
|
|
231
|
+
wave_range = instrument.wavelength_range,
|
|
232
|
+
wave = atmosphere_wave,
|
|
233
|
+
response = atmosphere_response
|
|
234
|
+
).model_dump_json() if ui else None
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
|