Metadata-Version: 2.4
Name: doe_analyzer
Version: 0.8.2
Summary: DOE Analyzer — desktop application for analysing design-of-experiments results from CSV.
Author-email: Niklas Nordin <niklas.nordin@nequam.se>, Akshay Kumar <akshay.kumar@scania.com>, "Claude (Anthropic)" <noreply@anthropic.com>
License-Expression: MIT AND Apache-2.0
Project-URL: Repository, https://gitlab.com/niklas.nordin.swe/doe_analyzer
Project-URL: Issues, https://gitlab.com/niklas.nordin.swe/doe_analyzer/-/issues
Classifier: Programming Language :: Python :: 3
Classifier: Operating System :: POSIX :: Linux
Classifier: Operating System :: Microsoft :: Windows
Classifier: Environment :: X11 Applications :: Qt
Classifier: Topic :: Scientific/Engineering
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: doe_analyzer/data/icons/LICENSE
Requires-Dist: PySide6<7,>=6.8; python_version >= "3.12"
Requires-Dist: PySide6<6.12,>=6.8; python_version < "3.12"
Requires-Dist: plotly<8,>=6.1
Requires-Dist: numpy<3,>=2.0
Requires-Dist: pandas<4,>=2.2
Requires-Dist: scipy<2,>=1.13
Requires-Dist: scikit-learn<2,>=1.5
Requires-Dist: SALib<2,>=1.5
Requires-Dist: pymoo<0.7,>=0.6.1
Requires-Dist: statsmodels<1,>=0.14
Requires-Dist: materialyoucolor<4,>=3.0
Requires-Dist: openpyxl<4,>=3.1
Provides-Extra: umap
Requires-Dist: umap-learn<1,>=0.5; extra == "umap"
Provides-Extra: catia
Requires-Dist: pywin32>=306; platform_system == "Windows" and extra == "catia"
Provides-Extra: dev
Requires-Dist: pytest<10,>=8; extra == "dev"
Requires-Dist: pytest-timeout<3,>=2; extra == "dev"
Requires-Dist: ruff<1,>=0.4; extra == "dev"
Dynamic: license-file

# DOE Analyzer

Desktop application for analysing design-of-experiments results stored in CSV or an Excel workbook:
one row per run, an ID column, the DOE parameters and the result functions,
with the names in the first line.

![DOE Analyzer: the app on the Pareto page, and plots of Surrogate models, What-if, Sensitivity, the self-organizing map, Classification and Clustering](https://gitlab.com/niklas.nordin.swe/doe_analyzer/-/raw/master/docs/collage.png)

## Install and run

Runs on Linux and Windows with Python 3.9 or newer. From PyPI:

```bash
pip install doe_analyzer                   # or 'doe_analyzer[umap]' for UMAP embeddings (numba)
doe_analyzer                               # or: doe_analyzer file.csv | project.doeproj | --demo branin
doe_analyzer --gl software                 # WebGL in software, where the GPU driver draws blank plots
```

On Windows `doe_analyzer-gui` starts it without a console window. A desktop Linux has the system libraries Qt needs;
a minimal one (a container, a server image) needs those the CI installs ([.gitlab-ci.yml](https://gitlab.com/niklas.nordin.swe/doe_analyzer/-/blob/master/.gitlab-ci.yml)).
A cluster login node often lacks `libxcb-cursor.so.0` (Qt 6.5+ needs it for X11): `doe_analyzer` then names the
missing libraries and their packages. Without root, copying the library files into the environment's
`site-packages/PySide6/Qt/lib` is enough, since Qt looks there first. Over `ssh -X` to an X server without OpenGL
(GLX) it starts without OpenGL; plots that need WebGL (parallel coordinates, 3D) then need a remote desktop with
OpenGL.

From a clone of the repository:

```bash
python3 -m venv .venv
.venv/bin/pip install -e '.[dev,umap]'     # umap is optional (UMAP embeddings)
./doe_analyzer.sh                          # or: .venv/bin/doe_analyzer [file.csv | project.doeproj]
./doe_analyzer.sh --demo zdt_constrained   # synthetic datasets: branin, ishigami, zdt_constrained,
                                           # mixed_categorical, large_20k
./doe_analyzer.sh --diagnose               # print the WebGL renderer in use
```

On Windows: `py -3 -m venv .venv`, `.venv\Scripts\pip install -e .[dev]`, then
`.venv\Scripts\doe_analyzer-gui` (no console window).

A Windows program that needs no Python, `DOE-Analyzer.exe` in a folder shipped as a zip, is built with
`packaging\build_windows.bat` (PyInstaller; see [DEVELOPMENT.md](https://gitlab.com/niklas.nordin.swe/doe_analyzer/-/blob/master/DEVELOPMENT.md#windows-build)).
It has everything but UMAP, and runs study scripts too: `DOE-Analyzer.exe -u study.py`.

### CATIA V5

On Windows, install with the `catia` extra (`pip install "doe_analyzer[catia]"`, or `.[catia]` from a clone; the
Windows program has it), start CATIA V5, open a CATPart, then choose
**More → CATIA V5 design…**. The dialog finds its editable numeric parameters and is the New design dialog for
them: each parameter continuous (a range on a Linear, Log10, Symlog or Power scale, optionally a step) or discrete (its levels, or their
number between a low and a high value), a rule per
parameter, any of the designs with constraints between the parameters, and *Check quality…* to measure the design and
*Improve* it before the parts are made. Its Outputs tab records CATIA's computed parameters (a mass) after each run,
with limits that flag the runs breaking them. It asks for an empty output folder, writes
`doe.csv`, `doe.doeproj` and `catia.json` there, and saves each configured part as
`doe.001/<part>.CATPart`, `doe.002/<part>.CATPart`, and so on.
The backend contract for development without CATIA is documented in [docs/CATIA_INTERFACE.md](https://gitlab.com/niklas.nordin.swe/doe_analyzer/-/blob/master/docs/CATIA_INTERFACE.md).
The CATIA V5 interface was written by Akshay Kumar.

## In the browser

From a clone of the repository, `python scripts/build_web.py` writes `doe_analyzer.html`: the application as one file of about 70 MB that opens in
a current Chrome, Edge or Firefox with no server and no network. It is the same application, not a report of one:
open a CSV or a project, set the columns, and use every page (the Python analysis code runs in the page, in
WebAssembly). Nothing leaves the browser.

* Files are opened with the folder button or dropped on the page; what the application writes (projects, plots as
  PNG, SVG or a zip, rows, reports) is downloaded. A project is the `.doeproj` file next to its CSV: give the page
  both files to restore it.
* *Reload* (F5) reads the CSV again in Chrome and Edge; other browsers cannot read a file twice and ask for it.
* Files the CSV names per run (images, curves) are given to the page with More → *Add run files…*.
* The first start takes about ten seconds (the page unpacks Python and its libraries); the pages show at once.
  Long computations (surrogates, optimisation) take about as long as in the desktop application, and *Cancel* stops
  one by starting the engine again. Unlike the desktop application, pages compute one at a time (a page opened
  while another computes waits for it, and says so), an option changed while a page computes takes effect when that
  computation ends, Export all plots and the report have *Stop*, which stops after the current plot, and Export all
  plots writes PNG or SVG (a PDF is made one plot at a time).
* Not in the browser: UMAP (PCA, t-SNE and MDS are; a project saved with UMAP opens with PCA), the PDF report
  (the HTML report is), Excel workbooks, data added from another CSV, New design's *Check quality…*, *Improve* and
  *Run folders…*, New design's *Very hard* parameters (split-split-plot and strip-plot designs: opened in the
  browser they are analysed), *Compare designs…* and *Sample size and power…* (power and sample size, reliability
  demonstrations, accelerated life test plans), *Add curves from a stacked CSV…*, *Table operations…*, *Open from a database…*, running a study (*Study script…* downloads its script), CATIA V5, the log folder,
  recent files, *Save project as…* and the WebGL setting.
* The build downloads the pinned runtime once into `build/web-cache`. The pipeline on master builds the file too
  (the `web` job's artifact) and publishes it on the project's GitLab Pages site. See [DEVELOPMENT.md](https://gitlab.com/niklas.nordin.swe/doe_analyzer/-/blob/master/DEVELOPMENT.md).

## Using it

1. **Open a CSV** (Ctrl+O, or drop it on the window; the dialog's File name field takes a pasted
   full path). The import preview shows the
   detected encoding, delimiter and decimal separator (Swedish Excel `1,5;2,3`
   works); override them if needed. An Excel workbook (.xlsx) opens from the worksheet you choose, and every
   table the app writes can be a workbook too. No runs yet? *New design…* writes a design to
   run: Latin hypercube or Sobol, full or two-level fractional factorial (highest
   resolution), Plackett–Burman, Taguchi orthogonal arrays, central composite,
   Box–Behnken, definitive screening, D-, I- or A-optimal, MaxPro, uniform, sphere packing, fast flexible filling,
   Bayesian D-optimal (terms if possible), alias-optimal, supersaturated, covering arrays, choice and MaxDiff sets,
   designs for a nonlinear model, or for a mixture a simplex lattice, simplex centroid or
   extreme vertices (with process variables), any of them in blocks (a factorial's confounded with its highest
   interactions); with a dataset open it also adds space-filling, D- or I-optimal, centre, replicate, fold-over or
   axial runs to its design. Hard-to-change parameters make a split-plot design (their runs grouped into whole
   plots), very hard ones a split-split-plot or strip-plot design; a discrete parameter takes its levels, or their number between a low and a high value. *Check
   quality…* measures the design before it is written and *Improve* moves its runs to a better one; *Run
   folders…* also writes a folder per run with your solver's input files filled in. *Compare designs…* sets designs
   side by side (relative efficiencies, power, fraction of design space) and *Sample size and power…* gives a test's
   runs or power, a reliability demonstration's units and an accelerated life test's plan.
   *Reload* (F5) reads the CSV again after new runs were added, keeping every setting; *Add runs from a CSV…* and
   *Add columns from a CSV…* join another file's data to it, *Table operations…* stacks, splits, joins, sorts,
   subsets, transposes or summarises it into a new table, and *Open from a database…* reads an SQLite table or a
   query (ODBC or a database URL too, with their packages).
2. **Data page → Columns.** Tick the ID column and the result functions; every
   other column is a DOE parameter (or tick Fidelity on the column that tells
   cheap runs from expensive ones, and Whole plot on a split-plot design's whole plots). Per column:
   * type (continuous / discrete / categorical),
   * scale — Linear, Log10, Symlog (signed values spread over decades; C is the
     half-width of the linear zone) or Power (Box–Cox, λ). The scale is suggested from the data and is
     used everywhere: plot axes, binning, correlation, models, sampling;
   * a unit, shown with the column's name on its axes;
   * for results: goal (minimise / maximise / target), a constraint (≤ / ≥ limit or a range)
     and an importance (1–5) for the overall desirability;
   * for parameters: design-space bounds, and a discrete or categorical one's levels (*Levels…* adds ones no
     run has yet, for Next runs, Optimisation and What-if to choose from).
   *Constraints* limits combinations of parameters (`x1 + x2 <= 10`): New design,
   Next runs, Optimisation and What-if keep to them. A mixture is one with `=`
   (`cement + water + sand = 100`; found on opening when the shares add up).
   *Derived column* adds a result computed from others (`P_out / P_in`).
3. **Explore** with the pages in the drawer. Lasso or box-select points in a
   scatter, brush parallel-coordinate axes, select table rows, click a category
   path: the selection is shared by every page. *Highlight* fades the rest;
   *Filter* shows only the selection on the other pages. Chips under the title
   clear it, turn it into a filter (keep only / exclude), hide failed runs or show
   feasible runs only. Filters come in named groups (*Groups ▾*): each group keeps
   its own filters, and a click on its chip puts them in play.
4. **Export** (download icon / Ctrl+E): PNG with embedded DPI, SVG and PDF
   (vector), with size presets, font size and theme (publication white, current UI,
   dark). *Export all plots* writes every page; rows can be exported as CSV with
   full precision. *Export plot as Python script* (code icon) writes the calls
   of `doe_analyzer.api` that draw the plot without the app (see Scripting below).
5. **Save the project** (Ctrl+S): `name.doeproj` next to the CSV keeps roles,
   scales, units, goals and targets, constraints, importances, bounds, levels no run has, derived columns,
   curve matches, constraints between parameters (a mixture's too), groups of filters, page
   settings, the run files folder and the neural network's shape, and is restored automatically when the
   CSV is opened again.

Numbers are read and written exactly (round-trip), stored as doubles, and shown
with N significant digits (Settings → Numbers); a nonzero value never shows as 0.

## Pages

Every page, plot, method and option is explained in [docs/HELP.md](https://gitlab.com/niklas.nordin.swe/doe_analyzer/-/blob/master/docs/HELP.md); the same
text is in the app (help button or F1 for the open page, Help in the navigation drawer for all of it).

| Group | Page | What it does |
|---|---|---|
| Data | Data | Column roles, scales, units, goals (minimise, maximise, target), constraints (≤, ≥, range), importance, bounds, levels, derived columns, constraints between parameters and mixtures (components adding up to a total); table; statistics |
| | DOE quality | Space filling (discrepancy, distances) against a random design, 1D/2D projections, parameter correlation and VIF; where runs fail: rate per bin, classifier AUC, drivers, P(fail) map; power, prediction variance (fraction of design space) and term correlations of a model, before the runs are made; a D-optimal subset of the runs (covariates) |
| | Outliers | Robust z, robust Mahalanobis (MCD), isolation forest, LOF, cross-validated model residuals; select or exclude the flagged runs |
| Explore | Distributions | Histograms, density, box, violin, ECDF, normal QQ; split by selection or category |
| | Scatter | 2D (colour, size, symbol, trend), scatter matrix, 3D, ternary (three components' shares), each sized and shaped by a column |
| | Graph builder | One graph of any columns: points, lines, smoother or line fit, bars of a summary with error bars, box plots, violins, histograms, heat maps or contours, coloured by a column and wrapped into small multiples |
| | Tabulate | Summary tables: rows grouped by one or two columns and across by another, N, mean, standard deviation, quantiles, sum and shares |
| | Parallel coordinates | Brushable axes (brushing selects), parallel categories; Boruta importance orders the parameter axes by how much they matter to a result (confirmed, tentative, rejected) |
| | Correlation | Pearson (scale space), Spearman, Kendall, partial, mutual information, distance correlation |
| Structure | PCA / t-SNE / UMAP | PCA scree, loadings and biplot; t-SNE, UMAP, MDS maps |
| | Self-organizing map | Component planes (each column on the same map of the runs: correlated columns look alike) and the U-matrix with the runs (groups and the borders between them; lasso to select) |
| | Clustering | k-means, Gaussian mixture, Ward, HDBSCAN, DBSCAN; profiles, choice of k, dendrogram; click a run to select its cluster |
| | Text explorer | Free text as documents: term and phrase counts, topics by latent semantic analysis, the terms tied to a result |
| Model | Sensitivity | SRC, SRRC, PRCC, RBD-FAST, PAWN, delta, type II ANOVA (a mixed model's for a split-plot design), main effects, interactions, half-normal plot and Pareto of effects; Sobol S1/ST and Morris on an automatic surrogate (its Q² shown); Sobol indices from a polynomial chaos expansion |
| | Surrogate models | Linear model, polynomial RSM (also with terms selected by AICc, by the lasso, elastic net or adaptive lasso, and a definitive screening design's two-stage fit), Gaussian process, RBF, random forest, extra trees, gradient boosting, SVR, MLP, kNN, polynomial chaos, multi-fidelity GP (co-kriging of cheap and expensive runs, marked by a Fidelity column), Scheffé mixture polynomials; 5-fold CV leaderboard in scale space and original units; a polynomial's coefficients (estimates, standard errors, p-values, intervals, VIF), each run's influence (leverage, studentised residual, Cook's distance) and Box–Cox; parity, residuals, response surface, partial dependence with ICE, importance, a neural network's training history; equations; any model exported as an FMU (FMI 2.0, for GT-SUITE, Simulink, Dymola) or as C code, or as a pickle |
| | Fit model | One model with JMP's Fit Model reports: parameter estimates and effect tests (F, or likelihood ratio), leverage plots, LS means with Tukey–Kramer, Dunnett, Bonferroni, Holm or Student comparisons and connecting letters, contrasts, inverse prediction; effects by stepwise (p-value, AICc, BIC, CV) or penalised regression (lasso, elastic net, ridge, adaptive lasso, with the path); normal (random effects by REML with variance components: blocks, operators, whole plots, subplots, strips), Poisson, binomial, gamma, negative binomial, ordinal logistic, conditional logit (choice sets) and MaxDiff models |
| | Functional data | Curves from curve files, stacked or wide tables: P-spline smoothing (λ by GCV), shift or landmark registration, functional principal components and their scores (as results too), the scores modelled in the parameters to predict whole curves (functional DOE) |
| | Time series | Forecasts by ARIMA (orders by AICc, stepwise) or Holt–Winters with prediction intervals, autocorrelation, STL decomposition, residual checks (Ljung–Box) |
| | Classification | Pass/fail and other outcomes (or which runs fail): logistic regression (also with quadratic terms), random forest, gradient boosting; cross-validated accuracy, ROC AUC, log loss and Brier score; ROC curves, the probability over two parameters, confusion matrix, permutation importance |
| | What-if | Sliders per parameter (a mixture's keeping their total), every result predicted with a 90 % interval, prediction profiler, desirability with importance; maximise desirability; sweet spot (probability that every constraint is met over two parameters); design space (the largest box of parameter ranges around the point in which every setting meets every constraint with the required probability); robustness (Monte Carlo of parameter tolerances, normal, uniform, triangular or lognormal, correlated, or as measured: P(met), ppm out, Cpk; FORM and importance sampling for small failure probabilities); predictions for a CSV of new settings; every result's model in one FMU or C file; a whole curve predicted by a reduced-order model (POD) of the runs' curves; a mixture's triangle (a result, D or P(met) over three components) |
| | Calibration | Bayesian calibration of the parameters experiments cannot set: a CSV of experiments (known settings, measured results and curves), the posterior by an ensemble MCMC on the surrogates (corner plot, credible intervals, measured against predicted, chains), the samples exported |
| Decide | Pareto | Non-dominated runs, 2D/3D fronts, objective matrix, hypervolume, knee, TOPSIS ranking; progress over run order (best so far, hypervolume, feasible share); export the Pareto set |
| | Compare runs | The selected runs side by side (the best values in bold, unmet constraints marked), as a radar of their parameters and results, their images, the images of the best and worst runs by a result, and their curves among all runs' (columns of per-run image or curve files, named relative to the CSV's folder or to another one you choose when they are not found there); match a measured curve (a result to minimise, for calibration). A run's picture also shows when hovering over its point on any plot |
| | Quality | Control charts (I-MR, X̄–R, X̄–S, p, np, c, u) with Nelson's or Western Electric's tests, process capability (Cp, Cpk, Pp, Ppk, Cpm, ppm), Gage R&R by REML (%GRR, ndc), equivalence by two one-sided tests |
| | Reliability | Life distributions (Weibull, lognormal, exponential, loglogistic, normal) with censoring: probability plots, B-lives and reliability with intervals, Kaplan–Meier, AICc; life vs stress (Arrhenius, inverse power); degradation paths to a threshold |
| | Optimisation | Single objective (differential evolution + local polish) or NSGA-II on surrogates, with constraints; candidates with intervals and the nearest real run; robust design: the mean against the spread under parameter scatter, with reliability constraints and a limit on the spread (± 3σ), the best mean within it first |
| | Next runs | Expected improvement, probability of improvement, lower confidence bound, expected hypervolume improvement (2–3 objectives), maximum uncertainty or maximin space filling; respects constraints and avoids failure regions; exports a CSV in your column layout |
| | Study | The runs of a study a script drives (or of any campaign, in file order) in the order they were made, coloured by how each was chosen: each result's best so far, where runs failed, what each step added and where it sampled; follows a running study as its CSV grows |

Model-based pages fit in each result's scale space (log10 of a Log10 result)
and report accuracy in both spaces. Long computations run in the background
with a progress bar and can be cancelled; GP fits use at most 1 500 runs, t-SNE
3 000 and MDS 1 500 by default (always stated on the plot).

**Reports** (download icon): an interactive HTML report (plotly inlined, opens
offline) or a PDF report, with the column table, statistics and the chosen
pages.

## Scripting

`doe_analyzer.api` is the same analysis without the window: each step a function, each returning a new dataset.
*Export plot as Python script* writes these calls for the plot on screen; they are kept working across versions.
The help chapter *Scripting the analysis* ([docs/HELP.md](https://gitlab.com/niklas.nordin.swe/doe_analyzer/-/blob/master/docs/HELP.md#scripting-the-analysis)) describes every
function, and names each page and plot.

```python
import doe_analyzer.api as da

ds = da.read_csv("study.csv")
ds = da.set_column(ds, "y", role="result", goal="min")
runs = da.runs(ds, [da.hide_failed()])
snap = da.snapshot(ds, runs)
opts = da.options(ds, "scatter", x="x1", y="y")
result = da.compute(snap, "scatter", opts)
fig = da.figure(snap, "scatter", "scatter", opts, result, da.publication_theme(font_pt=9))
da.export(fig, "scatter.png", width=160, height=100, unit="mm", dpi=300)     # offscreen: no display needed
```

### Driving a study

A script can also run the study: DOE Analyzer chooses each run, your model or simulation evaluates it,
and the runs go to a CSV with a project beside it that the app opens set up (bounds, goals, constraints).

```python
import doe_analyzer.api as da

def rosenbrock(p):
    return {"f": (1 - p["x1"]) ** 2 + 100 * (p["x2"] - p["x1"] ** 2) ** 2}

study = da.Study("rosenbrock.csv", {"x1": (-2, 2), "x2": (-2, 2)}, {"f": "min"}, rosenbrock)
study.design(runs=20)                          # a New design: Latin hypercube by default
study.next_runs(budget=60, criterion="ei")     # Next runs' batches, each evaluated, to 60 runs
study.optimise(budget=600)                     # or an optimiser calling the function itself
print(study.best())
```

- **Parameters** are `(low, high)`, `(low, high, "log10")` or a list of levels (numbers: discrete, text:
  categorical); **results** a goal (`"min"`, `"max"`) or column settings
  (`{"goal": "min", "scale": "log10"}`, `{"constraint": ("<=", 300), "unit": "MPa"}`); `constraints=["x1 + x2 <= 1"]`
  holds between parameters, a mixture's too, and `units={"x1": "mm"}` gives the parameters' units.
- **Choosing runs.** `design` evaluates any New design, and runs a split-plot one (`hard=["T"]`) whole plot by
  whole plot, writing each run's whole plot to the CSV for the analysis. `next_runs` proposes batches with
  Next runs' criteria: Expected improvement and its kin to optimise, Maximum uncertainty or Space filling to explore, EHVI for two
  or three goals. `optimise` runs differential evolution (one goal) or NSGA-II (several) on the evaluations
  themselves, for models that are fast. Each stops at `budget` runs in the study.
- **Evaluating runs.** A Python function takes the parameters as a dict and returns the results. A
  simulation runs through `da.Command("mysim", workers=4)`: each run gets a folder of its own with a
  `params.txt` of `name = value` lines; the command runs there as `mysim params.txt results.txt` and writes
  its results the same way. A solver needs no wrapper: `templates=["deck.inp"]` fills its own input deck's
  `{{name}}` placeholders with the run's values, `copy=["mesh"]` copies files as they are, and
  `outputs={"drag": ("log", r"Cd = (\S+)"), "mass": ("history.dat", 2)}` reads the results from its log and
  tables. `scheduler="slurm"` (or PBS, LSF, SGE) runs each run as a cluster job, `timeout=` and `retries=`
  stop a run that takes too long and run a failed one again. An exception, an error exit or results not found fail the run, which stays in the CSV without
  results (Next runs then steers away from such regions).
- **Stopping and going on.** The CSV is the study's memory: run the script again and nothing it holds is
  evaluated again, so a study interrupted, or given a larger budget, goes on where it stopped.

New design's *Study script…* writes such a script from the design, with the results' goals, the steps and the
evaluator; the desktop app runs it and shows its progress on the **Study** page. `python study.py --dry-run` (the
dialog's *Dry run…*) checks the set-up: nothing is run and the CSV is not written, but a command's first run gets
its folder, and the report says what would run there and how each result would be read. Open a study's project
(*Open project…*), finished or still running, to analyse it on every page.

[examples/rosenbrock](https://gitlab.com/niklas.nordin.swe/doe_analyzer/-/tree/master/examples/rosenbrock)
minimises Rosenbrock's function with each strategy, and with the function run as a separate program.

## Tests

```bash
.venv/bin/pytest                    # everything, headless (offscreen + software WebGL)
.venv/bin/pytest -m "not webengine" # without QtWebEngine
.venv/bin/python scripts/bench.py   # responsiveness on 20 000 rows
.venv/bin/python scripts/export_all.py --demo all --out /tmp/plots
.venv/bin/python scripts/build_help.py   # regenerate docs/HELP.md after editing doe_analyzer/data/help/*.md
```

The C export's tests build and run the code it writes with `cc` or `gcc`, and are skipped without one.

## License

MIT, see [LICENSE](https://gitlab.com/niklas.nordin.swe/doe_analyzer/-/blob/master/LICENSE). The icons are Google's Material Symbols, under
Apache-2.0: [doe_analyzer/data/icons/LICENSE](https://gitlab.com/niklas.nordin.swe/doe_analyzer/-/blob/master/doe_analyzer/data/icons/LICENSE).
