open-space-toolkit-astrodynamics 16.2.0__py312-none-manylinux2014_x86_64.whl → 16.4.0__py312-none-manylinux2014_x86_64.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 (21) hide show
  1. {open_space_toolkit_astrodynamics-16.2.0.dist-info → open_space_toolkit_astrodynamics-16.4.0.dist-info}/METADATA +1 -1
  2. {open_space_toolkit_astrodynamics-16.2.0.dist-info → open_space_toolkit_astrodynamics-16.4.0.dist-info}/RECORD +21 -16
  3. ostk/astrodynamics/OpenSpaceToolkitAstrodynamicsPy.cpython-312-x86_64-linux-gnu.so +0 -0
  4. ostk/astrodynamics/__init__.pyi +4 -4
  5. ostk/astrodynamics/flight/__init__.pyi +18 -9
  6. ostk/astrodynamics/flight/profile/model.pyi +19 -0
  7. ostk/astrodynamics/libopen-space-toolkit-astrodynamics.so.16 +0 -0
  8. ostk/astrodynamics/test/flight/profile/model/test_tabulated_profile.py +115 -0
  9. ostk/astrodynamics/test/flight/test_profile.py +4 -0
  10. ostk/astrodynamics/test/test_display.py +0 -2
  11. ostk/astrodynamics/test/test_trajectory.py +28 -7
  12. ostk/astrodynamics/test/test_viewer.py +56 -0
  13. ostk/astrodynamics/test/trajectory/model/test_nadir_trajectory.py +87 -0
  14. ostk/astrodynamics/test/trajectory/model/test_tabulated_trajectory.py +303 -0
  15. ostk/astrodynamics/test/trajectory/model/test_target_scan_trajectory.py +126 -0
  16. ostk/astrodynamics/trajectory/__init__.pyi +10 -9
  17. ostk/astrodynamics/trajectory/model.pyi +259 -0
  18. ostk/astrodynamics/viewer.py +316 -14
  19. {open_space_toolkit_astrodynamics-16.2.0.dist-info → open_space_toolkit_astrodynamics-16.4.0.dist-info}/WHEEL +0 -0
  20. {open_space_toolkit_astrodynamics-16.2.0.dist-info → open_space_toolkit_astrodynamics-16.4.0.dist-info}/top_level.txt +0 -0
  21. {open_space_toolkit_astrodynamics-16.2.0.dist-info → open_space_toolkit_astrodynamics-16.4.0.dist-info}/zip-safe +0 -0
@@ -0,0 +1,259 @@
1
+ from __future__ import annotations
2
+ import ostk.astrodynamics.trajectory
3
+ import ostk.mathematics.curve_fitting
4
+ import ostk.physics.coordinate.spherical
5
+ import ostk.physics.environment.object
6
+ import ostk.physics.time
7
+ import ostk.physics.unit
8
+ import typing
9
+ __all__ = ['Nadir', 'Tabulated', 'TargetScan']
10
+ class Nadir(ostk.astrodynamics.trajectory.Model):
11
+ """
12
+
13
+ Nadir trajectory model.
14
+
15
+ This model represents a trajectory that follows the nadir direction of an orbit.
16
+
17
+ """
18
+ __hash__: typing.ClassVar[None] = None
19
+ def __eq__(self, arg0: Nadir) -> bool:
20
+ ...
21
+ def __init__(self, orbit: ostk.astrodynamics.trajectory.Orbit, step_size: ostk.physics.time.Duration = ...) -> None:
22
+ """
23
+ Construct a `Nadir` object from an orbit.
24
+
25
+ Args:
26
+ orbit (Orbit): The orbit.
27
+ step_size (Duration): The step size for the trajectory. Defaults to 1e-2 seconds.
28
+
29
+ Returns:
30
+ Nadir: The `Nadir` object.
31
+ """
32
+ def __ne__(self, arg0: Nadir) -> bool:
33
+ ...
34
+ def __repr__(self) -> str:
35
+ ...
36
+ def __str__(self) -> str:
37
+ ...
38
+ def calculate_state_at(self, instant: ostk.physics.time.Instant) -> ostk.astrodynamics.trajectory.State:
39
+ """
40
+ Calculate the state at a given instant.
41
+
42
+ Args:
43
+ instant (Instant): The instant.
44
+
45
+ Returns:
46
+ State: The state at the given instant.
47
+ """
48
+ def get_orbit(self) -> ostk.astrodynamics.trajectory.Orbit:
49
+ """
50
+ Get the orbit of the nadir model.
51
+
52
+ Returns:
53
+ Orbit: The orbit of the nadir model.
54
+ """
55
+ def get_step_size(self) -> ostk.physics.time.Duration:
56
+ """
57
+ Get the step size of the nadir model.
58
+
59
+ Returns:
60
+ Duration: The step size of the nadir model.
61
+ """
62
+ def is_defined(self) -> bool:
63
+ """
64
+ Check if the model is defined.
65
+
66
+ Returns:
67
+ bool: True if the model is defined, False otherwise.
68
+ """
69
+ class Tabulated(ostk.astrodynamics.trajectory.Model):
70
+ """
71
+
72
+ A trajectory model defined by a set of states.
73
+
74
+
75
+ """
76
+ def __init__(self, states: list[ostk.astrodynamics.trajectory.State], interpolation_type: ostk.mathematics.curve_fitting.Interpolator.Type = ...) -> None:
77
+ """
78
+ Constructor.
79
+
80
+ Args:
81
+ states (Array[State]): The states of the model.
82
+ interpolation_type (Interpolator.Type): The type of interpolation to use. Defaults to Linear.
83
+ """
84
+ def __repr__(self) -> str:
85
+ """
86
+ Convert the model to a string.
87
+
88
+ Returns:
89
+ str: The string representation of the model.
90
+ """
91
+ def __str__(self) -> str:
92
+ """
93
+ Convert the model to a string.
94
+
95
+ Returns:
96
+ str: The string representation of the model.
97
+ """
98
+ def calculate_state_at(self, instant: ostk.physics.time.Instant) -> ostk.astrodynamics.trajectory.State:
99
+ """
100
+ Calculate the state of the model at a specific instant.
101
+
102
+ Args:
103
+ instant (Instant): The instant at which to calculate the state.
104
+
105
+ Returns:
106
+ State: The state of the model at the specified instant.
107
+ """
108
+ def calculate_states_at(self, instants: list[ostk.physics.time.Instant]) -> list[ostk.astrodynamics.trajectory.State]:
109
+ """
110
+ Calculate the states of the model at the specified instants.
111
+
112
+ Args:
113
+ instants (list[Instant]): The instants at which to calculate the states.
114
+
115
+ Returns:
116
+ list[State]: The states of the model at the specified instants.
117
+ """
118
+ def get_first_state(self) -> ostk.astrodynamics.trajectory.State:
119
+ """
120
+ Get the first state of the model.
121
+
122
+ Returns:
123
+ State: The first state of the model.
124
+ """
125
+ def get_interpolation_type(self) -> ostk.mathematics.curve_fitting.Interpolator.Type:
126
+ """
127
+ Get the interpolation type of the model.
128
+
129
+ Returns:
130
+ Interpolator.Type: The interpolation type of the model.
131
+ """
132
+ def get_interval(self) -> ostk.physics.time.Interval:
133
+ """
134
+ Get the interval of the model.
135
+
136
+ Returns:
137
+ Interval: The interval of the model.
138
+ """
139
+ def get_last_state(self) -> ostk.astrodynamics.trajectory.State:
140
+ """
141
+ Get the last state of the model.
142
+
143
+ Returns:
144
+ State: The last state of the model.
145
+ """
146
+ def is_defined(self) -> bool:
147
+ """
148
+ Check if the model is defined.
149
+
150
+ Returns:
151
+ bool: True if the model is defined, False otherwise.
152
+ """
153
+ class TargetScan(ostk.astrodynamics.trajectory.Model):
154
+ """
155
+
156
+ TargetScan trajectory model.
157
+
158
+ This model represents a trajectory that scans between two target locations on a celestial body.
159
+
160
+ """
161
+ __hash__: typing.ClassVar[None] = None
162
+ @staticmethod
163
+ def from_ground_speed(start_lla: ostk.physics.coordinate.spherical.LLA, end_lla: ostk.physics.coordinate.spherical.LLA, ground_speed: ostk.physics.unit.Derived, start_instant: ostk.physics.time.Instant, celestial: ostk.physics.environment.object.Celestial = ..., step_size: ostk.physics.time.Duration = ...) -> TargetScan:
164
+ """
165
+ Construct a `TargetScan` object from ground speed.
166
+
167
+ Args:
168
+ start_lla (LLA): The starting location.
169
+ end_lla (LLA): The ending location.
170
+ ground_speed (Derived): The ground speed.
171
+ start_instant (Instant): The starting instant.
172
+ celestial (Celestial): The celestial body.
173
+ step_size (Duration): The step size for the trajectory.
174
+
175
+ Returns:
176
+ TargetScan: The `TargetScan` object.
177
+ """
178
+ def __eq__(self, arg0: TargetScan) -> bool:
179
+ ...
180
+ def __init__(self, start_lla: ostk.physics.coordinate.spherical.LLA, end_lla: ostk.physics.coordinate.spherical.LLA, start_instant: ostk.physics.time.Instant, end_instant: ostk.physics.time.Instant, celestial: ostk.physics.environment.object.Celestial = ..., step_size: ostk.physics.time.Duration = ...) -> None:
181
+ """
182
+ Construct a `TargetScan` object.
183
+
184
+ Args:
185
+ start_lla (LLA): The starting location.
186
+ end_lla (LLA): The ending location.
187
+ start_instant (Instant): The starting instant.
188
+ end_instant (Instant): The ending instant.
189
+ celestial (Celestial): The celestial body. Defaults to Earth.WGS84().
190
+ step_size (Duration): The step size for the trajectory. Defaults to 1e-2 seconds.
191
+
192
+ Returns:
193
+ TargetScan: The `TargetScan` object.
194
+ """
195
+ def __ne__(self, arg0: TargetScan) -> bool:
196
+ ...
197
+ def __repr__(self) -> str:
198
+ ...
199
+ def __str__(self) -> str:
200
+ ...
201
+ def calculate_state_at(self, instant: ostk.physics.time.Instant) -> ostk.astrodynamics.trajectory.State:
202
+ """
203
+ Calculate the state at a given instant.
204
+
205
+ Args:
206
+ instant (Instant): The instant.
207
+
208
+ Returns:
209
+ State: The state at the given instant.
210
+ """
211
+ def get_celestial(self) -> ostk.physics.environment.object.Celestial:
212
+ """
213
+ Get the celestial object of the target scan.
214
+
215
+ Returns:
216
+ Celestial: The celestial object.
217
+ """
218
+ def get_end_instant(self) -> ostk.physics.time.Instant:
219
+ """
220
+ Get the ending instant of the target scan.
221
+
222
+ Returns:
223
+ Instant: The ending instant.
224
+ """
225
+ def get_end_lla(self) -> ostk.physics.coordinate.spherical.LLA:
226
+ """
227
+ Get the ending LLA of the target scan.
228
+
229
+ Returns:
230
+ LLA: The ending LLA.
231
+ """
232
+ def get_start_instant(self) -> ostk.physics.time.Instant:
233
+ """
234
+ Get the starting instant of the target scan.
235
+
236
+ Returns:
237
+ Instant: The starting instant.
238
+ """
239
+ def get_start_lla(self) -> ostk.physics.coordinate.spherical.LLA:
240
+ """
241
+ Get the starting LLA of the target scan.
242
+
243
+ Returns:
244
+ LLA: The starting LLA.
245
+ """
246
+ def get_step_size(self) -> ostk.physics.time.Duration:
247
+ """
248
+ Get the step size of the target scan.
249
+
250
+ Returns:
251
+ Duration: The step size.
252
+ """
253
+ def is_defined(self) -> bool:
254
+ """
255
+ Check if the model is defined.
256
+
257
+ Returns:
258
+ bool: True if the model is defined, False otherwise.
259
+ """
@@ -16,6 +16,7 @@ except ImportError:
16
16
 
17
17
  from ostk.mathematics.geometry.d3.transformation.rotation import Quaternion
18
18
 
19
+ from ostk.physics.environment.object import Celestial
19
20
  from ostk.physics.unit import Length
20
21
  from ostk.physics.unit import Angle
21
22
  from ostk.physics.time import Instant, Interval, Duration
@@ -23,16 +24,20 @@ from ostk.physics.coordinate import Position
23
24
  from ostk.physics.coordinate import Frame
24
25
  from ostk.physics.coordinate.spherical import LLA
25
26
 
27
+ from ostk.astrodynamics import Trajectory
26
28
  from ostk.astrodynamics.flight import Profile
27
29
  from ostk.astrodynamics.trajectory import Orbit
28
30
  from ostk.astrodynamics.trajectory import State
29
31
 
30
32
  from .converters import coerce_to_datetime
31
33
  from .utilities import lla_from_position
34
+ from .utilities import lla_from_state
35
+ from .utilities import position_from_lla
32
36
 
33
37
  DEFAULT_SATELLITE_IMAGE: str = (
34
38
  "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADJSURBVDhPnZHRDcMgEEMZjVEYpaNklIzSEfLfD4qNnXAJSFWfhO7w2Zc0Tf9QG2rXrEzSUeZLOGm47WoH95x3Hl3jEgilvDgsOQUTqsNl68ezEwn1vae6lceSEEYvvWNT/Rxc4CXQNGadho1NXoJ+9iaqc2xi2xbt23PJCDIB6TQjOC6Bho/sDy3fBQT8PrVhibU7yBFcEPaRxOoeTwbwByCOYf9VGp1BYI1BA+EeHhmfzKbBoJEQwn1yzUZtyspIQUha85MpkNIXB7GizqDEECsAAAAASUVORK5CYII="
35
39
  )
40
+ DEFAULT_STEP_DURATION: Duration = Duration.seconds(10.0)
36
41
 
37
42
 
38
43
  @dataclass
@@ -228,14 +233,7 @@ class Viewer:
228
233
  satellite = cesiumpy.Satellite(
229
234
  position=_generate_sampled_position_from_llas(instants, llas),
230
235
  orientation=_generate_sampled_orientation(states),
231
- availability=cesiumpy.TimeIntervalCollection(
232
- intervals=[
233
- cesiumpy.TimeInterval(
234
- start=coerce_to_datetime(self._interval.get_start()),
235
- stop=coerce_to_datetime(self._interval.get_end()),
236
- ),
237
- ],
238
- ),
236
+ availability=self._get_availability(),
239
237
  model=cesiumpy.IonResource(
240
238
  asset_id=cesium_asset_id or 0
241
239
  ), # TBM: Should be made more robust
@@ -267,6 +265,158 @@ class Viewer:
267
265
 
268
266
  return self
269
267
 
268
+ def add_celestial_body_direction(
269
+ self,
270
+ profile_or_trajectory: Profile | Trajectory,
271
+ celestial: Celestial,
272
+ time_step: Duration | None = None,
273
+ color: str | None = None,
274
+ ) -> Viewer:
275
+ """
276
+ Add the celestial direction to the viewer.
277
+
278
+ Args:
279
+ profile_or_trajectory (Profile | Trajectory): The profile or trajectory to be added.
280
+ celestial (Celestial, optional): The celestial body to be used.
281
+ time_step (Duration): The duration of each step in the grid.
282
+ Default to None. If None, the default step duration is used.
283
+ color (str, optional): The color of the celestial body direction.
284
+ Defaults to None. If None, the color depends on the celestial body (for the Earth, Sun and Moon).
285
+ Otherwise, use the default color (RED).
286
+
287
+ Returns:
288
+ Viewer: The Viewer.
289
+ """
290
+ time_step = time_step or DEFAULT_STEP_DURATION
291
+ alpha_color: float = 0.5
292
+ reference_frame: Frame = Frame.GCRF()
293
+ reference_vector: np.ndarray = np.array([0.0, 0.0, 1.0])
294
+ instants: list[Instant] = self._interval.generate_grid(time_step)
295
+ celestial_name: str = str(celestial.access_name())
296
+
297
+ if color is None:
298
+ if celestial_name == "Earth":
299
+ color = cesiumpy.color.BLUE
300
+ elif celestial_name == "Moon":
301
+ color = cesiumpy.color.GREY
302
+ elif celestial_name == "Sun":
303
+ color = cesiumpy.color.YELLOW
304
+ else:
305
+ color = cesiumpy.color.RED
306
+
307
+ # Apply an alpha to the color
308
+ color = color.with_alpha(alpha_color)
309
+
310
+ def _create_celestial_body_direction_state(
311
+ satellite_state: State,
312
+ reference_frame: Frame = reference_frame,
313
+ reference_vector: np.ndarray = reference_vector,
314
+ celestial: Celestial = celestial,
315
+ ) -> State:
316
+ state_in_reference_frame: State = satellite_state.in_frame(reference_frame)
317
+ return State(
318
+ instant=state_in_reference_frame.get_instant(),
319
+ position=state_in_reference_frame.get_position(),
320
+ velocity=state_in_reference_frame.get_velocity(),
321
+ attitude=Quaternion.shortest_rotation(
322
+ first_vector=_compute_celestial_direction_from_state(
323
+ state=satellite_state,
324
+ celestial=celestial,
325
+ frame=reference_frame,
326
+ ),
327
+ second_vector=reference_vector,
328
+ ),
329
+ angular_velocity=np.zeros(3),
330
+ attitude_frame=reference_frame,
331
+ )
332
+
333
+ celestial_direction_states: list[State] = list(
334
+ map(
335
+ _create_celestial_body_direction_state,
336
+ profile_or_trajectory.get_states_at(instants),
337
+ )
338
+ )
339
+
340
+ satellite = cesiumpy.Satellite(
341
+ position=_generate_sampled_position_from_llas(
342
+ instants=instants,
343
+ llas=_generate_llas(celestial_direction_states),
344
+ ),
345
+ orientation=_generate_sampled_orientation(celestial_direction_states),
346
+ availability=self._get_availability(),
347
+ )
348
+
349
+ _cesium_from_ostk_sensor(
350
+ ConicSensor(
351
+ name=celestial_name.lower() + "_direction",
352
+ direction=reference_vector,
353
+ # Compute the half angle from the celestial body diameter
354
+ half_angle=Angle.degrees(
355
+ _compute_celestial_angular_diameter_from_states(
356
+ celestial=celestial,
357
+ states=celestial_direction_states,
358
+ ).mean()
359
+ / 2.0
360
+ ),
361
+ length=Length.meters(2.0),
362
+ color=color,
363
+ )
364
+ ).render(
365
+ viewer=self._viewer,
366
+ satellite=satellite,
367
+ )
368
+
369
+ return self
370
+
371
+ def add_ground_tracks(
372
+ self,
373
+ profile_or_trajectory: Profile | Trajectory,
374
+ time_step: Duration | None = None,
375
+ show_current_position: bool = True,
376
+ ) -> Viewer:
377
+ """
378
+ Add ground tracks to the viewer.
379
+
380
+ Args:
381
+ profile_or_trajectory (Profile | Trajectory): The profile or trajectory to be added.
382
+ time_step (Duration, optional): The duration of each step in the grid.
383
+ Default to None. If None, the default step duration is used.
384
+ show_current_position (bool, optional): Whether to show the current position as a point. Defaults to True.
385
+
386
+ Returns:
387
+ Viewer: The Viewer.
388
+ """
389
+ time_step = time_step or DEFAULT_STEP_DURATION
390
+
391
+ instants: list[Instant] = self._interval.generate_grid(time_step)
392
+ llas: list[LLA] = []
393
+ ground_track_positions: list[Position] = []
394
+
395
+ for state in profile_or_trajectory.get_states_at(instants):
396
+ satellite_lla: LLA = lla_from_state(state)
397
+ lla: LLA = LLA(
398
+ latitude=satellite_lla.get_latitude(),
399
+ longitude=satellite_lla.get_longitude(),
400
+ altitude=Length.meters(0.0),
401
+ )
402
+ llas.append(lla)
403
+ ground_track_positions.append(position_from_lla(lla))
404
+
405
+ self.add_line(
406
+ positions=ground_track_positions,
407
+ size=1,
408
+ color=cesiumpy.color.GRAY,
409
+ )
410
+
411
+ if show_current_position:
412
+ self.add_moving_point(
413
+ instants=instants,
414
+ llas=llas,
415
+ color=cesiumpy.color.DARKORANGE,
416
+ )
417
+
418
+ return self
419
+
270
420
  def add_target(
271
421
  self,
272
422
  position: Position,
@@ -341,6 +491,40 @@ class Viewer:
341
491
 
342
492
  return self
343
493
 
494
+ def add_moving_point(
495
+ self,
496
+ instants: list[Instant],
497
+ llas: list[LLA],
498
+ color: str | None = None,
499
+ size: int | None = None,
500
+ ) -> Viewer:
501
+ """
502
+ Add a moving point to the Viewer.
503
+
504
+ Args:
505
+ instants (list[Instant]): The list of instants.
506
+ llas (list[LLA]): The list of Longitude, Latitude, Altitude (LLA) coordinates.
507
+ color (str, optional): The color of the point. Defaults to None. If None, the default color is used.
508
+ size (int, optional): The size of the point. Defaults to None. If None, the default size is used.
509
+
510
+ Returns:
511
+ Viewer: The Viewer.
512
+ """
513
+
514
+ self._viewer.entities.add(
515
+ cesiumpy.Point(
516
+ position=_generate_sampled_position_from_llas(
517
+ instants=instants,
518
+ llas=llas,
519
+ ),
520
+ availability=self._get_availability(),
521
+ color=color,
522
+ pixel_size=size,
523
+ )
524
+ )
525
+
526
+ return self
527
+
344
528
  def add_label(
345
529
  self,
346
530
  position: Position,
@@ -382,14 +566,21 @@ class Viewer:
382
566
 
383
567
  return self._viewer.to_html()
384
568
 
569
+ def _get_availability(self) -> cesiumpy.TimeIntervalCollection:
570
+ """
571
+ Get the availability of the viewer.
572
+
573
+ Returns:
574
+ cesiumpy.TimeIntervalCollection: The availability of the viewer.
575
+ """
576
+ return _cesium_from_ostk_intervals(intervals=[self._interval])
577
+
385
578
  def _repr_html_(self) -> str:
386
579
  return self.render()
387
580
 
388
581
 
389
582
  def _generate_llas(states: list[State]) -> list[LLA]:
390
- return [
391
- lla_from_position(state.get_position(), state.get_instant()) for state in states
392
- ]
583
+ return list(map(lla_from_state, states))
393
584
 
394
585
 
395
586
  def _generate_sampled_position_from_llas(
@@ -454,17 +645,38 @@ def _generate_sampled_position_from_states(
454
645
  Returns:
455
646
  cesiumpy.SampledPositionProperty: Sampled position property.
456
647
  """
648
+ return _generate_sampled_position_from_positions(
649
+ instants=[state.get_instant() for state in states],
650
+ positions=[state.get_position() for state in states],
651
+ )
652
+
653
+
654
+ def _generate_sampled_position_from_positions(
655
+ instants: list[Instant],
656
+ positions: list[Position],
657
+ ) -> cesiumpy.SampledPositionProperty:
658
+ """
659
+ Generate a sampled position property from a list of OSTk positions and instants.
660
+
661
+ Args:
662
+ instants (list[Instant]): A list of OSTk instants.
663
+ positions (list[Position]): A list of OSTk positions.
664
+
665
+ Returns:
666
+ cesiumpy.SampledPositionProperty: Sampled position property.
667
+ """
668
+ frame_itrf: Frame = Frame.ITRF()
457
669
 
458
670
  return cesiumpy.SampledPositionProperty(
459
671
  samples=[
460
672
  (
461
- coerce_to_datetime(state.get_instant()),
673
+ coerce_to_datetime(instant),
462
674
  _cesium_from_ostk_position(
463
- position=state.in_frame(Frame.ITRF()).get_position()
675
+ position.in_frame(instant=instant, frame=frame_itrf)
464
676
  ),
465
677
  None,
466
678
  )
467
- for state in states
679
+ for instant, position in zip(instants, positions)
468
680
  ],
469
681
  )
470
682
 
@@ -547,3 +759,93 @@ def _cesium_from_ostk_sensor(sensor: Sensor) -> cesiumpy.Sensor:
547
759
  )
548
760
 
549
761
  raise NotImplementedError("{sensor.__name__} is not supported yet.")
762
+
763
+
764
+ def _compute_celestial_direction_from_state(
765
+ state: State,
766
+ celestial: Celestial,
767
+ frame: Frame | None = None,
768
+ ) -> np.ndarray:
769
+ """
770
+ Compute the direction of a celestial body from a state.
771
+
772
+ Args:
773
+ state (State): The state of the observer.
774
+ celestial (Celestial): The celestial body.
775
+ frame (Frame): The frame in which the celestial body is expressed.
776
+ Defaults to None. If None, the GCRF frame is used.
777
+
778
+ Returns:
779
+ np.ndarray: The direction of the celestial body (in meters).
780
+ """
781
+ frame = frame or Frame.GCRF()
782
+ return (
783
+ celestial.get_position_in(
784
+ frame=frame,
785
+ instant=state.get_instant(),
786
+ )
787
+ .in_meters()
788
+ .get_coordinates()
789
+ - state.get_position()
790
+ .in_frame(
791
+ frame=frame,
792
+ instant=state.get_instant(),
793
+ )
794
+ .in_meters()
795
+ .get_coordinates()
796
+ )
797
+
798
+
799
+ def _compute_celestial_angular_diameter_from_states(
800
+ celestial: Celestial,
801
+ states: list[State],
802
+ ) -> np.ndarray:
803
+ """
804
+ Compute the angular diameter of a celestial body from the states of an observer.
805
+
806
+ Args:
807
+ celestial (Celestial): The celestial body.
808
+ states (list[State]): The states of the observer.
809
+
810
+ Returns:
811
+ np.ndarray: The angular diameter of the celestial body (in degrees).
812
+
813
+ Reference:
814
+ https://en.wikipedia.org/wiki/Angular_diameter
815
+ """
816
+ celestial_radius_meters: float = float(celestial.get_equatorial_radius().in_meters())
817
+ celestial_to_observer_meters: np.ndarray = np.zeros((3, len(states)))
818
+
819
+ for i, state in enumerate(states):
820
+ celestial_to_observer_meters[:, i] = (
821
+ state.in_frame(celestial.access_frame())
822
+ .get_position()
823
+ .in_meters()
824
+ .get_coordinates()
825
+ )
826
+ distances: np.ndarray = np.linalg.norm(celestial_to_observer_meters, axis=0)
827
+ return np.rad2deg(2 * np.arcsin(celestial_radius_meters / distances))
828
+
829
+
830
+ def _cesium_from_ostk_intervals(
831
+ intervals: list[Interval],
832
+ ) -> cesiumpy.TimeIntervalCollection:
833
+ """
834
+ Convert a list of OSTk intervals into Cesium TimeIntervalCollection.
835
+
836
+ Args:
837
+ intervals (list[Interval]): List of OSTk intervals.
838
+
839
+ Returns:
840
+ cesiumpy.TimeIntervalCollection: Converted intervals.
841
+ """
842
+
843
+ return cesiumpy.TimeIntervalCollection(
844
+ intervals=[
845
+ cesiumpy.TimeInterval(
846
+ start=coerce_to_datetime(interval.get_start()),
847
+ stop=coerce_to_datetime(interval.get_end()),
848
+ )
849
+ for interval in intervals
850
+ ],
851
+ )