///|
/// Value selection policy for resampling an asynchronous waveform.
pub enum SamplingMode {
HoldPrevious
NearestChange
ExactOnly
LinearReal
} derive(Debug, Eq)
///|
/// Sampling configuration shared by signal and trace resampling.
pub struct SamplingConfig {
start : Int64
end : Int64
step : Int64
mode : SamplingMode
include_unknown : Bool
} derive(Debug)
///|
/// One sampled value with provenance metadata.
pub struct SamplingPoint {
signal_id : String
time : Int64
value : Value?
source_time : Int64?
distance : Int64?
} derive(Debug)
///|
/// A sampled trace preserving the source declaration shape.
pub struct SampledTrace {
source_end : Int64
config : SamplingConfig
points : Array[SamplingPoint]
sample_count : Int
known_count : Int
} derive(Debug)
///|
/// List sampling modes.
pub fn sampling_mode_catalog() -> Array[SamplingMode] {
[HoldPrevious, NearestChange, ExactOnly, LinearReal]
}
///|
/// Create a hold-last-value sampling configuration.
pub fn sampling_config(
start : Int64,
end : Int64,
step : Int64,
) -> SamplingConfig {
{
start,
end: Int64::max(start, end),
step: Int64::max(1L, step),
mode: HoldPrevious,
include_unknown: true,
}
}
///|
/// Select a sampling mode.
pub fn SamplingConfig::with_mode(
self : SamplingConfig,
mode : SamplingMode,
) -> SamplingConfig {
{ ..self, mode, }
}
///|
/// Keep or omit points whose value is unknown.
pub fn SamplingConfig::with_unknown(
self : SamplingConfig,
keep_unknown : Bool,
) -> SamplingConfig {
{ ..self, include_unknown: keep_unknown }
}
///|
/// Resample every declared signal at a fixed time grid.
pub fn sample_trace(trace : Trace, config : SamplingConfig) -> SampledTrace {
let points : Array[SamplingPoint] = []
let mut time = Int64::max(0L, config.start)
let end = Int64::min(trace.end_time, config.end)
while time <= end {
for signal in trace.signals {
let point = sample_one(trace, signal.id, time, config.mode)
match point.value {
Some(value) if !config.include_unknown && value.is_unknown() => ()
_ => points.push(point)
}
}
time += config.step
}
{
source_end: trace.end_time,
config,
sample_count: points.length(),
known_count: points
.filter(fn(item) {
match item.value {
Some(value) => !value.is_unknown()
None => false
}
})
.length(),
points,
}
}
///|
/// Resample one signal only.
pub fn sample_trace_signal(
trace : Trace,
signal_id : String,
config : SamplingConfig,
) -> Array[SamplingPoint] {
let result : Array[SamplingPoint] = []
let mut time = Int64::max(0L, config.start)
let end = Int64::min(trace.end_time, config.end)
while time <= end {
let point = sample_one(trace, signal_id, time, config.mode)
match point.value {
Some(value) if !config.include_unknown && value.is_unknown() => ()
_ => result.push(point)
}
time += config.step
}
result
}
///|
/// Return points for one timestamp across all signals.
pub fn SampledTrace::at(
self : SampledTrace,
time : Int64,
) -> Array[SamplingPoint] {
self.points.filter(fn(item) { item.time == time })
}
///|
/// Return the number of grid timestamps represented in the result.
pub fn SampledTrace::timestamp_count(self : SampledTrace) -> Int {
let times : Array[Int64] = []
for point in self.points {
if !times.contains(point.time) {
times.push(point.time)
}
}
times.length()
}
///|
/// Export samples as CSV.
pub fn sampling_points_csv(points : Array[SamplingPoint]) -> String {
let out = StringBuilder()
out.write_string("signal,time,value,source_time,distance\n")
for point in points {
out.write_string(
escape_text(point.signal_id) + "," + point.time.to_string() + ",",
)
out.write_string(
match point.value {
Some(value) => escape_text(value.display())
None => ""
},
)
out.write_string(
"," +
sampling_optional_i64(point.source_time) +
"," +
sampling_optional_i64(point.distance) +
"\n",
)
}
out.to_string()
}
///|
/// Render sampling metadata as JSON.
pub fn SampledTrace::to_json(self : SampledTrace) -> String {
let points = self.points.map(sampling_point_json).join(",")
"{\"sample_count\":" +
self.sample_count.to_string() +
",\"known_count\":" +
self.known_count.to_string() +
",\"step\":" +
self.config.step.to_string() +
",\"points\":[" +
points +
"]}"
}
///|
/// Find the value according to the selected resampling mode.
fn sample_one(
trace : Trace,
signal_id : String,
time : Int64,
mode : SamplingMode,
) -> SamplingPoint {
let candidates = trace.changes.filter(fn(change) { change.id == signal_id })
let mut previous : Change? = None
let mut next : Change? = None
for change in candidates {
if change.time <= time {
previous = Some(change)
}
if change.time >= time {
next = Some(change)
break
}
}
match mode {
HoldPrevious =>
match previous {
Some(change) =>
{
signal_id,
time,
value: Some(change.value),
source_time: Some(change.time),
distance: Some(time - change.time),
}
None =>
{ signal_id, time, value: None, source_time: None, distance: None }
}
ExactOnly =>
match candidates.filter(fn(change) { change.time == time })[0:] {
[change, ..] =>
{
signal_id,
time,
value: Some(change.value),
source_time: Some(change.time),
distance: Some(0L),
}
_ => { signal_id, time, value: None, source_time: None, distance: None }
}
NearestChange => sampling_nearest(signal_id, time, previous, next)
LinearReal => sampling_linear(signal_id, time, previous, next)
}
}
///|
/// Select the closest known change, preferring the previous value on ties.
fn sampling_nearest(
signal_id : String,
time : Int64,
previous : Change?,
next : Change?,
) -> SamplingPoint {
match (previous, next) {
(Some(left), Some(right)) => {
let left_distance = time - left.time
let right_distance = right.time - time
if left_distance <= right_distance {
{
signal_id,
time,
value: Some(left.value),
source_time: Some(left.time),
distance: Some(left_distance),
}
} else {
{
signal_id,
time,
value: Some(right.value),
source_time: Some(right.time),
distance: Some(right_distance),
}
}
}
(Some(left), None) =>
{
signal_id,
time,
value: Some(left.value),
source_time: Some(left.time),
distance: Some(time - left.time),
}
(None, Some(right)) =>
{
signal_id,
time,
value: Some(right.value),
source_time: Some(right.time),
distance: Some(right.time - time),
}
_ => { signal_id, time, value: None, source_time: None, distance: None }
}
}
///|
/// Interpolate real values and fall back to nearest for digital values.
fn sampling_linear(
signal_id : String,
time : Int64,
previous : Change?,
next : Change?,
) -> SamplingPoint {
match (previous, next) {
(Some(left), Some(right)) =>
match (left.value, right.value) {
(Real(a), Real(b)) if right.time > left.time => {
let ratio = (time - left.time).to_double() /
(right.time - left.time).to_double()
{
signal_id,
time,
value: Some(Real(a + (b - a) * ratio)),
source_time: Some(left.time),
distance: Some(time - left.time),
}
}
_ => sampling_nearest(signal_id, time, Some(left), Some(right))
}
_ => sampling_nearest(signal_id, time, previous, next)
}
}
///|
/// Encode an optional timestamp.
fn sampling_optional_i64(value : Int64?) -> String {
match value {
Some(item) => item.to_string()
None => ""
}
}
///|
/// Encode one sample as JSON.
fn sampling_point_json(item : SamplingPoint) -> String {
let value = match item.value {
Some(item) => "\"" + escape_text(item.display()) + "\""
None => "null"
}
"{\"signal\":\"" +
escape_text(item.signal_id) +
"\",\"time\":" +
item.time.to_string() +
",\"value\":" +
value +
",\"source_time\":" +
sampling_optional_json_i64(item.source_time) +
",\"distance\":" +
sampling_optional_json_i64(item.distance) +
"}"
}
///|
/// Encode an optional integer as JSON.
fn sampling_optional_json_i64(value : Int64?) -> String {
match value {
Some(item) => item.to_string()
None => "null"
}
}