Lab 6 — Compute GM, then break it¶
In lessons 17–23 you learned the physics. Here you close the loop: compute KB, BM, KG, and GM by hand for a small ship, predict the verdict before running the tool, then deliberately overload the deck until the tool reports IMO NON-COMPLIANT. Understanding the exact moment a ship tips from stable to unstable — and watching CargoForge-C catch it — is the best way to make those equations stick.
Setup¶
From the repository root, build the binary if you have not already:
You should see a cargoforge binary appear in the project root. Verify it works:
Create a working directory for this lab:
Part 1 — Write the ship config¶
This lab uses a narrow coastal freighter — narrow beam means a smaller waterplane moment of inertia, so BM is modest and GM is easy to push negative.
Create /tmp/lab06/ship.cfg:
# Lab 06 ship — narrow coastal freighter
length_m=60
width_m=12
max_weight_tonnes=20000
lightship_weight_tonnes=3000
lightship_kg_m=7.0
Key numbers to remember: \(L = 60\ \text{m}\), \(B = 12\ \text{m}\), lightship \(= 3000\ \text{t}\) with its centre of gravity at \(KG_{\text{ls}} = 7.0\ \text{m}\) above keel.
Part 2 — Hand-calculate GM for the baseline cargo¶
Create a single-item cargo manifest /tmp/lab06/cargo_baseline.txt:
Before running anything, work through the arithmetic. The box-hull model
(perform_analysis in src/analysis.c) uses these constants:
| Constant | Value | Meaning |
|---|---|---|
| \(\rho_{\text{sw}}\) | 1.025 t/m³ | Seawater density |
| \(C_b\) | 0.75 | Block coefficient |
| \(C_w\) | 0.85 | Waterplane area coefficient |
| \(KB\_FACTOR\) | 0.53 | \(KB \approx 0.53 \times T\) |
Step-by-step¶
Displacement and volume
Draft (box-hull fallback)
Vertical centre of buoyancy
Transverse metacentric radius
Vertical centre of gravity
The placement engine (place_cargo_3d in src/placement_3d.c) puts DeepHoldItem
into the ForwardHold bin. That hold has its floor at \(z = -8\ \text{m}\).
An item of height \(h = 4\ \text{m}\) sitting on the floor has its centroid at:
The analysis code (src/analysis.c, line 99 and 115) sums the vertical moment
over lightship plus every placed cargo item:
GM
The IMO minimum is \(GM \geq 0.15\ \text{m}\) (IMO_GM_MIN in src/analysis.c). Your
predicted verdict: compliant.
Part 3 — Verify against the tool¶
Run the optimizer on your baseline files:
Scan the output for the stability block. You should see something like:
The tool's GM should match your hand-calculated 0.359 m to within rounding.
No free-surface correction yet
The baseline has no tank CSV, so ship->tanks is NULL and free_surface_correction
is exactly 0.0. That is why the hand calc and the tool agree precisely. Lab 8 adds
tanks; the correction term becomes non-trivial there.
You can also inspect cargo positions with the info subcommand:
Confirm DeepHoldItem shows a negative \(z\) position (inside the hold, below waterline).
Part 4 — Raise the cargo; watch GM fall¶
Now create a heavier, higher cargo manifest /tmp/lab06/cargo_heavy_deck.txt:
Before running it, predict the outcome. DeckPile is heavy — the optimizer will
try holds first, but ForwardHold has max_weight = 0.30 × 20000 = 6000\ \text{t},
so weight is not the constraint that forces it onto the Deck bin. What matters
is that this manifest is intentionally designed to be placed on deck: when you
inspect the output you will see it there. The Deck bin floor is at \(z = 0\ \text{m}\).
Centroid of DeckPile on the deck floor:
Repeat the calculation with \(W_{\text{cargo}} = 1000\ \text{t}\), \(\Delta = 4000\ \text{t}\):
\(GM\) is negative. The ship is unstable — it will capsize rather than self-right. Even the GZ curve already fails: the wall-sided formula gives
That is below the IMO threshold of 0.20 m.
Run the tool to confirm:
Expected output excerpt:
Why adding MORE cargo can hurt stability
You added 500 t more cargo and GM dropped from +0.359 m to −0.038 m. This feels counterintuitive — more displacement should help, right? The problem is WHERE the weight is. The deck cargo raised \(KG\) faster than the extra displacement raised \(KB\). The increase in \(BM\) from the higher displacement partially compensated, but not enough. This is the core reason stability booklets specify maximum deck-load heights, not just weights.
Part 5 — Use validate to see the rejection without placing cargo¶
The validate subcommand parses files and reports structural problems, but does
not run the placement engine. It will not report a stability verdict (placement
must happen first), but it confirms the manifest is well-formed:
Expected: no parse errors, exit code 0. The non-compliance is a physics result
of placement, not a format error — only optimize catches it.
Part 6 — Run the test suite and sanitizers¶
The stability calculations you just verified by hand are covered by test_analysis.
Run the full suite:
All 8 test binaries should exit clean. Now add AddressSanitizer and Undefined Behaviour Sanitizer:
ASan instruments every heap allocation and catches use-after-free bugs (one of
which — the heap-use-after-free in parse_cargo_list — was caught exactly this
way during development; see docs/BUG_JOURNAL.md for the entry). UBSan catches
signed integer overflow and misaligned accesses in the floating-point arithmetic
paths that compute KB, BM, and KG.
Part 7 — (Optional) JSON output¶
The same optimize run accepts a --format json flag:
./cargoforge optimize --ship /tmp/lab06/ship.cfg \
--cargo /tmp/lab06/cargo_heavy_deck.txt \
--format json | python3 -m json.tool | grep -E '"gm|"kb|"bm|"kg|"imo'
You will see the individual kb, bm, kg, gm, gm_corrected, and
imo_compliant fields from AnalysisResult serialized by json_output.c.
imo_compliant will be 0.
Solution¶
Full command sequence for both scenarios:
# Build
make
# Create files
mkdir -p /tmp/lab06
cat > /tmp/lab06/ship.cfg <<'EOF'
length_m=60
width_m=12
max_weight_tonnes=20000
lightship_weight_tonnes=3000
lightship_kg_m=7.0
EOF
cat > /tmp/lab06/cargo_baseline.txt <<'EOF'
DeepHoldItem 500 10x8x4 standard
EOF
cat > /tmp/lab06/cargo_heavy_deck.txt <<'EOF'
DeckPile 1000 10x8x4 standard
EOF
# Baseline — COMPLIANT, GM ≈ 0.36 m
./cargoforge optimize --ship /tmp/lab06/ship.cfg --cargo /tmp/lab06/cargo_baseline.txt
# Heavy deck — NON-COMPLIANT, GM ≈ -0.04 m
./cargoforge optimize --ship /tmp/lab06/ship.cfg --cargo /tmp/lab06/cargo_heavy_deck.txt
# Tests
make test
make test-asan
Expected key values:
| Scenario | \(T\) (m) | \(KB\) (m) | \(BM\) (m) | \(KG\) (m) | \(GM\) (m) | IMO |
|---|---|---|---|---|---|---|
| 500 t in hold | 6.32 | 3.35 | 2.15 | 5.14 | +0.36 | COMPLIANT |
| 1000 t on deck | 7.23 | 3.83 | 1.88 | 5.75 | −0.04 | NON-COMPLIANT |
Recap¶
- \(GM = KB + BM - KG\). A ship becomes unstable the moment \(GM\) drops below zero; IMO mandates \(GM \geq 0.15\ \text{m}\) as a minimum safety margin.
- \(BM = I_T / \nabla\). A narrow beam means a small \(I_T\) and therefore a small \(BM\) — less reserve against a rising \(KG\).
- Adding cargo does not automatically improve stability. Deck cargo raises \(KG\) faster than it raises \(KB\), and the increase in \(BM\) from extra displacement rarely compensates.
- CargoForge-C's
perform_analysisinsrc/analysis.cevaluates all six IMO intact-stability criteria;imo_compliantis 0 if any single criterion fails. make test-asanis the way to run the same crash-detection the development team uses; it found the heap-use-after-free inparse_cargo_listthat is documented indocs/BUG_JOURNAL.md.
Next: Config and manifest formats.