diff --git a/Cargo.lock b/Cargo.lock index f65e0b0a..f24f4c32 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -164,6 +164,10 @@ dependencies = [ "autocfg", ] +[[package]] +name = "nfpa_13" +version = "0.1.0" + [[package]] name = "once_cell" version = "1.21.3" @@ -182,6 +186,7 @@ dependencies = [ "hex-literal", "introduction_to_fire_dynamics", "lazy_static", + "nfpa_13", "pd_7974", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index c43b6936..02a7916c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ pd_7974 = { path = "./crates/pd_7974" } sfpe_handbook = { path = "./crates/sfpe_handbook" } tr17 = { path = "./crates/tr17" } eurocode_1_1_2 = { path = "./crates/eurocode_1_1_2" } +nfpa_13 = { path = "./crates/nfpa_13" } [workspace] members = ["crates/*"] diff --git a/crates/nfpa_13/Cargo.toml b/crates/nfpa_13/Cargo.toml new file mode 100644 index 00000000..57595cb3 --- /dev/null +++ b/crates/nfpa_13/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "nfpa_13" +version = "0.1.0" +edition = "2024" + +[lints] +workspace = true diff --git a/crates/nfpa_13/src/chapter_10.rs b/crates/nfpa_13/src/chapter_10.rs new file mode 100644 index 00000000..1fa5f173 --- /dev/null +++ b/crates/nfpa_13/src/chapter_10.rs @@ -0,0 +1 @@ +pub mod coverage_area; diff --git a/crates/nfpa_13/src/chapter_10/coverage_area.rs b/crates/nfpa_13/src/chapter_10/coverage_area.rs new file mode 100644 index 00000000..2aa2adde --- /dev/null +++ b/crates/nfpa_13/src/chapter_10/coverage_area.rs @@ -0,0 +1,93 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HazardClass { + LightHazard, + OrdinaryHazardGroup1, + OrdinaryHazardGroup2, + ExtraHazardGroup1, + ExtraHazardGroup2, +} + +pub fn coverage_area(spacing: f64, branchline_distance: f64) -> f64 { + spacing * branchline_distance +} + +#[cfg(not(coverage))] +pub fn coverage_area_equation(a: String, spacing: String, branchline_distance: String) -> String { + format!("{} = {} \\cdot {}", a, spacing, branchline_distance) +} + +pub fn max_coverage_area(hazard: HazardClass) -> f64 { + match hazard { + HazardClass::LightHazard => 20.9, + HazardClass::OrdinaryHazardGroup1 => 12.1, + HazardClass::OrdinaryHazardGroup2 => 12.1, + HazardClass::ExtraHazardGroup1 => 9.3, + HazardClass::ExtraHazardGroup2 => 9.3, + } +} + +pub fn is_within_limit(spacing: f64, branchline_distance: f64, hazard: HazardClass) -> bool { + coverage_area(spacing, branchline_distance) <= max_coverage_area(hazard) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_coverage_area_basic() { + assert!((coverage_area(4.0, 3.0) - 12.0).abs() < 1e-9); + } + + #[test] + fn test_coverage_area_zero() { + assert_eq!(coverage_area(0.0, 4.0), 0.0); + } + + #[test] + fn test_max_coverage_light() { + assert_eq!(max_coverage_area(HazardClass::LightHazard), 20.9); + } + + #[test] + fn test_max_coverage_ordinary_groups_equal() { + assert_eq!( + max_coverage_area(HazardClass::OrdinaryHazardGroup1), + max_coverage_area(HazardClass::OrdinaryHazardGroup2) + ); + } + + #[test] + fn test_max_coverage_extra_groups_equal() { + assert_eq!( + max_coverage_area(HazardClass::ExtraHazardGroup1), + max_coverage_area(HazardClass::ExtraHazardGroup2) + ); + } + + #[test] + fn test_max_coverage_decreases_with_hazard() { + let light = max_coverage_area(HazardClass::LightHazard); + let ordinary = max_coverage_area(HazardClass::OrdinaryHazardGroup1); + let extra = max_coverage_area(HazardClass::ExtraHazardGroup1); + assert!(light > ordinary); + assert!(ordinary > extra); + } + + #[test] + fn test_within_limit_passes() { + assert!(is_within_limit(4.0, 3.0, HazardClass::LightHazard)); + } + + #[test] + fn test_within_limit_fails() { + assert!(!is_within_limit(4.0, 4.0, HazardClass::ExtraHazardGroup1)); + } + + #[test] + fn test_within_limit_at_boundary() { + let area = max_coverage_area(HazardClass::OrdinaryHazardGroup1); + let s = area.sqrt(); + assert!(is_within_limit(s, s, HazardClass::OrdinaryHazardGroup1)); + } +} diff --git a/crates/nfpa_13/src/chapter_27.rs b/crates/nfpa_13/src/chapter_27.rs new file mode 100644 index 00000000..b4eef292 --- /dev/null +++ b/crates/nfpa_13/src/chapter_27.rs @@ -0,0 +1 @@ +pub mod k_factor; diff --git a/crates/nfpa_13/src/chapter_27/k_factor.rs b/crates/nfpa_13/src/chapter_27/k_factor.rs new file mode 100644 index 00000000..6657b852 --- /dev/null +++ b/crates/nfpa_13/src/chapter_27/k_factor.rs @@ -0,0 +1,69 @@ +pub fn flow_rate(k: f64, pressure: f64) -> f64 { + k * pressure.sqrt() +} + +pub fn pressure(k: f64, flow_rate: f64) -> f64 { + (flow_rate / k).powi(2) +} + +pub fn k_factor(flow_rate: f64, pressure: f64) -> f64 { + flow_rate / pressure.sqrt() +} + +#[cfg(not(coverage))] +pub fn flow_rate_equation(q: String, k: String, p: String) -> String { + format!("{} = {} \\cdot \\sqrt{{{}}}", q, k, p) +} + +#[cfg(not(coverage))] +pub fn pressure_equation(p: String, q: String, k: String) -> String { + format!("{} = \\left(\\dfrac{{{}}}{{{}}}\\right)^{{2}}", p, q, k) +} + +#[cfg(not(coverage))] +pub fn k_factor_equation(k: String, q: String, p: String) -> String { + format!("{} = \\dfrac{{{}}}{{\\sqrt{{{}}}}}", k, q, p) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_flow_rate_basic() { + let k = 80.0; + let p = 1.0; + assert!((flow_rate(k, p) - 80.0).abs() < 1e-9); + } + + #[test] + fn test_flow_rate_quadruple_pressure_doubles_flow() { + let k = 80.0; + let q1 = flow_rate(k, 1.0); + let q4 = flow_rate(k, 4.0); + assert!((q4 - 2.0 * q1).abs() < 1e-9); + } + + #[test] + fn test_pressure_inverts_flow_rate() { + let k = 115.0; + let p_in = 2.5; + let q = flow_rate(k, p_in); + let p_out = pressure(k, q); + assert!((p_out - p_in).abs() < 1e-9); + } + + #[test] + fn test_k_factor_inverts_flow_rate() { + let k_in = 100.0; + let p = 3.0; + let q = flow_rate(k_in, p); + let k_out = k_factor(q, p); + assert!((k_out - k_in).abs() < 1e-9); + } + + #[test] + fn test_zero_pressure_gives_zero_flow() { + assert_eq!(flow_rate(80.0, 0.0), 0.0); + } +} diff --git a/crates/nfpa_13/src/lib.rs b/crates/nfpa_13/src/lib.rs new file mode 100644 index 00000000..e9cf46e0 --- /dev/null +++ b/crates/nfpa_13/src/lib.rs @@ -0,0 +1,2 @@ +pub mod chapter_10; +pub mod chapter_27; diff --git a/crates/sfpe_handbook/src/chapter_14.rs b/crates/sfpe_handbook/src/chapter_14.rs index 0c29e396..81eb3d90 100644 --- a/crates/sfpe_handbook/src/chapter_14.rs +++ b/crates/sfpe_handbook/src/chapter_14.rs @@ -1 +1,3 @@ pub mod alpert; +pub mod droplet; +pub mod rti; diff --git a/crates/sfpe_handbook/src/chapter_14/alpert.rs b/crates/sfpe_handbook/src/chapter_14/alpert.rs index 551beb37..d3230cdd 100644 --- a/crates/sfpe_handbook/src/chapter_14/alpert.rs +++ b/crates/sfpe_handbook/src/chapter_14/alpert.rs @@ -1 +1,3 @@ pub mod heat_release; +pub mod temperature; +pub mod velocity; diff --git a/crates/sfpe_handbook/src/chapter_14/alpert/temperature.rs b/crates/sfpe_handbook/src/chapter_14/alpert/temperature.rs new file mode 100644 index 00000000..e74f19fb --- /dev/null +++ b/crates/sfpe_handbook/src/chapter_14/alpert/temperature.rs @@ -0,0 +1,90 @@ +pub fn temperature_rise(q: f64, height: f64, radial_position: f64) -> f64 { + if radial_position / height <= 0.18 { + 16.9 * q.powf(2.0 / 3.0) / height.powf(5.0 / 3.0) + } else { + 5.38 * (q / radial_position).powf(2.0 / 3.0) / height + } +} + +#[cfg(not(coverage))] +pub fn temperature_rise_equation( + delta_t: String, + q: String, + height: String, + radial_position: String, +) -> String { + format!( + "{} = \\begin{{cases}} 16.9 \\cdot \\dfrac{{{}^{{2/3}}}}{{{}^{{5/3}}}} & \\text{{if }} \\dfrac{{{}}}{{{}}} \\leq 0.18 \\\\[6pt] 5.38 \\cdot \\dfrac{{({} / {})^{{2/3}}}}{{{}}} & \\text{{if }} \\dfrac{{{}}}{{{}}} > 0.18 \\end{{cases}}", + delta_t, + q, height, + radial_position, height, + q, radial_position, height, + radial_position, height + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::chapter_14::alpert::heat_release::from_temperature_and_position; + + #[test] + fn test_zero_radial_position() { + let q = 1000.0; + let height = 10.0; + let radial_position = 0.0; + let result = temperature_rise(q, height, radial_position); + let expected = 16.9 * q.powf(2.0 / 3.0) / height.powf(5.0 / 3.0); + assert!((result - expected).abs() < 1e-6); + } + + #[test] + fn test_radial_position_at_threshold() { + let q = 1000.0; + let height = 10.0; + let radial_position = 0.18 * height; + let result = temperature_rise(q, height, radial_position); + let expected = 16.9 * q.powf(2.0 / 3.0) / height.powf(5.0 / 3.0); + assert!((result - expected).abs() < 1e-6); + } + + #[test] + fn test_radial_position_greater_than_threshold() { + let q = 1000.0; + let height = 10.0; + let radial_position = 5.0; + let result = temperature_rise(q, height, radial_position); + let expected = 5.38 * (q / radial_position).powf(2.0 / 3.0) / height; + assert!((result - expected).abs() < 1e-6); + } + + #[test] + fn test_inverse_consistency_near_field() { + let q = 1500.0; + let height = 8.0; + let radial_position = 0.5; + let temp_amb = 293.0; + let delta_t = temperature_rise(q, height, radial_position); + let recovered_q = + from_temperature_and_position(temp_amb + delta_t, temp_amb, height, radial_position); + assert!((recovered_q - q).abs() < 1e-3); + } + + #[test] + fn test_inverse_consistency_far_field() { + let q = 2500.0; + let height = 6.0; + let radial_position = 4.0; + let temp_amb = 293.0; + let delta_t = temperature_rise(q, height, radial_position); + let recovered_q = + from_temperature_and_position(temp_amb + delta_t, temp_amb, height, radial_position); + assert!((recovered_q - q).abs() < 1e-3); + } + + #[test] + fn test_zero_heat_release() { + let result = temperature_rise(0.0, 10.0, 1.0); + assert_eq!(result, 0.0); + } +} diff --git a/crates/sfpe_handbook/src/chapter_14/alpert/velocity.rs b/crates/sfpe_handbook/src/chapter_14/alpert/velocity.rs new file mode 100644 index 00000000..07b0c838 --- /dev/null +++ b/crates/sfpe_handbook/src/chapter_14/alpert/velocity.rs @@ -0,0 +1,84 @@ +pub fn velocity(q: f64, height: f64, radial_position: f64) -> f64 { + if radial_position / height <= 0.15 { + 0.96 * (q / height).powf(1.0 / 3.0) + } else { + 0.195 * q.powf(1.0 / 3.0) * height.powf(0.5) / radial_position.powf(5.0 / 6.0) + } +} + +#[cfg(not(coverage))] +pub fn velocity_equation( + u: String, + q: String, + height: String, + radial_position: String, +) -> String { + format!( + "{} = \\begin{{cases}} 0.96 \\cdot \\left(\\dfrac{{{}}}{{{}}}\\right)^{{1/3}} & \\text{{if }} \\dfrac{{{}}}{{{}}} \\leq 0.15 \\\\[6pt] 0.195 \\cdot \\dfrac{{{}^{{1/3}} \\cdot {}^{{1/2}}}}{{{}^{{5/6}}}} & \\text{{if }} \\dfrac{{{}}}{{{}}} > 0.15 \\end{{cases}}", + u, + q, height, + radial_position, height, + q, height, radial_position, + radial_position, height + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_zero_radial_position() { + let q = 1000.0; + let height = 10.0; + let radial_position = 0.0; + let result = velocity(q, height, radial_position); + let expected = 0.96 * (q / height).powf(1.0 / 3.0); + assert!((result - expected).abs() < 1e-6); + } + + #[test] + fn test_radial_position_at_threshold() { + let q = 1000.0; + let height = 10.0; + let radial_position = 0.15 * height; + let result = velocity(q, height, radial_position); + let expected = 0.96 * (q / height).powf(1.0 / 3.0); + assert!((result - expected).abs() < 1e-6); + } + + #[test] + fn test_radial_position_greater_than_threshold() { + let q = 1000.0; + let height = 10.0; + let radial_position = 5.0; + let result = velocity(q, height, radial_position); + let expected = + 0.195 * q.powf(1.0 / 3.0) * height.powf(0.5) / radial_position.powf(5.0 / 6.0); + assert!((result - expected).abs() < 1e-6); + } + + #[test] + fn test_velocity_decreases_with_radius_in_far_field() { + let q = 2000.0; + let height = 8.0; + let v_near = velocity(q, height, 2.0); + let v_far = velocity(q, height, 6.0); + assert!(v_far < v_near); + } + + #[test] + fn test_zero_heat_release() { + let result = velocity(0.0, 10.0, 1.0); + assert_eq!(result, 0.0); + } + + #[test] + fn test_velocity_increases_with_heat_release() { + let height = 10.0; + let radial_position = 2.0; + let v_low = velocity(500.0, height, radial_position); + let v_high = velocity(2000.0, height, radial_position); + assert!(v_high > v_low); + } +} diff --git a/crates/sfpe_handbook/src/chapter_14/droplet.rs b/crates/sfpe_handbook/src/chapter_14/droplet.rs new file mode 100644 index 00000000..a0081326 --- /dev/null +++ b/crates/sfpe_handbook/src/chapter_14/droplet.rs @@ -0,0 +1,2 @@ +pub mod evaporative_cooling; +pub mod terminal_velocity; diff --git a/crates/sfpe_handbook/src/chapter_14/droplet/evaporative_cooling.rs b/crates/sfpe_handbook/src/chapter_14/droplet/evaporative_cooling.rs new file mode 100644 index 00000000..1a2ae7f0 --- /dev/null +++ b/crates/sfpe_handbook/src/chapter_14/droplet/evaporative_cooling.rs @@ -0,0 +1,62 @@ +pub fn cooling_power(m_dot: f64, c_p_w: f64, t_boil: f64, t_w: f64, h_fg: f64) -> f64 { + m_dot * (c_p_w * (t_boil - t_w) + h_fg) +} + +#[cfg(not(coverage))] +pub fn cooling_power_equation( + q: String, + m_dot: String, + c_p_w: String, + t_boil: String, + t_w: String, + h_fg: String, +) -> String { + format!( + "{} = {} \\cdot \\left({} \\cdot ({} - {}) + {}\\right)", + q, m_dot, c_p_w, t_boil, t_w, h_fg + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_typical_values() { + let m_dot = 0.001; + let c_p_w = 4186.0; + let t_boil = 100.0; + let t_w = 20.0; + let h_fg = 2_257_000.0; + let result = cooling_power(m_dot, c_p_w, t_boil, t_w, h_fg); + let expected = m_dot * (c_p_w * (t_boil - t_w) + h_fg); + assert!((result - expected).abs() < 1e-6); + } + + #[test] + fn test_cooling_scales_linearly_with_mass_flow() { + let c_p_w = 4186.0; + let t_boil = 100.0; + let t_w = 20.0; + let h_fg = 2_257_000.0; + let q_low = cooling_power(0.001, c_p_w, t_boil, t_w, h_fg); + let q_high = cooling_power(0.002, c_p_w, t_boil, t_w, h_fg); + assert!((q_high - 2.0 * q_low).abs() < 1e-6); + } + + #[test] + fn test_water_at_boiling_only_uses_latent() { + let m_dot = 0.001; + let c_p_w = 4186.0; + let t_boil = 100.0; + let h_fg = 2_257_000.0; + let result = cooling_power(m_dot, c_p_w, t_boil, t_boil, h_fg); + assert!((result - m_dot * h_fg).abs() < 1e-6); + } + + #[test] + fn test_zero_mass_flow() { + let result = cooling_power(0.0, 4186.0, 100.0, 20.0, 2_257_000.0); + assert_eq!(result, 0.0); + } +} diff --git a/crates/sfpe_handbook/src/chapter_14/droplet/terminal_velocity.rs b/crates/sfpe_handbook/src/chapter_14/droplet/terminal_velocity.rs new file mode 100644 index 00000000..69539944 --- /dev/null +++ b/crates/sfpe_handbook/src/chapter_14/droplet/terminal_velocity.rs @@ -0,0 +1,64 @@ +const G: f64 = 9.81; + +pub fn terminal_velocity(diameter: f64, drag_coefficient: f64, rho_w: f64, rho_a: f64) -> f64 { + let numerator = 4.0 * G * diameter * (rho_w - rho_a); + let denominator = 3.0 * drag_coefficient * rho_a; + (numerator / denominator).sqrt() +} + +#[cfg(not(coverage))] +pub fn terminal_velocity_equation( + u_t: String, + diameter: String, + drag_coefficient: String, + rho_w: String, + rho_a: String, +) -> String { + format!( + "{} = \\sqrt{{\\dfrac{{4 \\cdot g \\cdot {} \\cdot ({} - {})}}{{3 \\cdot {} \\cdot {}}}}}", + u_t, diameter, rho_w, rho_a, drag_coefficient, rho_a + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_typical_water_droplet() { + let diameter = 0.001; + let drag_coefficient = 0.5; + let rho_w = 1000.0; + let rho_a = 1.2; + let result = terminal_velocity(diameter, drag_coefficient, rho_w, rho_a); + let expected = (4.0 * G * diameter * (rho_w - rho_a) / (3.0 * drag_coefficient * rho_a)) + .sqrt(); + assert!((result - expected).abs() < 1e-6); + } + + #[test] + fn test_velocity_increases_with_diameter() { + let drag_coefficient = 0.5; + let rho_w = 1000.0; + let rho_a = 1.2; + let v_small = terminal_velocity(0.0005, drag_coefficient, rho_w, rho_a); + let v_large = terminal_velocity(0.002, drag_coefficient, rho_w, rho_a); + assert!(v_large > v_small); + } + + #[test] + fn test_velocity_decreases_with_drag() { + let diameter = 0.001; + let rho_w = 1000.0; + let rho_a = 1.2; + let v_low_drag = terminal_velocity(diameter, 0.4, rho_w, rho_a); + let v_high_drag = terminal_velocity(diameter, 1.5, rho_w, rho_a); + assert!(v_high_drag < v_low_drag); + } + + #[test] + fn test_zero_density_difference() { + let result = terminal_velocity(0.001, 0.5, 1.2, 1.2); + assert_eq!(result, 0.0); + } +} diff --git a/crates/sfpe_handbook/src/chapter_14/rti.rs b/crates/sfpe_handbook/src/chapter_14/rti.rs new file mode 100644 index 00000000..12e0a0f2 --- /dev/null +++ b/crates/sfpe_handbook/src/chapter_14/rti.rs @@ -0,0 +1,2 @@ +pub mod activation_delay; +pub mod response_time; diff --git a/crates/sfpe_handbook/src/chapter_14/rti/activation_delay.rs b/crates/sfpe_handbook/src/chapter_14/rti/activation_delay.rs new file mode 100644 index 00000000..dc22ea81 --- /dev/null +++ b/crates/sfpe_handbook/src/chapter_14/rti/activation_delay.rs @@ -0,0 +1,110 @@ +use crate::chapter_14::alpert::temperature::temperature_rise; +use crate::chapter_14::alpert::velocity::velocity; +use crate::chapter_14::rti::response_time::activation_time; + +pub fn transport_time(q: f64, height: f64, radial_position: f64) -> f64 { + let u = velocity(q, height, radial_position); + if u == 0.0 { + return f64::INFINITY; + } + radial_position / u +} + +pub fn total_activation_delay( + rti: f64, + q: f64, + height: f64, + radial_position: f64, + t_a: f64, + t_act: f64, +) -> f64 { + let u = velocity(q, height, radial_position); + let delta_t = temperature_rise(q, height, radial_position); + let t_g = t_a + delta_t; + let t_thermal = activation_time(rti, u, t_g, t_a, t_act); + let t_transport = if u == 0.0 { + f64::INFINITY + } else { + radial_position / u + }; + t_transport + t_thermal +} + +#[cfg(not(coverage))] +pub fn transport_time_equation(t: String, radial_position: String, u: String) -> String { + format!("{} = \\dfrac{{{}}}{{{}}}", t, radial_position, u) +} + +#[cfg(not(coverage))] +pub fn total_activation_delay_equation( + t_total: String, + t_transport: String, + t_thermal: String, +) -> String { + format!("{} = {} + {}", t_total, t_transport, t_thermal) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_transport_time_basic() { + let q = 1000.0; + let height = 4.0; + let radial_position = 3.0; + let u = velocity(q, height, radial_position); + let result = transport_time(q, height, radial_position); + assert!((result - radial_position / u).abs() < 1e-6); + } + + #[test] + fn test_transport_time_zero_radius() { + let result = transport_time(1000.0, 4.0, 0.0); + assert_eq!(result, 0.0); + } + + #[test] + fn test_total_delay_matches_components() { + let rti = 100.0; + let q = 2000.0; + let height = 5.0; + let radial_position = 3.0; + let t_a = 20.0; + let t_act = 68.0; + + let u = velocity(q, height, radial_position); + let delta_t = temperature_rise(q, height, radial_position); + let t_g = t_a + delta_t; + let t_thermal = activation_time(rti, u, t_g, t_a, t_act); + let t_transport = radial_position / u; + let expected = t_transport + t_thermal; + + let result = total_activation_delay(rti, q, height, radial_position, t_a, t_act); + assert!((result - expected).abs() < 1e-6); + } + + #[test] + fn test_total_delay_no_activation_when_jet_too_cool() { + let rti = 100.0; + let q = 50.0; + let height = 10.0; + let radial_position = 8.0; + let t_a = 20.0; + let t_act = 200.0; + let result = total_activation_delay(rti, q, height, radial_position, t_a, t_act); + assert!(result.is_infinite() && result.is_sign_positive()); + } + + #[test] + fn test_total_delay_increases_with_radius() { + let rti = 100.0; + let q = 5000.0; + let height = 6.0; + let t_a = 20.0; + let t_act = 68.0; + let near = total_activation_delay(rti, q, height, 1.0, t_a, t_act); + let far = total_activation_delay(rti, q, height, 4.0, t_a, t_act); + assert!(far > near); + } +} diff --git a/crates/sfpe_handbook/src/chapter_14/rti/response_time.rs b/crates/sfpe_handbook/src/chapter_14/rti/response_time.rs new file mode 100644 index 00000000..19defe64 --- /dev/null +++ b/crates/sfpe_handbook/src/chapter_14/rti/response_time.rs @@ -0,0 +1,72 @@ +pub fn activation_time(rti: f64, u: f64, t_g: f64, t_a: f64, t_act: f64) -> f64 { + if t_g <= t_act { + return f64::INFINITY; + } + (rti / u.sqrt()) * ((t_g - t_a) / (t_g - t_act)).ln() +} + +#[cfg(not(coverage))] +pub fn activation_time_equation( + t: String, + rti: String, + u: String, + t_g: String, + t_a: String, + t_act: String, +) -> String { + format!( + "{} = \\dfrac{{{}}}{{\\sqrt{{{}}}}} \\cdot \\ln\\left(\\dfrac{{{} - {}}}{{{} - {}}}\\right)", + t, rti, u, t_g, t_a, t_g, t_act + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_basic_activation() { + let rti = 100.0; + let u = 1.0; + let t_g = 200.0; + let t_a = 20.0; + let t_act = 68.0; + let result = activation_time(rti, u, t_g, t_a, t_act); + let expected = (rti / u.sqrt()) * ((t_g - t_a) / (t_g - t_act)).ln(); + assert!((result - expected).abs() < 1e-6); + } + + #[test] + fn test_no_activation_when_gas_below_setpoint() { + let result = activation_time(100.0, 1.0, 60.0, 20.0, 68.0); + assert!(result.is_infinite() && result.is_sign_positive()); + } + + #[test] + fn test_no_activation_when_gas_equal_setpoint() { + let result = activation_time(100.0, 1.0, 68.0, 20.0, 68.0); + assert!(result.is_infinite() && result.is_sign_positive()); + } + + #[test] + fn test_higher_rti_gives_longer_response() { + let u = 2.0; + let t_g = 200.0; + let t_a = 20.0; + let t_act = 68.0; + let t_fast = activation_time(50.0, u, t_g, t_a, t_act); + let t_slow = activation_time(250.0, u, t_g, t_a, t_act); + assert!(t_slow > t_fast); + } + + #[test] + fn test_higher_velocity_gives_shorter_response() { + let rti = 100.0; + let t_g = 200.0; + let t_a = 20.0; + let t_act = 68.0; + let t_low_u = activation_time(rti, 0.5, t_g, t_a, t_act); + let t_high_u = activation_time(rti, 4.0, t_g, t_a, t_act); + assert!(t_high_u < t_low_u); + } +}