///|
/// Production lot identity used to join measurements from several stations.
pub(all) struct LotIdentity {
lot : String
product : String
revision : String
line : String
shift : String
start_window : String
end_window : String
} derive(Debug, ToJson)
///|
/// One observation event with source and routing metadata.
pub(all) struct MeasurementEvent {
identity : LotIdentity
station : String
feature : String
value : Double?
sequence : Int
accepted : Bool
} derive(Debug, ToJson)
///|
/// A grouped measurement stream for one lot and feature.
pub(all) struct LotFeature {
identity : LotIdentity
feature : String
values : Array[Double]
mut missing : Int
stations : Array[String]
} derive(Debug, ToJson)
///|
/// Create a stable lot identity.
pub fn LotIdentity::new(
lot~ : String,
product~ : String,
revision~ : String,
line~ : String,
shift~ : String,
start_window~ : String,
end_window~ : String,
) -> LotIdentity {
{ lot, product, revision, line, shift, start_window, end_window }
}
///|
/// Construct an event from an optional measurement.
pub fn MeasurementEvent::new(
identity~ : LotIdentity,
station~ : String,
feature~ : String,
value~ : Double?,
sequence~ : Int,
) -> MeasurementEvent {
{ identity, station, feature, value, sequence, accepted: true }
}
///|
/// Mark an event as rejected without discarding its traceability.
pub fn MeasurementEvent::reject(self : MeasurementEvent) -> MeasurementEvent {
{ ..self, accepted: false }
}
///|
/// Group events by feature and preserve source station order.
pub fn group_events(events : Array[MeasurementEvent]) -> Array[LotFeature] {
let result : Array[LotFeature] = []
for event in events {
let mut target : Int? = None
for index, group in result {
if group.feature == event.feature &&
group.identity.lot == event.identity.lot {
target = Some(index)
}
}
match target {
Some(index) => {
match event.value {
Some(value) => result[index].values.push(value)
None => result[index].missing += 1
}
let mut station_seen = false
for station in result[index].stations {
if station == event.station {
station_seen = true
}
}
if !station_seen {
result[index].stations.push(event.station)
}
}
None => {
let values : Array[Double] = []
let missing = match event.value {
Some(value) => {
values.push(value)
0
}
None => 1
}
result.push({
identity: event.identity,
feature: event.feature,
values,
missing,
stations: [event.station],
})
}
}
}
result
}
///|
/// Aggregate one lot feature into a quality summary.
pub(all) struct LotFeatureSummary {
lot : String
feature : String
count : Int
missing : Int
mean : Double?
deviation : Double?
minimum : Double?
maximum : Double?
defect_rate : Double?
station_count : Int
} derive(Debug, ToJson)
///|
/// Summarize a grouped lot feature against an optional specification.
pub fn summarize_lot_feature(
feature : LotFeature,
specification : Specification?,
) -> LotFeatureSummary {
let mean = if feature.values.length() == 0 {
None
} else {
Some(mean_of(feature.values))
}
let deviation = if feature.values.length() < 2 {
None
} else {
Some(std_of(feature.values, mean_of(feature.values)))
}
let minimum = if feature.values.length() == 0 {
None
} else {
let mut value = feature.values[0]
for item in feature.values {
value = value.min(item)
}
Some(value)
}
let maximum = if feature.values.length() == 0 {
None
} else {
let mut value = feature.values[0]
for item in feature.values {
value = value.max(item)
}
Some(value)
}
let defect_rate = match specification {
Some(spec) => defect_rate(feature.values, spec)
None => None
}
{
lot: feature.identity.lot,
feature: feature.feature,
count: feature.values.length(),
missing: feature.missing,
mean,
deviation,
minimum,
maximum,
defect_rate,
station_count: feature.stations.length(),
}
}
///|
/// Summarize every lot feature in a deterministic order.
pub fn summarize_lot_features(
features : Array[LotFeature],
specification : Specification?,
) -> Array[LotFeatureSummary] {
let result : Array[LotFeatureSummary] = []
for feature in features {
result.push(summarize_lot_feature(feature, specification))
}
result
}
///|
/// A station-to-station comparison result.
pub(all) struct StationComparison {
station : String
count : Int
mean : Double?
deviation : Double?
delta_from_lot : Double?
alarm : Bool
} derive(Debug, ToJson)
///|
/// Compare each station's values with a lot center.
pub fn compare_stations(
events : Array[MeasurementEvent],
feature : String,
threshold~ : Double,
) -> Array[StationComparison] {
let values : Array[Double] = []
let station_names : Array[String] = []
let station_values : Array[Array[Double]] = []
for event in events {
if event.feature == feature && event.accepted {
match event.value {
Some(value) => {
values.push(value)
let mut index : Int? = None
for current, station in station_names {
if station == event.station {
index = Some(current)
}
}
match index {
Some(current) => station_values[current].push(value)
None => {
station_names.push(event.station)
station_values.push([value])
}
}
}
None => ()
}
}
}
let center = if values.length() == 0 { 0.0 } else { mean_of(values) }
let output : Array[StationComparison] = []
for index, station in station_names {
let data = station_values[index]
let station_mean = mean_of(data)
output.push({
station,
count: data.length(),
mean: Some(station_mean),
deviation: if data.length() < 2 {
None
} else {
Some(std_of(data, station_mean))
},
delta_from_lot: Some(station_mean - center),
alarm: (station_mean - center).abs() > threshold,
})
}
output
}
///|
/// A lot-level release decision with reasons for review.
pub(all) struct ReleaseDecision {
released : Bool
reason_codes : Array[String]
measured_features : Int
rejected_events : Int
missing_measurements : Int
} derive(Debug, ToJson)
///|
/// Decide whether a lot can be released under minimum coverage rules.
pub fn release_decision(
events : Array[MeasurementEvent],
minimum_features~ : Int,
minimum_measurements~ : Int,
) -> ReleaseDecision {
let features : Array[String] = []
let mut rejected = 0
let mut missing = 0
let mut accepted_measurements = 0
for event in events {
if !event.accepted {
rejected += 1
}
let mut seen = false
for feature in features {
if feature == event.feature {
seen = true
}
}
if !seen {
features.push(event.feature)
}
match event.value {
Some(_) => if event.accepted { accepted_measurements += 1 }
None => missing += 1
}
}
let reasons : Array[String] = []
if features.length() < minimum_features {
reasons.push("INSUFFICIENT_FEATURES")
}
if accepted_measurements < minimum_measurements {
reasons.push("INSUFFICIENT_MEASUREMENTS")
}
if rejected > 0 {
reasons.push("REJECTED_EVENTS_PRESENT")
}
{
released: reasons.length() == 0,
reason_codes: reasons,
measured_features: features.length(),
rejected_events: rejected,
missing_measurements: missing,
}
}
///|
/// Sort events by sequence number for deterministic replay.
pub fn sort_events(events : Array[MeasurementEvent]) -> Array[MeasurementEvent] {
let output = events.copy()
for index in 1.. 0 && output[cursor - 1].sequence > event.sequence {
output[cursor] = output[cursor - 1]
cursor -= 1
}
output[cursor] = event
}
output
}
///|
/// Return events from one station.
pub fn station_events(
events : Array[MeasurementEvent],
station : String,
) -> Array[MeasurementEvent] {
let result : Array[MeasurementEvent] = []
for event in events {
if event.station == station {
result.push(event)
}
}
result
}
///|
/// Return events for one feature.
pub fn feature_events(
events : Array[MeasurementEvent],
feature : String,
) -> Array[MeasurementEvent] {
let result : Array[MeasurementEvent] = []
for event in events {
if event.feature == feature {
result.push(event)
}
}
result
}
///|
/// Count accepted events.
pub fn accepted_event_count(events : Array[MeasurementEvent]) -> Int {
let mut count = 0
for event in events {
if event.accepted {
count += 1
}
}
count
}
///|
/// Count rejected events.
pub fn rejected_event_count(events : Array[MeasurementEvent]) -> Int {
let mut count = 0
for event in events {
if !event.accepted {
count += 1
}
}
count
}
///|
/// Count non-missing events.
pub fn measured_event_count(events : Array[MeasurementEvent]) -> Int {
let mut count = 0
for event in events {
if event.value is Some(_) {
count += 1
}
}
count
}
///|
/// Count events missing a value.
pub fn missing_event_count(events : Array[MeasurementEvent]) -> Int {
let mut count = 0
for event in events {
if event.value is None {
count += 1
}
}
count
}
///|
/// Return unique station identifiers.
pub fn unique_stations(events : Array[MeasurementEvent]) -> Array[String] {
let result : Array[String] = []
for event in events {
let mut seen = false
for station in result {
if station == event.station {
seen = true
}
}
if !seen {
result.push(event.station)
}
}
result
}
///|
/// Return unique feature identifiers.
pub fn unique_features(events : Array[MeasurementEvent]) -> Array[String] {
let result : Array[String] = []
for event in events {
let mut seen = false
for feature in result {
if feature == event.feature {
seen = true
}
}
if !seen {
result.push(event.feature)
}
}
result
}
///|
/// Coverage rate for accepted numeric observations.
pub fn measurement_coverage(events : Array[MeasurementEvent]) -> Double {
if events.length() == 0 {
0.0
} else {
measured_event_count(events).to_double() / events.length().to_double()
}
}
///|
/// Acceptance rate for routed events.
pub fn event_acceptance_rate(events : Array[MeasurementEvent]) -> Double {
if events.length() == 0 {
0.0
} else {
accepted_event_count(events).to_double() / events.length().to_double()
}
}
///|
/// Filter events belonging to one lot.
pub fn lot_events(
events : Array[MeasurementEvent],
lot : String,
) -> Array[MeasurementEvent] {
let result : Array[MeasurementEvent] = []
for event in events {
if event.identity.lot == lot {
result.push(event)
}
}
result
}
///|
/// Build a feature's values from event records.
pub fn values_from_events(events : Array[MeasurementEvent]) -> Array[Double] {
let result : Array[Double] = []
for event in sort_events(events) {
match event.value {
Some(value) => result.push(value)
None => ()
}
}
result
}
///|
/// Run a quality report for every lot feature.
pub fn lot_feature_reports(
events : Array[MeasurementEvent],
specification : Specification?,
) -> Array[LotFeatureSummary] {
summarize_lot_features(group_events(events), specification)
}
///|
/// Merge events from multiple lots into a single chronological stream.
pub fn merge_event_streams(
first : Array[MeasurementEvent],
second : Array[MeasurementEvent],
) -> Array[MeasurementEvent] {
sort_events(concatenate_events(first, second))
}
///|
fn concatenate_events(
first : Array[MeasurementEvent],
second : Array[MeasurementEvent],
) -> Array[MeasurementEvent] {
let result = first.copy()
for event in second {
result.push(event)
}
result
}
///|
/// Validate event sequences for duplicate or negative sequence numbers.
pub fn validate_event_sequences(
events : Array[MeasurementEvent],
) -> Array[ValidationIssue] {
let issues : Array[ValidationIssue] = []
for index, event in events {
if event.sequence < 0 {
issues.push({
index,
field: "sequence",
message: "sequence must be non-negative",
})
}
for previous_index in 0..