Skip to main content

aci_rssa

CI PyPI License: EUPL-1.2

Fast, streaming reader for MCNP RSSA (surface source) files, with a Rust core and a Polars-based Python API.

RSSA files record every particle that crosses a given surface during an MCNP (or D1S-UNED) simulation, and can easily reach tens or hundreds of gigabytes. aci_rssa memory-maps the file and decodes track records in parallel (via rayon), handing the result to Python as a Polars DataFrame — either all at once, or as a lazy, batch-at-a-time stream for files too large to fit in memory.

Installation

pip install aci_rssa

Prebuilt wheels are published for Linux, macOS, and Windows (x86_64/arm64) on CPython ≥3.10 — no Rust toolchain needed to install.

Quickstart

from aci_rssa import RSSA

rssa = RSSA.read_from_file("surface_source.w")
print(rssa)  # summary: surfaces, track/history counts, source code

rssa.tracks          # polars.DataFrame: history, particle_type, weight, energy,
                      # time, x, y, z, u, v, w, surface_id
rssa.neutron_tracks   # tracks filtered to neutrons
rssa.photon_tracks    # tracks filtered to everything else (typically photons)

Reading only the header (fast, never touches the potentially huge track data):

from aci_rssa import FileParameters

parameters = FileParameters.read_from_file("surface_source.w")
parameters.nrss           # number of tracks recorded
parameters.surfaces       # surfaces tracks were recorded on
parameters.surface_ids    # just their ids

surface = parameters.get_surface(63)
surface.mnemonic          # "cz"
surface.axis, surface.center, surface.radius   # "z", (0.0, 0.0), 164.3
print(surface.describe()) # surface 63: cz (cylinder about the z axis, radius 164.30 cm)

Surface coefficients are exposed as MCNP stores them (surface.parameters, e.g. a cylinder's squared radius); the properties above do the conversion. Planes give normal and offset instead.

Very large files

For files too large to load eagerly, scan_tracks returns a Polars LazyFrame backed by a Rust reader that decodes the file batch by batch, driven by Polars' query engine — filters and column selection are pushed down so you only decode what you need:

from aci_rssa import scan_tracks
import polars as pl

(
    scan_tracks("surface_source.w")
    .filter(pl.col("energy") > 1.0)
    .select("x", "y", "z", "energy")
    .collect(engine="streaming")
)

RSSAPlot and RSSASpectraPlot accept a LazyFrame directly, so a scan_tracks() result can be plotted without ever loading the file eagerly — build them with RSSAPlot.for_surface() instead of going through RSSA.read_from_file(), which requires an in-memory RSSA:

from aci_rssa import FileParameters, RSSAPlot, neutrons, scan_tracks

parameters = FileParameters.read_from_file("surface_source.w")  # header only
plot = RSSAPlot.for_surface(scan_tracks("surface_source.w"), parameters, 63)

(
    plot.filter(neutrons())
    .with_bin_width(10)
    .particle_flux(1e17)
    .show()
)

Every aggregation (with_bin_width, particle_flux, …) streams through the file batch by batch rather than materializing it.

Plotting

rssa.plot_surface(63).with_bin_width(10).particle_flux(1e17).show()
rssa.plot_spectra().spectrum().show()

There are three steps, and they are separate on purpose: a projection decides how the surface is unrolled into 2D, a plot bins the tracks on it, and a result (CurrentMap, VectorCurrent, Spectrum) is a computed value that knows how to render itself.

Projections

plot_surface() takes the geometry from the file header: it keeps only the tracks that crossed that surface, and picks the projection from the surface's own type — a cylinder is unrolled around its own axis and centre over its full circumference, a px/py/pz plane is plotted on the two axes spanning it, and a tilted p plane gets an orthonormal basis of the plane itself. The surface id may be omitted only when the file recorded a single surface.

This is the only entry point to a current map, because the projection is what makes the map's numbers right: every quantity divides each bin's summed weight by dx * dy, so a projection that doesn't preserve distances on the surface — dropping a tilted plane onto two global axes shrinks one axis by the cosine of the tilt; measuring a cylinder's angle about the coordinate origin instead of its own axis smears it — gives a plausible plot that is wrong by that factor.

The escape hatch is therefore to override the geometry, not to skip it:

from aci_rssa import Cylinder

rssa.plot_surface(63, projection=Cylinder(axis="z", radius=164.3, center=(0.0, 0.0)))

Cylinder, AxisAlignedPlane and TiltedPlane are the built-in projections; a surface type none of them fits (a sphere, a torus) raises rather than being rastered onto two global axes. Unless verify=False, a sample of the tracks is checked against the surface's equation, so a header that means something other than what is assumed here fails loudly instead of producing a plausible, wrong plot.

Selecting tracks

The tracks are a Polars LazyFrame, so selection is done with ordinary Polars expressions via filter(). aci_rssa only supplies the two predicates that need knowledge of the file rather than of Polars — the packed neutron code, and surface ids (matched irrespective of sign, and refused on files that don't record them per track):

import polars as pl
from aci_rssa import neutrons, on_surface

plot.filter(neutrons(), pl.col("energy") > 1.0, pl.col("z").is_between(-600, 800))

filter() returns a new plot, so one object can feed several derived ones.

Results

plot = rssa.plot_surface(63).filter(neutrons()).with_bin_width(10)

flux = plot.particle_flux(1e17)        # CurrentMap: scalar flux, #/cm2/s per bin
current = plot.particle_current(1e17)  # CurrentMap: crossing rate per bin
net = plot.net_current(1e17)           # CurrentMap: net leakage per bin, signed
arrows = plot.vector_current(1e17)     # VectorCurrent: J and anisotropy per bin
angles = plot.angular_distribution(1e17)  # AngularDistribution: ψ(Ω) itself

flux.with_parameters(vmin=1e6, vmax=1e12).show()
flux.ratio_to(other_flux).save("difference.png")

Each result carries its own values, bins and rendering options; with_parameters() takes keyword overrides (keeping the labels and title derived from the surface) or a whole PlotParameters to replace them. figure() hands back the Matplotlib figure and axes for anything the parameters don't cover.

Pass errors=True to any of the maps to estimate its statistical error in the same pass, then plot it with error_map():

flux = plot.particle_flux(1e17, errors=True)
flux.relative_errors      # array, or None when it wasn't asked for
flux.error_map().show()   # the errors as a map of their own

The error is carried on the map rather than computed by a separate call, so it can only ever belong to the quantity it was estimated for — the error of a flux is not the error of a current.

Which quantity do you want?

All four come from one identity: a set of surface crossings samples the angular flux weighted by |μ| — head-on crossings are over-represented, because a patch of surface presents its full area to them. Summing w·f(Ω) over tracks then estimates ∫f(Ω)|μ|ψ(Ω)dΩ, and the choice of f is the choice of quantity:

f(Ω) Quantity Method Use for
1/|μ| scalar flux Φ particle_flux() dose rates, reaction rates, activation — anything multiplied by a response function
1 crossing rate J⁺+J⁻ particle_current() how much weight crosses a patch, e.g. re-emitting the file as a source
sign(μ) net current J·n̂ net_current() what actually leaks through, with backscatter cancelling
Ω/|μ| vector current J vector_current() which way it flows, and how collimated

If you are about to multiply a map by a response function, you want particle_flux(), not particle_current(). They are not interchangeable: for an isotropic field the mean of 1/|μ| over the crossing distribution is exactly 2, so a current map understates the flux by a factor of two where the field is diffuse while agreeing with it where the field is collimated — it distorts the shape of the map, not just its scale. This is MCNP's F1 vs F2 distinction, and aci_rssa computes both from the surface normal the projection already carries.

VectorCurrent.table's anisotropy column is |J|/Φ, the flux-to-current ratio: 1 for a beam, 0.5 for a half-isotropic distribution, ~0 where directions cancel. It also bounds the gap above — since 1/|μ| is convex, Jensen's inequality gives Φ / crossing rate ≥ 1 / anisotropy.

Tracks approaching tangency (|μ| → 0) would send 1/|μ| to infinity. Following MCNP, particle_flux() scores a track below |μ| = 1e-3 as if it were 5e-4, and grazing_fraction() maps how much of each bin's flux came from tracks that were capped. That blow-up is real physics — a shallow track really does deposit more path length in a thin volume at the surface — so it is capped, not clipped away.

Seeing the directions themselves

The four quantities above are all moments of one thing, the angular flux ψ(Ω). angular_distribution() estimates ψ(Ω) directly, which is worth doing wherever a moment is not enough — and one place it never is, is a mean direction: vector_current() cannot tell an isotropic field from two opposing beams, since both average to |J| = 0. Where anisotropy is low, this is what says which of the two you have.

angles = plot.angular_distribution(1e17)
angles.show()        # two equal-area polar disks, outgoing and incoming
angles.scalar_flux   # ∫ψ dΩ, the scalar flux averaged over the patch

Directions are binned on μ and on the azimuth φ in the surface's own tangent plane, aligned with the map's axes: φ = 0 points the way the plot's x axis grows, so a lobe leaning up-and-right in the disk means particles heading up-and-right on the current map — including on a cylinder, where the frame turns from bin to bin. Sectors are centred on the named directions rather than divided by them, so a beam aimed along an axis reads as one petal instead of two halves.

The radius is √(2(1−|μ|)) — head-on at the centre, grazing at the rim. That is the Lambert equal-area radius, chosen so each cell's area on the page is proportional to the solid angle it stands for: plotting the polar angle as the radius instead would inflate the near-normal directions into a wide bullseye, the same area-encoding bias that makes a bar-length rose diagram overstate its longest petal. The μ grid is uniform for the same reason, so every cell subtends the same solid angle and the colours are directly comparable — as are the two disks, which share one scale.

Each track enters its cell as w/|μ|, exactly as in particle_flux(). This is not cosmetic: crossings sample ψ weighted by |μ|, so summing raw weight per direction would draw a dent at grazing angles that is an artefact of how a surface samples a field rather than a feature of the field itself — and grazing angles are precisely where an angular plot gets read.

Unlike the maps, this is a pooled quantity: every track inside the bins contributes equally to one histogram, with no per-bin averaging. The bins therefore only choose the region — filter or narrow them to zoom in — and the area ψ is divided by comes from the extent the tracks span, not from the bin grid, so bin_width does not change the result. (It would otherwise: with_bin_width() pads its last bin past the data, so a 100 cm bin over tracks spanning 60 cm claims a strip two thirds empty and dilutes every value by 1.67. A per-bin map is unaffected — there the padding lands in one edge bin that really does cover surface no track reached.) Pass area= to state the patch explicitly when the region is a deliberate choice rather than wherever the tracks landed:

plot.angular_distribution(1e17, area=200 * 200)   # a defined window, empty parts and all

Migrating from 0.0.7

Before Now
rssa.plot_cyl(axis=...), rssa.plot_plane(x=..., y=...) rssa.plot_surface(id), optionally with projection=Cylinder(...) / AxisAlignedPlane(...) / TiltedPlane(...)
RSSAPlot(tracks, params).set_surface(surface) RSSAPlot.for_surface(tracks, params, surface_id)
.set_particle("n"), .set_surface_ids([63]), .set_z_limits(a, b), .set_axis_limits(c, a, b), .set_perimeter_limits(a, b) .filter(neutrons()), .filter(on_surface(params, 63)), .filter(pl.col("z").is_between(a, b)), … (the old names still work, with a DeprecationWarning)
.set_bins(x, y), .calculate_bins(bin_width=w) .with_bins(x, y), .with_bin_width(w)
.get_particle_current(i), .get_vector_current(i) .particle_current(i), .vector_current(i) — each returns a result rather than mutating the plot
.get_particle_current_errors() .particle_current(i, errors=True).error_map() — the error now travels with the quantity it belongs to, and any of particle_flux/particle_current/net_current can produce one
.get_ratio_to(other) current.ratio_to(other_current)
.set_plot_parameters(PlotParameters(...)) result.with_parameters(...)
.get_plot(), .show(), .save_figure(p) result.figure(), result.show(), result.save(p)
.get_vector_current_plot(), .show_vector_current(), .save_vector_current_figure(p) vector_current.figure() / .show() / .save(p)
.get_spectra_info(), .get_combined_plot_with_other_spectras(*others) .spectrum(), spectrum.overlay(*others)
rssa.x, rssa.y, rssa.z, rssa.energies, rssa.weight, rssa.histories rssa.tracks["x"], … (weight used to return .abs(), unlike every aggregation in the package)

The default colormap changed from jet to plasma, and relative errors now render on a linear scale from 0 rather than a log one.

Two changes affect numbers, not just names:

  • vector_current() now returns the real J. It used to sum w·Ω with no 1/|μ|, which is the first moment of the crossing distribution rather than of the flux — correct only at normal incidence, and short by the incidence cosine everywhere else. Its scalar_current column is now crossing_rate, joined by flux, net_current, and an anisotropy that is |J|/Φ rather than |J|/crossing rate.
  • particle_current() is unchanged, but is now one of four quantities rather than the only one. If you were reading it as a flux, switch to particle_flux(); if you were reading it as net leakage, switch to net_current().

Development

This is a Cargo workspace (rssa-core, the pure-Rust parsing/decoding library, and rssa-python, its PyO3 bindings) plus a Python package in python/, built together with maturin.

python -m venv .venv && source .venv/bin/activate
pip install "maturin>=1.9.4,<2.0"
maturin develop --extras test,dev   # builds the extension module, installs test/dev deps

cargo test                          # Rust tests (rssa-core)
pytest                              # Python tests
ty check python/aci_rssa            # type checking

License

Licensed under the European Union Public Licence v1.2 (EUPL-1.2).

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

aci_rssa-0.0.9.tar.gz (95.4 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

aci_rssa-0.0.9-cp310-abi3-win_arm64.whl (4.5 MB view details)

Uploaded CPython 3.10+Windows ARM64

aci_rssa-0.0.9-cp310-abi3-win_amd64.whl (5.1 MB view details)

Uploaded CPython 3.10+Windows x86-64

aci_rssa-0.0.9-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (5.0 MB view details)

Uploaded CPython 3.10+manylinux: glibc 2.17+ x86-64

aci_rssa-0.0.9-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (4.6 MB view details)

Uploaded CPython 3.10+manylinux: glibc 2.17+ ARM64

aci_rssa-0.0.9-cp310-abi3-macosx_11_0_arm64.whl (4.5 MB view details)

Uploaded CPython 3.10+macOS 11.0+ ARM64

aci_rssa-0.0.9-cp310-abi3-macosx_10_12_x86_64.whl (4.9 MB view details)

Uploaded CPython 3.10+macOS 10.12+ x86-64

File details

Details for the file aci_rssa-0.0.9.tar.gz.

File metadata

  • Download URL: aci_rssa-0.0.9.tar.gz
  • Upload date:
  • Size: 95.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for aci_rssa-0.0.9.tar.gz
Algorithm Hash digest
SHA256 7f580a07c8b22a3a35c93f7ff836dbfaed445306c2d847352dbdb7eeac358cfe
MD5 66133cafccc0816b8d6d55d43ba1a6a9
BLAKE2b-256 87cec9f06d0c3685e6e59af7e7f6e1240725a9a01ed4e9db3292d76506bf3f4e

See more details on using hashes here.

Provenance

The following attestation bundles were made for aci_rssa-0.0.9.tar.gz:

Publisher: release.yml on AlvaroCubi/aci_rssa

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file aci_rssa-0.0.9-cp310-abi3-win_arm64.whl.

File metadata

  • Download URL: aci_rssa-0.0.9-cp310-abi3-win_arm64.whl
  • Upload date:
  • Size: 4.5 MB
  • Tags: CPython 3.10+, Windows ARM64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for aci_rssa-0.0.9-cp310-abi3-win_arm64.whl
Algorithm Hash digest
SHA256 059da5f1299494466b70e3d00a9ad9b958cbde40696c4450f50e097ee7c5a158
MD5 bcd5bc26f8bbe8789d25fb53aaf75ebe
BLAKE2b-256 6b543cb52e566a2616c792137da8d505b7c4cc4fc25da248325905bb8a779a22

See more details on using hashes here.

Provenance

The following attestation bundles were made for aci_rssa-0.0.9-cp310-abi3-win_arm64.whl:

Publisher: release.yml on AlvaroCubi/aci_rssa

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file aci_rssa-0.0.9-cp310-abi3-win_amd64.whl.

File metadata

  • Download URL: aci_rssa-0.0.9-cp310-abi3-win_amd64.whl
  • Upload date:
  • Size: 5.1 MB
  • Tags: CPython 3.10+, Windows x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for aci_rssa-0.0.9-cp310-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 649d7d295e1ce02b5b43ff2eb8b6e1fcd164d1ef122f1133ef386272cbd65d39
MD5 09ea3537f4bab9a54cff029b3c9b0b4e
BLAKE2b-256 c81a1fbd52e76af6d28397b52a642407c249d889cb5dc68c0afba9749b896f10

See more details on using hashes here.

Provenance

The following attestation bundles were made for aci_rssa-0.0.9-cp310-abi3-win_amd64.whl:

Publisher: release.yml on AlvaroCubi/aci_rssa

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file aci_rssa-0.0.9-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for aci_rssa-0.0.9-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 e2d14f7b3302223977cf73d1d0790e4066dfd25ecfb87c8b923324aa02df170f
MD5 f207a9359828e63b791cef5fdb6a8e9c
BLAKE2b-256 ca19d35786b249efe27cf3afe2be912234259ad8feee8546f6496138c9bc5d80

See more details on using hashes here.

Provenance

The following attestation bundles were made for aci_rssa-0.0.9-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:

Publisher: release.yml on AlvaroCubi/aci_rssa

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file aci_rssa-0.0.9-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for aci_rssa-0.0.9-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 2e86ec5d525032f8ac8e264e0c7f5e172178416fd624861e991020b8a69e3f90
MD5 92c7f846e0d9dd9f85a9f1aeab12f4f7
BLAKE2b-256 6790cb6f7503b1dda0d6a9d5cd73e00b97f40a950128745b93bdf7352d6ebc58

See more details on using hashes here.

Provenance

The following attestation bundles were made for aci_rssa-0.0.9-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl:

Publisher: release.yml on AlvaroCubi/aci_rssa

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file aci_rssa-0.0.9-cp310-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for aci_rssa-0.0.9-cp310-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 b4b95081a12a4a9255092da0266d3a312c2f8396448f0d8a3e52f3ec783cff93
MD5 665e4695bfc284168a5285ef830384cf
BLAKE2b-256 012536cb92a66973ed1251b104542c8a3cb5d9d401ef57ba8270826ea2bff754

See more details on using hashes here.

Provenance

The following attestation bundles were made for aci_rssa-0.0.9-cp310-abi3-macosx_11_0_arm64.whl:

Publisher: release.yml on AlvaroCubi/aci_rssa

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file aci_rssa-0.0.9-cp310-abi3-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for aci_rssa-0.0.9-cp310-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 114ea59d598bb0f325b009406a7c0257bb963bb327842898d53a93fe7f2d519c
MD5 89541e398a28490343df782ee22dbc39
BLAKE2b-256 e4522ad1b892875fd83b9f1e442813d03ea72756752d5550177cbc71e2cbe82c

See more details on using hashes here.

Provenance

The following attestation bundles were made for aci_rssa-0.0.9-cp310-abi3-macosx_10_12_x86_64.whl:

Publisher: release.yml on AlvaroCubi/aci_rssa

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page