"""
Utilities for generating and reading OpenSpace Asset files
"""
import os
from dataclasses import dataclass
from typing import List, Literal, Optional, Tuple, Union
import numpy as np
import pyarrow as pa
from ...constants import KM_P_AU, S_P_DAY
from ...coordinates.keplerian import KeplerianCoordinates
from ...coordinates.origin import OriginCodes
from ...coordinates.transform import transform_coordinates
from ...orbits import Orbits
from .lua import LuaDict
from .renderable import (
Renderable,
RenderableOrbitalKepler,
RenderableOrbitalKeplerFormat,
RenderableOrbitalKeplerRendering,
RenderableTrailOrbit,
RenderableTrailRendering,
RenderBinMode,
Resource,
)
from .translation import KeplerTranslation, SpiceTranslation, Transform
def _safe_orbital_period(period: float) -> float:
"""
For an orbit with no period or infinite period, use a default value in days.
"""
orbital_period = float(period)
# Use a default value if orbital period is infinite or NaN
if np.isnan(orbital_period) or np.isinf(orbital_period):
return 10000.0
return orbital_period
[docs]
@dataclass(kw_only=True)
class Gui(LuaDict):
name: str
path: str
[docs]
@dataclass(kw_only=True)
class Asset(LuaDict):
identifier: str
parent: str
gui: Gui
renderable: Optional[Renderable] = None
transform: Optional[Transform] = None
[docs]
def orbits_to_sbdb_file(orbits: Orbits, path: str) -> str:
# Convert to Keplerian elements in heliocentric ecliptic J2000 frame
keplerian = transform_coordinates(
orbits.coordinates,
representation_out=KeplerianCoordinates,
frame_out="ecliptic",
origin_out=OriginCodes.SUN,
)
# Write epochs in correct format
epochs_tdb = orbits.coordinates.time.rescale("tdb").to_astropy()
epochs = pa.array(
[
iso.split(" ")[0] + "." + f"{mjd}".split(".")[1]
for iso, mjd in zip(epochs_tdb.iso, epochs_tdb.mjd)
]
)
table = pa.Table.from_pydict(
{
"full_name": orbits.orbit_id,
"epoch_cal": epochs,
"e": keplerian.e,
"a": keplerian.a,
"i": keplerian.i,
"om": keplerian.raan,
"w": keplerian.ap,
"ma": keplerian.M,
"per": keplerian.P,
}
)
table.to_pandas().to_csv(path, index=False)
return
[docs]
def create_initialization(assets: List[str]) -> str:
initialization = ["asset.onInitialize(function ()"]
deinitialization = ["asset.onDeinitialize(function ()"]
for asset in assets:
initialization.append(f" openspace.addSceneGraphNode({asset});")
deinitialization.append(f" openspace.removeSceneGraphNode({asset});")
initialization.append("end)")
deinitialization.append("end)")
combined = "\n".join(initialization) + "\n" + "\n".join(deinitialization)
return combined
[docs]
def create_renderable_orbital_kepler(
orbits: Orbits,
out_dir: str,
identifier: str,
gui_name: Optional[str] = None,
gui_path: Optional[str] = None,
color: Tuple[float, float, float] = (1.0, 1.0, 1.0),
segment_quality: int = 10,
contiguous_mode: Optional[bool] = None,
enable_max_size: Optional[bool] = None,
enable_outline: Optional[bool] = None,
max_size: Optional[float] = None,
outline_color: Optional[Tuple[float, float, float]] = None,
outline_width: Optional[float] = None,
point_size_exponent: Optional[float] = None,
rendering: Optional[Literal["Trail", "Point", "PointsTrails"]] = None,
render_size: Optional[int] = None,
start_render_idx: Optional[int] = None,
trail_fade: Optional[float] = None,
trail_width: Optional[float] = None,
dim_in_atmosphere: Optional[bool] = None,
enabled: Optional[bool] = None,
opacity: Optional[float] = None,
render_bin_mode: Literal["Opaque", "Transparent", "Both"] = None,
tag: Optional[Union[str, List[str]]] = None,
):
"""
Create a renderable orbital Kepler for a given set of orbits.
This is the best renderable to use for large numbers of orbits (e.g. > 1000).
Example
-------
>>> from adam_core.orbits.query import query_sbdb
>>> from adam_core.orbits.openspace import create_renderable_orbital_kepler
>>> orbits = query_sbdb(["2013 RR165", "2018 BP1"])
>>> create_renderable_orbital_kepler(orbits, "out_dir", "openspace_example")
Parameters
----------
orbits : Orbits
The orbits to create a renderable orbitals for.
out_dir : str
The directory to output the asset files
identifier : str
The identifier for the asset
gui_name : str, optional
The name of the GUI for the asset.
gui_path : str, optional
The path of the GUI for the asset.
color : tuple, optional
The color of the Keplerian orbital.
segment_quality : int, optional
The segment quality for the Keplerian orbital.
contiguous_mode : bool, optional
Whether to enable contiguous mode for the Keplerian orbital.
enable_max_size : bool, optional
Whether to enable max size for the Keplerian orbital.
enable_outline : bool, optional
Whether to enable outline for the Keplerian orbital.
max_size : float, optional
The max size for the Keplerian orbital.
outline_color : tuple, optional
The color of the outline for the Keplerian orbital.
outline_width : float, optional
The width of the outline for the Keplerian orbital.
point_size_exponent : float, optional
The size exponent for the Keplerian orbital.
rendering : str, optional
The rendering type for the Keplerian orbital.
render_size : int, optional
The render size for the Keplerian orbital.
start_render_idx : int, optional
The start render index for the Keplerian orbital.
trail_fade : float, optional
The trail fade for the Keplerian orbital.
trail_width : float, optional
The trail width for the Keplerian orbital.
dim_in_atmosphere : bool, optional
Whether to render the Keplerian orbital in the atmosphere.
enabled : bool, optional
Whether to enable the Keplerian orbital.
opacity : float, optional
The opacity of the Keplerian orbital.
render_bin_mode : str, optional
The render bin mode for the Keplerian orbital.
tag : str, optional
The tag for the Keplerian orbital.
"""
safe_identifier = identifier.replace(" ", "_")
if rendering is not None:
rendering = RenderableOrbitalKeplerRendering(rendering)
if render_bin_mode is not None:
render_bin_mode = RenderBinMode(render_bin_mode)
if gui_name is None:
gui_name = safe_identifier
if gui_path is None:
gui_path = "/ADAM"
gui = Gui(name=gui_name, path=gui_path)
# Create SBDB formatted file
os.makedirs(out_dir, exist_ok=True)
path = os.path.join(out_dir, f"{safe_identifier}.csv")
orbits_to_sbdb_file(orbits, path)
# Initialize the renderable
renderable = RenderableOrbitalKepler(
color=color,
format=RenderableOrbitalKeplerFormat.SBDB,
path=Resource(path=os.path.basename(path)),
segment_quality=segment_quality,
contiguous_mode=contiguous_mode,
enable_max_size=enable_max_size,
enable_outline=enable_outline,
max_size=max_size,
outline_color=outline_color,
outline_width=outline_width,
point_size_exponent=point_size_exponent,
rendering=rendering,
render_size=render_size,
start_render_idx=start_render_idx,
trail_fade=trail_fade,
trail_width=trail_width,
dim_in_atmosphere=dim_in_atmosphere,
enabled=enabled,
opacity=opacity,
render_bin_mode=render_bin_mode,
tag=tag,
)
# Declare the asset
asset = Asset(
identifier=safe_identifier,
parent="SunEclipJ2000",
renderable=renderable,
gui=gui,
)
with open(os.path.join(out_dir, f"{safe_identifier}.asset"), "w") as f:
f.write("local Object = ")
f.write(asset.to_string(indent=4))
f.write("\n\n")
f.write(create_initialization(["Object"]))
return
[docs]
def create_renderable_trail_orbit(
orbits,
out_dir: str,
identifier: str,
trail_head: Optional[bool] = True,
gui_name: Optional[str] = None,
gui_path: Optional[str] = None,
color: Tuple[float, float, float] = (1.0, 1.0, 1.0),
resolution: int = 86400,
translation_type: Literal["Kepler", "Spice"] = "Kepler",
enable_fade: Optional[bool] = None,
line_fade_amount: Optional[float] = None,
line_length: Optional[float] = None,
line_width: Optional[float] = None,
point_size: Optional[int] = None,
rendering: Literal["Lines", "Points", "Lines+Points"] = None,
dim_in_atmosphere: Optional[bool] = None,
enabled: Optional[bool] = None,
opacity: Optional[float] = None,
period: Optional[float] = None,
render_bin_mode: Literal["Opaque", "Transparent", "Both"] = None,
tag: Optional[Union[str, List[str]]] = None,
spice_kernel_path: Optional[str] = None,
spice_id_mappings: Optional[dict] = None,
):
"""
Create a renderable trail orbit for a given set of orbits. These orbits can be represented by
two different "translation" types:
- KeplerTranslation: This is a Keplerian translation, which takes in a set of Keplerian elements and a time (default)
- SpiceTranslation: This is a translation that is based on SPICE kernel data. SpiceKernels need to be created (see. )
and passed to this function.
The Keplerian translation is the default and is the best for small numbers of orbits (e.g. < 1000).
The Spice translation is the best to use for a few dozen orbits.
Example
-------
>>> from adam_core.orbits.query import query_sbdb
>>> from adam_core.orbits.openspace import create_renderable_trail_orbit
>>> orbits = query_sbdb(["2013 RR165", "2018 BP1"])
>>> create_renderable_trail_orbit(orbits, "out_dir", "openspace_example", translation_type="Kepler")
Parameters
----------
orbits : Orbits
The orbits to create a renderable trail orbit for
out_dir : str
The directory to output the asset files
identifier : str
The identifier for the asset
trail_head : bool, optional
Whether to include a trail head in the asset.
gui_name : str, optional
The name of the GUI for the asset.
gui_path : str, optional
The path of the GUI for the asset.
color : tuple, optional
The color of the trail.
resolution : int, optional
The resolution of the trail.
translation_type : str, optional
The type of translation to use.
enable_fade : bool, optional
Whether to enable fade for the trail.
line_fade_amount : float, optional
The amount of fade for the trail.
line_length : float, optional
The length of the trail.
line_width : float, optional
The width of the trail.
point_size : int, optional
The size of the points in the trail.
rendering : str, optional
The rendering type for the trail.
dim_in_atmosphere : bool, optional
Whether to render the trail in the atmosphere.
enabled : bool, optional
Whether to enable the trail.
opacity : float, optional
The opacity of the trail.
period : float, optional
The period of the trail.
render_bin_mode : str, optional
The render bin mode for the trail.
tag : str, optional
The tag for the trail.
spice_kernel_path : str, optional
The path to the SPICE kernel file.
spice_id_mappings : dict, optional
A dictionary mapping object IDs to SPICE IDs.
"""
safe_identifier = identifier.replace(" ", "_")
if rendering is not None:
rendering = RenderableTrailRendering(rendering)
if render_bin_mode is not None:
render_bin_mode = RenderBinMode(render_bin_mode)
if gui_path is None:
gui_path = "/ADAM"
keplerian = transform_coordinates(
orbits.coordinates,
representation_out=KeplerianCoordinates,
frame_out="ecliptic",
origin_out=OriginCodes.SUN,
)
if translation_type == "Spice":
if spice_kernel_path is None:
raise ValueError("Spice kernel path is required for Spice translation")
if spice_id_mappings is None:
raise ValueError("Spice ID mappings are required for Spice translation")
os.makedirs(out_dir, exist_ok=True)
with open(os.path.join(out_dir, f"{safe_identifier}.asset"), "w") as f:
assets = []
asset_number = 0
for i, (orbit, keplerian_i) in enumerate(zip(orbits, keplerian)):
orbit_id = orbit.orbit_id[0].as_py()
object_id = orbit.object_id[0].as_py()
if object_id is None:
object_id = orbit_id
if trail_head:
gui_trail = Gui(
name=f"{object_id} Trail", path=gui_path + f"/{object_id}"
)
gui_head = Gui(
name=f"{object_id} Head", path=gui_path + f"/{object_id}"
)
else:
gui_trail = Gui(name=f"{object_id}", path=gui_path)
if translation_type == "Kepler":
translation = KeplerTranslation(
epoch=keplerian_i.time[0].rescale("tdb").to_astropy().iso[0],
semi_major_axis=keplerian_i.a[0].as_py() * KM_P_AU,
eccentricity=keplerian_i.e[0].as_py(),
inclination=keplerian_i.i[0].as_py(),
argument_of_periapsis=keplerian_i.ap[0].as_py(),
ascending_node=keplerian_i.raan[0].as_py(),
mean_anomaly=keplerian_i.M[0].as_py(),
period=_safe_orbital_period(keplerian_i.P[0])
* S_P_DAY, # in seconds here
)
elif translation_type == "Spice":
translation = SpiceTranslation(
target=spice_id_mappings[object_id],
observer="SOLAR SYSTEM BARYCENTER",
)
else:
raise ValueError(f"Invalid translation type: {translation}")
renderable = RenderableTrailOrbit(
color=color,
period=(
_safe_orbital_period(keplerian_i.P[0]) if period is None else period
), # in days here (confusingly)
resolution=resolution,
translation=translation,
enable_fade=enable_fade,
line_fade_amount=line_fade_amount,
line_length=line_length,
line_width=line_width,
point_size=point_size,
rendering=rendering,
)
asset = Asset(
identifier=object_id.replace(" ", "_"),
parent="SunEclipJ2000",
renderable=renderable,
gui=gui_trail,
)
asset_id = f"Object{asset_number:08d}"
asset_number += 1
assets.append(asset_id)
f.write(f"local {asset_id} = ")
f.write(asset.to_string(indent=4))
f.write("\n\n")
if trail_head:
head_asset = Asset(
identifier=object_id.replace(" ", "_") + "_Head",
parent="SunEclipJ2000",
transform=Transform(
translation=translation,
),
gui=gui_head,
)
head_asset_id = f"Object{asset_number:08d}"
asset_number += 1
assets.append(head_asset_id)
f.write(f"local {head_asset_id} = ")
f.write(head_asset.to_string(indent=4))
f.write("\n\n")
initialization = create_initialization(assets)
with open(os.path.join(out_dir, f"{safe_identifier}.asset"), "a") as f:
f.write(initialization)
if translation_type == "Spice":
f.write("asset.onInitialize(function()\n")
f.write(
f" local kernelResource = openspace.resource('{os.path.relpath(spice_kernel_path, out_dir)}')\n"
)
f.write(" openspace.spice.loadKernel(kernelResource)\n")
f.write("end)\n")
f.write("asset.onDeinitialize(function()\n")
f.write(" openspace.spice.unloadKernel(kernelResource)\n")
f.write("end)\n")
return