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

Uploaded CPython 3.10+Windows ARM64

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

Uploaded CPython 3.10+Windows x86-64

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

Uploaded CPython 3.10+macOS 11.0+ ARM64

aci_rssa-0.0.8-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.8.tar.gz.

File metadata

  • Download URL: aci_rssa-0.0.8.tar.gz
  • Upload date:
  • Size: 94.9 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.8.tar.gz
Algorithm Hash digest
SHA256 4d01e51ed8c047a6a9cc33a6726b5550530542a7c67b35eb9163ea774687ba04
MD5 1aaf88aed8bb3cf861a1b18c76579390
BLAKE2b-256 6b6c60310d663d5fb5339a37145b824baad232ab2b37d24dc0899a89c44a8c78

See more details on using hashes here.

Provenance

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

File metadata

  • Download URL: aci_rssa-0.0.8-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.8-cp310-abi3-win_arm64.whl
Algorithm Hash digest
SHA256 ec5d88c3a051682d5c4df6896e049239582fc6818f78a0a0f80e8ac8af7d30bc
MD5 3079b6d3e600637ec3b7b3b26d080517
BLAKE2b-256 826bb33fb7128fda6e16d288b3771e6661f42d31d7b7ffd617111e6de027fc9a

See more details on using hashes here.

Provenance

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

File metadata

  • Download URL: aci_rssa-0.0.8-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.8-cp310-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 320590eec4ae617b11cd703018a268483f5e24ad73838c009a70d669e240d1b4
MD5 07157113906917579a15b88e56c73c50
BLAKE2b-256 8331b9c6e51e76516a0818df0603ae72afd39084a4b9ef7c92d93524e7cef7b1

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for aci_rssa-0.0.8-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 798599f15789afc8576dc82cf4b270615dc875a20666c842e0899a5340be8291
MD5 aca25ba401e4f8ee953ec9cb87b50a72
BLAKE2b-256 d88b96a9e5586a44b0bff12121e3f31cb2f2b7cb214d9a5d88a0969d6cb1d900

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for aci_rssa-0.0.8-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 58d2b7110ff476edeac1ddcfe851f3fecbb6b65e4cece76a1a8a975c8658d6de
MD5 f74eb09ab6793763a523cabc3202e4c7
BLAKE2b-256 7357bd1f45388060f1e03ad8bdaea4892dbcbbd74e9d05387629c085edb5f003

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for aci_rssa-0.0.8-cp310-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 d076219640ba1bb7ff4918a1a523d976dce44a653934583abff29f6c7454e6b8
MD5 29a3aa416a4c0177833eedf364682e38
BLAKE2b-256 1d5ec40ffd43d9b9e1d9594fb192ef810bdd14ef1f6cc36e63ce12a5d235a263

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for aci_rssa-0.0.8-cp310-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 8f6e4b8f291773d18c7fd8fca957f22c1dbe3fccfb32643eb3e9343fd9ce7aea
MD5 893dcecb504c0b80981737a86818959c
BLAKE2b-256 2c23b7ebee08f75a205b31af641491280c6c43f91d867636101a6d884c9ab087

See more details on using hashes here.

Provenance

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