///|
/// Policy for aligning traces from independent capture sessions.
pub enum AlignmentMode {
FirstEdge
RisingEdgeMedian
MarkerChange
StartAtZero
} derive(Debug, Eq)
///|
/// A measured time offset between two traces.
pub struct AlignmentEstimate {
mode : AlignmentMode
left_marker : String?
right_marker : String?
left_time : Int64?
right_time : Int64?
offset : Int64
confidence : Double
} derive(Debug)
///|
/// A pair of aligned traces and their estimate.
pub struct AlignmentResult {
left : Trace
right : Trace
estimate : AlignmentEstimate
overlap_start : Int64
overlap_end : Int64
comparable : Bool
} derive(Debug)
///|
/// List alignment modes.
pub fn alignment_mode_catalog() -> Array[AlignmentMode] {
[FirstEdge, RisingEdgeMedian, MarkerChange, StartAtZero]
}
///|
/// Estimate an offset using named marker signals.
pub fn estimate_alignment(
left : Trace,
right : Trace,
left_marker : String?,
right_marker : String?,
mode : AlignmentMode,
) -> AlignmentEstimate {
let left_times = alignment_marker_times(left, left_marker, mode)
let right_times = alignment_marker_times(right, right_marker, mode)
let left_time = alignment_first(left_times)
let right_time = alignment_first(right_times)
let offset = match (left_time, right_time) {
(Some(a), Some(b)) => a - b
_ => 0L
}
let confidence = if left_times.length() == 0 || right_times.length() == 0 {
0.0
} else {
Int::min(left_times.length(), right_times.length()).to_double() /
Int::max(left_times.length(), right_times.length()).to_double()
}
{ mode, left_marker, right_marker, left_time, right_time, offset, confidence }
}
///|
/// Shift two traces into one comparable time domain.
pub fn align_traces(
left : Trace,
right : Trace,
left_marker : String?,
right_marker : String?,
mode : AlignmentMode,
) -> AlignmentResult {
let estimate = estimate_alignment(
left, right, left_marker, right_marker, mode,
)
let shifted_right = shift_trace(right, estimate.offset)
let start = Int64::max(
0L,
Int64::max(first_alignment_time(left), first_alignment_time(shifted_right)),
)
let end = Int64::min(left.end_time, shifted_right.end_time)
{
left,
right: shifted_right,
estimate,
overlap_start: start,
overlap_end: end,
comparable: end >= start,
}
}
///|
/// Return whether two traces have a non-empty aligned overlap.
pub fn AlignmentResult::has_overlap(self : AlignmentResult) -> Bool {
self.comparable
}
///|
/// Return an inclusive overlap window.
pub fn AlignmentResult::overlap(self : AlignmentResult) -> (Int64, Int64) {
(self.overlap_start, self.overlap_end)
}
///|
/// Render an alignment estimate.
pub fn AlignmentEstimate::to_text(self : AlignmentEstimate) -> String {
"mode=" +
alignment_mode_name(self.mode) +
" offset=" +
self.offset.to_string() +
" confidence=" +
self.confidence.to_string()
}
///|
/// Render alignment metadata as JSON.
pub fn AlignmentResult::to_json(self : AlignmentResult) -> String {
"{\"offset\":" +
self.estimate.offset.to_string() +
",\"confidence\":" +
self.estimate.confidence.to_string() +
",\"overlap_start\":" +
self.overlap_start.to_string() +
",\"overlap_end\":" +
self.overlap_end.to_string() +
",\"comparable\":" +
self.comparable.to_string() +
"}"
}
///|
/// Return marker times according to an alignment mode.
fn alignment_marker_times(
trace : Trace,
marker : String?,
mode : AlignmentMode,
) -> Array[Int64] {
match mode {
StartAtZero => [0L]
_ =>
match marker {
Some(signal_id) =>
match mode {
FirstEdge | MarkerChange =>
trace.changes
.filter(fn(change) { change.id == signal_id })
.map(fn(change) { change.time })
RisingEdgeMedian =>
edges(trace, signal_id)
.filter(fn(item) { item.kind is Rising })
.map(fn(item) { item.time })
StartAtZero => [0L]
}
None => []
}
}
}
///|
/// Return the first change timestamp, or zero for an empty trace.
fn first_alignment_time(trace : Trace) -> Int64 {
if trace.changes.length() == 0 {
0L
} else {
trace.changes[0].time
}
}
///|
/// Read the first marker timestamp without relying on collection extensions.
fn alignment_first(values : Array[Int64]) -> Int64? {
if values.length() == 0 {
None
} else {
Some(values[0])
}
}
///|
/// Name an alignment mode.
pub fn alignment_mode_name(value : AlignmentMode) -> String {
match value {
FirstEdge => "first-edge"
RisingEdgeMedian => "rising-edge-median"
MarkerChange => "marker-change"
StartAtZero => "start-at-zero"
}
}