Skip to content

Commit

Permalink
PY: Add python bindings to allow Node based geometries to resize corr…
Browse files Browse the repository at this point in the history
…ectly (#906)

Signed-off-by: Michael Jackson <[email protected]>
  • Loading branch information
imikejackson authored Apr 3, 2024
1 parent 75122bb commit 8b1f237
Show file tree
Hide file tree
Showing 7 changed files with 191 additions and 8 deletions.
28 changes: 28 additions & 0 deletions src/Plugins/SimplnxCore/wrapping/python/simplnxpy.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -689,16 +689,44 @@ PYBIND11_MODULE(simplnx, mod)
py::class_<RectGridGeom, IGridGeometry, std::shared_ptr<RectGridGeom>> rectGridGeom(mod, "RectGridGeom");

py::class_<INodeGeometry0D, IGeometry, std::shared_ptr<INodeGeometry0D>> iNodeGeometry0D(mod, "INodeGeometry0D");
iNodeGeometry0D.def(
"resize_vertices",
[](INodeGeometry0D& nodeGeometry0D, usize size) {
nodeGeometry0D.resizeVertexList(size);
nodeGeometry0D.getVertexAttributeMatrix()->resizeTuples({size});
},
"This will resize the shared vertex list and also resize the associated attribute matrix");
py::class_<VertexGeom, INodeGeometry0D, std::shared_ptr<VertexGeom>> vertexGeom(mod, "VertexGeom");

py::class_<INodeGeometry1D, INodeGeometry0D, std::shared_ptr<INodeGeometry1D>> iNodeGeometry1D(mod, "INodeGeometry1D");
iNodeGeometry1D.def(
"resize_edges",
[](INodeGeometry1D& nodeGeometry1D, usize size) {
nodeGeometry1D.resizeEdgeList(size);
nodeGeometry1D.getEdgeAttributeMatrix()->resizeTuples({size});
},
"This will resize the shared edge list and also resize the associated attribute matrix");
py::class_<EdgeGeom, INodeGeometry1D, std::shared_ptr<EdgeGeom>> edgeGeom(mod, "EdgeGeom");

py::class_<INodeGeometry2D, INodeGeometry1D, std::shared_ptr<INodeGeometry2D>> iNodeGeometry2D(mod, "INodeGeometry2D");
iNodeGeometry2D.def(
"resize_faces",
[](INodeGeometry2D& nodeGeometry2D, usize size) {
nodeGeometry2D.resizeFaceList(size);
nodeGeometry2D.getEdgeAttributeMatrix()->resizeTuples({size});
},
"This will resize the shared triangle list and also resize the associated attribute matrix");
py::class_<TriangleGeom, INodeGeometry2D, std::shared_ptr<TriangleGeom>> triangleGeom(mod, "TriangleGeom");
py::class_<QuadGeom, INodeGeometry2D, std::shared_ptr<QuadGeom>> quadGeom(mod, "QuadGeom");

py::class_<INodeGeometry3D, INodeGeometry2D, std::shared_ptr<INodeGeometry3D>> iNodeGeometry3D(mod, "INodeGeometry3D");
iNodeGeometry3D.def(
"resize_polyhedra",
[](INodeGeometry3D& nodeGeometry3D, usize size) {
nodeGeometry3D.resizePolyhedraList(size);
nodeGeometry3D.getPolyhedraAttributeMatrix()->resizeTuples({size});
},
"This will resize the shared polyhedra list and also resize the associated attribute matrix");
py::class_<TetrahedralGeom, INodeGeometry3D, std::shared_ptr<TetrahedralGeom>> tetrahedralGeom(mod, "TetrahedralGeom");
py::class_<HexahedralGeom, INodeGeometry3D, std::shared_ptr<HexahedralGeom>> hexahedralGeom(mod, "HexahedralGeom");

Expand Down
7 changes: 5 additions & 2 deletions src/nxrunner/src/nxrunner.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -481,8 +481,11 @@ std::vector<std::string> GetPythonPluginList()
{
return {};
}

return StringUtilities::split(var, ';');
#if defined(Q_OS_WIN)
return nx::core::StringUtilities::split(var, ';');
#else
return nx::core::StringUtilities::split(var, ':');
#endif
}
#endif
} // namespace
Expand Down
5 changes: 4 additions & 1 deletion wrapping/python/docs/source/ReleaseNotes_127.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ Version 1.2.7
API Changes & Additions 1.2.7
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

The ColorTableParameter API has changed. Please see either the developer or user documentation for more details.
- The ColorTableParameter API has changed. Please see either the developer or user documentation for more details.
- A few filters have changed their name
- DataPath has had more API take from parts of PathLib. See the documentation for the new API additions
- Node based geometries allow the resizing of their internal data structures using the `resize_*` methods.

Change Log 1.2.7
^^^^^^^^^^^^^^^^^^^^
Expand Down
36 changes: 35 additions & 1 deletion wrapping/python/docs/source/Writing_A_New_Python_Filter.rst
Original file line number Diff line number Diff line change
Expand Up @@ -452,4 +452,38 @@ performed.
message_handler(nx.IFilter.Message(nx.IFilter.Message.Type.Info, f'Calculating Histogram Counts and Bin Bounds...'))
For more Python filter examples, check out the `ExamplePlugin <https://github.com/BlueQuartzSoftware/simplnx/tree/develop/wrapping/python/plugins/ExamplePlugin>`_.
For more Python filter examples, check out the `ExamplePlugin <https://github.com/BlueQuartzSoftware/simplnx/tree/develop/wrapping/python/plugins/ExamplePlugin>`_.

10. Debugging the Python Filter.
--------------------------------

Running the python filter through the DREAM3D-NX user interface will not allow you any opportunity to use a debugger to inspect troublesome code. For this
you will need to implement a separate python file that dynamically loads the python based plugin and then executes your filter with the proper arguments.
The below code is the bare minimum that you will need to implement.

.. code-block:: python
from typing import List
import simplnx as nx
# ------------------------------------------------------------------------------
# Replace NAME_OF_YOUR_PLUGIN with the actual name of your plugin
# Replace FILTER_NAME with the name of your filter that you would like to debug
import NAME_OF_YOUR_PLUGIN
nx.load_python_plugin(NAME_OF_YOUR_PLUGIN)
import NAME_OF_YOUR_PLUGIN.FILTER_NAME
# Create a Data Structure
data_structure = nx.DataStructure()
# Wrap the python filter in this "proxy" class from the target plugin so we can use it.
pynx_filter = nx.PyFilter(NAME_OF_YOUR_PLUGIN.FILTER_NAME())
# Execute the filter and check the result. We use the `execute2()` method to run the filter.
# Make sure to use all appropriate arguments to your filter. The named arguments are the values
# of each of the parameter keys that are defined at the top of the filter. For instance if you
# have this line:
# INPUT_IMAGE_ARRAY_KEY = 'input_image_array'
# then you would use 'input_image_array' as the named argument in the call to `execute2()` method
result = pynx_filter.execute2(data_structure=data_structure,
..... )
22 changes: 19 additions & 3 deletions wrapping/python/plugins/DataAnalysisToolkit/CliReaderFilter.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ def preflight_impl(self, data_structure: nx.DataStructure, args: dict, message_h
shared_vertices_array_name: str = args[CliReaderFilter.SHARED_VERTICES_ARRAY_NAME]
shared_edges_array_name: str = args[CliReaderFilter.SHARED_EDGES_ARRAY_NAME]


# 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)
# the number of vertices or edges at preflight time. During execute we will need to ensure that
# everything is sized correctly.
output_actions = nx.OutputActions()
output_actions.append_action(nx.CreateEdgeGeometryAction(geometry_path=output_edge_geom_path, num_edges=1, num_vertices=1, vertex_attribute_matrix_name=output_vertex_attrmat_name, edge_attribute_matrix_name=output_edge_attrmat_name, shared_vertices_name=shared_vertices_array_name, shared_edges_name=shared_edges_array_name))

Expand Down Expand Up @@ -87,6 +92,7 @@ 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 = []

Expand Down Expand Up @@ -124,33 +130,43 @@ def execute_impl(self, data_structure: nx.DataStructure, args: dict, message_han

edge_geom: nx.EdgeGeom = data_structure[output_edge_geom_path]

# Tell the Edge Geometry to resize the shared vertex list so that we can
# copy in the vertices.
message_handler(nx.IFilter.Message(nx.IFilter.Message.Type.Info, f'Saving Vertex List...'))
vertex_list = [item for pair in zip(start_vertices, end_vertices) for item in pair]
edge_geom.resize_vertices(len(vertex_list))

vertices_array = edge_geom.vertices
vertices_array.resize_tuples([len(vertex_list)])
vertices_view = vertices_array.store.npview()
vertices_view[:] = vertex_list

# Tell the Edge Geometry to resize the shared edge list so that we can
# copy in the edge list and also copy in all the edge arrays
message_handler(nx.IFilter.Message(nx.IFilter.Message.Type.Info, f'Saving Edges...'))
edge_geom.resize_edges(num_of_hatches)
edges_array = edge_geom.edges
edges_array.resize_tuples([num_of_hatches])
edges_view = edges_array.store.npview()
edges_view[:] = [[i, i+1] for i in range(0, len(vertex_list), 2)]

# Get the nx.DataPath to the Edge Attribute Matrix
edge_attr_mat_path = output_edge_geom_path.create_child_path(output_edge_attrmat_name)
# Copy the all the edge data into the edge attribute matrix
for array_name, values in data_arrays.items():
message_handler(nx.IFilter.Message(nx.IFilter.Message.Type.Info, f"Saving Cell Array '{array_name}'..."))
array_path = edge_attr_mat_path.create_child_path(array_name)
array: nx.IDataArray = data_structure[array_path]
array.resize_tuples([num_of_hatches])
values_arr = np.array(values)
values_arr = values_arr.reshape([len(values)] + array.cdims)
array_view = array.store.npview()
array_view[:] = values_arr


# Save the feature level data
feature_attr_mat_path = output_edge_geom_path.create_child_path(output_feature_attrmat_name)
label_feature_array_path = feature_attr_mat_path.create_child_path(self.LABEL_ARRAY_NAME)
label_feature_array: nx.StringArray = data_structure[label_feature_array_path]
message_handler(nx.IFilter.Message(nx.IFilter.Message.Type.Info, f"Saving Feature Array '{self.LABEL_ARRAY_NAME}'..."))
label_feature_array.initialize_with_list(list(hatch_labels.values()))

# Filter is complete, return the results.
return nx.Result()
2 changes: 1 addition & 1 deletion wrapping/python/plugins/ExamplePlugin/ExampleFilter1.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def clone(self):
"""
return ExampleFilter1()

def preflight_impl(self, data_structure: nx.DataStructure, args: dict, message_handler: nx.IFilter.MessageHandler, should_cancel: nx.AtomicBoolProxy) -> nx.IFilter.PreflightResult:
def parameters(self) -> nx.Parameters:
"""This function defines the parameters that are needed by the filter. Parameters collect the values from the user
or through a pipeline file.
"""
Expand Down
99 changes: 99 additions & 0 deletions wrapping/python/plugins/debugging_helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
"""
This code can be used to debug your python based DREAM3D-NX filter. There are a
number of bits of code that you will need to change in order for you to be able
to debug.
"""
from typing import List
import simplnx as nx

# ------------------------------------------------------------------------------
# This NEEDS to be executed here so that we can load the python based plugin
#
# You will need to REPLACE the name of the plugin and the name of the filter
# that you are trying to debug in the next 3 lines of code
# ------------------------------------------------------------------------------
import DataAnalysisToolkit
nx.load_python_plugin(DataAnalysisToolkit)
import DataAnalysisToolkit.CliReaderFilter


"""
The below are convenience functions that you can use to check the result of running
preflight or execute on the filter or execute on a loaded pipeline. You should
NOT have to change anything these functions.
"""
# ------------------------------------------------------------------------------
# check_filter_execute_result
# ------------------------------------------------------------------------------
def check_filter_preflight_result(filter: nx.IFilter, result: nx.IFilter.PreflightResult) -> None:
"""This function will check the result of a filter's preflight method"""
has_errors = len(result.get_result()) != 0
if has_errors:
print(f'{filter.name()} :: Errors: {result.get_result()}')
raise RuntimeError(result)

print(f"{filter.name()} :: No errors preflighting the filter")

# ------------------------------------------------------------------------------
# check_filter_execute_result
# ------------------------------------------------------------------------------
def check_filter_execute_result(filter: nx.IFilter, result: nx.IFilter.ExecuteResult) -> None:
"""This function will check the result of a filter's execute method."""
if len(result.warnings) != 0:
print(f'{filter.name()} :: Warnings: {result.warnings}')

has_errors = len(result.errors) != 0
if has_errors:
print(f'{filter.name()} :: Errors: {result.errors}')
raise RuntimeError(result)

print(f"{filter.name()} :: No errors running the filter")

# ------------------------------------------------------------------------------
# check_pipeline_execute_result
# ------------------------------------------------------------------------------
def check_pipeline_execute_result(result: nx.IFilter.ExecuteResult) -> None:
"""This method will check the result of a pipeline's execute method"""
if len(result.warnings) != 0:
print(f'{filter.name()} :: Warnings: {result.warnings}')

has_errors = len(result.errors) != 0
if has_errors:
print(f'{filter.name()} :: Errors: {result.errors}')
raise RuntimeError(result)

print(f"Pipeline :: No errors running the pipeline")


# *****************************************************************************
# This section is where you will need to programmatically execute what ever
# needs to be done to prep your filter to run. This may involve programmatically
# running filters one after another or loading a pipeline to prep the DataStructure
# and then running your filter. Take a look at the Examples/scripts and
# Examples/pipelines for examples to do that.
# *****************************************************************************


# Create a Data Structure
data_structure = nx.DataStructure()

# Wrap the python filter in this "proxy" class from the target plugin so we can use it.
pynx_filter = nx.PyFilter(DataAnalysisToolkit.CliReaderFilter())

# Execute the filter and check the result. We use the `execute2()` method to
# run the filter.
result = pynx_filter.execute2(data_structure=data_structure,
cli_file_path="/paht/to/input/file.cli")
check_filter_execute_result(pynx_filter, result)

# ------------------------------------------------------------------------------
# If we want to check the results of the filter, we can save this file to a
# dream3d file and load the .dream3d file directly into DREAM3D-NX to see the
# immediate results.
# ------------------------------------------------------------------------------
result = nx.WriteDREAM3DFilter.execute(data_structure=data_structure,
export_file_path="/path/to/output/file.dream3d",
write_xdmf_file=False)
check_filter_execute_result(nx.WriteDREAM3DFilter, result)

0 comments on commit 8b1f237

Please sign in to comment.