55from functools import wraps
66from math import pi , sqrt , atan2
77from numbers import Integral , Real
8+ from pathlib import Path
89from typing import Protocol
910
1011import h5py
@@ -2443,6 +2444,7 @@ class UnstructuredMesh(MeshBase):
24432444 _UNSUPPORTED_ELEM = - 1
24442445 _LINEAR_TET = 0
24452446 _LINEAR_HEX = 1
2447+ _VTK_TETRA = 10
24462448
24472449 def __init__ (self , filename : PathLike , library : str , mesh_id : int | None = None ,
24482450 name : str = '' , length_multiplier : float = 1.0 ,
@@ -2652,7 +2654,8 @@ def write_vtk_mesh(self, **kwargs):
26522654 warnings .warn (
26532655 "The 'UnstructuredMesh.write_vtk_mesh' method has been renamed "
26542656 "to 'write_data_to_vtk' and will be removed in a future version "
2655- " of OpenMC." , FutureWarning
2657+ " of OpenMC." ,
2658+ FutureWarning ,
26562659 )
26572660 self .write_data_to_vtk (** kwargs )
26582661
@@ -2670,26 +2673,54 @@ def write_data_to_vtk(
26702673 Parameters
26712674 ----------
26722675 filename : str or pathlib.Path
2673- Name of the VTK file to write. If the filename ends in '.vtu' then a
2674- binary VTU format file will be written, if the filename ends in
2675- '.vtk' then a legacy VTK file will be written.
2676+ Name of the VTK file to write. If the filename ends in '.vtkhdf'
2677+ then a VTKHDF format file will be written. If the filename ends in
2678+ '.vtu' then a binary VTU format file will be written. If the
2679+ filename ends in '.vtk' then a legacy VTK file will be written.
26762680 datasets : dict
26772681 Dictionary whose keys are the data labels and values are numpy
26782682 appropriately sized arrays of the data
26792683 volume_normalization : bool
26802684 Whether or not to normalize the data by the volume of the mesh
26812685 elements
26822686 """
2687+
2688+ if Path (filename ).suffix == ".vtkhdf" :
2689+
2690+ self ._write_data_to_vtk_hdf5_format (
2691+ filename = filename ,
2692+ datasets = datasets ,
2693+ volume_normalization = volume_normalization ,
2694+ )
2695+
2696+ elif Path (filename ).suffix == ".vtk" or Path (filename ).suffix == ".vtu" :
2697+
2698+ self ._write_data_to_vtk_ascii_format (
2699+ filename = filename ,
2700+ datasets = datasets ,
2701+ volume_normalization = volume_normalization ,
2702+ )
2703+
2704+ else :
2705+ raise ValueError (
2706+ "Unsupported file extension, The filename must end with "
2707+ "'.vtkhdf', '.vtu' or '.vtk'"
2708+ )
2709+
2710+ def _write_data_to_vtk_ascii_format (
2711+ self ,
2712+ filename : PathLike | None = None ,
2713+ datasets : dict | None = None ,
2714+ volume_normalization : bool = True ,
2715+ ):
26832716 from vtkmodules .util import numpy_support
26842717 from vtkmodules import vtkCommonCore
26852718 from vtkmodules import vtkCommonDataModel
26862719 from vtkmodules import vtkIOLegacy
26872720 from vtkmodules import vtkIOXML
26882721
26892722 if self .connectivity is None or self .vertices is None :
2690- raise RuntimeError (
2691- "This mesh has not been loaded from a statepoint file."
2692- )
2723+ raise RuntimeError ("This mesh has not been loaded from a statepoint file." )
26932724
26942725 if filename is None :
26952726 filename = f"mesh_{ self .id } .vtk"
@@ -2771,29 +2802,128 @@ def write_data_to_vtk(
27712802
27722803 writer .Write ()
27732804
2805+ def _write_data_to_vtk_hdf5_format (
2806+ self ,
2807+ filename : PathLike | None = None ,
2808+ datasets : dict | None = None ,
2809+ volume_normalization : bool = True ,
2810+ ):
2811+ def append_dataset (dset , array ):
2812+ """Convenience function to append data to an HDF5 dataset"""
2813+ origLen = dset .shape [0 ]
2814+ dset .resize (origLen + array .shape [0 ], axis = 0 )
2815+ dset [origLen :] = array
2816+
2817+ if self .library != "moab" :
2818+ raise NotImplementedError ("VTKHDF output is only supported for MOAB meshes" )
2819+
2820+ # the self.connectivity contains arrays of length 8 to support hex
2821+ # elements as well, in the case of tetrahedra mesh elements, the
2822+ # last 4 values are -1 and are removed
2823+ trimmed_connectivity = []
2824+ for cell in self .connectivity :
2825+ # Find the index of the first -1 value, if any
2826+ first_negative_index = np .where (cell == - 1 )[0 ]
2827+ if first_negative_index .size > 0 :
2828+ # Slice the array up to the first -1 value
2829+ trimmed_connectivity .append (cell [: first_negative_index [0 ]])
2830+ else :
2831+ # No -1 values, append the whole cell
2832+ trimmed_connectivity .append (cell )
2833+ trimmed_connectivity = np .array (trimmed_connectivity , dtype = "int32" ).flatten ()
2834+
2835+ # MOAB meshes supports tet elements only so we know it has 4 points per cell
2836+ points_per_cell = 4
2837+
2838+ # offsets are the indices of the first point of each cell in the array of points
2839+ offsets = np .arange (0 , self .n_elements * points_per_cell + 1 , points_per_cell )
2840+
2841+ for name , data in datasets .items ():
2842+ if data .shape != self .dimension :
2843+ raise ValueError (
2844+ f'Cannot apply dataset "{ name } " with '
2845+ f"shape { data .shape } to mesh { self .id } "
2846+ f"with dimensions { self .dimension } "
2847+ )
2848+
2849+ with h5py .File (filename , "w" ) as f :
2850+
2851+ root = f .create_group ("VTKHDF" )
2852+ vtk_file_format_version = (2 , 1 )
2853+ root .attrs ["Version" ] = vtk_file_format_version
2854+ ascii_type = "UnstructuredGrid" .encode ("ascii" )
2855+ root .attrs .create (
2856+ "Type" ,
2857+ ascii_type ,
2858+ dtype = h5py .string_dtype ("ascii" , len (ascii_type )),
2859+ )
2860+
2861+ # create hdf5 file structure
2862+ root .create_dataset ("NumberOfPoints" , (0 ,), maxshape = (None ,), dtype = "i8" )
2863+ root .create_dataset ("Types" , (0 ,), maxshape = (None ,), dtype = "uint8" )
2864+ root .create_dataset ("Points" , (0 , 3 ), maxshape = (None , 3 ), dtype = "f" )
2865+ root .create_dataset (
2866+ "NumberOfConnectivityIds" , (0 ,), maxshape = (None ,), dtype = "i8"
2867+ )
2868+ root .create_dataset ("NumberOfCells" , (0 ,), maxshape = (None ,), dtype = "i8" )
2869+ root .create_dataset ("Offsets" , (0 ,), maxshape = (None ,), dtype = "i8" )
2870+ root .create_dataset ("Connectivity" , (0 ,), maxshape = (None ,), dtype = "i8" )
2871+
2872+ append_dataset (root ["NumberOfPoints" ], np .array ([len (self .vertices )]))
2873+ append_dataset (root ["Points" ], self .vertices )
2874+ append_dataset (
2875+ root ["NumberOfConnectivityIds" ],
2876+ np .array ([len (trimmed_connectivity )]),
2877+ )
2878+ append_dataset (root ["Connectivity" ], trimmed_connectivity )
2879+ append_dataset (root ["NumberOfCells" ], np .array ([self .n_elements ]))
2880+ append_dataset (root ["Offsets" ], offsets )
2881+
2882+ append_dataset (
2883+ root ["Types" ], np .full (self .n_elements , self ._VTK_TETRA , dtype = "uint8" )
2884+ )
2885+
2886+ cell_data_group = root .create_group ("CellData" )
2887+
2888+ for name , data in datasets .items ():
2889+
2890+ cell_data_group .create_dataset (
2891+ name , (0 ,), maxshape = (None ,), dtype = "float64" , chunks = True
2892+ )
2893+
2894+ if volume_normalization :
2895+ data /= self .volumes
2896+ append_dataset (cell_data_group [name ], data )
2897+
27742898 @classmethod
27752899 def from_hdf5 (cls , group : h5py .Group , mesh_id : int , name : str ):
2776- filename = group [' filename' ][()].decode ()
2777- library = group [' library' ][()].decode ()
2778- if ' options' in group .attrs :
2900+ filename = group [" filename" ][()].decode ()
2901+ library = group [" library" ][()].decode ()
2902+ if " options" in group .attrs :
27792903 options = group .attrs ['options' ].decode ()
27802904 else :
27812905 options = None
27822906
2783- mesh = cls (filename = filename , library = library , mesh_id = mesh_id , name = name , options = options )
2907+ mesh = cls (
2908+ filename = filename ,
2909+ library = library ,
2910+ mesh_id = mesh_id ,
2911+ name = name ,
2912+ options = options ,
2913+ )
27842914 mesh ._has_statepoint_data = True
2785- vol_data = group [' volumes' ][()]
2915+ vol_data = group [" volumes" ][()]
27862916 mesh .volumes = np .reshape (vol_data , (vol_data .shape [0 ],))
27872917 mesh .n_elements = mesh .volumes .size
27882918
2789- vertices = group [' vertices' ][()]
2919+ vertices = group [" vertices" ][()]
27902920 mesh ._vertices = vertices .reshape ((- 1 , 3 ))
2791- connectivity = group [' connectivity' ][()]
2921+ connectivity = group [" connectivity" ][()]
27922922 mesh ._connectivity = connectivity .reshape ((- 1 , 8 ))
2793- mesh ._element_types = group [' element_types' ][()]
2923+ mesh ._element_types = group [" element_types" ][()]
27942924
2795- if ' length_multiplier' in group :
2796- mesh .length_multiplier = group [' length_multiplier' ][()]
2925+ if " length_multiplier" in group :
2926+ mesh .length_multiplier = group [" length_multiplier" ][()]
27972927
27982928 return mesh
27992929
@@ -2812,7 +2942,7 @@ def to_xml_element(self):
28122942
28132943 element .set ("library" , self ._library )
28142944 if self .options is not None :
2815- element .set (' options' , self .options )
2945+ element .set (" options" , self .options )
28162946 subelement = ET .SubElement (element , "filename" )
28172947 subelement .text = str (self .filename )
28182948
0 commit comments