-
Notifications
You must be signed in to change notification settings - Fork 9
/
Copy pathactor_builder.py
740 lines (643 loc) · 33.2 KB
/
actor_builder.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
from typing import Callable, Any
from loguru import logger
import numpy as np
from dotmap import DotMap
import pykep as pk
from skyfield.api import wgs84
from .base_actor import BaseActor
from .spacecraft_actor import SpacecraftActor
from .ground_station_actor import GroundstationActor
from ..central_body.central_body import CentralBody
from ..thermal.thermal_model import ThermalModel
from ..power.power_device_type import PowerDeviceType
from ..radiation.radiation_model import RadiationModel
from .spacecraft_body_model import SpacecraftBodyModel
from ..attitude.attitude_model import AttitudeModel, TorqueDisturbanceModel
class ActorBuilder:
"""This class is used to construct actors."""
def __new__(cls):
if not hasattr(cls, "instance"):
cls.instance = super(ActorBuilder, cls).__new__(cls)
else:
logger.debug(
"Tried to create another instance of ActorBuilder. Keeping original one..."
)
return cls.instance
def __init__(self):
logger.trace("Initializing ActorBuilder")
@staticmethod
def get_actor_scaffold(name: str, actor_type: object, epoch: pk.epoch):
"""Initiates an actor with minimal properties.
Args:
name (str): Name of the actor.
actor_type (object): Type of the actor (e.g. SpacecraftActor)
epoch (pykep.epoch): Current local time of the actor.
Returns:
Created actor
"""
assert (
actor_type != BaseActor
), "BaseActor cannot be initiated. Please use SpacecraftActor or GroundstationActor"
assert (
actor_type == SpacecraftActor or actor_type == GroundstationActor
), f"Unsupported actor_type {actor_type}, Please use SpacecraftActor or GroundstationActor."
logger.trace(f"Creating an actor blueprint with name {name}")
return actor_type(name, epoch)
@staticmethod
def set_ground_station_location(
actor: GroundstationActor,
latitude: float,
longitude: float,
elevation: float = 0,
minimum_altitude_angle: float = 30,
):
"""Define the position of a ground station actor.
Args:
actor (GroundstationActor): Actor to update.
latitude (float): Latitude of the ground station in degrees.
longitude (float): Longitude of the ground station in degrees.
elevation (float): A distance specifying elevation above (positive)
or below (negative) the surface of the Earth
ellipsoid specified by the WSG84 model in meters. Defaults to 0.
minimum_altitude_angle (float): Minimum angle above the horizon that
this station can communicate with.
"""
assert latitude >= -90 and latitude <= 90, "Latitude is -90 <= lat <= 90"
assert longitude >= -180 and longitude <= 180, "Longitude is -180 <= lat <= 180"
assert (
minimum_altitude_angle >= 0 and minimum_altitude_angle <= 90
), "0 <= minimum_altitude_angle <= 90."
actor._skyfield_position = wgs84.latlon(
latitude_degrees=latitude,
longitude_degrees=longitude,
elevation_m=elevation,
)
actor._minimum_altitude_angle = minimum_altitude_angle
@staticmethod
def set_central_body(
actor: SpacecraftActor,
pykep_planet: pk.planet,
mesh: tuple = None,
radius: float = None,
rotation_declination: float = None,
rotation_right_ascension: float = None,
rotation_period: float = None,
):
"""Define the central body of the actor. This is the body the actor is orbiting around.
If a mesh is provided, it will be used to compute visibility and eclipse checks.
Otherwise, a sphere with the provided radius will be used. One of the two has to be provided.
Note the specification here will not affect the actor orbit.
For that, use set_orbit, set_TLE or set_custom_orbit.
Args:
actor (SpacecraftActor): Actor to update.
pykep_planet (pk.planet): Central body as a pykep planet in heliocentric frame.
mesh (tuple): A tuple of vertices and triangles defining a mesh.
radius (float): Radius of the central body in meters. Only used if no mesh is provided.
rotation_declination (float): Declination of the rotation axis in degrees in the
central body's inertial frame. Rotation at current actor local time is presumed to be 0.
rotation_right_ascension (float): Right ascension of the rotation axis in degrees in
the central body's inertial frame. Rotation at current actor local time is presumed to be 0.
rotation_period (float): Rotation period in seconds. Rotation at current actor local time is presumed to be 0.
"""
assert isinstance(
actor, SpacecraftActor
), "Central body only supported for SpacecraftActors"
# Fuzzy type check for pykep planet
assert "pykep.planet" in str(type(pykep_planet)), "pykep_planet has to be a pykep planet."
assert mesh is not None or radius is not None, "Either mesh or radius has to be provided."
assert mesh is None or radius is None, "Either mesh or radius has to be provided, not both."
# Check rotation parameters
if rotation_declination is not None:
assert (
rotation_declination >= -90 and rotation_declination <= 90
), "Rotation declination has to be -90 <= dec <= 90"
if rotation_right_ascension is not None:
assert (
rotation_right_ascension >= -180 and rotation_right_ascension <= 180
), "Rotation right ascension has to be -180 <= ra <= 180"
if rotation_period is not None:
assert rotation_period > 0, "Rotation period has to be > 0"
# Check if rotation parameters are set
if (
rotation_period is not None
or rotation_right_ascension is not None
or rotation_declination is not None
):
assert (
rotation_right_ascension is not None
), "Rotation right ascension has to be set for rotation."
assert (
rotation_declination is not None
), "Rotation declination has to be set. for rotation."
assert rotation_period is not None, "Rotation period has to be set for rotation."
assert mesh is not None, "Radius cannot only be set for mesh-defined bodies."
if mesh is not None:
# Check mesh
assert isinstance(mesh, tuple), "Mesh has to be a tuple."
assert len(mesh) == 2, "Mesh has to be a tuple of length 2."
assert isinstance(mesh[0], np.ndarray), "Mesh vertices have to be a numpy array."
assert isinstance(mesh[1], np.ndarray), "Mesh triangles have to be a numpy array."
assert len(mesh[0].shape) == 2, "Mesh vertices have to be a numpy array of shape (n,3)."
assert (
len(mesh[1].shape) == 2
), "Mesh triangles have to be a numpy array of shape (n,3)."
# Check if pykep planet is either orbiting the sun or is the sunitself
# by comparing mu values
assert np.isclose(pykep_planet.mu_central_body, 1.32712440018e20) or np.isclose(
pykep_planet.mu_self, 1.32712440018e20
), "Central body has to either be the sun or orbiting the sun."
# Check if the actor already had a central body
if actor.has_central_body:
logger.warning(
"The actor already had a central body. Only one central body is supported. Overriding old body."
)
# Set central body
actor._central_body = CentralBody(
planet=pykep_planet,
initial_epoch=actor.local_time,
mesh=mesh,
encompassing_sphere_radius=radius,
rotation_declination=rotation_declination,
rotation_right_ascension=rotation_right_ascension,
rotation_period=rotation_period,
)
logger.debug(f"Added central body {pykep_planet} to actor {actor}")
@staticmethod
def set_custom_orbit(actor: SpacecraftActor, propagator_func: Callable, epoch: pk.epoch):
"""Define the orbit of the actor using a custom propagator function.
The custom function has to return position and velocity in meters
and meters per second respectively. The function will be called with the
current epoch as the only parameter.
Args:
actor (SpacecraftActor): Actor to update.
propagator_func (Callable): Function to propagate the orbit.
epoch (pk.epoch): Current epoch.
"""
assert callable(propagator_func), "propagator_func has to be callable."
assert isinstance(epoch, pk.epoch), "epoch has to be a pykep epoch."
assert isinstance(actor, SpacecraftActor), "Orbit only supported for SpacecraftActors"
assert actor._orbital_parameters is None, "Actor already has an orbit."
assert np.isclose(
actor.local_time.mjd2000, epoch.mjd2000
), "The initial epoch has to match actor's local time."
actor._custom_orbit_propagator = propagator_func
# Try evaluating position and velocity to check if the function works
try:
position, velocity = actor.get_position_velocity(epoch)
assert len(position) == 3, "Position has to be list of 3 floats."
assert all(
[isinstance(val, float) for val in position]
), "Position has to be list of 3 floats."
assert len(velocity) == 3, "Velocity has to be list of 3 floats."
assert all(
[isinstance(val, float) for val in velocity]
), "Velocity has to be list of 3 floats."
except Exception as e:
logger.error(f"Error evaluating custom orbit propagator function: {e}")
raise RuntimeError("Error evaluating custom orbit propagator function.")
logger.debug(f"Added custom orbit propagator to actor {actor}")
@staticmethod
def set_TLE(
actor: SpacecraftActor,
line1: str,
line2: str,
):
"""Define the orbit of the actor using a TLE. For more information on TLEs see
https://en.wikipedia.org/wiki/Two-line_element_set .
TLEs can be obtained from https://www.space-track.org/ or https://celestrak.com/NORAD/elements/
Args:
actor (SpacecraftActor): Actor to update.
line1 (str): First line of the TLE.
line2 (str): Second line of the TLE.
Raises:
RuntimeError: If the TLE could not be read.
"""
try:
actor._orbital_parameters = pk.planet.tle(line1, line2)
# TLE only works around Earth
ActorBuilder.set_central_body(actor, pk.planet.jpl_lp("earth"), radius=6371000)
except RuntimeError:
logger.error("Error reading TLE \n", line1, "\n", line2)
raise RuntimeError("Error reading TLE")
logger.debug(f"Added TLE to actor {actor}")
@staticmethod
def set_orbit(
actor: SpacecraftActor,
position,
velocity,
epoch: pk.epoch,
central_body: pk.planet,
):
"""Define the orbit of the actor
Args:
actor (BaseActor): The actor to define on
position (list of floats): [x,y,z].
velocity (list of floats): [vx,vy,vz].
epoch (pk.epoch): Time of position / velocity.
central_body (pk.planet): Central body around which the actor is orbiting as a pykep planet.
"""
assert isinstance(actor, SpacecraftActor), "Orbit only supported for SpacecraftActors"
ActorBuilder.set_central_body(actor, central_body, radius=central_body.radius)
actor._orbital_parameters = pk.planet.keplerian(
epoch,
position,
velocity,
central_body.mu_self,
1.0,
1.0,
1.0,
actor.name,
)
logger.debug(f"Added orbit to actor {actor}")
@staticmethod
def set_position(actor: BaseActor, position: list):
"""Sets the actors position. Use this if you do *not* want the actor to have a keplerian orbit around a central body.
Args:
actor (BaseActor): Actor set the position on.
position (list): [x,y,z] position for SpacecraftActor.
"""
assert not isinstance(
actor, GroundstationActor
), "Position changing not supported for GroundstationActors"
assert len(position) == 3, "Position has to be list of 3 floats."
assert all(
[isinstance(val, float) for val in position]
), "Position has to be list of 3 floats."
actor._position = position
logger.debug(f"Setting position {position} on actor {actor}")
@staticmethod
def set_spacecraft_body_model(
actor: SpacecraftActor, mass: float, vertices=None, faces=None, scale: float = 1
):
"""Define geometry of the spacecraft actor. This is done in the spacecraft body reference frame, and can be
transformed to the inertial/PASEOS reference frame using the reference frame transformations in the attitude
model. When used in the attitude model, the geometric model is in the body reference frame.
Args:
actor (SpacecraftActor): Actor to update.
mass (float): Mass of the spacecraft in kg.
vertices (list): List of coordinates [x, y, z] of the vertices of the mesh w.r.t. the body frame [m].
If not selected, it will default to a cube that can be scaled.
by the scale. Uses Trimesh to create the mesh from this and the list of faces.
faces (list): List of the indexes of the vertices of a face. This builds the faces of the satellite by
defining the three vertices to form a triangular face. For a cuboid each face is split into two
triangles. Uses Trimesh to create the mesh from this and the list of vertices.
scale (float): Parameter to scale the cuboid by, defaults to 1.
"""
# check for spacecraft actor
assert isinstance(
actor, SpacecraftActor
), "Body model is only supported for SpacecraftActors."
logger.trace("Checking mass values for sensibility.")
assert mass > 0, "Mass is > 0"
# Check if the actor already has mass.
if actor.mass:
logger.warning("The actor already had a mass. Overriding old mass value.")
actor._mass = mass
# Check if the actor already had a has_spacecraft_body_model.
if actor.has_spacecraft_body_model:
logger.warning(
"The actor already had a spacecraft body model. Overriding old body bodel."
)
# Create a spacraft body model
actor._spacecraft_body_model = SpacecraftBodyModel(
actor_mass=mass, vertices=vertices, faces=faces, scale=scale
)
# Logging
logger.debug(f"Added spacecraft body model to actor {actor}.")
@staticmethod
def set_power_devices(
actor: SpacecraftActor,
battery_level_in_Ws: float,
max_battery_level_in_Ws: float,
charging_rate_in_W: float,
power_device_type: PowerDeviceType = PowerDeviceType.SolarPanel,
):
"""Add a power device (battery + some charging mechanism (e.g. solar power)) to the actor.
This will allow constraints related to power consumption.
Args:
actor (SpacecraftActor): The actor to add to.
battery_level_in_Ws (float): Current battery level in Watt seconds / Joule
max_battery_level_in_Ws (float): Maximum battery level in Watt seconds / Joule
charging_rate_in_W (float): Charging rate of the battery in Watt
power_device_type (PowerDeviceType): Type of power device.
Either "SolarPanel" or "RTG" at the moment. Defaults to SolarPanel.
"""
# check for spacecraft actor
assert isinstance(
actor, SpacecraftActor
), "Power devices are only supported for SpacecraftActors"
# If solar panel, check if the actor has a central body
# to check eclipse
if power_device_type == PowerDeviceType.SolarPanel:
assert actor.has_central_body, "Solar panels require a central body to check eclipse."
# Check if the actor already had a power device
if actor.has_power_model:
logger.warning(
"The actor already had a power device. Currently only one device is supported. Overriding old device."
)
logger.trace("Checking battery values for sensibility.")
assert battery_level_in_Ws > 0, "Battery level must be positive"
assert max_battery_level_in_Ws > 0, "Battery level must be positive"
assert charging_rate_in_W > 0, "Battery level must be positive"
assert (
power_device_type == PowerDeviceType.SolarPanel
or power_device_type == PowerDeviceType.RTG
), "Only SolarPanel and RTG devices supported."
actor._power_device_type = power_device_type
actor._max_battery_level_in_Ws = max_battery_level_in_Ws
actor._battery_level_in_Ws = battery_level_in_Ws
actor._charging_rate_in_W = charging_rate_in_W
logger.debug(
f"Added {power_device_type} power device. MaxBattery={max_battery_level_in_Ws}Ws, "
+ f"CurrBattery={battery_level_in_Ws}Ws, "
+ f"ChargingRate={charging_rate_in_W}W to actor {actor}"
)
@staticmethod
def set_radiation_model(
actor: SpacecraftActor,
data_corruption_events_per_s: float,
restart_events_per_s: float,
failure_events_per_s: float,
):
"""Enables the radiation model allowing data corruption, activities being
interrupted by restarts and potentially critical device failures. Set any of the
passed rates to 0 to disable that particular model.
Args:
actor (SpacecraftActor): The actor to add to.
data_corruption_events_per_s (float): Single bit of data being corrupted, events per second,
i.e. a Single Event Upset (SEU).
restart_events_per_s (float): Device restart being triggered, events per second.
failure_events_per_s (float): Complete device failure, events per second, i.e. a Single Event Latch-Up (SEL).
"""
# check for spacecraft actor
assert isinstance(
actor, SpacecraftActor
), "Radiation models are only supported for SpacecraftActors"
assert data_corruption_events_per_s >= 0, "data_corruption_events_per_s cannot be negative."
assert restart_events_per_s >= 0, "restart_events_per_s cannot be negative."
assert failure_events_per_s >= 0, "failure_events_per_s cannot be negative."
actor._radiation_model = RadiationModel(
data_corruption_events_per_s=data_corruption_events_per_s,
restart_events_per_s=restart_events_per_s,
failure_events_per_s=failure_events_per_s,
)
logger.debug(f"Added radiation model to actor {actor}.")
@staticmethod
def set_thermal_model(
actor: SpacecraftActor,
actor_mass: float,
actor_initial_temperature_in_K: float,
actor_sun_absorptance: float,
actor_infrared_absorptance: float,
actor_sun_facing_area: float,
actor_central_body_facing_area: float,
actor_emissive_area: float,
actor_thermal_capacity: float,
body_solar_irradiance: float = 1360,
body_surface_temperature_in_K: float = 288,
body_emissivity: float = 0.6,
body_reflectance: float = 0.3,
power_consumption_to_heat_ratio: float = 0.5,
):
"""Add a thermal model to the actor to model temperature based on
heat flux from sun, central body albedo, central body IR, actor IR
emission and due to actor activities.
For the moment, it is a slightly simplified version
of the single node model from "Spacecraft Thermal Control" by Prof. Isidoro Martínez
available at http://imartinez.etsiae.upm.es/~isidoro/tc3/Spacecraft%20Thermal%20Modelling%20and%20Testing.pdf
Args:
actor (SpacecraftActor): Actor to model.
actor_mass (float): Actor's mass in kg.
actor_initial_temperature_in_K (float): Actor's initial temperature in K.
actor_sun_absorptance (float): Actor's absorptance ([0,1]) of solar light
actor_infrared_absorptance (float): Actor's absportance ([0,1]) of IR.
actor_sun_facing_area (float): Actor area facing the sun in m^2.
actor_central_body_facing_area (float): Actor area facing central body in m^2.
actor_emissive_area (float): Actor area emitting (radiating) heat.
actor_thermal_capacity (float): Actor's thermal capacity in J / (kg * K).
body_solar_irradiance (float, optional): Irradiance from the sun in W. Defaults to 1360.
body_surface_temperature_in_K (float, optional): Central body surface temperature. Defaults to 288.
body_emissivity (float, optional): Centrla body emissivity [0,1] in IR. Defaults to 0.6.
body_reflectance (float, optional): Central body reflectance of sun light. Defaults to 0.3.
power_consumption_to_heat_ratio (float, optional): Conversion ratio for activities.
0 leads to know heat-up due to activity. Defaults to 0.5.
"""
# check for spacecraft actor
assert isinstance(
actor, SpacecraftActor
), "Thermal models are only supported for SpacecraftActors"
# Check if the actor already had a thermal model
if actor.has_thermal_model:
logger.warning(
"The actor already had a thermal model. Currently only one model is supported. Overriding old model."
)
assert actor_mass > 0, "Actor mass has to be positive."
assert (
0 <= power_consumption_to_heat_ratio and power_consumption_to_heat_ratio <= 1.0
), "Heat ratio has to be 0 to 1."
logger.trace("Checking actor thermal values for sensibility.")
assert 0 <= actor_initial_temperature_in_K, "Actor initial temperature cannot be below 0K."
assert (
0 <= actor_sun_absorptance and actor_sun_absorptance <= 1.0
), "Absorptance has to be 0 to 1."
assert (
0 <= actor_infrared_absorptance and actor_infrared_absorptance <= 1.0
), "Absorptance has to be 0 to 1."
assert 0 < actor_sun_facing_area, "Sun-facing area has to be > 0."
assert 0 < actor_central_body_facing_area, "Body-facing area has to be > 0."
assert 0 < actor_emissive_area, "Actor emissive area has to be > 0."
assert 0 < actor_thermal_capacity, "Thermal capacity has to be > 0"
logger.trace("Checking body thermal values for sensibility.")
assert 0 < body_solar_irradiance, "Solar irradiance has to be > 0."
assert 0 <= body_surface_temperature_in_K, "Body surface temperature cannot be below 0K."
assert 0 <= body_emissivity and body_emissivity <= 1.0, "Body emissivity has to be 0 to 1"
assert (
0 <= body_reflectance and body_reflectance <= 1.0
), "Body reflectance has to be 0 to 1"
actor._mass = actor_mass
actor._thermal_model = ThermalModel(
local_actor=actor,
actor_initial_temperature_in_K=actor_initial_temperature_in_K,
actor_sun_absorptance=actor_sun_absorptance,
actor_infrared_absorptance=actor_infrared_absorptance,
actor_sun_facing_area=actor_sun_facing_area,
actor_central_body_facing_area=actor_central_body_facing_area,
actor_emissive_area=actor_emissive_area,
actor_thermal_capacity=actor_thermal_capacity,
body_solar_irradiance=body_solar_irradiance,
body_surface_temperature_in_K=body_surface_temperature_in_K,
body_emissivity=body_emissivity,
body_reflectance=body_reflectance,
power_consumption_to_heat_ratio=power_consumption_to_heat_ratio,
)
@staticmethod
def set_attitude_disturbances(
actor: SpacecraftActor,
aerodynamic: bool = False,
gravitational: bool = False,
magnetic: bool = False,
):
"""Enables the attitude disturbances to be considered in the attitude modelling for an actor.
Args:
actor (SpacecraftActor): The actor to add to.
aerodynamic (bool): Whether to consider aerodynamic disturbances in the attitude model. Defaults to False.
gravitational (bool): Whether to consider gravity disturbances in the attitude model. Defaults to False.
magnetic (bool): Whether to consider magnetic disturbances in the attitude model. Defaults to False.
"""
# check for spacecraft actor
assert isinstance(
actor, SpacecraftActor
), "Attitude disturbances are only supported for SpacecraftActors."
assert (
actor.has_attitude_model
), "The actor has no attitude model. Impossible to set attitude disturbances."
# Create a list with user specified disturbances which are considered in the attitude modelling.
disturbance_list = []
# Disturbance list name
disturbance_list_name = ""
if aerodynamic:
disturbance_list.append(TorqueDisturbanceModel.Aerodynamic)
disturbance_list_name += "Aerodynamic, "
if gravitational:
disturbance_list.append(TorqueDisturbanceModel.Gravitational)
disturbance_list_name += "Gravitational, "
if magnetic:
disturbance_list.append(TorqueDisturbanceModel.Magnetic)
disturbance_list_name += "Magnetic, "
if len(disturbance_list) > 0:
# Set attitude models.
actor._attitude_model._disturbances = disturbance_list
logger.debug(
f"Added {disturbance_list_name[:-2]} attitude torque disturbance models to actor {actor}."
)
else:
logger.warning("No disturbance model was specified.")
@staticmethod
def set_attitude_model(
actor: SpacecraftActor,
actor_initial_attitude_in_rad: list[float] = [0.0, 0.0, 0.0],
actor_initial_angular_velocity: list[float] = [0.0, 0.0, 0.0],
actor_pointing_vector_body: list[float] = [0.0, 0.0, 1.0],
actor_residual_magnetic_field_body: list[float] = [0.0, 0.0, 0.0],
accommodation_coefficient: float = 0.85,
):
"""Add an attitude model to the actor based on initial conditions: attitude (roll, pitch & yaw angles)
and angular velocity vector, modeling the evolution of the user specified pointing vector.
Args:
actor (SpacecraftActor): Actor to model.
actor_initial_attitude_in_rad (list of floats): Actor's initial attitude.
Defaults to [0.0, 0.0, 0.0].
actor_initial_angular_velocity (list of floats): Actor's initial angular velocity.
Defaults to [0.0, 0.0, 0.0].
actor_pointing_vector_body (list of floats): Actor's pointing vector with respect to the body frame.
Defaults to [0.0, 0.0, 1.0].
actor_residual_magnetic_field_body (list of floats): Actor's residual magnetic dipole moment vector
in the body frame. Only needed if magnetic torque disturbances are modelled.
Please, refer to [Tai L. Chow (2006) p. 148 - 149]. Defaults to [0.0, 0.0, 0.0].
accommodation_coefficient (float): Accommodation coefficient used for Aerodynamic torque disturbance calculation.
Defaults to 0.85.
"""
# check for spacecraft actor
assert isinstance(
actor, SpacecraftActor
), "Attitude model is only supported for SpacecraftActors"
# Check if actor has already an attitude model.
if actor.has_attitude_model:
logger.warning(
"The actor already had an attitude model. Overriding old attitude model."
)
assert (
np.asarray(actor_initial_attitude_in_rad).shape[0] == 3
and np.asarray(actor_initial_attitude_in_rad).ndim == 1
), "actor_initial_attitude_in_rad shall be [3] shaped."
assert (
np.asarray(actor_initial_angular_velocity).shape[0] == 3
and np.asarray(actor_initial_angular_velocity).ndim == 1
), "actor_initial_angular_velocity shall be [3] shaped."
assert (
np.asarray(actor_pointing_vector_body).shape[0] == 3
and np.asarray(actor_pointing_vector_body).ndim == 1
), "actor_pointing_vector_body shall be [3] shaped."
assert (
np.asarray(actor_residual_magnetic_field_body).shape[0] == 3
and np.asarray(actor_residual_magnetic_field_body).ndim == 1
), "actor_residual_magnetic_field_body shall be [3] shaped."
logger.trace("Checking accommodation coefficient for sensibility.")
assert accommodation_coefficient > 0, "Accommodation coefficient shall be positive."
# Set attitude model.
actor._attitude_model = AttitudeModel(
local_actor=actor,
actor_initial_attitude_in_rad=actor_initial_attitude_in_rad,
actor_initial_angular_velocity=actor_initial_angular_velocity,
actor_pointing_vector_body=actor_pointing_vector_body,
actor_residual_magnetic_field_body=actor_residual_magnetic_field_body,
accommodation_coefficient=accommodation_coefficient,
)
logger.debug(f"Added attitude model to actor {actor}.")
@staticmethod
def add_comm_device(actor: BaseActor, device_name: str, bandwidth_in_kbps: float):
"""Creates a communication device.
Args:
device_name (str): device_name of the communication device.
bandwidth_in_kbps (float): device bandwidth in kbps.
"""
if device_name in actor.communication_devices:
raise ValueError(
"Trying to add already existing communication device with device_name: "
+ device_name
)
actor._communication_devices[device_name] = DotMap(bandwidth_in_kbps=bandwidth_in_kbps)
logger.debug(f"Added comm device with bandwith={bandwidth_in_kbps} kbps to actor {actor}.")
@staticmethod
def add_custom_property(
actor: BaseActor,
property_name: str,
initial_value: Any,
update_function: Callable,
):
"""Adds a custom property to the actor. This e.g. allows tracking any physical
the user would like to track.
The update functions needs to take three parameters as input: the actor,
the time to advance the state / model and the current_power_consumption_in_W
and return the new value of the custom property.
The function will be called with (actor,0,0) to check correctness.
Args:
actor (BaseActor): The actor to add the custom property to.
property_name (str): The name of the custom property.
initial_value (Any): The initial value of the custom property.
update_function (Callable): The function to update the custom property.
"""
if property_name in actor._custom_properties:
raise ValueError(f"Custom property '{property_name}' already exists for actor {actor}.")
# Already adding property but will remove if the update function fails
actor._custom_properties[property_name] = initial_value
# Check if the update function accepts the required parameters
try:
logger.trace(f"Checking update function for actor {actor} with time 0 and power 0.")
new_value = update_function(actor, 0, 0)
logger.debug(
f"Update function returned {new_value} for actor {actor} with time 0 and power 0."
)
except TypeError as e:
logger.error(e)
# remove property if this failed
del actor._custom_properties[property_name]
raise TypeError(
"Update function must accept three parameters: actor, time_to_advance, current_power_consumption_in_W."
)
# Check that the update function returns a value of the same type as the initial value
if type(new_value) is not type(initial_value):
# remove property if this failed
del actor._custom_properties[property_name]
raise TypeError(
f"Update function must return a value of type {type(initial_value)} matching initial vaue."
)
# Check that the initial value is the same as the value returned by the update function with time 0
if new_value != initial_value:
# remove property if this failed
del actor._custom_properties[property_name]
raise ValueError(
"Update function must return the existing value when called with unchanged time (dt = 0)."
)
actor._custom_properties_update_function[property_name] = update_function
logger.debug(f"Added custom property '{property_name}' to actor {actor}.")