@@ -502,7 +502,32 @@ def material_volumes(
502502
503503 # Restore original tallies
504504 model .tallies = original_tallies
505- return volumes
505+
506+ # Sort each element's materials by material ID (ascending), with
507+ # None (-1) after positive IDs and empty slots (-2) last. This
508+ # gives a deterministic ordering that is independent of the ray
509+ # traversal order used by the C library.
510+ mat_arr = volumes ._materials .copy ()
511+ vol_arr = volumes ._volumes .copy ()
512+ bbox_arr = volumes ._bboxes .copy () if volumes ._bboxes is not None else None
513+ n_elements , table_size = mat_arr .shape
514+
515+ def _sort_key (m ):
516+ m = int (m )
517+ if m == - 2 :
518+ return (2 , 0 ) # empty slot – always last
519+ if m == - 1 :
520+ return (1 , 0 ) # vacuum / None – just before empty
521+ return (0 , m ) # real material – ascending ID
522+
523+ for i in range (n_elements ):
524+ indices = sorted (range (table_size ), key = lambda j : _sort_key (mat_arr [i , j ]))
525+ mat_arr [i ] = mat_arr [i , indices ]
526+ vol_arr [i ] = vol_arr [i , indices ]
527+ if bbox_arr is not None :
528+ bbox_arr [i ] = bbox_arr [i , indices ]
529+
530+ return MeshMaterialVolumes (mat_arr , vol_arr , bbox_arr )
506531
507532
508533class StructuredMesh (MeshBase ):
@@ -662,7 +687,7 @@ def num_mesh_cells(self):
662687 def write_data_to_vtk (self ,
663688 filename : PathLike ,
664689 datasets : dict | None = None ,
665- volume_normalization : bool = True ,
690+ volume_normalization : bool = False ,
666691 curvilinear : bool = False ):
667692 """Creates a VTK object of the mesh
668693
@@ -712,6 +737,15 @@ def write_data_to_vtk(self,
712737 >>> heating = tally.get_reshaped_data(expand_dims=True)
713738 >>> mesh.write_data_to_vtk({'heating': heating})
714739 """
740+ if Path (filename ).suffix == ".vtkhdf" :
741+ write_impl = getattr (self , '_write_vtk_hdf5' , None )
742+ if write_impl is None :
743+ raise NotImplementedError (f"VTKHDF output not implemented for { type (self ).__name__ } " )
744+ # write_impl is a bound method – do NOT pass self again
745+ write_impl (filename , datasets , volume_normalization )
746+ return None
747+
748+ # vtk is an optional dependency only needed for the legacy ASCII path
715749 import vtk
716750 from vtk .util import numpy_support as nps
717751
@@ -737,11 +771,11 @@ def write_data_to_vtk(self,
737771 # API
738772 # TODO: update to "C" ordering throughout
739773 if dataset .ndim == 3 :
740- dataset = dataset .T . ravel ()
774+ dataset = dataset .ravel ()
741775 datasets_out .append (dataset )
742776
743777 if volume_normalization :
744- dataset /= self .volumes .T . ravel ()
778+ dataset /= self .volumes .ravel ()
745779
746780 dataset_array = vtk .vtkDoubleArray ()
747781 dataset_array .SetName (label )
@@ -1477,6 +1511,72 @@ def get_indices_at_coords(self, coords: Sequence[float]) -> tuple:
14771511 indices = np .floor ((coords_array - lower_left ) / spacing ).astype (int )
14781512 return tuple (int (i ) for i in indices [:ndim ])
14791513
1514+ def _write_vtk_hdf5 (self , filename , datasets , volume_normalization ):
1515+ """Write RegularMesh as VTKHDF StructuredGrid format."""
1516+ dims = self .dimension
1517+ ndim = len (dims )
1518+
1519+ # Vertex dimensions (cells + 1) – store only ndim entries so that
1520+ # 1-D and 2-D meshes carry the right number of dimensions.
1521+ vertex_dims = [d + 1 for d in dims ]
1522+
1523+ # Build explicit point coordinates. Pad coordinate arrays to 3-D so
1524+ # that every point has an (x, y, z) triple; extra coordinates are 0.
1525+ coords_1d = []
1526+ for i in range (ndim ):
1527+ c = np .linspace (self .lower_left [i ], self .upper_right [i ], dims [i ] + 1 )
1528+ coords_1d .append (c )
1529+ while len (coords_1d ) < 3 :
1530+ coords_1d .append (np .array ([0.0 ]))
1531+
1532+ # np.meshgrid with indexing='ij' → axis 0 = x, axis 1 = y, axis 2 = z
1533+ xx , yy , zz = np .meshgrid (* coords_1d , indexing = 'ij' )
1534+ # Flatten in Fortran (x-fastest) order for VTK point ordering
1535+ points = np .column_stack ([
1536+ xx .ravel (order = 'F' ),
1537+ yy .ravel (order = 'F' ),
1538+ zz .ravel (order = 'F' ),
1539+ ]).astype (np .float64 ) # shape (n_points, 3)
1540+
1541+ with h5py .File (filename , "w" ) as f :
1542+ root = f .create_group ("VTKHDF" )
1543+ root .attrs ["Version" ] = (2 , 1 )
1544+ _type = "StructuredGrid" .encode ("ascii" )
1545+ root .attrs .create (
1546+ "Type" ,
1547+ _type ,
1548+ dtype = h5py .string_dtype ("ascii" , len (_type )),
1549+ )
1550+ root .create_dataset ("Dimensions" , data = vertex_dims , dtype = "i8" )
1551+ root .create_dataset ("Points" , data = points , dtype = "f8" )
1552+
1553+ cell_data_group = root .create_group ("CellData" )
1554+
1555+ if not datasets :
1556+ return
1557+
1558+ for name , data in datasets .items ():
1559+ data = self ._reshape_vtk_dataset (data )
1560+ if data .ndim > 1 and data .shape != self .dimension :
1561+ raise ValueError (
1562+ f'Cannot apply multidimensional dataset "{ name } " with '
1563+ f"shape { data .shape } to mesh { self .id } "
1564+ f"with dimensions { self .dimension } "
1565+ )
1566+ if data .size != self .n_elements :
1567+ raise ValueError (
1568+ f"The size of the dataset '{ name } ' ({ data .size } ) should be"
1569+ f" equal to the number of mesh cells ({ self .n_elements } )"
1570+ )
1571+
1572+ if volume_normalization :
1573+ data = data / self .volumes
1574+
1575+ flat_data = data .T .ravel () if data .ndim > 1 else data .ravel ()
1576+ cell_data_group .create_dataset (
1577+ name , data = flat_data , dtype = "float64" , chunks = True
1578+ )
1579+
14801580
14811581def Mesh (* args , ** kwargs ):
14821582 warnings .warn ("Mesh has been renamed RegularMesh. Future versions of "
@@ -1693,6 +1793,55 @@ def get_indices_at_coords(self, coords: Sequence[float]) -> tuple:
16931793 "get_indices_at_coords is not yet implemented for RectilinearMesh"
16941794 )
16951795
1796+ def _write_vtk_hdf5 (self , filename , datasets , volume_normalization ):
1797+ """Write RectilinearMesh as VTK-HDF5 StructuredGrid format.
1798+
1799+ Note: vtkRectilinearGrid is not part of the VTKHDF spec yet, so
1800+ StructuredGrid with explicit point coordinates is used instead.
1801+ """
1802+ nx , ny , nz = self .dimension
1803+ vertex_dims = [nx + 1 , ny + 1 , nz + 1 ]
1804+
1805+ vertices = np .stack (np .meshgrid (
1806+ self .x_grid , self .y_grid , self .z_grid , indexing = 'ij'
1807+ ), axis = - 1 )
1808+
1809+ with h5py .File (filename , "w" ) as f :
1810+ root = f .create_group ("VTKHDF" )
1811+ root .attrs ["Version" ] = (2 , 1 )
1812+ root .attrs ["Type" ] = b"StructuredGrid"
1813+ root .create_dataset ("Dimensions" , data = vertex_dims , dtype = "i8" )
1814+
1815+ points = vertices .reshape (- 1 , 3 )
1816+ root .create_dataset ("Points" , data = points .astype (np .float64 ), dtype = "f8" )
1817+
1818+ cell_data_group = root .create_group ("CellData" )
1819+
1820+ if not datasets :
1821+ return
1822+
1823+ for name , data in datasets .items ():
1824+ data = self ._reshape_vtk_dataset (data )
1825+ if data .ndim > 1 and data .shape != self .dimension :
1826+ raise ValueError (
1827+ f'Cannot apply dataset "{ name } " with '
1828+ f"shape { data .shape } to mesh { self .id } "
1829+ f"with dimensions { self .dimension } "
1830+ )
1831+ if data .size != self .n_elements :
1832+ raise ValueError (
1833+ f"The size of the dataset '{ name } ' ({ data .size } ) should be"
1834+ f" equal to the number of mesh cells ({ self .n_elements } )"
1835+ )
1836+
1837+ if volume_normalization :
1838+ data = data / self .volumes
1839+
1840+ flat_data = data .T .ravel () if data .ndim > 1 else data .ravel ()
1841+ cell_data_group .create_dataset (
1842+ name , data = flat_data , dtype = "float64" , chunks = True
1843+ )
1844+
16961845
16971846class CylindricalMesh (StructuredMesh ):
16981847 """A 3D cylindrical mesh
@@ -2146,6 +2295,53 @@ def _convert_to_cartesian(arr, origin: Sequence[float]):
21462295 arr [..., 2 ] += origin [2 ]
21472296 return arr
21482297
2298+ def _write_vtk_hdf5 (self , filename , datasets , volume_normalization ):
2299+ """Write CylindricalMesh as VTK-HDF5 StructuredGrid format."""
2300+ nr , nphi , nz = self .dimension
2301+ vertex_dims = [nr + 1 , nphi + 1 , nz + 1 ]
2302+
2303+ R , Phi , Z = np .meshgrid (self .r_grid , self .phi_grid , self .z_grid , indexing = 'ij' )
2304+ X = R * np .cos (Phi ) + self .origin [0 ]
2305+ Y = R * np .sin (Phi ) + self .origin [1 ]
2306+ Z = Z + self .origin [2 ]
2307+ vertices = np .stack ([X , Y , Z ], axis = - 1 )
2308+
2309+ with h5py .File (filename , "w" ) as f :
2310+ root = f .create_group ("VTKHDF" )
2311+ root .attrs ["Version" ] = (2 , 1 )
2312+ root .attrs ["Type" ] = b"StructuredGrid"
2313+ root .create_dataset ("Dimensions" , data = vertex_dims , dtype = "i8" )
2314+
2315+ points = vertices .reshape (- 1 , 3 )
2316+ root .create_dataset ("Points" , data = points .astype (np .float64 ), dtype = "f8" )
2317+
2318+ cell_data_group = root .create_group ("CellData" )
2319+
2320+ if not datasets :
2321+ return
2322+
2323+ for name , data in datasets .items ():
2324+ data = self ._reshape_vtk_dataset (data )
2325+ if data .ndim > 1 and data .shape != self .dimension :
2326+ raise ValueError (
2327+ f'Cannot apply dataset "{ name } " with '
2328+ f"shape { data .shape } to mesh { self .id } "
2329+ f"with dimensions { self .dimension } "
2330+ )
2331+ if data .size != self .n_elements :
2332+ raise ValueError (
2333+ f"The size of the dataset '{ name } ' ({ data .size } ) should be"
2334+ f" equal to the number of mesh cells ({ self .n_elements } )"
2335+ )
2336+
2337+ if volume_normalization :
2338+ data = data / self .volumes
2339+
2340+ flat_data = data .T .ravel () if data .ndim > 1 else data .ravel ()
2341+ cell_data_group .create_dataset (
2342+ name , data = flat_data , dtype = "float64" , chunks = True
2343+ )
2344+
21492345
21502346class SphericalMesh (StructuredMesh ):
21512347 """A 3D spherical mesh
@@ -2533,6 +2729,55 @@ def get_indices_at_coords(self, coords: Sequence[float]) -> tuple:
25332729 "get_indices_at_coords is not yet implemented for SphericalMesh"
25342730 )
25352731
2732+ def _write_vtk_hdf5 (self , filename , datasets , volume_normalization ):
2733+ """Write SphericalMesh as VTK-HDF5 StructuredGrid format."""
2734+ nr , ntheta , nphi = self .dimension
2735+ vertex_dims = [nr + 1 , ntheta + 1 , nphi + 1 ]
2736+
2737+ R , Theta , Phi = np .meshgrid (
2738+ self .r_grid , self .theta_grid , self .phi_grid , indexing = 'ij'
2739+ )
2740+ X = R * np .sin (Theta ) * np .cos (Phi ) + self .origin [0 ]
2741+ Y = R * np .sin (Theta ) * np .sin (Phi ) + self .origin [1 ]
2742+ Z = R * np .cos (Theta ) + self .origin [2 ]
2743+ vertices = np .stack ([X , Y , Z ], axis = - 1 )
2744+
2745+ with h5py .File (filename , "w" ) as f :
2746+ root = f .create_group ("VTKHDF" )
2747+ root .attrs ["Version" ] = (2 , 1 )
2748+ root .attrs ["Type" ] = b"StructuredGrid"
2749+ root .create_dataset ("Dimensions" , data = vertex_dims , dtype = "i8" )
2750+
2751+ points = vertices .reshape (- 1 , 3 )
2752+ root .create_dataset ("Points" , data = points .astype (np .float64 ), dtype = "f8" )
2753+
2754+ cell_data_group = root .create_group ("CellData" )
2755+
2756+ if not datasets :
2757+ return
2758+
2759+ for name , data in datasets .items ():
2760+ data = self ._reshape_vtk_dataset (data )
2761+ if data .ndim > 1 and data .shape != self .dimension :
2762+ raise ValueError (
2763+ f'Cannot apply dataset "{ name } " with '
2764+ f"shape { data .shape } to mesh { self .id } "
2765+ f"with dimensions { self .dimension } "
2766+ )
2767+ if data .size != self .n_elements :
2768+ raise ValueError (
2769+ f"The size of the dataset '{ name } ' ({ data .size } ) should be"
2770+ f" equal to the number of mesh cells ({ self .n_elements } )"
2771+ )
2772+
2773+ if volume_normalization :
2774+ data = data / self .volumes
2775+
2776+ flat_data = data .T .ravel () if data .ndim > 1 else data .ravel ()
2777+ cell_data_group .create_dataset (
2778+ name , data = flat_data , dtype = "float64" , chunks = True
2779+ )
2780+
25362781
25372782def require_statepoint_data (func ):
25382783 @wraps (func )
@@ -2989,6 +3234,10 @@ def _write_data_to_vtk_hdf5_format(
29893234 datasets : dict | None = None ,
29903235 volume_normalization : bool = True ,
29913236 ):
3237+ """Write UnstructuredMesh as VTK-HDF5 UnstructuredGrid format.
3238+
3239+ Supports linear tetrahedra and linear hexahedra elements.
3240+ """
29923241 def append_dataset (dset , array ):
29933242 """Convenience function to append data to an HDF5 dataset"""
29943243 origLen = dset .shape [0 ]
@@ -3028,7 +3277,6 @@ def append_dataset(dset, array):
30283277 )
30293278
30303279 with h5py .File (filename , "w" ) as f :
3031-
30323280 root = f .create_group ("VTKHDF" )
30333281 vtk_file_format_version = (2 , 1 )
30343282 root .attrs ["Version" ] = vtk_file_format_version
@@ -3067,7 +3315,7 @@ def append_dataset(dset, array):
30673315 cell_data_group = root .create_group ("CellData" )
30683316
30693317 for name , data in datasets .items ():
3070-
3318+
30713319 cell_data_group .create_dataset (
30723320 name , (0 ,), maxshape = (None ,), dtype = "float64" , chunks = True
30733321 )
@@ -3205,4 +3453,4 @@ def _read_meshes(elem):
32053453_HEX_MIDPOINT_CONN += ((2 , (0 , 0 , 0 )),
32063454 (2 , (1 , 0 , 0 )),
32073455 (2 , (1 , 1 , 0 )),
3208- (2 , (0 , 1 , 0 )))
3456+ (2 , (0 , 1 , 0 )))
0 commit comments