Skip to content

Commit

Permalink
ENH: Python CLI Reader Bounding Box (#946)
Browse files Browse the repository at this point in the history
Signed-off-by: Joey Kleingers <[email protected]>
  • Loading branch information
joeykleingers authored May 3, 2024
1 parent 9c17e33 commit ba22f21
Show file tree
Hide file tree
Showing 4 changed files with 234 additions and 46 deletions.
59 changes: 53 additions & 6 deletions wrapping/python/plugins/DataAnalysisToolkit/CliReaderFilter.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
# https://www.hmilch.net/downloads/cli_format.html

import simplnx as nx
import numpy as np
import sys
from typing import List, Dict
from pathlib import Path
from DataAnalysisToolkit.utilities.cli_tools import parse_file, parse_geometry_array_names

class CliReaderFilter:
CLI_FILE_PATH = 'cli_file_path'
MASK_X_DIMENSION = 'mask_x_dimension'
MASK_Y_DIMENSION = 'mask_y_dimension'
MASK_Z_DIMENSION = 'mask_z_dimension'
MIN_MAX_X_COORDS = 'min_max_x_coords'
MIN_MAX_Y_COORDS = 'min_max_y_coords'
MIN_MAX_Z_COORDS = 'min_max_z_coords'
OUTPUT_EDGE_GEOM_PATH = 'output_edge_geom_path'
OUTPUT_VERTEX_ATTRMAT_NAME = 'output_vertex_attrmat_name'
OUTPUT_EDGE_ATTRMAT_NAME = 'output_edge_attrmat_name'
Expand Down Expand Up @@ -38,6 +47,12 @@ def parameters(self) -> nx.Parameters:

params.insert(nx.Parameters.Separator("Parameters"))
params.insert(nx.FileSystemPathParameter(CliReaderFilter.CLI_FILE_PATH, 'Input CLI File', 'The path to the input CLI file that will be read.', '', {'.cli'}, nx.FileSystemPathParameter.PathType.InputFile))
params.insert_linkable_parameter(nx.BoolParameter(CliReaderFilter.MASK_X_DIMENSION, 'Mask X Dimension', 'Determines whether or not to use X bounds to mask out any part of the dataset that is outside the bounds in the X dimension.', False))
params.insert(nx.VectorFloat64Parameter(CliReaderFilter.MIN_MAX_X_COORDS, 'X Min/Max', 'The minimum and maximum X coordinate for the X bounds.', [0.0, 100.0], ['X Min', 'X Max']))
params.insert_linkable_parameter(nx.BoolParameter(CliReaderFilter.MASK_Y_DIMENSION, 'Mask Y Dimension', 'Determines whether or not to use Y bounds to mask out any part of the dataset that is outside the bounds in the Y dimension.', False))
params.insert(nx.VectorFloat64Parameter(CliReaderFilter.MIN_MAX_Y_COORDS, 'Y Min/Max', 'The minimum and maximum Y coordinate for the Y bounds.', [0.0, 100.0], ['Y Min', 'Y Max']))
params.insert_linkable_parameter(nx.BoolParameter(CliReaderFilter.MASK_Z_DIMENSION, 'Mask Z Dimension', 'Determines whether or not to use Z bounds to mask out any part of the dataset that is outside the bounds in the Z dimension.', False))
params.insert(nx.VectorFloat64Parameter(CliReaderFilter.MIN_MAX_Z_COORDS, 'Z Min/Max', 'The minimum and maximum Z coordinate for the Z bounds.', [0.0, 100.0], ['Z Min', 'Z Max']))
params.insert(nx.Parameters.Separator("Created Data Objects"))
params.insert(nx.DataGroupCreationParameter(CliReaderFilter.OUTPUT_EDGE_GEOM_PATH, 'Output Edge Geometry', 'The path to the newly created edge geometry.', nx.DataPath("[Edge Geometry]")))
params.insert(nx.DataObjectNameParameter(CliReaderFilter.OUTPUT_VERTEX_ATTRMAT_NAME, 'Output Vertex Attribute Matrix Name', 'The name of the newly created vertex attribute matrix.', 'Vertex Data'))
Expand All @@ -46,6 +61,10 @@ def parameters(self) -> nx.Parameters:
params.insert(nx.DataObjectNameParameter(CliReaderFilter.SHARED_VERTICES_ARRAY_NAME, 'Shared Vertices Array Name', 'The name of the newly created shared vertices array.', 'Shared Vertices'))
params.insert(nx.DataObjectNameParameter(CliReaderFilter.SHARED_EDGES_ARRAY_NAME, 'Shared Edges Array Name', 'The name of the newly created shared edges array.', 'Shared Edges'))

params.link_parameters(CliReaderFilter.MASK_X_DIMENSION, CliReaderFilter.MIN_MAX_X_COORDS, True)
params.link_parameters(CliReaderFilter.MASK_Y_DIMENSION, CliReaderFilter.MIN_MAX_Y_COORDS, True)
params.link_parameters(CliReaderFilter.MASK_Z_DIMENSION, CliReaderFilter.MIN_MAX_Z_COORDS, True)

return params

def preflight_impl(self, data_structure: nx.DataStructure, args: dict, message_handler: nx.IFilter.MessageHandler, should_cancel: nx.AtomicBoolProxy) -> nx.IFilter.PreflightResult:
Expand All @@ -56,7 +75,19 @@ def preflight_impl(self, data_structure: nx.DataStructure, args: dict, message_h
output_feature_attrmat_name: str = args[CliReaderFilter.OUTPUT_FEATURE_ATTRMAT_NAME]
shared_vertices_array_name: str = args[CliReaderFilter.SHARED_VERTICES_ARRAY_NAME]
shared_edges_array_name: str = args[CliReaderFilter.SHARED_EDGES_ARRAY_NAME]

mask_x_dimension: bool = args[CliReaderFilter.MASK_X_DIMENSION]
mask_y_dimension: bool = args[CliReaderFilter.MASK_Y_DIMENSION]
mask_z_dimension: bool = args[CliReaderFilter.MASK_Z_DIMENSION]
min_max_x_coords: list = args[CliReaderFilter.MIN_MAX_X_COORDS]
min_max_y_coords: list = args[CliReaderFilter.MIN_MAX_Y_COORDS]
min_max_z_coords: list = args[CliReaderFilter.MIN_MAX_Z_COORDS]

if mask_x_dimension and min_max_x_coords[0] > min_max_x_coords[1]:
return nx.IFilter.PreflightResult(nx.OutputActions(), [nx.Error(-9100, f"Invalid Bounding Box Mask: The minimum X coordinate ({min_max_x_coords[0]}) is larger than the maximum X coordinate ({min_max_x_coords[1]}).")])
if mask_y_dimension and min_max_y_coords[0] > min_max_y_coords[1]:
return nx.IFilter.PreflightResult(nx.OutputActions(), [nx.Error(-9101, f"Invalid Bounding Box Mask: The minimum Y coordinate ({min_max_y_coords[0]}) is larger than the maximum Y coordinate ({min_max_y_coords[1]}).")])
if mask_z_dimension and min_max_z_coords[0] > min_max_z_coords[1]:
return nx.IFilter.PreflightResult(nx.OutputActions(), [nx.Error(-9102, f"Invalid Bounding Box Mask: The minimum Z coordinate ({min_max_z_coords[0]}) is larger than the maximum Z coordinate ({min_max_z_coords[1]}).")])

# Here we create the Edge Geometry (and the 2 internal Attribute Matrix to hold vertex and edge data arrays.)
# Because this is a "reader" type of filter we do not know (at least in this reader implementation)
Expand Down Expand Up @@ -92,14 +123,28 @@ def execute_impl(self, data_structure: nx.DataStructure, args: dict, message_han
output_edge_geom_path: nx.DataPath = args[CliReaderFilter.OUTPUT_EDGE_GEOM_PATH]
output_edge_attrmat_name: str = args[CliReaderFilter.OUTPUT_EDGE_ATTRMAT_NAME]
output_feature_attrmat_name: str = args[CliReaderFilter.OUTPUT_FEATURE_ATTRMAT_NAME]
output_vertex_attrmat_name: str = args[CliReaderFilter.OUTPUT_VERTEX_ATTRMAT_NAME]

layer_features = []
mask_x_dimension: bool = args[CliReaderFilter.MASK_X_DIMENSION]
mask_y_dimension: bool = args[CliReaderFilter.MASK_Y_DIMENSION]
mask_z_dimension: bool = args[CliReaderFilter.MASK_Z_DIMENSION]
min_max_x_coords: list = args[CliReaderFilter.MIN_MAX_X_COORDS]
min_max_y_coords: list = args[CliReaderFilter.MIN_MAX_Y_COORDS]
min_max_z_coords: list = args[CliReaderFilter.MIN_MAX_Z_COORDS]

bounding_box_coords = [-sys.float_info.max, sys.float_info.max] * 3
if mask_x_dimension:
bounding_box_coords[0:2] = min_max_x_coords
if mask_y_dimension:
bounding_box_coords[2:4] = min_max_y_coords
if mask_z_dimension:
bounding_box_coords[4:6] = min_max_z_coords

try:
layer_features, layer_heights, hatch_labels = parse_file(Path(cli_file_path))
result = parse_file(Path(cli_file_path), bounding_box=bounding_box_coords)
if result.invalid():
return nx.Result(errors=result.errors)
layer_features, layer_heights, hatch_labels = result.value
except Exception as e:
return nx.Result([nx.Error(-1000, f"An error occurred while parsing the CLI file '{cli_file_path}': {e}")])
return nx.Result([nx.Error(-2010, f"An error occurred while parsing the CLI file '{cli_file_path}': {e}")])

start_vertices = []
end_vertices = []
Expand All @@ -116,6 +161,8 @@ def execute_impl(self, data_structure: nx.DataStructure, args: dict, message_han
message_handler(nx.IFilter.Message(nx.IFilter.Message.Type.Info, f'Importing layer {layer_idx + 1}/{len(layer_features)}...'))

for hatch in layer:
if hatch.n == 0:
continue
num_of_hatches += hatch.n
for start_x, start_y in zip(hatch.start_xvals, hatch.start_yvals):
start_vertices.append([start_x, start_y, hatch.z_height])
Expand Down
59 changes: 59 additions & 0 deletions wrapping/python/plugins/DataAnalysisToolkit/common/Result.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from typing import Generic, TypeVar, List
import simplnx as nx

T = TypeVar('T')

class Result(Generic[T]):
"""A class to represent a result that might be a value or an error."""
def __init__(self, value: T = None, errors: List[nx.Error] = None, warnings: List[nx.Warning] = None):
if errors and value is not None:
raise ValueError("Cannot create a Result with both errors and a value.")

self.value = value
self.errors = errors if errors else []
self.warnings = warnings if warnings else []

def valid(self) -> bool:
"""Check if the Result is valid (i.e., has no errors)."""
return not self.errors

def invalid(self) -> bool:
"""Check if the Result is invalid (i.e., has errors)."""
return self.errors

def make_error_result(code: int, message: str) -> Result:
return Result(errors=[nx.Error(code, message)])

def make_warning_result(code: int, message: str) -> Result:
return Result(warnings=[nx.Warning(code, message)])

def convert_result_to_void(result: Result) -> Result[None]:
"""Convert a Result of any type to a Result of type None, preserving errors and warnings."""
return Result(None, result.errors, result.warnings)


def merge_results(first: Result, second: Result) -> Result:
"""Merge two Results into one, combining their errors and warnings."""
merged_errors = first.errors + second.errors
merged_warnings = first.warnings + second.warnings
# Assuming we're merging results without values; adjust as needed for your use case
return Result(None, merged_errors, merged_warnings)


# Example usage
if __name__ == "__main__":
# Create an error and a warning
error = nx.Error(1, "An error occurred")
warning = nx.Warning(1, "This is a warning")

# Create a valid result and an invalid one
valid_result = Result(value="Success")
invalid_result = Result(errors=[error])

# Check if results are valid
print("Valid result is valid:", valid_result.valid())
print("Invalid result is valid:", invalid_result.valid())

# Merge results
merged_result = merge_results(valid_result, invalid_result)
print("Merged result has", len(merged_result.errors), "errors and", len(merged_result.warnings), "warnings.")
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,25 @@ IO (Input)

**Read CLI File** is a Python-based *simplnx* filter designed to read and process CLI (Common Layer Interface) files, typically used in additive manufacturing and 3D printing for representing slices of a 3D model.

This filter extracts geometric and attribute data from CLI files and organizes the data into the *simplnx* data structure by creating an edge geometry out of the imported data.
This filter extracts geometric and attribute data from CLI files and organizes the data into the *simplnx* data structure by creating an edge geometry out of the imported data. It provides options to selectively mask the dataset based on X, Y, and Z dimensions to focus on specific parts of the model.

*Note*: If any edges in the dataset straddle the specified mask bounds, this filter will return an error.

### Parameters

- `Input CLI File`: Filesystem path to the input CLI file.
- `Mask X Dimension`: Enable this option to apply X bounds, masking out any dataset portions that fall outside the specified X dimension bounds.
- `X Min/Max`: Minimum and maximum coordinates for the X bounds.
- `Mask Y Dimension`: Enable this option to apply Y bounds, masking out any dataset portions that fall outside the specified Y dimension bounds.
- `Y Min/Max`: Minimum and maximum coordinates for the Y bounds.
- `Mask Z Dimension`: Enable this option to apply Z bounds, masking out any dataset portions that fall outside the specified Z dimension bounds.
- `Z Min/Max`: Minimum and maximum coordinates for the Z bounds.
- `Output Edge Geometry`: Path where the edge geometry data will be stored in the data structure.
- `Output Vertex Attribute Matrix Name`: Name for the output vertex attribute matrix.
- `Output Edge Attribute Matrix Name`: Name for the output edge attribute matrix.
- `Output Feature Attribute Matrix Name`: Name for the output feature attribute matrix.
- `Shared Vertices Array Name`: Name of the shared vertices array created.
- `Shared Edges Array Name`: Name of the shared edges array created.

## Example Pipelines

Expand All @@ -26,4 +36,4 @@ Please see the description file distributed with this **Plugin**

## DREAM3D-NX Help

If you need help, need to file a bug report or want to request a new feature, please head over to the [DREAM3DNX-Issues](https://github.com/BlueQuartzSoftware/DREAM3DNX-Issues) GItHub site where the community of DREAM3D-NX users can help answer your questions.
If you need help, need to file a bug report, or want to request a new feature, please head over to the [DREAM3DNX-Issues](https://github.com/BlueQuartzSoftware/DREAM3DNX-Issues) GitHub site where the community of DREAM3D-NX users can help answer your questions.
Loading

0 comments on commit ba22f21

Please sign in to comment.