Skip to main content

Binary Reader and Analysis Suite Software

Project description

BRASS (Binary Reader and Analysis Suite Software)

A simple and extensible C++/Python library for reading and analyzing binary particle output files.

Features

  • C++ header-based binary file reader for particle data
  • Plugin-style extensible analysis system (via registry macros)
  • Optional Python bindings via pybind11
  • Histogramming utilities
  • No external dependencies (except optionally pybind11)
  • Devloped primarly for SMASH

Build Instructions

In repository

pip install pythonlib

Performance

image

brass-analyze

Command-line tool for running registered analyses on multiple SMASH run directories.

Usage

brass-analyze [OPTIONS] OUTPUT_DIR ANALYSIS_NAME

  • OUTPUT_DIR — top directory containing run subfolders (out-* by default)
  • ANALYSIS_NAME — name of a registered analysis (see --list-analyses)

Options

--list-analyses List registered analyses and exit.

--pattern PATTERN Glob for run folders (default: out-*).

--keys KEY1 KEY2 ... Dotted keys from config for labeling runs (last segment used as name). Example: --keys Modi.Collider.Sqrtsnn General.Nevents

--results-subdir DIR Subdirectory to store results (default: data).

--strict-quantities Fail if Quantities differ across runs (default: warn and use first).

-v, --verbose Print detailed information.

Writing Analyses

You can create a custom analysis by subclassing Analysis:

class MyAnalysis : public Analysis {
public:
    void analyze_particle_block(const ParticleBlock& block, const Accessor& accessor) override {
        // your logic here
    }
    void finalize() override {}
    void save(const std::string& path) override {}
};

REGISTER_ANALYSIS("my_analysis", MyAnalysis)
#include "analysis.h"
#include "analysisregister.h"
#include "histogram2d.h"
#include <cmath>
#include <vector>
#include <fstream>
#include <unordered_map>
#include <string>

class BulkObservables : public Analysis {
public:
    BulkObservables()
      : y_min(-4.0), y_max(4.0), y_bins(30),
        pt_min(0.0), pt_max(3.0), pt_bins(30),
        d2N_dpT_dy(pt_min, pt_max, pt_bins,
                   y_min, y_max, y_bins),
        n_events(0) {}

    Analysis& operator+=(const Analysis& other) override {
        auto const* o = dynamic_cast<const BulkObservables*>(&other);
        if (!o) throw std::runtime_error("merge mismatch");

        d2N_dpT_dy += o->d2N_dpT_dy;
        n_events   += o->n_events;

        for (auto const& [pdg, src] : o->per_pdg_) {
            auto& dst = obs_for(pdg);
            dst.d2N_dpT_dy += src.d2N_dpT_dy;
        }
        return *this;
    }

    void analyze_particle_block(const ParticleBlock& b, const Accessor& a) override {
        // event selection: needs at least one wounded nucleon
        bool has_wounded = false;
        for (size_t i = 0; i < b.npart; ++i) {
            int pdg = a.get_int("pdg", b, i);
            if ((pdg == 2212 || pdg == 2112) && a.get_int("ncoll", b, i) > 0) {
                has_wounded = true;
                break;
            }
        }
        if (!has_wounded) return;
        ++n_events;

        // fill spectra
        for (size_t i = 0; i < b.npart; ++i) {
            const int pdg   = a.get_int("pdg", b, i);
            const double E  = a.get_double("p0", b, i);
            const double pz = a.get_double("pz", b, i);
            const double px = a.get_double("px", b, i);
            const double py = a.get_double("py", b, i);

            if (E <= std::abs(pz)) continue;
            const double y  = 0.5 * std::log((E + pz) / (E - pz));
            const double pt = std::hypot(px, py);

            d2N_dpT_dy.fill(pt, y);

            auto& obs = obs_for(pdg);
            obs.d2N_dpT_dy.fill(pt, y);
        }
    }

    void finalize() override {
        if (n_events > 0) {
              d2N_dpT_dy.scale(1.0 / n_events);
              for (auto& [_, o] : per_pdg_) {
                 o.d2N_dpT_dy.scale(1.0 / n_events);
             }
         }
    }

    void save(const std::string& dir) override {
        YAML::Emitter out;
        out << YAML::BeginMap;

        out << YAML::Key << "merge_key"     << YAML::Value; to_yaml(out, keys);
        out << YAML::Key << "smash_version" << YAML::Value << YAML::DoubleQuoted << smash_version;
        out << YAML::Key << "n_events"      << YAML::Value << n_events;

        // totals
        out << YAML::Key << "d2N_dpT_dy"    << YAML::Value;
        to_yaml(out, "pt", "y", d2N_dpT_dy);

        // per-PDG spectra
        out << YAML::Key << "spectra" << YAML::Value << YAML::BeginMap;
        for (auto const& [pdg, o] : per_pdg_) {
            out << YAML::Key << std::to_string(pdg) << YAML::Value;
            to_yaml(out, "pt", "y", o.d2N_dpT_dy);
        }
        out << YAML::EndMap;

        out << YAML::EndMap;

        std::ofstream f(dir + "/bulk.yaml");
        f << out.c_str();
    }

private:
    struct Obs {
        Histogram2D d2N_dpT_dy;
        Obs(double pt_min, double pt_max, size_t pt_bins,
            double y_min, double y_max, size_t y_bins)
          : d2N_dpT_dy(pt_min, pt_max, pt_bins,
                       y_min, y_max, y_bins) {}
    };

    Obs& obs_for(int pdg) {
        auto [it, inserted] = per_pdg_.try_emplace(
            pdg, pt_min, pt_max, pt_bins, y_min, y_max, y_bins);
        return it->second;
    }

    double y_min, y_max, pt_min, pt_max;
    size_t y_bins, pt_bins;
    Histogram2D d2N_dpT_dy;
    int n_events;

    std::unordered_map<int, Obs> per_pdg_;
};

REGISTER_ANALYSIS("BulkObservables", BulkObservables);

How Analyses Work

Each analysis plugin in BRASS subclasses the Analysis interface and is responsible for processing particle blocks and storing results.

Merging by Metadata

When you run over multiple binary files, BRASS uses user-supplied metadata (like sqrt_s, projectile, target) to associate results with a merge key. You define metadata like this:

YAML Output

Each analysis writes a human-readable YAML file named after the analysis, e.g., simple.yaml, which contains:

  • The smash_version
  • The merge_keys
  • The data block with all computed quantities

Example output:

merge_key:
  sqrts: 17.3
  system: "PbPb"
smash_version: "SMASH-3.2-38-g5c9a7cbef"
n_events: 40
d2N_dpT_dy:
  pt_range: [0, 3]
  y_range: [-4, 4]
  pt_bins: 30
  y_bins: 30
  counts: ...

Python usage

from brass import BinaryReader, CollectorAccessor
import numpy as np
accessor = CollectorAccessor()
reader = BinaryReader("particles_binary.bin", ["pdg", "pz", "p0"], accessor)
reader.read()

pz = accessor.get_double_array("pz")
e  = accessor.get_double_array("p0")
pdg = accessor.get_int_array("pdg")

y = 0.5 * np.log((e + pz) / (e - pz))

Run analyses through Python

import os
import yaml
import brass as br

# --- point these to a few run directories you want to analyze ---
RUN_DIRS = [
    "runs/out-001",
    "runs/out-002",
]

def load_meta(yaml_path):
    with open(yaml_path, "r") as f:
        cfg = yaml.safe_load(f) or {}

    coll = (cfg.get("Modi", {}) or {}).get("Collider", {}) or {}
    proj = (coll.get("Projectile", {}) or {}).get("Particles", {}) or {}
    targ = (coll.get("Target", {}) or {}).get("Particles", {}) or {}

    Zp, Np = int(proj.get(2212, 0)), int(proj.get(2112, 0))
    Zt, Nt = int(targ.get(2212, 0)), int(targ.get(2112, 0))

    def sym(Z, N):
        # Just a couple of common cases; falls back to A=Z+N
        if   (Z, N) == (82, 126): return "Pb"
        if   (Z, N) == (1, 0):    return "p"
        return f"A{Z+N}"

    system = f"{sym(Zt, Nt)}{sym(Zp, Np)}"
    sqrts  = coll.get("Sqrtsnn", "unknown")
    return f"system={system},sqrts={sqrts}"

def main():
    file_and_meta = []
    used_quantities = None

    for d in RUN_DIRS:
        bin_path  = os.path.join(d, "particles_binary.bin")
        yaml_path = os.path.join(d, "config.yaml")
        if not (os.path.isfile(bin_path) and os.path.isfile(yaml_path)):
            print(f"[skip] Missing files in {d}")
            continue

        # Optional: read quantities once (if present). Otherwise, use [].
        with open(yaml_path, "r") as f:
            cfg = yaml.safe_load(f) or {}
        q = (((cfg.get("Output", {}) or {}).get("Particles", {}) or {})
             .get("Quantities", []) or [])
        q = [str(x) for x in q]

        if used_quantities is None:
            used_quantities = q
        elif used_quantities != q:
            print(f"[warn] Quantities differ in {yaml_path}; using the first set.")

        meta = load_meta(yaml_path)
        file_and_meta.append((bin_path, meta))

    if not file_and_meta:
        raise SystemExit("No valid runs found.")

    br.run_analysis(
        file_and_meta=file_and_meta,     # [(path_to_bin, "meta=..."), ...]
        analysis_name="my_analysis",     # label for outputs
        quantities=used_quantities or [],# [] if you don't care / not in YAML
        save_output=True,
        print_output=False,
        output_folder="results"          # will be created if missing
    )
    print("[done] brass analysis finished -> results/")

if __name__ == "__main__":
    main()

Project details


Download files

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

Source Distribution

pybrass-0.0.0.tar.gz (1.1 MB view details)

Uploaded Source

Built Distribution

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

pybrass-0.0.0-cp312-cp312-macosx_14_0_arm64.whl (6.3 kB view details)

Uploaded CPython 3.12macOS 14.0+ ARM64

File details

Details for the file pybrass-0.0.0.tar.gz.

File metadata

  • Download URL: pybrass-0.0.0.tar.gz
  • Upload date:
  • Size: 1.1 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.7

File hashes

Hashes for pybrass-0.0.0.tar.gz
Algorithm Hash digest
SHA256 3378e3aa81f2b24bb1c632d1e6e28d64fa4db8cc3e7c30a91cbc8dc355efd046
MD5 e2aa1106f3b07103cdc42661ea3cd21e
BLAKE2b-256 b7f887818a0a4e6bc0710c2058544e1d36cf33d78ada323408ba2dc8b654aa06

See more details on using hashes here.

File details

Details for the file pybrass-0.0.0-cp312-cp312-macosx_14_0_arm64.whl.

File metadata

File hashes

Hashes for pybrass-0.0.0-cp312-cp312-macosx_14_0_arm64.whl
Algorithm Hash digest
SHA256 8eba9678020bda463b88f67b31020704daef67ce2cd81d68407f859bf72059d7
MD5 7f1b61387ee58b4f29950eab4e6e7746
BLAKE2b-256 4c704a2f3e1ca68323a4b8736d73ff146ee8f78da911f219460d108a68d2b3a8

See more details on using hashes here.

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