Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
52 changes: 49 additions & 3 deletions SPEC.md

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion docs/how-to/cross-validate.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ from itis_sumo.evaluate.funs_evaluate import (
)

cv = evaluate_sumo_manual_crossvalidation(
run_dir, training_file, ["x1", "x2"], "y1", N_CROSS_VALIDATION=5,
run_dir,
training_file,
["x1", "x2"],
"y1",
N_CROSS_VALIDATION=5,
)
# cv["y1"] is the held-out actual values, cv["y1_hat"] the CV predictions
# (both length-N, NaN at any fold Dakota couldn't complete)
Expand Down
7 changes: 6 additions & 1 deletion docs/how-to/moga.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@ moga_kwargs = {
"seed": 42,
}
pareto = perform_moga_optimization(
run_dir, training_file, ["length", "width"], distributions, ["y1"], moga_kwargs,
run_dir,
training_file,
["length", "width"],
distributions,
["y1"],
moga_kwargs,
)
# pareto["y1"] — objective values on the front
# pareto["length"], pareto["width"] — the input points that produced them
Expand Down
10 changes: 8 additions & 2 deletions docs/how-to/sensitivity-analysis.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,15 @@ distributions = {
"width": {"distribution": "uniform", "min": 0.0, "max": 1.0},
}
result = evaluate_sobol_indices(
run_dir, training_file, ["length", "width"], "y1", distributions, preprocessor, seed=42,
run_dir,
training_file,
["length", "width"],
"y1",
distributions,
preprocessor,
seed=42,
)
sobol = result["sobol"] # {var: {"main", "total", "main_ci_low", ...}}
sobol = result["sobol"] # {var: {"main", "total", "main_ci_low", ...}}
second_order = result["sobolSecondOrder"] # {varA: {varB: float}}

for var, indices in sobol.items():
Expand Down
94 changes: 94 additions & 0 deletions docs/reference/api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Consumer API (`itis_sumo.api`)

`itis_sumo.api` is the whole of what an application embedding itis-sumo is expected to
import — a web service, a notebook, a script. Pass a table of samples and some
configuration; get back a typed result in your own units.

Everything in between belongs to itis-sumo and is not reachable from here: training-file
layout, Dakota configuration, normalization, internal variable renaming, run directories,
and inverse transforms.

```python
from itis_sumo.api import cross_validate

result = cross_validate(samples, variables=["width", "height"], response="stress")
result.predicted # one value per sample, in the units of `stress`
result.warnings # anything that went wrong but did not stop the run
```

The vocabulary is the one defined in the [Glossary](glossary.md): a **sample** is a row,
a **variable** (parameter) is an input column, a **response** (quantity of interest) is
an output column.

## Workflows

### `cross_validate`

Predicts every sample from a surrogate that never saw it. The samples are split into
folds; each fold is predicted by a surrogate trained on the others, so the result is an
honest picture of how the surrogate behaves on data it has not memorised.

Returns a `CrossValidationResult`: the observed values, the predicted values and their
standard deviations aligned positionally with your rows, plus any warnings.

If Dakota abandons a fold — which it occasionally does when the training points are
nearly degenerate — the samples in that fold keep a `NaN` prediction and the reason
appears in `warnings`, rather than the whole run failing.

### `evaluate_along_axes`

Sweeps each variable in turn across its observed range while every other variable is held
still. This is the shape behind a one-dimensional profile plot.

Returns an `AlongAxesResult` containing one `AxisSweep` per variable, keyed by the
variable's own name. Use `at=` to pin particular variables; anything you leave out is held
at its mean across the samples.

## Configuration

Sensible defaults are derived from your samples, so the common path needs no
configuration at all. Advanced users may override behaviour per column with
`PreprocessingSpec`:

```python
from itis_sumo.api import PreprocessingSpec, VariableSpec

spec = PreprocessingSpec(overrides={"width": VariableSpec(scale="linear")})
```

Overrides are expressed in terms of the *domain* — what a column is like — never in terms
of a transform. Which normalization or sign convention a choice implies is an
implementation detail. You can always read back what was actually used, via
`result.effective_config`; you cannot set it in transform terms.

Stochastic workflows take a `seed` that already has a value, so results are reproducible
by default without you having to think about it. The seed that was used comes back in the
result.

## Errors

Every error that escapes `itis_sumo.api` is a `SumoError`. Catch the subclass rather than
matching on message text.

| Error | Meaning | Typical HTTP status |
|---|---|---|
| `SumoInputError` | The samples or configuration are unusable. Fixable by sending something different. | 400 |
| `SumoResultError` | The run finished but produced none of the requested values. Retrying unchanged will not help. | 422 |
| `SumoEngineError` | Dakota itself failed. | 500 |

Dakota reports its failures opaquely. Because of that, working files are discarded when a
run succeeds but **kept when one fails** — and `SumoEngineError` carries the surviving run
directory along with the tail of Dakota's own stderr, so there is something to look at.

```python
try:
result = cross_validate(samples, variables, response)
except SumoInputError as error:
return bad_request(str(error))
except SumoEngineError as error:
logger.error("surrogate build failed; evidence at %s", error.run_dir)
return server_error("Could not build a surrogate from these samples")
```

Pass `workspace=` to keep working files from a successful run too, when you want to
inspect what Dakota was given.
54 changes: 54 additions & 0 deletions docs/reference/glossary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Glossary

itis-sumo uses one word per concept, everywhere: in the public API, in these docs, in
docstrings, in tests. If you see a synonym, it is drift and it is a bug.

## Data shape

| Term | Means | Not |
|---|---|---|
| **sample** | One row of tabular data — a single observation. | "job", "point", "record" |
| **variable**, **parameter** | One *input* column. | "feature", "factor" |
| **response**, **quantity of interest (QoI)** | One *output* column. | "target", "label" |

`variable` and `parameter` are interchangeable, as are `response` and `quantity of
interest`. Inputs and outputs stay distinguished throughout, because they receive
different downstream treatment — normalization, sign handling, sampling roles, and
their position in a Sobol' decomposition all depend on which side a column is on.

itis-sumo takes **plain tabular data**: a `pandas.DataFrame`, or an itis-sumo dataclass
that converts trivially to one. It has no notion of an oSPARC `FunctionJob`. Converting
job records into a table — and filtering out the ones that did not complete — belongs to
the consumer. Deciding that the resulting table has too few samples to build a surrogate
belongs to itis-sumo, which raises so the consumer can surface the problem.

## Two things that are never the same thing

| Term | Means | Can it be inferred from your samples? |
|---|---|---|
| **domain** | Where a variable may be drawn from or explored — its bounds and scale. A property of the *design space*. | **Yes.** Observed bounds plus a detected scale are a defensible default. |
| **distribution** | The real-world shape of a variable. A claim about *the world*. | **No.** A training set sampled uniformly over a box tells you nothing about whether the real input is normal. |

Conflating these is the single easiest way to produce a confidently wrong uncertainty
band. Sobol' analysis and MOGA optimization want a **domain** — a region to explore.
Uncertainty propagation and correlation analysis want a **distribution** — something to
draw from. They are configured separately.

## Modeling

| Term | Means |
|---|---|
| **surrogate**, **SuMo** | The trained metamodel that stands in for an expensive simulation. |
| **surrogate model ID** (`sumo_model_id`) | The server-minted UUID identifying a persisted surrogate in the model store. |

## What you pass, and what stays hidden

You pass **samples** and **configuration**. Everything else is itis-sumo's business:
training-file layout, Dakota configuration, normalization, sign handling, internal
variable renaming, run directories, and mapping predictions back to your original units.

Sensible defaults are generated for you, so the common path requires no configuration at
all. Advanced users may override them — but only in terms of the *domain*, saying things
like `scale="log"` or `direction="maximize"`. The transforms those choices imply are an
implementation detail and never appear in a signature. You can always read back the
configuration that was actually used; you cannot set it in transform terms.
12 changes: 8 additions & 4 deletions docs/tutorials/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ from itis_sumo.evaluate.funs_evaluate import evaluate_sumo

# x_train: 9 points, y_train = sin(x_train), written as a Dakota training file
result = evaluate_sumo(run_dir, train_file, eval_file, ["x"], "y")
y_hat = result["y_hat"] # posterior mean, μ(x)
y_std = result["y_std_hat"] # posterior std, σ(x) — V8df in SPEC.md
y_hat = result["y_hat"] # posterior mean, μ(x)
y_std = result["y_std_hat"] # posterior std, σ(x) — V8df in SPEC.md
```

![GP surrogate mean prediction and 95% uncertainty band on sin(x), fit from 9 training points](../assets/examples/gp_fit_uncertainty.png)
Expand All @@ -53,8 +53,12 @@ argues that training-point placement matters as much as training-point count.
```python
from itis_sumo.sampling.lhs import lhs

lhs_pts = lhs(2, 25, method="maximin", seed=42) # stratified: one draw per 1/25 bin, per axis
rand_pts = rng.uniform(0.0, 1.0, size=(25, 2)) # i.i.d. uniform — no stratification guarantee
lhs_pts = lhs(
2, 25, method="maximin", seed=42
) # stratified: one draw per 1/25 bin, per axis
rand_pts = rng.uniform(
0.0, 1.0, size=(25, 2)
) # i.i.d. uniform — no stratification guarantee
```

![Scatter comparison: 25 plain-random points show visible clustering and gaps; 25 Latin Hypercube points spread evenly across both marginals](../assets/examples/lhs_vs_random.png)
Expand Down
21 changes: 17 additions & 4 deletions docs/tutorials/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ runs as a CI smoke test.
## 1. Install

```sh
uv sync # Python 3.11, resolves itis-dakota==1.5.9
uv sync # Python 3.10-3.12 currently resolve itis-dakota==1.5.9
uv run itis-sumo validate # engine probe (expect: Version 1.5.9)
```

Expand All @@ -30,7 +30,12 @@ import pandas as pd
rng = np.random.default_rng(42)
length = rng.uniform(0.0, 1.0, size=30)
width = rng.uniform(0.0, 1.0, size=30)
stress = 3.0 * length + 2.0 * width**2 + 0.5 * np.sin(10.0 * length) + rng.normal(0, 0.05, 30)
stress = (
3.0 * length
+ 2.0 * width**2
+ 0.5 * np.sin(10.0 * length)
+ rng.normal(0, 0.05, 30)
)
train_raw = pd.DataFrame({"length": length, "width": width, "stress": stress})
```

Expand Down Expand Up @@ -75,7 +80,9 @@ why itis-sumo uses one: [How Gaussian Processes work](../explanation/gaussian-pr
```python
from itis_sumo.evaluate.funs_evaluate import evaluate_sumo_crossvalidation

cv_metrics = evaluate_sumo_crossvalidation(run_dir, training_file, ["x1", "x2"], "y1", N_CROSS_VALIDATION=5)
cv_metrics = evaluate_sumo_crossvalidation(
run_dir, training_file, ["x1", "x2"], "y1", N_CROSS_VALIDATION=5
)
print(cv_metrics["y1"]["root_mean_squared"])
```

Expand All @@ -94,7 +101,13 @@ distributions = {
"width": {"distribution": "uniform", "min": 0.0, "max": 1.0},
}
sobol = evaluate_sobol_indices(
run_dir, training_file, ["length", "width"], "y1", distributions, preprocessor, seed=42
run_dir,
training_file,
["length", "width"],
"y1",
distributions,
preprocessor,
seed=42,
)["sobol"]
for var, indices in sobol.items():
print(var, indices["main"], indices["total"])
Expand Down
17 changes: 11 additions & 6 deletions examples/headless_smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,12 @@ def make_training_data(n: int = N_TRAINING_SAMPLES) -> pd.DataFrame:
rng = np.random.default_rng(SEED)
length = rng.uniform(0.0, 1.0, size=n)
width = rng.uniform(0.0, 1.0, size=n)
stress = 3.0 * length + 2.0 * width**2 + 0.5 * np.sin(10.0 * length) + rng.normal(0, 0.05, n)
stress = (
3.0 * length
+ 2.0 * width**2
+ 0.5 * np.sin(10.0 * length)
+ rng.normal(0, 0.05, n)
)
return pd.DataFrame({"length": length, "width": width, "stress": stress})


Expand All @@ -46,9 +51,7 @@ def main() -> int:

# --- 1. Surrogate evaluation on a grid ---
grid = np.meshgrid(np.linspace(0.0, 1.0, 5), np.linspace(0.0, 1.0, 5))
grid_raw = pd.DataFrame(
{"length": grid[0].ravel(), "width": grid[1].ravel()}
)
grid_raw = pd.DataFrame({"length": grid[0].ravel(), "width": grid[1].ravel()})
grid_processed = preprocessor.transform(grid_raw)
eval_samples_file = run_dir / "eval_samples.dat"
grid_processed.to_csv(eval_samples_file, sep=" ", index=False)
Expand All @@ -62,8 +65,10 @@ def main() -> int:
)
y_hat = np.asarray(preds["y1_hat"])
assert len(y_hat) == len(grid_raw), "grid evaluation count mismatch"
print(f"[1/3] surrogate grid evaluation OK ({len(y_hat)} points, "
f"pred range [{y_hat.min():.3f}, {y_hat.max():.3f}])")
print(
f"[1/3] surrogate grid evaluation OK ({len(y_hat)} points, "
f"pred range [{y_hat.min():.3f}, {y_hat.max():.3f}])"
)

# --- 2. Cross-validation quality metrics ---
cv_metrics = evaluate_sumo_crossvalidation(
Expand Down
2 changes: 2 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ nav:
- Preprocess data: how-to/preprocess.md
- Reference:
- Overview: reference/index.md
- Glossary: reference/glossary.md
- Consumer API: reference/api.md
- Dakota engine core: reference/core.md
- Config (NIDR composers): reference/config.md
- Sampling: reference/sampling.md
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ dependencies = [
"pydantic>=2.0,<3.0",
"scikit-learn>=1.6,<2.0",
"scipy>=1.15,<2.0",
"typing-extensions>=4.0; python_version < '3.11'",
]

[project.urls]
Expand Down
Loading
Loading