molcrafts-molpack 0.2.0

Faithful Rust port of Packmol molecular packing (split off from molrs)
Documentation
# Quickstart

A Rust packing job has three parts:

1. Build one `Target` per molecule species.
2. Attach at least one spatial restraint to each mobile target, or use a
   packer-level global restraint.
3. Run `Molpack::pack(&targets, max_loops)`.

## One molecule type in a box

```rust
use molpack::{InsideBoxRestraint, Molpack, Target};

let water_positions = [
    [0.0, 0.0, 0.0],
    [0.96, 0.0, 0.0],
    [-0.24, 0.93, 0.0],
];
let water_radii = [1.52, 1.20, 1.20];

let water = Target::from_coords(&water_positions, &water_radii, 100)
    .with_name("water")
    .with_restraint(InsideBoxRestraint::new(
        [0.0, 0.0, 0.0],
        [40.0, 40.0, 40.0],
        [false, false, false],
    ));

let mut packer = Molpack::new()
    .with_tolerance(2.0)
    .with_seed(42);

let frame = packer.pack(&[water], 200)?;
let natoms = frame.get("atoms").and_then(|b| b.nrows()).unwrap_or(0);
println!("packed {natoms} atoms");
```

`pack()` returns a packed `molrs::Frame`. Use `pack_with_report()` if you also
need convergence fields:

```rust
let mut packer = Molpack::new().with_seed(42);
let result = packer.pack_with_report(&targets, 200)?;
println!("converged={} fdist={} frest={}", result.converged, result.fdist, result.frest);
```

## Builder defaults

Every tuning knob except `max_loops` has a Packmol-compatible default. Set a
builder value only when you need to change the default:

```rust
let packer = Molpack::new()
    .with_tolerance(2.0)
    .with_precision(0.01)
    .with_inner_iterations(20)
    .with_seed(42);
```

`max_loops` is positional because the right iteration budget depends on system
size and packing difficulty.

## Targets are snapshots

`Target` is a builder value. `pack()` snapshots the target slice at call time;
mutating or rebuilding a target after that does not affect an already running
pack.