use std::env;
use std::fs;
use std::path::Path;
const INPUT: &str = "data/zhl16c.csv";
const EXPECTED_COMPARTMENTS: usize = 16;
const EXPECTED_FIELDS: usize = 6;
struct TissueRow {
line_number: usize,
literals: Vec<String>,
values: [f64; EXPECTED_FIELDS],
}
fn main() {
println!("cargo:rerun-if-changed={INPUT}");
let input = fs::read_to_string(INPUT).expect("failed to read ZHL-16 tissue constants");
let rows = parse_rows(&input);
if rows.len() != EXPECTED_COMPARTMENTS {
panic!(
"{INPUT} must contain exactly {EXPECTED_COMPARTMENTS} data rows, found {}",
rows.len()
);
}
let mut output =
String::from("pub const TISSUE_CONSTANTS: [crate::body_model::TissueModel; 16] = [\n");
validate_rows(&rows);
for row in rows {
output.push_str(" crate::body_model::TissueModel::new(\n");
output.push_str(" crate::body_model::TissueModelPerGas::new(\n");
output.push_str(&format!(
" crate::quantities::Time::new_unchecked({}, crate::quantities::TimeUnit::Minutes),\n",
row.literals[0]
));
output.push_str(&format!(
" crate::quantities::Pressure::new_unchecked({}, crate::quantities::PressureUnit::Bar),\n",
row.literals[1]
));
output.push_str(&format!(" {},\n", row.literals[2]));
output.push_str(" ),\n");
output.push_str(" crate::body_model::TissueModelPerGas::new(\n");
output.push_str(&format!(
" crate::quantities::Time::new_unchecked({}, crate::quantities::TimeUnit::Minutes),\n",
row.literals[3]
));
output.push_str(&format!(
" crate::quantities::Pressure::new_unchecked({}, crate::quantities::PressureUnit::Bar),\n",
row.literals[4]
));
output.push_str(&format!(" {},\n", row.literals[5]));
output.push_str(" ),\n");
output.push_str(" ),\n");
}
output.push_str("];\n");
let out_dir = env::var_os("OUT_DIR").expect("OUT_DIR is not set");
fs::write(Path::new(&out_dir).join("tissue_constants.rs"), output)
.expect("failed to write generated tissue constants");
}
fn parse_rows(input: &str) -> Vec<TissueRow> {
input
.lines()
.enumerate()
.filter_map(|(line_idx, line)| {
let line = line.split_once('#').map_or(line, |(data, _)| data).trim();
if line.is_empty() {
return None;
}
let literals = line
.split(',')
.map(str::trim)
.collect::<Vec<_>>();
if literals.len() != EXPECTED_FIELDS {
panic!(
"{INPUT}: line {} must contain {EXPECTED_FIELDS} comma-separated fields, found {}",
line_idx + 1,
literals.len()
);
}
let mut values = [0.0; EXPECTED_FIELDS];
let literals = literals
.into_iter()
.enumerate()
.map(|(field_idx, field)| {
let (literal, value) = parse_float_literal(field, line_idx + 1);
values[field_idx] = value;
literal
})
.collect::<Vec<_>>();
Some(TissueRow {
line_number: line_idx + 1,
literals,
values,
})
})
.collect()
}
fn parse_float_literal(field: &str, line_number: usize) -> (String, f64) {
let value = field.parse::<f64>().unwrap_or_else(|_| {
panic!("{INPUT}: line {line_number} contains invalid float literal {field:?}")
});
if !value.is_finite() {
panic!("{INPUT}: line {line_number} contains non-finite value {field:?}");
}
let literal = if field.contains(['.', 'e', 'E']) {
field.to_owned()
} else {
format!("{field}.0")
};
(literal, value)
}
fn validate_rows(rows: &[TissueRow]) {
let mut previous_n2_half_time = None;
let mut previous_he_half_time = None;
for row in rows {
validate_positive(row, 0, "n2_half_time_min");
validate_positive(row, 1, "n2_a_bar");
validate_b(row, 2, "n2_b");
validate_positive(row, 3, "he_half_time_min");
validate_positive(row, 4, "he_a_bar");
validate_b(row, 5, "he_b");
validate_monotonic_half_time(row, 0, "n2_half_time_min", &mut previous_n2_half_time);
validate_monotonic_half_time(row, 3, "he_half_time_min", &mut previous_he_half_time);
}
}
fn validate_positive(row: &TissueRow, field_idx: usize, field_name: &str) {
if row.values[field_idx] <= 0.0 {
panic!(
"{INPUT}: line {} {field_name} must be > 0.0, found {}",
row.line_number, row.literals[field_idx]
);
}
}
fn validate_b(row: &TissueRow, field_idx: usize, field_name: &str) {
validate_positive(row, field_idx, field_name);
if row.values[field_idx] > 1.0 {
panic!(
"{INPUT}: line {} {field_name} must be <= 1.0, found {}",
row.line_number, row.literals[field_idx]
);
}
}
fn validate_monotonic_half_time(
row: &TissueRow,
field_idx: usize,
field_name: &str,
previous: &mut Option<(f64, usize)>,
) {
if let Some((previous_value, previous_line)) = previous {
if row.values[field_idx] <= *previous_value {
panic!(
"{INPUT}: line {} {field_name} must grow monotonically through the compartments; found {} after {} on line {}",
row.line_number, row.literals[field_idx], previous_value, previous_line
);
}
}
*previous = Some((row.values[field_idx], row.line_number));
}