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.
Files changed (56) hide show
  1. across/__init__.py +21 -0
  2. across/tools/__init__.py +19 -0
  3. across/tools/_version.py +34 -0
  4. across/tools/core/__init__.py +0 -0
  5. across/tools/core/config.py +29 -0
  6. across/tools/core/enums/__init__.py +7 -0
  7. across/tools/core/enums/constraint_type.py +16 -0
  8. across/tools/core/enums/depth_unit.py +12 -0
  9. across/tools/core/enums/energy_unit.py +13 -0
  10. across/tools/core/enums/frequency_unit.py +13 -0
  11. across/tools/core/enums/wavelength_unit.py +12 -0
  12. across/tools/core/math.py +36 -0
  13. across/tools/core/schemas/__init__.py +26 -0
  14. across/tools/core/schemas/bandpass.py +199 -0
  15. across/tools/core/schemas/base.py +16 -0
  16. across/tools/core/schemas/coordinate.py +42 -0
  17. across/tools/core/schemas/custom_types.py +17 -0
  18. across/tools/core/schemas/exceptions.py +16 -0
  19. across/tools/core/schemas/healpix_order.py +12 -0
  20. across/tools/core/schemas/polygon.py +65 -0
  21. across/tools/core/schemas/roll_angle.py +12 -0
  22. across/tools/core/schemas/tle.py +100 -0
  23. across/tools/core/schemas/visibility.py +39 -0
  24. across/tools/ephemeris/__init__.py +17 -0
  25. across/tools/ephemeris/base.py +212 -0
  26. across/tools/ephemeris/ground_ephem.py +135 -0
  27. across/tools/ephemeris/jpl_ephem.py +160 -0
  28. across/tools/ephemeris/spice_ephem.py +180 -0
  29. across/tools/ephemeris/tle_ephem.py +138 -0
  30. across/tools/example_module.py +23 -0
  31. across/tools/footprint/__init__.py +4 -0
  32. across/tools/footprint/footprint.py +86 -0
  33. across/tools/footprint/healpix_joins.py +44 -0
  34. across/tools/footprint/projection.py +82 -0
  35. across/tools/py.typed +0 -0
  36. across/tools/tle/__init__.py +3 -0
  37. across/tools/tle/exceptions.py +7 -0
  38. across/tools/tle/tle.py +166 -0
  39. across/tools/visibility/__init__.py +14 -0
  40. across/tools/visibility/base.py +275 -0
  41. across/tools/visibility/constraints/__init__.py +25 -0
  42. across/tools/visibility/constraints/alt_az.py +92 -0
  43. across/tools/visibility/constraints/base.py +73 -0
  44. across/tools/visibility/constraints/earth_limb.py +91 -0
  45. across/tools/visibility/constraints/moon_angle.py +81 -0
  46. across/tools/visibility/constraints/polygon.py +34 -0
  47. across/tools/visibility/constraints/saa.py +65 -0
  48. across/tools/visibility/constraints/sun_angle.py +87 -0
  49. across/tools/visibility/constraints_constructor.py +65 -0
  50. across/tools/visibility/ephemeris_visibility.py +179 -0
  51. across/tools/visibility/joint_visibility.py +157 -0
  52. across_tools-0.1.dev42.dist-info/METADATA +320 -0
  53. across_tools-0.1.dev42.dist-info/RECORD +56 -0
  54. across_tools-0.1.dev42.dist-info/WHEEL +5 -0
  55. across_tools-0.1.dev42.dist-info/licenses/LICENSE +201 -0
  56. 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
+ """
@@ -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
+ ]
@@ -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,12 @@
1
+ from enum import Enum
2
+
3
+
4
+ class DepthUnit(str, Enum):
5
+ """
6
+ Enum to represent the astronomical depth types
7
+ """
8
+
9
+ AB_MAG = "ab_mag"
10
+ VEGA_MAG = "vega_mag"
11
+ FLUX_ERG = "flux_erg"
12
+ FLUX_JY = "flux_jy"
@@ -0,0 +1,13 @@
1
+ from enum import Enum
2
+
3
+
4
+ class EnergyUnit(str, Enum):
5
+ """
6
+ Enum to represent the bandpass energy types
7
+ """
8
+
9
+ eV = "eV" # noqa: N815
10
+ keV = "keV" # noqa: N815
11
+ MeV = "MeV"
12
+ GeV = "GeV"
13
+ TeV = "TeV"
@@ -0,0 +1,13 @@
1
+ from enum import Enum
2
+
3
+
4
+ class FrequencyUnit(str, Enum):
5
+ """
6
+ Enum to represent the bandpass frequency types
7
+ """
8
+
9
+ Hz = "Hz"
10
+ kHz = "kHz" # noqa: N815
11
+ MHz = "MHz"
12
+ GHz = "GHz"
13
+ THz = "THz"
@@ -0,0 +1,12 @@
1
+ from enum import Enum
2
+
3
+
4
+ class WavelengthUnit(str, Enum):
5
+ """
6
+ Enum to represent the bandpass wavelength
7
+ """
8
+
9
+ NANOMETER = "nm"
10
+ ANGSTROM = "angstrom"
11
+ MICRON = "um"
12
+ MILLIMETER = "mm"
@@ -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,12 @@
1
+ from pydantic import Field
2
+
3
+ from .base import BaseSchema
4
+
5
+
6
+ class HealpixOrder(BaseSchema):
7
+ """
8
+ Class to represent and validate a Healpix Order
9
+ constraint: Must be (0 >= a >= 13)
10
+ """
11
+
12
+ value: int = Field(gt=0, lt=13, default=10)
@@ -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)
@@ -0,0 +1,12 @@
1
+ from pydantic import Field
2
+
3
+ from .base import BaseSchema
4
+
5
+
6
+ class RollAngle(BaseSchema):
7
+ """
8
+ Class to represent and validate a roll angle
9
+ constraint: Must be (-360.0 >= a >= 360.0)
10
+ """
11
+
12
+ value: float = Field(ge=-360, le=360)