Source code for fdtdx.conversion.stl

"""Utilities for exporting simulation data to various file formats.

This module provides functions to export 3D boolean matrices to STL files and 2D boolean
arrays to GDS files. The STL export is useful for visualizing 3D structures, while the GDS
export is designed for photolithography mask generation.

The module handles coordinate transformations between array indices and physical dimensions,
supporting both relative and absolute positioning in the output files.
"""

import math
from pathlib import Path

import numpy as np
import trimesh


def idx_to_xyz(idx: np.ndarray, shape: tuple[int, int, int]) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
    _, d1, d2 = shape
    x = idx // (d1 * d2)
    y = (idx // d2) % d1
    z = idx % d2
    return x, y, z


def xyz_to_idx(
    x: np.ndarray,
    y: np.ndarray,
    z: np.ndarray,
    shape: tuple[int, int, int],
) -> np.ndarray:
    _, d1, d2 = shape
    return x * (d1 * d2) + y * (d2) + z


[docs] def export_stl( matrix: np.ndarray, stl_filename: Path | str | None = None, voxel_grid_size: tuple[int, int, int] = (1, 1, 1), ) -> trimesh.Trimesh: """Export a 3D boolean matrix to an STL file. Converts a 3D boolean matrix into a mesh representation and saves it as an STL file. True values in the matrix are converted to solid voxels in the output mesh. Args: matrix (np.ndarray): 3D boolean numpy array representing the voxel grid. stl_filename (Path | str | None, optional): Output STL file path. If given, save the stl to this path. Defaults to None. voxel_grid_size (tuple[int, int, int], optional): Physical size of each voxel as (x, y, z) integers. Defaults to (1, 1, 1). Returns: trimesh.Trimesh: STL mesh. Raises: Exception: If input matrix is not 3-dimensional. """ if matrix.ndim != 3: raise Exception(f"Invalid matrix shape: {matrix.shape}") scaling_factor = np.asarray(voxel_grid_size, dtype=int) d0, d1, d2 = ( matrix.shape[0] + 1, matrix.shape[1] + 1, matrix.shape[2] + 1, ) vertex_shape = (d0, d1, d2) num_vertices = d0 * d1 * d2 num_voxels = int(math.prod(matrix.shape)) matrix_shape = (matrix.shape[0], matrix.shape[1], matrix.shape[2]) x, y, z = idx_to_xyz(np.arange(num_voxels), matrix_shape) matrix_flat = matrix.reshape(-1) stacked_idx = np.stack([x, y, z], axis=-1) use_sides_arr = np.zeros((num_voxels, 6), dtype=bool) use_sides_arr[matrix_flat & (stacked_idx[..., 0] == 0), 0] = True # left use_sides_arr[matrix_flat & (stacked_idx[..., 1] == 0), 1] = True # front use_sides_arr[matrix_flat & (stacked_idx[..., 2] == 0), 2] = True # bottom use_sides_arr[matrix_flat & (stacked_idx[..., 0] == matrix.shape[0] - 1), 3] = True # right use_sides_arr[matrix_flat & (stacked_idx[..., 1] == matrix.shape[1] - 1), 4] = True # back use_sides_arr[matrix_flat & (stacked_idx[..., 2] == matrix.shape[2] - 1), 5] = True # top inv_matrix = ~matrix x_p1, y_p1, z_p1 = x + 1, y + 1, z + 1 x_p1[x_p1 == matrix.shape[0]] = matrix.shape[0] - 1 y_p1[y_p1 == matrix.shape[1]] = matrix.shape[1] - 1 z_p1[z_p1 == matrix.shape[2]] = matrix.shape[2] - 1 use_sides_arr[matrix_flat & inv_matrix[x - 1, y, z], 0] = True # left use_sides_arr[matrix_flat & inv_matrix[x, y - 1, z], 1] = True # front use_sides_arr[matrix_flat & inv_matrix[x, y, z - 1], 2] = True # bottom use_sides_arr[matrix_flat & inv_matrix[x_p1, y, z], 3] = True # right use_sides_arr[matrix_flat & inv_matrix[x, y_p1, z], 4] = True # back use_sides_arr[matrix_flat & inv_matrix[x, y, z_p1], 5] = True # top v_idx = np.asarray( [ xyz_to_idx(x, y, z, shape=vertex_shape), xyz_to_idx(x, y, z + 1, shape=vertex_shape), xyz_to_idx(x, y + 1, z, shape=vertex_shape), xyz_to_idx(x, y + 1, z + 1, shape=vertex_shape), xyz_to_idx(x + 1, y, z, shape=vertex_shape), xyz_to_idx(x + 1, y, z + 1, shape=vertex_shape), xyz_to_idx(x + 1, y + 1, z, shape=vertex_shape), xyz_to_idx(x + 1, y + 1, z + 1, shape=vertex_shape), ] ) faces_raw = np.asarray( [ np.stack([[v_idx[0], v_idx[1], v_idx[2]], [v_idx[1], v_idx[3], v_idx[2]]], axis=-1), # left np.stack([[v_idx[0], v_idx[4], v_idx[5]], [v_idx[5], v_idx[1], v_idx[0]]], axis=-1), # front np.stack([[v_idx[0], v_idx[2], v_idx[6]], [v_idx[6], v_idx[4], v_idx[0]]], axis=-1), # bottom np.stack([[v_idx[4], v_idx[6], v_idx[7]], [v_idx[7], v_idx[5], v_idx[4]]], axis=-1), # right np.stack([[v_idx[2], v_idx[3], v_idx[7]], [v_idx[7], v_idx[6], v_idx[2]]], axis=-1), # back np.stack([[v_idx[1], v_idx[5], v_idx[3]], [v_idx[3], v_idx[5], v_idx[7]]], axis=-1), # top ] ) faces_raw = faces_raw.transpose((2, 0, 3, 1)) # select used faces only faces = faces_raw[use_sides_arr].reshape(-1, 3) vertex_arr = np.stack( idx_to_xyz( np.arange(num_vertices), shape=vertex_shape, ), axis=-1, ) vertex_arr = vertex_arr * scaling_factor # export to trimesh mesh = trimesh.Trimesh( vertices=vertex_arr, faces=faces, validate=False, ) if stl_filename is not None: mesh.export(stl_filename) return mesh