Metadata-Version: 2.4
Name: petektools
Version: 0.2.8
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Rust
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Topic :: Scientific/Engineering
Classifier: Operating System :: OS Independent
Requires-Dist: numpy ; extra == 'numpy'
Provides-Extra: numpy
License-File: LICENSE
License-File: NOTICE
Summary: Standalone numerics & geostatistics kernels: reproducible sampling, curated descriptive/weighted statistics, realization-set aggregation, and a geostatistics front-door (experimental variogram, fit, local kriging, sequential Gaussian simulation).
Keywords: geostatistics,gridding,interpolation,numerics,subsurface
Author-email: Kristian dF Kollsgård <kkollsg@gmail.com>
License: Apache-2.0
Requires-Python: >=3.10
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Documentation, https://peteksuite.readthedocs.io/en/latest/libraries/petektools/
Project-URL: Issues, https://github.com/kkollsga/petektools/issues
Project-URL: Repository, https://github.com/kkollsga/petektools

# petekTools

**Scattered-data gridding & geostatistics kernels for Rust** — the numerics
layer the ecosystem is missing.

Rust has excellent crates for linear algebra (`faer`), statistics and
distributions (`statrs`, `rand_distr`), FFT (`rustfft`), and spatial indexing
(`kiddo`, `rstar`). What it has lacked is a production-grade way to turn
**scattered `(x, y, z)` observations into a regular grid** — minimum-curvature
surfaces, inverse-distance weighting, nearest-neighbour fills. petekTools
fills exactly that gap, and curates the rest behind one small front-door.

If you have points and need a surface — a depth grid from well picks, a property
map from samples, any scattered field on a regular lattice — this is the crate.

## Documentation

The canonical docs for the whole petek family live on the **petekSuite site**
— petekTools' pages there:

- **[Library guide](https://peteksuite.readthedocs.io/en/latest/libraries/petektools/)** — the petekTools guide.
- **Tutorials** — [Synthetic data & testing](https://peteksuite.readthedocs.io/en/latest/tutorials/synthetic-testing/) · [Visualization](https://peteksuite.readthedocs.io/en/latest/tutorials/visualization/) (the viewer unit ships in this wheel).
- **[Notebooks](https://peteksuite.readthedocs.io/en/latest/notebooks/)** — executed examples: [geostat tour](https://peteksuite.readthedocs.io/en/latest/notebooks/petektools/01_geostat_tour/) · [synthetic-data tour](https://peteksuite.readthedocs.io/en/latest/notebooks/petektools/02_synthetic_data_tour/).

## Why reach for it

- **Real gridding methods, not toys.** Briggs minimum-curvature (biharmonic SOR),
  inverse-distance weighting, and nearest-neighbour — the workhorses, with their
  defaults and tolerances stated and tested, not hand-waved.
- **Warm-start / incremental re-gridding.** Editing a surface point-by-point?
  Re-solving from scratch each time is wasteful. Seed the solver from the prior
  field and it converges in a fraction of the iterations — **~4–7× faster** on a
  typical structural edit, rising to **~114×** in the near-converged incremental
  limit (measured: 1.55 ms → 13.6 µs, 64 points → 40×40), converging to the same
  field. A stateful [`ConvergentGridder`] makes interactive,
  one-control-at-a-time refinement cheap.
- **Type-agnostic by design.** Kernels speak a plain [`Lattice`] + `[[f64; 3]]`
  rows and return `ndarray::Array2<f64>` — never a domain type. Any regular,
  rotatable areal lattice (the IRAP/RMS model) maps on field-for-field, so
  adoption is a conversion at the call site, not a rewrite.
- **Deterministic and honest.** No RNG, no silent clamping; named, cited
  constants. Analytic cases are asserted as tests — a linear trend is the exact
  minimum-curvature solution, IDW is exact at coincident samples.
- **A pure leaf.** Depends only on general-purpose numeric crates. No I/O, no
  domain model, no heavy framework to adopt — drop it in.
- **Small formula engine.** Parse assignment strings, bind scalar `$params`,
  order intra-block dependencies, and evaluate vectorized expressions over named
  arrays without pulling in any static-model semantics.
- **Shared 1-D interpolation.** `interp1d` covers log-style resampling with
  nearest/step/linear methods and a natural cubic spline implemented in Rust.
- **Binding-friendly.** Owned inputs, no public lifetimes on kernels; PyO3
  bindings are a planned thin layer over this same surface.

## Install

```sh
cargo add petektools
```

## Quick start

```rust
use petektools::{grid, GridMethod, Lattice};

// A 100×80 grid, 25 m spacing, origin at (1000, 2000).
let lattice = Lattice::regular(1000.0, 2000.0, 25.0, 25.0, 100, 80);

// Scattered observations: [x, y, z] rows.
let points = [
    [1010.0, 2008.0, 12.5],
    [1240.0, 2300.0, 18.1],
    [1880.0, 3100.0,  9.4],
];

// Interpolate onto the grid → an (ncol × nrow) Array2<f64>;
// undefined nodes are NaN.
let surface = grid(&points, &lattice, GridMethod::MinimumCurvature).unwrap();
```

### Methods

| `GridMethod`        | What it does                                              |
| ------------------- | -------------------------------------------------------- |
| `Nearest`           | Each node takes its areally-closest sample's `z` (blocky, exact at data). |
| `InverseDistance`   | Global IDW, `p = 2`; exact at coincident samples.        |
| `MinimumCurvature`  | Briggs biharmonic SOR — smooth, honours the samples.     |

### Warm-start an incremental re-grid

```rust
use petektools::{grid, grid_min_curvature_seeded, GridMethod, Lattice};

let lattice = Lattice::regular(0.0, 0.0, 1.0, 1.0, 64, 64);
let points = [[1.0, 1.0, 10.0], [60.0, 60.0, 40.0]];

let cold = grid(&points, &lattice, GridMethod::MinimumCurvature).unwrap();

// Later, after nudging the data: relax from the prior field instead of
// cold-starting. A None / wrong-shape seed falls back to a cold solve.
let warm = grid_min_curvature_seeded(&points, &lattice, Some(&cold)).unwrap();
```

### Interactive refinement with `ConvergentGridder`

```rust
use petektools::{ConvergentGridder, Lattice};

let lattice = Lattice::regular(0.0, 0.0, 1.0, 1.0, 64, 64);
let points = [[1.0, 1.0, 10.0], [60.0, 60.0, 40.0]];

let mut gridder = ConvergentGridder::new(&points, &lattice).unwrap();

// Pin node (32, 40) to 25.0 as a hard constraint and re-solve incrementally.
let field = gridder.add_control(32, 40, 25.0);   // warm — cheap
// … add more controls; each call returns the updated field.
```

## Geometry: `Lattice`

A regular, **rotatable** areal lattice (the IRAP/RMS model): origin, spacing,
node counts, CCW rotation, and an optional y-flip. It carries the forward map
`node_xy(i, j)` and its inverse `xy_to_ij(x, y)`, plus a `bbox()` — everything a
kernel needs to place a sample on the grid.

## Design

- **One job.** Scattered-data gridding / geostatistics — the gap. Everything else
  (linear algebra, stats, neighbour search) is curated from mature crates, never
  reimplemented.
- **Type-agnostic kernels.** `Lattice` + `[[f64; 3]]` in, `ndarray` out. No
  domain types, no I/O.
- **Numerical honesty.** Deterministic, documented to a stated tolerance, with
  analytic tests as the safety net.

See [`SPEC.md`](SPEC.md) for the design constitution and [`API.md`](API.md) for
the locked public contract.

## Status & roadmap

The public contract — `Lattice`, `GridMethod`, `grid`, the warm-start entries —
is **locked and analytically tested**. Also shipped: ordinary kriging behind a
`Gridder` trait, the curated `stats` / `sampling` front-doors over `statrs` /
`rand_distr`, the `units` / `container` / `formula` modules, the PyO3 wheel, the
generic viewer unit, and the wheel-only synthetic asset writer/composer. On the
roadmap: RBF backends if a need appears.

## License

Apache-2.0 — see [LICENSE](LICENSE) and [NOTICE](NOTICE).

