use std::collections::HashMap;
use rhai::{Array, Dynamic, Map};
use crate::stability::CompleteStabilityResult;
#[derive(Debug, Clone)]
pub struct CriteriaContext {
result: CompleteStabilityResult,
vessel_name: String,
loading_condition: String,
params: HashMap<String, Dynamic>,
}
impl CriteriaContext {
pub fn new(
result: CompleteStabilityResult,
vessel_name: String,
loading_condition: String,
) -> Self {
Self {
result,
vessel_name,
loading_condition,
params: HashMap::new(),
}
}
pub fn set_param(&mut self, key: &str, value: Dynamic) {
self.params.insert(key.to_string(), value);
}
pub fn set_param_f64(&mut self, key: &str, value: f64) {
self.params.insert(key.to_string(), Dynamic::from(value));
}
pub fn set_param_str(&mut self, key: &str, value: &str) {
self.params
.insert(key.to_string(), Dynamic::from(value.to_string()));
}
pub fn set_param_bool(&mut self, key: &str, value: bool) {
self.params.insert(key.to_string(), Dynamic::from(value));
}
pub fn get_heels(&self) -> Array {
self.result
.gz_curve
.heels()
.into_iter()
.map(Dynamic::from)
.collect()
}
pub fn get_gz_values(&self) -> Array {
self.result
.gz_curve
.values()
.into_iter()
.map(Dynamic::from)
.collect()
}
pub fn area_under_curve(&self, from_angle: f64, to_angle: f64) -> f64 {
let heels = self.result.gz_curve.heels();
if heels.is_empty() {
return 0.0;
}
let mut area = 0.0;
for i in 0..heels.len() - 1 {
let h1 = heels[i];
let h2 = heels[i + 1];
if h2 <= from_angle || h1 >= to_angle {
continue;
}
let x1 = h1.max(from_angle);
let x2 = h2.min(to_angle);
let y1 = self.gz_at_angle(x1);
let y2 = self.gz_at_angle(x2);
let dx = (x2 - x1).to_radians();
area += 0.5 * (y1 + y2) * dx;
}
area
}
pub fn gz_at_angle(&self, angle: f64) -> f64 {
self.result.gz_curve.interpolate(angle).unwrap_or(0.0)
}
pub fn find_max_gz(&self) -> Map {
let mut map = Map::new();
if let Some(point) = self.result.gz_curve.max_value() {
map.insert("angle".into(), Dynamic::from(point.heel));
map.insert("value".into(), Dynamic::from(point.value));
} else {
map.insert("angle".into(), Dynamic::UNIT);
map.insert("value".into(), Dynamic::UNIT);
}
map
}
pub fn find_angle_of_vanishing_stability(&self) -> Dynamic {
let heels = self.result.gz_curve.heels();
let values = self.result.gz_curve.values();
if heels.is_empty() {
return Dynamic::UNIT;
}
let max_idx = values
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
.map(|(i, _)| i)
.unwrap_or(0);
for i in max_idx..values.len() - 1 {
if values[i] > 0.0 && values[i + 1] <= 0.0 {
let t = values[i] / (values[i] - values[i + 1]);
let angle = heels[i] + t * (heels[i + 1] - heels[i]);
return Dynamic::from(angle);
}
}
Dynamic::UNIT
}
pub fn get_first_flooding_angle(&self) -> Dynamic {
for point in &self.result.gz_curve.points {
if point.is_flooding {
return Dynamic::from(point.heel);
}
}
Dynamic::UNIT
}
pub fn get_deck_edge_immersion_angle(&self) -> Dynamic {
let points = &self.result.gz_curve.points;
for i in 0..points.len().saturating_sub(1) {
let p1 = &points[i];
let p2 = &points[i + 1];
if let (Some(f1), Some(f2)) = (p1.freeboard, p2.freeboard) {
if f1 >= 0.0 && f2 <= 0.0 {
if (f1 - f2).abs() < f64::EPSILON {
return Dynamic::from(p1.heel);
}
let fraction = f1 / (f1 - f2);
let angle = p1.heel + fraction * (p2.heel - p1.heel);
return Dynamic::from(angle);
}
}
}
Dynamic::UNIT
}
pub fn find_equilibrium_angle(&self, heeling_arm: f64) -> Dynamic {
let heels = self.result.gz_curve.heels();
let values = self.result.gz_curve.values();
if heels.is_empty() {
return Dynamic::UNIT;
}
for i in 0..values.len() - 1 {
let v1 = values[i];
let v2 = values[i + 1];
if (v1 <= heeling_arm && v2 >= heeling_arm) || (v1 >= heeling_arm && v2 <= heeling_arm)
{
let t = (heeling_arm - v1) / (v2 - v1);
let angle = heels[i] + t * (heels[i + 1] - heels[i]);
if angle >= 0.0 {
return Dynamic::from(angle);
}
}
}
Dynamic::UNIT
}
pub fn find_second_intercept(&self, heeling_arm: f64) -> Dynamic {
let heels = self.result.gz_curve.heels();
let values = self.result.gz_curve.values();
if heels.is_empty() {
return Dynamic::UNIT;
}
let mut intercept_count = 0;
for i in 0..values.len() - 1 {
let v1 = values[i];
let v2 = values[i + 1];
if (v1 <= heeling_arm && v2 >= heeling_arm) || (v1 >= heeling_arm && v2 <= heeling_arm)
{
let t = (heeling_arm - v1) / (v2 - v1);
let angle = heels[i] + t * (heels[i + 1] - heels[i]);
if angle >= 0.0 {
intercept_count += 1;
if intercept_count == 2 {
return Dynamic::from(angle);
}
}
}
}
Dynamic::UNIT
}
pub fn get_limiting_angle(&self, default: f64) -> f64 {
let mut limit = default;
if let Some(flooding) = self.get_first_flooding_angle().try_cast::<f64>() {
limit = limit.min(flooding);
}
if let Some(vanishing) = self.find_angle_of_vanishing_stability().try_cast::<f64>() {
limit = limit.min(vanishing);
}
limit
}
pub fn get_gm0(&self) -> Dynamic {
match self.result.gm0() {
Some(v) => Dynamic::from(v),
None => Dynamic::UNIT,
}
}
pub fn get_gm0_dry(&self) -> Dynamic {
match self.result.gm0_dry() {
Some(v) => Dynamic::from(v),
None => Dynamic::UNIT,
}
}
pub fn get_draft(&self) -> f64 {
self.result.hydrostatics.draft
}
pub fn get_trim(&self) -> f64 {
self.result.hydrostatics.trim
}
pub fn get_displacement(&self) -> f64 {
self.result.displacement
}
pub fn get_cog(&self) -> Array {
self.result.cog.iter().map(|&v| Dynamic::from(v)).collect()
}
pub fn get_cb(&self) -> f64 {
self.result.hydrostatics.cb
}
pub fn get_cm(&self) -> f64 {
self.result.hydrostatics.cm
}
pub fn get_cp(&self) -> f64 {
self.result.hydrostatics.cp
}
pub fn get_lwl(&self) -> f64 {
self.result.hydrostatics.lwl
}
pub fn get_bwl(&self) -> f64 {
self.result.hydrostatics.bwl
}
pub fn get_vcb(&self) -> f64 {
self.result.hydrostatics.vcb()
}
pub fn has_wind_data(&self) -> bool {
self.result.has_wind_data()
}
pub fn get_emerged_area(&self) -> Dynamic {
match &self.result.wind_data {
Some(wind) => Dynamic::from(wind.emerged_area),
None => Dynamic::UNIT,
}
}
pub fn get_wind_lever_arm(&self) -> Dynamic {
match &self.result.wind_data {
Some(wind) => Dynamic::from(wind.wind_lever_arm),
None => Dynamic::UNIT,
}
}
pub fn calculate_wind_heeling_lever(&self, wind_pressure: f64) -> Dynamic {
match &self.result.wind_data {
Some(wind) => {
let g = 9.81;
let lw1 = (wind_pressure * wind.emerged_area * wind.wind_lever_arm)
/ (self.result.displacement * g);
Dynamic::from(lw1)
}
None => Dynamic::UNIT,
}
}
pub fn get_param(&self, key: &str) -> Dynamic {
self.params.get(key).cloned().unwrap_or(Dynamic::UNIT)
}
pub fn has_param(&self, key: &str) -> bool {
self.params.contains_key(key)
}
pub fn get_vessel_name(&self) -> String {
self.vessel_name.clone()
}
pub fn get_loading_condition(&self) -> String {
self.loading_condition.clone()
}
pub fn result(&self) -> &CompleteStabilityResult {
&self.result
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hydrostatics::HydrostaticState;
use crate::stability::{CompleteStabilityResult, StabilityCurve, StabilityPoint};
fn create_mock_context(
heels: Vec<f64>,
gz_values: Vec<f64>,
flood_idx: Option<usize>,
) -> CriteriaContext {
let points = heels
.into_iter()
.zip(gz_values.into_iter())
.enumerate()
.map(|(i, (h, gz))| StabilityPoint {
heel: h,
draft: 5.0,
trim: 0.0,
value: gz,
is_flooding: flood_idx.map_or(false, |idx| i >= idx),
flooded_openings: vec![],
cog: None,
vessel_cog: None,
freeboard: None,
})
.collect();
let curve = StabilityCurve {
curve_type: "GZ".to_string(),
displacement: 1000.0,
cog: None,
points,
};
let result = CompleteStabilityResult {
hydrostatics: HydrostaticState::default(),
gz_curve: curve,
wind_data: None,
displacement: 1000.0,
cog: [0.0, 0.0, 0.0],
};
CriteriaContext::new(result, "Test".into(), "Load".into())
}
#[test]
fn test_area_under_curve_known_values() {
let ctx = create_mock_context(vec![0.0, 10.0, 20.0, 30.0], vec![0.0, 0.1, 0.2, 0.3], None);
let area = ctx.area_under_curve(0.0, 30.0);
let expected = 0.5 * 30.0f64.to_radians() * 0.3;
assert!(
(area - expected).abs() < 1e-4,
"Expected {}, got {}",
expected,
area
);
let area2 = ctx.area_under_curve(10.0, 20.0);
let expected2 = 0.5 * 10.0f64.to_radians() * 0.3;
assert!(
(area2 - expected2).abs() < 1e-4,
"Expected {}, got {}",
expected2,
area2
);
}
#[test]
fn test_gz_at_angle_interpolation() {
let ctx = create_mock_context(vec![0.0, 10.0, 20.0], vec![0.0, 0.1, 0.4], None);
assert_eq!(ctx.gz_at_angle(10.0), 0.1);
assert_eq!(ctx.gz_at_angle(15.0), 0.25);
assert_eq!(ctx.gz_at_angle(5.0), 0.05);
}
#[test]
fn test_find_max_gz_known_curve() {
let ctx = create_mock_context(
vec![0.0, 10.0, 20.0, 30.0, 40.0],
vec![0.0, 0.2, 0.5, 0.4, 0.1],
None,
);
let map = ctx.find_max_gz();
assert_eq!(map.get("angle").unwrap().as_float().unwrap(), 20.0);
assert_eq!(map.get("value").unwrap().as_float().unwrap(), 0.5);
}
#[test]
fn test_limiting_angle_with_flooding() {
let ctx = create_mock_context(
vec![0.0, 10.0, 20.0, 30.0, 40.0],
vec![0.0, 0.2, 0.5, 0.4, 0.1],
Some(3),
);
let limit_40 = ctx.get_limiting_angle(40.0);
let limit_20 = ctx.get_limiting_angle(20.0);
assert_eq!(limit_40, 30.0); assert_eq!(limit_20, 20.0); }
#[test]
fn test_vanishing_stability_angle() {
let ctx = create_mock_context(
vec![0.0, 10.0, 20.0, 30.0, 40.0, 50.0],
vec![0.0, 0.2, 0.4, 0.1, -0.1, -0.2],
None,
);
let vanish = ctx.find_angle_of_vanishing_stability();
let val = vanish.as_float().unwrap();
assert_eq!(val, 35.0);
}
#[test]
fn test_deck_edge_immersion_angle() {
let mut ctx =
create_mock_context(vec![0.0, 10.0, 20.0, 30.0], vec![0.0, 0.1, 0.2, 0.3], None);
ctx.result.gz_curve.points[0].freeboard = Some(2.0);
ctx.result.gz_curve.points[1].freeboard = Some(1.0);
ctx.result.gz_curve.points[2].freeboard = Some(-1.0);
ctx.result.gz_curve.points[3].freeboard = Some(-3.0);
let angle = ctx.get_deck_edge_immersion_angle();
let val = angle.as_float().unwrap();
assert_eq!(val, 15.0);
}
}