///|
/// A reason why a measurement was changed or rejected during preprocessing.
pub(all) struct CleanIssue {
index : Int
code : String
message : String
original : Double?
replacement : Double?
} derive(Debug, ToJson)
///|
/// Result of a deterministic measurement-cleaning pass.
pub(all) struct CleanResult {
values : Array[Double]
original_count : Int
removed : Int
replaced : Int
issues : Array[CleanIssue]
} derive(Debug, ToJson)
///|
/// A nullable series is useful when an upstream sensor reports missing values.
pub type MaybeMeasurement = Double?
///|
/// Filter values outside an engineering interval and retain an audit trail.
pub fn clean_values(
values : Array[Double],
lower~ : Double,
upper~ : Double,
) -> CleanResult {
let output : Array[Double] = []
let issues : Array[CleanIssue] = []
let mut removed = 0
if lower > upper {
issues.push({
index: -1,
code: "INVALID_LIMITS",
message: "lower limit exceeds upper limit",
original: None,
replacement: None,
})
{
values: output,
original_count: values.length(),
removed: values.length(),
replaced: 0,
issues,
}
} else {
for index, value in values {
if value < lower || value > upper {
removed += 1
issues.push({
index,
code: "OUT_OF_RANGE",
message: "measurement removed outside engineering interval",
original: Some(value),
replacement: None,
})
} else {
output.push(value)
}
}
{
values: output,
original_count: values.length(),
removed,
replaced: 0,
issues,
}
}
}
///|
/// Replace out-of-range values with a boundary instead of dropping them.
pub fn clamp_values(
values : Array[Double],
lower~ : Double,
upper~ : Double,
) -> CleanResult {
let output : Array[Double] = []
let issues : Array[CleanIssue] = []
let mut replaced = 0
if lower > upper {
issues.push({
index: -1,
code: "INVALID_LIMITS",
message: "lower limit exceeds upper limit",
original: None,
replacement: None,
})
{
values: [],
original_count: values.length(),
removed: values.length(),
replaced: 0,
issues,
}
} else {
for index, value in values {
let replacement = value.max(lower).min(upper)
if replacement != value {
replaced += 1
issues.push({
index,
code: "CLAMPED",
message: "measurement replaced by nearest boundary",
original: Some(value),
replacement: Some(replacement),
})
}
output.push(replacement)
}
{
values: output,
original_count: values.length(),
removed: 0,
replaced,
issues,
}
}
}
///|
/// Replace missing interior points by linear interpolation.
pub fn interpolate_missing(
values : Array[MaybeMeasurement],
) -> Array[MaybeMeasurement] {
let output = values.copy()
for index in 0..
output[index] = Some(left + (right - left) / distance.to_double())
None => ()
}
}
}
output
}
///|
fn surrounding_values(
values : Array[MaybeMeasurement],
index : Int,
) -> (Double, Double, Int)? {
let mut left_index = index - 1
let mut left : Double? = None
while left_index >= 0 {
match values[left_index] {
Some(value) => {
left = Some(value)
break
}
None => left_index -= 1
}
}
let mut right_index = index + 1
let mut right : Double? = None
while right_index < values.length() {
match values[right_index] {
Some(value) => {
right = Some(value)
break
}
None => right_index += 1
}
}
match (left, right) {
(Some(first), Some(second)) =>
Some((first, second, right_index - left_index))
_ => None
}
}
///|
/// Replace every missing point with a caller-supplied process center.
pub fn fill_missing(
values : Array[MaybeMeasurement],
replacement : Double,
) -> CleanResult {
let output : Array[Double] = []
let issues : Array[CleanIssue] = []
let mut replaced = 0
for index, value in values {
match value {
Some(number) => output.push(number)
None => {
replaced += 1
output.push(replacement)
issues.push({
index,
code: "MISSING_FILLED",
message: "missing measurement filled by policy",
original: None,
replacement: Some(replacement),
})
}
}
}
{
values: output,
original_count: values.length(),
removed: 0,
replaced,
issues,
}
}
///|
/// Return the first difference of a series, preserving time order.
pub fn first_difference(values : Array[Double]) -> Array[Double] {
let output : Array[Double] = []
if values.length() >= 2 {
for index in 1.. Array[Double] {
first_difference(first_difference(values))
}
///|
/// Cumulative sum for material balance and defect accumulation charts.
pub fn cumulative_sum(values : Array[Double]) -> Array[Double] {
let output : Array[Double] = []
let mut total = 0.0
for value in values {
total += value
output.push(total)
}
output
}
///|
/// Cumulative product for multiplicative reliability factors.
pub fn cumulative_product(values : Array[Double]) -> Array[Double] {
let output : Array[Double] = []
let mut total = 1.0
for value in values {
total *= value
output.push(total)
}
output
}
///|
/// Shift a series by a non-negative lag; the leading positions retain source values.
pub fn lag_values(values : Array[Double], lag : Int) -> Array[Double] {
let output : Array[Double] = []
if lag <= 0 || lag >= values.length() {
for value in values {
output.push(value)
}
} else {
for index in 0.. Array[Double] {
let output : Array[Double] = []
if values.length() == 0 || lead <= 0 || lead >= values.length() {
for value in values {
output.push(value)
}
} else {
let final_value = values[values.length() - 1]
for index in 0.. Array[Double]? {
if values.length() == 0 || values[0] == 0.0 {
None
} else {
let output : Array[Double] = []
for value in values {
output.push(value / values[0])
}
Some(output)
}
}
///|
/// Normalize a series by its last observation.
pub fn relative_to_last(values : Array[Double]) -> Array[Double]? {
if values.length() == 0 || values[values.length() - 1] == 0.0 {
None
} else {
let output : Array[Double] = []
let final_value = values[values.length() - 1]
for value in values {
output.push(value / final_value)
}
Some(output)
}
}
///|
/// Center values around a supplied reference.
pub fn center_values(values : Array[Double], center : Double) -> Array[Double] {
let output : Array[Double] = []
for value in values {
output.push(value - center)
}
output
}
///|
/// Scale values by a positive engineering scale.
pub fn scale_values(values : Array[Double], scale : Double) -> Array[Double]? {
if scale == 0.0 {
None
} else {
let output : Array[Double] = []
for value in values {
output.push(value / scale)
}
Some(output)
}
}
///|
/// Apply a center and scale transformation.
pub fn standardize_values(
values : Array[Double],
center : Double,
scale : Double,
) -> Array[Double]? {
match scale_values(center_values(values, center), scale) {
Some(output) => Some(output)
None => None
}
}
///|
/// Remove adjacent duplicate sensor readings.
pub fn deduplicate_adjacent(values : Array[Double]) -> Array[Double] {
let output : Array[Double] = []
for value in values {
match output.last() {
Some(previous) => if previous != value { output.push(value) }
None => output.push(value)
}
}
output
}
///|
/// Remove values that repeat more than a configured run length.
pub fn limit_repeated_runs(
values : Array[Double],
maximum : Int,
) -> CleanResult {
let output : Array[Double] = []
let issues : Array[CleanIssue] = []
let mut removed = 0
if maximum <= 0 {
for index, value in values {
issues.push({
index,
code: "RUN_REMOVED",
message: "all values rejected by non-positive run limit",
original: Some(value),
replacement: None,
})
removed += 1
}
{
values: output,
original_count: values.length(),
removed,
replaced: 0,
issues,
}
} else {
let mut previous : Double? = None
let mut run = 0
for index, value in values {
if previous is Some(_) {
run += 1
} else {
run = 1
}
if run <= maximum {
output.push(value)
} else {
removed += 1
issues.push({
index,
code: "RUN_TRIMMED",
message: "repeated sensor run exceeded policy",
original: Some(value),
replacement: None,
})
}
previous = Some(value)
}
{
values: output,
original_count: values.length(),
removed,
replaced: 0,
issues,
}
}
}
///|
/// Remove values with a robust IQR fence and return an audit trail.
pub fn remove_iqr_outliers(
values : Array[Double],
multiplier~ : Double,
) -> CleanResult {
let output : Array[Double] = []
let issues : Array[CleanIssue] = []
match (quantile(values, 0.25), quantile(values, 0.75)) {
(Some(first), Some(third)) => {
let low = first - multiplier * (third - first)
let high = third + multiplier * (third - first)
let mut removed = 0
for index, value in values {
if value < low || value > high {
removed += 1
issues.push({
index,
code: "IQR_OUTLIER",
message: "measurement outside robust IQR fence",
original: Some(value),
replacement: None,
})
} else {
output.push(value)
}
}
{
values: output,
original_count: values.length(),
removed,
replaced: 0,
issues,
}
}
_ =>
{
values: values.copy(),
original_count: values.length(),
removed: 0,
replaced: 0,
issues,
}
}
}
///|
/// Replace robust outliers with the nearest fence boundary.
pub fn winsorize_values(
values : Array[Double],
multiplier~ : Double,
) -> CleanResult {
let output : Array[Double] = []
let issues : Array[CleanIssue] = []
match (quantile(values, 0.25), quantile(values, 0.75)) {
(Some(first), Some(third)) => {
let low = first - multiplier * (third - first)
let high = third + multiplier * (third - first)
let mut replaced = 0
for index, value in values {
let replacement = value.max(low).min(high)
if replacement != value {
replaced += 1
issues.push({
index,
code: "IQR_WINSORIZED",
message: "outlier replaced by robust fence",
original: Some(value),
replacement: Some(replacement),
})
}
output.push(replacement)
}
{
values: output,
original_count: values.length(),
removed: 0,
replaced,
issues,
}
}
_ =>
{
values: values.copy(),
original_count: values.length(),
removed: 0,
replaced: 0,
issues,
}
}
}
///|
/// Return a moving average with an explicit partial-window policy.
pub fn centered_moving_average(
values : Array[Double],
radius : Int,
) -> Array[Double]? {
if radius < 0 {
None
} else {
let output : Array[Double] = []
for index in 0.. Array[Double] {
let output : Array[Double] = []
if alpha < 0.0 || alpha > 1.0 {
output
} else {
let mut state = initial
for value in values {
state = alpha * value + (1.0 - alpha) * state
output.push(state)
}
output
}
}
///|
/// Compute a stable rolling sum without allocating one array per window.
pub fn rolling_sum(values : Array[Double], width : Int) -> Array[Double]? {
if width <= 0 || width > values.length() {
None
} else {
let output : Array[Double] = []
let mut total = 0.0
for index in 0.. Int? {
if values.length() < 2 {
None
} else {
let mut best_index = 1
let mut best = (values[1] - values[0]).abs()
for index in 2.. best {
best = change
best_index = index
}
}
Some(best_index)
}
}
///|
/// Return a boolean mask for values within a closed interval.
pub fn within_mask(
values : Array[Double],
lower : Double,
upper : Double,
) -> Array[Bool] {
let output : Array[Bool] = []
for value in values {
output.push(value >= lower && value <= upper)
}
output
}
///|
/// Return a boolean mask for values outside a closed interval.
pub fn outside_mask(
values : Array[Double],
lower : Double,
upper : Double,
) -> Array[Bool] {
let output : Array[Bool] = []
for value in values {
output.push(value < lower || value > upper)
}
output
}
///|
/// Count true entries in a mask.
pub fn count_mask(mask : Array[Bool]) -> Int {
let mut count = 0
for value in mask {
if value {
count += 1
}
}
count
}
///|
/// Apply a boolean mask to a measurement series.
pub fn select_mask(values : Array[Double], mask : Array[Bool]) -> Array[Double] {
let output : Array[Double] = []
let length = values.length().min(mask.length())
for index in 0.. (Array[Double], Array[Double]) {
let left : Array[Double] = []
let right : Array[Double] = []
let pivot = index.max(0).min(values.length())
for cursor in 0.. Array[Double] {
let output = first.copy()
for value in second {
output.push(value)
}
output
}
///|
/// Reverse a series without mutating the caller's array.
pub fn reverse_values(values : Array[Double]) -> Array[Double] {
let output : Array[Double] = []
let mut index = values.length()
while index > 0 {
index -= 1
output.push(values[index])
}
output
}
///|
/// Rotate a series left by a normalized offset.
pub fn rotate_values(values : Array[Double], offset : Int) -> Array[Double] {
if values.length() == 0 {
[]
} else {
let normalized = (offset % values.length() + values.length()) %
values.length()
let output : Array[Double] = []
for index in 0.. Bool {
let mut valid = true
for index in 1.. Bool {
let mut valid = true
for index in 1.. values[index - 1] {
valid = false
}
}
valid
}
///|
/// Count missing entries in a nullable series.
pub fn count_missing(values : Array[MaybeMeasurement]) -> Int {
let mut count = 0
for value in values {
if value is None {
count += 1
}
}
count
}
///|
/// Convert a nullable series to a compact series by dropping missing entries.
pub fn drop_missing(values : Array[MaybeMeasurement]) -> Array[Double] {
let output : Array[Double] = []
for value in values {
match value {
Some(number) => output.push(number)
None => ()
}
}
output
}
///|
/// Return a nullable series' occupied index range.
pub fn occupied_range(values : Array[MaybeMeasurement]) -> (Int, Int)? {
let mut first : Int? = None
let mut last : Int? = None
for index, value in values {
if value is Some(_) {
if first is None {
first = Some(index)
}
last = Some(index)
}
}
match (first, last) {
(Some(left), Some(right)) => Some((left, right))
_ => None
}
}
///|
/// Calculate the fraction of missing sensor readings.
pub fn missing_rate(values : Array[MaybeMeasurement]) -> Double {
if values.length() == 0 {
0.0
} else {
count_missing(values).to_double() / values.length().to_double()
}
}
///|
/// A compact quality score combining coverage and specification compliance.
pub fn data_quality_score(
values : Array[MaybeMeasurement],
lower : Double,
upper : Double,
) -> Double {
let available = drop_missing(values)
if values.length() == 0 || lower > upper {
0.0
} else {
let coverage = available.length().to_double() / values.length().to_double()
let in_spec = count_mask(within_mask(available, lower, upper)).to_double() /
values.length().to_double()
0.5 * coverage + 0.5 * in_spec
}
}