Language interfaces

libgrav’s core library is written in Rust and exposed to other languages through thin binding layers. All interfaces accept and return values in SI units (kilograms for mass, radians for angles). Unit conversion helpers are provided for the Python interface.

Functions are organised by physics domain. The current domain is:

  • binary — compact binary system parameters (masses, spins)

Each language exposes this as a submodule or namespace so callers can write libgrav.binary.chirp_mass(...), Grav.Binary.chirp_mass(...), grav::binary::chirp_mass(...), etc. All top-level names are also re-exported for backward compatibility.


Command line

Install the grav CLI with Cargo:

cargo install --git https://github.com/transientlunatic/puddin grav-cli
# or, from a local checkout:
cargo install --path crates/grav-cli

Pre-built binaries for Linux, macOS, and Windows are attached to each GitHub Release.

Usage

Masses default to solar masses (M☉); use --si to switch to kilograms. Output is a bare number on stdout — suitable for shell pipelines. Add --verbose (-v) for a labelled result with units.

# Chirp mass of a 30+30 Msun binary
grav chirp-mass 30 30
# → 26.116516898883617

grav chirp-mass 30 30 --verbose
# → chirp_mass = 26.116516898883617 Msun

# Spin parameters
grav chi-eff 30 30 0.5 0.5 0.0 0.0    # both aligned at 0.5 → 0.5
grav chi-p   30 15 0.8 0.3 0.4 1.2

# Masses in kilograms
grav --si chirp-mass 5.97e30 5.97e30

# Compose with standard Unix tools
MC=$(grav chirp-mass 30 30)
echo "Mc = $MC Msun"

Subcommands

Subcommand

Aliases

Arguments

Output

total-mass

m

m1 m2

M (Msun/kg)

mass-ratio

q

m1 m2

q

sym-mass-ratio

eta

m1 m2

η

chirp-mass

mc

m1 m2

ℳ (Msun/kg)

chi-eff

chieff

m1 m2 a1 a2 t1 t2

χ_eff

chi-p

chip

m1 m2 a1 a2 t1 t2

χ_p

t1, t2 are tilt angles in radians. a1, a2 are dimensionless spin magnitudes in [0, 1]. Both spin subcommands require m1 ≥ m2.


Python

Install from PyPI:

pip install libgrav

Optional extras:

pip install "libgrav[pint]"   # pint unit support
pip install "libgrav[jax]"    # JAX / JIT support

numpy arrays (SI)

import numpy as np
import libgrav
import libgrav.binary   # domain-organised submodule

MSUN = 1.988_416e30  # kg

m1 = np.full(1000, 30.0 * MSUN)
m2 = np.full(1000, 30.0 * MSUN)

# Recommended: use the binary submodule
mc  = libgrav.binary.chirp_mass(m1, m2)
eta = libgrav.binary.symmetric_mass_ratio(m1, m2)

# Top-level shortcuts still work (backward compatible)
mc  = libgrav.chirp_mass(m1, m2)

astropy quantities

import astropy.units as u
import libgrav

m1 = 30 * u.Msun
m2 = 30 * u.Msun

mc = libgrav.chirp_mass(m1, m2)   # returns astropy Quantity in kg

pint quantities

import pint
ureg = pint.UnitRegistry()

m1 = 30 * ureg.solar_mass
mc = libgrav.chirp_mass(m1, m1)

JAX

libgrav functions are usable inside jax.jit-compiled code via jax.pure_callback. Analytical custom_vjp rules are registered so that gradients flow through correctly.

import jax
import jax.numpy as jnp
import libgrav

MSUN = 1.988_416e30

@jax.jit
def log_chirp_mass(m1, m2):
    return jnp.log(libgrav.chirp_mass(m1, m2))

m = jnp.array(30.0 * MSUN)
val, grad = jax.value_and_grad(log_chirp_mass)(m, m)

JavaScript / TypeScript

Install from npm:

npm install grav-wasm

The package is built with wasm-pack and ships TypeScript declaration files. It targets modern ES module bundlers (Vite, esbuild, webpack 5, Rollup).

Vectorised API (Float64Array)

All core functions accept and return Float64Array for batch processing. Import from the binary submodule (recommended) or from the top-level module:

// Domain-organised import (recommended)
import * as binary from 'grav-wasm/binary';

const m1 = new Float64Array([30 * binary.MSUN, 10 * binary.MSUN]);
const m2 = new Float64Array([30 * binary.MSUN,  5 * binary.MSUN]);

const mc  = binary.chirp_mass(m1, m2);
const eta = binary.symmetric_mass_ratio(m1, m2);
// Top-level import (backward compatible)
import { chirp_mass, symmetric_mass_ratio, MSUN } from 'grav-wasm/libgrav';

const mc  = chirp_mass(m1, m2);
const eta = symmetric_mass_ratio(m1, m2);

Scalar convenience functions

The TypeScript wrapper layer provides *_scalar() helpers that accept and return plain number, useful for single-event interactive work:

import { chirp_mass_scalar, chi_eff_scalar, MSUN } from 'grav-wasm/libgrav';

const mc = chirp_mass_scalar(30 * MSUN, 30 * MSUN);

const xe = chi_eff_scalar(
  30 * MSUN, 30 * MSUN,  // m1, m2
  0.5, 0.3,              // a1, a2
  0.2, 1.1               // tilt1, tilt2 (radians)
);

Available functions

Function

Description

total_mass / total_mass_scalar

\(M = m_1 + m_2\)

mass_ratio / mass_ratio_scalar

\(q = m_2/m_1\)

symmetric_mass_ratio / symmetric_mass_ratio_scalar

\(\eta = m_1 m_2/M^2\)

chirp_mass / chirp_mass_scalar

\(\mathcal{M} = (m_1 m_2)^{3/5}/M^{1/5}\)

chi_eff / chi_eff_scalar

Effective inspiral spin

chi_p / chi_p_scalar

Effective precession spin (\(m_1 \geq m_2\) required)


Julia

The Julia interface uses ccall to call into a Rust shared library (libgrav.so / .dylib / .dll). The C API is scalar only — Julia’s native broadcasting handles arrays idiomatically without any extra wrapping.

Note on distribution: The recommended approach for a registered Julia package with binary dependencies is a companion JLL package created with BinaryBuilder.jl. The instructions below describe the development workflow from source.

Build the shared library

cargo build --release -p grav-capi
# → target/release/libgrav.{so,dylib,dll}

Add the package

From the Julia REPL, with the monorepo checked out:

import Pkg
Pkg.develop(path="bindings/julia")

Usage

using Grav

const MSUN = Grav.MSUN   # 1.988_416e30 kg

# Recommended: use the Binary submodule
mc = Grav.Binary.chirp_mass(30.0 * MSUN, 30.0 * MSUN)   # ≈ 26.1 M☉ in kg

# Top-level shortcuts also work (backward compatible)
mc = chirp_mass(30.0 * MSUN, 30.0 * MSUN)

# Vectorised via Julia broadcasting — no extra work needed
m1 = rand(1000) .* 50.0 .* MSUN
m2 = rand(1000) .* 50.0 .* MSUN

mc_arr  = Grav.Binary.chirp_mass.(m1, m2)
eta_arr = Grav.Binary.symmetric_mass_ratio.(m1, m2)

xe_arr  = Grav.Binary.chi_eff.(m1, m2,
                                 fill(0.5, 1000), fill(0.3, 1000),
                                 fill(0.2, 1000), fill(1.1, 1000))

Available functions

Function

Description

total_mass(m1, m2)

\(M = m_1 + m_2\)

mass_ratio(m1, m2)

\(q = m_2/m_1\)

symmetric_mass_ratio(m1, m2)

\(\eta = m_1 m_2/M^2\)

chirp_mass(m1, m2)

\(\mathcal{M} = (m_1 m_2)^{3/5}/M^{1/5}\)

chi_eff(m1, m2, a1, a2, tilt1, tilt2)

Effective inspiral spin

chi_p(m1, m2, a1, a2, tilt1, tilt2)

Effective precession spin (\(m_1 \geq m_2\))

All masses in kg, all angles in radians


Rust

Add to Cargo.toml:

[dependencies]
grav = "0.1"
uom    = { version = "0.36", features = ["f64", "si"] }

All public functions use uom quantity types for compile-time dimensional analysis:

use grav::binary::{chirp_mass, chi_eff, MSUN};
use uom::si::f64::Mass;
use uom::si::mass::kilogram;

let m = Mass::new::<kilogram>(30.0 * MSUN);
let mc = chirp_mass(m, m);

// Spin parameters (dimensionless f64)
let xe = chi_eff(m, m, 0.5, 0.3, 0.2, 1.1);

Attempting to pass a length where a mass is expected is a compile error — no runtime checks needed.


C

The bindings/julia crate produces a standard C shared library. Include bindings/julia/include/grav.h to get the function declarations and the GRAV_MSUN constant.

# Build the library
cargo build --release -p grav-capi

# Compile and link your program
gcc example.c \
    -I bindings/julia/include \
    -L target/release -lgrav \
    -Wl,-rpath,$(pwd)/target/release \
    -o example -lm
#include "grav.h"
#include <stdio.h>

int main(void) {
    double m1 = 30.0 * GRAV_MSUN;
    double m2 = 30.0 * GRAV_MSUN;
    printf("Chirp mass: %.4f Msun\n", grav_chirp_mass(m1, m2) / GRAV_MSUN);
    return 0;
}

See examples/c/example.c in the repository for a complete runnable example with a Makefile.


C++

Include grav/binary.hpp to use the grav::binary:: namespace (recommended), or fall back to the flat C API via grav.h.

g++ example.cpp \
    -I bindings/julia/include \
    -L target/release -lgrav \
    -Wl,-rpath,$(pwd)/target/release \
    -o example
#include "grav/binary.hpp"   // provides grav::binary:: namespace
#include <iostream>

int main() {
    namespace binary = grav::binary;
    double m1 = 30.0 * binary::MSUN, m2 = 30.0 * binary::MSUN;
    std::cout << "Chirp mass: " << binary::chirp_mass(m1, m2) / binary::MSUN << " Msun\n";
}

See examples/cpp/example.cpp in the repository for the full example.


Fortran

The grav_binary Fortran module (in bindings/julia/include/grav_binary.f90) groups all binary parameter interfaces under a single module, mirroring the domain-organised structure of the other language interfaces.

gfortran bindings/julia/include/grav_binary.f90 example.f90 \
    -L target/release -lgrav \
    -Wl,-rpath,$(pwd)/target/release \
    -o example
use grav_binary

real(c_double), parameter :: MSUN = grav_binary_MSUN
print *, chirp_mass(30*MSUN, 30*MSUN) / MSUN   ! ~26.1

See examples/fortran/example.f90 in the repository for the full example.


Go

Go’s cgo interface handles the C ABI transparently. The example below wraps the C calls in a binary struct that mirrors the domain-organised structure used in the other language interfaces.

// #cgo CFLAGS:  -I../../bindings/julia/include
// #cgo LDFLAGS: -L../../target/release -lgrav -Wl,-rpath,../../target/release
// #include "grav.h"
import "C"
import "fmt"

var binary = struct {
    MSUN      float64
    ChirpMass func(float64, float64) float64
    // ... other fields
}{
    MSUN: float64(C.GRAV_MSUN),
    ChirpMass: func(m1, m2 float64) float64 {
        return float64(C.grav_chirp_mass(C.double(m1), C.double(m2)))
    },
}

func main() {
    m := 30.0 * binary.MSUN
    fmt.Printf("Chirp mass: %.4f Msun\n", binary.ChirpMass(m, m)/binary.MSUN)
}
cd examples/go && go run example.go

See examples/go/example.go in the repository for the full example. A reusable binary package is also available at bindings/go/binary/.


R

The R package wraps the C ABI via a thin .C()-compatible shim compiled as part of the package. All public functions are vectorised with Vectorize().

Installation

The package is self-contained: it carries a vendored copy of the Rust sources (bindings/r/src/rust) and compiles and statically links them when installed, so all you need is a Rust toolchain (cargo, rustc).

R CMD INSTALL bindings/r            # from a checkout
# or, from a built tarball / r-universe:
R CMD build bindings/r && R CMD INSTALL libgrav_*.tar.gz

To link a prebuilt shared library instead (for example from a release archive), set GRAV_LIB to the directory containing libgrav:

GRAV_LIB=/opt/libgrav/lib R CMD INSTALL bindings/r

After changing the Rust sources, refresh the vendored copy with python3 scripts/vendor_r.py (CI fails if it is out of date).

Usage

library(libgrav)

# Domain-organised access via the binary environment (recommended)
libgrav::binary$chirp_mass(30 * MSUN, 30 * MSUN) / MSUN   # ~26.1

# Top-level functions still work (backward compatible)
chirp_mass(30 * MSUN, 30 * MSUN) / MSUN

# Vectorised — works automatically
m1 <- c(30, 20, 10) * MSUN
m2 <- c(30, 20,  5) * MSUN
libgrav::binary$chirp_mass(m1, m2) / MSUN

# Spin parameters
libgrav::binary$chi_eff(30*MSUN, 30*MSUN, 0.5, 0.5, 0, 0)   # 0.5

Note on CRAN distribution: producing a CRAN package with a compiled dependency requires either a companion r2u / CRAN binary or distributing pre-built binaries via a system dependency. The current package is suitable for direct installation from source.


MATLAB

Use MATLAB’s loadlibrary / calllib to load the shared library directly, then wrap functions in a struct for domain-organised access.

loadlibrary('libgrav', 'bindings/julia/include/grav.h', 'alias', 'grav');

MSUN = 1.988416e30;

% Domain-organised binary struct (recommended)
binary.MSUN      = MSUN;
binary.chirp_mass = @(m1,m2) calllib('grav','grav_chirp_mass',m1,m2);
% ... (see examples/matlab/example.m for all fields)

mc = binary.chirp_mass(30*MSUN, 30*MSUN) / MSUN;
fprintf('Chirp mass: %.4f Msun\n', mc);

unloadlibrary('grav');

For batch processing, arrayfun provides a vectorised wrapper:

mc = arrayfun(@(m) binary.chirp_mass(m*MSUN, m*MSUN)/MSUN, 10:10:50);

See examples/matlab/example.m in the repository for the full example.