diff --git a/README.md b/README.md index bf88b27..c43e18c 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,10 @@ print(f"GMT (wet): {state.gmt:.3f} m") state_disp = calc.from_displacement(512500.0) print(f"Draft: {state_disp.draft:.3f} m") +# Option 3: From perpendicular drafts (AP/FP) +state_drafts = calc.from_drafts(draft_ap=6.0, draft_fp=4.0) +print(f"Trim: {state_drafts.trim:.2f}°") + # Calculate GZ curve stab = StabilityCalculator(vessel, water_density=1025.0) heels = [0, 10, 20, 30, 40, 50, 60] @@ -119,6 +123,10 @@ let calc = HydrostaticsCalculator::new(&vessel, 1025.0); let state = calc.from_draft(5.0, 0.0, 0.0, None)?; println!("Volume: {} m³", state.volume); +// Calculate from AP/FP drafts +let state_drafts = calc.from_drafts(6.0, 4.0, 0.0, None).unwrap(); +println!("Trim: {:.2}°", state_drafts.trim); + // Calculate GZ curve let stab = StabilityCalculator::new(&vessel, 1025.0); let heels = vec![0.0, 10.0, 20.0, 30.0, 40.0, 50.0, 60.0]; diff --git a/docs/source/api/python/hydrostatics.rst b/docs/source/api/python/hydrostatics.rst index 547d1af..b21fc45 100644 --- a/docs/source/api/python/hydrostatics.rst +++ b/docs/source/api/python/hydrostatics.rst @@ -24,6 +24,17 @@ HydrostaticsCalculator :type vcg: float or None :returns: HydrostaticState with all properties + .. py:method:: from_drafts(draft_ap, draft_fp, heel=0.0, vcg=None) + + Calculates hydrostatics from drafts at Aft and Forward Perpendiculars. + + :param draft_ap: Draft at Aft Perpendicular (m) + :param draft_fp: Draft at Forward Perpendicular (m) + :param heel: Heel angle (degrees) + :param vcg: Vertical center of gravity for GM calculation (optional) + :type vcg: float or None + :returns: HydrostaticState with all properties + .. py:method:: from_displacement(displacement_mass, vcg=None, cog=None, trim=None, heel=None) Calculates hydrostatics for a given displacement, finding the required draft. @@ -48,7 +59,23 @@ HydrostaticState .. py:attribute:: draft :type: float - Draft at reference point (m). + Draft at Midship Section (MP) in meters. + + .. py:attribute:: draft_ap + :type: float + + Draft at Aft Perpendicular (AP) in meters. + + .. py:attribute:: draft_fp + :type: float + + Draft at Forward Perpendicular (FP) in meters. + + .. py:attribute:: draft_mp + :type: float + + Draft at Midship Perpendicular (MP) in meters (alias for draft). + .. py:attribute:: volume :type: float diff --git a/docs/source/tutorials/hydrostatics.rst b/docs/source/tutorials/hydrostatics.rst index c45e028..eab2be1 100644 --- a/docs/source/tutorials/hydrostatics.rst +++ b/docs/source/tutorials/hydrostatics.rst @@ -51,6 +51,44 @@ Calculate hydrostatics at a specific draft: print(f"TCB: {state.tcb:.3f}m (should be ~0)") print(f"VCB: {state.vcb:.2f}m (KB)") + # Drafts at perpendiculars + print(f"Draft MP: {state.draft_mp:.3f}m") + print(f"Draft AP: {state.draft_ap:.3f}m") + print(f"Draft FP: {state.draft_fp:.3f}m") + +Calculating from Drafts at Perpendiculars +----------------------------------------- + +You can also calculate hydrostatics by specifying drafts at the Aft Perpendicular (AP) and Forward Perpendicular (FP): + +.. code-block:: python + + # Calculate from AP/FP drafts + # This automatically computes the trim and midship draft + state = calc.from_drafts( + draft_ap=6.5, + draft_fp=5.5, + heel=0.0, + vcg=7.5 + ) + + print(f"Calculated MP Draft: {state.draft_mp:.3f}m") + print(f"Calculated Trim: {state.trim:.2f} degrees") + print(f"Displacement: {state.displacement:.0f} kg") + +Note on Reference System +------------------------ + +In NavalToolbox, the input draft for calculations corresponds to the draft at the **Midship Section (MP)**. +The longitudinal position of MP is calculated as the average of the Aft Perpendicular (AP) and Forward Perpendicular (FP). +If AP and FP are not explicitly set on the vessel, they default to the minimum and maximum X bounds of the geometry. + +When an ongoing trim is applied (positive bow down), the drafts at perpendiculars are calculated as: + +- **Draft MP**: Equal to the input draft at the pivot point +- **Draft AP**: Calculated from MP draft and trim angle +- **Draft FP**: Calculated from MP draft and trim angle + Waterplane & Stability ---------------------- diff --git a/python/navaltoolbox/navaltoolbox.pyi b/python/navaltoolbox/navaltoolbox.pyi index e802a98..1899f01 100644 --- a/python/navaltoolbox/navaltoolbox.pyi +++ b/python/navaltoolbox/navaltoolbox.pyi @@ -537,12 +537,17 @@ class HydrostaticState: Contains the hydrostatic properties at a specific floating condition. + Attributes: draft: Draft at midship in meters. trim: Trim angle in degrees. heel: Heel angle in degrees. + draft_ap: Draft at Aft Perpendicular in meters. + draft_fp: Draft at Forward Perpendicular in meters. + draft_mp: Draft at Midship Perpendicular in meters. volume: Submerged volume in m³. displacement: Displacement mass in kg. + cob: Center of buoyancy as tuple (lcb, tcb, vcb). cog: Center of gravity as tuple (lcg, tcg, vcg) if specified, None otherwise. lcb: Longitudinal center of buoyancy (X) in meters. @@ -575,6 +580,9 @@ class HydrostaticState: draft: float trim: float heel: float + draft_ap: float + draft_fp: float + draft_mp: float volume: float displacement: float @@ -702,6 +710,29 @@ class HydrostaticsCalculator: """ ... + def from_drafts( + self, + draft_ap: float, + draft_fp: float, + heel: float = 0.0, + vcg: float | None = None, + ) -> HydrostaticState: + """Calculate hydrostatics from drafts at Aft and Forward Perpendiculars. + + Args: + draft_ap: Draft at Aft Perpendicular in meters. + draft_fp: Draft at Forward Perpendicular in meters. + heel: Heel angle in degrees (default: 0). + vcg: Optional vertical center of gravity in meters for GMT/GML calculation. + + Returns: + HydrostaticState with calculated properties. + + Raises: + ValueError: If no submerged volume at these drafts. + """ + ... + def from_displacement( self, displacement_mass: float, diff --git a/rust/src/hydrostatics/calculator.rs b/rust/src/hydrostatics/calculator.rs index 41c1b98..4502792 100644 --- a/rust/src/hydrostatics/calculator.rs +++ b/rust/src/hydrostatics/calculator.rs @@ -58,10 +58,24 @@ impl<'a> HydrostaticsCalculator<'a> { heel: f64, vcg: Option, ) -> Option { + // Use AP/FP (defaults to bounds min/max if not set) + let ap = self.vessel.ap(); + let fp = self.vessel.fp(); + let mp_x = (ap + fp) / 2.0; + + // Use bounds center for Y let bounds = self.vessel.get_bounds(); - let center_x = (bounds.0 + bounds.1) / 2.0; let center_y = (bounds.2 + bounds.3) / 2.0; - let pivot = Point3::new(center_x, center_y, draft); + + // Pivot is now at MP + let pivot = Point3::new(mp_x, center_y, draft); + + // Calculate specific drafts + // Trim is positive bow down -> draft increases forward + let tan_trim = trim.to_radians().tan(); + let draft_mp = draft; + let draft_fp = draft + (fp - mp_x) * tan_trim; + let draft_ap = draft + (ap - mp_x) * tan_trim; let mut total_volume = 0.0; let mut total_moment = [0.0, 0.0, 0.0]; @@ -267,6 +281,9 @@ impl<'a> HydrostaticsCalculator<'a> { draft, trim, heel, + draft_ap, + draft_fp, + draft_mp, volume: total_volume, displacement, cob, @@ -297,6 +314,49 @@ impl<'a> HydrostaticsCalculator<'a> { }) } + /// Calculates hydrostatics from drafts at Aft and Forward Perpendiculars. + /// + /// This is a convenience method that calculates the equivalent mean draft (at MP) + /// and trim angle, then calls `from_draft`. + /// + /// # Arguments + /// * `draft_ap` - Draft at Aft Perpendicular in meters + /// * `draft_fp` - Draft at Forward Perpendicular in meters + /// * `heel` - Heel angle in degrees + /// * `vcg` - Optional vertical center of gravity for GM calculation + pub fn from_drafts( + &self, + draft_ap: f64, + draft_fp: f64, + heel: f64, + vcg: Option, + ) -> Option { + let ap = self.vessel.ap(); + let fp = self.vessel.fp(); + let lpp = fp - ap; + + if lpp.abs() < 1e-4 { + // Lpp is too small, assume zero trim + return self.from_draft(draft_ap, 0.0, heel, vcg); + } + + // Calculate trim: positive bow down (fp draft > ap draft) + // tan(trim) = (T_fp - T_ap) / Lpp + let trim_rad = ((draft_fp - draft_ap) / lpp).atan(); + let trim_deg = trim_rad.to_degrees(); + + // Calculate draft at MP (midship) + // T_mp = T_ap + (MP - AP) * tan(trim) + // MP = (AP + FP) / 2 + // MP - AP = (FP - AP) / 2 = Lpp / 2 + // T_mp = T_ap + (Lpp/2) * (T_fp - T_ap) / Lpp + // T_mp = T_ap + (T_fp - T_ap) / 2 + // T_mp = (T_ap + T_fp) / 2 + let draft_mp = (draft_ap + draft_fp) / 2.0; + + self.from_draft(draft_mp, trim_deg, heel, vcg) + } + /// Calculate hydrostatics for a given displacement with optional constraints. /// /// # Arguments @@ -780,4 +840,30 @@ mod tests { let res = calc.from_displacement(100000.0, None, Some([0.0, 5.0, 0.0]), None, Some(0.0)); assert!(res.is_err(), "Should fail for both TCG and Heel specified"); } + #[test] + fn test_from_drafts() { + let hull = create_box_hull(100.0, 20.0, 10.0); + let mut vessel = Vessel::new(hull); + // Set AP/FP explicitly + vessel.set_ap(0.0); + vessel.set_fp(100.0); + + let calc = HydrostaticsCalculator::new(&vessel, 1025.0); + + // Case 1: Even Keel (Draft=5.0) + let state1 = calc.from_drafts(5.0, 5.0, 0.0, None).unwrap(); + assert!((state1.draft - 5.0).abs() < 1e-6); + assert!((state1.draft_ap - 5.0).abs() < 1e-6); + assert!((state1.draft_fp - 5.0).abs() < 1e-6); + assert!(state1.trim.abs() < 1e-6); + + // Case 2: Trimmed by stern (AP=6.0, FP=4.0) + // MP draft should be 5.0 + // Trim = atan((4-6)/100) = atan(-0.02) + let state2 = calc.from_drafts(6.0, 4.0, 0.0, None).unwrap(); + assert!((state2.draft_mp - 5.0).abs() < 1e-6); + assert!((state2.draft_ap - 6.0).abs() < 1e-6); + assert!((state2.draft_fp - 4.0).abs() < 1e-6); + assert!(state2.trim < 0.0); // Stern down is negative trim + } } diff --git a/rust/src/hydrostatics/dataclasses.rs b/rust/src/hydrostatics/dataclasses.rs index f9e906c..1ea6c3b 100644 --- a/rust/src/hydrostatics/dataclasses.rs +++ b/rust/src/hydrostatics/dataclasses.rs @@ -26,6 +26,12 @@ pub struct HydrostaticState { pub trim: f64, /// Heel angle in degrees pub heel: f64, + /// Draft at Aft Perpendicular in meters + pub draft_ap: f64, + /// Draft at Forward Perpendicular in meters + pub draft_fp: f64, + /// Draft at Midship (MP) in meters + pub draft_mp: f64, /// Submerged volume in m³ pub volume: f64, @@ -149,6 +155,9 @@ impl Default for HydrostaticState { free_surface_correction_l: 0.0, stiffness_matrix: [0.0; 36], los: 0.0, + draft_ap: 0.0, + draft_fp: 0.0, + draft_mp: 0.0, } } } diff --git a/rust/src/python.rs b/rust/src/python.rs index 37bb10e..284e97f 100644 --- a/rust/src/python.rs +++ b/rust/src/python.rs @@ -565,6 +565,12 @@ pub struct PyHydrostaticState { #[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, @@ -620,6 +626,9 @@ impl From for PyHydrostaticState { 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, @@ -730,8 +739,8 @@ impl PyHydrostaticState { }; format!( - "HydrostaticState(draft={:.3}m, volume={:.2}m³, displacement={:.0}kg, {})", - self.draft, self.volume, self.displacement, cog_str + "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 ) } } @@ -783,6 +792,33 @@ impl PyHydrostaticsCalculator { .ok_or_else(|| PyValueError::new_err("No submerged volume at this draft")) } + /// Calculate hydrostatics from drafts at Aft and Forward Perpendiculars. + /// + /// Args: + /// draft_ap: Draft at Aft Perpendicular in meters. + /// draft_fp: Draft at Forward Perpendicular in meters. + /// heel: Heel angle in degrees (default 0.0). + /// vcg: Optional vertical center of gravity for GM calculation. + /// + /// Returns: + /// HydrostaticState with all properties + /// + /// Raises: + /// ValueError: If no submerged volume at this draft. + #[pyo3(signature = (draft_ap, draft_fp, heel=0.0, vcg=None))] + fn from_drafts( + &self, + draft_ap: f64, + draft_fp: f64, + heel: f64, + vcg: Option, + ) -> PyResult { + 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")) + } + /// Calculate hydrostatics for a given displacement with optional constraints. /// /// Args: