xraydb 0.2.0

X-ray reference data for the elements: absorption edges, emission lines, and Elam/Chantler cross-sections
Documentation
<img src="https://raw.githubusercontent.com/Ameyanagi/xraydb-rs/main/assets/icon-512.png" alt="" width="88" align="left" hspace="12">

# xraydb-rs

X-ray reference data for the elements in Rust. A pure-Rust port of the [XrayDB](https://github.com/xraypy/XrayDB) project.

The complete database — elements, absorption edges, emission lines, Elam and Chantler cross-sections, Waasmaier–Kirfel form factors — is compiled into your binary as a 3 MB zstd blob and decoded lazily on first use. No runtime data files, no network access.

## Install

```toml
[dependencies]
xraydb = "0.2"
```

## Usage

```rust
use xraydb::{CrossSectionKind, XrayDb};

let db = XrayDb::try_new()?;

// Element facts — symbol, name, or Z all work as identifiers
assert_eq!(db.atomic_number("Fe")?, 26);
assert_eq!(db.symbol("iron")?, "Fe");
assert_eq!(db.atomic_name("26")?, "iron");

// Absorption edges
assert_eq!(db.xray_edge("Fe", "K")?.energy, 7112.0);

// Mass attenuation from the Elam tables (cm²/g)
let mu = db.mu_elam_at("Fe", 10_000.0, CrossSectionKind::Total)?;

// Compound attenuation (1/cm), by formula and density
let mu_water = db.material_mu_at("H2O", 1.0, 10_000.0, CrossSectionKind::Total)?;
assert!((mu_water - 5.33).abs() < 0.05);

// Anomalous scattering factors from the Chantler tables
let f1 = db.f1_chantler_at("Fe", 10_000.0)?;
let f2 = db.f2_chantler_at("Fe", 10_000.0)?;

// Refractive index decrements: n = 1 - delta - i*beta
let n = db.xray_delta_beta("SiO2", 2.2, 10_000.0)?;
println!("delta={:.3e} beta={:.3e} atlen={:.3} cm", n.delta, n.beta, n.attenuation_length_cm);
# Ok::<(), xraydb::XrayDbError>(())
```

### Scalar and batch forms

Every array-valued calculation has an `_at` scalar counterpart that allocates nothing. Use the batch form when you have many energies — it resolves the element and its table rows once for the whole slice.

```rust
# use xraydb::{CrossSectionKind, XrayDb};
# let db = XrayDb::try_new()?;
let energies: Vec<f64> = (0..500).map(|i| 5_000.0 + 20.0 * i as f64).collect();
let mu = db.mu_elam("Fe", &energies, CrossSectionKind::Total)?;
assert_eq!(mu.len(), energies.len());
# Ok::<(), xraydb::XrayDbError>(())
```

### Chemical formulas

One parser serves the whole crate, so anything `material_mu` accepts, `xray_delta_beta` accepts too.

| Notation | Example |
|---|---|
| plain | `H2O` |
| nested groups | `Mn(SO4)2(H2O)7` |
| fractional stoichiometry | `Fe0.7Mg0.3O`, `Fe.7Mg.3O` |
| scientific notation | `Zn1.e-5Fe3O4` |
| fractional group multipliers | `(N2)0.7808(O2)0.2095` |
| weight-percent mixtures | `Ru1wt%SiO2` |
| deuterium alias | `D2O` (counted as hydrogen) |

### Named materials

```rust
# use xraydb::{CrossSectionKind, XrayDb};
# let db = XrayDb::try_new()?;
let kapton = db.find_material("kapton").expect("in the materials table");
assert_eq!(kapton.formula, "C22H10N2O5");
assert_eq!(kapton.density, 1.42);

// Density is taken from the table unless you override it
let mu = db.material_mu_named("kapton", &[10_000.0], CrossSectionKind::Total, None)?;
# Ok::<(), xraydb::XrayDbError>(())
```

### Optics (feature `optics`)

Crystal Darwin widths and mirror/multilayer reflectivity. Parameter structs with `Default` keep call sites readable.

```toml
[dependencies]
xraydb = { version = "0.2", features = ["optics"] }
```

```rust
# #[cfg(feature = "optics")] {
use xraydb::{DarwinParams, MirrorParams, XrayDb};

let db = XrayDb::try_new()?;

let dw = db.darwin_width(DarwinParams {
    energy: 10_000.0,
    crystal: "Si",
    hkl: (1, 1, 1),
    ..Default::default()
})?.expect("Si(111) diffracts at 10 keV");
println!("Darwin width: {:.2} eV FWHM", dw.energy_fwhm);

let angles: Vec<f64> = (1..100).map(|i| i as f64 * 0.1e-3).collect();
let refl = db.mirror_reflectivity(MirrorParams {
    formula: "Pt",
    theta: &angles,
    energy: 10_000.0,
    density: 21.45,
    roughness: 5.0,
    ..Default::default()
})?;
# }
# Ok::<(), xraydb::XrayDbError>(())
```

### Energy clamping

Following upstream XrayDB, energies outside a table's range are **clamped** to its endpoints rather than rejected. Check first with `XrayDb::elam_energy_range()` and `db.chantler_energy_range(element)`.

## Command-line tool

```sh
cargo install --path xraydb-cli    # installs `xraydb`

xraydb element Fe
xraydb edges Fe
xraydb lines Cu --level K
xraydb mu H2O --density 1.0 --energy 5000:15000/11
xraydb delta-beta SiO2 --density 2.2 --energy 10000
xraydb guess-edge --energy 7100
xraydb materials
```

Energies accept a single value, a comma list, `start:stop:step`, or `start:stop/count` (log-spaced). Add `--json` or `--csv` for machine-readable output.

### For scripts and agents

The CLI is designed to be driven programmatically as well as typed:

```sh
xraydb commands --json      # every subcommand, argument, enum value, and default
```

- `--json` works on **every** subcommand, including `commands` itself, so an agent can
  discover the whole interface in one call instead of parsing `--help` prose.
- With `--json`, **failures are also JSON**, written to stderr as
  `{"error": "...", "context": [...]}`. Without it, errors stay plain text.
- On failure stdout is left empty, so a consumer parsing stdout never sees partial output.
- Exit code is `0` on success and `1` on error. A broken pipe (`| head`) exits `0`.
- Colour is only emitted to a TTY, and `NO_COLOR` disables it.

## Browser demo

A zero-dependency demo page with a periodic-table selector and live cross-section plots:

```sh
wasm-pack build --target web --release xraydb-wasm --out-dir ../web/pkg
python3 -m http.server -d web 8080
```

Then open <http://localhost:8080>. See [web/README.md](web/README.md).

## Workspace Structure

| Crate | Description |
|-------|-------------|
| `xraydb-data` | Shared serde data model (`#![no_std]`) |
| `xraydb-generate` | Binary that parses raw data sources into compressed binary format |
| `xraydb-lib` | Main library crate (`xraydb`) with embedded compressed data |
| `xraydb-wasm` | WASM bindings via `wasm-bindgen` |
| `xraydb-cli` | `xraydb` command-line tool |
| `web/` | Browser demo built on `xraydb-wasm` |

## Performance

Typical timings on an M-series Mac, release build (see `cargo bench`):

| Operation | Time |
|---|---|
| `f2_chantler_at` | ~120 ns |
| `mu_elam_at` | ~113 ns |
| `xray_edge` | ~78 ns |
| `xray_delta_beta` | ~1.1 µs |
| `mu_elam` batch of 200 | ~7 µs |

## Development

```sh
cargo test --all-features
cargo clippy --all-targets --all-features -- -D warnings
cargo fmt --all --check
```

Install the pre-commit hook (runs fmt, clippy, and tests):

```sh
git config core.hooksPath .githooks
```

The library contains no `unsafe` (`#![forbid(unsafe_code)]`) and no `unwrap`/`expect`
outside test code (lint-enforced).

## Regenerating the data

```sh
git clone https://github.com/xraypy/XrayDB.git XrayDB
cargo run -p xraydb-generate --release
```

## Attribution

The X-ray data used in this project comes from the [XrayDB](https://github.com/xraypy/XrayDB) project by Matt Newville et al., which is placed in the public domain (CC0 1.0). This Rust port is an independent reimplementation.

Data sources include:

- **Elam/Ravel/Sieber tables** — photoabsorption, scattering, and emission line data
- **Chantler tables** — anomalous scattering factors (f', f'') and mass attenuation coefficients
- **Waasmaier-Kirfel coefficients** — elastic (Thomson) scattering factors f0

## License

Dual-licensed under MIT and Apache-2.0. See [LICENSE-MIT](LICENSE-MIT) and [LICENSE-APACHE](LICENSE-APACHE).

The underlying X-ray data is in the public domain (CC0 1.0) from the [XrayDB](https://github.com/xraypy/XrayDB) project.