use pyo3::exceptions::{PyIOError, PyValueError};
use pyo3::prelude::*;
use crate::appendage::{Appendage as RustAppendage, AppendageGeometry};
use crate::deckedge::{DeckEdge as RustDeckEdge, DeckEdgeSide as RustDeckEdgeSide};
use crate::hull::Hull as RustHull;
use crate::hydrostatics::{
HydrostaticState as RustHydroState, HydrostaticsCalculator as RustHydroCalc,
};
use crate::scripting::{
CriteriaContext as RustCriteriaContext, CriteriaResult as RustCriteriaResult,
ScriptEngine as RustScriptEngine,
};
use crate::stability::{
CompleteStabilityResult as RustCompleteStabilityResult, StabilityCalculator as RustStabCalc,
StabilityCurve as RustStabCurve, WindHeelingData as RustWindHeelingData,
};
use crate::tanks::Tank as RustTank;
use crate::vessel::Vessel as RustVessel;
use std::path::Path;
#[pyclass(name = "Hull")]
pub struct PyHull {
inner: RustHull,
}
#[pymethods]
impl PyHull {
#[new]
fn new(file_path: &str) -> PyResult<Self> {
let path = Path::new(file_path);
let hull = RustHull::from_stl(path)
.map_err(|e| PyIOError::new_err(format!("Failed to load STL: {}", e)))?;
Ok(Self { inner: hull })
}
#[staticmethod]
fn from_box(length: f64, breadth: f64, depth: f64) -> Self {
let hull = RustHull::from_box(length, breadth, depth);
Self { inner: hull }
}
fn get_bounds(&self) -> (f64, f64, f64, f64, f64, f64) {
self.inner.get_bounds()
}
fn num_triangles(&self) -> usize {
self.inner.num_triangles()
}
fn num_vertices(&self) -> usize {
self.inner.num_vertices()
}
fn transform(
&mut self,
translation: (f64, f64, f64),
rotation: (f64, f64, f64),
pivot: (f64, f64, f64),
) {
self.inner.transform(translation, rotation, pivot);
}
fn scale(&mut self, factor: f64) {
self.inner.scale(factor);
}
fn scale_xyz(&mut self, sx: f64, sy: f64, sz: f64) {
self.inner.scale_xyz(sx, sy, sz);
}
fn simplify(&mut self, target_count: usize) {
self.inner.simplify(target_count);
}
fn to_simplified(&self, target_count: usize) -> Self {
Self {
inner: self.inner.to_simplified(target_count),
}
}
fn export_stl(&self, file_path: &str) -> PyResult<()> {
let path = Path::new(file_path);
self.inner
.export_stl(path)
.map_err(|e| PyIOError::new_err(format!("Failed to export STL: {}", e)))
}
fn get_vertices(&self) -> Vec<(f64, f64, f64)> {
self.inner
.mesh()
.vertices()
.iter()
.map(|v| (v.x, v.y, v.z))
.collect()
}
fn get_faces(&self) -> Vec<(u32, u32, u32)> {
self.inner
.mesh()
.indices()
.iter()
.map(|idx| (idx[0], idx[1], idx[2]))
.collect()
}
fn __repr__(&self) -> String {
let bounds = self.inner.get_bounds();
format!(
"Hull(triangles={}, vertices={}, bounds=({:.2}, {:.2}, {:.2}, {:.2}, {:.2}, {:.2}))",
self.inner.num_triangles(),
self.inner.num_vertices(),
bounds.0,
bounds.1,
bounds.2,
bounds.3,
bounds.4,
bounds.5
)
}
}
#[pyclass(name = "Vessel")]
pub struct PyVessel {
inner: RustVessel,
}
#[pymethods]
impl PyVessel {
#[new]
fn new(hull: &PyHull) -> Self {
Self {
inner: RustVessel::new(hull.inner.clone()),
}
}
fn get_bounds(&self) -> (f64, f64, f64, f64, f64, f64) {
self.inner.get_bounds()
}
#[getter]
fn ap(&self) -> f64 {
self.inner.ap()
}
#[setter]
fn set_ap(&mut self, ap: f64) {
self.inner.set_ap(ap);
}
#[getter]
fn fp(&self) -> f64 {
self.inner.fp()
}
#[setter]
fn set_fp(&mut self, fp: f64) {
self.inner.set_fp(fp);
}
#[getter]
fn lbp(&self) -> f64 {
self.inner.lbp()
}
fn num_hulls(&self) -> usize {
self.inner.hulls().len()
}
fn num_tanks(&self) -> usize {
self.inner.tanks().len()
}
fn add_tank(&mut self, tank: &PyTank) {
self.inner.add_tank(tank.inner.clone());
}
fn get_total_tanks_mass(&self) -> f64 {
self.inner.get_total_tanks_mass()
}
fn get_tanks_center_of_gravity(&self) -> [f64; 3] {
self.inner.get_tanks_center_of_gravity()
}
fn add_silhouette(&mut self, silhouette: &PySilhouette) {
self.inner.add_silhouette(silhouette.inner.clone());
}
fn num_silhouettes(&self) -> usize {
self.inner.num_silhouettes()
}
fn has_silhouettes(&self) -> bool {
self.inner.has_silhouettes()
}
fn clear_silhouettes(&mut self) {
self.inner.clear_silhouettes();
}
fn get_total_emerged_area(&self, waterline_z: f64) -> f64 {
self.inner.get_total_emerged_area(waterline_z)
}
fn get_combined_emerged_centroid(&self, waterline_z: f64) -> [f64; 2] {
self.inner.get_combined_emerged_centroid(waterline_z)
}
fn add_opening(&mut self, opening: &PyDownfloodingOpening) {
self.inner.add_downflooding_opening(opening.inner.clone());
}
fn num_openings(&self) -> usize {
self.inner.num_downflooding_openings()
}
fn clear_openings(&mut self) {
self.inner.clear_downflooding_openings();
}
fn get_hulls(&self) -> Vec<PyHull> {
self.inner
.hulls()
.iter()
.map(|h| PyHull { inner: h.clone() })
.collect()
}
fn get_tanks(&self) -> Vec<PyTank> {
self.inner
.tanks()
.iter()
.map(|t| PyTank { inner: t.clone() })
.collect()
}
fn get_silhouettes(&self) -> Vec<PySilhouette> {
self.inner
.silhouettes()
.iter()
.map(|s| PySilhouette { inner: s.clone() })
.collect()
}
fn get_openings(&self) -> Vec<PyDownfloodingOpening> {
self.inner
.downflooding_openings()
.iter()
.map(|o| PyDownfloodingOpening { inner: o.clone() })
.collect()
}
fn add_appendage(&mut self, appendage: &PyAppendage) {
self.inner.add_appendage(appendage.inner.clone());
}
fn num_appendages(&self) -> usize {
self.inner.num_appendages()
}
fn clear_appendages(&mut self) {
self.inner.clear_appendages();
}
fn get_appendages(&self) -> Vec<PyAppendage> {
self.inner
.appendages()
.iter()
.map(|a| PyAppendage { inner: a.clone() })
.collect()
}
fn get_total_appendage_volume(&self) -> f64 {
self.inner.get_total_appendage_volume()
}
fn get_total_appendage_wetted_surface(&self) -> f64 {
self.inner.get_total_appendage_wetted_surface()
}
fn add_deck_edge(&mut self, deck_edge: &PyDeckEdge) {
self.inner.add_deck_edge(deck_edge.inner.clone());
}
fn num_deck_edges(&self) -> usize {
self.inner.num_deck_edges()
}
fn has_deck_edges(&self) -> bool {
self.inner.has_deck_edges()
}
fn clear_deck_edges(&mut self) {
self.inner.clear_deck_edges();
}
fn get_deck_edges(&self) -> Vec<PyDeckEdge> {
self.inner
.deck_edges()
.iter()
.map(|d| PyDeckEdge { inner: d.clone() })
.collect()
}
fn get_min_freeboard(&self, heel: f64, trim: f64, waterline_z: f64) -> Option<f64> {
self.inner.get_min_freeboard(heel, trim, waterline_z)
}
fn __repr__(&self) -> String {
format!(
"Vessel(hulls={}, tanks={}, silhouettes={}, appendages={}, deck_edges={}, lbp={:.2}m)",
self.inner.hulls().len(),
self.inner.tanks().len(),
self.inner.num_silhouettes(),
self.inner.num_appendages(),
self.inner.num_deck_edges(),
self.inner.lbp()
)
}
}
use crate::silhouette::Silhouette as RustSilhouette;
#[pyclass(name = "Silhouette")]
pub struct PySilhouette {
inner: RustSilhouette,
}
#[pymethods]
impl PySilhouette {
#[new]
fn new(file_path: &str) -> PyResult<Self> {
let path = Path::new(file_path);
let silhouette = RustSilhouette::from_file(path)
.map_err(|e| PyIOError::new_err(format!("Failed to load silhouette: {}", e)))?;
Ok(Self { inner: silhouette })
}
#[staticmethod]
fn from_points(points: Vec<(f64, f64)>, name: &str) -> Self {
let pts: Vec<[f64; 3]> = points.iter().map(|(x, z)| [*x, 0.0, *z]).collect();
Self {
inner: RustSilhouette::new(pts, name.to_string()),
}
}
#[getter]
fn name(&self) -> &str {
self.inner.name()
}
fn num_points(&self) -> usize {
self.inner.num_points()
}
fn is_closed(&self) -> bool {
self.inner.is_closed()
}
fn get_points(&self) -> Vec<(f64, f64, f64)> {
self.inner
.points()
.iter()
.map(|p| (p[0], p[1], p[2]))
.collect()
}
fn get_area(&self) -> f64 {
self.inner.get_area()
}
fn get_centroid(&self) -> [f64; 2] {
self.inner.get_centroid()
}
fn get_bounds(&self) -> (f64, f64, f64, f64) {
self.inner.get_bounds()
}
fn get_emerged_area(&self, waterline_z: f64) -> f64 {
self.inner.get_emerged_area(waterline_z)
}
fn get_emerged_centroid(&self, waterline_z: f64) -> [f64; 2] {
self.inner.get_emerged_centroid(waterline_z)
}
fn __repr__(&self) -> String {
format!(
"Silhouette(name='{}', points={}, area={:.2}m²)",
self.inner.name(),
self.inner.num_points(),
self.inner.get_area()
)
}
}
#[pyclass(name = "Appendage")]
pub struct PyAppendage {
pub(crate) inner: RustAppendage,
}
#[pymethods]
impl PyAppendage {
#[staticmethod]
fn from_point(name: &str, center: (f64, f64, f64), volume: f64) -> Self {
Self {
inner: RustAppendage::from_point(name, [center.0, center.1, center.2], volume),
}
}
#[staticmethod]
fn from_file(name: &str, file_path: &str) -> PyResult<Self> {
let path = Path::new(file_path);
let appendage = RustAppendage::from_file(name, path)
.map_err(|e| PyIOError::new_err(format!("Failed to load appendage: {}", e)))?;
Ok(Self { inner: appendage })
}
#[staticmethod]
fn from_box(
name: &str,
xmin: f64,
xmax: f64,
ymin: f64,
ymax: f64,
zmin: f64,
zmax: f64,
) -> Self {
Self {
inner: RustAppendage::from_box(name, (xmin, xmax, ymin, ymax, zmin, zmax)),
}
}
#[staticmethod]
fn from_cube(name: &str, center: (f64, f64, f64), volume: f64) -> Self {
Self {
inner: RustAppendage::from_cube(name, [center.0, center.1, center.2], volume),
}
}
#[staticmethod]
fn from_sphere(name: &str, center: (f64, f64, f64), volume: f64) -> Self {
Self {
inner: RustAppendage::from_sphere(name, [center.0, center.1, center.2], volume),
}
}
#[getter]
fn name(&self) -> &str {
self.inner.name()
}
#[setter]
fn set_name(&mut self, name: &str) {
self.inner.set_name(name);
}
#[getter]
fn volume(&self) -> f64 {
self.inner.volume()
}
#[getter]
fn center(&self) -> (f64, f64, f64) {
let c = self.inner.center();
(c[0], c[1], c[2])
}
#[getter]
fn wetted_surface(&self) -> Option<f64> {
self.inner.wetted_surface()
}
#[setter]
fn set_wetted_surface(&mut self, surface: Option<f64>) {
self.inner.set_wetted_surface(surface);
}
fn geometry_type(&self) -> &str {
match self.inner.geometry() {
AppendageGeometry::Point { .. } => "Point",
AppendageGeometry::Mesh(_) => "Mesh",
AppendageGeometry::Box { .. } => "Box",
AppendageGeometry::Sphere { .. } => "Sphere",
AppendageGeometry::Cube { .. } => "Cube",
}
}
#[allow(clippy::type_complexity)]
fn get_mesh_data(&self) -> Option<(Vec<(f64, f64, f64)>, Vec<(usize, usize, usize)>)> {
if let AppendageGeometry::Mesh(mesh) = self.inner.geometry() {
let vertices: Vec<(f64, f64, f64)> =
mesh.vertices().iter().map(|p| (p.x, p.y, p.z)).collect();
let faces: Vec<(usize, usize, usize)> = mesh
.indices()
.iter()
.map(|tri| (tri[0] as usize, tri[1] as usize, tri[2] as usize))
.collect();
Some((vertices, faces))
} else {
None
}
}
#[getter]
fn bounds(&self) -> Option<(f64, f64, f64, f64, f64, f64)> {
match self.inner.geometry() {
AppendageGeometry::Box { bounds } => Some(*bounds),
AppendageGeometry::Cube { center, volume } => {
let s = volume.cbrt();
Some((
center[0] - s / 2.0,
center[0] + s / 2.0,
center[1] - s / 2.0,
center[1] + s / 2.0,
center[2] - s / 2.0,
center[2] + s / 2.0,
))
}
AppendageGeometry::Sphere { center, volume } => {
let r = (volume * 3.0 / (4.0 * std::f64::consts::PI)).cbrt();
Some((
center[0] - r,
center[0] + r,
center[1] - r,
center[1] + r,
center[2] - r,
center[2] + r,
))
}
AppendageGeometry::Mesh(mesh) => {
let aabb = mesh.aabb(&parry3d_f64::math::Isometry::identity());
Some((
aabb.mins.x,
aabb.maxs.x,
aabb.mins.y,
aabb.maxs.y,
aabb.mins.z,
aabb.maxs.z,
))
}
AppendageGeometry::Point { .. } => None,
}
}
fn __repr__(&self) -> String {
format!(
"Appendage(name='{}', type={}, volume={:.3}m³)",
self.inner.name(),
self.geometry_type(),
self.inner.volume()
)
}
}
#[pyclass(name = "DeckEdgeSide")]
#[derive(Clone)]
pub struct PyDeckEdgeSide {
inner: RustDeckEdgeSide,
}
#[pymethods]
impl PyDeckEdgeSide {
#[staticmethod]
fn port() -> Self {
Self {
inner: RustDeckEdgeSide::Port,
}
}
#[staticmethod]
fn starboard() -> Self {
Self {
inner: RustDeckEdgeSide::Starboard,
}
}
#[staticmethod]
fn both() -> Self {
Self {
inner: RustDeckEdgeSide::Both,
}
}
fn __repr__(&self) -> String {
format!("{:?}", self.inner)
}
}
#[pyclass(name = "DeckEdge")]
pub struct PyDeckEdge {
pub(crate) inner: RustDeckEdge,
}
#[pymethods]
impl PyDeckEdge {
#[staticmethod]
fn from_points(name: &str, points: Vec<(f64, f64, f64)>, side: &PyDeckEdgeSide) -> Self {
let pts: Vec<[f64; 3]> = points.iter().map(|(x, y, z)| [*x, *y, *z]).collect();
Self {
inner: RustDeckEdge::new(name, pts, side.inner.clone()),
}
}
#[staticmethod]
fn from_file(name: &str, file_path: &str) -> PyResult<Self> {
let path = Path::new(file_path);
let deck_edge = RustDeckEdge::from_file(name, path)
.map_err(|e| PyIOError::new_err(format!("Failed to load deck edge: {}", e)))?;
Ok(Self { inner: deck_edge })
}
#[getter]
fn name(&self) -> &str {
self.inner.name()
}
#[setter]
fn set_name(&mut self, name: &str) {
self.inner.set_name(name);
}
fn num_points(&self) -> usize {
self.inner.points().len()
}
fn get_points(&self) -> Vec<(f64, f64, f64)> {
self.inner
.points()
.iter()
.map(|p| (p[0], p[1], p[2]))
.collect()
}
fn get_side(&self) -> String {
format!("{:?}", self.inner.side())
}
fn get_freeboard(&self, heel: f64, trim: f64, pivot: (f64, f64, f64), waterline_z: f64) -> f64 {
self.inner
.get_freeboard(heel, trim, [pivot.0, pivot.1, pivot.2], waterline_z)
}
fn __repr__(&self) -> String {
format!(
"DeckEdge(name='{}', points={}, side={:?})",
self.inner.name(),
self.inner.points().len(),
self.inner.side()
)
}
}
use crate::downflooding::{
DownfloodingOpening as RustDownfloodingOpening, OpeningGeometry, OpeningType as RustOpeningType,
};
#[pyclass(name = "OpeningType")]
#[derive(Clone)]
pub struct PyOpeningType {
inner: RustOpeningType,
}
#[pymethods]
impl PyOpeningType {
#[staticmethod]
fn vent() -> Self {
Self {
inner: RustOpeningType::Vent,
}
}
#[staticmethod]
fn air_pipe() -> Self {
Self {
inner: RustOpeningType::AirPipe,
}
}
#[staticmethod]
fn hatch() -> Self {
Self {
inner: RustOpeningType::Hatch,
}
}
#[staticmethod]
fn door() -> Self {
Self {
inner: RustOpeningType::Door,
}
}
#[staticmethod]
fn window() -> Self {
Self {
inner: RustOpeningType::Window,
}
}
#[staticmethod]
fn other(name: &str) -> Self {
Self {
inner: RustOpeningType::Other(name.to_string()),
}
}
fn __repr__(&self) -> String {
format!("{:?}", self.inner)
}
}
#[pyclass(name = "DownfloodingOpening")]
pub struct PyDownfloodingOpening {
pub(crate) inner: RustDownfloodingOpening,
}
#[pymethods]
impl PyDownfloodingOpening {
#[staticmethod]
fn from_point(name: &str, position: (f64, f64, f64), opening_type: &PyOpeningType) -> Self {
Self {
inner: RustDownfloodingOpening::from_point(
name.to_string(),
[position.0, position.1, position.2],
opening_type.inner.clone(),
),
}
}
#[staticmethod]
fn from_contour(
name: &str,
points: Vec<(f64, f64, f64)>,
opening_type: &PyOpeningType,
) -> Self {
let pts: Vec<[f64; 3]> = points.iter().map(|(x, y, z)| [*x, *y, *z]).collect();
Self {
inner: RustDownfloodingOpening::from_contour(
name.to_string(),
pts,
opening_type.inner.clone(),
),
}
}
#[staticmethod]
#[pyo3(signature = (file_path, default_type, name=None))]
fn from_file(
file_path: &str,
default_type: &PyOpeningType,
name: Option<String>,
) -> PyResult<Vec<Self>> {
let path = Path::new(file_path);
let mut openings = RustDownfloodingOpening::from_file(path, default_type.inner.clone())
.map_err(|e| PyIOError::new_err(format!("Failed to load openings: {}", e)))?;
if let Some(base_name) = name {
if openings.len() == 1 {
openings[0].set_name(base_name);
} else {
for (i, opening) in openings.iter_mut().enumerate() {
opening.set_name(format!("{}_{}", base_name, i + 1));
}
}
}
Ok(openings.into_iter().map(|o| Self { inner: o }).collect())
}
#[getter]
fn name(&self) -> &str {
self.inner.name()
}
#[getter]
fn is_active(&self) -> bool {
self.inner.is_active()
}
fn set_active(&mut self, active: bool) {
self.inner.set_active(active);
}
fn num_points(&self) -> usize {
self.inner.get_points().len()
}
fn get_points(&self) -> Vec<(f64, f64, f64)> {
self.inner
.get_points()
.iter()
.map(|p| (p[0], p[1], p[2]))
.collect()
}
fn is_submerged(&self, heel: f64, trim: f64, pivot: (f64, f64, f64), waterline_z: f64) -> bool {
self.inner
.is_submerged(heel, trim, [pivot.0, pivot.1, pivot.2], waterline_z)
}
fn __repr__(&self) -> String {
let pts = self.inner.get_points();
let geometry = match &self.inner.geometry() {
OpeningGeometry::Point(_) => "Point",
OpeningGeometry::Contour(_) => "Contour",
};
format!(
"DownfloodingOpening(name='{}', type={:?}, geometry={}, points={})",
self.inner.name(),
self.inner.opening_type(),
geometry,
pts.len()
)
}
}
#[pyclass(name = "HydrostaticState")]
#[derive(Clone)]
pub struct PyHydrostaticState {
#[pyo3(get)]
pub draft: f64,
#[pyo3(get)]
pub trim: f64,
#[pyo3(get)]
pub heel: f64,
#[pyo3(get)]
pub draft_ap: f64,
#[pyo3(get)]
pub draft_fp: f64,
#[pyo3(get)]
pub draft_mp: f64,
#[pyo3(get)]
pub volume: f64,
#[pyo3(get)]
pub displacement: f64,
cob_internal: [f64; 3],
cog_internal: Option<[f64; 3]>,
#[pyo3(get)]
pub waterplane_area: f64,
#[pyo3(get)]
pub lcf: f64,
#[pyo3(get)]
pub bmt: f64,
#[pyo3(get)]
pub bml: f64,
gmt_internal: Option<f64>, gml_internal: Option<f64>, gmt_dry_internal: Option<f64>, gml_dry_internal: Option<f64>,
#[pyo3(get)]
pub lwl: f64,
#[pyo3(get)]
pub bwl: f64,
#[pyo3(get)]
pub los: f64,
#[pyo3(get)]
pub wetted_surface_area: f64,
#[pyo3(get)]
pub midship_area: f64,
#[pyo3(get)]
pub cm: f64,
#[pyo3(get)]
pub cb: f64,
#[pyo3(get)]
pub cp: f64,
#[pyo3(get)]
pub free_surface_correction_t: f64,
#[pyo3(get)]
pub free_surface_correction_l: f64,
#[pyo3(get)]
pub stiffness_matrix: Vec<f64>,
#[pyo3(get)]
pub sectional_areas: Vec<(f64, f64)>,
#[pyo3(get)]
pub freeboard: Option<f64>,
}
impl From<RustHydroState> for PyHydrostaticState {
fn from(state: RustHydroState) -> Self {
Self {
draft: state.draft,
trim: state.trim,
heel: state.heel,
draft_ap: state.draft_ap,
draft_fp: state.draft_fp,
draft_mp: state.draft_mp,
volume: state.volume,
displacement: state.displacement,
cob_internal: state.cob,
cog_internal: state.cog,
waterplane_area: state.waterplane_area,
lcf: state.lcf,
bmt: state.bmt,
bml: state.bml,
gmt_internal: state.gmt,
gml_internal: state.gml,
gmt_dry_internal: state.gmt_dry,
gml_dry_internal: state.gml_dry,
lwl: state.lwl,
bwl: state.bwl,
los: state.los,
wetted_surface_area: state.wetted_surface_area,
midship_area: state.midship_area,
cm: state.cm,
cb: state.cb,
cp: state.cp,
free_surface_correction_t: state.free_surface_correction_t,
free_surface_correction_l: state.free_surface_correction_l,
stiffness_matrix: state.stiffness_matrix.to_vec(),
sectional_areas: state.sectional_areas,
freeboard: state.freeboard,
}
}
}
#[pymethods]
impl PyHydrostaticState {
#[getter]
fn cob(&self) -> (f64, f64, f64) {
(
self.cob_internal[0],
self.cob_internal[1],
self.cob_internal[2],
)
}
#[getter]
fn cog(&self) -> Option<(f64, f64, f64)> {
self.cog_internal.map(|c| (c[0], c[1], c[2]))
}
#[getter]
fn lcb(&self) -> f64 {
self.cob_internal[0]
}
#[getter]
fn tcb(&self) -> f64 {
self.cob_internal[1]
}
#[getter]
fn vcb(&self) -> f64 {
self.cob_internal[2]
}
#[getter]
fn lcg(&self) -> Option<f64> {
self.cog_internal.map(|c| c[0])
}
#[getter]
fn tcg(&self) -> Option<f64> {
self.cog_internal.map(|c| c[1])
}
#[getter]
fn vcg(&self) -> Option<f64> {
self.cog_internal.map(|c| c[2])
}
#[getter]
fn gmt(&self) -> Option<f64> {
self.gmt_internal
}
#[getter]
fn gml(&self) -> Option<f64> {
self.gml_internal
}
#[getter]
fn gmt_dry(&self) -> Option<f64> {
self.gmt_dry_internal
}
#[getter]
fn gml_dry(&self) -> Option<f64> {
self.gml_dry_internal
}
fn __repr__(&self) -> String {
let cog_str = if let Some(c) = self.cog_internal {
format!("COG=({:.2}, {:.2}, {:.2})", c[0], c[1], c[2])
} else {
"COG=None".to_string()
};
format!(
"HydrostaticState(draft_mp={:.3}m (AP={:.3}, FP={:.3}), volume={:.2}m³, displacement={:.0}kg, {})",
self.draft_mp, self.draft_ap, self.draft_fp, self.volume, self.displacement, cog_str
)
}
}
#[pyclass(name = "HydrostaticsCalculator")]
pub struct PyHydrostaticsCalculator {
vessel: RustVessel,
water_density: f64,
}
#[pymethods]
impl PyHydrostaticsCalculator {
#[new]
#[pyo3(signature = (vessel, water_density=1025.0))]
fn new(vessel: &PyVessel, water_density: f64) -> Self {
Self {
vessel: vessel.inner.clone(),
water_density,
}
}
#[pyo3(signature = (draft, trim=0.0, heel=0.0, vcg=None, num_stations=None), name = "from_draft")]
#[allow(clippy::wrong_self_convention)]
fn from_draft(
&self,
draft: f64,
trim: f64,
heel: f64,
vcg: Option<f64>,
num_stations: Option<usize>,
) -> PyResult<PyHydrostaticState> {
let calc = RustHydroCalc::new(&self.vessel, self.water_density);
calc.from_draft_with_stations(draft, trim, heel, vcg, num_stations)
.map(|s| s.into())
.ok_or_else(|| PyValueError::new_err("No submerged volume at this draft"))
}
#[pyo3(signature = (draft_ap, draft_fp, heel=0.0, vcg=None))]
#[allow(clippy::wrong_self_convention)]
fn from_drafts(
&self,
draft_ap: f64,
draft_fp: f64,
heel: f64,
vcg: Option<f64>,
) -> PyResult<PyHydrostaticState> {
let calc = RustHydroCalc::new(&self.vessel, self.water_density);
calc.from_drafts(draft_ap, draft_fp, heel, vcg)
.map(|s| s.into())
.ok_or_else(|| PyValueError::new_err("No submerged volume at these drafts"))
}
#[pyo3(signature = (displacement_mass, vcg=None, cog=None, trim=None, heel=None), name = "from_displacement")]
#[allow(clippy::wrong_self_convention)]
fn from_displacement(
&self,
displacement_mass: f64,
vcg: Option<f64>,
cog: Option<(f64, f64, f64)>,
trim: Option<f64>,
heel: Option<f64>,
) -> PyResult<PyHydrostaticState> {
let calc = RustHydroCalc::new(&self.vessel, self.water_density);
let cog_array = cog.map(|(lcg, tcg, vcg_val)| [lcg, tcg, vcg_val]);
calc.from_displacement(displacement_mass, vcg, cog_array, trim, heel)
.map(|s| s.into())
.map_err(PyValueError::new_err)
}
fn water_density(&self) -> f64 {
self.water_density
}
}
#[pyclass(name = "StabilityPoint")]
#[derive(Clone)]
pub struct PyStabilityPoint {
#[pyo3(get)]
pub heel: f64,
#[pyo3(get)]
pub draft: f64,
#[pyo3(get)]
pub trim: f64,
#[pyo3(get)]
pub gz: f64,
#[pyo3(get)]
pub is_flooding: bool,
#[pyo3(get)]
pub flooded_openings: Vec<String>,
}
#[pyclass(name = "StabilityCurve")]
pub struct PyStabilityCurve {
inner: RustStabCurve,
}
#[pymethods]
impl PyStabilityCurve {
fn heels(&self) -> Vec<f64> {
self.inner.heels()
}
fn values(&self) -> Vec<f64> {
self.inner.values()
}
fn points(&self) -> Vec<(f64, f64, f64, f64)> {
self.inner
.points
.iter()
.map(|p| (p.heel, p.draft, p.trim, p.value))
.collect()
}
fn get_stability_points(&self) -> Vec<PyStabilityPoint> {
self.inner
.points
.iter()
.map(|p| PyStabilityPoint {
heel: p.heel,
draft: p.draft,
trim: p.trim,
gz: p.value,
is_flooding: p.is_flooding,
flooded_openings: p.flooded_openings.clone(),
})
.collect()
}
#[getter]
fn displacement(&self) -> f64 {
self.inner.displacement
}
fn __repr__(&self) -> String {
format!(
"StabilityCurve(displacement={:.0}kg, points={})",
self.inner.displacement,
self.inner.points.len()
)
}
}
#[pyclass(name = "WindHeelingData")]
#[derive(Clone)]
pub struct PyWindHeelingData {
#[pyo3(get)]
pub emerged_area: f64,
emerged_centroid_internal: [f64; 2],
#[pyo3(get)]
pub wind_lever_arm: f64,
#[pyo3(get)]
pub waterline_z: f64,
}
impl From<RustWindHeelingData> for PyWindHeelingData {
fn from(data: RustWindHeelingData) -> Self {
Self {
emerged_area: data.emerged_area,
emerged_centroid_internal: data.emerged_centroid,
wind_lever_arm: data.wind_lever_arm,
waterline_z: data.waterline_z,
}
}
}
#[pymethods]
impl PyWindHeelingData {
#[getter]
fn emerged_centroid(&self) -> (f64, f64) {
(
self.emerged_centroid_internal[0],
self.emerged_centroid_internal[1],
)
}
fn __repr__(&self) -> String {
format!(
"WindHeelingData(emerged_area={:.2}m², lever_arm={:.2}m)",
self.emerged_area, self.wind_lever_arm
)
}
}
#[pyclass(name = "CompleteStabilityResult")]
pub struct PyCompleteStabilityResult {
inner: RustCompleteStabilityResult,
}
impl From<RustCompleteStabilityResult> for PyCompleteStabilityResult {
fn from(result: RustCompleteStabilityResult) -> Self {
Self { inner: result }
}
}
#[pymethods]
impl PyCompleteStabilityResult {
#[getter]
fn hydrostatics(&self) -> PyHydrostaticState {
self.inner.hydrostatics.clone().into()
}
#[getter]
fn gz_curve(&self) -> PyStabilityCurve {
PyStabilityCurve {
inner: self.inner.gz_curve.clone(),
}
}
#[getter]
fn wind_data(&self) -> Option<PyWindHeelingData> {
self.inner.wind_data.clone().map(|d| d.into())
}
#[getter]
fn displacement(&self) -> f64 {
self.inner.displacement
}
#[getter]
fn cog(&self) -> (f64, f64, f64) {
(self.inner.cog[0], self.inner.cog[1], self.inner.cog[2])
}
#[getter]
fn gm0(&self) -> Option<f64> {
self.inner.gm0()
}
#[getter]
fn gm0_dry(&self) -> Option<f64> {
self.inner.gm0_dry()
}
#[getter]
fn max_gz(&self) -> Option<f64> {
self.inner.max_gz()
}
#[getter]
fn heel_at_max_gz(&self) -> Option<f64> {
self.inner.heel_at_max_gz()
}
fn has_wind_data(&self) -> bool {
self.inner.has_wind_data()
}
fn __repr__(&self) -> String {
let gm_str = self
.inner
.gm0()
.map(|gm| format!("{:.3}m", gm))
.unwrap_or_else(|| "N/A".to_string());
let max_gz_str = self
.inner
.max_gz()
.map(|gz| format!("{:.3}m", gz))
.unwrap_or_else(|| "N/A".to_string());
format!(
"CompleteStabilityResult(GM0={}, max_GZ={}, wind_data={})",
gm_str,
max_gz_str,
self.inner.has_wind_data()
)
}
}
#[pyclass(name = "StabilityCalculator")]
pub struct PyStabilityCalculator {
vessel: RustVessel,
water_density: f64,
}
#[pymethods]
impl PyStabilityCalculator {
#[new]
#[pyo3(signature = (vessel, water_density=1025.0))]
fn new(vessel: &PyVessel, water_density: f64) -> Self {
Self {
vessel: vessel.inner.clone(),
water_density,
}
}
fn gz_curve(
&self,
displacement_mass: f64,
cog: (f64, f64, f64),
heels: Vec<f64>,
) -> PyStabilityCurve {
let calc = RustStabCalc::new(&self.vessel, self.water_density);
let curve = calc.gz_curve(displacement_mass, [cog.0, cog.1, cog.2], &heels);
PyStabilityCurve { inner: curve }
}
#[pyo3(signature = (displacements, heels, lcg=0.0, tcg=0.0))]
fn kn_curve(
&self,
displacements: Vec<f64>,
heels: Vec<f64>,
lcg: f64,
tcg: f64,
) -> Vec<PyStabilityCurve> {
let calc = RustStabCalc::new(&self.vessel, self.water_density);
let curves = calc.kn_curve(&displacements, lcg, tcg, &heels);
curves
.into_iter()
.map(|c| PyStabilityCurve { inner: c })
.collect()
}
fn complete_stability(
&self,
displacement_mass: f64,
cog: (f64, f64, f64),
heels: Vec<f64>,
) -> PyCompleteStabilityResult {
let calc = RustStabCalc::new(&self.vessel, self.water_density);
let result = calc.complete_stability(displacement_mass, [cog.0, cog.1, cog.2], &heels);
result.into()
}
}
#[pyclass(name = "Tank")]
pub struct PyTank {
inner: RustTank,
}
#[pymethods]
impl PyTank {
#[new]
#[pyo3(signature = (file_path, fluid_density=1025.0, name=None))]
fn new(file_path: &str, fluid_density: f64, name: Option<&str>) -> PyResult<Self> {
let path = Path::new(file_path);
let mut tank = RustTank::from_file(path, fluid_density)
.map_err(|e| PyValueError::new_err(format!("Failed to load tank: {}", e)))?;
if let Some(n) = name {
tank.set_name(n);
}
Ok(Self { inner: tank })
}
#[staticmethod]
#[pyo3(signature = (hull, x_min, x_max, y_min, y_max, z_min, z_max, fluid_density=1025.0, name="HullTank"))]
#[allow(clippy::too_many_arguments)]
fn from_box_hull_intersection(
hull: &PyHull,
x_min: f64,
x_max: f64,
y_min: f64,
y_max: f64,
z_min: f64,
z_max: f64,
fluid_density: f64,
name: &str,
) -> PyResult<Self> {
let tank = RustTank::from_box_hull_intersection(
name,
&hull.inner,
x_min,
x_max,
y_min,
y_max,
z_min,
z_max,
fluid_density,
)
.map_err(|e| {
PyValueError::new_err(format!("Failed to create tank from intersection: {}", e))
})?;
Ok(Self { inner: tank })
}
#[staticmethod]
#[allow(clippy::too_many_arguments)]
fn from_box(
name: &str,
x_min: f64,
x_max: f64,
y_min: f64,
y_max: f64,
z_min: f64,
z_max: f64,
fluid_density: f64,
) -> Self {
Self {
inner: RustTank::from_box(
name,
x_min,
x_max,
y_min,
y_max,
z_min,
z_max,
fluid_density,
),
}
}
#[getter]
fn name(&self) -> &str {
self.inner.name()
}
#[getter]
fn total_volume(&self) -> f64 {
self.inner.total_volume()
}
#[getter]
fn fill_level(&self) -> f64 {
self.inner.fill_level()
}
#[setter]
fn set_fill_level(&mut self, level: f64) {
self.inner.set_fill_level(level);
}
#[getter]
fn fill_percent(&self) -> f64 {
self.inner.fill_percent()
}
#[setter]
fn set_fill_percent(&mut self, percent: f64) {
self.inner.set_fill_percent(percent);
}
#[getter]
fn fill_volume(&self) -> f64 {
self.inner.fill_volume()
}
#[getter]
fn fluid_mass(&self) -> f64 {
self.inner.fluid_mass()
}
#[getter]
fn center_of_gravity(&self) -> [f64; 3] {
self.inner.center_of_gravity()
}
#[getter]
fn free_surface_moment_t(&self) -> f64 {
self.inner.free_surface_moment_t()
}
#[getter]
fn free_surface_moment_l(&self) -> f64 {
self.inner.free_surface_moment_l()
}
fn __repr__(&self) -> String {
format!(
"Tank(name='{}', volume={:.2}m³, fill={:.1}%)",
self.inner.name(),
self.inner.total_volume(),
self.inner.fill_percent()
)
}
fn get_vertices(&self) -> Vec<(f64, f64, f64)> {
self.inner
.mesh()
.vertices()
.iter()
.map(|v| (v.x, v.y, v.z))
.collect()
}
fn get_faces(&self) -> Vec<(u32, u32, u32)> {
self.inner
.mesh()
.indices()
.iter()
.map(|idx| (idx[0], idx[1], idx[2]))
.collect()
}
#[pyo3(signature = (heel=0.0, trim=0.0))]
fn get_fluid_vertices(&self, heel: f64, trim: f64) -> Vec<(f64, f64, f64)> {
self.inner
.get_fluid_mesh_at(heel, trim)
.map(|m| m.vertices().iter().map(|v| (v.x, v.y, v.z)).collect())
.unwrap_or_default()
}
#[pyo3(signature = (heel=0.0, trim=0.0))]
fn get_fluid_faces(&self, heel: f64, trim: f64) -> Vec<(u32, u32, u32)> {
self.inner
.get_fluid_mesh_at(heel, trim)
.map(|m| {
m.indices()
.iter()
.map(|idx| (idx[0], idx[1], idx[2]))
.collect()
})
.unwrap_or_default()
}
}
#[pyclass(name = "CriterionResult")]
#[derive(Clone)]
pub struct PyCriterionResult {
#[pyo3(get)]
pub name: String,
#[pyo3(get)]
pub description: String,
#[pyo3(get)]
pub required_value: f64,
#[pyo3(get)]
pub actual_value: f64,
#[pyo3(get)]
pub unit: String,
#[pyo3(get)]
pub status: String,
#[pyo3(get)]
pub margin: f64,
#[pyo3(get)]
pub notes: Option<String>,
#[pyo3(get)]
pub plot_id: Option<String>,
}
#[pymethods]
impl PyCriterionResult {
fn __repr__(&self) -> String {
format!("<CriterionResult '{}': {}>", self.name, self.status)
}
}
#[pyclass(name = "CriteriaResult")]
#[derive(Clone)]
pub struct PyCriteriaResult {
#[pyo3(get)]
pub regulation_name: String,
#[pyo3(get)]
pub regulation_reference: String,
#[pyo3(get)]
pub vessel_name: String,
#[pyo3(get)]
pub loading_condition: String,
#[pyo3(get)]
pub displacement: f64,
#[pyo3(get)]
pub overall_pass: bool,
#[pyo3(get)]
pub pass_count: usize,
#[pyo3(get)]
pub fail_count: usize,
#[pyo3(get)]
pub notes: String,
#[pyo3(get)]
pub criteria: Vec<PyCriterionResult>,
#[pyo3(get)]
pub plots: Vec<String>,
}
#[pymethods]
impl PyCriteriaResult {
fn __repr__(&self) -> String {
format!(
"<CriteriaResult '{}': {} (Passed {}/{})>",
self.regulation_name,
if self.overall_pass { "PASS" } else { "FAIL" },
self.pass_count,
self.criteria.len()
)
}
}
#[pyclass(name = "CriteriaContext")]
#[derive(Clone)]
pub struct PyCriteriaContext {
inner: RustCriteriaContext,
}
#[pymethods]
impl PyCriteriaContext {
#[staticmethod]
fn from_result(
result: &PyCompleteStabilityResult,
vessel_name: String,
loading_condition: String,
) -> Self {
Self {
inner: RustCriteriaContext::new(result.inner.clone(), vessel_name, loading_condition),
}
}
fn get_first_flooding_angle(&self) -> Option<f64> {
self.inner.get_first_flooding_angle().try_cast::<f64>()
}
fn find_equilibrium_angle(&self, heeling_arm: f64) -> Option<f64> {
self.inner
.find_equilibrium_angle(heeling_arm)
.try_cast::<f64>()
}
fn find_second_intercept(&self, heeling_arm: f64) -> Option<f64> {
self.inner
.find_second_intercept(heeling_arm)
.try_cast::<f64>()
}
fn set_param(&mut self, key: &str, value: &Bound<PyAny>) -> PyResult<()> {
let val = if let Ok(s) = value.extract::<String>() {
rhai::Dynamic::from(s)
} else if let Ok(f) = value.extract::<f64>() {
rhai::Dynamic::from(f)
} else if let Ok(b) = value.extract::<bool>() {
rhai::Dynamic::from(b)
} else {
return Err(PyValueError::new_err(
"Unsupported parameter type. Use str, float, or bool.",
));
};
self.inner.set_param(key, val);
Ok(())
}
}
#[pyclass(name = "ScriptEngine")]
pub struct PyScriptEngine {
inner: RustScriptEngine,
}
#[pymethods]
impl PyScriptEngine {
#[new]
fn new() -> Self {
Self {
inner: RustScriptEngine::new(),
}
}
fn run_script_file(
&self,
path: &str,
context: &PyCriteriaContext,
) -> PyResult<PyCriteriaResult> {
let result = self
.inner
.run_script_file(path, context.inner.clone())
.map_err(|e| PyValueError::new_err(format!("Script error: {}", e)))?;
Ok(map_result_to_py(result))
}
fn run_script(&self, script: &str, context: &PyCriteriaContext) -> PyResult<PyCriteriaResult> {
let result = self
.inner
.run_script(script, context.inner.clone())
.map_err(|e| PyValueError::new_err(format!("Script error: {}", e)))?;
Ok(map_result_to_py(result))
}
}
fn map_result_to_py(res: RustCriteriaResult) -> PyCriteriaResult {
let criteria: Vec<PyCriterionResult> = res
.criteria
.iter()
.map(|c| PyCriterionResult {
name: c.name.clone(),
description: c.description.clone(),
required_value: c.required_value,
actual_value: c.actual_value,
unit: c.unit.clone(),
status: c.status.to_string(),
margin: c.margin,
notes: c.notes.clone(),
plot_id: c.plot_id.clone(),
})
.collect();
let plots: Vec<String> = res
.plots
.iter()
.map(|p| serde_json::to_string(p).unwrap_or_default())
.collect();
PyCriteriaResult {
regulation_name: res.regulation_name,
regulation_reference: res.regulation_reference,
vessel_name: res.vessel_name,
loading_condition: res.loading_condition,
displacement: res.displacement,
overall_pass: res.overall_pass,
pass_count: res.pass_count,
fail_count: res.fail_count,
notes: res.notes,
criteria,
plots,
}
}
#[pymodule]
fn navaltoolbox(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<PyHull>()?;
m.add_class::<PyVessel>()?;
m.add_class::<PySilhouette>()?;
m.add_class::<PyAppendage>()?;
m.add_class::<PyDeckEdgeSide>()?;
m.add_class::<PyDeckEdge>()?;
m.add_class::<PyOpeningType>()?;
m.add_class::<PyDownfloodingOpening>()?;
m.add_class::<PyHydrostaticState>()?;
m.add_class::<PyHydrostaticsCalculator>()?;
m.add_class::<PyStabilityPoint>()?;
m.add_class::<PyStabilityCurve>()?;
m.add_class::<PyWindHeelingData>()?;
m.add_class::<PyCompleteStabilityResult>()?;
m.add_class::<PyStabilityCalculator>()?;
m.add_class::<PyTank>()?;
m.add_class::<PyCriterionResult>()?;
m.add_class::<PyCriteriaResult>()?;
m.add_class::<PyCriteriaContext>()?;
m.add_class::<PyScriptEngine>()?;
Ok(())
}