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,532 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Provide Pydantic-compatible annotations and fields for validating and
|
|
3
|
+
serializing AstroPy unit-aware "quantities"
|
|
4
|
+
"""
|
|
5
|
+
# Copyright UParisSaclay/CEA/CFHT/CNRS
|
|
6
|
+
# Licensed under the MIT licence
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from numbers import Number
|
|
11
|
+
from re import findall
|
|
12
|
+
from typing import TYPE_CHECKING, Annotated, Any, Iterable, Literal, Tuple
|
|
13
|
+
|
|
14
|
+
from astropy import units as u #type: ignore[import-untyped]
|
|
15
|
+
import numpy as np
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from pydantic import GetCoreSchemaHandler
|
|
18
|
+
from pydantic import Field
|
|
19
|
+
from pydantic_core import core_schema
|
|
20
|
+
|
|
21
|
+
# Enable imperial units such as inches
|
|
22
|
+
u.imperial.enable()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class QuantityAnnotation:
|
|
27
|
+
"""
|
|
28
|
+
Pydantic compatible annotation for validating and serializing AstroPy
|
|
29
|
+
Quantity fields.
|
|
30
|
+
Loosely adapted from pint-quantity by Tyler Hughes <tylerxh111+git@proton.me>
|
|
31
|
+
|
|
32
|
+
Examples
|
|
33
|
+
--------
|
|
34
|
+
>>> from typing import Annotated
|
|
35
|
+
>>> from astropy import units as u
|
|
36
|
+
>>> from pydantic import BaseModel
|
|
37
|
+
>>> from .quantity import QuantityAnnotation
|
|
38
|
+
|
|
39
|
+
>>> class Coordinates(BaseModel):
|
|
40
|
+
... latitude: Annotated[
|
|
41
|
+
... u.Quantity, QuantityAnnotation("deg", ge=-90.*u.deg, le=90.*u.deg)
|
|
42
|
+
... ]
|
|
43
|
+
... longitude: Annotated[u.Quantity, QuantityAnnotation("deg")]
|
|
44
|
+
... altitude: Annotated[u.Quantity, QuantityAnnotation("km")]
|
|
45
|
+
|
|
46
|
+
>>> # The following instantiation validates
|
|
47
|
+
>>> coord = Coordinates(
|
|
48
|
+
... latitude="39.905705 deg",
|
|
49
|
+
... longitude="-75.166519 deg",
|
|
50
|
+
... altitude="12 m"
|
|
51
|
+
... )
|
|
52
|
+
|
|
53
|
+
>>> coord
|
|
54
|
+
Coordinates(latitude=<Quantity 39.905705 deg>,
|
|
55
|
+
longitude=<Quantity -75.166519 deg>, altitude=<Quantity 0.012 km>)
|
|
56
|
+
|
|
57
|
+
>>> f"{coord!r}"
|
|
58
|
+
'Coordinates(latitude=<Quantity 39.905705 deg>,
|
|
59
|
+
longitude=<Quantity -75.166519 deg>, altitude=<Quantity 0.012 km>)'
|
|
60
|
+
|
|
61
|
+
>>> coord.model_dump()
|
|
62
|
+
{'latitude': <Quantity 39.905705 deg>, 'longitude': <Quantity -75.166519 deg>,
|
|
63
|
+
'altitude': <Quantity 0.012 km>}
|
|
64
|
+
|
|
65
|
+
>>> coord.model_dump(mode="json")
|
|
66
|
+
{'latitude': '39.905705 deg', 'longitude': '-75.166519 deg',
|
|
67
|
+
'altitude': '0.012 km'}
|
|
68
|
+
|
|
69
|
+
>>> coord.model_dump_json()
|
|
70
|
+
'{"latitude":"39.905705 deg","longitude":"-75.166519 deg","altitude":"0.012 km"}'
|
|
71
|
+
|
|
72
|
+
>>> # The following instantiation does not validate
|
|
73
|
+
>>> coord = Coordinates(
|
|
74
|
+
... latitude="99.905705 deg",
|
|
75
|
+
... longitude="-75.166519 deg",
|
|
76
|
+
... altitude="12 m"
|
|
77
|
+
... )
|
|
78
|
+
Traceback (most recent call last):
|
|
79
|
+
...
|
|
80
|
+
pydantic_core._pydantic_core.ValidationError: 1 validation error for Coordinates
|
|
81
|
+
latitude
|
|
82
|
+
Value error, greater than 90.0 deg
|
|
83
|
+
[type=value_error, input_value='99.905705 deg', input_type=str]
|
|
84
|
+
For further information visit https://errors.pydantic.dev/2.7/v/value_error
|
|
85
|
+
|
|
86
|
+
Parameters
|
|
87
|
+
----------
|
|
88
|
+
unit: str
|
|
89
|
+
The unit type of the Pydantic field (e.g., `"m"`, or `"deg"`).
|
|
90
|
+
All input units must be convertible to this unit.
|
|
91
|
+
description: str, optional
|
|
92
|
+
Description string.
|
|
93
|
+
decimals: int, optional
|
|
94
|
+
Maximum number of decimals for the serialization of quantities.
|
|
95
|
+
min_shape: tuple[int], optional
|
|
96
|
+
Minimum number of vector components on each axis.
|
|
97
|
+
max_shape: tuple[int], optional
|
|
98
|
+
Maximum number of vector components on each axis.
|
|
99
|
+
ge: ~astropy.units.Quantity or str, optional
|
|
100
|
+
Lower limit (inclusive).
|
|
101
|
+
gt: ~astropy.units.Quantity or str, optional
|
|
102
|
+
Lower limit (strict).
|
|
103
|
+
le: ~astropy.units.Quantity or str, optional
|
|
104
|
+
Lower limit (inclusive).
|
|
105
|
+
lt: ~astropy.units.Quantity or str, optional
|
|
106
|
+
Lower limit (strict).
|
|
107
|
+
ser_mode: Literal["str", "dict"], optional
|
|
108
|
+
The mode for serializing the field; either `"str"` or `"dict"`.
|
|
109
|
+
By default, in Pydantic's `"python"` serialization mode, fields are
|
|
110
|
+
serialized to a `Quantity`;
|
|
111
|
+
in Pydantic's `"json"` serialization mode, fields are serialized to a `str`.
|
|
112
|
+
strict: bool, optional
|
|
113
|
+
Forces users to specify units; on by default.
|
|
114
|
+
If disabled, a value without units - provided by the user - will be
|
|
115
|
+
treated as the base units of the `QuantityUnit`.
|
|
116
|
+
"""
|
|
117
|
+
description: str = ""
|
|
118
|
+
decimals: int | None = None
|
|
119
|
+
min_shape: np.ndarray | None = None
|
|
120
|
+
max_shape: np.ndarray | None = None
|
|
121
|
+
ge: u.Quantity | str | None = None
|
|
122
|
+
gt: u.Quantity | str | None = None
|
|
123
|
+
le: u.Quantity | str | None = None
|
|
124
|
+
lt: u.Quantity | str | None = None
|
|
125
|
+
def __init__(
|
|
126
|
+
self,
|
|
127
|
+
unit: str,
|
|
128
|
+
*,
|
|
129
|
+
description: str = "",
|
|
130
|
+
decimals: int | None = None,
|
|
131
|
+
min_shape: Tuple[int, ...] | None = None,
|
|
132
|
+
max_shape: Tuple[int, ...] | None = None,
|
|
133
|
+
ge: u.Quantity | str | None = None,
|
|
134
|
+
gt: u.Quantity | str | None = None,
|
|
135
|
+
le: u.Quantity | str | None = None,
|
|
136
|
+
lt: u.Quantity | str | None = None,
|
|
137
|
+
ser_mode: Literal["str", "dict"] | None = None,
|
|
138
|
+
strict: bool = True):
|
|
139
|
+
|
|
140
|
+
self.ser_mode = ser_mode.lower() if ser_mode else None
|
|
141
|
+
self.strict = strict
|
|
142
|
+
|
|
143
|
+
self.unit = unit
|
|
144
|
+
self.description = description
|
|
145
|
+
self.decimals = decimals
|
|
146
|
+
|
|
147
|
+
self.min_shape = np.array(1 if min_shape is None else min_shape, dtype=np.int32)
|
|
148
|
+
self.max_shape = np.array(1 if max_shape is None else max_shape, dtype=np.int32)
|
|
149
|
+
# Make sure all shapes match
|
|
150
|
+
self.min_shape += 0 * self.max_shape
|
|
151
|
+
|
|
152
|
+
self.ge = u.Quantity(ge) if ge is not None else None
|
|
153
|
+
self.gt = u.Quantity(gt) if gt is not None else None
|
|
154
|
+
self.le = u.Quantity(le) if le is not None else None
|
|
155
|
+
self.lt = u.Quantity(lt) if lt is not None else None
|
|
156
|
+
|
|
157
|
+
def validate(
|
|
158
|
+
self,
|
|
159
|
+
v: dict | str | Number | u.Quantity,
|
|
160
|
+
info: core_schema.ValidationInfo | None = None) -> u.Quantity:
|
|
161
|
+
|
|
162
|
+
"""
|
|
163
|
+
Validate `Quantity`.
|
|
164
|
+
|
|
165
|
+
Parameters
|
|
166
|
+
----------
|
|
167
|
+
v: dict | str | numbers.Number ~astropy.units.Quantity
|
|
168
|
+
Quantity that should be validated.
|
|
169
|
+
info: ~pydantic.core_schema.ValidationInfo, optional
|
|
170
|
+
The validation info provided by the Pydantic schema.
|
|
171
|
+
|
|
172
|
+
Returns
|
|
173
|
+
-------
|
|
174
|
+
v: ~astropy.units.Quantity
|
|
175
|
+
Validated `Quantity` with the correct units.
|
|
176
|
+
|
|
177
|
+
Raises
|
|
178
|
+
------
|
|
179
|
+
ValueError: exception
|
|
180
|
+
An error occurred validating the specified value.
|
|
181
|
+
It is raised if any of the following occur:
|
|
182
|
+
- A `dict` is received and the keys `"value"` and `"units"` do not exist.
|
|
183
|
+
- There are no units provided.
|
|
184
|
+
- Provided units cannot be converted to base units.
|
|
185
|
+
- An unknown unit was provided.
|
|
186
|
+
- An unknown type for value was provided.
|
|
187
|
+
TypeError: exception
|
|
188
|
+
An error occurred from unit registry or unit registry context.
|
|
189
|
+
It is not propagated as a `pydantic.ValidationError` because
|
|
190
|
+
it does not stem from a user error.
|
|
191
|
+
"""
|
|
192
|
+
try:
|
|
193
|
+
if isinstance(v, dict):
|
|
194
|
+
v = f"{v['value']} {v.get('unit', '')}"
|
|
195
|
+
except KeyError as e:
|
|
196
|
+
raise ValueError("no `value` or `unit` keys found") from e
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
if isinstance(v, str):
|
|
200
|
+
# relies on units to return a number if no units are present
|
|
201
|
+
# if value is a quantity, then units are present and check on the units being convertible
|
|
202
|
+
# if value is a number, then check on strict mode will happen next
|
|
203
|
+
v = str_to_quantity_array(v)
|
|
204
|
+
except ValueError as e:
|
|
205
|
+
raise ValueError(e) from e
|
|
206
|
+
|
|
207
|
+
try:
|
|
208
|
+
if isinstance(v, Number) and not self.strict:
|
|
209
|
+
# check must happen after conversion from string because string might not have any units
|
|
210
|
+
# only applicable if dealing with no units and if in strict mode
|
|
211
|
+
v = u.Quantity(v, self.unit)
|
|
212
|
+
if isinstance(v, u.Quantity):
|
|
213
|
+
v = v.to(self.unit)
|
|
214
|
+
|
|
215
|
+
# Check array shape if any.
|
|
216
|
+
shape = np.array(np.array(v.value).shape)
|
|
217
|
+
if (len(shape) == 0 and self.min_shape is not None and \
|
|
218
|
+
np.any(self.min_shape > 1)) or np.any(shape < self.min_shape):
|
|
219
|
+
raise ValueError(
|
|
220
|
+
f"missing components (found {shape}, "
|
|
221
|
+
f"{self.min_shape} expected)")
|
|
222
|
+
if np.any(shape > self.max_shape):
|
|
223
|
+
raise ValueError(
|
|
224
|
+
f"too many components (found {shape}, "
|
|
225
|
+
f"{self.max_shape} expected)")
|
|
226
|
+
|
|
227
|
+
# Check limits if any
|
|
228
|
+
if self.ge is not None and np.any(v < self.ge):
|
|
229
|
+
raise ValueError(f"less than {self.ge}")
|
|
230
|
+
if self.gt is not None and np.any(v <= self.gt):
|
|
231
|
+
raise ValueError(f"equal to or less than {self.gt}")
|
|
232
|
+
if self.le is not None and np.any(v > self.le):
|
|
233
|
+
raise ValueError(f"greater than {self.le}")
|
|
234
|
+
if self.lt is not None and np.any(v >= self.lt):
|
|
235
|
+
raise ValueError(f"equal to or greater than {self.lt}")
|
|
236
|
+
return v
|
|
237
|
+
|
|
238
|
+
except AttributeError as e:
|
|
239
|
+
# raises attribute error if value is a number
|
|
240
|
+
# this case only happen when parsing from a string, the units are not present, and not in strict mode
|
|
241
|
+
# see comments above related to ureg returning a number
|
|
242
|
+
raise ValueError("no units found") from e
|
|
243
|
+
except u.UnitConversionError as e:
|
|
244
|
+
raise ValueError(e) from e
|
|
245
|
+
raise ValueError(f"unknown type {type(v)}")
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def serialize(
|
|
249
|
+
self,
|
|
250
|
+
v: u.Quantity,
|
|
251
|
+
info: core_schema.SerializationInfo | None = None,
|
|
252
|
+
*,
|
|
253
|
+
to_json: bool = False) -> dict | str | u.Quantity:
|
|
254
|
+
"""
|
|
255
|
+
Serialize `Quantity`.
|
|
256
|
+
|
|
257
|
+
Parameters
|
|
258
|
+
----------
|
|
259
|
+
v: ~astropy.units.Quantity
|
|
260
|
+
Quantity that should be serialized.
|
|
261
|
+
info: pydantic.core_schema.SerializationInfo, optional
|
|
262
|
+
Serialization info provided by the Pydantic schema.
|
|
263
|
+
to_json: bool, optional
|
|
264
|
+
Whether or not to serialize to a json convertible object.
|
|
265
|
+
Useful if using `QuantityUnit` as a utility outside of Pydantic models.
|
|
266
|
+
|
|
267
|
+
Returns
|
|
268
|
+
-------
|
|
269
|
+
quantity: str
|
|
270
|
+
The serialized `Quantity`.
|
|
271
|
+
"""
|
|
272
|
+
to_json = to_json or (info is not None and info.mode_is_json())
|
|
273
|
+
|
|
274
|
+
if self.ser_mode == "dict":
|
|
275
|
+
return {
|
|
276
|
+
"value": v.value.round(self.decimals) if self.decimals is not None \
|
|
277
|
+
else v.value,
|
|
278
|
+
"unit": v.unit if not to_json else f"{v.unit}",
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if to_json:
|
|
282
|
+
return {
|
|
283
|
+
"value": v.value.round(self.decimals).tolist() \
|
|
284
|
+
if self.decimals is not None \
|
|
285
|
+
else v.value.tolist(),
|
|
286
|
+
"unit": f"{v.unit}"
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if self.ser_mode == "str":
|
|
290
|
+
return f"{v.round(self.decimals)}" if self.decimals is not None \
|
|
291
|
+
else f"{v}"
|
|
292
|
+
|
|
293
|
+
return v
|
|
294
|
+
|
|
295
|
+
def __get_pydantic_core_schema__(
|
|
296
|
+
self,
|
|
297
|
+
source_type: Any,
|
|
298
|
+
handler: GetCoreSchemaHandler) -> core_schema.CoreSchema:
|
|
299
|
+
"""
|
|
300
|
+
Get the Pydantic core schema.
|
|
301
|
+
|
|
302
|
+
Parameters
|
|
303
|
+
----------
|
|
304
|
+
source_type:
|
|
305
|
+
The source type.
|
|
306
|
+
handler: ~pydantic.GetCoreSchemaHandler
|
|
307
|
+
The `GetCoreSchemaHandler` instance.
|
|
308
|
+
|
|
309
|
+
Returns
|
|
310
|
+
-------
|
|
311
|
+
core_schema: ~pydantic.core_schema.CoreSchema
|
|
312
|
+
The Pydantic core schema.
|
|
313
|
+
"""
|
|
314
|
+
_from_typedict_schema = {
|
|
315
|
+
"value": core_schema.typed_dict_field(
|
|
316
|
+
core_schema.str_schema(coerce_numbers_to_str=True)
|
|
317
|
+
),
|
|
318
|
+
"unit": core_schema.typed_dict_field(core_schema.str_schema()),
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
validate_schema = core_schema.chain_schema([
|
|
322
|
+
core_schema.union_schema([
|
|
323
|
+
core_schema.is_instance_schema(u.Quantity),
|
|
324
|
+
core_schema.str_schema(coerce_numbers_to_str=True),
|
|
325
|
+
core_schema.typed_dict_schema(_from_typedict_schema),
|
|
326
|
+
]),
|
|
327
|
+
core_schema.with_info_plain_validator_function(self.validate)
|
|
328
|
+
])
|
|
329
|
+
|
|
330
|
+
"""
|
|
331
|
+
validate_json_schema = core_schema.chain_schema([
|
|
332
|
+
core_schema.union_schema([
|
|
333
|
+
core_schema.str_schema(coerce_numbers_to_str=True),
|
|
334
|
+
core_schema.typed_dict_schema(_from_typedict_schema),
|
|
335
|
+
]),
|
|
336
|
+
core_schema.no_info_plain_validator_function(self.validate)
|
|
337
|
+
])
|
|
338
|
+
"""
|
|
339
|
+
validate_json_schema = core_schema.chain_schema([
|
|
340
|
+
core_schema.str_schema(coerce_numbers_to_str=True),
|
|
341
|
+
core_schema.no_info_plain_validator_function(self.validate)
|
|
342
|
+
])
|
|
343
|
+
|
|
344
|
+
serialize_schema = core_schema.plain_serializer_function_ser_schema(
|
|
345
|
+
self.serialize,
|
|
346
|
+
info_arg=True,
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
return core_schema.json_or_python_schema(
|
|
350
|
+
json_schema=validate_json_schema,
|
|
351
|
+
python_schema=validate_schema,
|
|
352
|
+
serialization=serialize_schema,
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def AnnotatedQuantity(
|
|
358
|
+
default: u.Quantity | str | None = None,
|
|
359
|
+
unit: str | None = None,
|
|
360
|
+
short: str | None = None,
|
|
361
|
+
description: str = "",
|
|
362
|
+
decimals: int | None = None,
|
|
363
|
+
min_shape: Tuple[int, ...] | None = None,
|
|
364
|
+
max_shape: Tuple[int, ...] | None = None,
|
|
365
|
+
ge: u.Quantity | str | None = None,
|
|
366
|
+
gt: u.Quantity | str | None = None,
|
|
367
|
+
le: u.Quantity | str | None = None,
|
|
368
|
+
lt: u.Quantity | str | None = None) -> Any:
|
|
369
|
+
"""
|
|
370
|
+
Pydantic pseudo-field for validating and serializing AstroPy Quantities.
|
|
371
|
+
|
|
372
|
+
Examples
|
|
373
|
+
--------
|
|
374
|
+
>>> from pydantic_settings import BaseSettings
|
|
375
|
+
>>> from .quantity import AnnotatedQuantity
|
|
376
|
+
|
|
377
|
+
>>> class Settings(BaseSettings):
|
|
378
|
+
... size: AnnotatedQuantity(
|
|
379
|
+
... short='S',
|
|
380
|
+
... description="an arbitrary length",
|
|
381
|
+
... default=10. * u.m,
|
|
382
|
+
... ge=1. * u.micron,
|
|
383
|
+
... lt=1. * u.km
|
|
384
|
+
... )
|
|
385
|
+
|
|
386
|
+
>>> # The following instantiation validates
|
|
387
|
+
>>> s = Settings(size="3. cm")
|
|
388
|
+
|
|
389
|
+
>>> s
|
|
390
|
+
Settings(size=<Quantity 0.03 m>)
|
|
391
|
+
|
|
392
|
+
>>> f"{s!r}"
|
|
393
|
+
'Settings(size=<Quantity 0.03 m>)'
|
|
394
|
+
|
|
395
|
+
>>> s.model_dump()
|
|
396
|
+
{'size': <Quantity 0.03 m>}
|
|
397
|
+
|
|
398
|
+
>>> s.model_dump(mode="json")
|
|
399
|
+
{'size': '0.03 m'}
|
|
400
|
+
|
|
401
|
+
>>> s.model_json_schema()
|
|
402
|
+
{'additionalProperties': False, 'properties': {'size': {'default':
|
|
403
|
+
'10.0 m', 'description': 'an arbitrary length', 'exclusiveMaximum':
|
|
404
|
+
'1.0 km', 'minimum': '1.0 micron', 'physType': 'length', 'short': 'S',
|
|
405
|
+
'title': 'Size', 'type': 'string'}}, 'title': 'Settings', 'type':
|
|
406
|
+
'object'}
|
|
407
|
+
|
|
408
|
+
>>> # The following instantiation does not validate
|
|
409
|
+
>>> s = Settings(size="4 deg")
|
|
410
|
+
Traceback (most recent call last):
|
|
411
|
+
...
|
|
412
|
+
pydantic_core._pydantic_core.ValidationError: 1 validation error for Settings
|
|
413
|
+
size
|
|
414
|
+
Value error, 'deg' (angle) and 'm' (length) are not convertible
|
|
415
|
+
[type=value_error, input_value='4 deg', input_type=str]
|
|
416
|
+
For further information visit https://errors.pydantic.dev/2.8/v/value_error
|
|
417
|
+
|
|
418
|
+
Parameters
|
|
419
|
+
----------
|
|
420
|
+
default: ~astropy.units.Quantity or str, optional
|
|
421
|
+
Default value.
|
|
422
|
+
unit: str, optional
|
|
423
|
+
Unit for quantity (overrided by default unit if provided).
|
|
424
|
+
short: str, optional
|
|
425
|
+
shortcut for keyword.
|
|
426
|
+
description: str, optional
|
|
427
|
+
Description string.
|
|
428
|
+
decimals: int, optional
|
|
429
|
+
Maximum number of decimals for the serialization of quantities.
|
|
430
|
+
min_shape: tuple[int], optional
|
|
431
|
+
Minimum number of vector components on each axis.
|
|
432
|
+
max_shape: tuple[int], optional
|
|
433
|
+
Maximum number of vector components on each axis.
|
|
434
|
+
ge: ~astropy.units.Quantity or str, optional
|
|
435
|
+
Lower limit (inclusive).
|
|
436
|
+
gt: ~astropy.units.Quantity or str, optional
|
|
437
|
+
Lower limit (strict).
|
|
438
|
+
le: ~astropy.units.Quantity or str, optional
|
|
439
|
+
Lower limit (inclusive).
|
|
440
|
+
lt: ~astropy.units.Quantity or str, optional
|
|
441
|
+
Lower limit (strict).
|
|
442
|
+
"""
|
|
443
|
+
if default is not None:
|
|
444
|
+
default = u.Quantity(default)
|
|
445
|
+
unit = default.unit #type: ignore[union-attr]
|
|
446
|
+
elif unit is None:
|
|
447
|
+
raise ValueError
|
|
448
|
+
physType = u.get_physical_type(u.Quantity("1 " + str(unit)))
|
|
449
|
+
json_extra: dict = {}
|
|
450
|
+
if default is not None:
|
|
451
|
+
json_extra['default'] = default.to_string() #type: ignore[union-attr]
|
|
452
|
+
if min_shape is not None:
|
|
453
|
+
json_extra['minShape'] = str(min_shape)
|
|
454
|
+
if max_shape is not None:
|
|
455
|
+
json_extra['maxShape'] = str(max_shape)
|
|
456
|
+
if lt is not None:
|
|
457
|
+
lt = u.Quantity(lt)
|
|
458
|
+
json_extra['exclusiveMaximum'] = lt.to_string() #type: ignore[union-attr]
|
|
459
|
+
if gt is not None:
|
|
460
|
+
gt = u.Quantity(gt)
|
|
461
|
+
json_extra['exclusiveMinimum'] = gt.to_string() #type: ignore[union-attr]
|
|
462
|
+
if le is not None:
|
|
463
|
+
le = u.Quantity(le)
|
|
464
|
+
json_extra['maximum'] = le.to_string() #type: ignore[union-attr]
|
|
465
|
+
if ge is not None:
|
|
466
|
+
ge = u.Quantity(ge)
|
|
467
|
+
json_extra['minimum'] = ge.to_string() #type: ignore[union-attr]
|
|
468
|
+
if physType is not None:
|
|
469
|
+
json_extra['physType'] = str(physType)
|
|
470
|
+
if short:
|
|
471
|
+
json_extra['short'] = short
|
|
472
|
+
return Annotated[
|
|
473
|
+
u.Quantity,
|
|
474
|
+
QuantityAnnotation(
|
|
475
|
+
unit=unit,
|
|
476
|
+
decimals=decimals,
|
|
477
|
+
min_shape=min_shape,
|
|
478
|
+
max_shape=max_shape,
|
|
479
|
+
ge=ge,
|
|
480
|
+
gt=gt,
|
|
481
|
+
le=le,
|
|
482
|
+
lt=lt
|
|
483
|
+
),
|
|
484
|
+
Field(
|
|
485
|
+
json_schema_extra=json_extra
|
|
486
|
+
)
|
|
487
|
+
]
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def str_to_quantity_array(s: str) -> u.Quantity | None:
|
|
492
|
+
"""
|
|
493
|
+
Convert string to Astropy "units" Quantity array
|
|
494
|
+
|
|
495
|
+
Notes
|
|
496
|
+
-----
|
|
497
|
+
Currently limited to "well-formed", 1D arrays.
|
|
498
|
+
|
|
499
|
+
Examples
|
|
500
|
+
--------
|
|
501
|
+
>>> from .quantity import str_to_quantity_array
|
|
502
|
+
|
|
503
|
+
>>> str_to_quantity_array("[3.14, 1e+06] m")
|
|
504
|
+
<Quantity [3.14e+00, 1.00e+06] m>
|
|
505
|
+
|
|
506
|
+
Parameters
|
|
507
|
+
----------
|
|
508
|
+
s: str
|
|
509
|
+
Input string.
|
|
510
|
+
|
|
511
|
+
Returns
|
|
512
|
+
-------
|
|
513
|
+
v: ~astropy.units.Quantity
|
|
514
|
+
Astropy units Quantity object.
|
|
515
|
+
"""
|
|
516
|
+
found = findall(
|
|
517
|
+
r"^\s*[\(\[]?((?:[\(\[\)\]\,\;\s]|(?:[\+\-]*\d\.?\d*(?:[eE][\+\-]?\d+)?))+)"
|
|
518
|
+
r"[\)\]]?\s*(\w+[\w\d\s\/\^\*\-\.]*)*$",
|
|
519
|
+
s
|
|
520
|
+
)
|
|
521
|
+
if found is None:
|
|
522
|
+
return None
|
|
523
|
+
# Return result with largest number of components
|
|
524
|
+
value = max(
|
|
525
|
+
[np.fromstring(found[0][0], sep=sep) for sep in [' ', ',', ';']],
|
|
526
|
+
key=lambda x: len(x)
|
|
527
|
+
)
|
|
528
|
+
return u.Quantity(
|
|
529
|
+
value=value if len(value) > 1 else float(value),
|
|
530
|
+
unit=found[0][1]
|
|
531
|
+
)
|
|
532
|
+
|