///|
/// A deterministic processing operation available in the production pipeline.
pub enum ProcessingStage {
RemoveMeanStage
RemoveTrendStage
CosineTaperStage
MedianDenoiseStage
PeakNormalizeStage
ZScoreNormalizeStage
IntegrateStage
DifferentiateStage
EnvelopeStage
ClipStage
}
///|
/// A configured stage with one numeric parameter.
pub struct ConfiguredStage {
stage : ProcessingStage
parameter : Double
enabled : Bool
}
///|
/// One record in a trace transformation history.
pub struct ProcessingHistoryEntry {
sequence : Int
name : String
input_samples : Int
output_samples : Int
input_rms : Double
output_rms : Double
}
///|
/// Output of a processing graph, including auditable history.
pub struct ProcessingGraphResult {
trace : Trace
history : Array[ProcessingHistoryEntry]
successful_stages : Int
rejected_stages : Int
final_score : Double
}
///|
/// A processing plan used by batch waveform workflows.
pub struct ProcessingPlan {
name : String
stages : Array[ConfiguredStage]
reject_on_invalid : Bool
expected_sample_rate : Double
}
///|
/// Return all stage constructors for configuration validation.
pub fn supported_processing_stages() -> Array[ProcessingStage] {
[
RemoveMeanStage,
RemoveTrendStage,
CosineTaperStage,
MedianDenoiseStage,
PeakNormalizeStage,
ZScoreNormalizeStage,
IntegrateStage,
DifferentiateStage,
EnvelopeStage,
ClipStage,
]
}
///|
/// Construct a default production processing plan.
pub fn default_processing_plan() -> ProcessingPlan {
{
name: "event-preprocess",
stages: [
{ stage: RemoveMeanStage, parameter: 0.0, enabled: true },
{ stage: RemoveTrendStage, parameter: 0.0, enabled: true },
{ stage: CosineTaperStage, parameter: 0.05, enabled: true },
],
reject_on_invalid: true,
expected_sample_rate: 0.0,
}
}
///|
/// Return a stable stage name for reports and tracing.
pub fn processing_stage_name(stage : ProcessingStage) -> String {
match stage {
RemoveMeanStage => "remove-mean"
RemoveTrendStage => "remove-trend"
CosineTaperStage => "cosine-taper"
MedianDenoiseStage => "median-denoise"
PeakNormalizeStage => "peak-normalize"
ZScoreNormalizeStage => "zscore-normalize"
IntegrateStage => "integrate"
DifferentiateStage => "differentiate"
EnvelopeStage => "envelope"
ClipStage => "clip"
}
}
///|
/// Apply one configured operation to a trace copy.
pub fn apply_processing_stage(
trace : Trace,
configured : ConfiguredStage,
) -> Result[Trace, String] {
if !configured.enabled {
return Ok(trace.clone())
}
match configured.stage {
RemoveMeanStage => {
let result = trace.clone()
result.demean()
Ok(result)
}
RemoveTrendStage => {
let result = trace_with_data(trace, detrend_copy(trace.data))
Ok(result)
}
CosineTaperStage => {
let result = trace.clone()
cosine_taper(result.data, configured.parameter)
Ok(result)
}
MedianDenoiseStage => {
let radius = configured.parameter.to_int().max(0)
Ok(trace_with_data(trace, moving_median(trace.data, radius)))
}
PeakNormalizeStage => Ok(normalize_trace_peak(trace))
ZScoreNormalizeStage => Ok(normalize_trace_zscore(trace))
IntegrateStage => Ok(integrate_trace(trace))
DifferentiateStage => Ok(differentiate_trace(trace))
EnvelopeStage => Ok(envelope_trace(trace))
ClipStage => {
let limit = configured.parameter.abs()
if limit <= 0.0 {
Err("clip stage requires a positive limit")
} else {
Ok(clamp_trace(trace, -limit, limit))
}
}
}
}
///|
/// Validate plan metadata before a batch run.
pub fn validate_processing_plan(plan : ProcessingPlan) -> Result[Unit, String] {
if plan.name == "" {
return Err("Processing plan name cannot be empty")
}
if plan.expected_sample_rate < 0.0 {
return Err("Expected sample rate cannot be negative")
}
for configured in plan.stages {
if configured.stage is CosineTaperStage &&
(configured.parameter < 0.0 || configured.parameter > 0.5) {
return Err("Cosine taper fraction must be between zero and one half")
}
if configured.stage is ClipStage &&
configured.enabled &&
configured.parameter <= 0.0 {
return Err("Clip stage needs a positive limit")
}
}
Ok(())
}
///|
/// Compute a quality score from a processing result.
pub fn processing_result_score(
input : Trace,
output : Trace,
rejected_stages : Int,
) -> Double {
let validity = if trace_is_well_formed(output) { 1.0 } else { 0.0 }
let size_score = if input.npts() == 0 {
0.0
} else {
(output.npts().to_double() / input.npts().to_double()).min(1.0)
}
let rejection_score = (1.0 - rejected_stages.to_double() * 0.1).max(0.0)
(0.5 * validity + 0.3 * size_score + 0.2 * rejection_score).min(1.0)
}
///|
/// Execute a plan while recording input and output statistics for every stage.
pub fn run_processing_plan(
trace : Trace,
plan : ProcessingPlan,
) -> ProcessingGraphResult {
let history : Array[ProcessingHistoryEntry] = Array::new()
let mut current = trace.clone()
let mut successful = 0
let mut rejected = 0
for index = 0; index < plan.stages.length(); index = index + 1 {
let configured = plan.stages[index]
if !configured.enabled {
continue
}
let before = current
match apply_processing_stage(current, configured) {
Ok(output) => {
current = output
successful += 1
history.push({
sequence: index,
name: processing_stage_name(configured.stage),
input_samples: before.npts(),
output_samples: current.npts(),
input_rms: rms_samples(before.data),
output_rms: rms_samples(current.data),
})
}
Err(_) => {
rejected += 1
if plan.reject_on_invalid {
break
}
}
}
}
{
trace: current,
history,
successful_stages: successful,
rejected_stages: rejected,
final_score: processing_result_score(trace, current, rejected),
}
}
///|
/// Append a stage to a plan while preserving value semantics.
pub fn append_processing_stage(
plan : ProcessingPlan,
stage : ProcessingStage,
parameter : Double,
) -> ProcessingPlan {
let stages = plan.stages.copy()
stages.push({ stage, parameter, enabled: true })
{
name: plan.name,
stages,
reject_on_invalid: plan.reject_on_invalid,
expected_sample_rate: plan.expected_sample_rate,
}
}
///|
/// Enable or disable a stage by sequence index.
pub fn set_processing_stage_enabled(
plan : ProcessingPlan,
sequence : Int,
enabled : Bool,
) -> ProcessingPlan {
let stages : Array[ConfiguredStage] = Array::new()
for index = 0; index < plan.stages.length(); index = index + 1 {
let current = plan.stages[index]
stages.push(
if index == sequence {
{ stage: current.stage, parameter: current.parameter, enabled }
} else {
current
},
)
}
{
name: plan.name,
stages,
reject_on_invalid: plan.reject_on_invalid,
expected_sample_rate: plan.expected_sample_rate,
}
}
///|
/// Serialize processing history for a run artifact.
pub fn processing_history_to_csv(
history : Array[ProcessingHistoryEntry],
) -> String {
let mut output = "sequence,stage,input_samples,output_samples,input_rms,output_rms\n"
for entry in history {
output = output +
entry.sequence.to_string() +
"," +
entry.name +
"," +
entry.input_samples.to_string() +
"," +
entry.output_samples.to_string() +
"," +
entry.input_rms.to_string() +
"," +
entry.output_rms.to_string() +
"\n"
}
output
}
///|
/// Return the number of enabled operations in a plan.
pub fn enabled_stage_count(plan : ProcessingPlan) -> Int {
plan.stages.filter(stage => stage.enabled).length()
}
///|
/// Return a compact plan description.
pub fn processing_plan_text(plan : ProcessingPlan) -> String {
let names = plan.stages
.filter(stage => stage.enabled)
.map(stage => processing_stage_name(stage.stage))
let mut joined = ""
for index = 0; index < names.length(); index = index + 1 {
if index > 0 {
joined = joined + " -> "
}
joined = joined + names[index]
}
"name=" +
plan.name +
" stages=" +
joined +
" reject_on_invalid=" +
plan.reject_on_invalid.to_string()
}
///|
/// Format a graph result headline.
pub fn processing_graph_result_text(result : ProcessingGraphResult) -> String {
"samples=" +
result.trace.npts().to_string() +
" successful=" +
result.successful_stages.to_string() +
" rejected=" +
result.rejected_stages.to_string() +
" history=" +
result.history.length().to_string() +
" score=" +
result.final_score.to_string()
}