Files
navaltoolbox-lib/rules/is_code_2008_complete.rhai
Antoine Anceau fec53f4012
CI / Lint Rust (push) Canceled after 0s
CI / Lint Python (push) Canceled after 0s
CI / Test Rust (push) Canceled after 0s
CI / Build wheels (macos-latest) (push) Canceled after 0s
CI / Build wheels (ubuntu-latest) (push) Canceled after 0s
CI / Build wheels (windows-latest) (push) Canceled after 0s
CI / Test Python 3.10 (push) Canceled after 0s
CI / Test Python 3.11 (push) Canceled after 0s
CI / Test Python 3.12 (push) Canceled after 0s
CI / Test Python 3.13 (push) Canceled after 0s
CI / Test Python 3.9 (push) Canceled after 0s
CI / CI Success (push) Canceled after 0s
Documentation / Build documentation (push) Canceled after 0s
refactor(rules): update criteria with <= operator where applicable
2026-07-07 06:56:58 +02:00

343 lines
13 KiB
Plaintext

// IS Code 2008 Code on Intact Stability
// Complete Verification (Section 3.1 & 3.2)
//
// =============================================================================
// USER PARAMETERS
// =============================================================================
// The following parameters can be provided via the Context (e.g. ctx.set_param in Python).
//
// 1. "wind_pressure" (float) [Pa]:
// - Wind pressure P in Pascal.
// - Default: 504.0 Pa (if not specified)
//
// 2. "roll_angle" (float) [deg]:
// - Pre-calculated roll angle phi1.
// - Default: Calculated via Section 3.2.2.3 if not provided.
//
// 3. "bilge_keel_area" (float) [m²]:
// - Total area of bilge keels (projected lateral area of keel alone, or Ap).
// - Required for roll calculation if "roll_angle" is not provided.
// - Default: 0.0
//
// 4. "bilge_type" (string):
// - "round" or "sharp".
// - Affects damping factor 's'.
// - Default: "round"
//
// 5. "length_bp" (float) [m]: optional override for LWL/LBP if needed for coefficients.
// - Default: uses ctx.get_lwl()
//
// =============================================================================
// Helper: Linear Interpolation
fn interp(x, x1, y1, x2, y2) {
if x <= x1 { return y1; }
if x >= x2 { return y2; }
return y1 + (x - x1) * (y2 - y1) / (x2 - x1);
}
// Table 3.2.2.3-1: Factor X1 vs B/d
fn get_x1(b_d) {
if b_d <= 2.4 { return 1.0; }
if b_d <= 2.5 { return interp(b_d, 2.4, 1.0, 2.5, 0.98); }
if b_d <= 2.6 { return interp(b_d, 2.5, 0.98, 2.6, 0.96); }
if b_d <= 2.7 { return interp(b_d, 2.6, 0.96, 2.7, 0.95); }
if b_d <= 2.8 { return interp(b_d, 2.7, 0.95, 2.8, 0.93); }
if b_d <= 2.9 { return interp(b_d, 2.8, 0.93, 2.9, 0.91); }
if b_d <= 3.0 { return interp(b_d, 2.9, 0.91, 3.0, 0.90); }
if b_d <= 3.1 { return interp(b_d, 3.0, 0.90, 3.1, 0.88); }
if b_d <= 3.2 { return interp(b_d, 3.1, 0.88, 3.2, 0.86); }
if b_d <= 3.4 { return interp(b_d, 3.2, 0.86, 3.4, 0.82); }
if b_d >= 3.5 { return 0.80; } // Extend table logic (usually stops at 3.5 = 0.80)
return interp(b_d, 3.4, 0.82, 3.5, 0.80);
}
// Table 3.2.2.3-2: Factor X2 vs Cb
fn get_x2(cb) {
if cb <= 0.45 { return 0.75; }
if cb <= 0.50 { return interp(cb, 0.45, 0.75, 0.50, 0.82); }
if cb <= 0.55 { return interp(cb, 0.50, 0.82, 0.55, 0.89); }
if cb <= 0.60 { return interp(cb, 0.55, 0.89, 0.60, 0.95); }
if cb <= 0.65 { return interp(cb, 0.60, 0.95, 0.65, 0.97); }
if cb >= 0.70 { return 1.0; }
return interp(cb, 0.65, 0.97, 0.70, 1.0);
}
// Table 3.2.2.3-3: Factor k vs (Ak x 100 / (L x B))
fn get_k(ratio) {
if ratio <= 0.0 { return 1.0; }
if ratio <= 1.0 { return interp(ratio, 0.0, 1.0, 1.0, 0.98); }
if ratio <= 1.5 { return interp(ratio, 1.0, 0.98, 1.5, 0.95); }
if ratio <= 2.0 { return interp(ratio, 1.5, 0.95, 2.0, 0.88); }
if ratio <= 2.5 { return interp(ratio, 2.0, 0.88, 2.5, 0.79); }
if ratio <= 3.0 { return interp(ratio, 2.5, 0.79, 3.0, 0.74); }
if ratio <= 3.5 { return interp(ratio, 3.0, 0.74, 3.5, 0.72); }
if ratio >= 4.0 { return 0.70; }
return interp(ratio, 3.5, 0.72, 4.0, 0.70);
}
// Table 3.2.2.3-4: Factor s vs T (rolling period)
fn get_s(t) {
if t <= 6.0 { return 0.100; }
if t <= 7.0 { return interp(t, 6.0, 0.100, 7.0, 0.098); }
if t <= 8.0 { return interp(t, 7.0, 0.098, 8.0, 0.093); }
if t <= 12.0 { return interp(t, 8.0, 0.093, 12.0, 0.065); }
if t <= 14.0 { return interp(t, 12.0, 0.065, 14.0, 0.053); }
if t <= 16.0 { return interp(t, 14.0, 0.053, 16.0, 0.044); }
if t <= 18.0 { return interp(t, 16.0, 0.044, 18.0, 0.038); }
if t >= 20.0 { return 0.035; }
return interp(t, 18.0, 0.038, 20.0, 0.035);
}
// Helper to calculate roll angle
fn calculate_roll_angle(ctx) {
// 1. Gather inputs
let l = ctx.get_lwl();
// Moulded breadth B per IS Code §2.13: maximum breadth at amidships to the moulded
// line of the frame. Falls back to the waterline beam (BWL) if not available.
let b = ctx.get_moulded_breadth();
if b == () { b = ctx.get_bwl(); }
let d = ctx.get_draft(); // Mean moulded draft
let cb = ctx.get_cb();
let gm = ctx.get_gm0();
let vcg = ctx.get_cog()[2]; // KG
// Safety check
if gm <= 0.0 { return 25.0; } // Unstable or invalid GM, fallback
// 2. Factors
let x1 = get_x1(b/d);
let x2 = get_x2(cb);
// k factor
let ak = 0.0;
if ctx.has_param("bilge_keel_area") {
ak = ctx.get_param("bilge_keel_area");
}
let k_ratio = ak * 100.0 / (l * b);
let k = get_k(k_ratio);
// 3. Rolling period T
// T = 2*C*B / sqrt(GM)
// C = 0.373 + 0.023(B/d) - 0.043(L/100)
let c = 0.373 + 0.023 * (b/d) - 0.043 * (l / 100.0);
let t_period = 2.0 * c * b / gm.sqrt();
// 4. s factor
let s = get_s(t_period);
// 5. r factor
// r = 0.73 + 0.6 * (OG / d)
// OG = KG - d
let og = vcg - d;
let r = 0.73 + 0.6 * (og / d);
// 6. Calculate phi1
// phi1 = 109 * k * X1 * X2 * sqrt(r * s)
let phi1 = 109.0 * k * x1 * x2 * (r * s).sqrt();
return phi1;
}
// Helper to generate area data for plotting
fn make_area_data(ctx, start_angle, end_angle, wind_arm, num_points) {
let x = [];
let y_lower = [];
let y_upper = [];
let step = (end_angle - start_angle) / (num_points - 1);
if step < 0.0 { step = 0.0; } // Handle case where start == end
for i in 0..num_points {
let angle = start_angle + i * step;
let gz = ctx.gz_at_angle(angle);
x.push(angle);
y_lower.push(gz);
y_upper.push(wind_arm);
}
return #{ x: x, y_lower: y_lower, y_upper: y_upper };
}
// Main check function
fn check(ctx) {
let results = [];
// =============================================================================
// 1. General Intact Stability Criteria (Section 3.1)
// =============================================================================
// 3.1.2.1: Area 0-30
let area_0_30 = ctx.area_under_curve(0.0, 30.0);
results.push(criterion("3.1.2.1 Area 0-30", "Area under GZ curve up to 30 deg", 0.055, area_0_30, "m.rad"));
// 3.1.2.1: Area 0-40 (or flooding)
let limit_40 = ctx.get_limiting_angle(40.0);
let area_0_40 = ctx.area_under_curve(0.0, limit_40);
results.push(criterion("3.1.2.1 Area 0-40", "Area under GZ curve up to 40 deg", 0.090, area_0_40, "m.rad"));
// 3.1.2.1: Area 30-40
let area_30_40 = ctx.area_under_curve(30.0, limit_40);
results.push(criterion("3.1.2.1 Area 30-40", "Area between 30 and 40 deg", 0.030, area_30_40, "m.rad"));
// 3.1.2.2: Max GZ
let max = ctx.find_max_gz();
results.push(criterion("3.1.2.2 Max GZ Value", "Max GZ >= 0.20m", 0.20, max.value, "m"));
results.push(criterion("3.1.2.2 Max GZ Angle", "Angle of Max GZ >= 25 deg", 25.0, max.angle, "deg"));
// 3.1.2.4: GM0
let gm0 = ctx.get_gm0();
results.push(criterion("3.1.2.4 GM0", "Initial metacentric height", 0.15, gm0, "m"));
// =============================================================================
// 2. Severe Wind and Rolling Criterion (Section 3.2)
// =============================================================================
let has_wind = ctx.has_wind_data();
let user_P = 504.0;
if ctx.has_param("wind_pressure") { user_P = ctx.get_param("wind_pressure"); }
let roll_deg = 0.0;
let roll_calculated = false;
if ctx.has_param("roll_angle") {
roll_deg = ctx.get_param("roll_angle");
} else {
// Calculate roll angle if not provided
roll_deg = calculate_roll_angle(ctx);
roll_calculated = true;
}
if has_wind {
// 1. lw1
let lw1 = ctx.calculate_wind_heeling_lever(user_P);
// 2. phi0
let phi0 = ctx.find_equilibrium_angle(lw1);
if phi0 != () {
// Check 3.2.2.1
results.push(criterion("3.2.2.1 Equilibrium", "Steady wind heel <= 16 deg", 16.0, phi0, "deg", "<="));
// 3. lw2 = 1.5 * lw1
let lw2 = 1.5 * lw1;
// 3a. phi_eq2 (first equilibrium under gust lw2)
let phi_eq2 = ctx.find_equilibrium_angle(lw2);
if phi_eq2 == () { phi_eq2 = phi_c; } // Fallback if tangent or weird, though usually it exists if phi_c exists
// 4. phi1 (heeled to windward)
let phi1 = phi0 - roll_deg;
// 5. phi2 (downflooding, 50deg, or intercept)
let phi_flood = ctx.get_first_flooding_angle();
let phi_c = ctx.find_second_intercept(lw2);
if phi_flood == () { phi_flood = 180.0; }
if phi_c == () { phi_c = 180.0; } // Assuming stability maintains
let phi2 = 50.0;
if phi_c < phi2 { phi2 = phi_c; }
if phi_flood < phi2 { phi2 = phi_flood; }
// 6. Areas (Net Areas between curves)
// Area A (Heeling Energy): Area between lw2 and GZ from phi1 to phi_eq2
// A = Integral(lw2 - GZ) dPhi
let area_gz_a = ctx.area_under_curve(phi1, phi_eq2);
let area_wind_a = lw2 * (phi_eq2 - phi1) * 3.14159 / 180.0;
let area_a = area_wind_a - area_gz_a; // Positive if Wind > GZ (which it is to left of eq)
// Area B (Restoring Energy): Area between GZ and lw2 from phi_eq2 to phi2
// B = Integral(GZ - lw2) dPhi
let area_gz_b = ctx.area_under_curve(phi_eq2, phi2);
let area_wind_b = lw2 * (phi2 - phi_eq2) * 3.14159 / 180.0;
let area_b = area_gz_b - area_wind_b; // Positive if GZ > Wind (which it is to right of eq)
// Ensure non-negative (logic check)
if area_a < 0.0 { area_a = 0.0; }
if area_b < 0.0 { area_b = 0.0; }
let margin = area_b - area_a;
let c_res = criterion("3.2.2.2 Weather", "Area b >= Area a", area_a, area_b, "m.rad");
let calc_note = if roll_calculated { "(Calculated)" } else { "(User)" };
c_res.notes = "Roll=" + roll_deg.toFixed(1) + "° " + calc_note;
c_res.notes += ", lw1=" + lw1.toFixed(3) + "m";
c_res.notes += ", lw2=" + lw2.toFixed(3) + "m";
c_res.notes += ", phi0=" + phi0.toFixed(1) + "°";
c_res.notes += ", phi_eq2=" + phi_eq2.toFixed(1) + "°";
results.push(c_res);
// Plots... (Simplified for template)
c_res.plot_id = "weather_plot";
let elements = [];
let heels = ctx.get_heels();
let gz_vals = ctx.get_gz_values();
elements.push(#{ type: "Curve", name: "GZ", x: heels, y: gz_vals });
elements.push(#{ type: "HorizontalLine", name: "lw1", y: lw1, color: "green", style: "dashed" });
elements.push(#{ type: "HorizontalLine", name: "lw2", y: lw2, color: "red" });
elements.push(#{ type: "VerticalLine", name: "phi1", x: phi1, y_max: lw2, style: "dotted" });
elements.push(#{ type: "VerticalLine", name: "phi_eq2", x: phi_eq2, y_max: lw2, style: "dotted" });
elements.push(#{ type: "VerticalLine", name: "phi2", x: phi2, y_max: lw2, style: "dotted" });
elements.push(#{ type: "VerticalLine", name: "phi2", x: phi2, y_max: lw2, style: "dotted" });
// Add Filled Areas
// Area A: phi1 to phi_eq2
let data_a = make_area_data(ctx, phi1, phi_eq2, lw2, 20);
let area_a_plot = #{
type: "FilledArea",
name: "Area A",
x: data_a.x,
y_lower: data_a.y_lower,
y_upper: data_a.y_upper,
color: "red",
alpha: 0.3
};
elements.push(area_a_plot);
// Area B: phi_eq2 to phi2
let data_b = make_area_data(ctx, phi_eq2, phi2, lw2, 20);
let area_b_plot = #{
type: "FilledArea",
name: "Area B",
x: data_b.x,
y_lower: data_b.y_lower,
y_upper: data_b.y_upper,
color: "green",
alpha: 0.3
};
elements.push(area_b_plot);
let weather_plot = #{
id: "weather_plot", title: "Weather Criterion", x_label: "Heel", y_label: "Lever",
elements: elements
};
// Return result
#{
regulation_name: "IS Code 2008",
vessel_name: ctx.get_vessel_name(),
loading_condition: ctx.get_loading_condition(),
displacement: ctx.get_displacement(),
overall_pass: true, // simplified logic
criteria: results,
plots: [weather_plot] // + main plot
}
} else {
// Error finding equilibrium
results.push(#{ name: "Weather Error", status: "FAIL", notes: "No equilibrium found for lw1" });
#{ regulation_name: "IS Code 2008", criteria: results }
}
} else {
// No wind data
#{ regulation_name: "IS Code 2008", criteria: results, notes: "No wind data" }
}
}