use std::collections::HashMap;
use astrodyn::frame_doc::{
Conventions, DocHeader, FrameDocument, FrameRecord, FrameSeries, FrameUid, Origin,
SeriesBuilder, SCHEMA_VERSION,
};
use astrodyn::frame_doc_io::{export_tree, record_epoch, record_state};
use astrodyn::typed_bridge::{rot_raw_to_self_ref, rot_typed_to_raw, trans_raw_to_typed};
use astrodyn::{
FrameId, IntegrationFrame, JeodQuat, RotationModel, RotationalState, TranslationalState,
};
use super::Simulation;
impl Simulation {
fn doc_header(&self) -> DocHeader {
DocHeader {
schema_version: SCHEMA_VERSION,
conventions: Conventions::current(),
simtime: self.time.simtime,
tai_tjt_at_epoch: self.time.tai_tjt_at_epoch,
}
}
fn frame_origin_classification(&self, id: FrameId) -> Origin {
if id == self.root_frame_id {
return Origin::Injected;
}
for (i, sf) in self.source_frame_ids.iter().enumerate() {
if sf.inertial == id {
return match &self.source_ephem_bodies[i] {
Some((target, observer)) => Origin::Derived {
model: format!("DE4xx:{target:?}/{observer:?}"),
},
None => Origin::Injected,
};
}
if sf.pfix == Some(id) {
return match self.gravity_data[i].rotation_model {
RotationModel::None => Origin::Injected,
model => Origin::Derived {
model: format!("{model:?}"),
},
};
}
}
for body in &self.bodies {
if body.body_frame_id == id {
let (attitude_quat, ang_vel_body) = match &body.rot {
Some(rot) => {
let raw = rot_typed_to_raw(rot);
(Some(raw.quaternion.data), Some(raw.ang_vel_body.to_array()))
}
None => (None, None),
};
return Origin::Integrated {
attitude_quat,
ang_vel_body,
};
}
}
panic!(
"frame_origin_classification: frame {id} (`{}`) was not registered by this \
simulation's construction paths (root / add_source* / add_body) — cannot \
classify its origin for export.",
self.frame_tree.get(id).name
);
}
pub fn export_frame_document(&self) -> FrameDocument {
export_tree(&self.frame_tree, self.doc_header(), |id| {
self.frame_origin_classification(id)
})
}
pub fn apply_frame_document(&mut self, doc: &FrameDocument) {
assert!(
!self.has_stepped,
"apply_frame_document: this simulation has already stepped — restore \
targets a freshly built simulation configured like the producer at \
the snapshot instant."
);
assert!(
self.time.simtime.to_bits() == 0.0_f64.to_bits(),
"apply_frame_document: simulation clock is at simtime {} (expected 0) — \
restore advances the clock itself; do not pre-advance.",
self.time.simtime
);
assert!(
self.time.tai_tjt_at_epoch.to_bits() == doc.header.tai_tjt_at_epoch.to_bits(),
"apply_frame_document: time-epoch mismatch — the document was produced at \
tai_tjt_at_epoch {}, this simulation is configured at {}. Derived time \
scales (TDB, GMST) are functions of the epoch; rebuild the simulation \
with the producer's epoch.",
doc.header.tai_tjt_at_epoch,
self.time.tai_tjt_at_epoch
);
self.time.advance(doc.header.simtime);
let body_of_frame: HashMap<FrameId, usize> = self
.bodies
.iter()
.enumerate()
.map(|(idx, b)| (b.body_frame_id, idx))
.collect();
for (pos, rec) in doc.records.iter().enumerate() {
let uid = &doc.uids[rec.uid_index as usize];
let fid = self.frame_tree.find(uid).unwrap_or_else(|| {
panic!(
"apply_frame_document: record {pos} (`{}`) has identity `{uid}` but \
this simulation's tree has no such frame — rebuild the scenario \
configuration to match the document's frame population.",
rec.name
)
});
let actual_parent = self
.frame_tree
.parent(fid)
.map(|pid| self.frame_tree.get(pid).uid().clone());
let declared_parent: Option<FrameUid> =
rec.parent.map(|p| doc.uids[p as usize].clone());
assert!(
actual_parent == declared_parent,
"apply_frame_document: topology mismatch at record {pos} (`{}`): the \
document declares parent {declared_parent:?}, but this simulation's \
tree has {actual_parent:?}. apply never reparents — rebuild the \
configuration to the document's topology (for a post-frame-switch \
snapshot: set the body's integ_source to the switch target, \
deactivate the switch, and pre-flip the gravity-control \
differential flags).",
rec.name
);
self.frame_tree.get_mut(fid).state = record_state(rec);
self.frame_tree.set_epoch(fid, record_epoch(rec));
match &rec.origin {
Origin::Integrated {
attitude_quat,
ang_vel_body,
} => {
let idx = *body_of_frame.get(&fid).unwrap_or_else(|| {
panic!(
"apply_frame_document: record {pos} (`{}`) is Integrated but \
frame {fid} is not a body frame in this simulation.",
rec.name
)
});
self.bodies[idx].trans =
trans_raw_to_typed::<IntegrationFrame>(&TranslationalState {
position: glam::DVec3::from_array(rec.trans.position),
velocity: glam::DVec3::from_array(rec.trans.velocity),
});
match (attitude_quat, ang_vel_body, self.bodies[idx].rot.is_some()) {
(Some(q), Some(w), true) => {
self.bodies[idx].rot = Some(rot_raw_to_self_ref(&RotationalState {
quaternion: JeodQuat::from_array(*q),
ang_vel_body: glam::DVec3::from_array(*w),
}));
}
(None, None, false) => {}
(payload_q, payload_w, has_rot) => panic!(
"apply_frame_document: record {pos} (`{}`) rotational payload \
(attitude: {}, ang_vel: {}) disagrees with the body's \
degrees of freedom (6-DOF: {has_rot}) — the rebuilt \
VehicleConfig must match the producer's.",
rec.name,
payload_q.is_some(),
payload_w.is_some(),
),
}
}
Origin::Derived { .. } | Origin::Injected => {
for (i, sf) in self.source_frame_ids.iter().enumerate() {
if sf.inertial == fid {
self.gravity_data[i].velocity =
glam::DVec3::from_array(rec.trans.velocity);
}
}
}
}
}
self.has_stepped = true;
}
}
#[derive(Debug)]
pub struct FrameSeriesRecorder {
builder: SeriesBuilder,
}
impl FrameSeriesRecorder {
pub fn new(sim: &Simulation) -> Self {
let doc = sim.export_frame_document();
Self {
builder: SeriesBuilder::new(doc.header, doc.uids),
}
}
pub fn record(&mut self, sim: &Simulation) {
let doc = sim.export_frame_document();
assert!(
doc.uids == self.builder.uids(),
"FrameSeriesRecorder::record: the frame population changed since \
recording began — replay v1 records a fixed population."
);
self.record_rows(sim.time.simtime, doc.records);
}
fn record_rows(&mut self, simtime: f64, rows: Vec<FrameRecord>) {
self.builder.push_epoch(simtime, rows);
}
pub fn finish(self) -> FrameSeries {
self.builder.finish()
}
}