Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
58a7c3e
docs(spec): fix review findings in 001-low-memory-interpolator
lachlangrose Oct 27, 2026
d13a244
docs(spec): add 001-low-memory-interpolator spec, plan and tasks
lachlangrose Oct 27, 2026
a9e89ea
chore(loop-interpolation-core): scaffold core package, fixture and na…
lachlangrose Oct 27, 2026
2ba2cf9
feat(loop-interpolation-core): port loop-rs interpolation-fd and add …
lachlangrose Oct 27, 2026
d031e28
feat(loop-interpolation-core): remove grid rotation and serde; skip r…
lachlangrose Oct 27, 2026
bc86724
feat(loop-interpolation-core): make rayon optional with a one-thread …
lachlangrose Oct 27, 2026
cb8af11
feat(loop-interpolation-core): add counting allocator, baseline memor…
lachlangrose Oct 27, 2026
ab21fcb
feat(loop-interpolation-core): cut shell tables to u32; add failing i…
lachlangrose Oct 27, 2026
d3beebc
feat(loop-interpolation-core): validate flat input and add MemTracker
lachlangrose Oct 27, 2026
c54ef16
test(loop-interpolation-core): add failing memory estimate and CG ses…
lachlangrose Oct 27, 2026
0e251c0
feat(loop-interpolation-core): add memory estimate, budget check and …
lachlangrose Oct 27, 2026
73e8f56
feat(loop-interpolation-core): add CG state machine and solve session
lachlangrose Oct 27, 2026
2c9cd0c
test(loop-interpolation-core): add failing ADMM and constraint family…
lachlangrose Oct 27, 2026
db5bcaa
fix(loop-interpolation-core): match the matrix solver rows for curvat…
lachlangrose Oct 27, 2026
b118764
feat(loop-interpolation-core): add ADMM state machine with feasibilit…
lachlangrose Oct 27, 2026
74ee73e
feat(loop-interpolation-core): add Field evaluation and cut ADMM memory
lachlangrose Oct 27, 2026
568a02e
docs(spec): record build decisions in the plan answers
lachlangrose Oct 27, 2026
e720720
docs(spec): correct the column scaling answer in the plan
lachlangrose Oct 27, 2026
1ef045d
feat(loop-interpolation-core): add PyO3 bindings; add failing single …
lachlangrose Oct 27, 2026
8dc8d11
feat(loop-interpolation): add native solver adapter; make stencils ge…
lachlangrose Oct 27, 2026
dc21063
feat(loop-interpolation): route solver native through solve_system; m…
lachlangrose Oct 27, 2026
7193f24
feat(loop-interpolation-core): make data and inequality operators gen…
lachlangrose Oct 27, 2026
b9382a6
test(loop-interpolation): run native tests against the built wheel
lachlangrose Oct 27, 2026
9f6f12b
feat(loop-interpolation-core): add single precision sessions; add nat…
lachlangrose Oct 27, 2026
37f7548
test(loop-interpolation-core): add reference problem tests and fit th…
lachlangrose Oct 27, 2026
4e442a9
style(loop-interpolation-core): fix clippy findings in the core and o…
lachlangrose Oct 27, 2026
db774ca
feat(loop-interpolation-core): build the Python wheel with the parall…
lachlangrose Oct 28, 2026
c595b97
docs(spec): record the OP1 confirmation in the plan answers
lachlangrose Oct 28, 2026
ad67100
test(loop-interpolation-core): add the Playwright fixture test for th…
lachlangrose Oct 28, 2026
ec55631
chore: ignore Playwright output of the core package
lachlangrose Oct 28, 2026
90ab269
test(loop-interpolation-core): add the browser responsiveness, cancel…
lachlangrose Oct 28, 2026
6a0a81f
feat(loop-interpolation-core): add the wasm-bindgen classes for the b…
lachlangrose Oct 28, 2026
3d86111
style(loop-interpolation-core): format wasm.rs
lachlangrose Oct 28, 2026
a453fc1
feat(loop-interpolation-core): add the browser Worker and the wasm bu…
lachlangrose Oct 28, 2026
21e2e33
docs(loop-interpolation-core): add TypeScript declarations and the we…
lachlangrose Oct 28, 2026
f031406
docs(loop-interpolation-core): complete the package README and record…
lachlangrose Oct 28, 2026
eefa41e
ci: add the workflow for the native solver (Rust, wheel, WebAssembly,…
lachlangrose Oct 28, 2026
02cf99b
test(loop-interpolation-core): add the browser reference run of R1
lachlangrose Oct 28, 2026
a8f7647
chore(loop-interpolation-core): mark the final checks of spec 001 done
lachlangrose Oct 28, 2026
274ece1
chore(loop-interpolation-core): align the version to 0.1.0
lachlangrose Oct 28, 2026
e37f101
fix(loop-interpolation-core): make a failed finalize final in SolveSe…
lachlangrose Oct 28, 2026
2783dc5
fix(loop-interpolation): make the native solver path safe for unsuppo…
lachlangrose Oct 28, 2026
d0572f2
fix(loop-interpolation): filter bad rows and cap pair bounds in the n…
lachlangrose Oct 28, 2026
dd2ada0
feat(loop-interpolation): add per-solve ridge factor and warm start t…
lachlangrose Oct 28, 2026
9772043
feat(loop-interpolation-core): add ridge factor and start value to Pr…
lachlangrose Oct 28, 2026
3d36054
feat(loop-interpolation): send ridge factor and start value to the na…
lachlangrose Oct 28, 2026
e31c112
style(loop-interpolation-core): fix clippy 1.99 findings
lachlangrose Oct 28, 2026
a18685e
fix: give loop-interpolation-core a uv source so the workspace locks
lachlangrose Oct 28, 2026
862b279
test(loop-interpolation): allow 1e-5 value difference in the golden f…
lachlangrose Oct 28, 2026
1754e30
ci: skip the style auto-commit for pull requests from forks
lachlangrose Oct 28, 2026
de9d902
test(loop-interpolation): compare repeated native solves with a toler…
lachlangrose Oct 28, 2026
bac125a
perf(loop-interpolation-core): apply the regularisation with per-clas…
lachlangrose Oct 28, 2026
35e072b
test(loop-interpolation): use the SC-003 limit for repeated native AD…
lachlangrose Oct 28, 2026
8b02ff4
ci: build loop-interpolation-core wheels and publish them to PyPI
lachlangrose Oct 28, 2026
81f640d
feat: pass the build solver to the interpolator before setup; add a n…
lachlangrose Oct 28, 2026
468e519
Merge branch 'master' into feat/native-claudius-script
lachlangrose Oct 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,10 @@ def build(self, data_region=None, **kwargs):

self.interpolator.support.maximum = newmax
self.interpolator.set_region(region=region)
# The interpolator must know the solver before setup: with
# solver="native" the setup builds no sparse rows in Python.
if kwargs.get('solver') is not None:
self.interpolator.solver = kwargs['solver']
logger.info(f'Setting up interpolator for {self.name}')
self.interpolator.setup_interpolator(**kwargs)
logger.info(f'installing gradient constraints {self.name}')
Expand Down
168 changes: 168 additions & 0 deletions scripts/native_claudius.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
"""Build the claudius model with the native (Rust) solver.

The script builds the stratigraphy of the claudius demo data set with the
finite difference interpolator and ``solver="native"``. It can also build
the same model with the Python ``cg`` solver and compare the two fields.

Needs the native core::

pip install loop-interpolation[native]

or, in a checkout::

maturin develop --release -m packages/loop_interpolation_core/Cargo.toml

Examples::

python scripts/native_claudius.py
python scripts/native_claudius.py --nelements 1000000 --solver native
python scripts/native_claudius.py --plot
python scripts/native_claudius.py --solver native --screenshot claudius.png
"""

from __future__ import annotations

import argparse
import logging
import time

import numpy as np
import pandas as pd

# Import the adapter now, so that its logger exists when main sets the level.
from loop_interpolation import _native # noqa: F401

import LoopStructural
from LoopStructural import GeologicalModel
from LoopStructural.datasets import load_claudius
from LoopStructural.modelling.features import GeologicalFeature

# Scalar field values of the horizons in the claudius data.
UNIT_VALUES = [0, 60, 250, 330, 600]


def build_model(
data: pd.DataFrame, bb: np.ndarray, solver: str, nelements: int, tol: float, maxiter: int
) -> tuple[GeologicalModel, GeologicalFeature, float]:
"""Build the claudius stratigraphy with one solver.

Return the model, the feature and the build time in seconds.
"""
model = GeologicalModel(bb[0, :], bb[1, :])
model.data = data
for i in range(len(UNIT_VALUES) - 1):
model.stratigraphic_column.add_unit(
f"unit_{i}", thickness=UNIT_VALUES[i + 1] - UNIT_VALUES[i], id=i
)
model.stratigraphic_column.group_mapping["Group_0"] = "strati"

if solver == "native":
# tol is the relative residual of the core. maxiter is the limit of
# the CG iterations (the core default is 5000).
solver_kwargs = {"tol": tol, "maxiter": maxiter}
else:
solver_kwargs = {"rtol": tol, "maxiter": maxiter}
feature = model.create_and_add_foliation(
"strati",
interpolatortype="FDI",
nelements=nelements,
buffer=0.3,
solver=solver,
solver_kwargs=solver_kwargs,
)

started = time.perf_counter()
model.update(progressbar=False)
seconds = time.perf_counter() - started
return model, feature, seconds


def report(name: str, feature: GeologicalFeature, seconds: float) -> None:
"""Print the grid, the time and the solver result of one build."""
interpolator = feature.builder.interpolator
nsteps = interpolator.support.nsteps
print(f"\n{name}")
print(f" grid {nsteps[0]} x {nsteps[1]} x {nsteps[2]} = {interpolator.dof} nodes")
print(f" build time {seconds:.2f} s")
native = getattr(interpolator, "latest_native_report", None)
if name == "native" and native is not None:
print(f" converged {native['converged']}")
print(f" iterations {native['iterations']}")
print(f" residual {native['residual']:.2e}")
print(
f" core peak {native['peak_bytes'] / 1e6:.1f} MB "
f"({native['peak_bytes'] / interpolator.dof:.1f} bytes per node)"
)
for warning in native.get("warnings", []):
print(f" warning {warning}")
else:
timing = interpolator.get_last_solve_timing()
print(f" converged {timing.get('up_to_date')}")
print(f" solve time {timing.get('solve_seconds', float('nan')):.2f} s")


def main() -> None:
"""Parse the arguments, build the models and print the results."""
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("--nelements", type=int, default=200_000, help="grid cells (default 200000)")
parser.add_argument(
"--solver",
choices=["native", "cg", "both"],
default="both",
help="solver to use; 'both' also compares the fields (default both)",
)
parser.add_argument("--tol", type=float, default=1e-8, help="relative residual (default 1e-8)")
parser.add_argument("--maxiter", type=int, default=20_000, help="CG iteration limit (default 20000)")
parser.add_argument("--plot", action="store_true", help="show the native model surfaces")
parser.add_argument(
"--screenshot", metavar="PATH", help="save the native model surfaces to a PNG file"
)
parser.add_argument("--verbose", action="store_true", help="show the INFO log messages")
args = parser.parse_args()
LoopStructural.setLogging("info" if args.verbose else "warning")
# loop_interpolation sets a level on each of its loggers, so set them all.
level = logging.INFO if args.verbose else logging.WARNING
for name in list(logging.root.manager.loggerDict):
if name.startswith("loop_interpolation"):
logging.getLogger(name).setLevel(level)

data, bb = load_claudius()
solvers = ["native", "cg"] if args.solver == "both" else [args.solver]

models = {}
for solver in solvers:
model, feature, seconds = build_model(
data, bb, solver, args.nelements, args.tol, args.maxiter
)
report(solver, feature, seconds)
models[solver] = model

if len(models) == 2:
# Compare the two scalar fields on a regular grid in the model box.
xyz = models["native"].regular_grid(nsteps=(40, 40, 40), shuffle=False)
native = models["native"].evaluate_feature_value("strati", xyz)
cg = models["cg"].evaluate_feature_value("strati", xyz)
field_range = float(np.nanmax(cg) - np.nanmin(cg))
difference = float(np.nanmax(np.abs(native - cg)))
print("\nnative vs cg")
print(f" largest difference {difference:.3e}")
print(f" field range {field_range:.3e}")
print(f" relative {difference / field_range:.2e}")

if args.plot or args.screenshot:
from LoopStructural.visualisation import Loop3DView

model = models.get("native", next(iter(models.values())))
# Without --plot the view is drawn off screen, for the screenshot only.
viewer = Loop3DView(model, off_screen=not args.plot)
viewer.plot_model_surfaces(cmap="tab20")
viewer.add_mesh(model.bounding_box.vtk().outline(), color="black", name="outline")
if args.screenshot:
viewer.screenshot(args.screenshot, window_size=(1400, 1000))
print(f"\nsaved {args.screenshot}")
if args.plot:
viewer.display()


if __name__ == "__main__":
main()
37 changes: 37 additions & 0 deletions tests/unit/modelling/test_builder_solver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""The ``solver`` build argument must reach the interpolator before setup."""

import logging

import pytest

from LoopStructural import GeologicalModel
from LoopStructural.datasets import load_claudius


def _claudius_model(**build_kwargs):
data, bb = load_claudius()
model = GeologicalModel(bb[0, :], bb[1, :])
model.data = data
feature = model.create_and_add_foliation(
"strati", interpolatortype="FDI", nelements=2000, **build_kwargs
)
return model, feature


def test_build_sets_the_interpolator_solver():
model, feature = _claudius_model(solver="lsmr")
model.update(progressbar=False)
assert feature.builder.interpolator.solver == "lsmr"


def test_native_solver_set_in_build_arguments_builds_no_rows(caplog):
pytest.importorskip("loop_interpolation_core")
model, feature = _claudius_model(solver="native")
with caplog.at_level(logging.WARNING):
model.update(progressbar=False)
interpolator = feature.builder.interpolator
assert interpolator.solver == "native"
assert interpolator.latest_native_report["converged"]
# A native setup stores the data but builds no sparse rows.
assert len(interpolator.constraints) == 0
assert "saves memory only when" not in caplog.text
Loading