From 65f07afc670ec4ece0ee738f5cc520bc126817ae Mon Sep 17 00:00:00 2001 From: Yuanrui Zhang Date: Fri, 4 Oct 2024 14:59:57 -0400 Subject: [PATCH 01/34] Finished the first version of enhanced image module --- .gitignore | 5 +- .../core/utils/enhanced_images.py | 548 ++++++++++++++++++ 2 files changed, 552 insertions(+), 1 deletion(-) create mode 100644 src/ansys/dynamicreporting/core/utils/enhanced_images.py diff --git a/.gitignore b/.gitignore index 0170261e0..f1c7aa13e 100644 --- a/.gitignore +++ b/.gitignore @@ -69,4 +69,7 @@ venv/ .vscode # Ignore Sphinx files -doc/_build \ No newline at end of file +doc/_build + +# TIFF files +*.tiff \ No newline at end of file diff --git a/src/ansys/dynamicreporting/core/utils/enhanced_images.py b/src/ansys/dynamicreporting/core/utils/enhanced_images.py new file mode 100644 index 000000000..e3260bfd1 --- /dev/null +++ b/src/ansys/dynamicreporting/core/utils/enhanced_images.py @@ -0,0 +1,548 @@ +""" +Methods that generate enhanced images. + +A group of functions that mainly create: +1. RGB buffer +2. Pick data buffer +3. Variable data buffer + +Then combine these 3 buffers along with a JSON metadata to generate a 3-page +enhanced image. +""" +import io +import json +from typing import Dict, Tuple, Union + +from PIL import Image, TiffImagePlugin +from ansys.dpf import core as dpf +import numpy as np +import vtk +from vtk.util.numpy_support import vtk_to_numpy + + +def create_sample_sphere(): + sphere = vtk.vtkSphereSource() + print(type(sphere)) + sphere.Update() + add_var_as_cell_data(sphere.GetOutput(), "Pick Data", lambda i: 3456) + add_var_as_cell_data(sphere.GetOutput(), "Temperature", lambda i: i % 10000) + + return sphere + + +def create_unstructured_grid(): + # Create the points + points = vtk.vtkPoints() + points.InsertNextPoint(0.0, 0.0, 0.0) + points.InsertNextPoint(1.0, 0.0, 0.0) + points.InsertNextPoint(1.0, 1.0, 0.0) + points.InsertNextPoint(0.0, 1.0, 0.0) + points.InsertNextPoint(0.0, 0.0, 1.0) + points.InsertNextPoint(1.0, 0.0, 1.0) + points.InsertNextPoint(1.0, 1.0, 1.0) + points.InsertNextPoint(0.0, 1.0, 1.0) + points.InsertNextPoint(2.0, 0.0, 0.0) + points.InsertNextPoint(2.0, 1.0, 0.0) + + # Create an unstructured grid + unstructured_grid = vtk.vtkUnstructuredGrid() + unstructured_grid.SetPoints(points) + + # Create a hexahedron cell (cube) + hexahedron = vtk.vtkHexahedron() + for i in range(8): + hexahedron.GetPointIds().SetId(i, i) # Add the first 8 points to form a hexahedron + + # Create a tetrahedron cell + tetra = vtk.vtkTetra() + tetra.GetPointIds().SetId(0, 0) + tetra.GetPointIds().SetId(1, 1) + tetra.GetPointIds().SetId(2, 8) + tetra.GetPointIds().SetId(3, 9) + + # Add the cells to the grid + unstructured_grid.InsertNextCell(hexahedron.GetCellType(), hexahedron.GetPointIds()) + unstructured_grid.InsertNextCell(tetra.GetCellType(), tetra.GetPointIds()) + + add_var_as_cell_data(unstructured_grid, "Pick Data", lambda i: 3456) + add_var_as_point_data(unstructured_grid, "Temperature", lambda i: i % 10000) + + return unstructured_grid + + +def add_var_as_cell_data(poly_data, var_name, val_calculator): + arr = vtk.vtkFloatArray() + arr.SetName(var_name) + arr.SetNumberOfComponents(1) + num_cells = poly_data.GetNumberOfCells() + arr.SetNumberOfTuples(num_cells) + + for i in range(num_cells): + arr.SetValue(i, val_calculator(i)) + + poly_data.GetCellData().AddArray(arr) + + +def add_var_as_point_data(poly_data, var_name, val_calculator): + arr = vtk.vtkFloatArray() + arr.SetName(var_name) + arr.SetNumberOfComponents(1) + num_points = poly_data.GetNumberOfPoints() + arr.SetNumberOfTuples(num_points) + + for i in range(num_points): + arr.SetValue(i, val_calculator(i)) + + poly_data.GetPointData().AddArray(arr) + # poly_data.GetOutput().GetPointData().SetActiveScalars(var_name) + + +def setup_render_routine(poly_data: vtk.vtkPolyData) -> Tuple[vtk.vtkRenderer, vtk.vtkRenderWindow]: + """ + Set up VTK render routine, including mapper, actor, renderer and render window. + + Parameters + ---------- + poly_data: vtk.vtkPolyData + A VTK poly data object. + + Returns + ------- + Tuple[vtk.vtkRenderer, vtk.vtkRenderWindow] + A pair of VTK renderer and render windown object. + """ + # Mapper and actor + mapper = vtk.vtkPolyDataMapper() + mapper.SetInputData(poly_data) + actor = vtk.vtkActor() + actor.SetMapper(mapper) + + # Create the renderer, render window, and interactor + renderer = vtk.vtkRenderer() + render_window = vtk.vtkRenderWindow() + render_window.SetOffScreenRendering(1) # Set it to 0 if there is an interactor + render_window.SetMultiSamples(0) + renderer.ResetCamera() + render_window.AddRenderer(renderer) + renderer.AddActor(actor) + + # Uncomment the following 2 lines to get an interactor + # render_window_interactor = vtk.vtkRenderWindowInteractor() + # render_window_interactor.SetRenderWindow(render_window) + + return renderer, render_window # , render_windowdow_interactor + + +def get_vtk_scalar_mode(poly_data: vtk.vtkPolyData, var_name: str) -> int: + """ + Given the var_name, get the scalar mode this var_name belongs to. + + Parameters + ---------- + poly_data: vtk.vtkPolyData + A VTK poly data object. + var_name: str + Variable name. + + Returns + ------- + int + An integer indicating the VTK scalar mode. + VTK_SCALAR_MODE_USE_POINT_FIELD_DATA or VTK_SCALAR_MODE_USE_CELL_FIELD_DATA. + + Raises + ------ + ValueError + If the given var_name is in neither point data nor cell data, + meaning the poly_data object does not have this variable. + """ + point_data = poly_data.GetPointData() + cell_data = poly_data.GetCellData() + point_data_array = point_data.GetArray(var_name) + cell_data_array = cell_data.GetArray(var_name) + if point_data_array is not None: + return vtk.VTK_SCALAR_MODE_USE_POINT_FIELD_DATA + if cell_data_array is not None: + return vtk.VTK_SCALAR_MODE_USE_CELL_FIELD_DATA + raise ValueError(f"{var_name} does not belong to point data, nor cell data") + + +def setup_value_pass( + poly_data: vtk.vtkPolyData, renderer: vtk.vtkRenderer, var_name: str +) -> vtk.vtkValuePass: + """ + Bind the variable data (point or cell) to value pass, in order to render the given + variable to each pixel. + + Parameters + ---------- + poly_data: vtk.vtkPolyData + A VTK poly data object. + renderer: vtk.vtkRenderer + A VTK renderer object. + var_name: str + Variable name. + + Returns + ------- + vtk.vtkValuePass + An integer indicating the VTK scalar mode. + VTK_SCALAR_MODE_USE_POINT_FIELD_DATA or VTK_SCALAR_MODE_USE_CELL_FIELD_DATA. + """ + value_pass = vtk.vtkValuePass() + vtk_scalar_mode = get_vtk_scalar_mode(poly_data, var_name) + value_pass.SetInputArrayToProcess(vtk_scalar_mode, var_name) + value_pass.SetInputComponentToProcess(0) + + passes = vtk.vtkRenderPassCollection() + passes.AddItem(value_pass) + + sequence = vtk.vtkSequencePass() + sequence.SetPasses(passes) + + camera_pass = vtk.vtkCameraPass() + camera_pass.SetDelegatePass(sequence) + renderer.SetPass(camera_pass) + + return value_pass + + +def get_rgb_value(render_window: vtk.vtkRenderWindow) -> np.ndarray: + """ + Get the RGB value from the render window. It starts from explicitly calling render + window's Render function. + + Parameters + ---------- + render_window: vtk.vtkRender + A VTK poly data object. + + Returns + ------- + vtk.vtkValuePass + A VTK value pass object for the following around of rendering. + """ + render_window.Render() + + width, height = render_window.GetSize() + + # Capture the rendering result + window_to_image_filter = vtk.vtkWindowToImageFilter() + window_to_image_filter.SetInput(render_window) + window_to_image_filter.Update() + + # Get the image data + image_data = window_to_image_filter.GetOutput() + + # Convert VTK image data to a NumPy array + width, height, _ = image_data.GetDimensions() + vtk_array = image_data.GetPointData().GetScalars() + np_array = vtk_to_numpy(vtk_array) + + # Reshape the array to a 3D array (height, width, 3) for RGB + np_array = np_array.reshape(height, width, -1) + + # If an interactor in involved, uncomment the next line + # render_window_interactor.Start() + + return np_array + + +def render_pick_data( + poly_data: vtk.vtkPolyData, renderer: vtk.vtkRenderer, render_window: vtk.vtkRenderWindow +) -> np.ndarray: + """ + Generate a buffer containing pick data from around of rendering by the value pass. + + Parameters + ---------- + poly_data: vtk.vtkPolyData + A VTK poly data object. + renderer: vtk.vtkRenderer + A VTK renderer object. + render_window: vtk.vtkRender + A VTK poly data object. + + Returns + ------- + np.ndarray + A numpy array as RGB format but only R and B channels are effective. + Specifically, R channel stores the lower 8 bits of the pick data; G channel + stores the higher 8. + """ + value_pass = setup_value_pass(poly_data, renderer, "Pick Data") + + render_window.Render() + + buffer = value_pass.GetFloatImageDataArray(renderer) + np_buffer = vtk_to_numpy(buffer) + + # Use NaN mask to eliminate NaN in np_buffer + width, height = render_window.GetSize() + np_buffer = np_buffer.reshape(height, width) + nan_mask = np.isnan(np_buffer) + np_buffer = np.where(nan_mask, 0, np_buffer) # Reset NaN to 0 + + np_buffer = np_buffer.astype(np.int16) + pick_buffer = np.zeros((height, width, 4), dtype=np.uint8) + + # Store the lower 8 bits to pick_buffer's R channel + pick_buffer[:, :, 0] = np_buffer & 0xFF + # Store the higher 8 bits to pick_buffer's G channel + pick_buffer[:, :, 1] = (np_buffer >> 8) & 0xFF + + return pick_buffer + + +def render_var_data( + poly_data: vtk.vtkPolyData, + renderer: vtk.vtkRenderer, + render_window: vtk.vtkRenderWindow, + var_name: str, +) -> np.ndarray: + """ + Generate a buffer containing variable data from a round of rendering by the value + pass. + + Parameters + ---------- + poly_data: vtk.vtkPolyData + A VTK poly data object. + renderer: vtk.vtkRenderer + A VTK renderer object. + render_window: vtk.vtkRender + A VTK poly data object. + var_name: str + The variable name. + + Returns + ------- + np.ndarray + A numpy array as float32 format. Each value represents the variable data on a pixel. + """ + value_pass = setup_value_pass(poly_data, renderer, var_name) + + render_window.Render() + + buffer = value_pass.GetFloatImageDataArray(renderer) + np_buffer = vtk_to_numpy(buffer) + width, height = render_window.GetSize() + np_buffer = np_buffer.reshape(height, width) + return np_buffer + + +def form_multipage_image( + json_data: Dict, + rgb_buffer: np.ndarray, + pick_buffer: np.ndarray, + var_buffer: np.ndarray, + output: Union[str, io.BytesIO], +) -> None: + """ + A helper function. Build up a multipage image and output to either a TIFF file on + disk or to a byte buffer. + + Parameters + ---------- + json_data: Dict + A dictionary that contains "parts" and "variables" sections. + rgb_buffer: np.ndarray + A int8 buffer with RGB values. Its dimension is [height, width, 3]. + pick_buffer: np.ndarray + A int8 buffer with pick data. Its dimension is [height, width, 3]. + var_buffer: np.ndarray + A float32 buffer with variable data. Its dimension is [height, width]. + output: Union[str, io.BytesIo] + Specify the output to be either a file name or a byte buffer. + """ + # json_data as metadata called image_description to store in the multipage image + image_description = json.dumps(json_data) + + # Create 3 images for each page + rgb_image = Image.fromarray(rgb_buffer, mode="RGB") + pick_image = Image.fromarray(pick_buffer, mode="RGBA") + var_image = Image.fromarray(var_buffer, mode="F") + + # Set up the metadata + tiffinfo = TiffImagePlugin.ImageFileDirectory_v2() + tiffinfo[TiffImagePlugin.IMAGEDESCRIPTION] = image_description + + rgb_image.save( + output, + format="TIFF", + save_all=True, + append_images=[pick_image, var_image], + tiffinfo=tiffinfo, + ) + + +def form_enhanced_image_as_tiff(json_data, rgb_buffer, pick_buffer, var_buffer, output_file_name): + """ + Generate a tiff file on disk. + + Parameters + ---------- + json_data: Dict + A dictionary that contains "parts" and "variables" sections. + rgb_buffer: np.ndarray + A int8 buffer with RGB values. Its dimension is [height, width, 3]. + pick_buffer: np.ndarray + A int8 buffer with pick data. Its dimension is [height, width, 3]. + var_buffer: np.ndarray + A float32 buffer with variable data. Its dimension is [height, width]. + output_file_name: str + The output TIFF file name. + """ + form_multipage_image(json_data, rgb_buffer, pick_buffer, var_buffer, output_file_name) + + +def form_enhanced_image_in_memory(json_data, rgb_buffer, pick_buffer, var_buffer) -> Image: + """ + Generate a multipage image in a byte buffer in memory. + + Parameters + ---------- + json_data: Dict + A dictionary that contains "parts" and "variables" sections. + rgb_buffer: np.ndarray + A int8 buffer with RGB values. Its dimension is [height, width, 3]. + pick_buffer: np.ndarray + A int8 buffer with pick data. Its dimension is [height, width, 3]. + var_buffer: np.ndarray + A float32 buffer with variable data. Its dimension is [height, width]. + + Returns + ------- + Image + A PIL Image object. + """ + # Create an in-memory bytes buffer + buffer = io.BytesIO() + form_multipage_image(json_data, rgb_buffer, pick_buffer, var_buffer, buffer) + buffer.seek(0) + image = Image.open(buffer) + return image + + +def generate_components_for_enhanced_image( + model: dpf.Model, var_field: dpf.Field +) -> Tuple[Dict, np.ndarray, np.ndarray, np.ndarray]: + """ + Esstential helper function for DPF inputs. Generate json metadata, rgb buffer, pick + data buffer and variable data buffer from a DPF model object and a DPF field object. + + Parameters + ---------- + model: dpf.Model + A DPF model object. + var_field: dpf.Field + A DPF field object that comes from the given model. The field is essentially + the variable in interest to visualize in an enhanced image. + + Returns + ------- + Tuple[Dict, np.ndarray, np.ndarray, np.ndarray] + A tuple of JSON metadata, rgb buffer, pick data buffer and variable data buffer + """ + # Todo: vector data support: is_scalar_data = var_data.ndim == 1 + + # Get components for metadata + var_unit: str = var_field.unit + var_name = var_field.name + var_meshed_region = var_field.meshed_region + dpf_unit_system = model.metadata.result_info.unit_system_name + unit_system_to_name = dpf_unit_system.split(":", 1)[0] + + mats: dpf.PropertyField = var_meshed_region.property_field("mat") # Pick data + + # Convert DPF to a pyvista UnstructuredGrid, which inherits from vtk + grid = vtk_helper.dpf_mesh_to_vtk(var_meshed_region) + # Add pick data + grid = vtk_helper.append_field_to_grid(mats, var_meshed_region, grid, "Pick Data") + # Add variable data + grid = vtk_helper.append_field_to_grid(var_field, var_meshed_region, grid, var_name) + + # Create a vtkGeometryFilter to convert UnstructuredGrid to PolyData + geometry_filter = vtk.vtkGeometryFilter() + geometry_filter.SetInputData(grid) + geometry_filter.Update() + poly_data = geometry_filter.GetOutput() + + renderer, render_window = setup_render_routine(poly_data) + rgb_buffer = get_rgb_value(render_window) + pick_buffer = render_pick_data(grid, renderer, render_window) + var_buffer = render_var_data(grid, renderer, render_window, var_name) + + # Todo: automatic colorby_var support + # global colorby_var_id + # colorby_var_int = colorby_var_id + # colorby_var_id += 1 + # colorby_var_decimal = 0 if is_scalar_data else 1 + # Todo: .1, .2, .3 corresponds to x, y, z dimension. Only supports scalar for now + # colorby_var = f"{colorby_var_int}.{colorby_var_decimal}" + + # For now, it only supports one part with one variable + json_data = { + "parts": [ + { + "name": "DPF sample", # hardcode + "id": str(mats.data[0]), + "colorby_var": "1.0", # colorby_var + } + ], + "variables": [ + { + "name": var_name, + "id": str(mats.data[0]), + "pal_id": "1", # colorby_var_int, + "unit_dims": "", + "unit_system_to_name": unit_system_to_name, + "unit_label": var_unit, + } + ], + } + + return json_data, rgb_buffer, pick_buffer, var_buffer + + +def generate_enhanced_image_as_tiff(model: dpf.Model, var_field: dpf.Field, output_file_name: str): + """ + Generate an enhanced image in the format of TIFF file on disk given DPF inputs. + + Parameters + ---------- + model: dpf.Model + A DPF model object. + var_field: dpf.Field + A DPF field object that comes from the given model. The field is essentially + the variable in interest to visualize in an enhanced image. + output_file_name: str + output TIFF file name with extension of .tiff or .tif + """ + json_data, rgb_buffer, pick_buffer, var_buffer = generate_components_for_enhanced_image( + model, var_field + ) + form_enhanced_image_as_tiff(json_data, rgb_buffer, pick_buffer, var_buffer, output_file_name) + + +def generate_enhanced_image_in_memory(model: dpf.Model, var_field: dpf.Field) -> Image: + """ + Generate an enhanced image as a PIL Image object given DPF inputs. + + Parameters + ---------- + model: dpf.Model + A DPF model object. + var_field: dpf.Field + A DPF field object that comes from the given model. The field is essentially + the variable in interest to visualize in an enhanced image. + + Returns + ------- + Image + A PIL Image object that represents the enhanced image. + """ + json_data, rgb_buffer, pick_buffer, var_buffer = generate_components_for_enhanced_image( + model, var_field + ) + return form_enhanced_image_in_memory(json_data, rgb_buffer, pick_buffer, var_buffer) From f44b11dfc3990a47ddde8767bd5803f22c1e9ce1 Mon Sep 17 00:00:00 2001 From: Yuanrui Zhang Date: Fri, 4 Oct 2024 15:25:20 -0400 Subject: [PATCH 02/34] Add a part name parameter to remove all hardcode --- .../core/utils/enhanced_images.py | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/ansys/dynamicreporting/core/utils/enhanced_images.py b/src/ansys/dynamicreporting/core/utils/enhanced_images.py index e3260bfd1..6963e8ec6 100644 --- a/src/ansys/dynamicreporting/core/utils/enhanced_images.py +++ b/src/ansys/dynamicreporting/core/utils/enhanced_images.py @@ -12,11 +12,12 @@ import io import json from typing import Dict, Tuple, Union +import numpy as np +import vtk from PIL import Image, TiffImagePlugin from ansys.dpf import core as dpf -import numpy as np -import vtk +from ansys.dpf.core import vtk_helper from vtk.util.numpy_support import vtk_to_numpy @@ -425,7 +426,7 @@ def form_enhanced_image_in_memory(json_data, rgb_buffer, pick_buffer, var_buffer def generate_components_for_enhanced_image( - model: dpf.Model, var_field: dpf.Field + model: dpf.Model, var_field: dpf.Field, part_name: str ) -> Tuple[Dict, np.ndarray, np.ndarray, np.ndarray]: """ Esstential helper function for DPF inputs. Generate json metadata, rgb buffer, pick @@ -438,6 +439,8 @@ def generate_components_for_enhanced_image( var_field: dpf.Field A DPF field object that comes from the given model. The field is essentially the variable in interest to visualize in an enhanced image. + part_name: str + The name of the part. It will showed on the interactive enhanced image in ADR. Returns ------- @@ -485,7 +488,7 @@ def generate_components_for_enhanced_image( json_data = { "parts": [ { - "name": "DPF sample", # hardcode + "name": part_name, "id": str(mats.data[0]), "colorby_var": "1.0", # colorby_var } @@ -505,7 +508,8 @@ def generate_components_for_enhanced_image( return json_data, rgb_buffer, pick_buffer, var_buffer -def generate_enhanced_image_as_tiff(model: dpf.Model, var_field: dpf.Field, output_file_name: str): +def generate_enhanced_image_as_tiff(model: dpf.Model, var_field: dpf.Field, + part_name: str, output_file_name: str): """ Generate an enhanced image in the format of TIFF file on disk given DPF inputs. @@ -516,16 +520,18 @@ def generate_enhanced_image_as_tiff(model: dpf.Model, var_field: dpf.Field, outp var_field: dpf.Field A DPF field object that comes from the given model. The field is essentially the variable in interest to visualize in an enhanced image. + part_name: str + The name of the part. It will showed on the interactive enhanced image in ADR. output_file_name: str output TIFF file name with extension of .tiff or .tif """ json_data, rgb_buffer, pick_buffer, var_buffer = generate_components_for_enhanced_image( - model, var_field + model, var_field, part_name ) form_enhanced_image_as_tiff(json_data, rgb_buffer, pick_buffer, var_buffer, output_file_name) -def generate_enhanced_image_in_memory(model: dpf.Model, var_field: dpf.Field) -> Image: +def generate_enhanced_image_in_memory(model: dpf.Model, var_field: dpf.Field, part_name: str) -> Image: """ Generate an enhanced image as a PIL Image object given DPF inputs. @@ -536,6 +542,8 @@ def generate_enhanced_image_in_memory(model: dpf.Model, var_field: dpf.Field) -> var_field: dpf.Field A DPF field object that comes from the given model. The field is essentially the variable in interest to visualize in an enhanced image. + part_name: str + The name of the part. It will showed on the interactive enhanced image in ADR. Returns ------- @@ -543,6 +551,6 @@ def generate_enhanced_image_in_memory(model: dpf.Model, var_field: dpf.Field) -> A PIL Image object that represents the enhanced image. """ json_data, rgb_buffer, pick_buffer, var_buffer = generate_components_for_enhanced_image( - model, var_field + model, var_field, part_name ) return form_enhanced_image_in_memory(json_data, rgb_buffer, pick_buffer, var_buffer) From 19b6b6d1fcf989bd187ee2ec8b8c8233c6e62f3e Mon Sep 17 00:00:00 2001 From: Yuanrui Zhang Date: Fri, 4 Oct 2024 15:45:10 -0400 Subject: [PATCH 03/34] Reformat. Remove functions only for testing --- .../core/utils/enhanced_images.py | 90 ++----------------- 1 file changed, 8 insertions(+), 82 deletions(-) diff --git a/src/ansys/dynamicreporting/core/utils/enhanced_images.py b/src/ansys/dynamicreporting/core/utils/enhanced_images.py index 6963e8ec6..3bf07fe5e 100644 --- a/src/ansys/dynamicreporting/core/utils/enhanced_images.py +++ b/src/ansys/dynamicreporting/core/utils/enhanced_images.py @@ -12,92 +12,15 @@ import io import json from typing import Dict, Tuple, Union -import numpy as np -import vtk from PIL import Image, TiffImagePlugin from ansys.dpf import core as dpf from ansys.dpf.core import vtk_helper +import numpy as np +import vtk from vtk.util.numpy_support import vtk_to_numpy -def create_sample_sphere(): - sphere = vtk.vtkSphereSource() - print(type(sphere)) - sphere.Update() - add_var_as_cell_data(sphere.GetOutput(), "Pick Data", lambda i: 3456) - add_var_as_cell_data(sphere.GetOutput(), "Temperature", lambda i: i % 10000) - - return sphere - - -def create_unstructured_grid(): - # Create the points - points = vtk.vtkPoints() - points.InsertNextPoint(0.0, 0.0, 0.0) - points.InsertNextPoint(1.0, 0.0, 0.0) - points.InsertNextPoint(1.0, 1.0, 0.0) - points.InsertNextPoint(0.0, 1.0, 0.0) - points.InsertNextPoint(0.0, 0.0, 1.0) - points.InsertNextPoint(1.0, 0.0, 1.0) - points.InsertNextPoint(1.0, 1.0, 1.0) - points.InsertNextPoint(0.0, 1.0, 1.0) - points.InsertNextPoint(2.0, 0.0, 0.0) - points.InsertNextPoint(2.0, 1.0, 0.0) - - # Create an unstructured grid - unstructured_grid = vtk.vtkUnstructuredGrid() - unstructured_grid.SetPoints(points) - - # Create a hexahedron cell (cube) - hexahedron = vtk.vtkHexahedron() - for i in range(8): - hexahedron.GetPointIds().SetId(i, i) # Add the first 8 points to form a hexahedron - - # Create a tetrahedron cell - tetra = vtk.vtkTetra() - tetra.GetPointIds().SetId(0, 0) - tetra.GetPointIds().SetId(1, 1) - tetra.GetPointIds().SetId(2, 8) - tetra.GetPointIds().SetId(3, 9) - - # Add the cells to the grid - unstructured_grid.InsertNextCell(hexahedron.GetCellType(), hexahedron.GetPointIds()) - unstructured_grid.InsertNextCell(tetra.GetCellType(), tetra.GetPointIds()) - - add_var_as_cell_data(unstructured_grid, "Pick Data", lambda i: 3456) - add_var_as_point_data(unstructured_grid, "Temperature", lambda i: i % 10000) - - return unstructured_grid - - -def add_var_as_cell_data(poly_data, var_name, val_calculator): - arr = vtk.vtkFloatArray() - arr.SetName(var_name) - arr.SetNumberOfComponents(1) - num_cells = poly_data.GetNumberOfCells() - arr.SetNumberOfTuples(num_cells) - - for i in range(num_cells): - arr.SetValue(i, val_calculator(i)) - - poly_data.GetCellData().AddArray(arr) - - -def add_var_as_point_data(poly_data, var_name, val_calculator): - arr = vtk.vtkFloatArray() - arr.SetName(var_name) - arr.SetNumberOfComponents(1) - num_points = poly_data.GetNumberOfPoints() - arr.SetNumberOfTuples(num_points) - - for i in range(num_points): - arr.SetValue(i, val_calculator(i)) - - poly_data.GetPointData().AddArray(arr) - # poly_data.GetOutput().GetPointData().SetActiveScalars(var_name) - - def setup_render_routine(poly_data: vtk.vtkPolyData) -> Tuple[vtk.vtkRenderer, vtk.vtkRenderWindow]: """ Set up VTK render routine, including mapper, actor, renderer and render window. @@ -508,8 +431,9 @@ def generate_components_for_enhanced_image( return json_data, rgb_buffer, pick_buffer, var_buffer -def generate_enhanced_image_as_tiff(model: dpf.Model, var_field: dpf.Field, - part_name: str, output_file_name: str): +def generate_enhanced_image_as_tiff( + model: dpf.Model, var_field: dpf.Field, part_name: str, output_file_name: str +): """ Generate an enhanced image in the format of TIFF file on disk given DPF inputs. @@ -531,7 +455,9 @@ def generate_enhanced_image_as_tiff(model: dpf.Model, var_field: dpf.Field, form_enhanced_image_as_tiff(json_data, rgb_buffer, pick_buffer, var_buffer, output_file_name) -def generate_enhanced_image_in_memory(model: dpf.Model, var_field: dpf.Field, part_name: str) -> Image: +def generate_enhanced_image_in_memory( + model: dpf.Model, var_field: dpf.Field, part_name: str +) -> Image: """ Generate an enhanced image as a PIL Image object given DPF inputs. From 8bbede58c0adb44ecbed4471f5f05fb3be8407e7 Mon Sep 17 00:00:00 2001 From: Yuanrui Zhang Date: Fri, 4 Oct 2024 16:08:36 -0400 Subject: [PATCH 04/34] Simplify the code --- .../core/utils/enhanced_images.py | 75 ++++--------------- 1 file changed, 13 insertions(+), 62 deletions(-) diff --git a/src/ansys/dynamicreporting/core/utils/enhanced_images.py b/src/ansys/dynamicreporting/core/utils/enhanced_images.py index 3bf07fe5e..598273562 100644 --- a/src/ansys/dynamicreporting/core/utils/enhanced_images.py +++ b/src/ansys/dynamicreporting/core/utils/enhanced_images.py @@ -255,7 +255,7 @@ def render_var_data( return np_buffer -def form_multipage_image( +def form_enhanced_image( json_data: Dict, rgb_buffer: np.ndarray, pick_buffer: np.ndarray, @@ -263,7 +263,7 @@ def form_multipage_image( output: Union[str, io.BytesIO], ) -> None: """ - A helper function. Build up a multipage image and output to either a TIFF file on + A helper function. Build up an enhanced image and output to either a TIFF file on disk or to a byte buffer. Parameters @@ -279,7 +279,7 @@ def form_multipage_image( output: Union[str, io.BytesIo] Specify the output to be either a file name or a byte buffer. """ - # json_data as metadata called image_description to store in the multipage image + # json_data as metadata called image_description to store in the enhanced image image_description = json.dumps(json_data) # Create 3 images for each page @@ -300,56 +300,8 @@ def form_multipage_image( ) -def form_enhanced_image_as_tiff(json_data, rgb_buffer, pick_buffer, var_buffer, output_file_name): - """ - Generate a tiff file on disk. - - Parameters - ---------- - json_data: Dict - A dictionary that contains "parts" and "variables" sections. - rgb_buffer: np.ndarray - A int8 buffer with RGB values. Its dimension is [height, width, 3]. - pick_buffer: np.ndarray - A int8 buffer with pick data. Its dimension is [height, width, 3]. - var_buffer: np.ndarray - A float32 buffer with variable data. Its dimension is [height, width]. - output_file_name: str - The output TIFF file name. - """ - form_multipage_image(json_data, rgb_buffer, pick_buffer, var_buffer, output_file_name) - - -def form_enhanced_image_in_memory(json_data, rgb_buffer, pick_buffer, var_buffer) -> Image: - """ - Generate a multipage image in a byte buffer in memory. - - Parameters - ---------- - json_data: Dict - A dictionary that contains "parts" and "variables" sections. - rgb_buffer: np.ndarray - A int8 buffer with RGB values. Its dimension is [height, width, 3]. - pick_buffer: np.ndarray - A int8 buffer with pick data. Its dimension is [height, width, 3]. - var_buffer: np.ndarray - A float32 buffer with variable data. Its dimension is [height, width]. - - Returns - ------- - Image - A PIL Image object. - """ - # Create an in-memory bytes buffer - buffer = io.BytesIO() - form_multipage_image(json_data, rgb_buffer, pick_buffer, var_buffer, buffer) - buffer.seek(0) - image = Image.open(buffer) - return image - - -def generate_components_for_enhanced_image( - model: dpf.Model, var_field: dpf.Field, part_name: str +def generate_enhanced_image( + model: dpf.Model, var_field: dpf.Field, part_name: str, output: Union[str, io.BytesIO] ) -> Tuple[Dict, np.ndarray, np.ndarray, np.ndarray]: """ Esstential helper function for DPF inputs. Generate json metadata, rgb buffer, pick @@ -428,7 +380,7 @@ def generate_components_for_enhanced_image( ], } - return json_data, rgb_buffer, pick_buffer, var_buffer + form_enhanced_image(json_data, rgb_buffer, pick_buffer, var_buffer, output) def generate_enhanced_image_as_tiff( @@ -449,10 +401,7 @@ def generate_enhanced_image_as_tiff( output_file_name: str output TIFF file name with extension of .tiff or .tif """ - json_data, rgb_buffer, pick_buffer, var_buffer = generate_components_for_enhanced_image( - model, var_field, part_name - ) - form_enhanced_image_as_tiff(json_data, rgb_buffer, pick_buffer, var_buffer, output_file_name) + generate_enhanced_image(model, var_field, part_name, output_file_name) def generate_enhanced_image_in_memory( @@ -476,7 +425,9 @@ def generate_enhanced_image_in_memory( Image A PIL Image object that represents the enhanced image. """ - json_data, rgb_buffer, pick_buffer, var_buffer = generate_components_for_enhanced_image( - model, var_field, part_name - ) - return form_enhanced_image_in_memory(json_data, rgb_buffer, pick_buffer, var_buffer) + # Create an in-memory bytes buffer + buffer = io.BytesIO() + generate_enhanced_image(model, var_field, part_name, buffer) + buffer.seek(0) + image = Image.open(buffer) + return image From 22ba8899517f124dbc01e69f4a51ad1e2b80d9f4 Mon Sep 17 00:00:00 2001 From: Yuanrui Zhang Date: Wed, 9 Oct 2024 10:20:49 -0400 Subject: [PATCH 05/34] Mark helper functions by appending '_' --- .../core/utils/enhanced_images.py | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/src/ansys/dynamicreporting/core/utils/enhanced_images.py b/src/ansys/dynamicreporting/core/utils/enhanced_images.py index 598273562..b2fc0c4dd 100644 --- a/src/ansys/dynamicreporting/core/utils/enhanced_images.py +++ b/src/ansys/dynamicreporting/core/utils/enhanced_images.py @@ -21,7 +21,9 @@ from vtk.util.numpy_support import vtk_to_numpy -def setup_render_routine(poly_data: vtk.vtkPolyData) -> Tuple[vtk.vtkRenderer, vtk.vtkRenderWindow]: +def _setup_render_routine( + poly_data: vtk.vtkPolyData, +) -> Tuple[vtk.vtkRenderer, vtk.vtkRenderWindow]: """ Set up VTK render routine, including mapper, actor, renderer and render window. @@ -57,7 +59,7 @@ def setup_render_routine(poly_data: vtk.vtkPolyData) -> Tuple[vtk.vtkRenderer, v return renderer, render_window # , render_windowdow_interactor -def get_vtk_scalar_mode(poly_data: vtk.vtkPolyData, var_name: str) -> int: +def _get_vtk_scalar_mode(poly_data: vtk.vtkPolyData, var_name: str) -> int: """ Given the var_name, get the scalar mode this var_name belongs to. @@ -91,7 +93,7 @@ def get_vtk_scalar_mode(poly_data: vtk.vtkPolyData, var_name: str) -> int: raise ValueError(f"{var_name} does not belong to point data, nor cell data") -def setup_value_pass( +def _setup_value_pass( poly_data: vtk.vtkPolyData, renderer: vtk.vtkRenderer, var_name: str ) -> vtk.vtkValuePass: """ @@ -114,7 +116,7 @@ def setup_value_pass( VTK_SCALAR_MODE_USE_POINT_FIELD_DATA or VTK_SCALAR_MODE_USE_CELL_FIELD_DATA. """ value_pass = vtk.vtkValuePass() - vtk_scalar_mode = get_vtk_scalar_mode(poly_data, var_name) + vtk_scalar_mode = _get_vtk_scalar_mode(poly_data, var_name) value_pass.SetInputArrayToProcess(vtk_scalar_mode, var_name) value_pass.SetInputComponentToProcess(0) @@ -131,7 +133,7 @@ def setup_value_pass( return value_pass -def get_rgb_value(render_window: vtk.vtkRenderWindow) -> np.ndarray: +def _get_rgb_value(render_window: vtk.vtkRenderWindow) -> np.ndarray: """ Get the RGB value from the render window. It starts from explicitly calling render window's Render function. @@ -172,7 +174,7 @@ def get_rgb_value(render_window: vtk.vtkRenderWindow) -> np.ndarray: return np_array -def render_pick_data( +def _render_pick_data( poly_data: vtk.vtkPolyData, renderer: vtk.vtkRenderer, render_window: vtk.vtkRenderWindow ) -> np.ndarray: """ @@ -194,7 +196,7 @@ def render_pick_data( Specifically, R channel stores the lower 8 bits of the pick data; G channel stores the higher 8. """ - value_pass = setup_value_pass(poly_data, renderer, "Pick Data") + value_pass = _setup_value_pass(poly_data, renderer, "Pick Data") render_window.Render() @@ -218,7 +220,7 @@ def render_pick_data( return pick_buffer -def render_var_data( +def _render_var_data( poly_data: vtk.vtkPolyData, renderer: vtk.vtkRenderer, render_window: vtk.vtkRenderWindow, @@ -244,7 +246,7 @@ def render_var_data( np.ndarray A numpy array as float32 format. Each value represents the variable data on a pixel. """ - value_pass = setup_value_pass(poly_data, renderer, var_name) + value_pass = _setup_value_pass(poly_data, renderer, var_name) render_window.Render() @@ -255,7 +257,7 @@ def render_var_data( return np_buffer -def form_enhanced_image( +def _form_enhanced_image( json_data: Dict, rgb_buffer: np.ndarray, pick_buffer: np.ndarray, @@ -300,7 +302,7 @@ def form_enhanced_image( ) -def generate_enhanced_image( +def _generate_enhanced_image( model: dpf.Model, var_field: dpf.Field, part_name: str, output: Union[str, io.BytesIO] ) -> Tuple[Dict, np.ndarray, np.ndarray, np.ndarray]: """ @@ -346,10 +348,10 @@ def generate_enhanced_image( geometry_filter.Update() poly_data = geometry_filter.GetOutput() - renderer, render_window = setup_render_routine(poly_data) - rgb_buffer = get_rgb_value(render_window) - pick_buffer = render_pick_data(grid, renderer, render_window) - var_buffer = render_var_data(grid, renderer, render_window, var_name) + renderer, render_window = _setup_render_routine(poly_data) + rgb_buffer = _get_rgb_value(render_window) + pick_buffer = _render_pick_data(grid, renderer, render_window) + var_buffer = _render_var_data(grid, renderer, render_window, var_name) # Todo: automatic colorby_var support # global colorby_var_id @@ -380,7 +382,7 @@ def generate_enhanced_image( ], } - form_enhanced_image(json_data, rgb_buffer, pick_buffer, var_buffer, output) + _form_enhanced_image(json_data, rgb_buffer, pick_buffer, var_buffer, output) def generate_enhanced_image_as_tiff( @@ -401,7 +403,7 @@ def generate_enhanced_image_as_tiff( output_file_name: str output TIFF file name with extension of .tiff or .tif """ - generate_enhanced_image(model, var_field, part_name, output_file_name) + _generate_enhanced_image(model, var_field, part_name, output_file_name) def generate_enhanced_image_in_memory( @@ -427,7 +429,7 @@ def generate_enhanced_image_in_memory( """ # Create an in-memory bytes buffer buffer = io.BytesIO() - generate_enhanced_image(model, var_field, part_name, buffer) + _generate_enhanced_image(model, var_field, part_name, buffer) buffer.seek(0) image = Image.open(buffer) return image From a72e6bdb08e850eb49682d4f8adfca9cb8866f43 Mon Sep 17 00:00:00 2001 From: Yuanrui Zhang Date: Wed, 9 Oct 2024 19:55:40 -0400 Subject: [PATCH 06/34] Check package dependency --- .../core/utils/enhanced_images.py | 125 ++++++++++-------- 1 file changed, 71 insertions(+), 54 deletions(-) diff --git a/src/ansys/dynamicreporting/core/utils/enhanced_images.py b/src/ansys/dynamicreporting/core/utils/enhanced_images.py index b2fc0c4dd..877f1e4f1 100644 --- a/src/ansys/dynamicreporting/core/utils/enhanced_images.py +++ b/src/ansys/dynamicreporting/core/utils/enhanced_images.py @@ -14,11 +14,78 @@ from typing import Dict, Tuple, Union from PIL import Image, TiffImagePlugin -from ansys.dpf import core as dpf -from ansys.dpf.core import vtk_helper import numpy as np -import vtk -from vtk.util.numpy_support import vtk_to_numpy + +# import vtk +# from vtk.util.numpy_support import vtk_to_numpy +# from ansys.dpf import core as dpf +# from ansys.dpf.core import vtk_helper + +try: + import vtk + from vtk.util.numpy_support import vtk_to_numpy + + HAS_VTK = True +except ModuleNotFoundError: + HAS_VTK = False + +try: + from ansys.dpf import core as dpf + from ansys.dpf.core import vtk_helper + + HAS_DPF = True +except ModuleNotFoundError: + HAS_DPF = False + +if HAS_VTK and HAS_DPF: + + def generate_enhanced_image_as_tiff( + model: dpf.Model, var_field: dpf.Field, part_name: str, output_file_name: str + ): + """ + Generate an enhanced image in the format of TIFF file on disk given DPF inputs. + + Parameters + ---------- + model: dpf.Model + A DPF model object. + var_field: dpf.Field + A DPF field object that comes from the given model. The field is essentially + the variable in interest to visualize in an enhanced image. + part_name: str + The name of the part. It will showed on the interactive enhanced image in ADR. + output_file_name: str + output TIFF file name with extension of .tiff or .tif + """ + _generate_enhanced_image(model, var_field, part_name, output_file_name) + + def generate_enhanced_image_in_memory( + model: dpf.Model, var_field: dpf.Field, part_name: str + ) -> Image: + """ + Generate an enhanced image as a PIL Image object given DPF inputs. + + Parameters + ---------- + model: dpf.Model + A DPF model object. + var_field: dpf.Field + A DPF field object that comes from the given model. The field is essentially + the variable in interest to visualize in an enhanced image. + part_name: str + The name of the part. It will showed on the interactive enhanced image in ADR. + + Returns + ------- + Image + A PIL Image object that represents the enhanced image. + """ + # Create an in-memory bytes buffer + buffer = io.BytesIO() + _generate_enhanced_image(model, var_field, part_name, buffer) + buffer.seek(0) + image = Image.open(buffer) + return image def _setup_render_routine( @@ -383,53 +450,3 @@ def _generate_enhanced_image( } _form_enhanced_image(json_data, rgb_buffer, pick_buffer, var_buffer, output) - - -def generate_enhanced_image_as_tiff( - model: dpf.Model, var_field: dpf.Field, part_name: str, output_file_name: str -): - """ - Generate an enhanced image in the format of TIFF file on disk given DPF inputs. - - Parameters - ---------- - model: dpf.Model - A DPF model object. - var_field: dpf.Field - A DPF field object that comes from the given model. The field is essentially - the variable in interest to visualize in an enhanced image. - part_name: str - The name of the part. It will showed on the interactive enhanced image in ADR. - output_file_name: str - output TIFF file name with extension of .tiff or .tif - """ - _generate_enhanced_image(model, var_field, part_name, output_file_name) - - -def generate_enhanced_image_in_memory( - model: dpf.Model, var_field: dpf.Field, part_name: str -) -> Image: - """ - Generate an enhanced image as a PIL Image object given DPF inputs. - - Parameters - ---------- - model: dpf.Model - A DPF model object. - var_field: dpf.Field - A DPF field object that comes from the given model. The field is essentially - the variable in interest to visualize in an enhanced image. - part_name: str - The name of the part. It will showed on the interactive enhanced image in ADR. - - Returns - ------- - Image - A PIL Image object that represents the enhanced image. - """ - # Create an in-memory bytes buffer - buffer = io.BytesIO() - _generate_enhanced_image(model, var_field, part_name, buffer) - buffer.seek(0) - image = Image.open(buffer) - return image From 07bb501ad14a9303d448624852cb177fec3f2589 Mon Sep 17 00:00:00 2001 From: Yuanrui Zhang Date: Thu, 10 Oct 2024 13:40:00 -0400 Subject: [PATCH 07/34] Backup _test_enhanced_images.py --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index f1c7aa13e..a32f298b6 100644 --- a/.gitignore +++ b/.gitignore @@ -72,4 +72,5 @@ venv/ doc/_build # TIFF files -*.tiff \ No newline at end of file +*.tiff +_test_enhanced_images.py From dc07365cb6987b75c0ecea463893ab50be1cf9e9 Mon Sep 17 00:00:00 2001 From: Yuanrui Zhang Date: Thu, 10 Oct 2024 14:10:15 -0400 Subject: [PATCH 08/34] properly teardown for in-mem version --- src/ansys/dynamicreporting/core/utils/enhanced_images.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/ansys/dynamicreporting/core/utils/enhanced_images.py b/src/ansys/dynamicreporting/core/utils/enhanced_images.py index 877f1e4f1..2ca61a93d 100644 --- a/src/ansys/dynamicreporting/core/utils/enhanced_images.py +++ b/src/ansys/dynamicreporting/core/utils/enhanced_images.py @@ -84,7 +84,8 @@ def generate_enhanced_image_in_memory( buffer = io.BytesIO() _generate_enhanced_image(model, var_field, part_name, buffer) buffer.seek(0) - image = Image.open(buffer) + with Image.open(buffer) as image: + image.load() return image @@ -340,11 +341,11 @@ def _form_enhanced_image( json_data: Dict A dictionary that contains "parts" and "variables" sections. rgb_buffer: np.ndarray - A int8 buffer with RGB values. Its dimension is [height, width, 3]. + An int8 buffer with RGB values. Its dimension is [height, width, 3]. pick_buffer: np.ndarray - A int8 buffer with pick data. Its dimension is [height, width, 3]. + An int8 buffer with pick data. Its dimension is [height, width, 3]. var_buffer: np.ndarray - A float32 buffer with variable data. Its dimension is [height, width]. + An float32 buffer with variable data. Its dimension is [height, width]. output: Union[str, io.BytesIo] Specify the output to be either a file name or a byte buffer. """ From e4a7767af80a312dda6cf766041207f337b6a79a Mon Sep 17 00:00:00 2001 From: Yuanrui Zhang Date: Thu, 10 Oct 2024 22:46:42 -0400 Subject: [PATCH 09/34] Switch to returning a buffer instead of an image --- .../dynamicreporting/core/utils/enhanced_images.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/ansys/dynamicreporting/core/utils/enhanced_images.py b/src/ansys/dynamicreporting/core/utils/enhanced_images.py index 2ca61a93d..fd283dbd2 100644 --- a/src/ansys/dynamicreporting/core/utils/enhanced_images.py +++ b/src/ansys/dynamicreporting/core/utils/enhanced_images.py @@ -61,7 +61,7 @@ def generate_enhanced_image_as_tiff( def generate_enhanced_image_in_memory( model: dpf.Model, var_field: dpf.Field, part_name: str - ) -> Image: + ) -> io.BytesIO: """ Generate an enhanced image as a PIL Image object given DPF inputs. @@ -77,16 +77,15 @@ def generate_enhanced_image_in_memory( Returns ------- - Image - A PIL Image object that represents the enhanced image. + buffer + A IO buffer that represents the enhanced image. + The returned buffer can be opened by PIL Image.open """ # Create an in-memory bytes buffer buffer = io.BytesIO() _generate_enhanced_image(model, var_field, part_name, buffer) buffer.seek(0) - with Image.open(buffer) as image: - image.load() - return image + return buffer def _setup_render_routine( From b72274e91283521bd5d142d50cbe584ad7197f44 Mon Sep 17 00:00:00 2001 From: Yuanrui Zhang Date: Thu, 10 Oct 2024 22:47:05 -0400 Subject: [PATCH 10/34] Finish testing. --- tests/test_enhanced_images.py | 83 +++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 tests/test_enhanced_images.py diff --git a/tests/test_enhanced_images.py b/tests/test_enhanced_images.py new file mode 100644 index 000000000..643663ebc --- /dev/null +++ b/tests/test_enhanced_images.py @@ -0,0 +1,83 @@ +import json +import sys + +from PIL import Image +from PIL.TiffTags import TAGS +from ansys.dpf import core as dpf +from ansys.dpf.core import examples + +# from ansys.dynamicreporting.core.utils import enhanced_images as ei + +import pytest + +from ansys.dynamicreporting.core.utils import report_utils as ru + +sys.path.append( + "C:\\Users\\yuzhang\\ADRdev\\pydynamicreporting\\src\\ansys\\dynamicreporting\\core\\utils" +) +import enhanced_images as ei + +def get_dpf_model_field_example(): + model = dpf.Model(examples.find_electric_therm()) + results = model.results + electric_potential = results.electric_potential() + fields = electric_potential.outputs.fields_container() + potential = fields[0] + + return model, potential + + +def setup_dpf_tiff_generation(): + model, field = get_dpf_model_field_example() + + tiff_name = "dpf_find_electric_therm.tiff" + ei.generate_enhanced_image_as_tiff(model, field, "DPF Sample", tiff_name) + + image = Image.open(tiff_name) + yield image + image.close() + + +def setup_dpf_inmem_generation(): + model, field = get_dpf_model_field_example() + buffer = ei.generate_enhanced_image_in_memory(model, field, "DPF Sample") + + image = Image.open(buffer) + yield image + image.close() + + +@pytest.fixture(params=[setup_dpf_tiff_generation, setup_dpf_inmem_generation]) +def setup_generation_flow(request): + return next(request.param()) + + +def test_basic_format(setup_generation_flow): + image = setup_generation_flow + image.seek(0) + result = ru.is_enhanced(image) + assert result is not None + + +def test_image_description(setup_generation_flow): + image = setup_generation_flow + image.seek(0) + metadata_dict = {TAGS[key]: image.tag[key] for key in image.tag_v2} + image_description = json.loads(metadata_dict["ImageDescription"][0]) + part_info = image_description["parts"][0] + var_info = image_description["variables"][0] + + assert ( + part_info["name"] == "DPF Sample" + and part_info["id"] == "1" + and part_info["colorby_var"] == "1.0" + ) + + assert ( + var_info["name"] == "electric_potential_1.s" + and var_info["id"] == "1" + and var_info["pal_id"] == "1" + and var_info["unit_dims"] == "" + and var_info["unit_system_to_name"] == "MKS" + and var_info["unit_label"] == "V" + ) From a1dfbf2cc834047057c81911ff25eebf058ad79a Mon Sep 17 00:00:00 2001 From: Yuanrui Zhang Date: Tue, 15 Oct 2024 14:56:53 -0400 Subject: [PATCH 11/34] Add vtk and dpf dependency --- .github/workflows/ci_cd.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index fb8c9fe13..34cd89069 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -89,6 +89,12 @@ jobs: - name: Pull Docker container run: make pull-docker + - name: Install Python Dependencies + run: | + python -m pip install --upgrade pip + pip install vtk + pip install ansys-dpf-core + - name: Run pytest uses: ansys/actions/tests-pytest@v4 env: From a8c1936e1eb2b943ee00a050a8262c59450c3b0e Mon Sep 17 00:00:00 2001 From: Yuanrui Zhang Date: Tue, 15 Oct 2024 15:21:15 -0400 Subject: [PATCH 12/34] Finish the initial test script for enhanced images --- tests/test_enhanced_images.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/tests/test_enhanced_images.py b/tests/test_enhanced_images.py index 643663ebc..9703a87f4 100644 --- a/tests/test_enhanced_images.py +++ b/tests/test_enhanced_images.py @@ -5,17 +5,16 @@ from PIL.TiffTags import TAGS from ansys.dpf import core as dpf from ansys.dpf.core import examples - -# from ansys.dynamicreporting.core.utils import enhanced_images as ei - import pytest +from ansys.dynamicreporting.core.utils import enhanced_images as ei from ansys.dynamicreporting.core.utils import report_utils as ru -sys.path.append( - "C:\\Users\\yuzhang\\ADRdev\\pydynamicreporting\\src\\ansys\\dynamicreporting\\core\\utils" -) -import enhanced_images as ei +# sys.path.append( +# "C:\\Users\\yuzhang\\ADRdev\\pydynamicreporting\\src\\ansys\\dynamicreporting\\core\\utils" +# ) +# import enhanced_images as ei + def get_dpf_model_field_example(): model = dpf.Model(examples.find_electric_therm()) From 622f1b5906ef9e26762767b3663751d32a1300c5 Mon Sep 17 00:00:00 2001 From: Yuanrui Zhang Date: Tue, 15 Oct 2024 23:16:16 -0400 Subject: [PATCH 13/34] Enable the test to install required packages --- .github/workflows/ci_cd.yml | 21 ++++++++------------- .github/workflows/nightly.yml | 10 +++------- Makefile | 6 ++---- pyproject.toml | 6 +++++- 4 files changed, 18 insertions(+), 25 deletions(-) diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index 843562ef9..463a41219 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -69,12 +69,12 @@ jobs: test: name: Testing - needs: [smoke-tests] + # needs: [smoke-tests] runs-on: ${{ matrix.os }} strategy: matrix: os: [ ubuntu-latest ] - python-version: [ '3.9', '3.10', '3.11', '3.12' ] + python-version: [ '3.9' ] #, '3.10', '3.11', '3.12' ] steps: - uses: actions/checkout@v4 @@ -89,20 +89,15 @@ jobs: - name: Pull Docker container run: make pull-docker - - name: Install Python Dependencies - run: | - python -m pip install --upgrade pip - pip install vtk - pip install ansys-dpf-core + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} - name: Run pytest - uses: ansys/actions/tests-pytest@v8 + run: make test env: - ANSYSLMD_LICENSE_FILE: ${{ format('1055@{0}', secrets.LICENSE_SERVER )}} - with: - checkout: false - python-version: ${{ env.MAIN_PYTHON_VERSION }} - pytest-extra-args: -rvx --setup-show --cov=ansys.dynamicreporting --cov-report html:coverage-html --cov-report term --cov-report xml:coverage.xml + ANSYSLMD_LICENSE_FILE: ${{ format('1055@{0}', secrets.LICENSE_SERVER) }} - name: Upload coverage report if: env.MAIN_PYTHON_VERSION == matrix.python-version diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 263d9b2de..46f897fff 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -65,14 +65,10 @@ jobs: run: make pull-docker - name: Run pytest - uses: ansys/actions/tests-pytest@v8 + run: make test env: - ANSYSLMD_LICENSE_FILE: ${{ format('1055@{0}', secrets.LICENSE_SERVER )}} - with: - checkout: false - python-version: ${{ env.MAIN_PYTHON_VERSION }} - pytest-extra-args: -rvx --setup-show --cov=ansys.dynamicreporting --cov-report html:coverage-html --cov-report term --cov-report xml:coverage.xml - + ANSYSLMD_LICENSE_FILE: ${{ format('1055@{0}', secrets.LICENSE_SERVER) }} + nightly_and_upload: name: nightly_and_upload runs-on: ubuntu-latest diff --git a/Makefile b/Makefile index 319f38598..d5dc7fa0e 100644 --- a/Makefile +++ b/Makefile @@ -27,10 +27,8 @@ pull-docker: bash .ci/pull_adr_image.sh test: - pytest -rvx --setup-show --cov=ansys.dynamicreporting.core \ - --cov-report html:coverage-html \ - --cov-report term \ - --cov-report xml:coverage.xml + pip install -e .[test] + pytest -rvx --setup-show --cov=ansys.dynamicreporting.core --cov-report html:coverage-html --cov-report term --cov-report xml:coverage.xml smoketest: python -c "from ansys.dynamicreporting.core import __version__; print(__version__)" diff --git a/pyproject.toml b/pyproject.toml index 9ba7ae160..ba093fcac 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,12 +70,16 @@ repository = "https://github.com/ansys/pydynamicreporting" ci = "https://github.com/ansys/pydynamicreporting/actions" [project.optional-dependencies] -tests = [ +test = [ "docker>=7.1.0", "numpy==1.25.1", "psutil==6.0.0", + "exceptiongroup==1.0.0", "pytest==8.3.3", "pytest-cov==4.1.0", + "pyvista==0.44.1", + "vtk==9.3.1", + "ansys-dpf-core==0.13.0", ] doc = [ "ansys-sphinx-theme==0.12.4", From 6142549efb17d2261ce69a8340c77c3337f1706e Mon Sep 17 00:00:00 2001 From: Yuanrui Zhang Date: Wed, 16 Oct 2024 12:16:35 -0400 Subject: [PATCH 14/34] reformat --- .github/workflows/nightly.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 46f897fff..ca5cf11e9 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -68,7 +68,7 @@ jobs: run: make test env: ANSYSLMD_LICENSE_FILE: ${{ format('1055@{0}', secrets.LICENSE_SERVER) }} - + nightly_and_upload: name: nightly_and_upload runs-on: ubuntu-latest From 468e02bce177ad0e199cf328300f10c7f404db98 Mon Sep 17 00:00:00 2001 From: Yuanrui Zhang Date: Wed, 16 Oct 2024 12:20:27 -0400 Subject: [PATCH 15/34] Add # pragma: no cover; make all the helper functions under the module check --- .../core/utils/enhanced_images.py | 722 +++++++++--------- 1 file changed, 357 insertions(+), 365 deletions(-) diff --git a/src/ansys/dynamicreporting/core/utils/enhanced_images.py b/src/ansys/dynamicreporting/core/utils/enhanced_images.py index fd283dbd2..95d30e1e4 100644 --- a/src/ansys/dynamicreporting/core/utils/enhanced_images.py +++ b/src/ansys/dynamicreporting/core/utils/enhanced_images.py @@ -26,7 +26,7 @@ from vtk.util.numpy_support import vtk_to_numpy HAS_VTK = True -except ModuleNotFoundError: +except ImportError: HAS_VTK = False try: @@ -34,10 +34,10 @@ from ansys.dpf.core import vtk_helper HAS_DPF = True -except ModuleNotFoundError: +except (ImportError, ValueError): HAS_DPF = False -if HAS_VTK and HAS_DPF: +if HAS_VTK and HAS_DPF: # pragma: no cover def generate_enhanced_image_as_tiff( model: dpf.Model, var_field: dpf.Field, part_name: str, output_file_name: str @@ -87,366 +87,358 @@ def generate_enhanced_image_in_memory( buffer.seek(0) return buffer + def _setup_render_routine( + poly_data: vtk.vtkPolyData, + ) -> Tuple[vtk.vtkRenderer, vtk.vtkRenderWindow]: + """ + Set up VTK render routine, including mapper, actor, renderer and render window. + + Parameters + ---------- + poly_data: vtk.vtkPolyData + A VTK poly data object. + + Returns + ------- + Tuple[vtk.vtkRenderer, vtk.vtkRenderWindow] + A pair of VTK renderer and render windown object. + """ + # Mapper and actor + mapper = vtk.vtkPolyDataMapper() + mapper.SetInputData(poly_data) + actor = vtk.vtkActor() + actor.SetMapper(mapper) + + # Create the renderer, render window, and interactor + renderer = vtk.vtkRenderer() + render_window = vtk.vtkRenderWindow() + render_window.SetOffScreenRendering(1) # Set it to 0 if there is an interactor + render_window.SetMultiSamples(0) + renderer.ResetCamera() + render_window.AddRenderer(renderer) + renderer.AddActor(actor) + + # Uncomment the following 2 lines to get an interactor + # render_window_interactor = vtk.vtkRenderWindowInteractor() + # render_window_interactor.SetRenderWindow(render_window) + + return renderer, render_window # , render_windowdow_interactor + + def _get_vtk_scalar_mode(poly_data: vtk.vtkPolyData, var_name: str) -> int: + """ + Given the var_name, get the scalar mode this var_name belongs to. + + Parameters + ---------- + poly_data: vtk.vtkPolyData + A VTK poly data object. + var_name: str + Variable name. + + Returns + ------- + int + An integer indicating the VTK scalar mode. + VTK_SCALAR_MODE_USE_POINT_FIELD_DATA or VTK_SCALAR_MODE_USE_CELL_FIELD_DATA. + + Raises + ------ + ValueError + If the given var_name is in neither point data nor cell data, + meaning the poly_data object does not have this variable. + """ + point_data = poly_data.GetPointData() + cell_data = poly_data.GetCellData() + point_data_array = point_data.GetArray(var_name) + cell_data_array = cell_data.GetArray(var_name) + if point_data_array is not None: + return vtk.VTK_SCALAR_MODE_USE_POINT_FIELD_DATA + if cell_data_array is not None: + return vtk.VTK_SCALAR_MODE_USE_CELL_FIELD_DATA + raise ValueError(f"{var_name} does not belong to point data, nor cell data") + + def _setup_value_pass( + poly_data: vtk.vtkPolyData, renderer: vtk.vtkRenderer, var_name: str + ) -> vtk.vtkValuePass: + """ + Bind the variable data (point or cell) to value pass, in order to render the given + variable to each pixel. + + Parameters + ---------- + poly_data: vtk.vtkPolyData + A VTK poly data object. + renderer: vtk.vtkRenderer + A VTK renderer object. + var_name: str + Variable name. + + Returns + ------- + vtk.vtkValuePass + An integer indicating the VTK scalar mode. + VTK_SCALAR_MODE_USE_POINT_FIELD_DATA or VTK_SCALAR_MODE_USE_CELL_FIELD_DATA. + """ + value_pass = vtk.vtkValuePass() + vtk_scalar_mode = _get_vtk_scalar_mode(poly_data, var_name) + value_pass.SetInputArrayToProcess(vtk_scalar_mode, var_name) + value_pass.SetInputComponentToProcess(0) + + passes = vtk.vtkRenderPassCollection() + passes.AddItem(value_pass) + + sequence = vtk.vtkSequencePass() + sequence.SetPasses(passes) + + camera_pass = vtk.vtkCameraPass() + camera_pass.SetDelegatePass(sequence) + renderer.SetPass(camera_pass) + + return value_pass + + def _get_rgb_value(render_window: vtk.vtkRenderWindow) -> np.ndarray: + """ + Get the RGB value from the render window. It starts from explicitly calling render + window's Render function. + + Parameters + ---------- + render_window: vtk.vtkRender + A VTK poly data object. + + Returns + ------- + vtk.vtkValuePass + A VTK value pass object for the following around of rendering. + """ + render_window.Render() + + width, height = render_window.GetSize() + + # Capture the rendering result + window_to_image_filter = vtk.vtkWindowToImageFilter() + window_to_image_filter.SetInput(render_window) + window_to_image_filter.Update() + + # Get the image data + image_data = window_to_image_filter.GetOutput() + + # Convert VTK image data to a NumPy array + width, height, _ = image_data.GetDimensions() + vtk_array = image_data.GetPointData().GetScalars() + np_array = vtk_to_numpy(vtk_array) + + # Reshape the array to a 3D array (height, width, 3) for RGB + np_array = np_array.reshape(height, width, -1) + + # If an interactor in involved, uncomment the next line + # render_window_interactor.Start() + + return np_array -def _setup_render_routine( - poly_data: vtk.vtkPolyData, -) -> Tuple[vtk.vtkRenderer, vtk.vtkRenderWindow]: - """ - Set up VTK render routine, including mapper, actor, renderer and render window. - - Parameters - ---------- - poly_data: vtk.vtkPolyData - A VTK poly data object. - - Returns - ------- - Tuple[vtk.vtkRenderer, vtk.vtkRenderWindow] - A pair of VTK renderer and render windown object. - """ - # Mapper and actor - mapper = vtk.vtkPolyDataMapper() - mapper.SetInputData(poly_data) - actor = vtk.vtkActor() - actor.SetMapper(mapper) - - # Create the renderer, render window, and interactor - renderer = vtk.vtkRenderer() - render_window = vtk.vtkRenderWindow() - render_window.SetOffScreenRendering(1) # Set it to 0 if there is an interactor - render_window.SetMultiSamples(0) - renderer.ResetCamera() - render_window.AddRenderer(renderer) - renderer.AddActor(actor) - - # Uncomment the following 2 lines to get an interactor - # render_window_interactor = vtk.vtkRenderWindowInteractor() - # render_window_interactor.SetRenderWindow(render_window) - - return renderer, render_window # , render_windowdow_interactor - - -def _get_vtk_scalar_mode(poly_data: vtk.vtkPolyData, var_name: str) -> int: - """ - Given the var_name, get the scalar mode this var_name belongs to. - - Parameters - ---------- - poly_data: vtk.vtkPolyData - A VTK poly data object. - var_name: str - Variable name. - - Returns - ------- - int - An integer indicating the VTK scalar mode. - VTK_SCALAR_MODE_USE_POINT_FIELD_DATA or VTK_SCALAR_MODE_USE_CELL_FIELD_DATA. - - Raises - ------ - ValueError - If the given var_name is in neither point data nor cell data, - meaning the poly_data object does not have this variable. - """ - point_data = poly_data.GetPointData() - cell_data = poly_data.GetCellData() - point_data_array = point_data.GetArray(var_name) - cell_data_array = cell_data.GetArray(var_name) - if point_data_array is not None: - return vtk.VTK_SCALAR_MODE_USE_POINT_FIELD_DATA - if cell_data_array is not None: - return vtk.VTK_SCALAR_MODE_USE_CELL_FIELD_DATA - raise ValueError(f"{var_name} does not belong to point data, nor cell data") - - -def _setup_value_pass( - poly_data: vtk.vtkPolyData, renderer: vtk.vtkRenderer, var_name: str -) -> vtk.vtkValuePass: - """ - Bind the variable data (point or cell) to value pass, in order to render the given - variable to each pixel. - - Parameters - ---------- - poly_data: vtk.vtkPolyData - A VTK poly data object. - renderer: vtk.vtkRenderer - A VTK renderer object. - var_name: str - Variable name. - - Returns - ------- - vtk.vtkValuePass - An integer indicating the VTK scalar mode. - VTK_SCALAR_MODE_USE_POINT_FIELD_DATA or VTK_SCALAR_MODE_USE_CELL_FIELD_DATA. - """ - value_pass = vtk.vtkValuePass() - vtk_scalar_mode = _get_vtk_scalar_mode(poly_data, var_name) - value_pass.SetInputArrayToProcess(vtk_scalar_mode, var_name) - value_pass.SetInputComponentToProcess(0) - - passes = vtk.vtkRenderPassCollection() - passes.AddItem(value_pass) - - sequence = vtk.vtkSequencePass() - sequence.SetPasses(passes) - - camera_pass = vtk.vtkCameraPass() - camera_pass.SetDelegatePass(sequence) - renderer.SetPass(camera_pass) - - return value_pass - - -def _get_rgb_value(render_window: vtk.vtkRenderWindow) -> np.ndarray: - """ - Get the RGB value from the render window. It starts from explicitly calling render - window's Render function. - - Parameters - ---------- - render_window: vtk.vtkRender - A VTK poly data object. - - Returns - ------- - vtk.vtkValuePass - A VTK value pass object for the following around of rendering. - """ - render_window.Render() - - width, height = render_window.GetSize() - - # Capture the rendering result - window_to_image_filter = vtk.vtkWindowToImageFilter() - window_to_image_filter.SetInput(render_window) - window_to_image_filter.Update() - - # Get the image data - image_data = window_to_image_filter.GetOutput() - - # Convert VTK image data to a NumPy array - width, height, _ = image_data.GetDimensions() - vtk_array = image_data.GetPointData().GetScalars() - np_array = vtk_to_numpy(vtk_array) - - # Reshape the array to a 3D array (height, width, 3) for RGB - np_array = np_array.reshape(height, width, -1) - - # If an interactor in involved, uncomment the next line - # render_window_interactor.Start() - - return np_array - - -def _render_pick_data( - poly_data: vtk.vtkPolyData, renderer: vtk.vtkRenderer, render_window: vtk.vtkRenderWindow -) -> np.ndarray: - """ - Generate a buffer containing pick data from around of rendering by the value pass. - - Parameters - ---------- - poly_data: vtk.vtkPolyData - A VTK poly data object. - renderer: vtk.vtkRenderer - A VTK renderer object. - render_window: vtk.vtkRender - A VTK poly data object. - - Returns - ------- - np.ndarray - A numpy array as RGB format but only R and B channels are effective. - Specifically, R channel stores the lower 8 bits of the pick data; G channel - stores the higher 8. - """ - value_pass = _setup_value_pass(poly_data, renderer, "Pick Data") - - render_window.Render() - - buffer = value_pass.GetFloatImageDataArray(renderer) - np_buffer = vtk_to_numpy(buffer) - - # Use NaN mask to eliminate NaN in np_buffer - width, height = render_window.GetSize() - np_buffer = np_buffer.reshape(height, width) - nan_mask = np.isnan(np_buffer) - np_buffer = np.where(nan_mask, 0, np_buffer) # Reset NaN to 0 - - np_buffer = np_buffer.astype(np.int16) - pick_buffer = np.zeros((height, width, 4), dtype=np.uint8) - - # Store the lower 8 bits to pick_buffer's R channel - pick_buffer[:, :, 0] = np_buffer & 0xFF - # Store the higher 8 bits to pick_buffer's G channel - pick_buffer[:, :, 1] = (np_buffer >> 8) & 0xFF - - return pick_buffer - - -def _render_var_data( - poly_data: vtk.vtkPolyData, - renderer: vtk.vtkRenderer, - render_window: vtk.vtkRenderWindow, - var_name: str, -) -> np.ndarray: - """ - Generate a buffer containing variable data from a round of rendering by the value - pass. - - Parameters - ---------- - poly_data: vtk.vtkPolyData - A VTK poly data object. - renderer: vtk.vtkRenderer - A VTK renderer object. - render_window: vtk.vtkRender - A VTK poly data object. - var_name: str - The variable name. - - Returns - ------- - np.ndarray - A numpy array as float32 format. Each value represents the variable data on a pixel. - """ - value_pass = _setup_value_pass(poly_data, renderer, var_name) - - render_window.Render() - - buffer = value_pass.GetFloatImageDataArray(renderer) - np_buffer = vtk_to_numpy(buffer) - width, height = render_window.GetSize() - np_buffer = np_buffer.reshape(height, width) - return np_buffer - - -def _form_enhanced_image( - json_data: Dict, - rgb_buffer: np.ndarray, - pick_buffer: np.ndarray, - var_buffer: np.ndarray, - output: Union[str, io.BytesIO], -) -> None: - """ - A helper function. Build up an enhanced image and output to either a TIFF file on - disk or to a byte buffer. - - Parameters - ---------- - json_data: Dict - A dictionary that contains "parts" and "variables" sections. - rgb_buffer: np.ndarray - An int8 buffer with RGB values. Its dimension is [height, width, 3]. - pick_buffer: np.ndarray - An int8 buffer with pick data. Its dimension is [height, width, 3]. - var_buffer: np.ndarray - An float32 buffer with variable data. Its dimension is [height, width]. - output: Union[str, io.BytesIo] - Specify the output to be either a file name or a byte buffer. - """ - # json_data as metadata called image_description to store in the enhanced image - image_description = json.dumps(json_data) - - # Create 3 images for each page - rgb_image = Image.fromarray(rgb_buffer, mode="RGB") - pick_image = Image.fromarray(pick_buffer, mode="RGBA") - var_image = Image.fromarray(var_buffer, mode="F") - - # Set up the metadata - tiffinfo = TiffImagePlugin.ImageFileDirectory_v2() - tiffinfo[TiffImagePlugin.IMAGEDESCRIPTION] = image_description - - rgb_image.save( - output, - format="TIFF", - save_all=True, - append_images=[pick_image, var_image], - tiffinfo=tiffinfo, - ) - - -def _generate_enhanced_image( - model: dpf.Model, var_field: dpf.Field, part_name: str, output: Union[str, io.BytesIO] -) -> Tuple[Dict, np.ndarray, np.ndarray, np.ndarray]: - """ - Esstential helper function for DPF inputs. Generate json metadata, rgb buffer, pick - data buffer and variable data buffer from a DPF model object and a DPF field object. - - Parameters - ---------- - model: dpf.Model - A DPF model object. - var_field: dpf.Field - A DPF field object that comes from the given model. The field is essentially - the variable in interest to visualize in an enhanced image. - part_name: str - The name of the part. It will showed on the interactive enhanced image in ADR. - - Returns - ------- - Tuple[Dict, np.ndarray, np.ndarray, np.ndarray] - A tuple of JSON metadata, rgb buffer, pick data buffer and variable data buffer - """ - # Todo: vector data support: is_scalar_data = var_data.ndim == 1 - - # Get components for metadata - var_unit: str = var_field.unit - var_name = var_field.name - var_meshed_region = var_field.meshed_region - dpf_unit_system = model.metadata.result_info.unit_system_name - unit_system_to_name = dpf_unit_system.split(":", 1)[0] - - mats: dpf.PropertyField = var_meshed_region.property_field("mat") # Pick data - - # Convert DPF to a pyvista UnstructuredGrid, which inherits from vtk - grid = vtk_helper.dpf_mesh_to_vtk(var_meshed_region) - # Add pick data - grid = vtk_helper.append_field_to_grid(mats, var_meshed_region, grid, "Pick Data") - # Add variable data - grid = vtk_helper.append_field_to_grid(var_field, var_meshed_region, grid, var_name) - - # Create a vtkGeometryFilter to convert UnstructuredGrid to PolyData - geometry_filter = vtk.vtkGeometryFilter() - geometry_filter.SetInputData(grid) - geometry_filter.Update() - poly_data = geometry_filter.GetOutput() - - renderer, render_window = _setup_render_routine(poly_data) - rgb_buffer = _get_rgb_value(render_window) - pick_buffer = _render_pick_data(grid, renderer, render_window) - var_buffer = _render_var_data(grid, renderer, render_window, var_name) - - # Todo: automatic colorby_var support - # global colorby_var_id - # colorby_var_int = colorby_var_id - # colorby_var_id += 1 - # colorby_var_decimal = 0 if is_scalar_data else 1 - # Todo: .1, .2, .3 corresponds to x, y, z dimension. Only supports scalar for now - # colorby_var = f"{colorby_var_int}.{colorby_var_decimal}" - - # For now, it only supports one part with one variable - json_data = { - "parts": [ - { - "name": part_name, - "id": str(mats.data[0]), - "colorby_var": "1.0", # colorby_var - } - ], - "variables": [ - { - "name": var_name, - "id": str(mats.data[0]), - "pal_id": "1", # colorby_var_int, - "unit_dims": "", - "unit_system_to_name": unit_system_to_name, - "unit_label": var_unit, - } - ], - } - - _form_enhanced_image(json_data, rgb_buffer, pick_buffer, var_buffer, output) + def _render_pick_data( + poly_data: vtk.vtkPolyData, renderer: vtk.vtkRenderer, render_window: vtk.vtkRenderWindow + ) -> np.ndarray: + """ + Generate a buffer containing pick data from around of rendering by the value pass. + + Parameters + ---------- + poly_data: vtk.vtkPolyData + A VTK poly data object. + renderer: vtk.vtkRenderer + A VTK renderer object. + render_window: vtk.vtkRender + A VTK poly data object. + + Returns + ------- + np.ndarray + A numpy array as RGB format but only R and B channels are effective. + Specifically, R channel stores the lower 8 bits of the pick data; G channel + stores the higher 8. + """ + value_pass = _setup_value_pass(poly_data, renderer, "Pick Data") + + render_window.Render() + + buffer = value_pass.GetFloatImageDataArray(renderer) + np_buffer = vtk_to_numpy(buffer) + + # Use NaN mask to eliminate NaN in np_buffer + width, height = render_window.GetSize() + np_buffer = np_buffer.reshape(height, width) + nan_mask = np.isnan(np_buffer) + np_buffer = np.where(nan_mask, 0, np_buffer) # Reset NaN to 0 + + np_buffer = np_buffer.astype(np.int16) + pick_buffer = np.zeros((height, width, 4), dtype=np.uint8) + + # Store the lower 8 bits to pick_buffer's R channel + pick_buffer[:, :, 0] = np_buffer & 0xFF + # Store the higher 8 bits to pick_buffer's G channel + pick_buffer[:, :, 1] = (np_buffer >> 8) & 0xFF + + return pick_buffer + + def _render_var_data( + poly_data: vtk.vtkPolyData, + renderer: vtk.vtkRenderer, + render_window: vtk.vtkRenderWindow, + var_name: str, + ) -> np.ndarray: + """ + Generate a buffer containing variable data from a round of rendering by the value + pass. + + Parameters + ---------- + poly_data: vtk.vtkPolyData + A VTK poly data object. + renderer: vtk.vtkRenderer + A VTK renderer object. + render_window: vtk.vtkRender + A VTK poly data object. + var_name: str + The variable name. + + Returns + ------- + np.ndarray + A numpy array as float32 format. Each value represents the variable data on a pixel. + """ + value_pass = _setup_value_pass(poly_data, renderer, var_name) + + render_window.Render() + + buffer = value_pass.GetFloatImageDataArray(renderer) + np_buffer = vtk_to_numpy(buffer) + width, height = render_window.GetSize() + np_buffer = np_buffer.reshape(height, width) + return np_buffer + + def _form_enhanced_image( + json_data: Dict, + rgb_buffer: np.ndarray, + pick_buffer: np.ndarray, + var_buffer: np.ndarray, + output: Union[str, io.BytesIO], + ) -> None: + """ + A helper function. Build up an enhanced image and output to either a TIFF file on + disk or to a byte buffer. + + Parameters + ---------- + json_data: Dict + A dictionary that contains "parts" and "variables" sections. + rgb_buffer: np.ndarray + An int8 buffer with RGB values. Its dimension is [height, width, 3]. + pick_buffer: np.ndarray + An int8 buffer with pick data. Its dimension is [height, width, 3]. + var_buffer: np.ndarray + An float32 buffer with variable data. Its dimension is [height, width]. + output: Union[str, io.BytesIo] + Specify the output to be either a file name or a byte buffer. + """ + # json_data as metadata called image_description to store in the enhanced image + image_description = json.dumps(json_data) + + # Create 3 images for each page + rgb_image = Image.fromarray(rgb_buffer, mode="RGB") + pick_image = Image.fromarray(pick_buffer, mode="RGBA") + var_image = Image.fromarray(var_buffer, mode="F") + + # Set up the metadata + tiffinfo = TiffImagePlugin.ImageFileDirectory_v2() + tiffinfo[TiffImagePlugin.IMAGEDESCRIPTION] = image_description + + rgb_image.save( + output, + format="TIFF", + save_all=True, + append_images=[pick_image, var_image], + tiffinfo=tiffinfo, + ) + + def _generate_enhanced_image( + model: dpf.Model, var_field: dpf.Field, part_name: str, output: Union[str, io.BytesIO] + ) -> Tuple[Dict, np.ndarray, np.ndarray, np.ndarray]: + """ + Esstential helper function for DPF inputs. Generate json metadata, rgb buffer, pick + data buffer and variable data buffer from a DPF model object and a DPF field object. + + Parameters + ---------- + model: dpf.Model + A DPF model object. + var_field: dpf.Field + A DPF field object that comes from the given model. The field is essentially + the variable in interest to visualize in an enhanced image. + part_name: str + The name of the part. It will showed on the interactive enhanced image in ADR. + + Returns + ------- + Tuple[Dict, np.ndarray, np.ndarray, np.ndarray] + A tuple of JSON metadata, rgb buffer, pick data buffer and variable data buffer + """ + # Todo: vector data support: is_scalar_data = var_data.ndim == 1 + + # Get components for metadata + var_unit: str = var_field.unit + var_name = var_field.name + var_meshed_region = var_field.meshed_region + dpf_unit_system = model.metadata.result_info.unit_system_name + unit_system_to_name = dpf_unit_system.split(":", 1)[0] + + mats: dpf.PropertyField = var_meshed_region.property_field("mat") # Pick data + + # Convert DPF to a pyvista UnstructuredGrid, which inherits from vtk + grid = vtk_helper.dpf_mesh_to_vtk(var_meshed_region) + # Add pick data + grid = vtk_helper.append_field_to_grid(mats, var_meshed_region, grid, "Pick Data") + # Add variable data + grid = vtk_helper.append_field_to_grid(var_field, var_meshed_region, grid, var_name) + + # Create a vtkGeometryFilter to convert UnstructuredGrid to PolyData + geometry_filter = vtk.vtkGeometryFilter() + geometry_filter.SetInputData(grid) + geometry_filter.Update() + poly_data = geometry_filter.GetOutput() + + renderer, render_window = _setup_render_routine(poly_data) + rgb_buffer = _get_rgb_value(render_window) + pick_buffer = _render_pick_data(grid, renderer, render_window) + var_buffer = _render_var_data(grid, renderer, render_window, var_name) + + # Todo: automatic colorby_var support + # global colorby_var_id + # colorby_var_int = colorby_var_id + # colorby_var_id += 1 + # colorby_var_decimal = 0 if is_scalar_data else 1 + # Todo: .1, .2, .3 corresponds to x, y, z dimension. Only supports scalar for now + # colorby_var = f"{colorby_var_int}.{colorby_var_decimal}" + + # For now, it only supports one part with one variable + json_data = { + "parts": [ + { + "name": part_name, + "id": str(mats.data[0]), + "colorby_var": "1.0", # colorby_var + } + ], + "variables": [ + { + "name": var_name, + "id": str(mats.data[0]), + "pal_id": "1", # colorby_var_int, + "unit_dims": "", + "unit_system_to_name": unit_system_to_name, + "unit_label": var_unit, + } + ], + } + + _form_enhanced_image(json_data, rgb_buffer, pick_buffer, var_buffer, output) From 7c39895e6051a34c05173c7527a09a81ecc080d2 Mon Sep 17 00:00:00 2001 From: Yuanrui Zhang Date: Wed, 16 Oct 2024 13:11:37 -0400 Subject: [PATCH 16/34] Add a try catch block to bypass the test if there is no DPF server available --- tests/test_enhanced_images.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/tests/test_enhanced_images.py b/tests/test_enhanced_images.py index 9703a87f4..6557538d8 100644 --- a/tests/test_enhanced_images.py +++ b/tests/test_enhanced_images.py @@ -1,5 +1,4 @@ import json -import sys from PIL import Image from PIL.TiffTags import TAGS @@ -10,11 +9,6 @@ from ansys.dynamicreporting.core.utils import enhanced_images as ei from ansys.dynamicreporting.core.utils import report_utils as ru -# sys.path.append( -# "C:\\Users\\yuzhang\\ADRdev\\pydynamicreporting\\src\\ansys\\dynamicreporting\\core\\utils" -# ) -# import enhanced_images as ei - def get_dpf_model_field_example(): model = dpf.Model(examples.find_electric_therm()) @@ -52,14 +46,24 @@ def setup_generation_flow(request): def test_basic_format(setup_generation_flow): - image = setup_generation_flow + try: + image = setup_generation_flow + except ValueError as e: + print(e) + return + image.seek(0) result = ru.is_enhanced(image) assert result is not None def test_image_description(setup_generation_flow): - image = setup_generation_flow + try: + image = setup_generation_flow + except ValueError as e: + print(e) + return + image.seek(0) metadata_dict = {TAGS[key]: image.tag[key] for key in image.tag_v2} image_description = json.loads(metadata_dict["ImageDescription"][0]) From cea61fb9f21e5a41435ba3dd63be7b404478939a Mon Sep 17 00:00:00 2001 From: Yuanrui Zhang Date: Wed, 16 Oct 2024 14:45:55 -0400 Subject: [PATCH 17/34] terminate the testing program if DPF server is not set --- tests/test_enhanced_images.py | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/tests/test_enhanced_images.py b/tests/test_enhanced_images.py index 6557538d8..cb747aba9 100644 --- a/tests/test_enhanced_images.py +++ b/tests/test_enhanced_images.py @@ -1,4 +1,5 @@ import json +import sys from PIL import Image from PIL.TiffTags import TAGS @@ -11,7 +12,14 @@ def get_dpf_model_field_example(): - model = dpf.Model(examples.find_electric_therm()) + try: + model = dpf.Model(examples.find_electric_therm()) + except ( + ValueError + ) as e: # The exception is raised when DPF server is not found due to unset env var + print(e) + sys.exit(1) + results = model.results electric_potential = results.electric_potential() fields = electric_potential.outputs.fields_container() @@ -46,24 +54,14 @@ def setup_generation_flow(request): def test_basic_format(setup_generation_flow): - try: - image = setup_generation_flow - except ValueError as e: - print(e) - return - + image = setup_generation_flow image.seek(0) result = ru.is_enhanced(image) assert result is not None def test_image_description(setup_generation_flow): - try: - image = setup_generation_flow - except ValueError as e: - print(e) - return - + image = setup_generation_flow image.seek(0) metadata_dict = {TAGS[key]: image.tag[key] for key in image.tag_v2} image_description = json.loads(metadata_dict["ImageDescription"][0]) From 143dd0aa8c57728819238a43bf7dd0bbcdd778f2 Mon Sep 17 00:00:00 2001 From: Yuanrui Zhang Date: Wed, 16 Oct 2024 15:17:14 -0400 Subject: [PATCH 18/34] Return None when DPF server is not found in the testing process --- .github/workflows/ci_cd.yml | 2 +- tests/test_enhanced_images.py | 67 +++++++++++++++++++---------------- 2 files changed, 37 insertions(+), 32 deletions(-) diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index 463a41219..43b458c96 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -74,7 +74,7 @@ jobs: strategy: matrix: os: [ ubuntu-latest ] - python-version: [ '3.9' ] #, '3.10', '3.11', '3.12' ] + python-version: [ '3.9', '3.10', '3.11', '3.12' ] steps: - uses: actions/checkout@v4 diff --git a/tests/test_enhanced_images.py b/tests/test_enhanced_images.py index cb747aba9..74e039058 100644 --- a/tests/test_enhanced_images.py +++ b/tests/test_enhanced_images.py @@ -12,13 +12,7 @@ def get_dpf_model_field_example(): - try: - model = dpf.Model(examples.find_electric_therm()) - except ( - ValueError - ) as e: # The exception is raised when DPF server is not found due to unset env var - print(e) - sys.exit(1) + model = dpf.Model(examples.find_electric_therm()) results = model.results electric_potential = results.electric_potential() @@ -50,35 +44,46 @@ def setup_dpf_inmem_generation(): @pytest.fixture(params=[setup_dpf_tiff_generation, setup_dpf_inmem_generation]) def setup_generation_flow(request): - return next(request.param()) + try: + return next(request.param()) + # The exception is raised when DPF server is not found due to unset env var + # In this case, we return None in order to skip the test. + except ValueError: + return None def test_basic_format(setup_generation_flow): image = setup_generation_flow - image.seek(0) - result = ru.is_enhanced(image) - assert result is not None + if image is None: + assert True + else: + image.seek(0) + result = ru.is_enhanced(image) + assert result is not None def test_image_description(setup_generation_flow): image = setup_generation_flow - image.seek(0) - metadata_dict = {TAGS[key]: image.tag[key] for key in image.tag_v2} - image_description = json.loads(metadata_dict["ImageDescription"][0]) - part_info = image_description["parts"][0] - var_info = image_description["variables"][0] - - assert ( - part_info["name"] == "DPF Sample" - and part_info["id"] == "1" - and part_info["colorby_var"] == "1.0" - ) - - assert ( - var_info["name"] == "electric_potential_1.s" - and var_info["id"] == "1" - and var_info["pal_id"] == "1" - and var_info["unit_dims"] == "" - and var_info["unit_system_to_name"] == "MKS" - and var_info["unit_label"] == "V" - ) + if image is None: + assert True + else: + image.seek(0) + metadata_dict = {TAGS[key]: image.tag[key] for key in image.tag_v2} + image_description = json.loads(metadata_dict["ImageDescription"][0]) + part_info = image_description["parts"][0] + var_info = image_description["variables"][0] + + assert ( + part_info["name"] == "DPF Sample" + and part_info["id"] == "1" + and part_info["colorby_var"] == "1.0" + ) + + assert ( + var_info["name"] == "electric_potential_1.s" + and var_info["id"] == "1" + and var_info["pal_id"] == "1" + and var_info["unit_dims"] == "" + and var_info["unit_system_to_name"] == "MKS" + and var_info["unit_label"] == "V" + ) From cdab4b4a28d45b0796c983d7f0eab11e55d356be Mon Sep 17 00:00:00 2001 From: Yuanrui Zhang Date: Wed, 16 Oct 2024 15:29:28 -0400 Subject: [PATCH 19/34] Remove unecessary import; Add pytest.mark decorations --- tests/test_enhanced_images.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_enhanced_images.py b/tests/test_enhanced_images.py index 74e039058..d2c2225ea 100644 --- a/tests/test_enhanced_images.py +++ b/tests/test_enhanced_images.py @@ -1,5 +1,4 @@ import json -import sys from PIL import Image from PIL.TiffTags import TAGS @@ -52,6 +51,7 @@ def setup_generation_flow(request): return None +@pytest.mark.ado_test def test_basic_format(setup_generation_flow): image = setup_generation_flow if image is None: @@ -62,6 +62,7 @@ def test_basic_format(setup_generation_flow): assert result is not None +@pytest.mark.ado_test def test_image_description(setup_generation_flow): image = setup_generation_flow if image is None: From 58715c894a34c58260fec7acd75ea0abce97995a Mon Sep 17 00:00:00 2001 From: Yuanrui Zhang Date: Wed, 16 Oct 2024 16:27:17 -0400 Subject: [PATCH 20/34] Upgrade setuptools --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ba093fcac..b057b583b 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [build-system] build-backend = "setuptools.build_meta" requires = [ - "setuptools>=45.0", + "setuptools>=65.5.0", "setuptools-scm", "wheel>=0.37.0", "pre-commit==3.3.3", From 9844d922d78c4c7fbae9927903451d46808e5fdd Mon Sep 17 00:00:00 2001 From: Yuanrui Zhang Date: Wed, 16 Oct 2024 21:45:46 -0400 Subject: [PATCH 21/34] skip 3.12 --- .github/workflows/ci_cd.yml | 2 +- .github/workflows/nightly.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index 43b458c96..9dfd3df6c 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -74,7 +74,7 @@ jobs: strategy: matrix: os: [ ubuntu-latest ] - python-version: [ '3.9', '3.10', '3.11', '3.12' ] + python-version: [ '3.9', '3.10', '3.11'] #, '3.12' ] steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index ca5cf11e9..dc3bb5394 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -47,7 +47,7 @@ jobs: strategy: matrix: os: [ ubuntu-latest ] - python-version: [ '3.9', '3.10', '3.11', '3.12' ] + python-version: [ '3.9', '3.10', '3.11'] # , '3.12' ] steps: - uses: actions/checkout@v4 From fe4850bdfceb616b6cddcc7d1063c34b5cae12d1 Mon Sep 17 00:00:00 2001 From: Yuanrui Zhang Date: Thu, 17 Oct 2024 16:11:11 -0400 Subject: [PATCH 22/34] rm a test file --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index a32f298b6..245b9d632 100644 --- a/.gitignore +++ b/.gitignore @@ -73,4 +73,3 @@ doc/_build # TIFF files *.tiff -_test_enhanced_images.py From 1fa7c40ac4e4b79830ffe873e95cf2c73c1100f4 Mon Sep 17 00:00:00 2001 From: Yuanrui Zhang Date: Thu, 17 Oct 2024 17:01:16 -0400 Subject: [PATCH 23/34] Exclude dpf-core for python 3.12 --- .github/workflows/ci_cd.yml | 2 +- .github/workflows/nightly.yml | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index 9dfd3df6c..43b458c96 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -74,7 +74,7 @@ jobs: strategy: matrix: os: [ ubuntu-latest ] - python-version: [ '3.9', '3.10', '3.11'] #, '3.12' ] + python-version: [ '3.9', '3.10', '3.11', '3.12' ] steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index dc3bb5394..ca5cf11e9 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -47,7 +47,7 @@ jobs: strategy: matrix: os: [ ubuntu-latest ] - python-version: [ '3.9', '3.10', '3.11'] # , '3.12' ] + python-version: [ '3.9', '3.10', '3.11', '3.12' ] steps: - uses: actions/checkout@v4 diff --git a/pyproject.toml b/pyproject.toml index b057b583b..9578476e2 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,7 +79,7 @@ test = [ "pytest-cov==4.1.0", "pyvista==0.44.1", "vtk==9.3.1", - "ansys-dpf-core==0.13.0", + 'ansys-dpf-core==0.13.0; python_version < "3.12"', ] doc = [ "ansys-sphinx-theme==0.12.4", From e23254c03c1372d9bdcc8ea0dd2ffdcb9d4dd727 Mon Sep 17 00:00:00 2001 From: Yuanrui Zhang Date: Thu, 17 Oct 2024 17:19:03 -0400 Subject: [PATCH 24/34] Upgrade numpy for testing --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9578476e2..b97d5214f 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -98,7 +98,7 @@ dev = [ "ipython", "whatsonpypi", "ansys-sphinx-theme==0.12.4", - "numpy==1.25.1", + "numpy==1.26.4", "numpydoc==1.8.0", "pillow==10.4.0", "psutil==6.0.0", From 5af384c479c0df5acff3610b15e1b93ef578399e Mon Sep 17 00:00:00 2001 From: Yuanrui Zhang Date: Thu, 17 Oct 2024 17:22:22 -0400 Subject: [PATCH 25/34] Upgrade numpy in test --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b97d5214f..dbb61fdaa 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -72,7 +72,7 @@ ci = "https://github.com/ansys/pydynamicreporting/actions" [project.optional-dependencies] test = [ "docker>=7.1.0", - "numpy==1.25.1", + "numpy==1.26.4", "psutil==6.0.0", "exceptiongroup==1.0.0", "pytest==8.3.3", @@ -98,7 +98,7 @@ dev = [ "ipython", "whatsonpypi", "ansys-sphinx-theme==0.12.4", - "numpy==1.26.4", + "numpy==1.25.1", "numpydoc==1.8.0", "pillow==10.4.0", "psutil==6.0.0", From d5eef71e2ad698f142f0ea02fc26d3ec7e36b917 Mon Sep 17 00:00:00 2001 From: Yuanrui Zhang Date: Thu, 17 Oct 2024 17:30:22 -0400 Subject: [PATCH 26/34] Remove constraint for dpf-core --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index dbb61fdaa..97ea4d660 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,7 +79,7 @@ test = [ "pytest-cov==4.1.0", "pyvista==0.44.1", "vtk==9.3.1", - 'ansys-dpf-core==0.13.0; python_version < "3.12"', + 'ansys-dpf-core==0.13.0', ] doc = [ "ansys-sphinx-theme==0.12.4", From 95f835f839b5d7443647b9a73d11c975a18880c6 Mon Sep 17 00:00:00 2001 From: Yuanrui Zhang Date: Fri, 18 Oct 2024 11:42:30 -0400 Subject: [PATCH 27/34] ignore extra testing files for enhanced images --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 245b9d632..a32f298b6 100644 --- a/.gitignore +++ b/.gitignore @@ -73,3 +73,4 @@ doc/_build # TIFF files *.tiff +_test_enhanced_images.py From b26b23d5962f20d2b1ac98bdca6ae391cb92895a Mon Sep 17 00:00:00 2001 From: Yuanrui Zhang Date: Fri, 18 Oct 2024 12:17:11 -0400 Subject: [PATCH 28/34] Move numpy version back to 1.25.1 and limit pyvista, vtk, dpf-core to be used only when python version is less than 3.12 --- pyproject.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 97ea4d660..035000028 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -72,14 +72,14 @@ ci = "https://github.com/ansys/pydynamicreporting/actions" [project.optional-dependencies] test = [ "docker>=7.1.0", - "numpy==1.26.4", + "numpy==1.25.1", "psutil==6.0.0", "exceptiongroup==1.0.0", "pytest==8.3.3", "pytest-cov==4.1.0", - "pyvista==0.44.1", - "vtk==9.3.1", - 'ansys-dpf-core==0.13.0', + "pyvista==0.44.1; python_version <= '3.11'", + "vtk==9.3.1; python_version <= '3.11'", + "ansys-dpf-core==0.13.0; python_version <= '3.11'", ] doc = [ "ansys-sphinx-theme==0.12.4", From 5719097bcdc5999edda3d6084139790e5857d9a7 Mon Sep 17 00:00:00 2001 From: Yuanrui Zhang Date: Fri, 18 Oct 2024 12:24:59 -0400 Subject: [PATCH 29/34] Remove numpy in [test] --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 035000028..50f3b3b0b 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -72,7 +72,6 @@ ci = "https://github.com/ansys/pydynamicreporting/actions" [project.optional-dependencies] test = [ "docker>=7.1.0", - "numpy==1.25.1", "psutil==6.0.0", "exceptiongroup==1.0.0", "pytest==8.3.3", From cbbbe546f908a78ae6b4b9699c2af616086b4c2e Mon Sep 17 00:00:00 2001 From: Yuanrui Zhang Date: Fri, 18 Oct 2024 12:29:21 -0400 Subject: [PATCH 30/34] Allow pyvista, vtk and dpf-core to run with python 3.12 --- pyproject.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 50f3b3b0b..5528ffbc6 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,9 +76,9 @@ test = [ "exceptiongroup==1.0.0", "pytest==8.3.3", "pytest-cov==4.1.0", - "pyvista==0.44.1; python_version <= '3.11'", - "vtk==9.3.1; python_version <= '3.11'", - "ansys-dpf-core==0.13.0; python_version <= '3.11'", + "pyvista==0.44.1", + "vtk==9.3.1", + "ansys-dpf-core==0.13.0", ] doc = [ "ansys-sphinx-theme==0.12.4", From b603ec1a9d4e00c5d07ab9c6763ac56ef9d41d49 Mon Sep 17 00:00:00 2001 From: Yuanrui Zhang Date: Mon, 21 Oct 2024 12:08:35 -0400 Subject: [PATCH 31/34] uncomment need smoke test --- .github/workflows/ci_cd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index 43b458c96..f3d15c36e 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -69,7 +69,7 @@ jobs: test: name: Testing - # needs: [smoke-tests] + needs: [smoke-tests] runs-on: ${{ matrix.os }} strategy: matrix: From 8fa2c358c1fc72d943a96f9bc6326ed10a9848db Mon Sep 17 00:00:00 2001 From: Yuanrui Zhang Date: Tue, 22 Oct 2024 16:22:48 -0400 Subject: [PATCH 32/34] 1. Set background color to white. 2. Tracking the pick data in enhanced image module instead of from DPF. 3. Use global mesh region instead of the field mesh region --- .../core/utils/enhanced_images.py | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/src/ansys/dynamicreporting/core/utils/enhanced_images.py b/src/ansys/dynamicreporting/core/utils/enhanced_images.py index 95d30e1e4..e0a9bb385 100644 --- a/src/ansys/dynamicreporting/core/utils/enhanced_images.py +++ b/src/ansys/dynamicreporting/core/utils/enhanced_images.py @@ -117,6 +117,7 @@ def _setup_render_routine( renderer.ResetCamera() render_window.AddRenderer(renderer) renderer.AddActor(actor) + renderer.SetBackground(1, 1, 1) # Uncomment the following 2 lines to get an interactor # render_window_interactor = vtk.vtkRenderWindowInteractor() @@ -236,6 +237,16 @@ def _get_rgb_value(render_window: vtk.vtkRenderWindow) -> np.ndarray: return np_array + def _add_pick_data(poly_data: vtk.vtkPolyData, part_id: int): + arr = vtk.vtkFloatArray() + arr.SetName("Pick Data") + arr.SetNumberOfComponents(1) + num_points = poly_data.GetNumberOfPoints() + arr.SetNumberOfTuples(num_points) + for i in range(num_points): + arr.SetValue(i, part_id) + poly_data.GetPointData().AddArray(arr) + def _render_pick_data( poly_data: vtk.vtkPolyData, renderer: vtk.vtkRenderer, render_window: vtk.vtkRenderWindow ) -> np.ndarray: @@ -388,29 +399,26 @@ def _generate_enhanced_image( # Get components for metadata var_unit: str = var_field.unit var_name = var_field.name - var_meshed_region = var_field.meshed_region dpf_unit_system = model.metadata.result_info.unit_system_name unit_system_to_name = dpf_unit_system.split(":", 1)[0] - - mats: dpf.PropertyField = var_meshed_region.property_field("mat") # Pick data + meshed_region = model.metadata.meshed_region # Whole mesh region # Convert DPF to a pyvista UnstructuredGrid, which inherits from vtk - grid = vtk_helper.dpf_mesh_to_vtk(var_meshed_region) - # Add pick data - grid = vtk_helper.append_field_to_grid(mats, var_meshed_region, grid, "Pick Data") + grid = vtk_helper.dpf_mesh_to_vtk(meshed_region) # Add variable data - grid = vtk_helper.append_field_to_grid(var_field, var_meshed_region, grid, var_name) + grid = vtk_helper.append_field_to_grid(var_field, meshed_region, grid, var_name) # Create a vtkGeometryFilter to convert UnstructuredGrid to PolyData geometry_filter = vtk.vtkGeometryFilter() geometry_filter.SetInputData(grid) geometry_filter.Update() poly_data = geometry_filter.GetOutput() + _add_pick_data(poly_data, 1) # Todo: optimize hardcoded part ID renderer, render_window = _setup_render_routine(poly_data) rgb_buffer = _get_rgb_value(render_window) - pick_buffer = _render_pick_data(grid, renderer, render_window) - var_buffer = _render_var_data(grid, renderer, render_window, var_name) + pick_buffer = _render_pick_data(poly_data, renderer, render_window) + var_buffer = _render_var_data(poly_data, renderer, render_window, var_name) # Todo: automatic colorby_var support # global colorby_var_id @@ -425,14 +433,14 @@ def _generate_enhanced_image( "parts": [ { "name": part_name, - "id": str(mats.data[0]), + "id": 1, # Todo: optimize hardcoded part ID "colorby_var": "1.0", # colorby_var } ], "variables": [ { "name": var_name, - "id": str(mats.data[0]), + "id": 1, # Todo: optimize hardcoded part ID "pal_id": "1", # colorby_var_int, "unit_dims": "", "unit_system_to_name": unit_system_to_name, From ca2d00b1b68a9dba6f93bf581a00386161168115 Mon Sep 17 00:00:00 2001 From: Yuanrui Zhang Date: Thu, 24 Oct 2024 16:04:38 -0400 Subject: [PATCH 33/34] Support rotation by user inputting --- .../core/utils/enhanced_images.py | 42 ++++++++++++------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/src/ansys/dynamicreporting/core/utils/enhanced_images.py b/src/ansys/dynamicreporting/core/utils/enhanced_images.py index e0a9bb385..eb17171bd 100644 --- a/src/ansys/dynamicreporting/core/utils/enhanced_images.py +++ b/src/ansys/dynamicreporting/core/utils/enhanced_images.py @@ -16,11 +16,6 @@ from PIL import Image, TiffImagePlugin import numpy as np -# import vtk -# from vtk.util.numpy_support import vtk_to_numpy -# from ansys.dpf import core as dpf -# from ansys.dpf.core import vtk_helper - try: import vtk from vtk.util.numpy_support import vtk_to_numpy @@ -40,7 +35,11 @@ if HAS_VTK and HAS_DPF: # pragma: no cover def generate_enhanced_image_as_tiff( - model: dpf.Model, var_field: dpf.Field, part_name: str, output_file_name: str + model: dpf.Model, + var_field: dpf.Field, + part_name: str, + output_file_name: str, + rotation: Tuple[float, float, float] = (0.0, 0.0, 0.0), ): """ Generate an enhanced image in the format of TIFF file on disk given DPF inputs. @@ -55,12 +54,17 @@ def generate_enhanced_image_as_tiff( part_name: str The name of the part. It will showed on the interactive enhanced image in ADR. output_file_name: str - output TIFF file name with extension of .tiff or .tif + output TIFF file name with extension of .tiff or .tif. + rotation: Tuple[float, float, float] + Rotation degrees about X, Y, Z axes. Note not in radians. """ - _generate_enhanced_image(model, var_field, part_name, output_file_name) + _generate_enhanced_image(model, var_field, part_name, output_file_name, rotation) def generate_enhanced_image_in_memory( - model: dpf.Model, var_field: dpf.Field, part_name: str + model: dpf.Model, + var_field: dpf.Field, + part_name: str, + rotation: Tuple[float, float, float] = (0.0, 0.0, 0.0), ) -> io.BytesIO: """ Generate an enhanced image as a PIL Image object given DPF inputs. @@ -74,6 +78,8 @@ def generate_enhanced_image_in_memory( the variable in interest to visualize in an enhanced image. part_name: str The name of the part. It will showed on the interactive enhanced image in ADR. + rotation: Tuple[float, float, float] + Rotation degrees about X, Y, Z axes. Note not in radians. Returns ------- @@ -83,12 +89,12 @@ def generate_enhanced_image_in_memory( """ # Create an in-memory bytes buffer buffer = io.BytesIO() - _generate_enhanced_image(model, var_field, part_name, buffer) + _generate_enhanced_image(model, var_field, part_name, buffer, rotation) buffer.seek(0) return buffer def _setup_render_routine( - poly_data: vtk.vtkPolyData, + poly_data: vtk.vtkPolyData, rotation: Tuple[float, float, float] = (0.0, 0.0, 0.0) ) -> Tuple[vtk.vtkRenderer, vtk.vtkRenderWindow]: """ Set up VTK render routine, including mapper, actor, renderer and render window. @@ -117,13 +123,17 @@ def _setup_render_routine( renderer.ResetCamera() render_window.AddRenderer(renderer) renderer.AddActor(actor) + actor.RotateX(rotation[0]) + actor.RotateY(rotation[1]) + actor.RotateZ(rotation[2]) + renderer.SetBackground(1, 1, 1) # Uncomment the following 2 lines to get an interactor # render_window_interactor = vtk.vtkRenderWindowInteractor() # render_window_interactor.SetRenderWindow(render_window) - return renderer, render_window # , render_windowdow_interactor + return renderer, render_window # , render_windodow_interactor def _get_vtk_scalar_mode(poly_data: vtk.vtkPolyData, var_name: str) -> int: """ @@ -373,7 +383,11 @@ def _form_enhanced_image( ) def _generate_enhanced_image( - model: dpf.Model, var_field: dpf.Field, part_name: str, output: Union[str, io.BytesIO] + model: dpf.Model, + var_field: dpf.Field, + part_name: str, + output: Union[str, io.BytesIO], + rotation: Tuple[float, float, float] = (0.0, 0.0, 0.0), ) -> Tuple[Dict, np.ndarray, np.ndarray, np.ndarray]: """ Esstential helper function for DPF inputs. Generate json metadata, rgb buffer, pick @@ -415,7 +429,7 @@ def _generate_enhanced_image( poly_data = geometry_filter.GetOutput() _add_pick_data(poly_data, 1) # Todo: optimize hardcoded part ID - renderer, render_window = _setup_render_routine(poly_data) + renderer, render_window = _setup_render_routine(poly_data, rotation) rgb_buffer = _get_rgb_value(render_window) pick_buffer = _render_pick_data(poly_data, renderer, render_window) var_buffer = _render_var_data(poly_data, renderer, render_window, var_name) From bf84778090e45f929887c7ff83156105c32157aa Mon Sep 17 00:00:00 2001 From: Yuanrui Zhang Date: Tue, 29 Oct 2024 11:43:05 -0400 Subject: [PATCH 34/34] Remove enhanced images for 25.1 --- .../core/utils/enhanced_images.py | 466 ------------------ tests/test_enhanced_images.py | 90 ---- 2 files changed, 556 deletions(-) delete mode 100644 src/ansys/dynamicreporting/core/utils/enhanced_images.py delete mode 100644 tests/test_enhanced_images.py diff --git a/src/ansys/dynamicreporting/core/utils/enhanced_images.py b/src/ansys/dynamicreporting/core/utils/enhanced_images.py deleted file mode 100644 index eb17171bd..000000000 --- a/src/ansys/dynamicreporting/core/utils/enhanced_images.py +++ /dev/null @@ -1,466 +0,0 @@ -""" -Methods that generate enhanced images. - -A group of functions that mainly create: -1. RGB buffer -2. Pick data buffer -3. Variable data buffer - -Then combine these 3 buffers along with a JSON metadata to generate a 3-page -enhanced image. -""" -import io -import json -from typing import Dict, Tuple, Union - -from PIL import Image, TiffImagePlugin -import numpy as np - -try: - import vtk - from vtk.util.numpy_support import vtk_to_numpy - - HAS_VTK = True -except ImportError: - HAS_VTK = False - -try: - from ansys.dpf import core as dpf - from ansys.dpf.core import vtk_helper - - HAS_DPF = True -except (ImportError, ValueError): - HAS_DPF = False - -if HAS_VTK and HAS_DPF: # pragma: no cover - - def generate_enhanced_image_as_tiff( - model: dpf.Model, - var_field: dpf.Field, - part_name: str, - output_file_name: str, - rotation: Tuple[float, float, float] = (0.0, 0.0, 0.0), - ): - """ - Generate an enhanced image in the format of TIFF file on disk given DPF inputs. - - Parameters - ---------- - model: dpf.Model - A DPF model object. - var_field: dpf.Field - A DPF field object that comes from the given model. The field is essentially - the variable in interest to visualize in an enhanced image. - part_name: str - The name of the part. It will showed on the interactive enhanced image in ADR. - output_file_name: str - output TIFF file name with extension of .tiff or .tif. - rotation: Tuple[float, float, float] - Rotation degrees about X, Y, Z axes. Note not in radians. - """ - _generate_enhanced_image(model, var_field, part_name, output_file_name, rotation) - - def generate_enhanced_image_in_memory( - model: dpf.Model, - var_field: dpf.Field, - part_name: str, - rotation: Tuple[float, float, float] = (0.0, 0.0, 0.0), - ) -> io.BytesIO: - """ - Generate an enhanced image as a PIL Image object given DPF inputs. - - Parameters - ---------- - model: dpf.Model - A DPF model object. - var_field: dpf.Field - A DPF field object that comes from the given model. The field is essentially - the variable in interest to visualize in an enhanced image. - part_name: str - The name of the part. It will showed on the interactive enhanced image in ADR. - rotation: Tuple[float, float, float] - Rotation degrees about X, Y, Z axes. Note not in radians. - - Returns - ------- - buffer - A IO buffer that represents the enhanced image. - The returned buffer can be opened by PIL Image.open - """ - # Create an in-memory bytes buffer - buffer = io.BytesIO() - _generate_enhanced_image(model, var_field, part_name, buffer, rotation) - buffer.seek(0) - return buffer - - def _setup_render_routine( - poly_data: vtk.vtkPolyData, rotation: Tuple[float, float, float] = (0.0, 0.0, 0.0) - ) -> Tuple[vtk.vtkRenderer, vtk.vtkRenderWindow]: - """ - Set up VTK render routine, including mapper, actor, renderer and render window. - - Parameters - ---------- - poly_data: vtk.vtkPolyData - A VTK poly data object. - - Returns - ------- - Tuple[vtk.vtkRenderer, vtk.vtkRenderWindow] - A pair of VTK renderer and render windown object. - """ - # Mapper and actor - mapper = vtk.vtkPolyDataMapper() - mapper.SetInputData(poly_data) - actor = vtk.vtkActor() - actor.SetMapper(mapper) - - # Create the renderer, render window, and interactor - renderer = vtk.vtkRenderer() - render_window = vtk.vtkRenderWindow() - render_window.SetOffScreenRendering(1) # Set it to 0 if there is an interactor - render_window.SetMultiSamples(0) - renderer.ResetCamera() - render_window.AddRenderer(renderer) - renderer.AddActor(actor) - actor.RotateX(rotation[0]) - actor.RotateY(rotation[1]) - actor.RotateZ(rotation[2]) - - renderer.SetBackground(1, 1, 1) - - # Uncomment the following 2 lines to get an interactor - # render_window_interactor = vtk.vtkRenderWindowInteractor() - # render_window_interactor.SetRenderWindow(render_window) - - return renderer, render_window # , render_windodow_interactor - - def _get_vtk_scalar_mode(poly_data: vtk.vtkPolyData, var_name: str) -> int: - """ - Given the var_name, get the scalar mode this var_name belongs to. - - Parameters - ---------- - poly_data: vtk.vtkPolyData - A VTK poly data object. - var_name: str - Variable name. - - Returns - ------- - int - An integer indicating the VTK scalar mode. - VTK_SCALAR_MODE_USE_POINT_FIELD_DATA or VTK_SCALAR_MODE_USE_CELL_FIELD_DATA. - - Raises - ------ - ValueError - If the given var_name is in neither point data nor cell data, - meaning the poly_data object does not have this variable. - """ - point_data = poly_data.GetPointData() - cell_data = poly_data.GetCellData() - point_data_array = point_data.GetArray(var_name) - cell_data_array = cell_data.GetArray(var_name) - if point_data_array is not None: - return vtk.VTK_SCALAR_MODE_USE_POINT_FIELD_DATA - if cell_data_array is not None: - return vtk.VTK_SCALAR_MODE_USE_CELL_FIELD_DATA - raise ValueError(f"{var_name} does not belong to point data, nor cell data") - - def _setup_value_pass( - poly_data: vtk.vtkPolyData, renderer: vtk.vtkRenderer, var_name: str - ) -> vtk.vtkValuePass: - """ - Bind the variable data (point or cell) to value pass, in order to render the given - variable to each pixel. - - Parameters - ---------- - poly_data: vtk.vtkPolyData - A VTK poly data object. - renderer: vtk.vtkRenderer - A VTK renderer object. - var_name: str - Variable name. - - Returns - ------- - vtk.vtkValuePass - An integer indicating the VTK scalar mode. - VTK_SCALAR_MODE_USE_POINT_FIELD_DATA or VTK_SCALAR_MODE_USE_CELL_FIELD_DATA. - """ - value_pass = vtk.vtkValuePass() - vtk_scalar_mode = _get_vtk_scalar_mode(poly_data, var_name) - value_pass.SetInputArrayToProcess(vtk_scalar_mode, var_name) - value_pass.SetInputComponentToProcess(0) - - passes = vtk.vtkRenderPassCollection() - passes.AddItem(value_pass) - - sequence = vtk.vtkSequencePass() - sequence.SetPasses(passes) - - camera_pass = vtk.vtkCameraPass() - camera_pass.SetDelegatePass(sequence) - renderer.SetPass(camera_pass) - - return value_pass - - def _get_rgb_value(render_window: vtk.vtkRenderWindow) -> np.ndarray: - """ - Get the RGB value from the render window. It starts from explicitly calling render - window's Render function. - - Parameters - ---------- - render_window: vtk.vtkRender - A VTK poly data object. - - Returns - ------- - vtk.vtkValuePass - A VTK value pass object for the following around of rendering. - """ - render_window.Render() - - width, height = render_window.GetSize() - - # Capture the rendering result - window_to_image_filter = vtk.vtkWindowToImageFilter() - window_to_image_filter.SetInput(render_window) - window_to_image_filter.Update() - - # Get the image data - image_data = window_to_image_filter.GetOutput() - - # Convert VTK image data to a NumPy array - width, height, _ = image_data.GetDimensions() - vtk_array = image_data.GetPointData().GetScalars() - np_array = vtk_to_numpy(vtk_array) - - # Reshape the array to a 3D array (height, width, 3) for RGB - np_array = np_array.reshape(height, width, -1) - - # If an interactor in involved, uncomment the next line - # render_window_interactor.Start() - - return np_array - - def _add_pick_data(poly_data: vtk.vtkPolyData, part_id: int): - arr = vtk.vtkFloatArray() - arr.SetName("Pick Data") - arr.SetNumberOfComponents(1) - num_points = poly_data.GetNumberOfPoints() - arr.SetNumberOfTuples(num_points) - for i in range(num_points): - arr.SetValue(i, part_id) - poly_data.GetPointData().AddArray(arr) - - def _render_pick_data( - poly_data: vtk.vtkPolyData, renderer: vtk.vtkRenderer, render_window: vtk.vtkRenderWindow - ) -> np.ndarray: - """ - Generate a buffer containing pick data from around of rendering by the value pass. - - Parameters - ---------- - poly_data: vtk.vtkPolyData - A VTK poly data object. - renderer: vtk.vtkRenderer - A VTK renderer object. - render_window: vtk.vtkRender - A VTK poly data object. - - Returns - ------- - np.ndarray - A numpy array as RGB format but only R and B channels are effective. - Specifically, R channel stores the lower 8 bits of the pick data; G channel - stores the higher 8. - """ - value_pass = _setup_value_pass(poly_data, renderer, "Pick Data") - - render_window.Render() - - buffer = value_pass.GetFloatImageDataArray(renderer) - np_buffer = vtk_to_numpy(buffer) - - # Use NaN mask to eliminate NaN in np_buffer - width, height = render_window.GetSize() - np_buffer = np_buffer.reshape(height, width) - nan_mask = np.isnan(np_buffer) - np_buffer = np.where(nan_mask, 0, np_buffer) # Reset NaN to 0 - - np_buffer = np_buffer.astype(np.int16) - pick_buffer = np.zeros((height, width, 4), dtype=np.uint8) - - # Store the lower 8 bits to pick_buffer's R channel - pick_buffer[:, :, 0] = np_buffer & 0xFF - # Store the higher 8 bits to pick_buffer's G channel - pick_buffer[:, :, 1] = (np_buffer >> 8) & 0xFF - - return pick_buffer - - def _render_var_data( - poly_data: vtk.vtkPolyData, - renderer: vtk.vtkRenderer, - render_window: vtk.vtkRenderWindow, - var_name: str, - ) -> np.ndarray: - """ - Generate a buffer containing variable data from a round of rendering by the value - pass. - - Parameters - ---------- - poly_data: vtk.vtkPolyData - A VTK poly data object. - renderer: vtk.vtkRenderer - A VTK renderer object. - render_window: vtk.vtkRender - A VTK poly data object. - var_name: str - The variable name. - - Returns - ------- - np.ndarray - A numpy array as float32 format. Each value represents the variable data on a pixel. - """ - value_pass = _setup_value_pass(poly_data, renderer, var_name) - - render_window.Render() - - buffer = value_pass.GetFloatImageDataArray(renderer) - np_buffer = vtk_to_numpy(buffer) - width, height = render_window.GetSize() - np_buffer = np_buffer.reshape(height, width) - return np_buffer - - def _form_enhanced_image( - json_data: Dict, - rgb_buffer: np.ndarray, - pick_buffer: np.ndarray, - var_buffer: np.ndarray, - output: Union[str, io.BytesIO], - ) -> None: - """ - A helper function. Build up an enhanced image and output to either a TIFF file on - disk or to a byte buffer. - - Parameters - ---------- - json_data: Dict - A dictionary that contains "parts" and "variables" sections. - rgb_buffer: np.ndarray - An int8 buffer with RGB values. Its dimension is [height, width, 3]. - pick_buffer: np.ndarray - An int8 buffer with pick data. Its dimension is [height, width, 3]. - var_buffer: np.ndarray - An float32 buffer with variable data. Its dimension is [height, width]. - output: Union[str, io.BytesIo] - Specify the output to be either a file name or a byte buffer. - """ - # json_data as metadata called image_description to store in the enhanced image - image_description = json.dumps(json_data) - - # Create 3 images for each page - rgb_image = Image.fromarray(rgb_buffer, mode="RGB") - pick_image = Image.fromarray(pick_buffer, mode="RGBA") - var_image = Image.fromarray(var_buffer, mode="F") - - # Set up the metadata - tiffinfo = TiffImagePlugin.ImageFileDirectory_v2() - tiffinfo[TiffImagePlugin.IMAGEDESCRIPTION] = image_description - - rgb_image.save( - output, - format="TIFF", - save_all=True, - append_images=[pick_image, var_image], - tiffinfo=tiffinfo, - ) - - def _generate_enhanced_image( - model: dpf.Model, - var_field: dpf.Field, - part_name: str, - output: Union[str, io.BytesIO], - rotation: Tuple[float, float, float] = (0.0, 0.0, 0.0), - ) -> Tuple[Dict, np.ndarray, np.ndarray, np.ndarray]: - """ - Esstential helper function for DPF inputs. Generate json metadata, rgb buffer, pick - data buffer and variable data buffer from a DPF model object and a DPF field object. - - Parameters - ---------- - model: dpf.Model - A DPF model object. - var_field: dpf.Field - A DPF field object that comes from the given model. The field is essentially - the variable in interest to visualize in an enhanced image. - part_name: str - The name of the part. It will showed on the interactive enhanced image in ADR. - - Returns - ------- - Tuple[Dict, np.ndarray, np.ndarray, np.ndarray] - A tuple of JSON metadata, rgb buffer, pick data buffer and variable data buffer - """ - # Todo: vector data support: is_scalar_data = var_data.ndim == 1 - - # Get components for metadata - var_unit: str = var_field.unit - var_name = var_field.name - dpf_unit_system = model.metadata.result_info.unit_system_name - unit_system_to_name = dpf_unit_system.split(":", 1)[0] - meshed_region = model.metadata.meshed_region # Whole mesh region - - # Convert DPF to a pyvista UnstructuredGrid, which inherits from vtk - grid = vtk_helper.dpf_mesh_to_vtk(meshed_region) - # Add variable data - grid = vtk_helper.append_field_to_grid(var_field, meshed_region, grid, var_name) - - # Create a vtkGeometryFilter to convert UnstructuredGrid to PolyData - geometry_filter = vtk.vtkGeometryFilter() - geometry_filter.SetInputData(grid) - geometry_filter.Update() - poly_data = geometry_filter.GetOutput() - _add_pick_data(poly_data, 1) # Todo: optimize hardcoded part ID - - renderer, render_window = _setup_render_routine(poly_data, rotation) - rgb_buffer = _get_rgb_value(render_window) - pick_buffer = _render_pick_data(poly_data, renderer, render_window) - var_buffer = _render_var_data(poly_data, renderer, render_window, var_name) - - # Todo: automatic colorby_var support - # global colorby_var_id - # colorby_var_int = colorby_var_id - # colorby_var_id += 1 - # colorby_var_decimal = 0 if is_scalar_data else 1 - # Todo: .1, .2, .3 corresponds to x, y, z dimension. Only supports scalar for now - # colorby_var = f"{colorby_var_int}.{colorby_var_decimal}" - - # For now, it only supports one part with one variable - json_data = { - "parts": [ - { - "name": part_name, - "id": 1, # Todo: optimize hardcoded part ID - "colorby_var": "1.0", # colorby_var - } - ], - "variables": [ - { - "name": var_name, - "id": 1, # Todo: optimize hardcoded part ID - "pal_id": "1", # colorby_var_int, - "unit_dims": "", - "unit_system_to_name": unit_system_to_name, - "unit_label": var_unit, - } - ], - } - - _form_enhanced_image(json_data, rgb_buffer, pick_buffer, var_buffer, output) diff --git a/tests/test_enhanced_images.py b/tests/test_enhanced_images.py deleted file mode 100644 index d2c2225ea..000000000 --- a/tests/test_enhanced_images.py +++ /dev/null @@ -1,90 +0,0 @@ -import json - -from PIL import Image -from PIL.TiffTags import TAGS -from ansys.dpf import core as dpf -from ansys.dpf.core import examples -import pytest - -from ansys.dynamicreporting.core.utils import enhanced_images as ei -from ansys.dynamicreporting.core.utils import report_utils as ru - - -def get_dpf_model_field_example(): - model = dpf.Model(examples.find_electric_therm()) - - results = model.results - electric_potential = results.electric_potential() - fields = electric_potential.outputs.fields_container() - potential = fields[0] - - return model, potential - - -def setup_dpf_tiff_generation(): - model, field = get_dpf_model_field_example() - - tiff_name = "dpf_find_electric_therm.tiff" - ei.generate_enhanced_image_as_tiff(model, field, "DPF Sample", tiff_name) - - image = Image.open(tiff_name) - yield image - image.close() - - -def setup_dpf_inmem_generation(): - model, field = get_dpf_model_field_example() - buffer = ei.generate_enhanced_image_in_memory(model, field, "DPF Sample") - - image = Image.open(buffer) - yield image - image.close() - - -@pytest.fixture(params=[setup_dpf_tiff_generation, setup_dpf_inmem_generation]) -def setup_generation_flow(request): - try: - return next(request.param()) - # The exception is raised when DPF server is not found due to unset env var - # In this case, we return None in order to skip the test. - except ValueError: - return None - - -@pytest.mark.ado_test -def test_basic_format(setup_generation_flow): - image = setup_generation_flow - if image is None: - assert True - else: - image.seek(0) - result = ru.is_enhanced(image) - assert result is not None - - -@pytest.mark.ado_test -def test_image_description(setup_generation_flow): - image = setup_generation_flow - if image is None: - assert True - else: - image.seek(0) - metadata_dict = {TAGS[key]: image.tag[key] for key in image.tag_v2} - image_description = json.loads(metadata_dict["ImageDescription"][0]) - part_info = image_description["parts"][0] - var_info = image_description["variables"][0] - - assert ( - part_info["name"] == "DPF Sample" - and part_info["id"] == "1" - and part_info["colorby_var"] == "1.0" - ) - - assert ( - var_info["name"] == "electric_potential_1.s" - and var_info["id"] == "1" - and var_info["pal_id"] == "1" - and var_info["unit_dims"] == "" - and var_info["unit_system_to_name"] == "MKS" - and var_info["unit_label"] == "V" - )