Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/workflows/on-release-main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ name: release-main
on:
release:
types: [published]
branches: [main]

jobs:
set-version:
Expand Down
4 changes: 3 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,11 @@ Please note this documentation assumes you already have `uv` and `Git` installed
Then, install and activate the environment with:

```bash
uv sync
uv sync --all-extras
```

Using `--all-extras` ensures that all optional dependencies, including those for generating test data (`pymeshup`), are installed.

4. Install pre-commit to run linters/formatters at commit time:

```bash
Expand Down
5 changes: 4 additions & 1 deletion Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -122,14 +122,17 @@ generate-ship-rotation:
@uv run python examples/defraction_box.py --output-dir examples --file-base boxship --only-base; exit 0

# Run fleetmaster examples
fleetmaster-all: fleetmaster-full fleetmaster-half
fleetmaster-all: fleetmaster-full fleetmaster-half fleetmaster-rotation
fleetmaster-full: generate-box-mesh-full
@fleetmaster -v run --settings-file examples/settings_full.yml --lid; exit 0
fleetmaster-half: generate-box-mesh-half
@fleetmaster -v run --settings-file examples/settings_half.yml; exit 0
fleetmaster-rotation: generate-ship-rotation
@fleetmaster -v run --settings-file examples/settings_rotations.yml; exit 0

fitting-example:
@uv run python examples/fitting_example.py

# clean examples directory
clean-examples: clean-examples-stl clean-examples-hdf5
# clean examples stl files
Expand Down
Empty file added ci.yml
Empty file.
80 changes: 77 additions & 3 deletions examples/defraction_box.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@
import argparse
from pathlib import Path

import numpy as np
import trimesh
from pymeshup import Box
from trimesh.transformations import compose_matrix

# Constants for the box dimensions
BOX_LENGTH = 10
Expand All @@ -25,7 +28,13 @@
FILE_BASE = "defraction_box"


def main(grid_symmetry: bool, output_dir: Path, file_base: str, only_base: bool = False):
def main(
grid_symmetry: bool,
output_dir: Path,
file_base: str,
only_base: bool = False,
generate_fitting_meshes: bool = False,
):
"""
Generates STL meshes for a defraction box based on specified parameters.

Expand All @@ -35,6 +44,7 @@ def main(grid_symmetry: bool, output_dir: Path, file_base: str, only_base: bool
output_dir (Path): The directory where the generated STL files will be saved.
file_base (str): The base name for the generated STL files.
only_base (bool): If True, only the base mesh will be generated.
generate_fitting_meshes (bool): If True, generates specific STL files for the fitting example.
"""
if grid_symmetry:
print(f"Grid symmetry on with file base {file_base}")
Expand Down Expand Up @@ -67,13 +77,68 @@ def main(grid_symmetry: bool, output_dir: Path, file_base: str, only_base: bool
return

for draft in DRAFTS:
box_draft = box_base.move(z=-draft)
# Start from the original buoy and move it, similar to how box_base was created.
box_draft = box_buoy.move(x=-half_length, z=-draft)
box_draft = box_draft.cut_at_waterline()
box_draft_mesh = box_draft.regrid(pct=REGRID_PERCENTAGE)
box_draft_filename = output_dir / f"{file_base}_{draft}m.stl"
print(f"Saving draft mesh {box_draft_filename}")
box_draft_mesh.save(str(box_draft_filename))

if generate_fitting_meshes:
generate_fitting_stl_files(output_dir, file_base)


def generate_fitting_stl_files(output_dir: Path, file_base: str):
"""
Generates specific rotated and translated STL meshes required by settings_rotations.yml
for the fitting example. These meshes are based on the full 'boxship.stl' and then transformed.
"""
base_stl_path = output_dir / f"{file_base}.stl"
if not base_stl_path.exists():
print(
f"Error: Base mesh '{base_stl_path}' not found. "
"Please ensure it's generated first (e.g., by running with --only-base)."
)
return

print(f"\n--- Generating fitting example STL files based on '{base_stl_path.name}' ---")
loaded_mesh = trimesh.load(base_stl_path)

# trimesh.load can return a Scene object or None. We need a single Trimesh object.
if isinstance(loaded_mesh, trimesh.Scene):
# Combine all geometries in the scene into a single mesh
base_mesh_untransformed = loaded_mesh.dump(concatenate=True)
else:
base_mesh_untransformed = loaded_mesh

if not isinstance(base_mesh_untransformed, trimesh.Trimesh) or base_mesh_untransformed.is_empty:
print(f"Error: Failed to load a valid mesh from '{base_stl_path}'. Aborting fitting mesh generation.")
return

# The `translation` in the original settings_rotations.yml is the desired position of the mesh's geometric center
# relative to the database origin. We bake this transformation directly into the STL.
fitting_cases = [
("boxship_t_1_r_00_00_00.stl", -1.0, 0.0, 0.0, 0.0),
("boxship_t_2_r_00_00_00.stl", -2.0, 0.0, 0.0, 0.0),
("boxship_t_1_r_45_00_00.stl", -1.0, 45.0, 0.0, 0.0),
("boxship_t_1_r_00_10_00.stl", -1.0, 0.0, 10.0, 0.0),
("boxship_t_1_r_20_20_00.stl", -1.0, 20.0, 20.0, 0.0),
]

for filename, target_z_rel_db_origin, roll_deg, pitch_deg, yaw_deg in fitting_cases:
# The absolute translation is the target position relative to the database origin.
translation_vec = [0.0, 0.0, target_z_rel_db_origin]

transform_matrix = compose_matrix(angles=np.radians([roll_deg, pitch_deg, yaw_deg]), translate=translation_vec)

transformed_mesh = base_mesh_untransformed.copy()
transformed_mesh.apply_transform(transform_matrix)

output_path = output_dir / filename
transformed_mesh.export(str(output_path))
print(f"Generated: {output_path.name}")


if __name__ == "__main__":
parser = argparse.ArgumentParser(
Expand All @@ -97,7 +162,16 @@ def main(grid_symmetry: bool, output_dir: Path, file_base: str, only_base: bool
action="store_true",
help="Only generate the base mesh.",
)
parser.add_argument(
"--generate-fitting-meshes",
action="store_true",
help="Generate specific STL files for the fitting example based on settings_rotations.yml.",
)
args = parser.parse_args()
main(
grid_symmetry=args.grid_symmetry, output_dir=args.output_dir, file_base=args.file_base, only_base=args.only_base
grid_symmetry=args.grid_symmetry,
output_dir=args.output_dir,
file_base=args.file_base,
only_base=args.only_base,
generate_fitting_meshes=args.generate_fitting_meshes,
)
149 changes: 86 additions & 63 deletions examples/fitting_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,38 @@
logger = logging.getLogger(__name__)


def _run_and_print_test_case(
case_number: int,
description: str,
hdf5_path: Path,
target_translation: list[float],
target_rotation: list[float],
water_level: float,
expected_match: str,
note: str = "",
):
"""Runs a single fitting test case and prints the results."""
print(f"\n\n--- Running Test Case {case_number}: {description} ---")
logger.info(f"Searching for best match for translation={target_translation}, rotation={target_rotation}...\n")

best_match, distance = find_best_matching_mesh(
hdf5_path=hdf5_path,
target_translation=target_translation,
target_rotation=target_rotation,
water_level=water_level,
)

print(f"\n--- Result for Test Case {case_number} ---")
if best_match:
print(f"✅ Best match found: '{best_match}'")
print(f" - Minimized Chamfer Distance: {distance:.6f}")
print(f" - Expected match: '{expected_match}'")
if note:
print(f" - Note: {note}")
else:
print("❌ No match found.")


def run_fitting_example():
"""Runs the fitting example.

Expand All @@ -32,85 +64,76 @@ def run_fitting_example():
# the meshes in the database were generated (wetted surface).
water_level = 0.0

draft = 2.0

# --- Test Case 1: A transformation that should perfectly match an existing mesh ---
# We are looking for a mesh that corresponds to a Z-translation of -1.0,
# a roll of 20 degrees, and a pitch of 20 degrees.
# The database contains 'boxship_t_1_r_20_20_00.stl' with these exact parameters.
print("\n--- Running Test Case 1: Exact Match ---")
target_translation_1 = [0.0, 0.0, -draft]
target_rotation_1 = [20.0, 20.0, 0.0] # [roll, pitch, yaw]

logger.info(f"Searching for best match for translation={target_translation_1}, rotation={target_rotation_1}...\n")

best_match_1, distance_1 = find_best_matching_mesh(
_run_and_print_test_case(
case_number=1,
description="Exact Match Draft 1 meter",
hdf5_path=hdf5_path,
target_translation=target_translation_1,
target_rotation=target_rotation_1,
target_translation=[0.0, 0.0, -1.0],
target_rotation=[20.0, 20.0, 0.0],
water_level=water_level,
expected_match="boxship_t_1_r_20_20_00",
)

print("\n--- Result for Test Case 1 ---")
if best_match_1:
print(f"✅ Best match found: '{best_match_1}'")
print(f" - Minimized Chamfer Distance: {distance_1:.6f}")
print(" - Expected match: 'boxship_t_1_r_20_20_00'")
else:
print("❌ No match found.")

# --- Test Case 2: A transformation with irrelevant translations and rotations ---
# This case has the same core properties (Z-trans, X/Y-rot) as Case 1,
# but with added X/Y translation and a Z rotation (yaw).
# The optimization algorithm should ignore these and still find the same best match.
print("\n\n--- Running Test Case 2: Match with Noise ---")
target_translation_2 = [2.5, -4.2, -draft] # Added dx, dy
target_rotation_2 = [20.0, 20.0, 15.0] # Added yaw

logger.info(f"Searching for best match for translation={target_translation_2}, rotation={target_rotation_2}...\n")

best_match_2, distance_2 = find_best_matching_mesh(
_run_and_print_test_case(
case_number=2,
description="Match with Noise draft 1.0",
hdf5_path=hdf5_path,
target_translation=target_translation_2,
target_rotation=target_rotation_2,
target_translation=[2.5, -4.2, -1.1], # Added dx, dy and dz
target_rotation=[20.0, 20.0, 15.0], # Added yaw
water_level=water_level,
expected_match="boxship_t_1_r_20_20_00",
note="The distance should be very close to the distance in Case 1.",
)

print("\n--- Result for Test Case 2 ---")
if best_match_2:
print(f"✅ Best match found: '{best_match_2}'")
print(f" - Minimized Chamfer Distance: {distance_2:.6f}")
print(" - Expected match: 'boxship_t_1_r_20_20_00'")
print(" - Note: The distance should be very close to the distance in Case 1.")
else:
print("❌ No match found.")

# --- Test Case 3: A transformation with irrelevant translations and rotations ---
# This case has the same core properties (Z-trans, X/Y-rot) as Case 1,
# but with added X/Y translation and a Z rotation (yaw).
# this time, difference valeus for the z-translation and x,y rotation are assumed.
# the expected result should give a larger distance
print("\n\n--- Running Test Case 3: Match with Noise ---")
target_translation_3 = [2.5, -4.2, -draft * 1.2] # Added dx, dy AND dz
target_rotation_3 = [23.0, 19.0, 15.0] # Added yaw AND roll and pitch
# --- Test Case 2: A transformation with irrelevant translations and rotations ---
_run_and_print_test_case(
case_number=3,
description="Different Match with Noise draft 1.0",
hdf5_path=hdf5_path,
target_translation=[2.5, -4.2, -1.1], # Added dx, dy AND dz
target_rotation=[23.0, 19.0, 15.0], # Added yaw AND roll and pitch
water_level=water_level,
expected_match="boxship_t_1_r_00_00_00",
note="The distance should be larger than both case 1 and case 2.",
)

logger.info(f"Searching for best match for translation={target_translation_3}, rotation={target_rotation_3}...\n")
# --- Test Case 4: A transformation with different core properties ---
_run_and_print_test_case(
case_number=4,
description="Exact Match for draft 2.0",
hdf5_path=hdf5_path,
target_translation=[0.0, -0.0, -2],
target_rotation=[0.0, 0.0, 0.0],
water_level=water_level,
expected_match="boxship_t_2_r_00_00_00",
note="The distance should be zero.",
)

best_match_3, distance_3 = find_best_matching_mesh(
# --- Test Case 5: A transformation with different core properties ---
_run_and_print_test_case(
case_number=5,
description="Exact Match for draft 2.0 with deviation in xy plane and yaw",
hdf5_path=hdf5_path,
target_translation=target_translation_3,
target_rotation=target_rotation_3,
target_translation=[10.0, -20.0, -2],
target_rotation=[0.0, 0.0, 15.0],
water_level=water_level,
expected_match="boxship_t_2_r_00_00_00",
note="The distance should be zero.",
)

print("\n--- Result for Test Case 3 ---")
if best_match_2:
print(f"✅ Best match found: '{best_match_3}'")
print(f" - Minimized Chamfer Distance: {distance_3:.6f}")
print(" - Expected match: 'boxship_t_1_r_20_20_00'")
print(" - Note: The distance should be larger than both case 1 and case 2.")
else:
print("❌ No match found.")
# --- Test Case 6: A transformation with different core properties ---
_run_and_print_test_case(
case_number=6,
description="Match for draft 2.0 with noise",
hdf5_path=hdf5_path,
target_translation=[10.0, -20.0, -2.2],
target_rotation=[4.0, -1.0, 15.0],
water_level=water_level,
expected_match="boxship_t_2_r_00_00_00",
note="The distance should be larger than zero.",
)


if __name__ == "__main__":
Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,9 @@ skip_empty = true
[tool.coverage.run]
branch = true
source = ["src"]
omit = [
"tests/*",
]


# ========================================================================================
Expand Down
Loading
Loading