across-tools 0.1.dev42__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.
- across/__init__.py +21 -0
- across/tools/__init__.py +19 -0
- across/tools/_version.py +34 -0
- across/tools/core/__init__.py +0 -0
- across/tools/core/config.py +29 -0
- across/tools/core/enums/__init__.py +7 -0
- across/tools/core/enums/constraint_type.py +16 -0
- across/tools/core/enums/depth_unit.py +12 -0
- across/tools/core/enums/energy_unit.py +13 -0
- across/tools/core/enums/frequency_unit.py +13 -0
- across/tools/core/enums/wavelength_unit.py +12 -0
- across/tools/core/math.py +36 -0
- across/tools/core/schemas/__init__.py +26 -0
- across/tools/core/schemas/bandpass.py +199 -0
- across/tools/core/schemas/base.py +16 -0
- across/tools/core/schemas/coordinate.py +42 -0
- across/tools/core/schemas/custom_types.py +17 -0
- across/tools/core/schemas/exceptions.py +16 -0
- across/tools/core/schemas/healpix_order.py +12 -0
- across/tools/core/schemas/polygon.py +65 -0
- across/tools/core/schemas/roll_angle.py +12 -0
- across/tools/core/schemas/tle.py +100 -0
- across/tools/core/schemas/visibility.py +39 -0
- across/tools/ephemeris/__init__.py +17 -0
- across/tools/ephemeris/base.py +212 -0
- across/tools/ephemeris/ground_ephem.py +135 -0
- across/tools/ephemeris/jpl_ephem.py +160 -0
- across/tools/ephemeris/spice_ephem.py +180 -0
- across/tools/ephemeris/tle_ephem.py +138 -0
- across/tools/example_module.py +23 -0
- across/tools/footprint/__init__.py +4 -0
- across/tools/footprint/footprint.py +86 -0
- across/tools/footprint/healpix_joins.py +44 -0
- across/tools/footprint/projection.py +82 -0
- across/tools/py.typed +0 -0
- across/tools/tle/__init__.py +3 -0
- across/tools/tle/exceptions.py +7 -0
- across/tools/tle/tle.py +166 -0
- across/tools/visibility/__init__.py +14 -0
- across/tools/visibility/base.py +275 -0
- across/tools/visibility/constraints/__init__.py +25 -0
- across/tools/visibility/constraints/alt_az.py +92 -0
- across/tools/visibility/constraints/base.py +73 -0
- across/tools/visibility/constraints/earth_limb.py +91 -0
- across/tools/visibility/constraints/moon_angle.py +81 -0
- across/tools/visibility/constraints/polygon.py +34 -0
- across/tools/visibility/constraints/saa.py +65 -0
- across/tools/visibility/constraints/sun_angle.py +87 -0
- across/tools/visibility/constraints_constructor.py +65 -0
- across/tools/visibility/ephemeris_visibility.py +179 -0
- across/tools/visibility/joint_visibility.py +157 -0
- across_tools-0.1.dev42.dist-info/METADATA +320 -0
- across_tools-0.1.dev42.dist-info/RECORD +56 -0
- across_tools-0.1.dev42.dist-info/WHEEL +5 -0
- across_tools-0.1.dev42.dist-info/licenses/LICENSE +201 -0
- across_tools-0.1.dev42.dist-info/top_level.txt +1 -0
across/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NASA Docket No. GSC-19,469-1, and identified as "Astrophysics Cross-Observatory
|
|
3
|
+
Science Support (ACROSS) System
|
|
4
|
+
|
|
5
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
you may not use this file except in compliance with the License.
|
|
7
|
+
You may obtain a copy of the License at
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software distributed
|
|
11
|
+
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
|
12
|
+
CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
|
13
|
+
specific language governing permissions and limitations under the License.“
|
|
14
|
+
|
|
15
|
+
The copyright notice to be included in the software is as follows:
|
|
16
|
+
|
|
17
|
+
Copyright © 2025 United States Government as represented by the Administrator
|
|
18
|
+
of the National Aeronautics and Space Administration and The Penn State
|
|
19
|
+
Research Foundation. All rights reserved. This software is licensed under the
|
|
20
|
+
Apache 2.0 License.
|
|
21
|
+
"""
|
across/tools/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from .core import enums
|
|
2
|
+
from .core.schemas import (
|
|
3
|
+
Coordinate,
|
|
4
|
+
EnergyBandpass,
|
|
5
|
+
FrequencyBandpass,
|
|
6
|
+
Polygon,
|
|
7
|
+
WavelengthBandpass,
|
|
8
|
+
convert_to_wave,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"Coordinate",
|
|
13
|
+
"EnergyBandpass",
|
|
14
|
+
"FrequencyBandpass",
|
|
15
|
+
"Polygon",
|
|
16
|
+
"WavelengthBandpass",
|
|
17
|
+
"convert_to_wave",
|
|
18
|
+
"enums",
|
|
19
|
+
]
|
across/tools/_version.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"__version__",
|
|
6
|
+
"__version_tuple__",
|
|
7
|
+
"version",
|
|
8
|
+
"version_tuple",
|
|
9
|
+
"__commit_id__",
|
|
10
|
+
"commit_id",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
TYPE_CHECKING = False
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from typing import Tuple
|
|
16
|
+
from typing import Union
|
|
17
|
+
|
|
18
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
+
COMMIT_ID = Union[str, None]
|
|
20
|
+
else:
|
|
21
|
+
VERSION_TUPLE = object
|
|
22
|
+
COMMIT_ID = object
|
|
23
|
+
|
|
24
|
+
version: str
|
|
25
|
+
__version__: str
|
|
26
|
+
__version_tuple__: VERSION_TUPLE
|
|
27
|
+
version_tuple: VERSION_TUPLE
|
|
28
|
+
commit_id: COMMIT_ID
|
|
29
|
+
__commit_id__: COMMIT_ID
|
|
30
|
+
|
|
31
|
+
__version__ = version = '0.1.dev42'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 1, 'dev42')
|
|
33
|
+
|
|
34
|
+
__commit_id__ = commit_id = None
|
|
File without changes
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class BaseConfig(BaseSettings):
|
|
5
|
+
"""Base configuration for the application."""
|
|
6
|
+
|
|
7
|
+
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Config(BaseConfig):
|
|
11
|
+
"""Configuration for the application.
|
|
12
|
+
The Config class extends BaseConfig and provides configuration settings for the application,
|
|
13
|
+
including environment, hosting, logging, and request-related parameters.
|
|
14
|
+
|
|
15
|
+
Attributes
|
|
16
|
+
----------
|
|
17
|
+
SPACETRACK_USER : str
|
|
18
|
+
Space-Track.org username
|
|
19
|
+
SPACETRACK_PWD : str
|
|
20
|
+
Space-Track.org password
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
SPACETRACK_USER: str | None = None
|
|
24
|
+
SPACETRACK_PWD: str | None = None
|
|
25
|
+
|
|
26
|
+
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
config = Config()
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from .constraint_type import ConstraintType
|
|
2
|
+
from .depth_unit import DepthUnit
|
|
3
|
+
from .energy_unit import EnergyUnit
|
|
4
|
+
from .frequency_unit import FrequencyUnit
|
|
5
|
+
from .wavelength_unit import WavelengthUnit
|
|
6
|
+
|
|
7
|
+
__all__ = ["DepthUnit", "EnergyUnit", "FrequencyUnit", "WavelengthUnit", "ConstraintType"]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ConstraintType(str, Enum):
|
|
5
|
+
"""
|
|
6
|
+
Represents a constraint.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
SUN = "Sun Angle"
|
|
10
|
+
MOON = "Moon Angle"
|
|
11
|
+
EARTH = "Earth Limb"
|
|
12
|
+
WINDOW = "Window"
|
|
13
|
+
UNKNOWN = "Unknown"
|
|
14
|
+
SAA = "South Atlantic Anomaly"
|
|
15
|
+
ALT_AZ = "Altitude/Azimuth Avoidance"
|
|
16
|
+
TEST = "Test Constraint"
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from collections import Counter
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import numpy.typing as npt
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def find_duplicates(input_list: list[int]) -> list[int]:
|
|
8
|
+
"""
|
|
9
|
+
Finds duplicates in a list.
|
|
10
|
+
Taken from https://stackoverflow.com/questions/9835762/
|
|
11
|
+
"""
|
|
12
|
+
return [item for item, count in Counter(input_list).items() if count > 1]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def x_rot(theta_deg: float) -> npt.NDArray[np.float64]:
|
|
16
|
+
"""
|
|
17
|
+
Performs matrix rotation around the cartesian x direction
|
|
18
|
+
"""
|
|
19
|
+
theta = np.deg2rad(theta_deg)
|
|
20
|
+
return np.asarray([[1, 0, 0], [0, np.cos(theta), -np.sin(theta)], [0, np.sin(theta), np.cos(theta)]])
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def y_rot(theta_deg: float) -> npt.NDArray[np.float64]:
|
|
24
|
+
"""
|
|
25
|
+
Performs matrix rotation around the cartesian y direction
|
|
26
|
+
"""
|
|
27
|
+
theta = np.deg2rad(theta_deg)
|
|
28
|
+
return np.asarray([[np.cos(theta), 0, np.sin(theta)], [0, 1, 0], [-np.sin(theta), 0, np.cos(theta)]])
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def z_rot(theta_deg: float) -> npt.NDArray[np.float64]:
|
|
32
|
+
"""
|
|
33
|
+
Performs matrix rotation around the cartesian z direction
|
|
34
|
+
"""
|
|
35
|
+
theta = np.deg2rad(theta_deg)
|
|
36
|
+
return np.asarray([[np.cos(theta), -np.sin(theta), 0], [np.sin(theta), np.cos(theta), 0], [0, 0, 1]])
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from .bandpass import EnergyBandpass, FrequencyBandpass, WavelengthBandpass, convert_to_wave
|
|
2
|
+
from .base import BaseSchema
|
|
3
|
+
from .coordinate import Coordinate
|
|
4
|
+
from .custom_types import AstropyDateTime, AstropyTimeDelta
|
|
5
|
+
from .healpix_order import HealpixOrder
|
|
6
|
+
from .polygon import Polygon
|
|
7
|
+
from .roll_angle import RollAngle
|
|
8
|
+
from .visibility import ConstrainedDate, ConstraintReason, VisibilityWindow, Window
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"Coordinate",
|
|
12
|
+
"Polygon",
|
|
13
|
+
"BaseSchema",
|
|
14
|
+
"RollAngle",
|
|
15
|
+
"HealpixOrder",
|
|
16
|
+
"EnergyBandpass",
|
|
17
|
+
"WavelengthBandpass",
|
|
18
|
+
"FrequencyBandpass",
|
|
19
|
+
"convert_to_wave",
|
|
20
|
+
"VisibilityWindow",
|
|
21
|
+
"ConstrainedDate",
|
|
22
|
+
"Window",
|
|
23
|
+
"ConstraintReason",
|
|
24
|
+
"AstropyDateTime",
|
|
25
|
+
"AstropyTimeDelta",
|
|
26
|
+
]
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
from typing import Any, Literal
|
|
2
|
+
|
|
3
|
+
from astropy import units as u # type: ignore[import-untyped]
|
|
4
|
+
|
|
5
|
+
from ..enums import EnergyUnit, FrequencyUnit, WavelengthUnit
|
|
6
|
+
from .base import BaseSchema
|
|
7
|
+
from .exceptions import BandwidthValueError, MinMaxValueError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BaseBandpass(BaseSchema):
|
|
11
|
+
"""
|
|
12
|
+
A base class for defining bandpass filters with a specified range.
|
|
13
|
+
|
|
14
|
+
Attributes:
|
|
15
|
+
filter_name (str): The name of the filter, if provided.
|
|
16
|
+
min (float | None): The minimum value of the bandpass range.
|
|
17
|
+
max (float | None): The maximum value of the bandpass range.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
filter_name: str | None = None
|
|
21
|
+
min: float | None = None
|
|
22
|
+
max: float | None = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class WavelengthBandpass(BaseBandpass):
|
|
26
|
+
"""
|
|
27
|
+
A class representing a bandpass filter defined in terms of wavelength.
|
|
28
|
+
|
|
29
|
+
Inherits from `BaseBandpass`, this class specializes the filter to operate in the
|
|
30
|
+
wavelength domain and provides additional functionality for wavelength-based filters.
|
|
31
|
+
|
|
32
|
+
Attributes:
|
|
33
|
+
type (Literal['WAVELENGTH']): A constant string indicating the type of the bandpass filter.
|
|
34
|
+
central_wavelength (float | None): The central wavelength of the filter.
|
|
35
|
+
bandwidth (float | None): The bandwidth of the filter.
|
|
36
|
+
unit (WavelengthUnit): The unit of measurement for the wavelength.
|
|
37
|
+
|
|
38
|
+
Methods:
|
|
39
|
+
model_post_init(__context: Any) -> None:
|
|
40
|
+
Performs validation and calculation of central wavelength and bandwidth based on the min/max range
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
type: Literal["WAVELENGTH"] = "WAVELENGTH"
|
|
44
|
+
central_wavelength: float | None = None
|
|
45
|
+
peak_wavelength: float | None = None
|
|
46
|
+
bandwidth: float | None = None
|
|
47
|
+
unit: WavelengthUnit
|
|
48
|
+
|
|
49
|
+
def model_post_init(self, __context: Any) -> None:
|
|
50
|
+
"""
|
|
51
|
+
Validates the min and max values of the wavelength bandpass and calculates the central wavelength
|
|
52
|
+
and bandwidth if they are not provided. Also ensures the values are positive and that the max
|
|
53
|
+
wavelength is greater than the min wavelength. Lastly, it converts the units to angstroms.
|
|
54
|
+
|
|
55
|
+
Raises:
|
|
56
|
+
ValueError: If the min/max values are invalid or if the calculated values for central wavelength
|
|
57
|
+
or bandwidth are non-positive.
|
|
58
|
+
"""
|
|
59
|
+
if (self.min and not self.max) or (self.max and not self.min):
|
|
60
|
+
raise MinMaxValueError("Both min and max must be defined.")
|
|
61
|
+
|
|
62
|
+
if self.min and self.max:
|
|
63
|
+
if self.max < self.min:
|
|
64
|
+
raise MinMaxValueError("Max wavelength cannot be less than min wavelength.")
|
|
65
|
+
|
|
66
|
+
if not all([self.min > 0, self.max > 0]):
|
|
67
|
+
raise MinMaxValueError("Wavelength values must be positive.")
|
|
68
|
+
|
|
69
|
+
self.bandwidth = 0.5 * (self.max - self.min)
|
|
70
|
+
self.central_wavelength = self.min + self.bandwidth
|
|
71
|
+
|
|
72
|
+
if not (self.central_wavelength and self.bandwidth):
|
|
73
|
+
raise BandwidthValueError("Both central wavelength and bandwidth must be defined.")
|
|
74
|
+
|
|
75
|
+
if not all([self.central_wavelength > 0, self.bandwidth > 0]):
|
|
76
|
+
raise BandwidthValueError("Central wavelength and bandwidth must be positive.")
|
|
77
|
+
|
|
78
|
+
self.bandwidth = float(
|
|
79
|
+
(self.bandwidth * u.Unit(self.unit.value)).to(u.Unit(WavelengthUnit.ANGSTROM.value)).value
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
self.central_wavelength = float(
|
|
83
|
+
(self.central_wavelength * u.Unit(self.unit.value))
|
|
84
|
+
.to(u.Unit(WavelengthUnit.ANGSTROM.value))
|
|
85
|
+
.value
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
self.min = self.central_wavelength - self.bandwidth
|
|
89
|
+
|
|
90
|
+
self.max = self.central_wavelength + self.bandwidth
|
|
91
|
+
|
|
92
|
+
self.unit = WavelengthUnit.ANGSTROM
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class EnergyBandpass(BaseBandpass):
|
|
96
|
+
"""
|
|
97
|
+
A class representing a bandpass filter defined in terms of energy.
|
|
98
|
+
|
|
99
|
+
Inherits from `BaseBandpass`, this class specializes the filter to operate in the energy domain.
|
|
100
|
+
|
|
101
|
+
Attributes:
|
|
102
|
+
type (Literal['ENERGY']): A constant string indicating the type of the bandpass filter.
|
|
103
|
+
unit (EnergyUnit): The unit of measurement for the energy.
|
|
104
|
+
|
|
105
|
+
Methods:
|
|
106
|
+
model_post_init(__context: Any) -> None:
|
|
107
|
+
Ensures the min and max energy values are positive and valid.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
type: Literal["ENERGY"] = "ENERGY"
|
|
111
|
+
unit: EnergyUnit
|
|
112
|
+
|
|
113
|
+
def model_post_init(self, __context: Any) -> None:
|
|
114
|
+
"""
|
|
115
|
+
Validates that the min and max energy values are positive.
|
|
116
|
+
|
|
117
|
+
Raises:
|
|
118
|
+
ValueError: If the min or max energy values are not defined, are non-positive, and max is greater
|
|
119
|
+
than min.
|
|
120
|
+
"""
|
|
121
|
+
if not (self.min and self.max):
|
|
122
|
+
raise MinMaxValueError("Both min and max energy values must be defined.")
|
|
123
|
+
|
|
124
|
+
if not all([self.min > 0, self.max > 0]):
|
|
125
|
+
raise MinMaxValueError("Energy values must be positive.")
|
|
126
|
+
|
|
127
|
+
if self.max < self.min:
|
|
128
|
+
raise MinMaxValueError("Max wavelength cannot be less than min wavelength.")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class FrequencyBandpass(BaseBandpass):
|
|
132
|
+
"""
|
|
133
|
+
A class representing a bandpass filter defined in terms of frequency.
|
|
134
|
+
|
|
135
|
+
Inherits from `BaseBandpass`, this class specializes the filter to operate in the frequency domain.
|
|
136
|
+
|
|
137
|
+
Attributes:
|
|
138
|
+
type (Literal['FREQUENCY']): A constant string indicating the type of the bandpass filter.
|
|
139
|
+
unit (FrequencyUnit): The unit of measurement for the frequency.
|
|
140
|
+
|
|
141
|
+
Methods:
|
|
142
|
+
model_post_init(__context: Any) -> None:
|
|
143
|
+
Ensures the min and max frequency values are positive and valid.
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
type: Literal["FREQUENCY"] = "FREQUENCY"
|
|
147
|
+
unit: FrequencyUnit
|
|
148
|
+
|
|
149
|
+
def model_post_init(self, __context: Any) -> None:
|
|
150
|
+
"""
|
|
151
|
+
Validates that the min and max frequency values are positive.
|
|
152
|
+
|
|
153
|
+
Raises:
|
|
154
|
+
ValueError: If the min or max energy values are not defined, are non-positive, and max is greater
|
|
155
|
+
than min.
|
|
156
|
+
"""
|
|
157
|
+
if not (self.min and self.max):
|
|
158
|
+
raise MinMaxValueError("Both min and max frequency values must be defined.")
|
|
159
|
+
|
|
160
|
+
if not all([self.min > 0, self.max > 0]):
|
|
161
|
+
raise MinMaxValueError("Frequency values must be positive.")
|
|
162
|
+
|
|
163
|
+
if self.max < self.min:
|
|
164
|
+
raise MinMaxValueError("Max wavelength cannot be less than min wavelength.")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def convert_to_wave(bandpass: EnergyBandpass | FrequencyBandpass) -> WavelengthBandpass:
|
|
168
|
+
"""
|
|
169
|
+
Converts a given EnergyBandpass or FrequencyBandpass to a WavelengthBandPass.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
bandpass (EnergyBandpass | FrequencyBandpass): The bandpass filter in energy or frequency domain.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
WavelengthBandPass: The corresponding bandpass filter in the wavelength domain.
|
|
176
|
+
|
|
177
|
+
Important Note:
|
|
178
|
+
When converting from Energy/Frequency to wavelength, the min/max values are inverted
|
|
179
|
+
in relation to their corresponding wavelengths. Thus, the min/max values are switched during
|
|
180
|
+
conversion.
|
|
181
|
+
"""
|
|
182
|
+
bandpass_min_angstrom = (
|
|
183
|
+
(bandpass.max * u.Unit(bandpass.unit.value))
|
|
184
|
+
.to(u.Unit(WavelengthUnit.ANGSTROM.value), equivalencies=u.spectral())
|
|
185
|
+
.value
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
bandpass_max_angstrom = (
|
|
189
|
+
(bandpass.min * u.Unit(bandpass.unit.value))
|
|
190
|
+
.to(u.Unit(WavelengthUnit.ANGSTROM.value), equivalencies=u.spectral())
|
|
191
|
+
.value
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
return WavelengthBandpass(
|
|
195
|
+
min=bandpass_min_angstrom,
|
|
196
|
+
max=bandpass_max_angstrom,
|
|
197
|
+
unit=WavelengthUnit.ANGSTROM,
|
|
198
|
+
filter_name=bandpass.filter_name,
|
|
199
|
+
)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from pydantic import BaseModel, ConfigDict
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class BaseSchema(BaseModel):
|
|
5
|
+
"""
|
|
6
|
+
Base class for schemas.
|
|
7
|
+
|
|
8
|
+
This class provides a base implementation for schemas and defines the `from_attributes` method.
|
|
9
|
+
Subclasses can inherit from this class and override the `from_attributes` method to define their
|
|
10
|
+
own schema logic.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
model_config = ConfigDict(from_attributes=True, arbitrary_types_allowed=True)
|
|
14
|
+
|
|
15
|
+
def __hash__(self) -> int:
|
|
16
|
+
return hash((type(self),) + tuple(self.__dict__.values()))
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from pydantic import Field
|
|
5
|
+
|
|
6
|
+
from .base import BaseSchema
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Coordinate(BaseSchema):
|
|
10
|
+
"""
|
|
11
|
+
Class that represents a point in spherical space
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
ra: float = Field(ge=-360, le=360)
|
|
15
|
+
dec: float = Field(ge=-90, le=90)
|
|
16
|
+
|
|
17
|
+
def model_post_init(self, __context: Any) -> None:
|
|
18
|
+
"""
|
|
19
|
+
Pydantic post-init hook
|
|
20
|
+
|
|
21
|
+
Post-Init validations.
|
|
22
|
+
Ensure the RA is positive, and rounded to an appropriate precision
|
|
23
|
+
"""
|
|
24
|
+
self.ra = round(360 + self.ra, 5) if self.ra < 0 else round(self.ra, 5)
|
|
25
|
+
self.dec = round(self.dec, 5)
|
|
26
|
+
|
|
27
|
+
def __repr__(self) -> str:
|
|
28
|
+
"""
|
|
29
|
+
Overrides the print statement
|
|
30
|
+
"""
|
|
31
|
+
return f"{self.__class__.__name__}(ra={self.ra}, dec={self.dec})"
|
|
32
|
+
|
|
33
|
+
def __eq__(self, other: object) -> bool:
|
|
34
|
+
"""
|
|
35
|
+
Overrides the coordinate equals
|
|
36
|
+
"""
|
|
37
|
+
if not isinstance(other, Coordinate):
|
|
38
|
+
return NotImplemented
|
|
39
|
+
|
|
40
|
+
ra_eq = np.isclose(self.ra, other.ra, atol=1e-5)
|
|
41
|
+
dec_eq = np.isclose(self.dec, other.dec, atol=1e-5)
|
|
42
|
+
return bool(ra_eq and dec_eq)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
from astropy.time import Time, TimeDelta # type: ignore[import-untyped]
|
|
4
|
+
from pydantic import PlainSerializer, PlainValidator
|
|
5
|
+
|
|
6
|
+
# Custom pydantic type to handle serialization of Astropy Time
|
|
7
|
+
AstropyDateTime = Annotated[
|
|
8
|
+
Time,
|
|
9
|
+
PlainValidator(lambda x: Time(x)),
|
|
10
|
+
PlainSerializer(lambda x: x.utc.datetime if x.isscalar else x.utc.to_datetime().tolist()),
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
AstropyTimeDelta = Annotated[
|
|
14
|
+
TimeDelta,
|
|
15
|
+
PlainValidator(lambda x: TimeDelta(x)),
|
|
16
|
+
PlainSerializer(lambda x: x.to_datetime()),
|
|
17
|
+
]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
class MinMaxValueError(ValueError):
|
|
2
|
+
"""
|
|
3
|
+
Exception raise for invalid bandpass min/max values
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
def __init__(self, message: str):
|
|
7
|
+
super().__init__(message)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BandwidthValueError(ValueError):
|
|
11
|
+
"""
|
|
12
|
+
Exception raise for invalid bandpass central_wavelength/bandwidth values
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, message: str):
|
|
16
|
+
super().__init__(message)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from .base import BaseSchema
|
|
4
|
+
from .coordinate import Coordinate
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Polygon(BaseSchema):
|
|
8
|
+
"""
|
|
9
|
+
Class to represent a spherical polygon
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
coordinates: list[Coordinate]
|
|
13
|
+
|
|
14
|
+
def model_post_init(self, __context: Any) -> None:
|
|
15
|
+
"""
|
|
16
|
+
Pydantic post-init hook
|
|
17
|
+
|
|
18
|
+
Post-Init validations.
|
|
19
|
+
1.) a polygon must contain a list of coordinates
|
|
20
|
+
1.) a polygon's first and final coordinates must be the same (wrapping)
|
|
21
|
+
2.) a polygon has more than 3 unique coordinates
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
if not len(self.coordinates):
|
|
25
|
+
raise ValueError("Invalid polygon, coordinates cannot be empty.")
|
|
26
|
+
|
|
27
|
+
first_coordinate = self.coordinates[0]
|
|
28
|
+
last_coordinate = self.coordinates[len(self.coordinates) - 1]
|
|
29
|
+
|
|
30
|
+
if first_coordinate != last_coordinate:
|
|
31
|
+
self.coordinates.append(first_coordinate)
|
|
32
|
+
|
|
33
|
+
if len(self.coordinates) < 4:
|
|
34
|
+
raise ValueError("Invalid polygon, must contain more than 3 unique coordinates to be a polygon")
|
|
35
|
+
|
|
36
|
+
def __repr__(self) -> str:
|
|
37
|
+
"""
|
|
38
|
+
Overrides the print statement
|
|
39
|
+
"""
|
|
40
|
+
statement = f"{self.__class__.__name__}(\n"
|
|
41
|
+
|
|
42
|
+
for coordinate in self.coordinates:
|
|
43
|
+
statement += f"\t{coordinate.__class__.__name__}({coordinate.ra}, {coordinate.dec}),\n"
|
|
44
|
+
|
|
45
|
+
statement += ")"
|
|
46
|
+
|
|
47
|
+
return statement
|
|
48
|
+
|
|
49
|
+
def __eq__(self, other: object) -> bool:
|
|
50
|
+
"""
|
|
51
|
+
Overrides the coordinate equals
|
|
52
|
+
"""
|
|
53
|
+
if not isinstance(other, Polygon):
|
|
54
|
+
return NotImplemented
|
|
55
|
+
|
|
56
|
+
if len(self.coordinates) != len(other.coordinates):
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
else:
|
|
60
|
+
equivalence: list[bool] = []
|
|
61
|
+
for coordinate_iterable in range(len(self.coordinates)):
|
|
62
|
+
equivalence.append(
|
|
63
|
+
self.coordinates[coordinate_iterable] == other.coordinates[coordinate_iterable]
|
|
64
|
+
)
|
|
65
|
+
return all(equivalence)
|