From bb66b1563d96a7c947b067b228ecbad4243f32aa Mon Sep 17 00:00:00 2001 From: Vamsi Subraveti Date: Tue, 11 Nov 2025 10:54:20 -0600 Subject: [PATCH 1/4] testing suite for api endpoints and core. --- pyproject.toml | 3 +- src/raptor/structures.py | 19 ++ tests/__init__.py | 10 + tests/test_api.py | 532 +++++++++++++++++++++++++++++++++ tests/test_core.py | 619 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 1182 insertions(+), 1 deletion(-) create mode 100644 tests/__init__.py create mode 100644 tests/test_api.py create mode 100644 tests/test_core.py diff --git a/pyproject.toml b/pyproject.toml index 2fa0a21..20caf4e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,8 @@ dependencies = [ "numba", "PyYAML", "vtk", - "scikit-image" + "scikit-image", + "pytest" ] [project.scripts] diff --git a/src/raptor/structures.py b/src/raptor/structures.py index 3d0b8c2..82b32a1 100644 --- a/src/raptor/structures.py +++ b/src/raptor/structures.py @@ -183,15 +183,34 @@ def __init__( bound_box: Optional[np.ndarray] = None, path_vectors: Optional[List[PathVector]] = None, ): + if voxel_resolution <= 0.0: + raise ValueError( + "Voxel resolution must be a positive non-zero value." + ) self.resolution = voxel_resolution if bound_box is not None: # Option 1: Grid is constructed from a user-defined bounding box. + if bound_box.shape != (2, 3): + raise ValueError( + "Bounding box must be of shape (2, 3) " + "representing [[x0, y0, z0], [x1, y1, z1]]." + ) + if np.any(bound_box[1] <= bound_box[0]): + raise ValueError( + "Invalid bounding box: " + "Maximum corner must be greater than minimum corner." + ) gx0, gy0, gz0 = bound_box[0] gx1, gy1, gz1 = bound_box[1] elif path_vectors is not None: # Option 2: Grid is constructed from boundaries of path vectors. + for pv in path_vectors: + if not isinstance(pv, PathVector): + raise ValueError( + "All elements in 'path_vectors' must be of type PathVector." + ) all_points = np.vstack( [p.start_point for p in path_vectors] + [p.end_point for p in path_vectors] diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..d572071 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,10 @@ +# ============================================================================= +# Copyright (c) 2025 Oak Ridge National Laboratory +# +# All rights reserved. +# +# This file is part of Raptor. +# +# For details, see the top-level LICENSE file at: +# https://github.com/ORNL-MDF/Raptor/LICENSE +# ============================================================================= diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 0000000..0df0836 --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,532 @@ +# ============================================================================= +# Copyright (c) 2025 Oak Ridge National Laboratory +# +# All rights reserved. +# +# This file is part of Raptor. +# +# For details, see the top-level LICENSE file at: +# https://github.com/ORNL-MDF/Raptor/LICENSE +# ============================================================================= +""" +Test suite for raptor.api module. + +This module contains unit tests for all public API functions in the raptor.api module, +including grid creation, path vector generation, spectral component computation, +melt pool creation, porosity computation, and VTK output generation. +""" + +import pytest +import numpy as np +import tempfile +import os +from pathlib import Path +from typing import List, Dict, Any + +# Import the module under test +from raptor.api import ( + create_grid, + create_path_vectors, + compute_spectral_components, + create_melt_pool, + compute_porosity, + write_vtk, + compute_morphology, + write_morphology, +) +from raptor.structures import Grid, PathVector, MeltPool + + +# ============================================================================= +# Fixtures +# ============================================================================= + +@pytest.fixture +def sample_bound_box(): + """Fixture providing a sample bounding box for testing.""" + return np.array([ + [0.0, 0.0, 0.0], # min point + [1.0, 1.0, 0.5] # max point + ]) + + +@pytest.fixture +def sample_voxel_resolution(): + """Fixture providing a sample voxel resolution.""" + return 0.01 + +@pytest.fixture +def sample_path_vectors(): + """Fixture providing sample path vectors.""" + start_point = np.array([0.0, 0.0, 0.0]) + end_point = np.array([1.0, 1.0, 0.0]) + start_time = 0.0 + end_time = 1.0 + path_vector = PathVector( + start_point=start_point, + end_point=end_point, + start_time=start_time, + end_time=end_time + ) + return [path_vector] + +@pytest.fixture +def sample_process_parameters(): + """Fixture providing sample process parameters.""" + return { + 'power': 200.0, + 'scan_speed': 1.0, + 'hatch_spacing': 0.1, + 'layer_height': 0.05, + 'rotation': 67.0, + 'scan_extension': 0.1, + 'extra_layers': 0 + } + + +@pytest.fixture +def sample_time_series_data(): + """Fixture providing sample time series data for melt pool.""" + t = np.linspace(0, 1, 100) + values = 0.0001 + 0.00002 * np.sin(2 * np.pi * 5 * t) + return np.column_stack([t, values]) + + +@pytest.fixture +def sample_spectral_components(): + """Fixture providing sample spectral components.""" + # Format: [amplitude, frequency, phase] + return np.array([ + [0.0001, 0.0, 0.0], # mode 0 (mean) + [0.00002, 5.0, 0.0], # mode 1 + [0.00001, 10.0, np.pi/2] # mode 2 + ], dtype=np.float64) + + +@pytest.fixture +def sample_melt_pool_dict(sample_time_series_data): + """Fixture providing a sample melt pool dictionary.""" + return { + 'width': (sample_time_series_data, 3, 1.0, 2.0), + 'depth': (sample_time_series_data, 3, 1.0, 2.0), + 'height': (sample_time_series_data, 3, 1.0, 2.0) + } + + +@pytest.fixture +def temp_output_dir(): + """Fixture providing a temporary directory for output files.""" + with tempfile.TemporaryDirectory() as tmpdir: + yield Path(tmpdir) + + +# ============================================================================= +# Tests for create_grid +# ============================================================================= + +class TestCreateGrid: + """Test cases for the create_grid function.""" + + def test_create_grid_with_bound_box(self, sample_voxel_resolution, sample_bound_box): + """Test grid creation with a bounding box.""" + grid = create_grid(sample_voxel_resolution, bound_box=sample_bound_box) + + assert isinstance(grid, Grid) + assert grid.resolution == sample_voxel_resolution + assert grid.origin.shape == (3,) + assert grid.shape[0] * grid.shape[1] * grid.shape[2] > 0 + assert grid.voxels.shape == (grid.shape[0] * grid.shape[1] * grid.shape[2], 3) + + def test_create_grid_with_path_vectors(self, sample_voxel_resolution, sample_path_vectors): + """Test grid creation with path vectors.""" + grid = create_grid(sample_voxel_resolution, path_vectors=sample_path_vectors) + + + assert isinstance(grid, Grid) + assert grid.resolution == sample_voxel_resolution + assert np.all(grid.origin == np.array([0.0, 0.0, 0.0])) + + def test_create_grid_invalid_resolution(self, bound_box=sample_bound_box): + """Test grid creation with invalid voxel resolution.""" + with pytest.raises(ValueError): + create_grid(-0.01, bound_box=bound_box) + + def test_create_grid_invalid_bound_box(self, sample_voxel_resolution): + """Test grid creation with invalid bounding box.""" + invalid_bound_box = np.array([[0, 0, 0], [1, -1, 1]]) + with pytest.raises(ValueError): + create_grid(sample_voxel_resolution, bound_box=invalid_bound_box) + + def test_create_grid_invalid_path_vectors(self, sample_voxel_resolution): + """Test grid creation with invalid path vectors.""" + invalid_path_vectors = [123, "invalid", None] + with pytest.raises(ValueError): + create_grid(sample_voxel_resolution, path_vectors=invalid_path_vectors) + + +# ============================================================================= +# Tests for create_path_vectors +# ============================================================================= + +class TestCreatePathVectors: + """Test cases for the create_path_vectors function.""" + + def test_create_path_vectors_basic(self, sample_bound_box, sample_process_parameters): + """Test basic path vector generation.""" + path_vectors = create_path_vectors( + sample_bound_box, + **sample_process_parameters + ) + + assert isinstance(path_vectors, list) + assert len(path_vectors) > 0 + assert all(isinstance(pv, PathVector) for pv in path_vectors) + + def test_create_path_vectors_single_layer(self, sample_bound_box, sample_process_parameters): + """Test path vector generation for a single layer.""" + params = sample_process_parameters.copy() + params['extra_layers'] = 0 + + path_vectors = create_path_vectors(sample_bound_box, **params) + + # TODO: Verify single layer generation + assert len(path_vectors) > 0 + + def test_create_path_vectors_multiple_layers(self, sample_bound_box, sample_process_parameters): + """Test path vector generation for multiple layers.""" + params = sample_process_parameters.copy() + params['extra_layers'] = 3 + + path_vectors = create_path_vectors(sample_bound_box, **params) + + # TODO: Verify multiple layer generation + assert len(path_vectors) > 0 + + def test_create_path_vectors_rotation(self, sample_bound_box, sample_process_parameters): + """Test path vector generation with rotation.""" + # TODO: Test different rotation angles + pass + + def test_create_path_vectors_hatch_spacing(self, sample_bound_box, sample_process_parameters): + """Test path vector generation with different hatch spacings.""" + # TODO: Test effect of hatch spacing on vector count + pass + + def test_create_path_vectors_invalid_parameters(self, sample_bound_box): + """Test path vector generation with invalid parameters.""" + # TODO: Test with negative or invalid values + pass + + +# ============================================================================= +# Tests for compute_spectral_components +# ============================================================================= + +class TestComputeSpectralComponents: + """Test cases for the compute_spectral_components function.""" + + def test_compute_spectral_components_basic(self, sample_time_series_data): + """Test basic spectral component computation.""" + n_modes = 3 + spectral_array = compute_spectral_components(sample_time_series_data, n_modes) + + assert isinstance(spectral_array, np.ndarray) + assert spectral_array.shape == (n_modes, 3) + assert spectral_array.dtype == np.float64 + + def test_compute_spectral_components_single_mode(self, sample_time_series_data): + """Test spectral component computation with single mode.""" + n_modes = 1 + spectral_array = compute_spectral_components(sample_time_series_data, n_modes) + + assert spectral_array.shape == (1, 3) + assert spectral_array[0, 1] == 0 # frequency should be 0 + assert spectral_array[0, 2] == 0 # phase should be 0 + + def test_compute_spectral_components_multiple_modes(self, sample_time_series_data): + """Test spectral component computation with multiple modes.""" + for n_modes in [2, 5, 10]: + spectral_array = compute_spectral_components(sample_time_series_data, n_modes) + assert spectral_array.shape == (n_modes, 3) + + def test_compute_spectral_components_mean_value(self, sample_time_series_data): + """Test that mode 0 matches the mean of input data.""" + spectral_array = compute_spectral_components(sample_time_series_data, 3) + expected_mean = sample_time_series_data[:, 1].mean() + + np.testing.assert_allclose(spectral_array[0, 0], expected_mean) + + def test_compute_spectral_components_invalid_input(self): + """Test spectral component computation with invalid input.""" + # TODO: Test with malformed input data + pass + + +# ============================================================================= +# Tests for create_melt_pool +# ============================================================================= + +class TestCreateMeltPool: + """Test cases for the create_melt_pool function.""" + + def test_create_melt_pool_basic(self, sample_melt_pool_dict): + """Test basic melt pool creation.""" + melt_pool = create_melt_pool(sample_melt_pool_dict, enable_random_phases=False) + + assert isinstance(melt_pool, MeltPool) + assert melt_pool.enable_random_phases == False + + def test_create_melt_pool_random_phases(self, sample_melt_pool_dict): + """Test melt pool creation with random phases enabled.""" + melt_pool = create_melt_pool(sample_melt_pool_dict, enable_random_phases=True) + + assert melt_pool.enable_random_phases == True + + def test_create_melt_pool_spectral_input(self, sample_spectral_components): + """Test melt pool creation with spectral component input.""" + melt_pool_dict = { + 'width': (sample_spectral_components, 3, 1.0, 2.0), + 'depth': (sample_spectral_components, 3, 1.0, 2.0), + 'height': (sample_spectral_components, 3, 1.0, 2.0) + } + + melt_pool = create_melt_pool(melt_pool_dict, enable_random_phases=False) + + assert isinstance(melt_pool, MeltPool) + + def test_create_melt_pool_mode_padding(self): + """Test that melt pool correctly pads modes to match maximum.""" + # TODO: Create inputs with different mode counts + pass + + def test_create_melt_pool_scaling(self, sample_time_series_data): + """Test that scaling is correctly applied.""" + scale_factor = 2.0 + melt_pool_dict = { + 'width': (sample_time_series_data, 3, scale_factor, 2.0), + 'depth': (sample_time_series_data, 3, 1.0, 2.0), + 'height': (sample_time_series_data, 3, 1.0, 2.0) + } + + melt_pool = create_melt_pool(melt_pool_dict, enable_random_phases=False) + + # TODO: Verify scaling is applied correctly + assert isinstance(melt_pool, MeltPool) + + def test_create_melt_pool_shape_factors(self, sample_melt_pool_dict): + """Test that shape factors are correctly set.""" + melt_pool = create_melt_pool(sample_melt_pool_dict, enable_random_phases=False) + + assert melt_pool.width_shape_factor == 2.0 + assert melt_pool.depth_shape_factor == 2.0 + assert melt_pool.height_shape_factor == 2.0 + + def test_create_melt_pool_invalid_data_shape(self): + """Test melt pool creation with invalid data shape.""" + # TODO: Test with data that is not Nx2 or Nx3 + pass + + +# ============================================================================= +# Tests for compute_porosity +# ============================================================================= + +class TestComputePorosity: + """Test cases for the compute_porosity function.""" + + def test_compute_porosity_basic(self): + """Test basic porosity computation.""" + # TODO: Create minimal grid, path vectors, and melt pool + pass + + def test_compute_porosity_with_warmup(self): + """Test porosity computation with JIT warmup enabled.""" + # TODO: Test with jit_warmup=True + pass + + def test_compute_porosity_without_warmup(self): + """Test porosity computation with JIT warmup disabled.""" + # TODO: Test with jit_warmup=False + pass + + def test_compute_porosity_output_shape(self): + """Test that output porosity field has correct shape.""" + # TODO: Verify output shape matches grid shape + pass + + def test_compute_porosity_output_dtype(self): + """Test that output porosity field has correct dtype.""" + # TODO: Verify output dtype is int8 + pass + + def test_compute_porosity_empty_path_vectors(self): + """Test porosity computation with no path vectors.""" + # TODO: Test edge case with empty path vector list + pass + + def test_compute_porosity_single_vector(self): + """Test porosity computation with a single path vector.""" + # TODO: Test minimal case + pass + + +# ============================================================================= +# Tests for write_vtk +# ============================================================================= + +class TestWriteVtk: + """Test cases for the write_vtk function.""" + + def test_write_vtk_basic(self, temp_output_dir): + """Test basic VTK file writing.""" + origin = np.array([0.0, 0.0, 0.0]) + voxel_resolution = 0.01 + porosity = np.zeros((10, 10, 10), dtype=np.int8) + porosity[5, 5, 5] = 1 + + output_path = temp_output_dir / "test_output.vti" + + write_vtk(origin, voxel_resolution, porosity, str(output_path)) + + assert output_path.exists() + assert output_path.stat().st_size > 0 + + def test_write_vtk_file_creation(self, temp_output_dir): + """Test that VTK file is created at specified path.""" + origin = np.array([0.0, 0.0, 0.0]) + porosity = np.ones((5, 5, 5), dtype=np.int8) + output_path = temp_output_dir / "porosity.vti" + + write_vtk(origin, 0.01, porosity, str(output_path)) + + assert output_path.exists() + + def test_write_vtk_different_origins(self, temp_output_dir): + """Test VTK writing with different origin points.""" + # TODO: Test with various origin coordinates + pass + + def test_write_vtk_different_resolutions(self, temp_output_dir): + """Test VTK writing with different voxel resolutions.""" + # TODO: Test with various resolutions + pass + + def test_write_vtk_large_array(self, temp_output_dir): + """Test VTK writing with large porosity array.""" + # TODO: Test with larger array sizes + pass + + def test_write_vtk_invalid_path(self): + """Test VTK writing with invalid output path.""" + # TODO: Test with invalid file path + pass + + +# ============================================================================= +# Tests for compute_morphology +# ============================================================================= + +class TestComputeMorphology: + """Test cases for the compute_morphology function.""" + + def test_compute_morphology_basic(self): + """Test basic morphology computation.""" + porosity = np.zeros((20, 20, 20), dtype=np.uint8) + porosity[5:8, 5:8, 5:8] = 1 # Add a pore + + morphology_fields = ['area', 'centroid'] + properties = compute_morphology(porosity, 0.01, morphology_fields) + + assert isinstance(properties, (dict, np.ndarray)) + # TODO: Add more specific assertions + + def test_compute_morphology_single_pore(self): + """Test morphology computation with a single pore.""" + # TODO: Create porosity with single isolated pore + pass + + def test_compute_morphology_multiple_pores(self): + """Test morphology computation with multiple pores.""" + # TODO: Create porosity with multiple isolated pores + pass + + def test_compute_morphology_no_pores(self): + """Test morphology computation with no pores.""" + porosity = np.zeros((10, 10, 10), dtype=np.uint8) + + properties = compute_morphology(porosity, 0.01, ['area']) + + # TODO: Verify behavior with no pores + assert isinstance(properties, (dict, np.ndarray)) + + def test_compute_morphology_all_fields(self): + """Test morphology computation with all available fields.""" + # TODO: Test with comprehensive list of morphology fields + pass + + def test_compute_morphology_small_object_removal(self): + """Test that small objects are correctly removed.""" + # TODO: Test filtering of small pores + pass + + +# ============================================================================= +# Tests for write_morphology +# ============================================================================= + +class TestWriteMorphology: + """Test cases for the write_morphology function.""" + + def test_write_morphology_basic(self, temp_output_dir): + """Test basic morphology file writing.""" + properties = { + 'area': np.array([1.0, 2.0, 3.0]), + 'centroid-0': np.array([0.5, 1.5, 2.5]), + 'centroid-1': np.array([0.5, 1.5, 2.5]), + 'centroid-2': np.array([0.5, 1.5, 2.5]) + } + + output_path = temp_output_dir / "morphology.csv" + + write_morphology(properties, str(output_path)) + + assert output_path.exists() + assert output_path.stat().st_size > 0 + + def test_write_morphology_file_format(self, temp_output_dir): + """Test that morphology file has correct CSV format.""" + # TODO: Parse and verify CSV structure + pass + + def test_write_morphology_empty_properties(self, temp_output_dir): + """Test morphology writing with empty properties.""" + # TODO: Test edge case with no defects + pass + + def test_write_morphology_column_headers(self, temp_output_dir): + """Test that column headers match property keys.""" + # TODO: Verify CSV headers + pass + + +# ============================================================================= +# Integration Tests +# ============================================================================= + +class TestApiIntegration: + """Integration tests combining multiple API functions.""" + + def test_full_workflow_basic(self, temp_output_dir): + """Test complete workflow from grid creation to VTK output.""" + # TODO: Create end-to-end test + pass + + def test_full_workflow_with_morphology(self, temp_output_dir): + """Test complete workflow including morphology analysis.""" + # TODO: Create end-to-end test with morphology + pass + + def test_grid_to_porosity_pipeline(self): + """Test pipeline from grid creation through porosity computation.""" + # TODO: Test combined workflow + pass diff --git a/tests/test_core.py b/tests/test_core.py new file mode 100644 index 0000000..652ca0d --- /dev/null +++ b/tests/test_core.py @@ -0,0 +1,619 @@ +# ============================================================================= +# Copyright (c) 2025 Oak Ridge National Laboratory +# +# All rights reserved. +# +# This file is part of Raptor. +# +# For details, see the top-level LICENSE file at: +# https://github.com/ORNL-MDF/Raptor/LICENSE +# ============================================================================= +""" +Test suite for raptor.core module. + +This module contains unit tests for the core computation functions including +geometric containment testing, melt mask computation, and the main parallelized +implicit melt mask calculation. +""" + +import pytest +import numpy as np +from typing import List +from unittest.mock import Mock, MagicMock + +# Import the module under test +from raptor.core import ( + is_inside, + compute_melt_mask, + compute_melt_mask_implicit +) +from raptor.structures import MeltPool, PathVector + + +# ============================================================================= +# Fixtures +# ============================================================================= + +@pytest.fixture +def sample_melt_pool(): + """Fixture providing a sample MeltPool object.""" + width_osc = np.array([ + [0.0001, 0.0, 0.0], + [0.00002, 5.0, 0.0], + [0.00001, 10.0, 0.0] + ], dtype=np.float64) + + depth_osc = np.array([ + [0.00008, 0.0, 0.0], + [0.00001, 5.0, 0.0], + [0.00001, 10.0, 0.0] + ], dtype=np.float64) + + height_osc = np.array([ + [0.00006, 0.0, 0.0], + [0.00001, 5.0, 0.0], + [0.00001, 10.0, 0.0] + ], dtype=np.float64) + + return MeltPool( + width_oscillations=width_osc, + depth_oscillations=depth_osc, + height_oscillations=height_osc, + width_max=0.00012, + depth_max=0.0001, + height_max=0.00008, + width_shape_factor=2.0, + height_shape_factor=2.0, + depth_shape_factor=2.0, + enable_random_phases=False + ) + + +@pytest.fixture +def sample_path_vector(): + """Fixture providing a sample PathVector object.""" + start = np.array([0.0, 0.0, 0.0], dtype=np.float64) + end = np.array([0.001, 0.0, 0.0], dtype=np.float64) + + path_vec = PathVector( + start_point=start, + end_point=end, + start_time=0.0, + end_time=0.001 + ) + path_vec.set_coordinate_frame() + + return path_vec + + +@pytest.fixture +def sample_path_vectors(sample_path_vector): + """Fixture providing a list of sample PathVector objects.""" + vectors = [] + + # Create multiple parallel vectors + for i in range(3): + start = np.array([0.0, i * 0.0001, 0.0], dtype=np.float64) + end = np.array([0.001, i * 0.0001, 0.0], dtype=np.float64) + + vec = PathVector( + start_point=start, + end_point=end, + start_time=i * 0.001, + end_time=(i + 1) * 0.001 + ) + vec.set_coordinate_frame() + vectors.append(vec) + + return vectors + + +@pytest.fixture +def sample_voxels(): + """Fixture providing a sample voxel array.""" + # Create a small grid of voxels + x = np.linspace(0, 0.001, 10) + y = np.linspace(0, 0.0005, 5) + z = np.linspace(0, 0.0002, 3) + + xx, yy, zz = np.meshgrid(x, y, z, indexing='ij') + + voxels = np.column_stack([ + xx.ravel(), + yy.ravel(), + zz.ravel() + ]) + + return voxels.astype(np.float64) + + +@pytest.fixture +def minimal_voxels(): + """Fixture providing a minimal voxel array for testing.""" + return np.array([ + [0.0, 0.0, 0.0], + [0.0005, 0.0, 0.0], + [0.001, 0.0, 0.0] + ], dtype=np.float64) + + +# ============================================================================= +# Tests for is_inside function +# ============================================================================= + +class TestIsInside: + """Test cases for the is_inside geometric containment function.""" + + def test_is_inside_center_point(self): + """Test that the center point (0,0) is always inside.""" + width = 0.0002 + height = 0.0001 + depth = 0.00008 + + result = is_inside(0.0, 0.0, width, height, depth, 2.0, 2.0) + + assert result == True + + def test_is_inside_ellipse_shape(self): + """Test containment with ellipse shape (n=2).""" + width = 0.0002 + height = 0.0001 + depth = 0.00008 + + # Point on the boundary (should be inside) + y = width / 2.0 + z = 0.0 + result = is_inside(y, z, width, height, depth, 2.0, 2.0) + assert result == True + + # Point clearly outside + y = width + z = height + result = is_inside(y, z, width, height, depth, 2.0, 2.0) + assert result == False + + def test_is_inside_parabola_shape(self): + """Test containment with parabola shape (n=1).""" + # TODO: Test with n=1 + pass + + def test_is_inside_bell_shape(self): + """Test containment with bell-like shape (n=0.5).""" + # TODO: Test with n=0.5 + pass + + def test_is_inside_positive_z(self): + """Test containment for points with positive z (using height).""" + width = 0.0002 + height = 0.0001 + depth = 0.00008 + + # Point with positive z + y = 0.0 + z = height / 2.0 # Should be inside + result = is_inside(y, z, width, height, depth, 2.0, 2.0) + assert result == True + + def test_is_inside_negative_z(self): + """Test containment for points with negative z (using depth).""" + width = 0.0002 + height = 0.0001 + depth = 0.00008 + + # Point with negative z + y = 0.0 + z = -depth / 2.0 # Should be inside + result = is_inside(y, z, width, height, depth, 2.0, 2.0) + assert result == True + + def test_is_inside_different_shape_factors(self): + """Test containment with different shape factors for height and depth.""" + width = 0.0002 + height = 0.0001 + depth = 0.00008 + + # Different shape factors + result = is_inside(0.00005, 0.00003, width, height, depth, 2.0, 10.0) + # TODO: Verify different shape factor behavior + + def test_is_inside_boundary_cases(self): + """Test points exactly on the boundary.""" + width = 0.0002 + height = 0.0001 + depth = 0.00008 + + # Point at width boundary (y = width/2, z = 0) + result = is_inside(width / 2.0, 0.0, width, height, depth, 2.0, 2.0) + assert result == True + + # Point at height boundary (y = 0, z = height) + result = is_inside(0.0, height, width, height, depth, 2.0, 2.0) + assert result == True + + def test_is_inside_outside_points(self): + """Test points clearly outside the shape.""" + width = 0.0002 + height = 0.0001 + depth = 0.00008 + + # Point far outside + result = is_inside(width * 2, height * 2, width, height, depth, 2.0, 2.0) + assert result == False + + def test_is_inside_zero_dimensions(self): + """Test with zero or very small dimensions.""" + # TODO: Test edge case with zero width/height/depth + pass + + def test_is_inside_symmetry(self): + """Test symmetry about y-axis.""" + width = 0.0002 + height = 0.0001 + depth = 0.00008 + + # Test symmetry for positive and negative y + result_pos = is_inside(0.00005, 0.00002, width, height, depth, 2.0, 2.0) + result_neg = is_inside(-0.00005, 0.00002, width, height, depth, 2.0, 2.0) + + assert result_pos == result_neg + + +# ============================================================================= +# Tests for compute_melt_mask function +# ============================================================================= + +class TestComputeMeltMask: + """Test cases for the compute_melt_mask wrapper function.""" + + def test_compute_melt_mask_basic(self, minimal_voxels, sample_melt_pool, sample_path_vectors): + """Test basic melt mask computation.""" + # Set melt pool properties for path vectors + for vec in sample_path_vectors: + vec.set_melt_pool_properties(sample_melt_pool) + + result = compute_melt_mask(minimal_voxels, sample_melt_pool, sample_path_vectors) + + assert isinstance(result, np.ndarray) + assert result.dtype == np.bool_ + assert result.shape[0] == minimal_voxels.shape[0] + + def test_compute_melt_mask_unpacking(self, minimal_voxels, sample_melt_pool, sample_path_vectors): + """Test that MeltPool and PathVector objects are correctly unpacked.""" + for vec in sample_path_vectors: + vec.set_melt_pool_properties(sample_melt_pool) + + result = compute_melt_mask(minimal_voxels, sample_melt_pool, sample_path_vectors) + + # TODO: Verify correct unpacking of properties + assert isinstance(result, np.ndarray) + + def test_compute_melt_mask_empty_path_vectors(self, minimal_voxels, sample_melt_pool): + """Test melt mask computation with no path vectors.""" + empty_vectors = [] + + result = compute_melt_mask(minimal_voxels, sample_melt_pool, empty_vectors) + + # All voxels should be False (not melted) + assert result.shape[0] == minimal_voxels.shape[0] + assert not result.any() + + def test_compute_melt_mask_single_voxel(self, sample_melt_pool, sample_path_vectors): + """Test with a single voxel.""" + single_voxel = np.array([[0.0005, 0.0, 0.0]], dtype=np.float64) + + for vec in sample_path_vectors: + vec.set_melt_pool_properties(sample_melt_pool) + + result = compute_melt_mask(single_voxel, sample_melt_pool, sample_path_vectors) + + assert result.shape[0] == 1 + assert isinstance(result[0], (bool, np.bool_)) + + def test_compute_melt_mask_single_path_vector(self, minimal_voxels, sample_melt_pool, sample_path_vector): + """Test with a single path vector.""" + sample_path_vector.set_melt_pool_properties(sample_melt_pool) + + result = compute_melt_mask(minimal_voxels, sample_melt_pool, [sample_path_vector]) + + assert result.shape[0] == minimal_voxels.shape[0] + + def test_compute_melt_mask_output_shape(self, sample_voxels, sample_melt_pool, sample_path_vectors): + """Test that output shape matches input voxel count.""" + for vec in sample_path_vectors: + vec.set_melt_pool_properties(sample_melt_pool) + + result = compute_melt_mask(sample_voxels, sample_melt_pool, sample_path_vectors) + + assert result.shape[0] == sample_voxels.shape[0] + + def test_compute_melt_mask_dtype(self, minimal_voxels, sample_melt_pool, sample_path_vectors): + """Test that output has correct boolean dtype.""" + for vec in sample_path_vectors: + vec.set_melt_pool_properties(sample_melt_pool) + + result = compute_melt_mask(minimal_voxels, sample_melt_pool, sample_path_vectors) + + assert result.dtype == np.bool_ + + +# ============================================================================= +# Tests for compute_melt_mask_implicit function +# ============================================================================= + +class TestComputeMeltMaskImplicit: + """Test cases for the compute_melt_mask_implicit parallelized function.""" + + def test_compute_melt_mask_implicit_basic(self, minimal_voxels, sample_melt_pool, sample_path_vectors): + """Test basic implicit melt mask computation.""" + # Set up melt pool properties + for vec in sample_path_vectors: + vec.set_melt_pool_properties(sample_melt_pool) + + # Prepare inputs + n_voxels = minimal_voxels.shape[0] + melt_mask = np.zeros(n_voxels, dtype=np.bool_) + + start_points = np.array([v.start_point for v in sample_path_vectors]) + end_points = np.array([v.end_point for v in sample_path_vectors]) + e0 = np.array([v.e0 for v in sample_path_vectors]) + e1 = np.array([v.e1 for v in sample_path_vectors]) + e2 = np.array([v.e2 for v in sample_path_vectors]) + L0 = np.array([v.L0 for v in sample_path_vectors]) + L1 = np.array([v.L1 for v in sample_path_vectors]) + L2 = np.array([v.L2 for v in sample_path_vectors]) + start_times = np.array([v.start_time for v in sample_path_vectors]) + end_times = np.array([v.end_time for v in sample_path_vectors]) + AABB = np.array([v.AABB for v in sample_path_vectors]) + phases = np.array([v.phases for v in sample_path_vectors]) + centroids = np.array([v.centroid for v in sample_path_vectors]) + distances = np.array([v.distance for v in sample_path_vectors]) + + width_amp = sample_melt_pool.width_oscillations[:, 0] + width_freq = sample_melt_pool.width_oscillations[:, 1] + depth_amp = sample_melt_pool.depth_oscillations[:, 0] + depth_freq = sample_melt_pool.depth_oscillations[:, 1] + height_amp = sample_melt_pool.height_oscillations[:, 0] + height_freq = sample_melt_pool.height_oscillations[:, 1] + + result = compute_melt_mask_implicit( + minimal_voxels, melt_mask, + start_points, end_points, + e0, e1, e2, + L0, L1, L2, + start_times, end_times, + AABB, phases, centroids, distances, + width_amp, width_freq, + depth_amp, depth_freq, + height_amp, height_freq, + sample_melt_pool.height_shape_factor, + sample_melt_pool.depth_shape_factor + ) + + assert isinstance(result, np.ndarray) + assert result.dtype == np.bool_ + assert result.shape[0] == n_voxels + + def test_compute_melt_mask_implicit_aabb_culling(self): + """Test that AABB (axis-aligned bounding box) culling works correctly.""" + # TODO: Create test where voxels are outside AABB + pass + + def test_compute_melt_mask_implicit_obb_culling(self): + """Test that OBB (oriented bounding box) culling works correctly.""" + # TODO: Create test where voxels pass AABB but fail OBB test + pass + + def test_compute_melt_mask_implicit_time_fraction(self): + """Test time fraction calculation along path vector.""" + # TODO: Test time interpolation along scan path + pass + + def test_compute_melt_mask_implicit_coordinate_transform(self): + """Test transformation to local coordinate system.""" + # TODO: Verify local coordinate calculation + pass + + def test_compute_melt_mask_implicit_oscillation_computation(self): + """Test that oscillations are correctly computed.""" + # TODO: Verify width, depth, height oscillation calculations + pass + + def test_compute_melt_mask_implicit_zero_distance_vector(self): + """Test handling of path vectors with zero distance.""" + # TODO: Test edge case with zero-length vectors + pass + + def test_compute_melt_mask_implicit_boundary_voxels(self): + """Test voxels exactly on path boundaries.""" + # TODO: Test boundary conditions + pass + + +# ============================================================================= +# Integration Tests +# ============================================================================= + +class TestCoreIntegration: + """Integration tests combining multiple core functions.""" + + def test_is_inside_with_compute_melt_mask(self, minimal_voxels, sample_melt_pool, sample_path_vectors): + """Test integration of is_inside with compute_melt_mask.""" + for vec in sample_path_vectors: + vec.set_melt_pool_properties(sample_melt_pool) + + result = compute_melt_mask(minimal_voxels, sample_melt_pool, sample_path_vectors) + + # Verify result is consistent + assert isinstance(result, np.ndarray) + + +# ============================================================================= +# Geometric Tests +# ============================================================================= + +class TestGeometricAccuracy: + """Tests for geometric accuracy of computations.""" + + def test_lamé_curve_accuracy(self): + """Test accuracy of Lamé curve implementation.""" + # TODO: Compare with analytical solutions + pass + + def test_coordinate_transformation_accuracy(self): + """Test accuracy of coordinate transformations.""" + # TODO: Verify orthogonality and normalization + pass + + def test_distance_calculation_accuracy(self): + """Test accuracy of distance calculations.""" + # TODO: Compare with known distances + pass + + def test_time_interpolation_accuracy(self): + """Test accuracy of time interpolation along paths.""" + # TODO: Verify linear interpolation + pass + + def test_bounding_box_accuracy(self): + """Test that bounding boxes correctly contain geometry.""" + # TODO: Verify AABB and OBB correctness + pass + + +# ============================================================================= +# Edge Cases and Error Handling +# ============================================================================= + +class TestCoreEdgeCases: + """Test edge cases and boundary conditions.""" + + def test_is_inside_very_small_dimensions(self): + """Test is_inside with very small dimensions.""" + result = is_inside(0.0, 0.0, 1e-10, 1e-10, 1e-10, 2.0, 2.0) + # TODO: Verify behavior with tiny dimensions + pass + + def test_compute_melt_mask_empty_voxels(self, sample_melt_pool): + """Test compute_melt_mask with empty voxel array.""" + empty_voxels = np.empty((0, 3), dtype=np.float64) + + result = compute_melt_mask(empty_voxels, sample_melt_pool, []) + + assert result.shape[0] == 0 + + def test_compute_melt_mask_single_mode(self): + """Test with single mode (no oscillations).""" + # TODO: Test with n_modes = 1 + pass + + def test_zero_time_duration(self): + """Test handling of path vectors with zero time duration.""" + # TODO: Test edge case with start_time == end_time + pass + + +# ============================================================================= +# Shape Factor Tests +# ============================================================================= + +class TestShapeFactors: + """Detailed tests for different shape factors.""" + + def test_ellipse_shape_n2(self): + """Test elliptical cross-section (n=2).""" + width = 0.0002 + height = 0.0001 + depth = 0.00008 + + # Test points on ellipse boundary + # For ellipse: (y/a)^2 + (z/b)^2 = 1 + a = width / 2.0 + b = height + + # Point on boundary + y = a * np.cos(np.pi / 4) + z = b * np.sin(np.pi / 4) + + result = is_inside(y, z, width, height, depth, 2.0, 2.0) + assert result == True + + def test_box_shape_n10(self): + """Test box-like cross-section (n=10).""" + # TODO: Test with n=10 for box-like shape + pass + + def test_parabola_shape_n1(self): + """Test parabolic cross-section (n=1).""" + # TODO: Test with n=1 for parabolic shape + pass + + def test_bell_shape_n05(self): + """Test bell-like cross-section (n=0.5).""" + # TODO: Test with n=0.5 for bell-like shape + pass + + def test_mixed_shape_factors(self): + """Test with different shape factors for top and bottom.""" + # TODO: Test height_shape_factor != depth_shape_factor + pass + + +# ============================================================================= +# Oscillation Tests +# ============================================================================= + +class TestOscillations: + """Tests for melt pool oscillation effects.""" + + def test_oscillation_amplitude_effect(self): + """Test effect of oscillation amplitudes on melt mask.""" + # TODO: Vary amplitudes and check melt mask changes + pass + + def test_oscillation_frequency_effect(self): + """Test effect of oscillation frequencies on melt mask.""" + # TODO: Vary frequencies and check melt mask changes + pass + + def test_oscillation_phase_effect(self): + """Test effect of phase shifts on melt mask.""" + # TODO: Vary phases and check melt mask changes + pass + + def test_multiple_modes_superposition(self): + """Test that multiple modes combine correctly.""" + # TODO: Verify superposition of multiple oscillation modes + pass + + def test_random_phases(self): + """Test melt pool with random phases enabled.""" + # TODO: Test random phase generation and effects + pass + + +# ============================================================================= +# Coordinate Frame Tests +# ============================================================================= + +class TestCoordinateFrames: + """Tests for coordinate frame transformations.""" + + def test_local_to_global_transformation(self): + """Test transformation from local to global coordinates.""" + # TODO: Verify coordinate transformations + pass + + def test_orthogonal_basis_vectors(self): + """Test that e0, e1, e2 form an orthogonal basis.""" + # TODO: Verify orthogonality + pass + + def test_coordinate_frame_along_x_axis(self): + """Test coordinate frame for vector along X axis.""" + # TODO: Test aligned with X + pass + + def test_coordinate_frame_arbitrary_direction(self): + """Test coordinate frame for arbitrary vector direction.""" + # TODO: Test arbitrary orientation + pass From 259483739bb779ac4590e96ed4db9c316ffa2c42 Mon Sep 17 00:00:00 2001 From: Vamsi Subraveti Date: Tue, 11 Nov 2025 10:57:56 -0600 Subject: [PATCH 2/4] updated regionprops_table call from scikit-image --- src/raptor/api.py | 2 +- tests/test_api.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/raptor/api.py b/src/raptor/api.py index 18f6e0b..4510d1c 100644 --- a/src/raptor/api.py +++ b/src/raptor/api.py @@ -250,7 +250,7 @@ def compute_morphology( minsize = 2 filtered_defects = remove_small_objects(labeled_defects, minsize) - return measure.regionproperties_table( + return measure.regionprops_table( filtered_defects, spacing=voxel_resolution, properties=morphology_fields ) diff --git a/tests/test_api.py b/tests/test_api.py index 0df0836..730e456 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -317,7 +317,7 @@ def test_create_melt_pool_shape_factors(self, sample_melt_pool_dict): """Test that shape factors are correctly set.""" melt_pool = create_melt_pool(sample_melt_pool_dict, enable_random_phases=False) - assert melt_pool.width_shape_factor == 2.0 + assert melt_pool.depth_shape_factor == 2.0 assert melt_pool.height_shape_factor == 2.0 From d2ee6df91ce87ae50d94bb2e80a94ee3ea430580 Mon Sep 17 00:00:00 2001 From: Vamsi Subraveti Date: Tue, 11 Nov 2025 11:13:12 -0600 Subject: [PATCH 3/4] cleaned up warnings re: single mode --- src/raptor/api.py | 16 +++++++-------- tests/test_api.py | 12 +----------- tests/test_core.py | 49 +++++++++++----------------------------------- 3 files changed, 19 insertions(+), 58 deletions(-) diff --git a/src/raptor/api.py b/src/raptor/api.py index 4510d1c..1c59eaf 100644 --- a/src/raptor/api.py +++ b/src/raptor/api.py @@ -69,18 +69,16 @@ def compute_spectral_components(melt_pool_data: np.ndarray, n_modes: int) -> np. fft_resolution = np.fft.fft(melt_pool_data[:, 1]) F = np.zeros_like(fft_resolution) n_fft = len(fft_resolution) - - for i in range(1, n_modes): - F[i] = fft_resolution[i] - F[n_fft - i] = fft_resolution[n_fft - i] - - frequencies = np.float64(1 / (dt * n_fft)) * np.arange(n_modes, dtype=np.float64) - phases = np.float64(np.angle(F[:n_modes])) - amplitudes = np.float64(np.abs(F[:n_modes]) / n_fft) - if n_modes == 1: spectral_array = np.array([[mode0, 0, 0]]) else: + for i in range(1, n_modes): + F[i] = fft_resolution[i] + F[n_fft - i] = fft_resolution[n_fft - i] + + frequencies = np.float64(1 / (dt * n_fft)) * np.arange(n_modes, dtype=np.float64) + phases = np.float64(np.angle(F[:n_modes])) + amplitudes = np.float64(np.abs(F[:n_modes]) / n_fft) spectral_array = np.vstack( [ np.array([mode0, 0, 0]), diff --git a/tests/test_api.py b/tests/test_api.py index 730e456..ffba096 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -317,7 +317,7 @@ def test_create_melt_pool_shape_factors(self, sample_melt_pool_dict): """Test that shape factors are correctly set.""" melt_pool = create_melt_pool(sample_melt_pool_dict, enable_random_phases=False) - + assert melt_pool.depth_shape_factor == 2.0 assert melt_pool.height_shape_factor == 2.0 @@ -359,11 +359,6 @@ def test_compute_porosity_output_dtype(self): # TODO: Verify output dtype is int8 pass - def test_compute_porosity_empty_path_vectors(self): - """Test porosity computation with no path vectors.""" - # TODO: Test edge case with empty path vector list - pass - def test_compute_porosity_single_vector(self): """Test porosity computation with a single path vector.""" # TODO: Test minimal case @@ -463,11 +458,6 @@ def test_compute_morphology_all_fields(self): """Test morphology computation with all available fields.""" # TODO: Test with comprehensive list of morphology fields pass - - def test_compute_morphology_small_object_removal(self): - """Test that small objects are correctly removed.""" - # TODO: Test filtering of small pores - pass # ============================================================================= diff --git a/tests/test_core.py b/tests/test_core.py index 652ca0d..812a189 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -287,16 +287,6 @@ def test_compute_melt_mask_unpacking(self, minimal_voxels, sample_melt_pool, sam # TODO: Verify correct unpacking of properties assert isinstance(result, np.ndarray) - def test_compute_melt_mask_empty_path_vectors(self, minimal_voxels, sample_melt_pool): - """Test melt mask computation with no path vectors.""" - empty_vectors = [] - - result = compute_melt_mask(minimal_voxels, sample_melt_pool, empty_vectors) - - # All voxels should be False (not melted) - assert result.shape[0] == minimal_voxels.shape[0] - assert not result.any() - def test_compute_melt_mask_single_voxel(self, sample_melt_pool, sample_path_vectors): """Test with a single voxel.""" single_voxel = np.array([[0.0005, 0.0, 0.0]], dtype=np.float64) @@ -492,24 +482,11 @@ def test_is_inside_very_small_dimensions(self): result = is_inside(0.0, 0.0, 1e-10, 1e-10, 1e-10, 2.0, 2.0) # TODO: Verify behavior with tiny dimensions pass - - def test_compute_melt_mask_empty_voxels(self, sample_melt_pool): - """Test compute_melt_mask with empty voxel array.""" - empty_voxels = np.empty((0, 3), dtype=np.float64) - - result = compute_melt_mask(empty_voxels, sample_melt_pool, []) - - assert result.shape[0] == 0 def test_compute_melt_mask_single_mode(self): """Test with single mode (no oscillations).""" # TODO: Test with n_modes = 1 pass - - def test_zero_time_duration(self): - """Test handling of path vectors with zero time duration.""" - # TODO: Test edge case with start_time == end_time - pass # ============================================================================= @@ -519,14 +496,15 @@ def test_zero_time_duration(self): class TestShapeFactors: """Detailed tests for different shape factors.""" - def test_ellipse_shape_n2(self): - """Test elliptical cross-section (n=2).""" + def test_ellipse_shape_k2(self): + """Test elliptical cross-section (k=2).""" width = 0.0002 height = 0.0001 depth = 0.00008 # Test points on ellipse boundary - # For ellipse: (y/a)^2 + (z/b)^2 = 1 + # For ellipse: (y/a)^2 + (z/b)^k = 1 + # k=2 a = width / 2.0 b = height @@ -537,19 +515,14 @@ def test_ellipse_shape_n2(self): result = is_inside(y, z, width, height, depth, 2.0, 2.0) assert result == True - def test_box_shape_n10(self): - """Test box-like cross-section (n=10).""" - # TODO: Test with n=10 for box-like shape - pass - - def test_parabola_shape_n1(self): - """Test parabolic cross-section (n=1).""" - # TODO: Test with n=1 for parabolic shape + def test_parabola_shape_k1(self): + """Test parabolic cross-section (k=1).""" + # TODO: Test with k=1 for parabolic shape pass - - def test_bell_shape_n05(self): - """Test bell-like cross-section (n=0.5).""" - # TODO: Test with n=0.5 for bell-like shape + + def test_bell_shape_k05(self): + """Test bell-like cross-section (k=0.5).""" + # TODO: Test with k=0.5 for bell-like shape pass def test_mixed_shape_factors(self): From 566ed1ec7bdf2d948000041aa11ba1ba8e6340de Mon Sep 17 00:00:00 2001 From: John Date: Thu, 13 Nov 2025 15:55:02 -0500 Subject: [PATCH 4/4] add pore to supress warning in pytest. fit formatting --- README.md | 2 +- src/raptor/api.py | 4 +- src/raptor/structures.py | 4 +- tests/test_api.py | 264 ++++++++++++++++-------------- tests/test_core.py | 335 +++++++++++++++++++++------------------ 5 files changed, 334 insertions(+), 275 deletions(-) diff --git a/README.md b/README.md index 7918099..e9527c2 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ RAPTOR requires requires Python 3 (tested with Python 3.8+). The following Pytho ``` * **NumPy**: For numerical operations and array manipulation. - + * **Numba**: For JIT compilation and performance acceleration. * **PyYAML**: For reading and parsing YAML configuration files * **VTK**: For writing the output porosity map in `.vti` format diff --git a/src/raptor/api.py b/src/raptor/api.py index 1c59eaf..6436607 100644 --- a/src/raptor/api.py +++ b/src/raptor/api.py @@ -76,7 +76,9 @@ def compute_spectral_components(melt_pool_data: np.ndarray, n_modes: int) -> np. F[i] = fft_resolution[i] F[n_fft - i] = fft_resolution[n_fft - i] - frequencies = np.float64(1 / (dt * n_fft)) * np.arange(n_modes, dtype=np.float64) + frequencies = np.float64(1 / (dt * n_fft)) * np.arange( + n_modes, dtype=np.float64 + ) phases = np.float64(np.angle(F[:n_modes])) amplitudes = np.float64(np.abs(F[:n_modes]) / n_fft) spectral_array = np.vstack( diff --git a/src/raptor/structures.py b/src/raptor/structures.py index 82b32a1..fe36e97 100644 --- a/src/raptor/structures.py +++ b/src/raptor/structures.py @@ -184,9 +184,7 @@ def __init__( path_vectors: Optional[List[PathVector]] = None, ): if voxel_resolution <= 0.0: - raise ValueError( - "Voxel resolution must be a positive non-zero value." - ) + raise ValueError("Voxel resolution must be a positive non-zero value.") self.resolution = voxel_resolution if bound_box is not None: diff --git a/tests/test_api.py b/tests/test_api.py index ffba096..7885c3a 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -41,13 +41,11 @@ # Fixtures # ============================================================================= + @pytest.fixture def sample_bound_box(): """Fixture providing a sample bounding box for testing.""" - return np.array([ - [0.0, 0.0, 0.0], # min point - [1.0, 1.0, 0.5] # max point - ]) + return np.array([[0.0, 0.0, 0.0], [1.0, 1.0, 0.5]]) # min point # max point @pytest.fixture @@ -55,6 +53,7 @@ def sample_voxel_resolution(): """Fixture providing a sample voxel resolution.""" return 0.01 + @pytest.fixture def sample_path_vectors(): """Fixture providing sample path vectors.""" @@ -66,21 +65,22 @@ def sample_path_vectors(): start_point=start_point, end_point=end_point, start_time=start_time, - end_time=end_time + end_time=end_time, ) return [path_vector] + @pytest.fixture def sample_process_parameters(): """Fixture providing sample process parameters.""" return { - 'power': 200.0, - 'scan_speed': 1.0, - 'hatch_spacing': 0.1, - 'layer_height': 0.05, - 'rotation': 67.0, - 'scan_extension': 0.1, - 'extra_layers': 0 + "power": 200.0, + "scan_speed": 1.0, + "hatch_spacing": 0.1, + "layer_height": 0.05, + "rotation": 67.0, + "scan_extension": 0.1, + "extra_layers": 0, } @@ -96,20 +96,23 @@ def sample_time_series_data(): def sample_spectral_components(): """Fixture providing sample spectral components.""" # Format: [amplitude, frequency, phase] - return np.array([ - [0.0001, 0.0, 0.0], # mode 0 (mean) - [0.00002, 5.0, 0.0], # mode 1 - [0.00001, 10.0, np.pi/2] # mode 2 - ], dtype=np.float64) + return np.array( + [ + [0.0001, 0.0, 0.0], # mode 0 (mean) + [0.00002, 5.0, 0.0], # mode 1 + [0.00001, 10.0, np.pi / 2], # mode 2 + ], + dtype=np.float64, + ) @pytest.fixture def sample_melt_pool_dict(sample_time_series_data): """Fixture providing a sample melt pool dictionary.""" return { - 'width': (sample_time_series_data, 3, 1.0, 2.0), - 'depth': (sample_time_series_data, 3, 1.0, 2.0), - 'height': (sample_time_series_data, 3, 1.0, 2.0) + "width": (sample_time_series_data, 3, 1.0, 2.0), + "depth": (sample_time_series_data, 3, 1.0, 2.0), + "height": (sample_time_series_data, 3, 1.0, 2.0), } @@ -124,28 +127,32 @@ def temp_output_dir(): # Tests for create_grid # ============================================================================= + class TestCreateGrid: """Test cases for the create_grid function.""" - - def test_create_grid_with_bound_box(self, sample_voxel_resolution, sample_bound_box): + + def test_create_grid_with_bound_box( + self, sample_voxel_resolution, sample_bound_box + ): """Test grid creation with a bounding box.""" grid = create_grid(sample_voxel_resolution, bound_box=sample_bound_box) - + assert isinstance(grid, Grid) assert grid.resolution == sample_voxel_resolution assert grid.origin.shape == (3,) - assert grid.shape[0] * grid.shape[1] * grid.shape[2] > 0 + assert grid.shape[0] * grid.shape[1] * grid.shape[2] > 0 assert grid.voxels.shape == (grid.shape[0] * grid.shape[1] * grid.shape[2], 3) - - def test_create_grid_with_path_vectors(self, sample_voxel_resolution, sample_path_vectors): + + def test_create_grid_with_path_vectors( + self, sample_voxel_resolution, sample_path_vectors + ): """Test grid creation with path vectors.""" grid = create_grid(sample_voxel_resolution, path_vectors=sample_path_vectors) - assert isinstance(grid, Grid) assert grid.resolution == sample_voxel_resolution assert np.all(grid.origin == np.array([0.0, 0.0, 0.0])) - + def test_create_grid_invalid_resolution(self, bound_box=sample_bound_box): """Test grid creation with invalid voxel resolution.""" with pytest.raises(ValueError): @@ -168,50 +175,60 @@ def test_create_grid_invalid_path_vectors(self, sample_voxel_resolution): # Tests for create_path_vectors # ============================================================================= + class TestCreatePathVectors: """Test cases for the create_path_vectors function.""" - - def test_create_path_vectors_basic(self, sample_bound_box, sample_process_parameters): + + def test_create_path_vectors_basic( + self, sample_bound_box, sample_process_parameters + ): """Test basic path vector generation.""" path_vectors = create_path_vectors( - sample_bound_box, - **sample_process_parameters + sample_bound_box, **sample_process_parameters ) - + assert isinstance(path_vectors, list) assert len(path_vectors) > 0 assert all(isinstance(pv, PathVector) for pv in path_vectors) - - def test_create_path_vectors_single_layer(self, sample_bound_box, sample_process_parameters): + + def test_create_path_vectors_single_layer( + self, sample_bound_box, sample_process_parameters + ): """Test path vector generation for a single layer.""" params = sample_process_parameters.copy() - params['extra_layers'] = 0 - + params["extra_layers"] = 0 + path_vectors = create_path_vectors(sample_bound_box, **params) - + # TODO: Verify single layer generation assert len(path_vectors) > 0 - - def test_create_path_vectors_multiple_layers(self, sample_bound_box, sample_process_parameters): + + def test_create_path_vectors_multiple_layers( + self, sample_bound_box, sample_process_parameters + ): """Test path vector generation for multiple layers.""" params = sample_process_parameters.copy() - params['extra_layers'] = 3 - + params["extra_layers"] = 3 + path_vectors = create_path_vectors(sample_bound_box, **params) - + # TODO: Verify multiple layer generation assert len(path_vectors) > 0 - - def test_create_path_vectors_rotation(self, sample_bound_box, sample_process_parameters): + + def test_create_path_vectors_rotation( + self, sample_bound_box, sample_process_parameters + ): """Test path vector generation with rotation.""" # TODO: Test different rotation angles pass - - def test_create_path_vectors_hatch_spacing(self, sample_bound_box, sample_process_parameters): + + def test_create_path_vectors_hatch_spacing( + self, sample_bound_box, sample_process_parameters + ): """Test path vector generation with different hatch spacings.""" # TODO: Test effect of hatch spacing on vector count pass - + def test_create_path_vectors_invalid_parameters(self, sample_bound_box): """Test path vector generation with invalid parameters.""" # TODO: Test with negative or invalid values @@ -222,40 +239,43 @@ def test_create_path_vectors_invalid_parameters(self, sample_bound_box): # Tests for compute_spectral_components # ============================================================================= + class TestComputeSpectralComponents: """Test cases for the compute_spectral_components function.""" - + def test_compute_spectral_components_basic(self, sample_time_series_data): """Test basic spectral component computation.""" n_modes = 3 spectral_array = compute_spectral_components(sample_time_series_data, n_modes) - + assert isinstance(spectral_array, np.ndarray) assert spectral_array.shape == (n_modes, 3) assert spectral_array.dtype == np.float64 - + def test_compute_spectral_components_single_mode(self, sample_time_series_data): """Test spectral component computation with single mode.""" n_modes = 1 spectral_array = compute_spectral_components(sample_time_series_data, n_modes) - + assert spectral_array.shape == (1, 3) assert spectral_array[0, 1] == 0 # frequency should be 0 assert spectral_array[0, 2] == 0 # phase should be 0 - + def test_compute_spectral_components_multiple_modes(self, sample_time_series_data): """Test spectral component computation with multiple modes.""" for n_modes in [2, 5, 10]: - spectral_array = compute_spectral_components(sample_time_series_data, n_modes) + spectral_array = compute_spectral_components( + sample_time_series_data, n_modes + ) assert spectral_array.shape == (n_modes, 3) - + def test_compute_spectral_components_mean_value(self, sample_time_series_data): """Test that mode 0 matches the mean of input data.""" spectral_array = compute_spectral_components(sample_time_series_data, 3) expected_mean = sample_time_series_data[:, 1].mean() - + np.testing.assert_allclose(spectral_array[0, 0], expected_mean) - + def test_compute_spectral_components_invalid_input(self): """Test spectral component computation with invalid input.""" # TODO: Test with malformed input data @@ -266,61 +286,61 @@ def test_compute_spectral_components_invalid_input(self): # Tests for create_melt_pool # ============================================================================= + class TestCreateMeltPool: """Test cases for the create_melt_pool function.""" - + def test_create_melt_pool_basic(self, sample_melt_pool_dict): """Test basic melt pool creation.""" melt_pool = create_melt_pool(sample_melt_pool_dict, enable_random_phases=False) - + assert isinstance(melt_pool, MeltPool) assert melt_pool.enable_random_phases == False - + def test_create_melt_pool_random_phases(self, sample_melt_pool_dict): """Test melt pool creation with random phases enabled.""" melt_pool = create_melt_pool(sample_melt_pool_dict, enable_random_phases=True) - + assert melt_pool.enable_random_phases == True - + def test_create_melt_pool_spectral_input(self, sample_spectral_components): """Test melt pool creation with spectral component input.""" melt_pool_dict = { - 'width': (sample_spectral_components, 3, 1.0, 2.0), - 'depth': (sample_spectral_components, 3, 1.0, 2.0), - 'height': (sample_spectral_components, 3, 1.0, 2.0) + "width": (sample_spectral_components, 3, 1.0, 2.0), + "depth": (sample_spectral_components, 3, 1.0, 2.0), + "height": (sample_spectral_components, 3, 1.0, 2.0), } - + melt_pool = create_melt_pool(melt_pool_dict, enable_random_phases=False) - + assert isinstance(melt_pool, MeltPool) - + def test_create_melt_pool_mode_padding(self): """Test that melt pool correctly pads modes to match maximum.""" # TODO: Create inputs with different mode counts pass - + def test_create_melt_pool_scaling(self, sample_time_series_data): """Test that scaling is correctly applied.""" scale_factor = 2.0 melt_pool_dict = { - 'width': (sample_time_series_data, 3, scale_factor, 2.0), - 'depth': (sample_time_series_data, 3, 1.0, 2.0), - 'height': (sample_time_series_data, 3, 1.0, 2.0) + "width": (sample_time_series_data, 3, scale_factor, 2.0), + "depth": (sample_time_series_data, 3, 1.0, 2.0), + "height": (sample_time_series_data, 3, 1.0, 2.0), } - + melt_pool = create_melt_pool(melt_pool_dict, enable_random_phases=False) - + # TODO: Verify scaling is applied correctly assert isinstance(melt_pool, MeltPool) - + def test_create_melt_pool_shape_factors(self, sample_melt_pool_dict): """Test that shape factors are correctly set.""" melt_pool = create_melt_pool(sample_melt_pool_dict, enable_random_phases=False) - assert melt_pool.depth_shape_factor == 2.0 assert melt_pool.height_shape_factor == 2.0 - + def test_create_melt_pool_invalid_data_shape(self): """Test melt pool creation with invalid data shape.""" # TODO: Test with data that is not Nx2 or Nx3 @@ -331,34 +351,35 @@ def test_create_melt_pool_invalid_data_shape(self): # Tests for compute_porosity # ============================================================================= + class TestComputePorosity: """Test cases for the compute_porosity function.""" - + def test_compute_porosity_basic(self): """Test basic porosity computation.""" # TODO: Create minimal grid, path vectors, and melt pool pass - + def test_compute_porosity_with_warmup(self): """Test porosity computation with JIT warmup enabled.""" # TODO: Test with jit_warmup=True pass - + def test_compute_porosity_without_warmup(self): """Test porosity computation with JIT warmup disabled.""" # TODO: Test with jit_warmup=False pass - + def test_compute_porosity_output_shape(self): """Test that output porosity field has correct shape.""" # TODO: Verify output shape matches grid shape pass - + def test_compute_porosity_output_dtype(self): """Test that output porosity field has correct dtype.""" # TODO: Verify output dtype is int8 pass - + def test_compute_porosity_single_vector(self): """Test porosity computation with a single path vector.""" # TODO: Test minimal case @@ -369,48 +390,49 @@ def test_compute_porosity_single_vector(self): # Tests for write_vtk # ============================================================================= + class TestWriteVtk: """Test cases for the write_vtk function.""" - + def test_write_vtk_basic(self, temp_output_dir): """Test basic VTK file writing.""" origin = np.array([0.0, 0.0, 0.0]) voxel_resolution = 0.01 porosity = np.zeros((10, 10, 10), dtype=np.int8) porosity[5, 5, 5] = 1 - + output_path = temp_output_dir / "test_output.vti" - + write_vtk(origin, voxel_resolution, porosity, str(output_path)) - + assert output_path.exists() assert output_path.stat().st_size > 0 - + def test_write_vtk_file_creation(self, temp_output_dir): """Test that VTK file is created at specified path.""" origin = np.array([0.0, 0.0, 0.0]) porosity = np.ones((5, 5, 5), dtype=np.int8) output_path = temp_output_dir / "porosity.vti" - + write_vtk(origin, 0.01, porosity, str(output_path)) - + assert output_path.exists() - + def test_write_vtk_different_origins(self, temp_output_dir): """Test VTK writing with different origin points.""" # TODO: Test with various origin coordinates pass - + def test_write_vtk_different_resolutions(self, temp_output_dir): """Test VTK writing with different voxel resolutions.""" # TODO: Test with various resolutions pass - + def test_write_vtk_large_array(self, temp_output_dir): """Test VTK writing with large porosity array.""" # TODO: Test with larger array sizes pass - + def test_write_vtk_invalid_path(self): """Test VTK writing with invalid output path.""" # TODO: Test with invalid file path @@ -421,39 +443,41 @@ def test_write_vtk_invalid_path(self): # Tests for compute_morphology # ============================================================================= + class TestComputeMorphology: """Test cases for the compute_morphology function.""" - + def test_compute_morphology_basic(self): """Test basic morphology computation.""" porosity = np.zeros((20, 20, 20), dtype=np.uint8) porosity[5:8, 5:8, 5:8] = 1 # Add a pore - - morphology_fields = ['area', 'centroid'] + porosity[15:18, 15:18, 15:18] = 1 # Add another pore + + morphology_fields = ["area", "centroid"] properties = compute_morphology(porosity, 0.01, morphology_fields) - + assert isinstance(properties, (dict, np.ndarray)) # TODO: Add more specific assertions - + def test_compute_morphology_single_pore(self): """Test morphology computation with a single pore.""" # TODO: Create porosity with single isolated pore pass - + def test_compute_morphology_multiple_pores(self): """Test morphology computation with multiple pores.""" # TODO: Create porosity with multiple isolated pores pass - + def test_compute_morphology_no_pores(self): """Test morphology computation with no pores.""" porosity = np.zeros((10, 10, 10), dtype=np.uint8) - - properties = compute_morphology(porosity, 0.01, ['area']) - + + properties = compute_morphology(porosity, 0.01, ["area"]) + # TODO: Verify behavior with no pores assert isinstance(properties, (dict, np.ndarray)) - + def test_compute_morphology_all_fields(self): """Test morphology computation with all available fields.""" # TODO: Test with comprehensive list of morphology fields @@ -464,35 +488,36 @@ def test_compute_morphology_all_fields(self): # Tests for write_morphology # ============================================================================= + class TestWriteMorphology: """Test cases for the write_morphology function.""" - + def test_write_morphology_basic(self, temp_output_dir): """Test basic morphology file writing.""" properties = { - 'area': np.array([1.0, 2.0, 3.0]), - 'centroid-0': np.array([0.5, 1.5, 2.5]), - 'centroid-1': np.array([0.5, 1.5, 2.5]), - 'centroid-2': np.array([0.5, 1.5, 2.5]) + "area": np.array([1.0, 2.0, 3.0]), + "centroid-0": np.array([0.5, 1.5, 2.5]), + "centroid-1": np.array([0.5, 1.5, 2.5]), + "centroid-2": np.array([0.5, 1.5, 2.5]), } - + output_path = temp_output_dir / "morphology.csv" - + write_morphology(properties, str(output_path)) - + assert output_path.exists() assert output_path.stat().st_size > 0 - + def test_write_morphology_file_format(self, temp_output_dir): """Test that morphology file has correct CSV format.""" # TODO: Parse and verify CSV structure pass - + def test_write_morphology_empty_properties(self, temp_output_dir): """Test morphology writing with empty properties.""" # TODO: Test edge case with no defects pass - + def test_write_morphology_column_headers(self, temp_output_dir): """Test that column headers match property keys.""" # TODO: Verify CSV headers @@ -503,19 +528,20 @@ def test_write_morphology_column_headers(self, temp_output_dir): # Integration Tests # ============================================================================= + class TestApiIntegration: """Integration tests combining multiple API functions.""" - + def test_full_workflow_basic(self, temp_output_dir): """Test complete workflow from grid creation to VTK output.""" # TODO: Create end-to-end test pass - + def test_full_workflow_with_morphology(self, temp_output_dir): """Test complete workflow including morphology analysis.""" # TODO: Create end-to-end test with morphology pass - + def test_grid_to_porosity_pipeline(self): """Test pipeline from grid creation through porosity computation.""" # TODO: Test combined workflow diff --git a/tests/test_core.py b/tests/test_core.py index 812a189..43c49dd 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -22,11 +22,7 @@ from unittest.mock import Mock, MagicMock # Import the module under test -from raptor.core import ( - is_inside, - compute_melt_mask, - compute_melt_mask_implicit -) +from raptor.core import is_inside, compute_melt_mask, compute_melt_mask_implicit from raptor.structures import MeltPool, PathVector @@ -34,27 +30,25 @@ # Fixtures # ============================================================================= + @pytest.fixture def sample_melt_pool(): """Fixture providing a sample MeltPool object.""" - width_osc = np.array([ - [0.0001, 0.0, 0.0], - [0.00002, 5.0, 0.0], - [0.00001, 10.0, 0.0] - ], dtype=np.float64) - - depth_osc = np.array([ - [0.00008, 0.0, 0.0], - [0.00001, 5.0, 0.0], - [0.00001, 10.0, 0.0] - ], dtype=np.float64) - - height_osc = np.array([ - [0.00006, 0.0, 0.0], - [0.00001, 5.0, 0.0], - [0.00001, 10.0, 0.0] - ], dtype=np.float64) - + width_osc = np.array( + [[0.0001, 0.0, 0.0], [0.00002, 5.0, 0.0], [0.00001, 10.0, 0.0]], + dtype=np.float64, + ) + + depth_osc = np.array( + [[0.00008, 0.0, 0.0], [0.00001, 5.0, 0.0], [0.00001, 10.0, 0.0]], + dtype=np.float64, + ) + + height_osc = np.array( + [[0.00006, 0.0, 0.0], [0.00001, 5.0, 0.0], [0.00001, 10.0, 0.0]], + dtype=np.float64, + ) + return MeltPool( width_oscillations=width_osc, depth_oscillations=depth_osc, @@ -65,7 +59,7 @@ def sample_melt_pool(): width_shape_factor=2.0, height_shape_factor=2.0, depth_shape_factor=2.0, - enable_random_phases=False + enable_random_phases=False, ) @@ -74,15 +68,12 @@ def sample_path_vector(): """Fixture providing a sample PathVector object.""" start = np.array([0.0, 0.0, 0.0], dtype=np.float64) end = np.array([0.001, 0.0, 0.0], dtype=np.float64) - + path_vec = PathVector( - start_point=start, - end_point=end, - start_time=0.0, - end_time=0.001 + start_point=start, end_point=end, start_time=0.0, end_time=0.001 ) path_vec.set_coordinate_frame() - + return path_vec @@ -90,21 +81,21 @@ def sample_path_vector(): def sample_path_vectors(sample_path_vector): """Fixture providing a list of sample PathVector objects.""" vectors = [] - + # Create multiple parallel vectors for i in range(3): start = np.array([0.0, i * 0.0001, 0.0], dtype=np.float64) end = np.array([0.001, i * 0.0001, 0.0], dtype=np.float64) - + vec = PathVector( start_point=start, end_point=end, start_time=i * 0.001, - end_time=(i + 1) * 0.001 + end_time=(i + 1) * 0.001, ) vec.set_coordinate_frame() vectors.append(vec) - + return vectors @@ -115,146 +106,141 @@ def sample_voxels(): x = np.linspace(0, 0.001, 10) y = np.linspace(0, 0.0005, 5) z = np.linspace(0, 0.0002, 3) - - xx, yy, zz = np.meshgrid(x, y, z, indexing='ij') - - voxels = np.column_stack([ - xx.ravel(), - yy.ravel(), - zz.ravel() - ]) - + + xx, yy, zz = np.meshgrid(x, y, z, indexing="ij") + + voxels = np.column_stack([xx.ravel(), yy.ravel(), zz.ravel()]) + return voxels.astype(np.float64) @pytest.fixture def minimal_voxels(): """Fixture providing a minimal voxel array for testing.""" - return np.array([ - [0.0, 0.0, 0.0], - [0.0005, 0.0, 0.0], - [0.001, 0.0, 0.0] - ], dtype=np.float64) + return np.array( + [[0.0, 0.0, 0.0], [0.0005, 0.0, 0.0], [0.001, 0.0, 0.0]], dtype=np.float64 + ) # ============================================================================= # Tests for is_inside function # ============================================================================= + class TestIsInside: """Test cases for the is_inside geometric containment function.""" - + def test_is_inside_center_point(self): """Test that the center point (0,0) is always inside.""" width = 0.0002 height = 0.0001 depth = 0.00008 - + result = is_inside(0.0, 0.0, width, height, depth, 2.0, 2.0) - + assert result == True - + def test_is_inside_ellipse_shape(self): """Test containment with ellipse shape (n=2).""" width = 0.0002 height = 0.0001 depth = 0.00008 - + # Point on the boundary (should be inside) y = width / 2.0 z = 0.0 result = is_inside(y, z, width, height, depth, 2.0, 2.0) assert result == True - + # Point clearly outside y = width z = height result = is_inside(y, z, width, height, depth, 2.0, 2.0) assert result == False - + def test_is_inside_parabola_shape(self): """Test containment with parabola shape (n=1).""" # TODO: Test with n=1 pass - + def test_is_inside_bell_shape(self): """Test containment with bell-like shape (n=0.5).""" # TODO: Test with n=0.5 pass - + def test_is_inside_positive_z(self): """Test containment for points with positive z (using height).""" width = 0.0002 height = 0.0001 depth = 0.00008 - + # Point with positive z y = 0.0 z = height / 2.0 # Should be inside result = is_inside(y, z, width, height, depth, 2.0, 2.0) assert result == True - + def test_is_inside_negative_z(self): """Test containment for points with negative z (using depth).""" width = 0.0002 height = 0.0001 depth = 0.00008 - + # Point with negative z y = 0.0 z = -depth / 2.0 # Should be inside result = is_inside(y, z, width, height, depth, 2.0, 2.0) assert result == True - + def test_is_inside_different_shape_factors(self): """Test containment with different shape factors for height and depth.""" width = 0.0002 height = 0.0001 depth = 0.00008 - + # Different shape factors result = is_inside(0.00005, 0.00003, width, height, depth, 2.0, 10.0) # TODO: Verify different shape factor behavior - + def test_is_inside_boundary_cases(self): """Test points exactly on the boundary.""" width = 0.0002 height = 0.0001 depth = 0.00008 - + # Point at width boundary (y = width/2, z = 0) result = is_inside(width / 2.0, 0.0, width, height, depth, 2.0, 2.0) assert result == True - + # Point at height boundary (y = 0, z = height) result = is_inside(0.0, height, width, height, depth, 2.0, 2.0) assert result == True - + def test_is_inside_outside_points(self): """Test points clearly outside the shape.""" width = 0.0002 height = 0.0001 depth = 0.00008 - + # Point far outside result = is_inside(width * 2, height * 2, width, height, depth, 2.0, 2.0) assert result == False - + def test_is_inside_zero_dimensions(self): """Test with zero or very small dimensions.""" # TODO: Test edge case with zero width/height/depth pass - + def test_is_inside_symmetry(self): """Test symmetry about y-axis.""" width = 0.0002 height = 0.0001 depth = 0.00008 - + # Test symmetry for positive and negative y result_pos = is_inside(0.00005, 0.00002, width, height, depth, 2.0, 2.0) result_neg = is_inside(-0.00005, 0.00002, width, height, depth, 2.0, 2.0) - + assert result_pos == result_neg @@ -262,67 +248,88 @@ def test_is_inside_symmetry(self): # Tests for compute_melt_mask function # ============================================================================= + class TestComputeMeltMask: """Test cases for the compute_melt_mask wrapper function.""" - - def test_compute_melt_mask_basic(self, minimal_voxels, sample_melt_pool, sample_path_vectors): + + def test_compute_melt_mask_basic( + self, minimal_voxels, sample_melt_pool, sample_path_vectors + ): """Test basic melt mask computation.""" # Set melt pool properties for path vectors for vec in sample_path_vectors: vec.set_melt_pool_properties(sample_melt_pool) - - result = compute_melt_mask(minimal_voxels, sample_melt_pool, sample_path_vectors) - + + result = compute_melt_mask( + minimal_voxels, sample_melt_pool, sample_path_vectors + ) + assert isinstance(result, np.ndarray) assert result.dtype == np.bool_ assert result.shape[0] == minimal_voxels.shape[0] - - def test_compute_melt_mask_unpacking(self, minimal_voxels, sample_melt_pool, sample_path_vectors): + + def test_compute_melt_mask_unpacking( + self, minimal_voxels, sample_melt_pool, sample_path_vectors + ): """Test that MeltPool and PathVector objects are correctly unpacked.""" for vec in sample_path_vectors: vec.set_melt_pool_properties(sample_melt_pool) - - result = compute_melt_mask(minimal_voxels, sample_melt_pool, sample_path_vectors) - + + result = compute_melt_mask( + minimal_voxels, sample_melt_pool, sample_path_vectors + ) + # TODO: Verify correct unpacking of properties assert isinstance(result, np.ndarray) - - def test_compute_melt_mask_single_voxel(self, sample_melt_pool, sample_path_vectors): + + def test_compute_melt_mask_single_voxel( + self, sample_melt_pool, sample_path_vectors + ): """Test with a single voxel.""" single_voxel = np.array([[0.0005, 0.0, 0.0]], dtype=np.float64) - + for vec in sample_path_vectors: vec.set_melt_pool_properties(sample_melt_pool) - + result = compute_melt_mask(single_voxel, sample_melt_pool, sample_path_vectors) - + assert result.shape[0] == 1 assert isinstance(result[0], (bool, np.bool_)) - - def test_compute_melt_mask_single_path_vector(self, minimal_voxels, sample_melt_pool, sample_path_vector): + + def test_compute_melt_mask_single_path_vector( + self, minimal_voxels, sample_melt_pool, sample_path_vector + ): """Test with a single path vector.""" sample_path_vector.set_melt_pool_properties(sample_melt_pool) - - result = compute_melt_mask(minimal_voxels, sample_melt_pool, [sample_path_vector]) - + + result = compute_melt_mask( + minimal_voxels, sample_melt_pool, [sample_path_vector] + ) + assert result.shape[0] == minimal_voxels.shape[0] - - def test_compute_melt_mask_output_shape(self, sample_voxels, sample_melt_pool, sample_path_vectors): + + def test_compute_melt_mask_output_shape( + self, sample_voxels, sample_melt_pool, sample_path_vectors + ): """Test that output shape matches input voxel count.""" for vec in sample_path_vectors: vec.set_melt_pool_properties(sample_melt_pool) - + result = compute_melt_mask(sample_voxels, sample_melt_pool, sample_path_vectors) - + assert result.shape[0] == sample_voxels.shape[0] - - def test_compute_melt_mask_dtype(self, minimal_voxels, sample_melt_pool, sample_path_vectors): + + def test_compute_melt_mask_dtype( + self, minimal_voxels, sample_melt_pool, sample_path_vectors + ): """Test that output has correct boolean dtype.""" for vec in sample_path_vectors: vec.set_melt_pool_properties(sample_melt_pool) - - result = compute_melt_mask(minimal_voxels, sample_melt_pool, sample_path_vectors) - + + result = compute_melt_mask( + minimal_voxels, sample_melt_pool, sample_path_vectors + ) + assert result.dtype == np.bool_ @@ -330,19 +337,22 @@ def test_compute_melt_mask_dtype(self, minimal_voxels, sample_melt_pool, sample_ # Tests for compute_melt_mask_implicit function # ============================================================================= + class TestComputeMeltMaskImplicit: """Test cases for the compute_melt_mask_implicit parallelized function.""" - - def test_compute_melt_mask_implicit_basic(self, minimal_voxels, sample_melt_pool, sample_path_vectors): + + def test_compute_melt_mask_implicit_basic( + self, minimal_voxels, sample_melt_pool, sample_path_vectors + ): """Test basic implicit melt mask computation.""" # Set up melt pool properties for vec in sample_path_vectors: vec.set_melt_pool_properties(sample_melt_pool) - + # Prepare inputs n_voxels = minimal_voxels.shape[0] melt_mask = np.zeros(n_voxels, dtype=np.bool_) - + start_points = np.array([v.start_point for v in sample_path_vectors]) end_points = np.array([v.end_point for v in sample_path_vectors]) e0 = np.array([v.e0 for v in sample_path_vectors]) @@ -357,62 +367,75 @@ def test_compute_melt_mask_implicit_basic(self, minimal_voxels, sample_melt_pool phases = np.array([v.phases for v in sample_path_vectors]) centroids = np.array([v.centroid for v in sample_path_vectors]) distances = np.array([v.distance for v in sample_path_vectors]) - + width_amp = sample_melt_pool.width_oscillations[:, 0] width_freq = sample_melt_pool.width_oscillations[:, 1] depth_amp = sample_melt_pool.depth_oscillations[:, 0] depth_freq = sample_melt_pool.depth_oscillations[:, 1] height_amp = sample_melt_pool.height_oscillations[:, 0] height_freq = sample_melt_pool.height_oscillations[:, 1] - + result = compute_melt_mask_implicit( - minimal_voxels, melt_mask, - start_points, end_points, - e0, e1, e2, - L0, L1, L2, - start_times, end_times, - AABB, phases, centroids, distances, - width_amp, width_freq, - depth_amp, depth_freq, - height_amp, height_freq, + minimal_voxels, + melt_mask, + start_points, + end_points, + e0, + e1, + e2, + L0, + L1, + L2, + start_times, + end_times, + AABB, + phases, + centroids, + distances, + width_amp, + width_freq, + depth_amp, + depth_freq, + height_amp, + height_freq, sample_melt_pool.height_shape_factor, - sample_melt_pool.depth_shape_factor + sample_melt_pool.depth_shape_factor, ) - + assert isinstance(result, np.ndarray) assert result.dtype == np.bool_ assert result.shape[0] == n_voxels - + def test_compute_melt_mask_implicit_aabb_culling(self): """Test that AABB (axis-aligned bounding box) culling works correctly.""" # TODO: Create test where voxels are outside AABB pass - + def test_compute_melt_mask_implicit_obb_culling(self): """Test that OBB (oriented bounding box) culling works correctly.""" # TODO: Create test where voxels pass AABB but fail OBB test pass - + def test_compute_melt_mask_implicit_time_fraction(self): """Test time fraction calculation along path vector.""" # TODO: Test time interpolation along scan path pass - + def test_compute_melt_mask_implicit_coordinate_transform(self): """Test transformation to local coordinate system.""" # TODO: Verify local coordinate calculation pass - + def test_compute_melt_mask_implicit_oscillation_computation(self): """Test that oscillations are correctly computed.""" # TODO: Verify width, depth, height oscillation calculations pass - + def test_compute_melt_mask_implicit_zero_distance_vector(self): """Test handling of path vectors with zero distance.""" # TODO: Test edge case with zero-length vectors pass - + def test_compute_melt_mask_implicit_boundary_voxels(self): """Test voxels exactly on path boundaries.""" # TODO: Test boundary conditions @@ -423,16 +446,21 @@ def test_compute_melt_mask_implicit_boundary_voxels(self): # Integration Tests # ============================================================================= + class TestCoreIntegration: """Integration tests combining multiple core functions.""" - - def test_is_inside_with_compute_melt_mask(self, minimal_voxels, sample_melt_pool, sample_path_vectors): + + def test_is_inside_with_compute_melt_mask( + self, minimal_voxels, sample_melt_pool, sample_path_vectors + ): """Test integration of is_inside with compute_melt_mask.""" for vec in sample_path_vectors: vec.set_melt_pool_properties(sample_melt_pool) - - result = compute_melt_mask(minimal_voxels, sample_melt_pool, sample_path_vectors) - + + result = compute_melt_mask( + minimal_voxels, sample_melt_pool, sample_path_vectors + ) + # Verify result is consistent assert isinstance(result, np.ndarray) @@ -441,29 +469,30 @@ def test_is_inside_with_compute_melt_mask(self, minimal_voxels, sample_melt_pool # Geometric Tests # ============================================================================= + class TestGeometricAccuracy: """Tests for geometric accuracy of computations.""" - + def test_lamé_curve_accuracy(self): """Test accuracy of Lamé curve implementation.""" # TODO: Compare with analytical solutions pass - + def test_coordinate_transformation_accuracy(self): """Test accuracy of coordinate transformations.""" # TODO: Verify orthogonality and normalization pass - + def test_distance_calculation_accuracy(self): """Test accuracy of distance calculations.""" # TODO: Compare with known distances pass - + def test_time_interpolation_accuracy(self): """Test accuracy of time interpolation along paths.""" # TODO: Verify linear interpolation pass - + def test_bounding_box_accuracy(self): """Test that bounding boxes correctly contain geometry.""" # TODO: Verify AABB and OBB correctness @@ -474,15 +503,16 @@ def test_bounding_box_accuracy(self): # Edge Cases and Error Handling # ============================================================================= + class TestCoreEdgeCases: """Test edge cases and boundary conditions.""" - + def test_is_inside_very_small_dimensions(self): """Test is_inside with very small dimensions.""" result = is_inside(0.0, 0.0, 1e-10, 1e-10, 1e-10, 2.0, 2.0) # TODO: Verify behavior with tiny dimensions pass - + def test_compute_melt_mask_single_mode(self): """Test with single mode (no oscillations).""" # TODO: Test with n_modes = 1 @@ -493,28 +523,29 @@ def test_compute_melt_mask_single_mode(self): # Shape Factor Tests # ============================================================================= + class TestShapeFactors: """Detailed tests for different shape factors.""" - + def test_ellipse_shape_k2(self): """Test elliptical cross-section (k=2).""" width = 0.0002 height = 0.0001 depth = 0.00008 - + # Test points on ellipse boundary # For ellipse: (y/a)^2 + (z/b)^k = 1 # k=2 a = width / 2.0 b = height - + # Point on boundary y = a * np.cos(np.pi / 4) z = b * np.sin(np.pi / 4) - + result = is_inside(y, z, width, height, depth, 2.0, 2.0) assert result == True - + def test_parabola_shape_k1(self): """Test parabolic cross-section (k=1).""" # TODO: Test with k=1 for parabolic shape @@ -524,7 +555,7 @@ def test_bell_shape_k05(self): """Test bell-like cross-section (k=0.5).""" # TODO: Test with k=0.5 for bell-like shape pass - + def test_mixed_shape_factors(self): """Test with different shape factors for top and bottom.""" # TODO: Test height_shape_factor != depth_shape_factor @@ -535,29 +566,30 @@ def test_mixed_shape_factors(self): # Oscillation Tests # ============================================================================= + class TestOscillations: """Tests for melt pool oscillation effects.""" - + def test_oscillation_amplitude_effect(self): """Test effect of oscillation amplitudes on melt mask.""" # TODO: Vary amplitudes and check melt mask changes pass - + def test_oscillation_frequency_effect(self): """Test effect of oscillation frequencies on melt mask.""" # TODO: Vary frequencies and check melt mask changes pass - + def test_oscillation_phase_effect(self): """Test effect of phase shifts on melt mask.""" # TODO: Vary phases and check melt mask changes pass - + def test_multiple_modes_superposition(self): """Test that multiple modes combine correctly.""" # TODO: Verify superposition of multiple oscillation modes pass - + def test_random_phases(self): """Test melt pool with random phases enabled.""" # TODO: Test random phase generation and effects @@ -568,24 +600,25 @@ def test_random_phases(self): # Coordinate Frame Tests # ============================================================================= + class TestCoordinateFrames: """Tests for coordinate frame transformations.""" - + def test_local_to_global_transformation(self): """Test transformation from local to global coordinates.""" # TODO: Verify coordinate transformations pass - + def test_orthogonal_basis_vectors(self): """Test that e0, e1, e2 form an orthogonal basis.""" # TODO: Verify orthogonality pass - + def test_coordinate_frame_along_x_axis(self): """Test coordinate frame for vector along X axis.""" # TODO: Test aligned with X pass - + def test_coordinate_frame_arbitrary_direction(self): """Test coordinate frame for arbitrary vector direction.""" # TODO: Test arbitrary orientation