335 lines
11 KiB
Python
335 lines
11 KiB
Python
"""Tests for LoadingCondition feature."""
|
|
|
|
import json
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
|
|
class TestMassCategory:
|
|
"""Tests for MassCategory enum."""
|
|
|
|
def test_create_categories(self):
|
|
from navaltoolbox import MassCategory
|
|
ls = MassCategory.lightship()
|
|
dw = MassCategory.deadweight()
|
|
ot = MassCategory.other()
|
|
assert repr(ls) # Should not raise
|
|
assert repr(dw)
|
|
assert repr(ot)
|
|
|
|
|
|
class TestMassItem:
|
|
"""Tests for MassItem class."""
|
|
|
|
def test_create_with_defaults(self):
|
|
from navaltoolbox import MassItem
|
|
item = MassItem("Test", 1000.0, (10.0, 0.0, 5.0))
|
|
assert item.name == "Test"
|
|
assert item.mass == 1000.0
|
|
assert item.cog == (10.0, 0.0, 5.0)
|
|
|
|
def test_create_with_category(self):
|
|
from navaltoolbox import MassItem, MassCategory
|
|
item = MassItem(
|
|
"Lightship",
|
|
5_000_000.0,
|
|
(45.0, 0.0, 4.5),
|
|
MassCategory.lightship(),
|
|
)
|
|
assert item.name == "Lightship"
|
|
assert item.mass == 5_000_000.0
|
|
|
|
|
|
class TestLoadingCondition:
|
|
"""Tests for LoadingCondition class."""
|
|
|
|
def test_create(self):
|
|
from navaltoolbox import LoadingCondition
|
|
lc = LoadingCondition("Test Loading")
|
|
assert lc.name == "Test Loading"
|
|
assert lc.num_masses() == 0
|
|
assert lc.num_tank_overrides() == 0
|
|
|
|
def test_add_masses(self):
|
|
from navaltoolbox import LoadingCondition, MassCategory
|
|
lc = LoadingCondition("Test")
|
|
lc.add_mass_simple(
|
|
"Lightship", 5_000_000, (45.0, 0.0, 4.5), MassCategory.lightship()
|
|
)
|
|
lc.add_mass_simple("Crew", 3_000, (35.0, 0.0, 8.0))
|
|
assert lc.num_masses() == 2
|
|
masses = lc.get_masses()
|
|
assert len(masses) == 2
|
|
assert masses[0].name == "Lightship"
|
|
assert masses[1].name == "Crew"
|
|
|
|
def test_remove_mass(self):
|
|
from navaltoolbox import LoadingCondition
|
|
lc = LoadingCondition("Test")
|
|
lc.add_mass_simple("A", 1000, (0.0, 0.0, 0.0))
|
|
lc.add_mass_simple("B", 2000, (0.0, 0.0, 0.0))
|
|
assert lc.num_masses() == 2
|
|
assert lc.remove_mass("A") is True
|
|
assert lc.num_masses() == 1
|
|
assert lc.remove_mass("X") is False
|
|
|
|
def test_tank_fill_overrides(self):
|
|
from navaltoolbox import LoadingCondition
|
|
lc = LoadingCondition("Test")
|
|
lc.set_tank_fill_percent("FO_1P", 95.0)
|
|
lc.set_tank_fill("FW_1", 0.5)
|
|
assert lc.num_tank_overrides() == 2
|
|
fills = lc.get_tank_fills()
|
|
assert abs(fills["FO_1P"] - 0.95) < 1e-6
|
|
assert abs(fills["FW_1"] - 0.5) < 1e-6
|
|
|
|
def test_apply_and_resolve(self):
|
|
"""Test apply/resolve with a vessel that has tanks."""
|
|
from navaltoolbox import LoadingCondition, Hull, Vessel, Tank
|
|
|
|
hull = Hull.from_box(length=100.0, breadth=20.0, depth=10.0)
|
|
vessel = Vessel(hull)
|
|
|
|
# Add a tank: 10x10x2 = 200 m³, density 1000 kg/m³
|
|
tank = Tank.from_box("FO_1P", 20.0, 30.0, -5.0, 5.0, 0.0, 2.0, 1000.0)
|
|
tank.fill_percent = 0.0 # Start empty
|
|
vessel.add_tank(tank)
|
|
|
|
lc = LoadingCondition("Test")
|
|
lc.add_mass_simple("Lightship", 100_000, (50.0, 0.0, 5.0))
|
|
lc.set_tank_fill_percent("FO_1P", 50.0)
|
|
|
|
# Apply and resolve
|
|
lc.apply(vessel)
|
|
|
|
# Tank should now be 50% filled
|
|
tanks = vessel.get_tanks()
|
|
assert abs(tanks[0].fill_percent - 50.0) < 1e-6
|
|
|
|
disp, cog = lc.resolve(vessel)
|
|
# Total = 100_000 + tank_mass (~100_000 kg for 100m³ at 1000 kg/m³)
|
|
assert disp > 100_000 # Must include tank mass
|
|
assert cog[0] > 0 # LCG should be positive
|
|
|
|
# Test item-only resolution (excluding tanks)
|
|
item_disp, item_cog = lc.resolve_items()
|
|
assert item_disp == 100_000
|
|
assert item_cog[0] == 50.0
|
|
|
|
def test_unaffected_tanks_keep_fill(self):
|
|
"""Tanks not in the override list must keep their current fill."""
|
|
from navaltoolbox import LoadingCondition, Hull, Vessel, Tank
|
|
|
|
hull = Hull.from_box(length=100.0, breadth=20.0, depth=10.0)
|
|
vessel = Vessel(hull)
|
|
|
|
tank1 = Tank.from_box("FO_1P", 20.0, 30.0, -5.0, 0.0, 0.0, 2.0, 1000.0)
|
|
tank1.fill_percent = 80.0
|
|
vessel.add_tank(tank1)
|
|
|
|
tank2 = Tank.from_box("FW_1", 40.0, 50.0, -5.0, 5.0, 0.0, 2.0, 1000.0)
|
|
tank2.fill_percent = 60.0
|
|
vessel.add_tank(tank2)
|
|
|
|
# Only override FO_1P, FW_1 should stay at 60%
|
|
lc = LoadingCondition("Test")
|
|
lc.add_mass_simple("Lightship", 100_000, (50.0, 0.0, 5.0))
|
|
lc.set_tank_fill_percent("FO_1P", 10.0)
|
|
lc.apply(vessel)
|
|
|
|
tanks = vessel.get_tanks()
|
|
assert abs(tanks[0].fill_percent - 10.0) < 1e-6 # FO_1P changed
|
|
assert abs(tanks[1].fill_percent - 60.0) < 1e-6 # FW_1 unchanged
|
|
|
|
def test_json_serialization(self):
|
|
"""Test JSON round-trip."""
|
|
from navaltoolbox import LoadingCondition, MassCategory
|
|
|
|
lc = LoadingCondition("Departure")
|
|
lc.add_mass_simple(
|
|
"Lightship", 5_000_000, (45.0, 0.0, 4.5), MassCategory.lightship()
|
|
)
|
|
lc.add_mass_simple("Crew", 3_000, (35.0, 0.0, 8.0))
|
|
lc.set_tank_fill_percent("FO_1P", 95.0)
|
|
lc.set_tank_fill("FW_1", 0.5)
|
|
|
|
# Serialize
|
|
json_str = lc.to_json()
|
|
data = json.loads(json_str)
|
|
assert data["name"] == "Departure"
|
|
assert len(data["masses"]) == 2
|
|
assert "FO_1P" in data["tank_fills"]
|
|
|
|
# Deserialize
|
|
lc2 = LoadingCondition.from_json(json_str)
|
|
assert lc2.name == "Departure"
|
|
assert lc2.num_masses() == 2
|
|
assert lc2.num_tank_overrides() == 2
|
|
|
|
def test_json_file_io(self):
|
|
"""Test JSON file save/load."""
|
|
from navaltoolbox import LoadingCondition
|
|
|
|
lc = LoadingCondition("Test File")
|
|
lc.add_mass_simple("Test", 1000.0, (10.0, 0.0, 5.0))
|
|
|
|
with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as f:
|
|
path = f.name
|
|
|
|
try:
|
|
lc.save_json(path)
|
|
lc2 = LoadingCondition.load_json(path)
|
|
assert lc2.name == "Test File"
|
|
assert lc2.num_masses() == 1
|
|
finally:
|
|
Path(path).unlink(missing_ok=True)
|
|
|
|
def test_from_csv(self):
|
|
"""Test deserializing from a CSV string."""
|
|
from navaltoolbox import LoadingCondition
|
|
|
|
csv_data = (
|
|
"Type,Name,Mass,LCG,TCG,VCG,Category,FillPercent\n"
|
|
"Mass,Lightship,1000.0,10.0,0.0,2.0,Lightship,\n"
|
|
"Mass,Cargo,500.0,15.0,0.0,3.0,Deadweight,\n"
|
|
"Tank,Tank_1,,,,,,85.5\n"
|
|
)
|
|
lc = LoadingCondition.from_csv(csv_data)
|
|
|
|
assert lc.name == "Imported Loading Condition"
|
|
assert lc.num_masses() == 2
|
|
assert lc.num_tank_overrides() == 1
|
|
|
|
masses = lc.get_masses()
|
|
assert masses[0].name == "Lightship"
|
|
assert masses[0].mass == 1000.0
|
|
|
|
tanks = lc.get_tank_fills()
|
|
assert tanks["Tank_1"] == 0.855
|
|
|
|
def test_csv_file_io(self):
|
|
"""Test CSV file load."""
|
|
from navaltoolbox import LoadingCondition
|
|
|
|
csv_data = (
|
|
"Type,Name,Mass,LCG,TCG,VCG,Category,FillPercent\n"
|
|
"Mass,Test_Mass,2000.0,10.0,0.0,5.0,Other,\n"
|
|
)
|
|
|
|
with tempfile.NamedTemporaryFile(suffix=".csv", delete=False) as f:
|
|
path = f.name
|
|
f.write(csv_data.encode("utf-8"))
|
|
|
|
try:
|
|
lc = LoadingCondition.load_csv(path)
|
|
# The name becomes the file stem
|
|
assert lc.num_masses() == 1
|
|
assert lc.get_masses()[0].name == "Test_Mass"
|
|
finally:
|
|
Path(path).unlink(missing_ok=True)
|
|
|
|
def test_copy(self):
|
|
"""Test copy with optional new name."""
|
|
from navaltoolbox import LoadingCondition
|
|
|
|
lc = LoadingCondition("Original")
|
|
lc.add_mass_simple("A", 1000, (0.0, 0.0, 0.0))
|
|
lc.set_tank_fill_percent("FO_1P", 95.0)
|
|
|
|
# Copy without name -> keeps original
|
|
lc2 = lc.copy()
|
|
assert lc2.name == "Original"
|
|
assert lc2.num_masses() == 1
|
|
|
|
# Copy with new name
|
|
lc3 = lc.copy("Arrival")
|
|
assert lc3.name == "Arrival"
|
|
assert lc3.num_masses() == 1
|
|
assert lc3.num_tank_overrides() == 1
|
|
|
|
def test_set_name(self):
|
|
from navaltoolbox import LoadingCondition
|
|
lc = LoadingCondition("Original")
|
|
lc.name = "Modified"
|
|
assert lc.name == "Modified"
|
|
|
|
def test_from_loading_restores_fills(self):
|
|
"""Test that from_loading methods restore the tank fills."""
|
|
from navaltoolbox import (
|
|
LoadingCondition,
|
|
Hull,
|
|
Vessel,
|
|
Tank,
|
|
HydrostaticsCalculator,
|
|
)
|
|
|
|
hull = Hull.from_box(length=100.0, breadth=20.0, depth=10.0)
|
|
vessel = Vessel(hull)
|
|
tank = Tank.from_box("FO_1P", 20.0, 30.0, -5.0, 5.0, 0.0, 2.0, 1000.0)
|
|
tank.fill_percent = 25.0
|
|
vessel.add_tank(tank)
|
|
|
|
lc = LoadingCondition("Test")
|
|
lc.add_mass_simple("Lightship", 100_000, (50.0, 0.0, 5.0))
|
|
lc.set_tank_fill_percent("FO_1P", 100.0)
|
|
|
|
calc = HydrostaticsCalculator(vessel)
|
|
_ = calc.from_loading(lc)
|
|
|
|
# Tank should be restored to 25.0%
|
|
assert abs(vessel.get_tanks()[0].fill_percent - 25.0) < 1e-6
|
|
|
|
def test_gz_curve_from_loading(self):
|
|
"""Test gz_curve matches manual resolve_items workflow."""
|
|
from navaltoolbox import (
|
|
LoadingCondition,
|
|
Hull,
|
|
Vessel,
|
|
Tank,
|
|
StabilityCalculator,
|
|
)
|
|
|
|
hull = Hull.from_box(length=100.0, breadth=20.0, depth=10.0)
|
|
vessel = Vessel(hull)
|
|
tank = Tank.from_box("FO_1P", 20.0, 30.0, -5.0, 5.0, 0.0, 2.0, 1000.0)
|
|
vessel.add_tank(tank)
|
|
|
|
lc = LoadingCondition("Test")
|
|
lc.add_mass_simple("Lightship", 3_000_000, (50.0, 0.0, 5.0))
|
|
lc.set_tank_fill_percent("FO_1P", 50.0)
|
|
|
|
calc = StabilityCalculator(vessel)
|
|
heels = [0.0, 10.0, 20.0]
|
|
|
|
curve = calc.gz_curve_from_loading(lc, heels)
|
|
assert len(curve.values()) == 3
|
|
|
|
def test_from_loading_hydrostatics(self):
|
|
"""Test from_loading matches manual resolve workflow."""
|
|
from navaltoolbox import (
|
|
LoadingCondition,
|
|
Hull,
|
|
Vessel,
|
|
Tank,
|
|
HydrostaticsCalculator,
|
|
)
|
|
|
|
hull = Hull.from_box(length=100.0, breadth=20.0, depth=10.0)
|
|
vessel = Vessel(hull)
|
|
tank = Tank.from_box("FO_1P", 20.0, 30.0, -5.0, 5.0, 0.0, 2.0, 1000.0)
|
|
vessel.add_tank(tank)
|
|
|
|
lc = LoadingCondition("Test")
|
|
lc.add_mass_simple("Lightship", 5_000_000, (50.0, 0.0, 5.0))
|
|
lc.set_tank_fill_percent("FO_1P", 80.0)
|
|
|
|
calc = HydrostaticsCalculator(vessel)
|
|
state1 = calc.from_loading(lc)
|
|
|
|
# Manual apply and resolve
|
|
lc.apply(vessel)
|
|
disp, cog = lc.resolve(vessel)
|
|
state2 = calc.from_displacement(displacement_mass=disp, cog=cog)
|
|
|
|
assert abs(state1.draft - state2.draft) < 1e-6
|