• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

openmc-dev / openmc / 21616189382

03 Feb 2026 03:49AM UTC coverage: 81.772% (-0.2%) from 81.993%
21616189382

Pull #3756

github

web-flow
Merge 46d05d381 into fc0d9eec6
Pull Request #3756: Refactor ParticleType to use PDG Monte Carlo numbering scheme

17310 of 24247 branches covered (71.39%)

Branch coverage included in aggregate %.

346 of 501 new or added lines in 32 files covered. (69.06%)

497 existing lines in 8 files now uncovered.

55908 of 65292 relevant lines covered (85.63%)

44288421.04 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.93
/openmc/source.py
1
from __future__ import annotations
11✔
2
from abc import ABC, abstractmethod
11✔
3
from collections.abc import Iterable, Sequence
11✔
4
from numbers import Real
11✔
5
from pathlib import Path
11✔
6
import warnings
11✔
7
from typing import Any
11✔
8

9
import lxml.etree as ET
11✔
10
import numpy as np
11✔
11
import h5py
11✔
12
import pandas as pd
11✔
13

14
import openmc
11✔
15
import openmc.checkvalue as cv
11✔
16
from openmc.checkvalue import PathLike
11✔
17
from openmc.stats.multivariate import UnitSphere, Spatial
11✔
18
from openmc.stats.univariate import Univariate
11✔
19
from ._xml import get_elem_list, get_text
11✔
20
from .mesh import MeshBase, StructuredMesh, UnstructuredMesh
11✔
21
from .particle_type import ParticleType
11✔
22
from .statepoint import _VERSION_STATEPOINT
11✔
23
from .utility_funcs import input_path
11✔
24

25

26
class SourceBase(ABC):
11✔
27
    """Base class for external sources
28

29
    Parameters
30
    ----------
31
    strength : float
32
        Strength of the source
33
    constraints : dict
34
        Constraints on sampled source particles. Valid keys include 'domains',
35
        'time_bounds', 'energy_bounds', 'fissionable', and 'rejection_strategy'.
36
        For 'domains', the corresponding value is an iterable of
37
        :class:`openmc.Cell`, :class:`openmc.Material`, or
38
        :class:`openmc.Universe` for which sampled sites must be within. For
39
        'time_bounds' and 'energy_bounds', the corresponding value is a sequence
40
        of floats giving the lower and upper bounds on time in [s] or energy in
41
        [eV] that the sampled particle must be within. For 'fissionable', the
42
        value is a bool indicating that only sites in fissionable material
43
        should be accepted. The 'rejection_strategy' indicates what should
44
        happen when a source particle is rejected: either 'resample' (pick a new
45
        particle) or 'kill' (accept and terminate).
46

47
    Attributes
48
    ----------
49
    type : {'independent', 'file', 'compiled', 'mesh'}
50
        Indicator of source type.
51
    strength : float
52
        Strength of the source
53
    constraints : dict
54
        Constraints on sampled source particles. Valid keys include
55
        'domain_type', 'domain_ids', 'time_bounds', 'energy_bounds',
56
        'fissionable', and 'rejection_strategy'.
57

58
    """
59

60
    def __init__(
11✔
61
        self,
62
        strength: float | None = 1.0,
63
        constraints: dict[str, Any] | None = None
64
    ):
65
        self.strength = strength
11✔
66
        self.constraints = constraints
11✔
67

68
    @property
11✔
69
    def strength(self):
11✔
70
        return self._strength
11✔
71

72
    @strength.setter
11✔
73
    def strength(self, strength):
11✔
74
        cv.check_type('source strength', strength, Real, none_ok=True)
11✔
75
        if strength is not None:
11✔
76
            cv.check_greater_than('source strength', strength, 0.0, True)
11✔
77
        self._strength = strength
11✔
78

79
    @property
11✔
80
    def constraints(self) -> dict[str, Any]:
11✔
81
        return self._constraints
11✔
82

83
    @constraints.setter
11✔
84
    def constraints(self, constraints: dict[str, Any] | None):
11✔
85
        self._constraints = {}
11✔
86
        if constraints is None:
11✔
87
            return
11✔
88

89
        for key, value in constraints.items():
11✔
90
            if key == 'domains':
11✔
91
                cv.check_type('domains', value, Iterable,
11✔
92
                              (openmc.Cell, openmc.Material, openmc.Universe))
93
                if isinstance(value[0], openmc.Cell):
11✔
94
                    self._constraints['domain_type'] = 'cell'
11✔
95
                elif isinstance(value[0], openmc.Material):
11✔
96
                    self._constraints['domain_type'] = 'material'
×
97
                elif isinstance(value[0], openmc.Universe):
11✔
98
                    self._constraints['domain_type'] = 'universe'
11✔
99
                self._constraints['domain_ids'] = [d.id for d in value]
11✔
100
            elif key == 'time_bounds':
11✔
101
                cv.check_type('time bounds', value, Iterable, Real)
11✔
102
                self._constraints['time_bounds'] = tuple(value)
11✔
103
            elif key == 'energy_bounds':
11✔
104
                cv.check_type('energy bounds', value, Iterable, Real)
11✔
105
                self._constraints['energy_bounds'] = tuple(value)
11✔
106
            elif key == 'fissionable':
11✔
107
                cv.check_type('fissionable', value, bool)
11✔
108
                self._constraints['fissionable'] = value
11✔
109
            elif key == 'rejection_strategy':
×
110
                cv.check_value('rejection strategy',
×
111
                               value, ('resample', 'kill'))
112
                self._constraints['rejection_strategy'] = value
×
113
            else:
114
                raise ValueError(
×
115
                    f'Unknown key in constraints dictionary: {key}')
116

117
    @abstractmethod
11✔
118
    def populate_xml_element(self, element):
11✔
119
        """Add necessary source information to an XML element
120

121
        Returns
122
        -------
123
        element : lxml.etree._Element
124
            XML element containing source data
125

126
        """
127

128
    def to_xml_element(self) -> ET.Element:
11✔
129
        """Return XML representation of the source
130

131
        Returns
132
        -------
133
        element : xml.etree.ElementTree.Element
134
            XML element containing source data
135

136
        """
137
        element = ET.Element("source")
11✔
138
        element.set("type", self.type)
11✔
139
        if self.strength is not None:
11✔
140
            element.set("strength", str(self.strength))
11✔
141
        self.populate_xml_element(element)
11✔
142
        constraints = self.constraints
11✔
143
        if constraints:
11✔
144
            constraints_elem = ET.SubElement(element, "constraints")
11✔
145
            if "domain_ids" in constraints:
11✔
146
                dt_elem = ET.SubElement(constraints_elem, "domain_type")
11✔
147
                dt_elem.text = constraints["domain_type"]
11✔
148
                id_elem = ET.SubElement(constraints_elem, "domain_ids")
11✔
149
                id_elem.text = ' '.join(str(uid)
11✔
150
                                        for uid in constraints["domain_ids"])
151
            if "time_bounds" in constraints:
11✔
152
                dt_elem = ET.SubElement(constraints_elem, "time_bounds")
11✔
153
                dt_elem.text = ' '.join(str(t)
11✔
154
                                        for t in constraints["time_bounds"])
155
            if "energy_bounds" in constraints:
11✔
156
                dt_elem = ET.SubElement(constraints_elem, "energy_bounds")
11✔
157
                dt_elem.text = ' '.join(str(E)
11✔
158
                                        for E in constraints["energy_bounds"])
159
            if "fissionable" in constraints:
11✔
160
                dt_elem = ET.SubElement(constraints_elem, "fissionable")
11✔
161
                dt_elem.text = str(constraints["fissionable"]).lower()
11✔
162
            if "rejection_strategy" in constraints:
11✔
163
                dt_elem = ET.SubElement(constraints_elem, "rejection_strategy")
×
164
                dt_elem.text = constraints["rejection_strategy"]
×
165

166
        return element
11✔
167

168
    @classmethod
11✔
169
    def from_xml_element(cls, elem: ET.Element, meshes=None) -> SourceBase:
11✔
170
        """Generate source from an XML element
171

172
        Parameters
173
        ----------
174
        elem : lxml.etree._Element
175
            XML element
176
        meshes : dict
177
            Dictionary with mesh IDs as keys and openmc.MeshBase instances as
178
            values
179

180
        Returns
181
        -------
182
        openmc.SourceBase
183
            Source generated from XML element
184

185
        """
186
        source_type = get_text(elem, 'type')
11✔
187

188
        if source_type is None:
11✔
189
            # attempt to determine source type based on attributes
190
            # for backward compatibility
191
            if get_text(elem, 'file') is not None:
11✔
192
                return FileSource.from_xml_element(elem)
×
193
            elif get_text(elem, 'library') is not None:
11✔
194
                return CompiledSource.from_xml_element(elem)
×
195
            else:
196
                return IndependentSource.from_xml_element(elem)
11✔
197
        else:
198
            if source_type == 'independent':
11✔
199
                return IndependentSource.from_xml_element(elem, meshes)
11✔
200
            elif source_type == 'compiled':
11✔
201
                return CompiledSource.from_xml_element(elem)
×
202
            elif source_type == 'file':
11✔
203
                return FileSource.from_xml_element(elem)
×
204
            elif source_type == 'mesh':
11✔
205
                return MeshSource.from_xml_element(elem, meshes)
11✔
206
            else:
207
                raise ValueError(
×
208
                    f'Source type {source_type} is not recognized')
209

210
    @staticmethod
11✔
211
    def _get_constraints(elem: ET.Element) -> dict[str, Any]:
11✔
212
        # Find element containing constraints
213
        constraints_elem = elem.find("constraints")
11✔
214
        elem = constraints_elem if constraints_elem is not None else elem
11✔
215

216
        constraints = {}
11✔
217
        domain_type = get_text(elem, "domain_type")
11✔
218
        if domain_type is not None:
11✔
219
            domain_ids = get_elem_list(elem, "domain_ids", int)
×
220

221
            # Instantiate some throw-away domains that are used by the
222
            # constructor to assign IDs
223
            with warnings.catch_warnings():
×
224
                warnings.simplefilter('ignore', openmc.IDWarning)
×
225
                if domain_type == 'cell':
×
226
                    domains = [openmc.Cell(uid) for uid in domain_ids]
×
227
                elif domain_type == 'material':
×
228
                    domains = [openmc.Material(uid) for uid in domain_ids]
×
229
                elif domain_type == 'universe':
×
230
                    domains = [openmc.Universe(uid) for uid in domain_ids]
×
231
            constraints['domains'] = domains
×
232

233
        time_bounds = get_elem_list(elem, "time_bounds", float)
11✔
234
        if time_bounds is not None:
11✔
235
            constraints['time_bounds'] = time_bounds
×
236

237
        energy_bounds = get_elem_list(elem, "energy_bounds", float)
11✔
238
        if energy_bounds is not None:
11✔
239
            constraints['energy_bounds'] = energy_bounds
×
240

241
        fissionable = get_text(elem, "fissionable")
11✔
242
        if fissionable is not None:
11✔
243
            constraints['fissionable'] = fissionable in ('true', '1')
11✔
244

245
        rejection_strategy = get_text(elem, "rejection_strategy")
11✔
246
        if rejection_strategy is not None:
11✔
247
            constraints['rejection_strategy'] = rejection_strategy
×
248

249
        return constraints
11✔
250

251

252
class IndependentSource(SourceBase):
11✔
253
    """Distribution of phase space coordinates for source sites.
254

255
    .. versionadded:: 0.14.0
256

257
    Parameters
258
    ----------
259
    space : openmc.stats.Spatial
260
        Spatial distribution of source sites
261
    angle : openmc.stats.UnitSphere
262
        Angular distribution of source sites
263
    energy : openmc.stats.Univariate
264
        Energy distribution of source sites
265
    time : openmc.stats.Univariate
266
        time distribution of source sites
267
    strength : float
268
        Strength of the source
269
    particle : str or int or openmc.ParticleType
270
        Source particle type (name, PDG number, or type)
271
    domains : iterable of openmc.Cell, openmc.Material, or openmc.Universe
272
        Domains to reject based on, i.e., if a sampled spatial location is not
273
        within one of these domains, it will be rejected.
274

275
        .. deprecated:: 0.15.0
276
            Use the `constraints` argument instead.
277
    constraints : dict
278
        Constraints on sampled source particles. Valid keys include 'domains',
279
        'time_bounds', 'energy_bounds', 'fissionable', and 'rejection_strategy'.
280
        For 'domains', the corresponding value is an iterable of
281
        :class:`openmc.Cell`, :class:`openmc.Material`, or
282
        :class:`openmc.Universe` for which sampled sites must be within. For
283
        'time_bounds' and 'energy_bounds', the corresponding value is a sequence
284
        of floats giving the lower and upper bounds on time in [s] or energy in
285
        [eV] that the sampled particle must be within. For 'fissionable', the
286
        value is a bool indicating that only sites in fissionable material
287
        should be accepted. The 'rejection_strategy' indicates what should
288
        happen when a source particle is rejected: either 'resample' (pick a new
289
        particle) or 'kill' (accept and terminate).
290

291
    Attributes
292
    ----------
293
    space : openmc.stats.Spatial or None
294
        Spatial distribution of source sites
295
    angle : openmc.stats.UnitSphere or None
296
        Angular distribution of source sites
297
    energy : openmc.stats.Univariate or None
298
        Energy distribution of source sites
299
    time : openmc.stats.Univariate or None
300
        time distribution of source sites
301
    strength : float
302
        Strength of the source
303
    type : str
304
        Indicator of source type: 'independent'
305

306
        .. versionadded:: 0.14.0
307
    particle : str or int or openmc.ParticleType
308
        Source particle type (alias, PDG number, or GNDS nuclide name)
309
    constraints : dict
310
        Constraints on sampled source particles. Valid keys include
311
        'domain_type', 'domain_ids', 'time_bounds', 'energy_bounds',
312
        'fissionable', and 'rejection_strategy'.
313

314
    """
315

316
    def __init__(
11✔
317
        self,
318
        space: openmc.stats.Spatial | None = None,
319
        angle: openmc.stats.UnitSphere | None = None,
320
        energy: openmc.stats.Univariate | None = None,
321
        time: openmc.stats.Univariate | None = None,
322
        strength: float = 1.0,
323
        particle: str | int | ParticleType = 'neutron',
324
        domains: Sequence[openmc.Cell | openmc.Material |
325
                          openmc.Universe] | None = None,
326
        constraints: dict[str, Any] | None = None
327
    ):
328
        if domains is not None:
11✔
329
            warnings.warn("The 'domains' arguments has been replaced by the "
×
330
                          "'constraints' argument.", FutureWarning)
331
            constraints = {'domains': domains}
×
332

333
        super().__init__(strength=strength, constraints=constraints)
11✔
334

335
        self._space = None
11✔
336
        self._angle = None
11✔
337
        self._energy = None
11✔
338
        self._time = None
11✔
339

340
        if space is not None:
11✔
341
            self.space = space
11✔
342
        if angle is not None:
11✔
343
            self.angle = angle
11✔
344
        if energy is not None:
11✔
345
            self.energy = energy
11✔
346
        if time is not None:
11✔
347
            self.time = time
11✔
348
        self.particle = particle
11✔
349

350
    @property
11✔
351
    def type(self) -> str:
11✔
352
        return 'independent'
11✔
353

354
    def __getattr__(self, name):
11✔
355
        cls_names = {'file': 'FileSource', 'library': 'CompiledSource',
11✔
356
                     'parameters': 'CompiledSource'}
357
        if name in cls_names:
11✔
358
            raise AttributeError(
11✔
359
                f'The "{name}" attribute has been deprecated on the '
360
                f'IndependentSource class. Please use the {cls_names[name]} class.')
361
        else:
362
            super().__getattribute__(name)
11✔
363

364
    def __setattr__(self, name, value):
11✔
365
        if name in ('file', 'library', 'parameters'):
11✔
366
            # Ensure proper AttributeError is thrown
367
            getattr(self, name)
11✔
368
        else:
369
            super().__setattr__(name, value)
11✔
370

371
    @property
11✔
372
    def space(self):
11✔
373
        return self._space
11✔
374

375
    @space.setter
11✔
376
    def space(self, space):
11✔
377
        cv.check_type('spatial distribution', space, Spatial)
11✔
378
        self._space = space
11✔
379

380
    @property
11✔
381
    def angle(self):
11✔
382
        return self._angle
11✔
383

384
    @angle.setter
11✔
385
    def angle(self, angle):
11✔
386
        cv.check_type('angular distribution', angle, UnitSphere)
11✔
387
        self._angle = angle
11✔
388

389
    @property
11✔
390
    def energy(self):
11✔
391
        return self._energy
11✔
392

393
    @energy.setter
11✔
394
    def energy(self, energy):
11✔
395
        cv.check_type('energy distribution', energy, Univariate)
11✔
396
        self._energy = energy
11✔
397

398
    @property
11✔
399
    def time(self):
11✔
400
        return self._time
11✔
401

402
    @time.setter
11✔
403
    def time(self, time):
11✔
404
        cv.check_type('time distribution', time, Univariate)
11✔
405
        self._time = time
11✔
406

407
    @property
11✔
408
    def particle(self) -> ParticleType:
11✔
409
        return self._particle
11✔
410

411
    @particle.setter
11✔
412
    def particle(self, particle):
11✔
413
        self._particle = ParticleType(particle)
11✔
414

415
    def populate_xml_element(self, element):
11✔
416
        """Add necessary source information to an XML element
417

418
        Returns
419
        -------
420
        element : lxml.etree._Element
421
            XML element containing source data
422

423
        """
424
        element.set("particle", str(self.particle))
11✔
425
        if self.space is not None:
11✔
426
            element.append(self.space.to_xml_element())
11✔
427
        if self.angle is not None:
11✔
428
            element.append(self.angle.to_xml_element())
11✔
429
        if self.energy is not None:
11✔
430
            element.append(self.energy.to_xml_element('energy'))
11✔
431
        if self.time is not None:
11✔
432
            element.append(self.time.to_xml_element('time'))
11✔
433

434
    @classmethod
11✔
435
    def from_xml_element(cls, elem: ET.Element, meshes=None) -> SourceBase:
11✔
436
        """Generate source from an XML element
437

438
        Parameters
439
        ----------
440
        elem : lxml.etree._Element
441
            XML element
442
        meshes : dict
443
            Dictionary with mesh IDs as keys and openmc.MeshBase instaces as
444
            values
445

446
        Returns
447
        -------
448
        openmc.Source
449
            Source generated from XML element
450

451
        """
452
        constraints = cls._get_constraints(elem)
11✔
453
        source = cls(constraints=constraints)
11✔
454

455
        strength = get_text(elem, 'strength')
11✔
456
        if strength is not None:
11✔
457
            source.strength = float(strength)
11✔
458

459
        particle = get_text(elem, 'particle')
11✔
460
        if particle is not None:
11✔
461
            source.particle = particle
11✔
462

463
        space = elem.find('space')
11✔
464
        if space is not None:
11✔
465
            source.space = Spatial.from_xml_element(space, meshes)
11✔
466

467
        angle = elem.find('angle')
11✔
468
        if angle is not None:
11✔
469
            source.angle = UnitSphere.from_xml_element(angle)
11✔
470

471
        energy = elem.find('energy')
11✔
472
        if energy is not None:
11✔
473
            source.energy = Univariate.from_xml_element(energy)
11✔
474

475
        time = elem.find('time')
11✔
476
        if time is not None:
11✔
477
            source.time = Univariate.from_xml_element(time)
×
478

479
        return source
11✔
480

481

482
class MeshSource(SourceBase):
11✔
483
    """A source with a spatial distribution over mesh elements
484

485
    This class represents a mesh-based source in which random positions are
486
    uniformly sampled within mesh elements and each element can have independent
487
    angle, energy, and time distributions. The element sampled is chosen based
488
    on the relative strengths of the sources applied to the elements. The
489
    strength of the mesh source as a whole is the sum of all source strengths
490
    applied to the elements.
491

492
    .. versionadded:: 0.15.0
493

494
    Parameters
495
    ----------
496
    mesh : openmc.MeshBase
497
        The mesh over which source sites will be generated.
498
    sources : sequence of openmc.SourceBase
499
        Sources for each element in the mesh. Sources must be specified as
500
        either a 1-D array in the order of the mesh indices or a
501
        multidimensional array whose shape matches the mesh shape. If spatial
502
        distributions are set on any of the source objects, they will be ignored
503
        during source site sampling.
504
    constraints : dict
505
        Constraints on sampled source particles. Valid keys include 'domains',
506
        'time_bounds', 'energy_bounds', 'fissionable', and 'rejection_strategy'.
507
        For 'domains', the corresponding value is an iterable of
508
        :class:`openmc.Cell`, :class:`openmc.Material`, or
509
        :class:`openmc.Universe` for which sampled sites must be within. For
510
        'time_bounds' and 'energy_bounds', the corresponding value is a sequence
511
        of floats giving the lower and upper bounds on time in [s] or energy in
512
        [eV] that the sampled particle must be within. For 'fissionable', the
513
        value is a bool indicating that only sites in fissionable material
514
        should be accepted. The 'rejection_strategy' indicates what should
515
        happen when a source particle is rejected: either 'resample' (pick a new
516
        particle) or 'kill' (accept and terminate).
517

518
    Attributes
519
    ----------
520
    mesh : openmc.MeshBase
521
        The mesh over which source sites will be generated.
522
    sources : numpy.ndarray of openmc.SourceBase
523
        Sources to apply to each element
524
    strength : float
525
        Strength of the source
526
    type : str
527
        Indicator of source type: 'mesh'
528
    constraints : dict
529
        Constraints on sampled source particles. Valid keys include
530
        'domain_type', 'domain_ids', 'time_bounds', 'energy_bounds',
531
        'fissionable', and 'rejection_strategy'.
532

533
    """
534

535
    def __init__(
11✔
536
            self,
537
            mesh: MeshBase,
538
            sources: Sequence[SourceBase],
539
            constraints: dict[str, Any] | None = None,
540
    ):
541
        super().__init__(strength=None, constraints=constraints)
11✔
542
        self.mesh = mesh
11✔
543
        self.sources = sources
11✔
544

545
    @property
11✔
546
    def type(self) -> str:
11✔
547
        return "mesh"
11✔
548

549
    @property
11✔
550
    def mesh(self) -> MeshBase:
11✔
551
        return self._mesh
11✔
552

553
    @property
11✔
554
    def strength(self) -> float:
11✔
555
        return sum(s.strength for s in self.sources)
11✔
556

557
    @property
11✔
558
    def sources(self) -> np.ndarray:
11✔
559
        return self._sources
11✔
560

561
    @mesh.setter
11✔
562
    def mesh(self, m):
11✔
563
        cv.check_type('source mesh', m, MeshBase)
11✔
564
        self._mesh = m
11✔
565

566
    @sources.setter
11✔
567
    def sources(self, s):
11✔
568
        cv.check_iterable_type('mesh sources', s, SourceBase, max_depth=3)
11✔
569

570
        s = np.asarray(s)
11✔
571

572
        if isinstance(self.mesh, StructuredMesh):
11✔
573
            if s.size != self.mesh.n_elements:
11✔
574
                raise ValueError(
×
575
                    f'The length of the source array ({s.size}) does not match '
576
                    f'the number of mesh elements ({self.mesh.n_elements}).')
577

578
            # If user gave a multidimensional array, flatten in the order
579
            # of the mesh indices
580
            if s.ndim > 1:
11✔
581
                s = s.ravel(order='F')
11✔
582

583
        elif isinstance(self.mesh, UnstructuredMesh):
3✔
584
            if s.ndim > 1:
3✔
585
                raise ValueError(
×
586
                    'Sources must be a 1-D array for unstructured mesh')
587

588
        self._sources = s
11✔
589
        for src in self._sources:
11✔
590
            if isinstance(src, IndependentSource) and src.space is not None:
11✔
591
                warnings.warn('Some sources on the mesh have spatial '
×
592
                              'distributions that will be ignored at runtime.')
593
                break
×
594

595
    @strength.setter
11✔
596
    def strength(self, val):
11✔
597
        if val is not None:
11✔
598
            cv.check_type('mesh source strength', val, Real)
11✔
599
            self.set_total_strength(val)
11✔
600

601
    def set_total_strength(self, strength: float):
11✔
602
        """Scales the element source strengths based on a desired total strength.
603

604
        Parameters
605
        ----------
606
        strength : float
607
            Total source strength
608

609
        """
610
        current_strength = self.strength if self.strength != 0.0 else 1.0
11✔
611

612
        for s in self.sources:
11✔
613
            s.strength *= strength / current_strength
11✔
614

615
    def normalize_source_strengths(self):
11✔
616
        """Update all element source strengths such that they sum to 1.0."""
617
        self.set_total_strength(1.0)
11✔
618

619
    def populate_xml_element(self, elem: ET.Element):
11✔
620
        """Add necessary source information to an XML element
621

622
        Returns
623
        -------
624
        element : lxml.etree._Element
625
            XML element containing source data
626

627
        """
628
        elem.set("mesh", str(self.mesh.id))
11✔
629

630
        # write in the order of mesh indices
631
        for s in self.sources:
11✔
632
            elem.append(s.to_xml_element())
11✔
633

634
    @classmethod
11✔
635
    def from_xml_element(cls, elem: ET.Element, meshes) -> openmc.MeshSource:
11✔
636
        """
637
        Generate MeshSource from an XML element
638

639
        Parameters
640
        ----------
641
        elem : lxml.etree._Element
642
            XML element
643
        meshes : dict
644
            A dictionary with mesh IDs as keys and openmc.MeshBase instances as
645
            values
646

647
        Returns
648
        -------
649
        openmc.MeshSource
650
            MeshSource generated from the XML element
651
        """
652
        mesh_id = int(get_text(elem, 'mesh'))
11✔
653
        mesh = meshes[mesh_id]
11✔
654

655
        sources = [SourceBase.from_xml_element(
11✔
656
            e) for e in elem.iterchildren('source')]
657
        constraints = cls._get_constraints(elem)
11✔
658
        return cls(mesh, sources, constraints=constraints)
11✔
659

660

661
def Source(*args, **kwargs):
11✔
662
    """
663
    A function for backward compatibility of sources. Will be removed in the
664
    future. Please update to IndependentSource.
665
    """
666
    warnings.warn(
11✔
667
        "This class is deprecated in favor of 'IndependentSource'", FutureWarning)
668
    return openmc.IndependentSource(*args, **kwargs)
11✔
669

670

671
class CompiledSource(SourceBase):
11✔
672
    """A source based on a compiled shared library
673

674
    .. versionadded:: 0.14.0
675

676
    Parameters
677
    ----------
678
    library : path-like
679
        Path to a compiled shared library
680
    parameters : str
681
        Parameters to be provided to the compiled shared library function
682
    strength : float
683
        Strength of the source
684
    constraints : dict
685
        Constraints on sampled source particles. Valid keys include 'domains',
686
        'time_bounds', 'energy_bounds', 'fissionable', and 'rejection_strategy'.
687
        For 'domains', the corresponding value is an iterable of
688
        :class:`openmc.Cell`, :class:`openmc.Material`, or
689
        :class:`openmc.Universe` for which sampled sites must be within. For
690
        'time_bounds' and 'energy_bounds', the corresponding value is a sequence
691
        of floats giving the lower and upper bounds on time in [s] or energy in
692
        [eV] that the sampled particle must be within. For 'fissionable', the
693
        value is a bool indicating that only sites in fissionable material
694
        should be accepted. The 'rejection_strategy' indicates what should
695
        happen when a source particle is rejected: either 'resample' (pick a new
696
        particle) or 'kill' (accept and terminate).
697

698
    Attributes
699
    ----------
700
    library : pathlib.Path
701
        Path to a compiled shared library
702
    parameters : str
703
        Parameters to be provided to the compiled shared library function
704
    strength : float
705
        Strength of the source
706
    type : str
707
        Indicator of source type: 'compiled'
708
    constraints : dict
709
        Constraints on sampled source particles. Valid keys include
710
        'domain_type', 'domain_ids', 'time_bounds', 'energy_bounds',
711
        'fissionable', and 'rejection_strategy'.
712

713
    """
714

715
    def __init__(
11✔
716
        self,
717
        library: PathLike,
718
        parameters: str | None = None,
719
        strength: float = 1.0,
720
        constraints: dict[str, Any] | None = None
721
    ) -> None:
722
        super().__init__(strength=strength, constraints=constraints)
11✔
723
        self.library = library
11✔
724
        self._parameters = None
11✔
725
        if parameters is not None:
11✔
726
            self.parameters = parameters
×
727

728
    @property
11✔
729
    def type(self) -> str:
11✔
730
        return "compiled"
11✔
731

732
    @property
11✔
733
    def library(self) -> Path:
11✔
734
        return self._library
11✔
735

736
    @library.setter
11✔
737
    def library(self, library_name: PathLike):
11✔
738
        cv.check_type('library', library_name, PathLike)
11✔
739
        self._library = input_path(library_name)
11✔
740

741
    @property
11✔
742
    def parameters(self) -> str:
11✔
743
        return self._parameters
11✔
744

745
    @parameters.setter
11✔
746
    def parameters(self, parameters_path):
11✔
747
        cv.check_type('parameters', parameters_path, str)
11✔
748
        self._parameters = parameters_path
11✔
749

750
    def populate_xml_element(self, element):
11✔
751
        """Add necessary compiled source information to an XML element
752

753
        Returns
754
        -------
755
        element : lxml.etree._Element
756
            XML element containing source data
757

758
        """
759
        element.set("library", str(self.library))
11✔
760

761
        if self.parameters is not None:
11✔
762
            element.set("parameters", self.parameters)
11✔
763

764
    @classmethod
11✔
765
    def from_xml_element(cls, elem: ET.Element) -> openmc.CompiledSource:
11✔
766
        """Generate a compiled source from an XML element
767

768
        Parameters
769
        ----------
770
        elem : lxml.etree._Element
771
            XML element
772
        meshes : dict
773
            Dictionary with mesh IDs as keys and openmc.MeshBase instances as
774
            values
775

776
        Returns
777
        -------
778
        openmc.CompiledSource
779
            Source generated from XML element
780

781
        """
782
        kwargs = {'constraints': cls._get_constraints(elem)}
×
783
        kwargs['library'] = get_text(elem, 'library')
×
784

785
        source = cls(**kwargs)
×
786

787
        strength = get_text(elem, 'strength')
×
788
        if strength is not None:
×
789
            source.strength = float(strength)
×
790

791
        parameters = get_text(elem, 'parameters')
×
792
        if parameters is not None:
×
793
            source.parameters = parameters
×
794

795
        return source
×
796

797

798
class FileSource(SourceBase):
11✔
799
    """A source based on particles stored in a file
800

801
    .. versionadded:: 0.14.0
802

803
    Parameters
804
    ----------
805
    path : path-like
806
        Path to the source file from which sites should be sampled
807
    strength : float
808
        Strength of the source (default is 1.0)
809
    constraints : dict
810
        Constraints on sampled source particles. Valid keys include 'domains',
811
        'time_bounds', 'energy_bounds', 'fissionable', and 'rejection_strategy'.
812
        For 'domains', the corresponding value is an iterable of
813
        :class:`openmc.Cell`, :class:`openmc.Material`, or
814
        :class:`openmc.Universe` for which sampled sites must be within. For
815
        'time_bounds' and 'energy_bounds', the corresponding value is a sequence
816
        of floats giving the lower and upper bounds on time in [s] or energy in
817
        [eV] that the sampled particle must be within. For 'fissionable', the
818
        value is a bool indicating that only sites in fissionable material
819
        should be accepted. The 'rejection_strategy' indicates what should
820
        happen when a source particle is rejected: either 'resample' (pick a new
821
        particle) or 'kill' (accept and terminate).
822

823
    Attributes
824
    ----------
825
    path : Pathlike
826
        Source file from which sites should be sampled
827
    strength : float
828
        Strength of the source
829
    type : str
830
        Indicator of source type: 'file'
831
    constraints : dict
832
        Constraints on sampled source particles. Valid keys include
833
        'domain_type', 'domain_ids', 'time_bounds', 'energy_bounds',
834
        'fissionable', and 'rejection_strategy'.
835

836
    """
837

838
    def __init__(
11✔
839
        self,
840
        path: PathLike,
841
        strength: float = 1.0,
842
        constraints: dict[str, Any] | None = None
843
    ):
844
        super().__init__(strength=strength, constraints=constraints)
11✔
845
        self.path = path
11✔
846

847
    @property
11✔
848
    def type(self) -> str:
11✔
849
        return "file"
11✔
850

851
    @property
11✔
852
    def path(self) -> PathLike:
11✔
853
        return self._path
11✔
854

855
    @path.setter
11✔
856
    def path(self, p: PathLike):
11✔
857
        cv.check_type('source file', p, PathLike)
11✔
858
        self._path = input_path(p)
11✔
859

860
    def populate_xml_element(self, element):
11✔
861
        """Add necessary file source information to an XML element
862

863
        Returns
864
        -------
865
        element : lxml.etree._Element
866
            XML element containing source data
867

868
        """
869
        if self.path is not None:
11✔
870
            element.set("file", str(self.path))
11✔
871

872
    @classmethod
11✔
873
    def from_xml_element(cls, elem: ET.Element) -> openmc.FileSource:
11✔
874
        """Generate file source from an XML element
875

876
        Parameters
877
        ----------
878
        elem : lxml.etree._Element
879
            XML element
880
        meshes : dict
881
            Dictionary with mesh IDs as keys and openmc.MeshBase instances as
882
            values
883

884
        Returns
885
        -------
886
        openmc.FileSource
887
            Source generated from XML element
888

889
        """
890
        kwargs = {'constraints': cls._get_constraints(elem)}
×
891
        kwargs['path'] = get_text(elem, 'file')
×
892
        strength = get_text(elem, 'strength')
×
893
        if strength is not None:
×
894
            kwargs['strength'] = float(strength)
×
895

896
        return cls(**kwargs)
×
897

898

899

900

901
class SourceParticle:
11✔
902
    """Source particle
903

904
    This class can be used to create source particles that can be written to a
905
    file and used by OpenMC
906

907
    Parameters
908
    ----------
909
    r : iterable of float
910
        Position of particle in Cartesian coordinates
911
    u : iterable of float
912
        Directional cosines
913
    E : float
914
        Energy of particle in [eV]
915
    time : float
916
        Time of particle in [s]
917
    wgt : float
918
        Weight of the particle
919
    delayed_group : int
920
        Delayed group particle was created in (neutrons only)
921
    surf_id : int
922
        Surface ID where particle is at, if any.
923
    particle : ParticleType or str or int
924
        Type of the particle (type, name, or PDG number)
925

926
    """
927

928
    def __init__(
11✔
929
        self,
930
        r: Iterable[float] = (0., 0., 0.),
931
        u: Iterable[float] = (0., 0., 1.),
932
        E: float = 1.0e6,
933
        time: float = 0.0,
934
        wgt: float = 1.0,
935
        delayed_group: int = 0,
936
        surf_id: int = 0,
937
        particle: ParticleType | str | int = ParticleType.NEUTRON
938
    ):
939

940
        self.r = tuple(r)
11✔
941
        self.u = tuple(u)
11✔
942
        self.E = float(E)
11✔
943
        self.time = float(time)
11✔
944
        self.wgt = float(wgt)
11✔
945
        self.delayed_group = delayed_group
11✔
946
        self.surf_id = surf_id
11✔
947
        self.particle = particle
11✔
948

949
    @property
11✔
950
    def particle(self) -> ParticleType:
11✔
951
        return self._particle
11✔
952

953
    @particle.setter
11✔
954
    def particle(self, particle):
11✔
955
        self._particle = ParticleType(particle)
11✔
956

957
    def __repr__(self):
11✔
NEW
958
        return f'<SourceParticle: {str(self.particle)} at E={self.E:.6e} eV>'
×
959

960
    def to_tuple(self) -> tuple:
11✔
961
        """Return source particle attributes as a tuple
962

963
        Returns
964
        -------
965
        tuple
966
            Source particle attributes
967

968
        """
969
        return (self.r, self.u, self.E, self.time, self.wgt,
11✔
970
                self.delayed_group, self.surf_id, self.particle.pdg_number)
971

972

973
def write_source_file(
11✔
974
    source_particles: Iterable[SourceParticle],
975
    filename: PathLike, **kwargs
976
):
977
    """Write a source file using a collection of source particles
978

979
    Parameters
980
    ----------
981
    source_particles : iterable of SourceParticle
982
        Source particles to write to file
983
    filename : str or path-like
984
        Path to source file to write
985
    **kwargs
986
        Keyword arguments to pass to :class:`h5py.File`
987

988
    See Also
989
    --------
990
    openmc.SourceParticle
991

992
    """
993
    cv.check_iterable_type(
11✔
994
        "source particles", source_particles, SourceParticle)
995
    pl = ParticleList(source_particles)
11✔
996
    pl.export_to_hdf5(filename, **kwargs)
11✔
997

998

999
class ParticleList(list):
11✔
1000
    """A collection of SourceParticle objects.
1001

1002
    Parameters
1003
    ----------
1004
    particles : list of SourceParticle
1005
        Particles to collect into the list
1006

1007
    """
1008
    @classmethod
11✔
1009
    def from_hdf5(cls, filename: PathLike) -> ParticleList:
11✔
1010
        """Create particle list from an HDF5 file.
1011

1012
        Parameters
1013
        ----------
1014
        filename : path-like
1015
            Path to source file to read.
1016

1017
        Returns
1018
        -------
1019
        ParticleList instance
1020

1021
        """
1022
        with h5py.File(filename, 'r') as fh:
11✔
1023
            filetype = fh.attrs['filetype']
11✔
1024
            arr = fh['source_bank'][...]
11✔
1025

1026
        if filetype != b'source':
11✔
1027
            raise ValueError(f'File {filename} is not a source file')
×
1028

1029
        source_particles = [
11✔
1030
            SourceParticle(*params, ParticleType(particle))
1031
            for *params, particle in arr
1032
        ]
1033
        return cls(source_particles)
11✔
1034

1035
    @classmethod
11✔
1036
    def from_mcpl(cls, filename: PathLike) -> ParticleList:
11✔
1037
        """Create particle list from an MCPL file.
1038

1039
        Parameters
1040
        ----------
1041
        filename : path-like
1042
            Path to MCPL file to read.
1043

1044
        Returns
1045
        -------
1046
        ParticleList instance
1047

1048
        """
1049
        import mcpl
×
1050
        # Process .mcpl file
1051
        particles = []
×
1052
        with mcpl.MCPLFile(filename) as f:
×
1053
            for particle in f.particles:
×
NEW
1054
                particle_type = ParticleType(particle.pdgcode)
×
1055

1056
                # Create a source particle instance. Note that MCPL stores
1057
                # energy in MeV and time in ms.
1058
                source_particle = SourceParticle(
×
1059
                    r=tuple(particle.position),
1060
                    u=tuple(particle.direction),
1061
                    E=1.0e6*particle.ekin,
1062
                    time=1.0e-3*particle.time,
1063
                    wgt=particle.weight,
1064
                    particle=particle_type
1065
                )
1066
                particles.append(source_particle)
×
1067

1068
        return cls(particles)
×
1069

1070
    def __getitem__(self, index):
11✔
1071
        """
1072
        Return a new ParticleList object containing the particle(s)
1073
        at the specified index or slice.
1074

1075
        Parameters
1076
        ----------
1077
        index : int, slice or list
1078
            The index, slice or list to select from the list of particles
1079

1080
        Returns
1081
        -------
1082
        openmc.ParticleList or openmc.SourceParticle
1083
            A new object with the selected particle(s)
1084
        """
1085
        if isinstance(index, int):
11✔
1086
            # If it's a single integer, return the corresponding particle
1087
            return super().__getitem__(index)
11✔
1088
        elif isinstance(index, slice):
11✔
1089
            # If it's a slice, return a new ParticleList object with the
1090
            # sliced particles
1091
            return ParticleList(super().__getitem__(index))
11✔
1092
        elif isinstance(index, list):
11✔
1093
            # If it's a list of integers, return a new ParticleList object with
1094
            # the selected particles. Note that Python 3.10 gets confused if you
1095
            # use super() here, so we call list.__getitem__ directly.
1096
            return ParticleList([list.__getitem__(self, i) for i in index])
11✔
1097
        else:
1098
            raise TypeError(f"Invalid index type: {type(index)}. Must be int, "
×
1099
                            "slice, or list of int.")
1100

1101
    def to_dataframe(self) -> pd.DataFrame:
11✔
1102
        """A dataframe representing the source particles
1103

1104
        Returns
1105
        -------
1106
        pandas.DataFrame
1107
            DataFrame containing the source particles attributes.
1108
        """
1109
        # Extract the attributes of the source particles into a list of tuples
1110
        data = [(sp.r[0], sp.r[1], sp.r[2], sp.u[0], sp.u[1], sp.u[2],
11✔
1111
                 sp.E, sp.time, sp.wgt, sp.delayed_group, sp.surf_id,
1112
                 str(sp.particle)) for sp in self]
1113

1114
        # Define the column names for the DataFrame
1115
        columns = ['x', 'y', 'z', 'u_x', 'u_y', 'u_z', 'E', 'time', 'wgt',
11✔
1116
                   'delayed_group', 'surf_id', 'particle']
1117

1118
        # Create the pandas DataFrame from the data
1119
        return pd.DataFrame(data, columns=columns)
11✔
1120

1121
    def export_to_hdf5(self, filename: PathLike, **kwargs):
11✔
1122
        """Export particle list to an HDF5 file.
1123

1124
        This method write out an .h5 file that can be used as a source file in
1125
        conjunction with the :class:`openmc.FileSource` class.
1126

1127
        Parameters
1128
        ----------
1129
        filename : path-like
1130
            Path to source file to write
1131
        **kwargs
1132
            Keyword arguments to pass to :class:`h5py.File`
1133

1134
        See Also
1135
        --------
1136
        openmc.FileSource
1137

1138
        """
1139
        # Create compound datatype for source particles
1140
        pos_dtype = np.dtype([('x', '<f8'), ('y', '<f8'), ('z', '<f8')])
11✔
1141
        source_dtype = np.dtype([
11✔
1142
            ('r', pos_dtype),
1143
            ('u', pos_dtype),
1144
            ('E', '<f8'),
1145
            ('time', '<f8'),
1146
            ('wgt', '<f8'),
1147
            ('delayed_group', '<i4'),
1148
            ('surf_id', '<i4'),
1149
            ('particle', '<i4'),
1150
        ])
1151

1152
        # Create array of source particles
1153
        arr = np.array([s.to_tuple() for s in self], dtype=source_dtype)
11✔
1154

1155
        # Write array to file
1156
        kwargs.setdefault('mode', 'w')
11✔
1157
        with h5py.File(filename, **kwargs) as fh:
11✔
1158
            fh.attrs['filetype'] = np.bytes_("source")
11✔
1159
            fh.attrs['version'] = np.array([_VERSION_STATEPOINT, 2])
11✔
1160
            fh.create_dataset('source_bank', data=arr, dtype=source_dtype)
11✔
1161

1162

1163
def read_source_file(filename: PathLike) -> ParticleList:
11✔
1164
    """Read a source file and return a list of source particles.
1165

1166
    .. versionadded:: 0.15.0
1167

1168
    Parameters
1169
    ----------
1170
    filename : str or path-like
1171
        Path to source file to read
1172

1173
    Returns
1174
    -------
1175
    openmc.ParticleList
1176

1177
    See Also
1178
    --------
1179
    openmc.SourceParticle
1180

1181
    """
1182
    filename = Path(filename)
11✔
1183
    if filename.suffix not in ('.h5', '.mcpl'):
11✔
1184
        raise ValueError('Source file must have a .h5 or .mcpl extension.')
×
1185

1186
    if filename.suffix == '.h5':
11✔
1187
        return ParticleList.from_hdf5(filename)
11✔
1188
    else:
1189
        return ParticleList.from_mcpl(filename)
×
1190

1191

1192
def read_collision_track_hdf5(filename):
11✔
1193
    """Read a collision track file in HDF5 format.
1194

1195
    Parameters
1196
    ----------
1197
    filename : str or path-like
1198
        Path to the HDF5 collision track file.
1199

1200
    Returns
1201
    -------
1202
    numpy.ndarray
1203
        Structured array containing collision track data.
1204

1205
    See Also
1206
    --------
1207
    read_collision_track_mcpl
1208
    read_collision_track_file
1209
    """
1210

1211
    with h5py.File(filename, 'r') as file:
11✔
1212
        data = file['collision_track_bank'][:]
11✔
1213

1214
    return data
11✔
1215

1216

1217
def read_collision_track_mcpl(file_path):
11✔
1218
    """Read a collision track file in MCPL format.
1219

1220
    Parameters
1221
    ----------
1222
    file_path : str or path-like
1223
        Path to the MCPL collision track file.
1224

1225
    Returns
1226
    -------
1227
    numpy.ndarray
1228
        Structured array of particle collision track information, including
1229
        position, direction, energy, weight, reaction data, and identifiers.
1230

1231
    See Also
1232
    --------
1233
    read_collision_track_hdf5
1234
    read_collision_track_file
1235
    """
1236
    import mcpl
11✔
1237
    myfile = mcpl.MCPLFile(file_path)
11✔
1238
    data = {
11✔
1239
        'r': [],  # for position (x, y, z)
1240
        'u': [],  # for direction (ux, uy, uz)
1241
        'E': [], 'dE': [], 'time': [],
1242
        'wgt': [], 'event_mt': [], 'delayed_group': [],
1243
        'cell_id': [], 'nuclide_id': [], 'material_id': [],
1244
        'universe_id': [], 'n_collision': [], 'particle': [],
1245
        'parent_id': [], 'progeny_id': []
1246
    }
1247

1248
    # Read and collect data from the MCPL file
1249
    for i, p in enumerate(myfile.particles):
11✔
1250
        if f'blob_{i}' in myfile.blobs:
11✔
1251
            blob_data = myfile.blobs[f'blob_{i}']
11✔
1252
            decoded_str = blob_data.decode('utf-8')
11✔
1253
            pairs = decoded_str.split(';')
11✔
1254
            values_dict = {k.strip(): v.strip()
11✔
1255
                           for k, v in (pair.split(':') for pair in pairs if pair.strip())}
1256

1257
            data['r'].append((p.x, p.y, p.z))  # Append as tuple
11✔
1258
            data['u'].append((p.ux, p.uy, p.uz))  # Append as tuple
11✔
1259
            data['E'].append(p.ekin * 1e6)
11✔
1260
            data['dE'].append(float(values_dict.get('dE', 0)))
11✔
1261
            data['time'].append(p.time * 1e-3)
11✔
1262
            data['wgt'].append(p.weight)
11✔
1263
            data['event_mt'].append(int(values_dict.get('event_mt', 0)))
11✔
1264
            data['delayed_group'].append(
11✔
1265
                int(values_dict.get('delayed_group', 0)))
1266
            data['cell_id'].append(int(values_dict.get('cell_id', 0)))
11✔
1267
            data['nuclide_id'].append(int(values_dict.get('nuclide_id', 0)))
11✔
1268
            data['material_id'].append(int(values_dict.get('material_id', 0)))
11✔
1269
            data['universe_id'].append(int(values_dict.get('universe_id', 0)))
11✔
1270
            data['n_collision'].append(int(values_dict.get('n_collision', 0)))
11✔
1271
            data['particle'].append(ParticleType(p.pdgcode))
11✔
1272
            data['parent_id'].append(int(values_dict.get('parent_id', 0)))
11✔
1273
            data['progeny_id'].append(int(values_dict.get('progeny_id', 0)))
11✔
1274

1275
    dtypes = [
11✔
1276
        ('r', [('x', 'f8'), ('y', 'f8'), ('z', 'f8')]),
1277
        ('u', [('x', 'f8'), ('y', 'f8'), ('z', 'f8')]),
1278
        ('E', 'f8'), ('dE', 'f8'), ('time', 'f8'), ('wgt', 'f8'),
1279
        ('event_mt', 'f8'), ('delayed_group', 'i4'), ('cell_id', 'i4'),
1280
        ('nuclide_id', 'i4'), ('material_id', 'i4'), ('universe_id', 'i4'),
1281
        ('n_collision', 'i4'), ('particle', 'i4'),
1282
        ('parent_id', 'i8'), ('progeny_id', 'i8')
1283
    ]
1284

1285
    structured_array = np.zeros(len(data['r']), dtype=dtypes)
11✔
1286
    for key in data:
11✔
1287
        structured_array[key] = data[key]  # Assign data
11✔
1288

1289
    return structured_array
11✔
1290

1291

1292
def read_collision_track_file(filename):
11✔
1293
    """Read a collision track file (HDF5 or MCPL) and return its data.
1294

1295
    Parameters
1296
    ----------
1297
    filename : str or path-like
1298
        Path to the collision track file to read. Must end with
1299
        ``.h5`` or ``.mcpl``.
1300

1301
    Returns
1302
    -------
1303
    numpy.ndarray
1304
        Structured array containing collision track data.
1305

1306
    See Also
1307
    --------
1308
    read_collision_track_hdf5
1309
    read_collision_track_mcpl
1310
    """
1311

1312
    filename = Path(filename)
11✔
1313
    if filename.suffix not in ('.h5', '.mcpl'):
11✔
1314
        raise ValueError('Collision track file must have a .h5 or .mcpl extension.')
×
1315

1316
    if filename.suffix == '.h5':
11✔
1317
        return read_collision_track_hdf5(filename)
11✔
1318
    else:
1319
        return read_collision_track_mcpl(filename)
11✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc