Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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/*"]
Expand Down
7 changes: 7 additions & 0 deletions crates/nfpa_13/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[package]
name = "nfpa_13"
version = "0.1.0"
edition = "2024"

[lints]
workspace = true
1 change: 1 addition & 0 deletions crates/nfpa_13/src/chapter_10.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod coverage_area;
93 changes: 93 additions & 0 deletions crates/nfpa_13/src/chapter_10/coverage_area.rs
Original file line number Diff line number Diff line change
@@ -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));
}
}
1 change: 1 addition & 0 deletions crates/nfpa_13/src/chapter_27.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod k_factor;
69 changes: 69 additions & 0 deletions crates/nfpa_13/src/chapter_27/k_factor.rs
Original file line number Diff line number Diff line change
@@ -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);
}
}
2 changes: 2 additions & 0 deletions crates/nfpa_13/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod chapter_10;
pub mod chapter_27;
2 changes: 2 additions & 0 deletions crates/sfpe_handbook/src/chapter_14.rs
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
pub mod alpert;
pub mod droplet;
pub mod rti;
2 changes: 2 additions & 0 deletions crates/sfpe_handbook/src/chapter_14/alpert.rs
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
pub mod heat_release;
pub mod temperature;
pub mod velocity;
90 changes: 90 additions & 0 deletions crates/sfpe_handbook/src/chapter_14/alpert/temperature.rs
Original file line number Diff line number Diff line change
@@ -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);
}
}
84 changes: 84 additions & 0 deletions crates/sfpe_handbook/src/chapter_14/alpert/velocity.rs
Original file line number Diff line number Diff line change
@@ -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);
}
}
2 changes: 2 additions & 0 deletions crates/sfpe_handbook/src/chapter_14/droplet.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod evaporative_cooling;
pub mod terminal_velocity;
Loading
Loading