-
Notifications
You must be signed in to change notification settings - Fork 75
/
subfindhdf.py
599 lines (463 loc) · 24.1 KB
/
subfindhdf.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
import os.path
import warnings
import numpy as np
from .. import array, config_parser, snapshot, units
from ..snapshot import gadgethdf
from . import Halo, HaloCatalogue
class SubFindHDFSubhaloCatalogue(HaloCatalogue) :
"""
Gadget's SubFind HDF Subhalo catalogue.
Initialized with the parent FOF group catalogue and created
automatically when an fof group is created
"""
def __init__(self, group_id, group_catalogue) :
super().__init__(group_catalogue.base)
self._group_id = group_id
self._group_catalogue = group_catalogue
self.__calc_len()
def __calc_len(self):
next_group_id = self._group_id + 1
next_first_subhalo = self._group_catalogue.nsubhalos
while next_group_id < len(self._group_catalogue._fof_group_first_subhalo)-1:
if self._group_catalogue._fof_group_first_subhalo[self._group_id + 1]!=-1:
next_first_subhalo = self._group_catalogue._fof_group_first_subhalo[next_group_id]
break
next_group_id+=1
self._len = (next_first_subhalo - self._group_catalogue._fof_group_first_subhalo[self._group_id])
def __len__(self):
return self._len
def _get_halo(self, halo_number):
if self.base is None :
raise RuntimeError("Parent SimSnap has been deleted")
if halo_number > len(self)-1 :
raise ValueError("FOF group %d does not have subhalo %d" % (self._group_id, halo_number))
# need this to index the global offset and length arrays
absolute_id = self._group_catalogue._fof_group_first_subhalo[self._group_id] + halo_number
# now form the particle IDs needed for this subhalo
type_map = self.base._family_to_group_map
halo_lengths = self._group_catalogue._subfind_halo_lengths
halo_offsets = self._group_catalogue._subfind_halo_offsets
# create the particle lists
tot_len = 0
for g_ptypes in list(type_map.values()) :
for g_ptype in g_ptypes:
tot_len += halo_lengths[g_ptype][absolute_id]
plist = np.zeros(tot_len,dtype='int64')
npart = 0
for ptype in self.base._families_ordered():
# family slice in the SubFindHDFSnap
sl = self.base._family_slice[ptype]
for g_ptype in type_map[ptype]:
# add the particle indices to the particle list
offset = halo_offsets[g_ptype][absolute_id]
length = halo_lengths[g_ptype][absolute_id]
ind = np.arange(sl.start + offset, sl.start + offset + length)
plist[npart:npart+length] = ind
npart += length
return SubFindHDFSubHalo(halo_number, self._group_id, self._group_catalogue, self, self.base, plist)
@property
def base(self) :
return self._base()
from .details import number_mapping, particle_indices
from .subhalo_catalogue import SubhaloCatalogue
class SubFindHDFHaloCatalogue(HaloCatalogue) :
"""Halo or group catalogue generated from subfind information stored inside a GadgetHDF snapshot
"""
# Names of various groups and attributes in the hdf file (which vary in different versions of SubFind?)
_fof_name = 'FOF'
_header_name = 'FOF'
_subfind_name = 'SUBFIND'
_subfind_grnr_name = 'GrNr'
_subfind_first_gr_name = 'SUBFIND/FirstSubOfHalo'
_numgrps_name = 'Total_Number_of_groups'
_numsubs_name = 'Total_Number_of_subgroups'
_grp_offset_name = 'Offset'
_grp_len_name = 'Length'
_sub_offset_name = 'SUB_Offset'
_sub_len_name = 'SUB_Length'
def __init__(self, sim, subs=False, _inherit_data_from=None) :
"""Initialise a Subfind catalogue.
By default, the FoF groups are imported, and subhalos are available via the 'subhalos' attribute of each
halo object, e.g.
>>> f = pynbody.load('path/to/snapshot.hdf5')
>>> h = f.halos()
>>> h[1].subhalos[2] # returns the third subhalo of FoF group 1
*sim*: The SimSnap
*subs*: If True, enumerate the subhalos instead of the groups. Otherwise, the jth subhalo of FOF group i
is still available, as halo_cat[i].subhalos[j].
*_inherit_data_from*: for internal use only, allows subhalo catalogue to share data with its parent FOF catalogue
"""
self._sub_mode = subs
self._hdf_files = self._get_catalogue_multifile(sim)
super().__init__(sim, number_mapping.SimpleHaloNumberMapper(0, self.__get_length()))
if _inherit_data_from:
self.__inherit_data(_inherit_data_from)
else:
self.__init_halo_offset_data()
self.__init_subhalo_relationships()
self.__init_halo_properties()
self.__reshape_multidimensional_properties()
self.__reassign_properties_from_sub_to_fof()
if not subs:
self._subhalo_catalogue = type(self)(sim, subs=True, _inherit_data_from=self)
def __inherit_data(self, parent):
attrs_to_share = ["_fof_properties", "_sub_properties", "_fof_group_offsets", "_fof_group_lengths",
"_subfind_halo_offsets", "_subfind_halo_lengths",
"fof_ignore", "sub_ignore"]
for attr in attrs_to_share:
setattr(self, attr, getattr(parent, attr))
def _get_catalogue_multifile(self, sim):
"""Some variants of Subfind put all the particle data in the catalogue files, in which case the catalogue
HDF files are already present in the base simulation. In other cases, notably Gadget4/Arepo, this must be
overridden to locate the actual HDF5 files for the catalogue"""
if not isinstance(sim, gadgethdf.SubFindHDFSnap):
raise ValueError("SubFindHDFHaloCatalogue can only work with a SubFindHDFSnap simulation")
return sim._hdf_files
def __init_ignorable_keys(self):
self.fof_ignore = list(map(str.strip,config_parser.get("SubfindHDF","FoF-ignore").split(",")))
self.sub_ignore = list(map(str.strip,config_parser.get("SubfindHDF","Sub-ignore").split(",")))
for t in list(self.base._family_to_group_map.values()):
# Don't add SubFind particles ever as this list is actually spherical overdensity
self.sub_ignore.append(t[0])
self.fof_ignore.append(t[0])
def __init_halo_properties(self):
self.__init_ignorable_keys()
self._fof_properties = self.__get_property_dictionary_from_hdf(self._fof_name)
self._sub_properties = self.__get_property_dictionary_from_hdf(self._subfind_name)
def __get_property_dictionary_from_hdf(self, hdf_key):
props = self._get_properties_from_multifile(self._hdf_files, hdf_key)
for property_key in list(props.keys()):
arr_units = self._get_units(hdf_key, property_key)
if property_key in props:
props[property_key] = props[property_key].view(array.SimArray)
props[property_key].units = arr_units
props[property_key].sim = self.base
return props
def _get_properties_from_multifile(self, multifile, hdf_key):
hdf0 = multifile.get_file0_root()
props = {}
for h in multifile.iterroot():
for property_key in hdf0[hdf_key].keys():
if property_key in self.fof_ignore or property_key not in h[hdf_key]:
continue
if property_key in props:
props[property_key] = np.append(props[property_key], h[hdf_key][property_key][()])
else:
props[property_key] = np.asarray(h[hdf_key][property_key])
return props
def _get_units(self, hdf_key, property_key):
hdf0 = self._hdf_files.get_file0_root()
return self.base._get_units_from_hdf_attr(hdf0[hdf_key][property_key].attrs)
def __reshape_multidimensional_properties(self):
self.__reshape_multidimensional_properties_one_dictionary(self._sub_properties, self._nsubhalos)
self.__reshape_multidimensional_properties_one_dictionary(self._fof_properties, self._ngroups)
def __reshape_multidimensional_properties_one_dictionary(self, properties_dict, expected_array_length):
for key in list(properties_dict.keys()):
# Test if there are no remainders, i.e. array is multiple of halo length
# then solve for the case where this is 1, 2 or 3 dimension
if len(properties_dict[key]) % expected_array_length == 0:
ndim = len(properties_dict[key]) // expected_array_length
if ndim > 1:
properties_dict[key] = properties_dict[key].reshape(expected_array_length, ndim)
def __reassign_properties_from_sub_to_fof(self):
reassign = []
for k,v in self._sub_properties.items():
if v.shape[0]==self._ngroups:
reassign.append(k)
for reassign_i in reassign:
self._fof_properties[reassign_i] = self._sub_properties[reassign_i]
del self._sub_properties[reassign_i]
def __init_subhalo_relationships(self):
nsub = 0
nfof = 0
self._subfind_halo_parent_groups = np.empty(self._nsubhalos, dtype=int)
self._fof_group_first_subhalo = np.empty(self._ngroups, dtype=int)
for h in self._hdf_files.iterroot():
parent_groups = h[self._subfind_name].get(self._subfind_grnr_name, [])
self._subfind_halo_parent_groups[nsub:nsub + len(parent_groups)] = parent_groups
nsub += len(parent_groups)
first_groups = h.get(self._subfind_first_gr_name,[])
self._fof_group_first_subhalo[nfof:nfof + len(first_groups)] = first_groups
nfof += len(first_groups)
def __get_length(self):
hdf0 = self._hdf_files.get_file0_root()
if self._sub_mode:
return int(hdf0[self._header_name].attrs[self._numsubs_name])
else:
return int(hdf0[self._header_name].attrs[self._numgrps_name])
def __init_halo_offset_data(self):
hdf0 = self._hdf_files.get_file0_root()
self._fof_group_offsets = {}
self._fof_group_lengths = {}
self._subfind_halo_offsets = {}
self._subfind_halo_lengths = {}
self._ngroups = int(hdf0[self._header_name].attrs[self._numgrps_name])
self._nsubhalos = int(hdf0[self._header_name].attrs[self._numsubs_name])
for fam in self.base._families_ordered():
ptypes = self.base._family_to_group_map[fam]
for ptype in ptypes:
self._fof_group_offsets[ptype] = np.empty(self._ngroups, dtype='int64')
self._fof_group_lengths[ptype] = np.empty(self._ngroups, dtype='int64')
self._subfind_halo_offsets[ptype] = np.empty(self._nsubhalos, dtype='int64')
self._subfind_halo_lengths[ptype] = np.empty(self._nsubhalos, dtype='int64')
curr_groups = 0
curr_subhalos = 0
for h in self._hdf_files:
# fof groups
offset = self._get_halodata_array_with_default(h, self._grp_offset_name, self._fof_name, ptype, [])
length = self._get_halodata_array_with_default(h, self._grp_len_name, self._fof_name, ptype, [])
self._fof_group_offsets[ptype][curr_groups:curr_groups + len(offset)] = offset
self._fof_group_lengths[ptype][curr_groups:curr_groups + len(offset)] = length
curr_groups += len(offset)
# subfind subhalos
offset = self._get_halodata_array_with_default(h, self._sub_offset_name, self._subfind_name, ptype, [])
length = self._get_halodata_array_with_default(h, self._sub_len_name, self._subfind_name, ptype, [])
self._subfind_halo_offsets[ptype][curr_subhalos:curr_subhalos + len(offset)] = offset
self._subfind_halo_lengths[ptype][curr_subhalos:curr_subhalos + len(offset)] = length
curr_subhalos += len(offset)
if curr_groups!=self._ngroups:
warnings.warn(f"Incorrect number of groups recovered from HDF files. Expected {self._ngroups}, found {curr_groups}")
self._ngroups = curr_groups
self._fof_group_offsets[ptype] = self._fof_group_offsets[ptype][:curr_groups]
self._fof_group_lengths[ptype] = self._fof_group_lengths[ptype][:curr_groups]
if curr_subhalos!=self._nsubhalos:
warnings.warn(f"Incorrect number of subhalos recovered from HDF files. Expected {self._nsubhalos}, found {curr_subhalos}")
self._nsubhalos = curr_subhalos
self._subfind_halo_offsets[ptype] = self._subfind_halo_offsets[ptype][:curr_groups]
self._subfind_halo_lengths[ptype] = self._subfind_halo_lengths[ptype][:curr_groups]
def _get_halodata_array(self, hdf_file, array_name, halo_or_group, particle_type):
# In gadget3 implementation, halo_or_group is not needed. In Gadget4 implementation (below), it is.
return hdf_file[particle_type][array_name]
def _get_halodata_array_with_default(self, hdf_file, array_name, halo_or_group, particle_type, default):
try:
return self._get_halodata_array(hdf_file, array_name, halo_or_group, particle_type)
except KeyError:
return default
def get_properties_one_halo(self, i):
def extract(arr, i):
if np.issubdtype(arr.dtype, np.integer):
return arr[i]
else:
return units.get_item_with_unit(arr, i)
properties = {}
if self._sub_mode:
for key in self._sub_properties:
properties[key] = extract(self._sub_properties[key], i)
else:
for key in self._fof_properties:
properties[key] = extract(self._fof_properties[key], i)
properties['children'], = np.where(self._subfind_halo_parent_groups == i)
return properties
def get_properties_all_halos(self, with_units=True) -> dict:
if self._sub_mode:
result = self._sub_properties
else:
result = self._fof_properties
if with_units:
return result
else:
return {k: v.view(np.ndarray) for k, v in result.items()}
def _get_particle_indices_one_halo(self, number):
if self.base is None :
raise RuntimeError("Parent SimSnap has been deleted")
if number > len(self)-1 :
description = "Subhalo" if self._sub_mode else "Group"
raise ValueError(f"{description} {number} does not exist")
type_map = self.base._family_to_group_map
if self._sub_mode:
lengths = self._subfind_halo_lengths
offsets = self._subfind_halo_offsets
else:
lengths = self._fof_group_lengths
offsets = self._fof_group_offsets
# create the particle lists
tot_len = 0
for g_ptypes in list(type_map.values()) :
for g_ptype in g_ptypes:
tot_len += lengths[g_ptype][number]
plist = np.empty(tot_len,dtype='int64')
self._write_pynbody_index_list_into_array(plist, offsets, lengths, number, type_map)
return plist
def _write_pynbody_index_list_into_array(self, particle_indices, halo_or_group_offsets,
halo_or_group_lengths, halo_or_group_index, type_map):
npart = 0
for ptype in self.base._families_ordered():
# family slice in the SubFindHDFSnap
sl = self.base._family_slice[ptype]
for g_ptype in type_map[ptype]:
# add the particle indices to the particle list
offset = halo_or_group_offsets[g_ptype][halo_or_group_index]
length = halo_or_group_lengths[g_ptype][halo_or_group_index]
ind = np.arange(sl.start + offset, sl.start + offset + length)
particle_indices[npart:npart + length] = ind
npart += length
return npart
def _get_all_particle_indices(self):
if self.base is None:
raise RuntimeError("Parent SimSnap has been deleted")
type_map = self.base._family_to_group_map
if self._sub_mode:
lengths = self._subfind_halo_lengths
offsets = self._subfind_halo_offsets
else:
lengths = self._fof_group_lengths
offsets = self._fof_group_offsets
# create the particle lists
tot_len = 0
for g_ptypes in list(type_map.values()):
for g_ptype in g_ptypes:
tot_len += lengths[g_ptype].sum()
plist = np.empty(tot_len, dtype='int64')
boundaries = np.empty((len(self), 2), dtype='int64')
location = 0
for i in range(len(self)):
npart = self._write_pynbody_index_list_into_array(plist[location:], offsets, lengths, i, type_map)
boundaries[i] = (location, location := location+npart)
assert location == tot_len
return particle_indices.HaloParticleIndices(plist, boundaries)
def _group_and_halo_from_halo_index(self, i):
"""Return the group, halo pair of indices from the overall halo index i
Subfind stores a long list of halos, gathered together into groups. This maps from the index in the long
list to the particular group and subhalo indices."""
if i>=self._nsubhalos:
raise ValueError("Subhalo out of range")
group = np.argmax(self._fof_group_first_subhalo>i)-1
halo = i - self._fof_group_first_subhalo[group]
return group, halo
def _get_subhalo_catalogue(self, parent_halo_number):
if not self._sub_mode:
props = self.get_properties_one_halo(parent_halo_number)
return SubhaloCatalogue(self._subhalo_catalogue, props['children'])
else:
return SubhaloCatalogue(self._subhalo_catalogue, [])
@classmethod
def _can_load(cls, sim, subs=False):
if isinstance(sim, gadgethdf.SubFindHDFSnap):
return True
else:
return False
class SubFindFOFGroup(Halo) :
"""
SubFind FOF group class
"""
def __init__(self, group_id, *args) :
"""Construct a special halo representing subfind's FOF group"""
super().__init__(group_id, *args)
self._subhalo_catalogue = SubFindHDFSubhaloCatalogue(group_id, self._halo_catalogue)
self._descriptor = "fof_group_"+str(group_id)
self.properties.update(self._halo_catalogue.get_halo_properties(group_id, subs=False))
def __getattr__(self, name):
if name == 'sub':
return self._subhalo_catalogue
else :
return super().__getattr__(name)
class SubFindHDFSubHalo(Halo) :
"""
SubFind subhalo class
"""
def __init__(self, halo_number, group_id, subfind_data_object, *args) :
"""Construct a special halo representing subfind's subhalo
*halo_id*: The halo_id, where 0 is the first halo within the specified group
*group_id*: The group across the entire snapshot
*subfind_data_object*: The object which actually holds the HDF data
Other arguments get passed to the standard halo constructor
"""
super().__init__(halo_number, *args)
self._group_id = group_id
self._descriptor = "fof_group_%d_subhalo_%d"%(group_id, halo_number)
# need this to index the global offset and length arrays
absolute_id = subfind_data_object._fof_group_first_subhalo[self._group_id] + halo_number
# load properties
self.properties.update(subfind_data_object.get_halo_properties(absolute_id, subs=True))
class Gadget4SubfindHDFCatalogue(SubFindHDFHaloCatalogue):
_fof_name = 'Group'
_subfind_name = 'Subhalo'
_header_name = 'Header'
_subfind_grnr_name = 'SubhaloGroupNr'
_subfind_first_gr_name = 'Group/GroupFirstSub'
_numgrps_name = 'Ngroups_Total'
_numsubs_name = 'Nsubhalos_Total'
_grp_offset_name = 'GroupOffsetType'
_grp_len_name = 'GroupLenType'
_sub_offset_name = 'SubhaloOffsetType'
_sub_len_name = 'SubhaloLenType'
def __init__(self, sim, **kwargs):
super().__init__(sim, **kwargs)
i = 0
for prog_or_desc in "prog", "desc":
try:
files = self._get_progenitor_or_descendant_multifile(sim, prog_or_desc)
except FileNotFoundError:
continue
props = self._get_properties_from_multifile(files, 'Subhalo')
self._sub_properties.update(props)
def _get_halodata_array(self, hdf_file, array_name, halo_or_group, particle_type):
return hdf_file[halo_or_group][array_name][:,int(particle_type[-1])]
def _get_units(self, hdf_key, property_key):
# Gadget4 doesn't seem to store unit information, so have a good guess
if property_key == 'SubhaloVmax':
dimensions = 'm s^-1'
elif 'Mass' in property_key or '_M_' in property_key:
dimensions = 'kg'
elif 'Pos' in property_key or '_R_' in property_key or 'CM' in property_key or 'VmaxRad' in property_key:
dimensions = 'm'
elif 'Vel' in property_key:
dimensions = 'm s^-1'
else:
return None
return self.base.infer_original_units(dimensions)
@classmethod
def _get_catalogue_multifile(cls, sim):
class Gadget4SubfindHdfMultiFileManager(gadgethdf.SubfindHdfMultiFileManager):
_nfiles_groupname = cls._fof_name
_nfiles_attrname = "NTask"
_subgroup_name = None
return Gadget4SubfindHdfMultiFileManager(cls._catalogue_filename(sim))
@classmethod
def _get_progenitor_or_descendant_multifile(cls, sim, prog_or_desc):
class Gadget4SubfindHdfProgenitorsMultiFileManager(gadgethdf.SubfindHdfMultiFileManager):
_nfiles_groupname = "Header"
_nfiles_attrname = "NumFiles"
_subgroup_name = None
return Gadget4SubfindHdfProgenitorsMultiFileManager(cls._catalogue_filename(sim, "subhalo_"+prog_or_desc+"_"))
@classmethod
def _catalogue_filename(cls, sim, namestem ="fof_subhalo_tab_"):
snapnum = os.path.basename(sim.filename).split("_")[-1]
parent_dir = os.path.dirname(os.path.abspath(sim.filename))
return os.path.join(parent_dir, namestem + snapnum)
@classmethod
def _can_load(cls, sim, **kwargs):
file = cls._catalogue_filename(sim)
if os.path.exists(file) and (file.endswith(".hdf5") or os.listdir(file)[0].endswith(".hdf5")) \
or os.path.exists(file+".0.hdf5"):
# very hard to figure out whether it's the right sort of hdf5 file without just going ahead and loading it
try:
cls(sim, **kwargs)
return True
except:
return False
class ArepoSubfindHDFCatalogue(Gadget4SubfindHDFCatalogue):
_numsubs_name = 'Nsubgroups_Total'
_subfind_grnr_name = 'SubhaloGrNr'
def _get_halodata_array(self, hdf_file, array_name, halo_or_group, particle_type):
if array_name.endswith("OffsetType"):
len_array_name = array_name.replace("OffsetType", "LenType")
# this doesn't exist in arepo
lens = super()._get_halodata_array(hdf_file, len_array_name, halo_or_group, particle_type)
return np.concatenate(([0],np.cumsum(lens)[:-1]))
else:
return super()._get_halodata_array(hdf_file, array_name, halo_or_group, particle_type)
class TNGSubfindHDFCatalogue(ArepoSubfindHDFCatalogue):
@classmethod
def _catalogue_filename(cls, sim, namestem ="fof_subhalo_tab_"):
snapnum = os.path.basename(sim.filename).split("_")[-1]
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(sim.filename)))
f = os.path.join(parent_dir, "groups_"+snapnum, namestem + snapnum)
return f
@classmethod
def _get_catalogue_multifile(cls, sim):
class TNGSubfindHdfMultiFileManager(gadgethdf.SubfindHdfMultiFileManager):
_nfiles_groupname = "Header"
_nfiles_attrname = "NumFiles"
_subgroup_name = None
return TNGSubfindHdfMultiFileManager(cls._catalogue_filename(sim))