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.10.tar.gz (95.6 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.10-cp310-abi3-win_arm64.whl (4.5 MB view details)

Uploaded CPython 3.10+Windows ARM64

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

Uploaded CPython 3.10+Windows x86-64

aci_rssa-0.0.10-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.10-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.10-cp310-abi3-macosx_11_0_arm64.whl (4.5 MB view details)

Uploaded CPython 3.10+macOS 11.0+ ARM64

aci_rssa-0.0.10-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.10.tar.gz.

File metadata

  • Download URL: aci_rssa-0.0.10.tar.gz
  • Upload date:
  • Size: 95.6 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.10.tar.gz
Algorithm Hash digest
SHA256 d39f65b7517010920e34c3773f417cd8c32f3c4b5c12feb75576b21f2e5f7d22
MD5 c622d57af45e69d0f464772443a30ab0
BLAKE2b-256 c6b61692dffe639fed4dada3a3494c20fe185388ba0b737b8b9739b61cdc1f39

See more details on using hashes here.

Provenance

The following attestation bundles were made for aci_rssa-0.0.10.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.10-cp310-abi3-win_arm64.whl.

File metadata

  • Download URL: aci_rssa-0.0.10-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.10-cp310-abi3-win_arm64.whl
Algorithm Hash digest
SHA256 793b915b1612a3f818ad784b6eab42195c4650047896aa764b9c246c913151ba
MD5 eaf361eb005948d8bf5180286ed1d12d
BLAKE2b-256 abed3ceccba37fc5d24da6e324d1f8dd22c299c17bb96732761ea98f12e4194a

See more details on using hashes here.

Provenance

The following attestation bundles were made for aci_rssa-0.0.10-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.10-cp310-abi3-win_amd64.whl.

File metadata

  • Download URL: aci_rssa-0.0.10-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.10-cp310-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 3b83460e31886988507121a0da4d37471e202b2d0497a5bfd95996910ec3e716
MD5 03b1f92c4f393a83941b5e19712751c3
BLAKE2b-256 d0e90bd206297e9b717d80c05764e088e353f13fabc6e6e1ef5ddb6f6e484634

See more details on using hashes here.

Provenance

The following attestation bundles were made for aci_rssa-0.0.10-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.10-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for aci_rssa-0.0.10-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 69d0dc391a73c666580a0ded119f3cf09d988d2dbee5b1c28d79ba863b574c99
MD5 38893f79ff9d07fca51aaa7a689cd02a
BLAKE2b-256 97e70604d623e65ea79c0e90dd0d04c539e4e84bc5dbb02621588722556edb4e

See more details on using hashes here.

Provenance

The following attestation bundles were made for aci_rssa-0.0.10-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.10-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for aci_rssa-0.0.10-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 c51e677832e9d49be3e9d3a68716d14687eaa3ac2c7aeaf001cfe3e7334fd644
MD5 e2eb3d00441c8bbe523e84933d7ca104
BLAKE2b-256 8a9f5a0148fe5431a1da2990d16df96734970f8e03fbdc4974611252cb6e890d

See more details on using hashes here.

Provenance

The following attestation bundles were made for aci_rssa-0.0.10-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.10-cp310-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for aci_rssa-0.0.10-cp310-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 ab607cb2db5e3897707b08c721ade1ca4ee42bba0eca19a8520fe83b8770d853
MD5 40b9fb79f2113298bdf1a0fab94d4c22
BLAKE2b-256 881cb08e8521a341e0c82d3294a29abb45559d0d80a79327bb801fe240f70b40

See more details on using hashes here.

Provenance

The following attestation bundles were made for aci_rssa-0.0.10-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.10-cp310-abi3-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for aci_rssa-0.0.10-cp310-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 83b6369f47c46f7fa8136334c723ceae2b352432e3f4f7a550921f6566cd4017
MD5 0aac58f05c02cee68bcbb27c0338de18
BLAKE2b-256 972e85d6fd21ac82afcac6728a7027445d48ec60b272c60a84343fd691a2df99

See more details on using hashes here.

Provenance

The following attestation bundles were made for aci_rssa-0.0.10-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