use axonml_autograd::Variable;
use super::{
apollo::Apollo,
aquilo::Aquilo,
boreas::Boreas,
colossus::Colossus,
data::{HvacSensorData, PipelineOutput},
gaia::Gaia,
naiad::Naiad,
vulcan::Vulcan,
zephyrus::Zephyrus,
};
pub struct HvacPipeline {
pub aquilo: Aquilo,
pub boreas: Boreas,
pub naiad: Naiad,
pub vulcan: Vulcan,
pub zephyrus: Zephyrus,
pub colossus: Colossus,
pub gaia: Gaia,
pub apollo: Apollo,
}
impl Default for HvacPipeline {
fn default() -> Self {
Self::new()
}
}
impl HvacPipeline {
pub fn new() -> Self {
Self {
aquilo: Aquilo::new(),
boreas: Boreas::new(),
naiad: Naiad::new(),
vulcan: Vulcan::new(),
zephyrus: Zephyrus::new(),
colossus: Colossus::new(),
gaia: Gaia::new(),
apollo: Apollo::new(),
}
}
pub fn diagnose(&self, sensor_data: &HvacSensorData) -> PipelineOutput {
let batch = sensor_data.electrical.shape()[0];
let elec_flat = flatten_sensor(&sensor_data.electrical, batch, 64, 7, 168);
let (aquilo_fault, _, _, _, aquilo_emb) = self.aquilo.forward_all(&elec_flat);
let (boreas_fault, _, _, _, boreas_emb) =
self.boreas.forward_all(&sensor_data.refrigeration);
let water_t = transpose_last_two(&sensor_data.water, batch, 64, 7);
let (naiad_fault, _, _, _, naiad_emb) = self.naiad.forward_all(&water_t);
let mech_flat = flatten_sensor(&sensor_data.mechanical, batch, 96, 7, 672);
let (vulcan_fault, _, _, _, vulcan_emb) = self.vulcan.forward_all(&mech_flat);
let air_t = transpose_last_two(&sensor_data.airflow, batch, 72, 7);
let (zephyrus_fault, _, _, _, zephyrus_emb) = self.zephyrus.forward_all(&air_t);
let specialist_features = super::aquilo::concat_variables(
&[
&aquilo_emb,
&boreas_emb,
&naiad_emb,
&vulcan_emb,
&zephyrus_emb,
],
batch,
);
let (_, _, _, _, colossus_emb) = self.colossus.forward_specialists(
&aquilo_emb,
&boreas_emb,
&naiad_emb,
&vulcan_emb,
&zephyrus_emb,
);
let (_, safety_score, _, _, gaia_emb) =
self.gaia.forward_parts(&specialist_features, &colossus_emb);
let raw_sensor_summary = summarize_sensors(sensor_data, batch);
let model_embs = [
&aquilo_emb,
&boreas_emb,
&naiad_emb,
&vulcan_emb,
&zephyrus_emb,
&colossus_emb,
&gaia_emb,
];
let model_refs: Vec<&Variable> = model_embs.to_vec();
let (diagnosis, _, _, _, _) = self.apollo.forward_parts(&model_refs, &raw_sensor_summary);
PipelineOutput {
specialist_features,
aggregator_output: colossus_emb,
safety_output: safety_score,
diagnosis,
specialist_faults: vec![
aquilo_fault,
boreas_fault,
naiad_fault,
vulcan_fault,
zephyrus_fault,
],
}
}
pub fn total_parameters(&self) -> usize {
use axonml_nn::Module;
self.aquilo.num_parameters()
+ self.boreas.num_parameters()
+ self.naiad.num_parameters()
+ self.vulcan.num_parameters()
+ self.zephyrus.num_parameters()
+ self.colossus.num_parameters()
+ self.gaia.num_parameters()
+ self.apollo.num_parameters()
}
pub fn eval(&mut self) {
use axonml_nn::Module;
self.aquilo.set_training(false);
self.boreas.set_training(false);
self.naiad.set_training(false);
self.vulcan.set_training(false);
self.zephyrus.set_training(false);
self.colossus.set_training(false);
self.gaia.set_training(false);
self.apollo.set_training(false);
}
pub fn train(&mut self) {
use axonml_nn::Module;
self.aquilo.set_training(true);
self.boreas.set_training(true);
self.naiad.set_training(true);
self.vulcan.set_training(true);
self.zephyrus.set_training(true);
self.colossus.set_training(true);
self.gaia.set_training(true);
self.apollo.set_training(true);
}
}
fn flatten_sensor(
input: &Variable,
batch: usize,
time: usize,
channels: usize,
target_dim: usize,
) -> Variable {
let full_dim = time * channels;
let dim = target_dim.min(full_dim);
let flat = input.reshape(&[batch, full_dim]);
if dim < full_dim {
flat.narrow(1, 0, dim)
} else {
flat
}
}
fn transpose_last_two(input: &Variable, _batch: usize, _time: usize, _channels: usize) -> Variable {
input.transpose(1, 2)
}
fn summarize_sensors(data: &HvacSensorData, _batch: usize) -> Variable {
let elec_mean = data.electrical.mean_dim(1, false); let refrig_mean = data.refrigeration.mean_dim(1, false); let water_mean = data.water.mean_dim(1, false); let mech_mean = data.mechanical.mean_dim(1, false); let air_mean = data.airflow.mean_dim(1, false);
Variable::cat(
&[&elec_mean, &refrig_mean, &water_mean, &mech_mean, &air_mean],
1,
)
}
#[cfg(test)]
mod tests {
use super::super::data::SyntheticHvacGenerator;
use super::*;
use axonml_nn::Module;
use axonml_tensor::Tensor;
#[test]
fn test_pipeline_creation() {
let pipeline = HvacPipeline::new();
let total = pipeline.total_parameters();
assert!(
total > 3_000_000 && total < 15_000_000,
"Total pipeline has {} params",
total
);
}
#[test]
fn test_pipeline_end_to_end() {
let pipeline = HvacPipeline::new();
let generator = SyntheticHvacGenerator::new(42);
let (sensor_data, _labels) = generator.generate_normal(2);
let output = pipeline.diagnose(&sensor_data);
assert_eq!(output.diagnosis.shape(), vec![2, 12]);
assert_eq!(output.specialist_faults.len(), 5);
assert_eq!(output.specialist_faults[0].shape(), vec![2, 13]); assert_eq!(output.specialist_faults[1].shape(), vec![2, 16]); assert_eq!(output.safety_output.shape(), vec![2, 1]);
}
#[test]
fn test_pipeline_eval_mode() {
let mut pipeline = HvacPipeline::new();
pipeline.eval();
assert!(!pipeline.aquilo.is_training());
assert!(!pipeline.boreas.is_training());
pipeline.train();
assert!(pipeline.aquilo.is_training());
}
#[test]
fn test_transpose_last_two() {
let input = Variable::new(
Tensor::from_vec(
vec![
1.0, 2.0, 3.0, 4.0, 5.0, 6.0, ],
&[1, 2, 3],
)
.unwrap(),
false,
);
let output = transpose_last_two(&input, 1, 2, 3);
let data = output.data().to_vec();
assert_eq!(output.shape(), vec![1, 3, 2]);
assert_eq!(data, vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0]);
}
#[test]
fn test_summarize_sensors() {
let generator = SyntheticHvacGenerator::new(42);
let (sensor_data, _) = generator.generate_normal(2);
let summary = summarize_sensors(&sensor_data, 2);
assert_eq!(summary.shape(), vec![2, 35]);
let data = summary.data().to_vec();
for val in &data {
assert!(val.is_finite(), "Sensor summary contains non-finite value");
}
}
}