diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index c98a043..9ad215b 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -80,8 +80,7 @@ jobs: - name: Install package (with notebook + backend deps) run: uv sync --all-groups - # Runs the 2 Jupyter basic tutorials (via nbconvert) and the 2 marimo export - # tutorials (via `marimo export`) so the tutorials cannot silently drift out - # of sync with the code again. + # Executes every committed rendered tutorial and every canonical marimo + # source so the published learning path cannot silently drift from the code. - name: Execute tutorial notebooks run: uv run pytest tests/test_notebooks.py --run-notebooks --no-cov -v diff --git a/docs/api/cli.md b/docs/api/cli.md new file mode 100644 index 0000000..45702e8 --- /dev/null +++ b/docs/api/cli.md @@ -0,0 +1,127 @@ +# Command-line reference + +LANfactory installs six commands. The tables below describe the complete +command-line surface; run `COMMAND --help` to inspect the version installed in +your environment. Training commands consume LANfactory configuration files, +export commands convert saved trainer artifacts, and Hub commands move reviewed +artifacts to or from a Hugging Face repository. + +## `jaxtrain` + +Train a JAX network. Either `--training-data-folder` or +`--data-generation-experiment-id` must identify the training data. + +| Option | Required/default | Contract | +| --- | --- | --- | +| `--config-path PATH` | bundled configuration | YAML training configuration | +| `--training-data-folder PATH` | unset | Training data directory; optional when an MLflow data-generation experiment is supplied | +| `--network-id INTEGER` | `0` | Network entry selected from the configuration | +| `--dl-workers INTEGER` | `1` | DataLoader worker count; non-positive values request automatic sizing | +| `--networks-path-base PATH` | required | Base directory for saved network artifacts | +| `--dry-run` | off | Validate configuration and data discovery without training | +| `--export-onnx` / `--no-export-onnx` | on | Export the HSSM-consumable ONNX artifact after training | +| `--mlflow-run-name TEXT` | unset | Enable tracking under this run name | +| `--mlflow-experiment-name TEXT` | `MLFLOW_EXPERIMENT_NAME` or unset | MLflow experiment name | +| `--mlflow-run-id TEXT` | unset | Resume an existing MLflow run | +| `--data-generation-experiment-id TEXT` | unset | Derive the data location and lineage from an MLflow experiment | +| `--mlflow-tracking-uri TEXT` | `MLFLOW_TRACKING_URI` or `sqlite:///mlflow.db` | MLflow tracking backend | +| `--mlflow-artifact-location TEXT` | `MLFLOW_ARTIFACT_LOCATION` or `./mlruns` | MLflow artifact root | +| `--log-level LEVEL`, `-l LEVEL` | `WARNING` | Logging threshold | + +## `torchtrain` + +Train a PyTorch network. Its data-discovery and MLflow options match +`jaxtrain`; PyTorch artifacts can be converted with `transform-onnx`. + +| Option | Required/default | Contract | +| --- | --- | --- | +| `--config-path PATH` | bundled configuration | YAML training configuration | +| `--training-data-folder PATH` | unset | Training data directory; optional when an MLflow data-generation experiment is supplied | +| `--networks-path-base PATH` | required | Base directory for saved network artifacts | +| `--network-id INTEGER` | `0` | Network entry selected from the configuration | +| `--dl-workers INTEGER` | `1` | DataLoader worker count; non-positive values request automatic sizing | +| `--dry-run` | off | Validate configuration and data discovery without training | +| `--mlflow-run-name TEXT` | unset | Enable tracking under this run name | +| `--mlflow-experiment-name TEXT` | `MLFLOW_EXPERIMENT_NAME` or unset | MLflow experiment name | +| `--mlflow-run-id TEXT` | unset | Resume an existing MLflow run | +| `--data-generation-experiment-id TEXT` | unset | Derive the data location and lineage from an MLflow experiment | +| `--mlflow-tracking-uri TEXT` | `MLFLOW_TRACKING_URI` or `sqlite:///mlflow.db` | MLflow tracking backend | +| `--mlflow-artifact-location TEXT` | `MLFLOW_ARTIFACT_LOCATION` or `./mlruns` | MLflow artifact root | +| `--log-level LEVEL`, `-l LEVEL` | `WARNING` | Logging threshold | + +## `transform-onnx` + +Convert a saved PyTorch `TorchMLP` configuration and state dictionary to ONNX. + +| Option | Required/default | Contract | +| --- | --- | --- | +| `--network-config-file TEXT` | required | Pickled network configuration | +| `--state-dict-file TEXT` | required | Saved PyTorch state dictionary | +| `--input-shape INTEGER` | required | Concrete single-trial input width | +| `--output-onnx-file TEXT` | required | Destination ONNX file | + +## `transform-jax-onnx` + +Convert a saved `jaxtrain` network configuration and Flax state to ONNX. + +| Option | Required/default | Contract | +| --- | --- | --- | +| `--network-config-file TEXT` | required | Pickled network configuration | +| `--state-file TEXT` | required | `*_train_state.jax` Flax parameter bytes | +| `--input-shape INTEGER` | required | Concrete single-trial input width | +| `--output-onnx-file TEXT` | required | Destination ONNX file | +| `--opset INTEGER` | `17` | Target ONNX opset | + +Both transform commands produce concrete-shape, single-trial artifacts. Rank is +exporter-specific; HSSM owns the consumer contract and trial-wise vectorization. +See the [sbi](../exporting_sbi_models.md) and +[BayesFlow](../exporting_bayesflow_models.md) exporter references for the same +cross-package boundary. + +## `upload-hf` + +Publish a trained artifact set, its optional model card, canonical root alias, +and manifest entry. + +| Option | Required/default | Contract | +| --- | --- | --- | +| `--model-folder PATH` | required | Folder containing the trained artifacts; `model_card.yaml` is optional | +| `--network-type TEXT` | required | One of `lan`, `cpn`, `opn`, or `gonogo` | +| `--model-name TEXT` | required | Model identifier used in the folder and root filename | +| `--repo-id TEXT` | `franklab/HSSM` | Target Hub repository | +| `--commit-message TEXT` | `Upload model` | Hub commit message | +| `--private` | off | Create a private repository when creating the target | +| `--create-repo` | off | Create the target repository if absent | +| `--include-patterns TEXT` | unset | Comma-separated filename globs to include | +| `--exclude-patterns TEXT` | unset | Comma-separated filename globs to exclude | +| `--revision TEXT` | unset | Target branch or tag | +| `--token TEXT` | `HF_TOKEN` or unset | Explicit token or environment fallback | +| `--dry-run` | off | Print the publication plan without uploading or mutating files | +| `--publish-root-alias` / `--no-publish-root-alias` | on | Publish the canonical root filename consumed by HSSM | +| `--update-manifest` / `--no-update-manifest` | on | Read-modify-write the root `manifest.json` | +| `--require-model-card` | off | Reject a missing `model_card.yaml` instead of generating metadata | +| `--canonical-onnx PATH` | inferred | Select the ONNX file copied to the repository root | +| `--overwrite-root` | off | Permit replacement of an existing HSSM-facing root artifact | +| `--log-level LEVEL`, `-l LEVEL` | `WARNING` | Logging threshold | + +## `download-hf` + +Retrieve one `{network-type}/{model-name}/` folder from a Hub repository. + +| Option | Required/default | Contract | +| --- | --- | --- | +| `--network-type TEXT` | required | One of `lan`, `cpn`, `opn`, or `gonogo` | +| `--model-name TEXT` | required | Model folder to retrieve | +| `--output-folder PATH` | required | Local destination; must be absent unless `--force` is set | +| `--repo-id TEXT` | `franklab/HSSM` | Source Hub repository | +| `--revision TEXT` | unset (Hub default: `main`) | Branch, tag, or commit to retrieve | +| `--include-patterns TEXT` | unset | Comma-separated filename globs to include | +| `--exclude-patterns TEXT` | unset | Comma-separated filename globs to exclude | +| `--token TEXT` | `HF_TOKEN` or unset | Explicit token or environment fallback for private repositories | +| `--force` | off | Replace an existing destination | +| `--log-level LEVEL`, `-l LEVEL` | `WARNING` | Logging threshold | + +For task-oriented workflows, see [Track training with MLflow](../using_mlflow.md) +and [Share trained networks on Hugging Face Hub](../using_huggingface.md). The +[Python API reference](hf.md) documents the Hub helpers and constants called by +the two Hub entry points. diff --git a/docs/api/config.md b/docs/api/config.md index 7f9fa8b..e2be244 100755 --- a/docs/api/config.md +++ b/docs/api/config.md @@ -1,3 +1,19 @@ :::lanfactory.config :::lanfactory.config.network_configs + +## Public configuration dictionaries + +| Export | Purpose | +| --- | --- | +| `network_config_mlp` | Default LAN MLP architecture | +| `network_config_choice_prob` | Shared choice-probability architecture | +| `network_config_cpn` | Backward-compatible alias of `network_config_choice_prob` | +| `network_config_opn` | Backward-compatible alias of `network_config_choice_prob` | +| `train_config_mlp` | Default LAN training settings | +| `train_config_choice_prob` | Shared choice-probability training settings | +| `train_config_cpn` | Backward-compatible alias of `train_config_choice_prob` | +| `train_config_opn` | Backward-compatible alias of `train_config_choice_prob` | + +Copy a dictionary before changing it; the objects exported by the module are +shared mutable defaults. diff --git a/docs/api/hf.md b/docs/api/hf.md index 4aaaf1b..3774fe4 100644 --- a/docs/api/hf.md +++ b/docs/api/hf.md @@ -1 +1,23 @@ :::lanfactory.hf + +## Public constants + +| Export | Value | Contract | +| --- | --- | --- | +| `DEFAULT_REPO_ID` | `franklab/HSSM` | Default artifact repository used by Hub helpers | +| `DEFAULT_LICENSE` | `bsd-2-clause` | License metadata used for generated model cards | +| `VALID_NETWORK_TYPES` | `lan`, `cpn`, `opn`, `gonogo` | Network types accepted by Hub publication helpers | + +## Public helpers + +- `load_model_card_yaml` reads model-card metadata. +- `generate_readme` renders a model card from metadata. +- `ModelCardConfig` stores model-card metadata and defaults. +- `upload_model` publishes a trained artifact and its metadata. +- `download_model` retrieves a published network artifact. + +The installed `upload-hf` and `download-hf` entry points, including every flag +and safety default, are documented in the [command-line reference](cli.md). + +For the task-oriented publication sequence, see +[Share trained networks on Hugging Face Hub](../using_huggingface.md). diff --git a/docs/api/network_inspectors.md b/docs/api/network_inspectors.md new file mode 100644 index 0000000..ed9576d --- /dev/null +++ b/docs/api/network_inspectors.md @@ -0,0 +1,18 @@ +# `lanfactory.network_inspectors` + +The public inspection namespace loads a trained Torch LAN, compares its +likelihood with simulation-based KDE estimates, and plots likelihood manifolds. +The configuration dataclasses keep model metadata, evaluation grids, and plot +defaults explicit. + +::: lanfactory.network_inspectors.get_torch_mlp + +::: lanfactory.network_inspectors.kde_vs_lan_likelihoods + +::: lanfactory.network_inspectors.lan_manifold + +::: lanfactory.network_inspectors.ModelSpec + +::: lanfactory.network_inspectors.PlotConfig + +::: lanfactory.network_inspectors.GridSpec diff --git a/docs/basic_tutorial/basic_tutorial_lan_jax.ipynb b/docs/basic_tutorial/basic_tutorial_lan_jax.ipynb index f6b701f..88ac53a 100644 --- a/docs/basic_tutorial/basic_tutorial_lan_jax.ipynb +++ b/docs/basic_tutorial/basic_tutorial_lan_jax.ipynb @@ -1,92 +1,91 @@ { "cells": [ { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": null, + "id": "Hbol", "metadata": {}, + "outputs": [], "source": [ - "# Train with the JAX backend" + "import marimo as mo" ] }, { "cell_type": "markdown", - "metadata": {}, + "id": "MJUe", + "metadata": { + "marimo": { + "config": { + "hide_code": true + }, + "md_prefix": "r" + } + }, "source": [ - "\n", - "The `LANfactory` package is a light-weight convenience package for training `likelihood approximation networks` (LANs) in PyTorch (or JAX/Flax), \n", - "starting from supplied training data.\n", - "\n", - "[LANs](https://elifesciences.org/articles/65074), although more general in potential scope of applications, were conceived in the context of sequential sampling modeling\n", - "to account for cognitive processes giving rise to *choice* and *reaction time* data in *n-alternative forced choice experiments* commonly encountered in the cognitive sciences.\n", - "\n", - "In this quick tutorial we will use the [`ssm-simulators`](https://lnccbrown.github.io/ssm-simulators/) package to generate our training data using such a sequential sampling model (SSM). The use of the `LANfactory` package is in no way bound to utilize this `ssm-simulators` package (imported as `ssms`)." + "# Train with the JAX backend\n", + "\n", + "This is the JAX/Flax companion to [Train your first LAN\n", + "(PyTorch)](../basic_tutorial_lan_torch/). Complete that learning tutorial\n", + "first: it owns the explanation of LAN training data, configuration, and the\n", + "end-to-end workflow. Here we repeat a deliberately tiny data fixture so this\n", + "how-to remains executable, then focus on the JAX-specific factory, trainer,\n", + "saved state, and inference call.\n", + "\n", + "The fixture uses [`ssm-simulators`](https://lnccbrown.github.io/ssm-simulators/)\n", + "to generate DDM data. LANfactory can train on compatible data from other\n", + "generators as well." ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": null, + "id": "vblA", "metadata": {}, + "outputs": [], "source": [ - "#### Install\n", - "\n", - "To install the `ssm-simulators` package (imported as `ssms`) type,\n", - "\n", - "`pip install ssm-simulators`\n", - "\n", - "To install the `LANfactory` package type,\n", - "\n", - "`pip install lanfactory`\n", + "from copy import deepcopy\n", + "from pathlib import Path\n", "\n", - "Necessary dependency should be installed automatically in the process." + "import lanfactory\n", + "import numpy as np\n", + "import ssms" ] }, { - "cell_type": "code", - "execution_count": 1, + "cell_type": "markdown", + "id": "bkHC", "metadata": { - "execution": { - "iopub.execute_input": "2026-07-12T13:22:39.992161Z", - "iopub.status.busy": "2026-07-12T13:22:39.991943Z", - "iopub.status.idle": "2026-07-12T13:22:42.822300Z", - "shell.execute_reply": "2026-07-12T13:22:42.822036Z" + "marimo": { + "config": { + "hide_code": true + }, + "md_prefix": "r" } }, - "outputs": [], "source": [ - "import ssms\n", - "import lanfactory\n", - "import os\n", - "import numpy as np\n", - "from copy import deepcopy\n", - "import torch\n", - "import pickle\n", - "from pathlib import Path" + "## Generate a small training fixture\n", + "\n", + "These settings make two small files so the example runs quickly. For real\n", + "training, choose the simulator budget and parameter coverage described in\n", + "the PyTorch learning tutorial and the\n", + "[`ssm-simulators` data-generation documentation](https://lnccbrown.github.io/ssm-simulators/)." ] }, { "cell_type": "code", - "execution_count": 2, - "metadata": { - "execution": { - "iopub.execute_input": "2026-07-12T13:22:42.823534Z", - "iopub.status.busy": "2026-07-12T13:22:42.823400Z", - "iopub.status.idle": "2026-07-12T13:22:42.825438Z", - "shell.execute_reply": "2026-07-12T13:22:42.825233Z" - } - }, + "execution_count": null, + "id": "lEQa", + "metadata": {}, "outputs": [], "source": [ - "# MAKE CONFIGS\n", - "RUN_SIMS = True\n", - "DEVICE = \"cpu\"\n", - "\n", - "# Define a model\n", "MODEL = \"ddm\"\n", "OUT_FOLDER = Path(\"jax_nb_data\") / \"training_data\"\n", "MODEL_FOLDER = Path(\"jax_nb_data\") / \"jax_models\" / \"lan\"\n", "N_DATA_FILES = 2\n", "BATCH_SIZE = 1000\n", - "os.makedirs(OUT_FOLDER, exist_ok=True)\n", + "OUT_FOLDER.mkdir(parents=True, exist_ok=True)\n", + "MODEL_FOLDER.mkdir(parents=True, exist_ok=True)\n", "\n", - "# Initialize the generator config (nested config object from ssm-simulators)\n", "generator_config = ssms.config.get_default_generator_config(\"lan\")\n", "generator_config[\"model\"] = MODEL\n", "generator_config[\"pipeline\"][\"n_parameter_sets\"] = 100\n", @@ -95,138 +94,108 @@ "generator_config[\"training\"][\"n_samples_per_param\"] = 200\n", "generator_config[\"output\"][\"folder\"] = str(OUT_FOLDER)\n", "\n", - "# Make model config dict\n", "model_config = deepcopy(ssms.config.model_config[MODEL])" ] }, { "cell_type": "code", - "execution_count": 3, - "metadata": { - "execution": { - "iopub.execute_input": "2026-07-12T13:22:42.826343Z", - "iopub.status.busy": "2026-07-12T13:22:42.826268Z", - "iopub.status.idle": "2026-07-12T13:22:43.325292Z", - "shell.execute_reply": "2026-07-12T13:22:43.325069Z" - } - }, + "execution_count": null, + "id": "PKri", + "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Generating data file 1 / 2\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ + "Generating data file 1 / 2\n", "Generating data file 2 / 2\n" ] } ], "source": [ - "# MAKE DATA\n", - "if RUN_SIMS:\n", - " for i in range(N_DATA_FILES):\n", - " print(f\"Generating data file {i + 1} / {N_DATA_FILES}\")\n", - " my_dataset_generator = ssms.dataset_generators.lan_mlp.TrainingDataGenerator(\n", - " config=generator_config, model_config=model_config\n", - " )\n", - " _ = my_dataset_generator.generate_data_training(save=True)" + "for _i in range(N_DATA_FILES):\n", + " print(f\"Generating data file {_i + 1} / {N_DATA_FILES}\")\n", + " _generator = ssms.dataset_generators.lan_mlp.TrainingDataGenerator(\n", + " config=generator_config,\n", + " model_config=model_config,\n", + " )\n", + " _generator.generate_data_training(save=True)" ] }, { "cell_type": "code", - "execution_count": 4, - "metadata": { - "execution": { - "iopub.execute_input": "2026-07-12T13:22:43.326334Z", - "iopub.status.busy": "2026-07-12T13:22:43.326273Z", - "iopub.status.idle": "2026-07-12T13:22:43.328115Z", - "shell.execute_reply": "2026-07-12T13:22:43.327936Z" - } - }, + "execution_count": null, + "id": "Xref", + "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Network config: \n", + "Network config:\n", "{'layer_sizes': [100, 100, 100, 1], 'activations': ['tanh', 'tanh', 'tanh', 'linear'], 'train_output_type': 'logprob'}\n", - "Train config: \n", + "Train config:\n", "{'cpu_batch_size': 4096, 'gpu_batch_size': 4096, 'n_epochs': 2, 'optimizer': 'adam', 'learning_rate': 2e-06, 'lr_scheduler': 'reduce_on_plateau', 'lr_scheduler_params': {}, 'weight_decay': 0.0, 'loss': 'huber', 'save_history': True}\n" ] } ], "source": [ - "from copy import deepcopy\n", - "\n", - "# MAKE FIXTURE WITH SOMEWHAT RANDOM PROPERTIES\n", "network_config = deepcopy(lanfactory.config.network_configs.network_config_mlp)\n", "network_config[\"layer_sizes\"] = [100, 100, 100, 1]\n", "network_config[\"activations\"] = [\"tanh\", \"tanh\", \"tanh\", \"linear\"]\n", - "\n", - "print(\"Network config: \")\n", + "print(\"Network config:\")\n", "print(network_config)\n", "\n", "train_config = deepcopy(lanfactory.config.network_configs.train_config_mlp)\n", - "train_config[\"learning_rate\"] = 0.000002\n", - "\n", - "# CHECK CORNER CASES\n", + "train_config[\"learning_rate\"] = 2e-6\n", "train_config[\"cpu_batch_size\"] = 4096\n", "train_config[\"gpu_batch_size\"] = 4096\n", "train_config[\"n_epochs\"] = 2\n", - "\n", - "\n", - "print(\"Train config: \")\n", + "print(\"Train config:\")\n", "print(train_config)" ] }, { "cell_type": "markdown", - "metadata": {}, + "id": "SFPL", + "metadata": { + "marimo": { + "config": { + "hide_code": true + }, + "md_prefix": "r" + } + }, "source": [ - "#### Prepare for Training\n", + "## Reuse the shared data loader\n", "\n", - "Next we set up dataloaders for training. The `LANfactory` provides convenient helper functions for this.\n", + "`make_train_valid_dataloaders` is backend-neutral at this boundary. It:\n", "\n", - "The `make_train_valid_dataloaders` function handles:\n", - "- Splitting your data files into training and validation sets\n", - "- Creating the appropriate `DatasetTorch` objects\n", - "- Wrapping them in PyTorch `DataLoader` objects with sensible defaults\n", + "- splits your data files into training and validation sets;\n", + "- creates the appropriate `DatasetTorch` objects; and\n", + "- wraps them in PyTorch `DataLoader` objects with sensible defaults.\n", "\n", - "The data is returned as numpy arrays, which JAX handles seamlessly." + "The loaders yield NumPy-compatible batches that the JAX trainer consumes." ] }, { "cell_type": "code", - "execution_count": 5, - "metadata": { - "execution": { - "iopub.execute_input": "2026-07-12T13:22:43.329009Z", - "iopub.status.busy": "2026-07-12T13:22:43.328947Z", - "iopub.status.idle": "2026-07-12T13:22:43.332910Z", - "shell.execute_reply": "2026-07-12T13:22:43.332714Z" - } - }, + "execution_count": null, + "id": "BYtC", + "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Training batches: 80\n", - "Validation batches: 80\n", + "Training batches: 20\n", + "Validation batches: 20\n", "Input dimension: 6\n" ] } ], "source": [ - "# MAKE DATALOADERS\n", - "\n", - "# Data files were generated directly into OUT_FOLDER above\n", - "file_list_ = list(Path(OUT_FOLDER).glob(\"*.pickle\"))\n", + "file_list_ = sorted(OUT_FOLDER.glob(\"*.pickle\"))\n", "\n", "# num_workers=0 keeps data loading in-process: ssm-simulators sets the\n", "# multiprocessing start method to \"spawn\" at import, which is unsafe for\n", @@ -247,60 +216,60 @@ }, { "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Define Network" - ] - }, - { - "cell_type": "code", - "execution_count": 6, + "id": "RGSE", "metadata": { - "execution": { - "iopub.execute_input": "2026-07-12T13:22:43.333824Z", - "iopub.status.busy": "2026-07-12T13:22:43.333774Z", - "iopub.status.idle": "2026-07-12T13:22:43.335251Z", - "shell.execute_reply": "2026-07-12T13:22:43.335029Z" + "marimo": { + "config": { + "hide_code": true + }, + "md_prefix": "r" } }, - "outputs": [], "source": [ - "# LOAD NETWORK\n", - "# Test properties of network\n", - "jax_net = lanfactory.trainers.JaxMLPFactory(network_config=network_config, train=True)\n", + "## Create the JAX/Flax network\n", "\n", - "# Save model config\n", - "# model_folder = os.path.join(\"data\", \"jax_models\", MODEL)\n", - "# os.makedirs(model_folder, exist_ok = True)\n", - "\n", - "# pickle.dump(\n", - "# network_config,\n", - "# open(os.path.join(model_folder,\n", - "# \t\t\t\t\t \"jax_network_config.pickle\"), \"wb\")\n", - "# \t\t)" + "`JaxMLPFactory` materializes the configured Flax MLP. Passing `train=True`\n", + "prepares it for the training-state lifecycle used by `ModelTrainerJaxMLP`." ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": null, + "id": "Kclp", "metadata": {}, + "outputs": [], "source": [ - "#### Train " + "jax_net = lanfactory.trainers.JaxMLPFactory(\n", + " network_config=network_config,\n", + " train=True,\n", + ")" ] }, { - "cell_type": "code", - "execution_count": 7, + "cell_type": "markdown", + "id": "emfo", "metadata": { - "execution": { - "iopub.execute_input": "2026-07-12T13:22:43.336200Z", - "iopub.status.busy": "2026-07-12T13:22:43.336148Z", - "iopub.status.idle": "2026-07-12T13:22:43.337698Z", - "shell.execute_reply": "2026-07-12T13:22:43.337531Z" + "marimo": { + "config": { + "hide_code": true + }, + "md_prefix": "r" } }, + "source": [ + "## Train and save the JAX state\n", + "\n", + "The JAX trainer takes the same configuration and loaders as the PyTorch\n", + "path, but writes a Flax training-state artifact rather than a Torch model." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "Hstk", + "metadata": {}, "outputs": [], "source": [ - "# Test properties of jax trainer\n", "jax_trainer = lanfactory.trainers.ModelTrainerJaxMLP(\n", " train_config=train_config,\n", " model=jax_net,\n", @@ -312,47 +281,35 @@ }, { "cell_type": "code", - "execution_count": 8, - "metadata": { - "execution": { - "iopub.execute_input": "2026-07-12T13:22:43.338601Z", - "iopub.status.busy": "2026-07-12T13:22:43.338529Z", - "iopub.status.idle": "2026-07-12T13:22:43.916572Z", - "shell.execute_reply": "2026-07-12T13:22:43.916340Z" - } - }, + "execution_count": null, + "id": "nWHF", + "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Epoch: 0 of 2\n", - "Training - Step: 0 of 80 - Loss: 4.7319036\n", - "Epoch 0/2 time: 0.14957714080810547s\n", - "Validation - Step: 0 of 80 - Loss: 0.5624591\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Epoch 0/2 time: 0.052381038665771484s\n", - "Epoch: 0 / 2, test_loss: 0.6855981945991516\n", + "Training - Step: 0 of 20 - Loss: 4.5220714\n", + "Epoch 0/2 time: 0.21618890762329102s\n", + "Validation - Step: 0 of 20 - Loss: 3.173557\n", + "Epoch 0/2 time: 0.06603813171386719s\n", + "Epoch: 0 / 2, test_loss: 3.1969714164733887\n", "Epoch: 1 of 2\n", - "Training - Step: 0 of 80 - Loss: 0.5325153\n", - "Epoch 1/2 time: 0.0390627384185791s\n", - "Validation - Step: 0 of 80 - Loss: 0.38235876\n", - "Epoch 1/2 time: 0.017911195755004883s\n", - "Epoch: 1 / 2, test_loss: 0.4255990982055664\n", + "Training - Step: 0 of 20 - Loss: 2.8688884\n", + "Epoch 1/2 time: 0.0069768428802490234s\n", + "Validation - Step: 0 of 20 - Loss: 1.7008276\n", + "Epoch 1/2 time: 0.0024831295013427734s\n", + "Epoch: 1 / 2, test_loss: 1.8143354654312134\n", "Saving training history to: jax_nb_data/jax_models/lan/jax_lan_ddm__jax_training_history.csv\n", "Saving model parameters to: jax_nb_data/jax_models/lan/jax_lan_ddm__train_state.jax\n", "Saving training config to: jax_nb_data/jax_models/lan/jax_lan_ddm__train_config.pickle\n", - "Saving training data details to: jax_nb_data/jax_models/lan/jax_lan_ddm__data_details.pickle\n" + "Saving training data details to: jax_nb_data/jax_models/lan/jax_lan_ddm__data_details.pickle\n", + "Saving ONNX export to: jax_nb_data/jax_models/lan/jax_lan_ddm__model.onnx\n" ] } ], "source": [ - "# Test if training loop works\n", "train_state = jax_trainer.train_and_evaluate(\n", " output_folder=MODEL_FOLDER,\n", " output_file_id=MODEL,\n", @@ -365,151 +322,132 @@ }, { "cell_type": "markdown", - "metadata": {}, + "id": "iLit", + "metadata": { + "marimo": { + "config": { + "hide_code": true + }, + "md_prefix": "r" + } + }, "source": [ - "#### Check Trained Network\n", + "## Reload the state for inference\n", "\n", - "We can now re-instantiate our network from the trained weights and check that the output are reasonable." + "Recreate the factory with `train=False`, load the saved state, and request a\n", + "JIT-compiled forward function. The input width is the model parameter count\n", + "plus reaction time and response." ] }, { "cell_type": "code", - "execution_count": 9, - "metadata": { - "execution": { - "iopub.execute_input": "2026-07-12T13:22:43.917648Z", - "iopub.status.busy": "2026-07-12T13:22:43.917581Z", - "iopub.status.idle": "2026-07-12T13:22:43.919092Z", - "shell.execute_reply": "2026-07-12T13:22:43.918904Z" - } - }, + "execution_count": null, + "id": "ZHCJ", + "metadata": {}, "outputs": [], "source": [ - "# Loaded Net\n", - "# Test passing network config as path and as object\n", - "\n", "jax_infer = lanfactory.trainers.JaxMLPFactory(\n", - "\t network_config=network_config,\n", - " train=False,\n", - " )" + " network_config=network_config,\n", + " train=False,\n", + ")" ] }, { "cell_type": "code", - "execution_count": 10, - "metadata": { - "execution": { - "iopub.execute_input": "2026-07-12T13:22:43.919931Z", - "iopub.status.busy": "2026-07-12T13:22:43.919878Z", - "iopub.status.idle": "2026-07-12T13:22:43.951320Z", - "shell.execute_reply": "2026-07-12T13:22:43.951023Z" - } - }, + "execution_count": null, + "id": "ROlb", + "metadata": {}, "outputs": [], "source": [ - "# Test passing train state as path and as object\n", - "forward_pass, forward_pass_jitted = jax_infer.make_forward_partial(\n", + "# Establish the reactive dependency before reloading the saved state.\n", + "_ = train_state\n", + "_forward_pass, forward_pass_jitted = jax_infer.make_forward_partial(\n", " seed=42,\n", " input_dim=model_config[\"n_params\"] + 2,\n", - " state=os.path.join(MODEL_FOLDER,\n", - "\t\t\t\t\t \"jax_lan_\" + MODEL + \"__train_state.jax\"),\n", + " state=str(MODEL_FOLDER / f\"jax_lan_{MODEL}__train_state.jax\"),\n", " add_jitted=True,\n", ")" ] }, { "cell_type": "code", - "execution_count": 11, - "metadata": { - "execution": { - "iopub.execute_input": "2026-07-12T13:22:43.952460Z", - "iopub.status.busy": "2026-07-12T13:22:43.952391Z", - "iopub.status.idle": "2026-07-12T13:22:44.251430Z", - "shell.execute_reply": "2026-07-12T13:22:44.251089Z" - } - }, + "execution_count": null, + "id": "qnkX", + "metadata": {}, "outputs": [], "source": [ "import jax.numpy as jnp\n", "\n", - "# Test parameters:\n", "theta = deepcopy(ssms.config.model_config[MODEL][\"default_params\"])\n", - "\n", - "# Comparison simulator run\n", "sim_out = ssms.basic_simulators.simulator.simulator(\n", - " model=MODEL, theta=theta, n_samples=50000\n", + " model=MODEL,\n", + " theta=theta,\n", + " n_samples=50_000,\n", ")\n", - "\n", - "# Make input matrix\n", "input_mat = jnp.zeros((2000, len(theta) + 2))\n", - "for i in range(len(theta)):\n", - " input_mat = input_mat.at[:, i].set(jnp.ones(2000) * theta[i])\n", - "\n", + "for _i in range(len(theta)):\n", + " input_mat = input_mat.at[:, _i].set(jnp.ones(2000) * theta[_i])\n", "input_mat = input_mat.at[:, len(theta)].set(\n", " jnp.array(\n", " np.concatenate(\n", " [\n", - " np.linspace(5, 0, 1000).astype(np.float32),\n", - " np.linspace(0, 5, 1000).astype(np.float32),\n", + " np.linspace(5, 0, 1000, dtype=np.float32),\n", + " np.linspace(0, 5, 1000, dtype=np.float32),\n", " ]\n", " )\n", " )\n", ")\n", "input_mat = input_mat.at[:, len(theta) + 1].set(\n", " jnp.array(\n", - " np.concatenate([np.repeat(-1.0, 1000), np.repeat(1.0, 1000)]).astype(np.float32)\n", + " np.concatenate(\n", + " [np.repeat(-1.0, 1000), np.repeat(1.0, 1000)]\n", + " ).astype(np.float32)\n", " )\n", ")\n", - "\n", "net_out = forward_pass_jitted(input_mat)" ] }, { "cell_type": "code", - "execution_count": 12, - "metadata": { - "execution": { - "iopub.execute_input": "2026-07-12T13:22:44.252624Z", - "iopub.status.busy": "2026-07-12T13:22:44.252562Z", - "iopub.status.idle": "2026-07-12T13:22:44.519342Z", - "shell.execute_reply": "2026-07-12T13:22:44.519137Z" - } - }, + "execution_count": null, + "id": "TqIu", + "metadata": {}, "outputs": [ { "data": { - "image/png": "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/eSTT3QQUa/rvHnz/D5nswx+2mmnGa+88op+7uqHmbrYgXo+o0aNMj7zmc/o948ePeq+xNLr/OyzzxqpqanGk08+qf8w+M53vmPk5ua6d8R985vfNG6//Xb3/d944w29PPfAAw/of/vqB70Kndu3bzfs4Prrr9d/SG3cuNHrNa2vr3ffp+NzVj/v1B9I6t/+li1bdDhNS0szduzYYdiF+qNJPWf171K9hrNnzzby8vL0bqJYfJ09w7X6GaSWZzuKxdc5bsLI/Pnzfa6xv/rqq+77HDhwwPjc5z5npKen63/s6pvAM5Gq+6qPUd8UpuPHj+vwoQKOqqIsWLDAHXCsSj3ec845x+d/U8/N8/+X4uJi/QupX79++ge/+iWn1uWrqqoMO1DfmGpbn/ohrr45x48fb/zyl7/0qnZ1fM7KqVOnjBtuuEH/BZaRkWF86Utf8vplbvXqn7+eklh7nR966CH9A1v1+KhKyVtvveW1VVl933v605/+pPu81P1PP/1048UXXzTswt9rql5vf8/55ptvdv//k5+fb1x66aXG1q1bDTuZO3euMWjQIP0cVOVLXffsY4q119mkwoV6fXfv3m10FIuvs6cE9T/RXioCAADxK67njAAAgOgjjAAAgKgijAAAgKgijAAAgKgijAAAgKgijAAAgKgijAAAgKgijAAAgKgijAAAgKgijAAAgKgijAAAgKgijAAAAImm/w/aRWtRmt6J2QAAAABJRU5ErkJggg==", - "text/plain": [ - "
" - ] + "image/png": "iVBORw0KGgoAAAANSUhEUgAABEgAAAM6CAYAAACSNnaSAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjExLjEsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvctoD+AAAAAlwSFlzAAAewgAAHsIBbtB1PgAA1dBJREFUeJzs3Qd4G4X5x/FX3nslcXZCEkIIYc8wwyx7drBaCqWsllEKlLaUQindBdrybym7rEIHe5S9C4S9A2Rvx/GKt2XJ+j/vpVJ8J9nWuJNOuu/neYwlWZZk2UHST+/whUKhkAAAAAAAAHhYXqZvAAAAAAAAQKYRkAAAAAAAAM8jIAEAAAAAAJ5HQAIAAAAAADyPgAQAAAAAAHgeAQkAAAAAAPA8AhIAAAAAAOB5BCQAAAAAAMDzCEgAAAAAAIDnEZAAAAAAAADPIyABAAAAAACeR0ACAAAAAAA8j4AEAAAAAAB4HgEJAAAAAADwPAISAAAAAADgeQQkAAAAAADA8whIAAAAAACA5xV4/h6wSW9vr3z00UfG4TFjxkhBAXctAAAAAAB2CwQCsn79euPwNttsIyUlJbZcLq/ibaLhyK677mrXxQEAAAAAgBG8+eabsssuu4gdaLEBAAAAAACeRwWJTbStZnCCNX78eM//cQEAAAAAYLe1a9dGOjgGvxZPFQGJXXfkoJkjGo5MmjTJrosGAAAAAAAx2Dn/kxYbAAAAAADgeQQkAAAAAADA8whIAAAAAACA5xGQAAAAAAAAzyMgAQAAAAAAnkdAAgAAAAAAPI+ABAAAAAAAeB4BCQAAAAAA8DwCEgAAAAAA4HkFnr8HAAAAPKC3t1fa2tqku7tbgsFgpm8OAMDD8vPzpaioSKqqqqSiokLy8txRu0FAAgAAkMNCoZCsXbtWNmzYkOmbAgCAIRAISF9fn3R0dIjP55OJEydKZWWlZBoBCQAAQA5rbm6OCkcKCngKCADInGAwaAT4Sj+vXr3aFSEJj44AAAA5yu/3y/r16yPH6+vrpaamxihtBgAgU0KhkNHy2dLSIp2dnZGQZIsttshou407Gn0AAABgO33SGTZq1Cjjg3AEAJBpPp9PysvLZdKkScYMEqUhyeDHrUwgIAEAAMhRXV1dkcM6CA8AALcFJXV1dZHj7e3tGb09BCQAAAA53GITfgJaXFyc6ZsDAECUsrIy43Fq8ONWphCQAAAA5KiBgQHjs7bVhJ98AgDgJj6fL9L+mek19AQkAAAAAADA8whIAAAAAACA5xGQAAAAAAAAzyMgAQAAAAAAnlfg+XsAAAAAAEbw6quvSlNTk4waNUr23nvvhL/+3HPPSUdHh4wfP1522223tN/fr732mjQ2NhorVffZZ5+Ev/78888bK1jHjRsnc+fOFa94++23ZdWqVVJRUSEHHnhgpm8OHOYLhUIhp6/EC/QfzeTJk43DK1eulEmTJmX6JgEAAI9buHChBAIBKSgokJkzZ2b65uS8l156SVpbW41tDEceeeSI59d1lk888UTk+AEHHCCVlZXDfk9nZ6c8++yzxuGamhrZd999R7yelpYWefnll43D9fX1sscee0gi9EWzvnhWW221lWyxxRZih8WLF8tHH32U0M+i3nnnHeP5dpi+aNUXr8N5+umnpbu7W6qrq2W//fZL6vbq7dPf8Z577mmEIYl+fcstt5TPP/9cjj76aHnooYck3fR+0pBGw5k33ngj4a9vvfXW8sknn8jhhx8ujz32mHjFCSecIP/4xz9kxowZsmjRokzfnJy1MMHHK6def1NBAgAAANjgoosuMl68FxcXS29v74jnf/jhh+VrX/ta5Pj1118v55577rDfo0HATTfdJP/5z3+M1Zgaluy///7Dfs+pp54qjz76qHH+J598UhL17rvvyrHHHmsc/vnPfy4/+clPxA7f//735ZFHHjEOFxYWGi94NMAZyXXXXSf33HNP5Phll10mV1999bDfc+aZZ8ry5ctlu+22k/fff9+GWw8gFzGDBAAAAMiAW265xXT81ltvjfv7amtrRQvBv/WtbxltD0O58cYbjXBEnXfeefKlL31J3GDt2rWm6pn+/n658847k7osDUz08jJtr732MqpDYrWnZAOtfNHbP2/evEzfFCBjCEgAAACANNNqhnCrTPgFtVY2aAXKSCZMmGBUm4Qv58ILLxyyZF2rWsKtMb/5zW/ELf72t79FyunD8yziDYistHXmyiuvlEzTKhZtnfnlL38p2ehnP/uZcfvd9HcCpBsBCQAAAJBmt912mwwMDEhJSYkx32D06NExq0qGcvLJJ8txxx0XuazHH3/c9HUNH77+9a9LV1eXFBUVGS0pel1uoJUvepvVUUcdJT/60Y+Mw5999pn897//TeiydB6G0svT+R4A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euiQz3VH2vRh3Uqib/zp82194bV+/XqjUvqrX/2qzJkzJ+qF1gMPPCBvvfWWcT59B/vLX/6y7LDDDkNWXetzd3XBBRcYwxuH8r3vfc+oBt9jjz2MuYLJ0LaiF198UT766CNZu3at8cZpeXm5UX2urxV23XXXYQdP6jve6umnn456F1tf8F155ZUxv1/bCXQ+or5Y3bBhgzETceedd5YjjzxSqqurR7zd+rvS+1Xfae/u7jZ+v8cee6xst9124gT9WZ955hlZsGCBtLa2Gm9A6+9y7733jntUgVYw6GXo/aw/r36v/j3psM6R6Jve2lbx0ksvyZo1G6uxxo8fbwz2POCAA4a9jES22KTyc6byO0323228tJ1F/83qfa8hid6PQ4180Pv65JNPNtrHlP6N6+tZO6pIhqKXrffxZ599ZhzXAcOebfcLOeSUU07R+evGx5IlS4Y837/+9a/I+a6//npHbsuzzz4buY5DDz3UketYuXJl5Dr0MAAAgNrmiidDUy99LPLx+uKmYe+Yv89fbjr/EX96Jek78osvvgh9+umnxucR/X7LUOiKqo2fc02afra//vWvoYqKishzQuuHz+cLHXPMMTG/d+zYscZ5Tj/99JhfD1/GpZdeGlq1alVop512irr8vLy80M9+9rPI93z00UehadOmxbwdl112Wczr0e8Jn+/ee+8d9uedMWOGcb7jjz8+qZ/p/PPPD5WUlAx5f+nH3LlzY/79xvr5rR977rln1Pc1NDSEjjvuOOM+iPU9dXV1oTvvvHPYn/v+++8PjR49Oub368/a19cXmjNnjnH86KOPDqVizZo1oWOPPXbI26sf06dPDzU3Nw/597J+/frQvHnzYn7vtttuO+Jrl1dffTU0e/bsIa9/iy22CL344otDfv9Ifwep/Jx2/E5T+XebiKeeeipUXFxsXObEiRNDixYtijpPIBAInXjiiZHrPvXUU0MDAwMpXW/434pe93C+//3vx/1v/+abb46c9/bbbw/ZIaHHKwdffztWQfLmm28anzV5G244iw6AsX6P3W699dbIYW3tAQAASIeBgZB09AVMp1WVDF9BMs7SYtNABUlW0GoCfadc6bvOX/va14wqiNLSUuNd4+XLlxvDELVSIhVacaGz9LTSIlwWr4f1+e6nn34qV1xxhWyxxRbGXAB9R1jXeF5++eXGu9W6avWv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tbfFxlxBQosNAAD2ISABAACwSX9wQAIDIccCkhoqSAAAcAwBCQAAgE16LdUjqrTI2QqSUMgcyAAAgOQUSJqsXr1ann76aVm1apUUFhbKtGnT5JBDDpHq6mpHrq+jo0Oee+45WbJkibS3t8uECRNk5syZsvfee0tBQdp+bAAA4CG9/eYBraqkIM+xgKQ/GJIuf1AqinluAwBAqhx/NG1paZELLrhA7rnnnqh3OIqLi+UHP/iBXH755UZoYofm5mb5yU9+Irfffrv09fVFfX3UqFFy8cUXyw9/+ENbrg8AAGC4ChJbW2zKo58vtXb5CUgAAHB7QKKVGwcccIC8//77xvE999xT5s2bJ36/Xx599FH5/PPP5ec//7ksXLhQ/v73v4vP50vp+vRyDjzwQFmxYoVxfPvttzeuT0MRrSj54IMP5MUXXzQ+CEgAAEC2BSSVxQVSkOczzTlp6+6XyXW2XQUAAJ7laECilRrhcOTaa6+VCy+8MPK1X//613L66afLHXfcIffdd5/st99+cuaZZyZ9Xa2trbL//vsbLTy1tbVGxcqhhx4a83wffvhh0tcDAAAQb4tNUX6e5Oel9gbQYPpmkg5qber0R05r7d50GAAAuHBI6+LFi+W2224zDh9++OGmcETl5+fLX//6V5k6dapx/IorrpBAIJD09X3ve98zwpG8vDx57LHHYoYjSsMTrSoBAABwfsWv/U+1aixzSAhIAABweUCiVSHB4MYnCeeff37M85SUlMhZZ51lHG5oaJDnn38+qetavny5UTGivv71r8see+yR9O0GAACwq8XGzvaasFrLql9tsQEAAC4OSHRjTXgQ6z777DPk+b70pS9FDj/11FNJXdedd94ZCWO+9a1vJXUZAAAAdleQ2LniN4wKEgAAsmwGyccff2x81tW6WikylG222cbop9UNN+HvSdQrr7wSCWN233134/Bbb70lr732mrHVpqamxhjYutdee0lRkbksFQAAwC59AfMMkmIbV/yGUUECAEAWBSS6vUbX+6opU6YMe14NLMaOHWu02CxdujSp69PtNGrzzTc3Zp+cdtppMn/+/KjzjR8/3hgOe8oppyR8HTrfZDhr165N+DIBAEBu6UtLiw0zSAAAyKqAJKyysnLE8+t5NCDRVbzJCIcxfX19xgDW9evXy9Zbby0HH3ywcdkLFiyQhx56yAgxvvnNb8rKlSvlsssuS+g6Jk+enNRtAwAA3pGOCpLoFhtmkAAA4NqApKenJ3I4npaWcAvO4O+LV29vb2T7zaJFiyIbcfRDW3fCvvjiCznwwAONcOTyyy83Zp/ssssuCV8fAABA/AFJOoa0suYXAADXBiRlZWWRw36/P66Qw/p98dK5I7rad2Bg4xOSfffdV6688sqo822xxRZyyy23GFUlOu/kuuuuk7///e9xX48GK8PR6pRdd9014dsPAAByR18gmIEKEgISAABcG5BUVVXFbLcZSvg88bTjWGmViH7fhg0bjOPDzRfRqhGdQ6JhxosvvpjQ9UyaNCnh2wYAALylr99SQVLIkFYAADy95lcDi9GjRxuHly9fPux5dW5IY2OjcXjGjBlJXd/g79NKkeGEv64zT8KrgQEAALKmxabcXEHS0RuQQNB8vQAAwCUBSXh9r1q4cKF0d3cPeb7333/faHlROlg1Gdtuu23kcPiyhhL+ulaeDJ5RAgAAkBUtNqXmGSSqrYdBrQAAuDYgOeSQQ4zP/f398sILLwx5vieffDJy+NBDD03qug4//PDIYd1YM1w48tlnn0W20ujsEgAAgGzeYqMY1AoAQOocSwhOOOEEKSzc+A6HDkSNpbOzU2666aZIYLHPPvskHZCEW3puu+22IatIHnnkkUg7TzjAAQAAcG4Gif0tNkUFeVJeZL7cNlb9AgDg3oBkypQpcs455xiHn3vuuajNMjp75NRTT5U1a9YYx3/xi19Ifn7sJxE//OEPjY/f/OY3Mb9eWloqV199tXH4jTfekIsuuiiy+jfsvffek7POOiuyVviSSy6x4acEAABIb4tN7E02tNgAAODKLTZhv/71r+Wtt96S119/XX72s5/J/fffL/PmzTNW/z7xxBOyevVq43zf/va35Rvf+MaQlxMORqZOnSqXXnppzPOceeaZMn/+fLn99tuNipUHH3xQDjroIKmoqDDabp555hljKGtBQYH87W9/S3ogLAAAQCZbbFRNWaGsbuuJHGfVLwAALg9ItLLj6aeflh/96EdGK83HH39sfAxeB/zTn/5ULrzwwpSvSweu3nLLLbLddtsZYcyyZcvk5ptvNp1nl112McKTPffcM+XrAwAAyMQWG1VrqSBhBgkAAC4PSJRWcFx//fXy85//XJ5//nmjakSrOKZNmyb77befFBcXj3gZ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0cUGQW3VqlUmtVaNGjVKevTo4e+XhAZAp2goTa298cYbnS4TExNjisYp3Qa3bNni09cYrLRInhZorEn5lFrWTf2xaNEiUzBb6WfPVXFU+7RKvVCkqe6BPhZqLzExscZl2rdvLz179jT3+ZzDCnPmzKlQuNcZ3VfYp3DoNLDFixfz5gcInYK7fv16c3/48OHSp08ff78k+AETIRHUtL6AndaIsHIHq11QdH6pzjfWA7VTTjlFzj77bFP3AP6lVeW3b98uhw4dMusjKSnJ0bnEivnh2rFG9e3b19S1cUXroNir5X/55ZfSq1cvj8eGNcp3O9ATWyvoXHOtR5Genm46WWjdE61XoHPTrah9ADF1P+y07ocr+nnXef/aZUA/ew899FBAjwXvf9at+pxrF6Pp06fLnj17zHNqQFY7o2lHo+bNm1syBupu27ZtppOJ1gfRQIXWBRo6dKiMGzdOoqOjPX5r7fsF3cfrcZ8r5eug6X7h8ssv93hsBO55gQZdtKOddjOynxdoAEbPC6hBE4D8ncIC+EteXp6tWbNmJoUuNjbW/L8n3Olik5iYaHv99dct+zfA2i422rnE03ogWq/C3ZoDmm5vX/aWW25hdQaQZ5991rFunnrqKa92senWrZvt008/tfT1B3vnssaNG7uc8mLXu3dvs2xCQkLAjwXv+Pnnn83UJ103Q4cO9WoXGz3e0NpHpaWllr1+WNfFJi4uztSk85R96mzfvn2rXU63g/DwcLOsdruB/504ccKx/vTz6unUJ3e62LRu3dr2n//8x7J/A6xBBgmClqY65+TkmPtaYVw7jXhKrwLrFQO9aequZg9otWy9orBw4UJJS0uT6667Tnbu3CmPPfaYBf8K1IZG7XUqlV4t0vWjVcN1nXz22WeydOlS07lEq4w/++yz8qc//alOb+6BAwcc9zVDoDrlHy//d/CvrKws02FINW7cWC677DKPnk+zlEaPHm2uIus616vIml2mnQx036D7A716+eqrr1bbXQE1s3+ONCvM1ZQXO10X2s1E99Ga3VHbq3i+HAveodk89syRKVOmePRcug3069dPxowZIx06dDDd0LKzs02HI512occbOp5mmH7wwQdkjfmYrh/dB+sxQHJysskc0W4l3333nVkfen/q1Kmm+5Hui+tC17F2qnPn+1+PF9u2bWsyWfj+Dwy6HdjX36RJ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" + }, + "metadata": { + "image/png": { + "height": 413, + "width": 548 + } }, - "metadata": {}, "output_type": "display_data" } ], "source": [ "from matplotlib import pyplot as plt\n", "\n", - "# Plot network log likelihood\n", - "plt.plot(np.linspace(-5, 5, 2000).astype(np.float32), np.exp(net_out));\n", - "\n", - "# Plot simulation histogram\n", + "plt.plot(\n", + " np.linspace(-5, 5, 2000, dtype=np.float32),\n", + " np.exp(net_out),\n", + " label=\"JAX LAN likelihood\",\n", + ")\n", "plt.hist(\n", " sim_out[\"rts\"] * sim_out[\"choices\"],\n", " bins=100,\n", " histtype=\"step\",\n", " fill=None,\n", " density=True,\n", - ");" + " label=\"simulated choices × RT\",\n", + ")\n", + "plt.legend()\n", + "plt.show()" ] } ], "metadata": { - "kernelspec": { - "display_name": ".venv", - "language": "python", - "name": "python3" - }, "language_info": { "codemirror_mode": { "name": "ipython", @@ -519,10 +457,13 @@ "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.12.13" + "pygments_lexer": "ipython3" + }, + "marimo": { + "header": "# marimo source for the \"Train with the JAX backend\" documentation how-to.\n#\n# Regenerate the rendered docs notebook (with outputs) after editing:\n# uv run marimo export ipynb notebooks/basic_tutorial_lan_jax.py \\\n# -o docs/basic_tutorial/basic_tutorial_lan_jax.ipynb --include-outputs\n#\n# Edit interactively with: uv run marimo edit notebooks/basic_tutorial_lan_jax.py\n\n", + "marimo_version": "0.24.0" } }, "nbformat": 4, - "nbformat_minor": 2 + "nbformat_minor": 5 } diff --git a/docs/exporting_bayesflow_models.md b/docs/exporting_bayesflow_models.md index 5682466..b758ada 100644 --- a/docs/exporting_bayesflow_models.md +++ b/docs/exporting_bayesflow_models.md @@ -7,132 +7,12 @@ LANfactory's [`transform_bayesflow_to_onnx`](api/onnx.md) is the bayesflow sibling of [`transform_sbi_to_onnx`](exporting_sbi_models.md). It wraps a trained [`bayesflow`](https://github.com/bayesflow-org/bayesflow) `ContinuousApproximator` (NLE) or `RatioApproximator` (NRE) and writes a -single-trial ONNX file that HSSM's `loglik_kind="approx_differentiable"` -path can consume exactly like an sbi export. Same user gesture, same file -format, same HSSM-side loader — regardless of which training framework you -came from. - -## Installation - -```bash -pip install lanfactory[bayesflow] -``` - -The `bayesflow` extra pulls `bayesflow>=2.0.8` and `keras>=3.12`. For both -libraries side-by-side use `pip install lanfactory[all]`. - -## Critical: set the Keras backend before importing - -`torch.onnx.export` cannot trace a JAX-backed Keras model. You **must** set -`KERAS_BACKEND=torch` *before* importing keras or bayesflow: - -```python -import os -os.environ["KERAS_BACKEND"] = "torch" -# On Apple silicon, also pin to CPU — the orthogonal initializer needs -# torch.linalg.qr which MPS does not implement. -os.environ["KERAS_TORCH_DEVICE"] = "cpu" - -import bayesflow as bf # now safe -``` - -The exporter checks this and raises a clear `RuntimeError` if the backend is -anything other than `torch` at export time. - -## Quick start (NLE) - -```python -import os -os.environ["KERAS_BACKEND"] = "torch" -os.environ["KERAS_TORCH_DEVICE"] = "cpu" - -import bayesflow as bf -import keras -from bayesflow.datasets import OfflineDataset -from bayesflow.networks.inference.coupling.transforms import AffineTransform -from lanfactory.onnx import transform_bayesflow_to_onnx - -# 1. Build an ONNX-friendly ContinuousApproximator. -# NLE convention: inference_variables=x (obs), inference_conditions=θ. -approximator = bf.ContinuousApproximator( - inference_network=bf.networks.CouplingFlow( - depth=4, - subnet_kwargs={"widths": (64, 64), "activation": "silu", "dropout": None}, - permutation=None, # see Known constraints below - use_actnorm=False, - transform=AffineTransform(clamp=False), - ), - standardize="inference_variables", -) -approximator.build({ - "inference_variables": (None, x_dim), - "inference_conditions": (None, theta_dim), -}) -approximator.compile(optimizer=keras.optimizers.Adam(learning_rate=5e-4)) - -# 2. Train on your simulator output. -# `x` is observations, `theta` is parameters — numpy float32 arrays. -dataset = OfflineDataset( - data={"inference_variables": x, "inference_conditions": theta}, - batch_size=200, adapter=None, -) -approximator.fit(dataset=dataset, epochs=30, verbose=0) - -# 3. Export to a single ONNX file. -transform_bayesflow_to_onnx( - approximator, - "ddm_nle.onnx", - mode="nle", - example_theta_dim=theta_dim, - example_x_dim=x_dim, -) - -# 4. Hand it to HSSM exactly like an sbi or LAN file. -# Enable JAX x64 BEFORE HSSM imports jax: ONNX graphs carry int64 index -# tensors that JAX's default 32-bit mode silently truncates, producing wrong -# log-probs (see "Known constraints" below). -import jax - -jax.config.update("jax_enable_x64", True) - -import hssm -model = hssm.HSSM( - data=obs_data, - model="ddm", - loglik_kind="approx_differentiable", - loglik="ddm_nle.onnx", - p_outlier=0, -) -idata = model.sample(sampler="numpyro", draws=500, tune=500, chains=2) -``` - -## Quick start (NRE) - -```python -approximator = bf.RatioApproximator( - inference_network=bf.networks.MLP( - widths=(64, 64), - activation="silu", - residual=False, - dropout=None, - ), - standardize="inference_variables", -) -# NRE convention: inference_variables=θ, inference_conditions=x. -approximator.build({ - "inference_variables": (None, theta_dim), - "inference_conditions": (None, x_dim), -}) -# ... train as above with the OfflineDataset keys flipped ... - -transform_bayesflow_to_onnx( - approximator, - "ddm_nre.onnx", - mode="nre", - example_theta_dim=theta_dim, - example_x_dim=x_dim, -) -``` +single-trial ONNX file. This page owns exporter architecture support, +constraints, and numerical guarantees. Follow the runnable +[bayesflow export tutorial](tutorials/exporting_bayesflow_to_onnx.ipynb) for +installation, training, export, and cross-backend verification. HSSM owns the +downstream [ONNX likelihood contract](https://lnccbrown.github.io/HSSM/how_to/custom_onnx_likelihoods/) +and model-loading procedure. The classifier logit is `log p(x|θ)/p(x) = log p(x|θ) − log p(x)`. The θ-independent `log p(x)` term drops out under MCMC, so the raw logit is the @@ -198,23 +78,6 @@ style chains. Move pointwise transforms (log/sqrt of observations) into your simulator output and apply them externally to your HSSM data before sampling. -### 5. Enable JAX x64 in the consuming process - -Same caveat as the sbi exporter — ONNX graphs from `torch.onnx.export` carry -int64 shape/index tensors. With JAX's default 32-bit mode, those get -silently truncated to int32, producing wrong log-prob outputs. Before -importing JAX in the consuming process: - -```python -import jax -jax.config.update("jax_enable_x64", True) -``` - -HSSM's `onnx2jax` consumer sets the related -`jaxort_only_allow_initializers_as_static_args = False` flag -automatically. The x64 setting is process-wide and must be opted into by -the caller. - ## Explicitly out of scope (v1) | Excluded | Reason | @@ -237,19 +100,17 @@ The bayesflow regression tests (`tests/test_bayesflow_*_export.py`) assert: If you observe drift larger than these thresholds, please open an issue with a minimal reproducer. -## Two paths into HSSM, side by side - -| Path | Source library | Mechanism | When to use | -|---|---|---|---| -| `loglik="file.onnx"` | sbi or bayesflow | ONNX file, framework-agnostic | Portability, reproducibility, sharing trained surrogates | -| `loglik=` | bayesflow (LRE tutorial) | In-memory JAX callable | Fast iteration during model development; bayesflow-only | +## HSSM handoff -The two paths produce numerically equivalent results on the same trained -network. The ONNX path is what you'd ship; the JAX-callable path is what you'd -prototype with. +LANfactory owns the portable ONNX exporter. HSSM owns the choice between a +file-backed likelihood and an in-memory JAX callable, together with its model +configuration and sampling behavior. Continue with HSSM's rendered ONNX +contract instead of duplicating that consumer procedure here. ## Related API - [`lanfactory.onnx.transform_bayesflow_to_onnx`](api/onnx.md) — this exporter. - [`lanfactory.onnx.transform_sbi_to_onnx`](api/onnx.md) — the sbi sibling. - [`lanfactory.onnx.transform_to_onnx`](api/onnx.md) — the LAN-MLP exporter. +- [Export a bayesflow model to ONNX](tutorials/exporting_bayesflow_to_onnx.ipynb) + — the executable task guide for this reference. diff --git a/docs/exporting_sbi_models.md b/docs/exporting_sbi_models.md index 0779a57..4e163f2 100644 --- a/docs/exporting_sbi_models.md +++ b/docs/exporting_sbi_models.md @@ -2,71 +2,13 @@ LANfactory's [`transform_sbi_to_onnx`](api/onnx.md) wraps a trained [`sbi`](https://github.com/sbi-dev/sbi) estimator and writes a single-trial -ONNX file that HSSM's `loglik_kind="approx_differentiable"` path can consume -exactly like a LAN export (the artifact rules are collected in -[The ONNX likelihood contract](https://lnccbrown.github.io/HSSM/how_to/custom_onnx_likelihoods/)). Use it to bring sbi-trained NLE density estimators -or NRE ratio classifiers into a [HSSM](https://github.com/lnccbrown/HSSM) model. - -## Installation - -```bash -pip install lanfactory[all] -``` - -The `all` extra pulls `sbi>=0.26` and `nflows>=0.14` in addition to LANfactory's -other optional integrations. - -## Quick start (NLE) - -```python -import torch -from sbi.inference import NLE_A -from sbi.utils import BoxUniform -from lanfactory.onnx import transform_sbi_to_onnx - -# 1. Train a likelihood estimator (your simulator + prior here). -prior = BoxUniform(low=torch.tensor([-3.0, -3.0]), high=torch.tensor([3.0, 3.0])) -inference = NLE_A(prior=prior, density_estimator="maf") -theta = prior.sample((5_000,)) -x = my_simulator(theta) # shape: (5000, x_dim) -estimator = inference.append_simulations(theta, x).train() - -# 2. Export to a HSSM-compatible ONNX file. -transform_sbi_to_onnx( - estimator, - "ddm_nle.onnx", - mode="nle", - example_theta_dim=theta.shape[-1], - example_x_dim=x.shape[-1], -) - -# 3. Hand it to HSSM exactly like a LAN file. -import hssm -model = hssm.HSSM( - data=obs_data, - model="ddm", - model_config=my_model_config, - loglik_kind="approx_differentiable", - loglik="ddm_nle.onnx", - p_outlier=0, -) -idata = model.sample(sampler="numpyro", draws=500, tune=500, chains=2) -``` - -## Quick start (NRE) - -```python -from sbi.inference import NRE_A -inference = NRE_A(prior=prior) -classifier = inference.append_simulations(theta, x).train() -transform_sbi_to_onnx( - classifier, - "ddm_nre.onnx", - mode="nre", - example_theta_dim=theta.shape[-1], - example_x_dim=x.shape[-1], -) -``` +ONNX file that satisfies the same single-trial artifact contract as a LAN +export. This page owns exporter architecture support, constraints, and +numerical guarantees. Follow the runnable +[sbi export tutorial](tutorials/exporting_sbi_to_onnx.ipynb) for installation, +training, export, and cross-backend verification. HSSM owns the downstream +[ONNX likelihood contract](https://lnccbrown.github.io/HSSM/how_to/custom_onnx_likelihoods/) +and model-loading procedure. The classifier logit is `log p(x, θ) / p(x) p(θ) = log p(x | θ) − log p(x)`. The θ-independent `log p(x)` term drops out under MCMC and under HSSM's posterior @@ -96,7 +38,7 @@ clear `ValueError`. If you encounter an unsupported architecture, please open an ## Known constraints -Three constraints arose during validation and apply to anyone training their +Two constraints arose during validation and apply to anyone training their own sbi estimators for export: 1. **For NLE with `density_estimator="maf"`, use ≥2D for both θ and x.** A 1D @@ -122,21 +64,6 @@ own sbi estimators for export: ) ``` -3. **Enable JAX x64 before importing JAX in the consuming process.** ONNX - graphs from `torch.onnx.export` carry int64 shape/index tensors. With JAX's - default 32-bit mode, those get silently truncated to int32, producing - ~0.5-unit drift in log-prob outputs. Set: - - ```python - import jax - jax.config.update("jax_enable_x64", True) - # ...subsequent imports of jaxonnxruntime, hssm, etc. - ``` - - HSSM's `onnx2jax` consumer sets the related `jaxort_only_allow_initializers_as_static_args = False` - flag automatically, but the x64 setting is process-wide and must be opted - into by the caller. - ## Numerical guarantees The C2–C5 regression tests assert: @@ -151,16 +78,14 @@ an issue with a minimal repro. ## Float precision -ONNX exports default to float32. PyMC defaults to float64. When sampling, either: - -- Cast at the JAX boundary, or -- Set `pytensor.config.floatX = "float32"` for the whole model. - -HSSM handles this consistently in its `approx_differentiable` path; if you're -hand-rolling a model with `pm.CustomDist` you'll need to do this yourself. +The exporter writes float32 graphs. Consumer-side dtype configuration belongs +to HSSM; follow its linked ONNX contract rather than copying a PyMC boundary +recipe from this exporter reference. ## Related API - [`lanfactory.onnx.transform_sbi_to_onnx`](api/onnx.md) — the exporter. - [`lanfactory.onnx.transform_to_onnx`](api/onnx.md) — the LAN-MLP exporter. Same family, different network source. +- [Export an sbi model to ONNX](tutorials/exporting_sbi_to_onnx.ipynb) — the + executable task guide for this reference. diff --git a/docs/index.md b/docs/index.md index 4916d85..151a102 100755 --- a/docs/index.md +++ b/docs/index.md @@ -20,17 +20,8 @@ data, it provides dataloaders, network factories, and training loops, and exports the trained networks to ONNX so they can serve as likelihoods in [HSSM](https://lnccbrown.github.io/HSSM/). ---- - -## Ecosystem fit - -LANfactory is the network-training layer of the HSSM ecosystem: it trains -LAN/CPN/OPN networks on simulated data and exports them to ONNX in the form -that HSSM consumes as likelihoods. - -For the full map — what each package owns, how artifacts flow between them, -and which versions work together — see -[The HSSM ecosystem](https://lnccbrown.github.io/HSSM/ecosystem/). +New to the idea? Read [likelihood approximation networks and LANfactory](what_are_lans.md) +before choosing a training backend. --- @@ -47,11 +38,25 @@ and `lanfactory[bayesflow]` (ONNX export of externally trained networks), or --- +## Ecosystem fit + +LANfactory is the network-training layer of the HSSM ecosystem: it trains LAN, +CPN, OPN, and `gonogo` networks on simulated data and exports them to ONNX in +the form that HSSM consumes as likelihoods. + +For the full map — what each package owns, how artifacts flow between them, +and which versions work together — see +[The HSSM ecosystem](https://lnccbrown.github.io/HSSM/ecosystem/). + +--- + ## Quickstart Given a folder of training data files generated with [ssm-simulators](https://lnccbrown.github.io/ssm-simulators/), the minimal -PyTorch training loop is: +PyTorch training loop is shown below. This abbreviated API sketch assumes those +files already exist; the linked tutorial is the first-success path from data +generation through a trained network. ```python from pathlib import Path @@ -112,7 +117,9 @@ export guides. - [MLflow integration](using_mlflow.md) — track and compare training runs. - [HuggingFace Hub](using_huggingface.md) — upload and download trained networks. - [Exporting sbi models](exporting_sbi_models.md) and [exporting bayesflow models](exporting_bayesflow_models.md) — bring externally trained networks into HSSM. -- **API reference** — [config](api/config.md), [trainers](api/trainers.md), [onnx](api/onnx.md), [hf](api/hf.md), [utils](api/utils.md). +- **API reference** — [config](api/config.md), [trainers](api/trainers.md), + [ONNX](api/onnx.md), [network inspectors](api/network_inspectors.md), + [Hugging Face](api/hf.md), and [utilities](api/utils.md). We hope this package may be helpful in case you attempt to train [LANs](https://elifesciences.org/articles/65074) for your own research. diff --git a/docs/tutorials/exporting_bayesflow_to_onnx.ipynb b/docs/tutorials/exporting_bayesflow_to_onnx.ipynb index 0084364..cdc8a77 100644 --- a/docs/tutorials/exporting_bayesflow_to_onnx.ipynb +++ b/docs/tutorials/exporting_bayesflow_to_onnx.ipynb @@ -19,16 +19,12 @@ } }, "source": [ - "# Exporting a `bayesflow` model to ONNX for HSSM\n", + "# Export a `bayesflow` model to ONNX\n", "\n", "[`bayesflow`](https://github.com/bayesflow-org/bayesflow) trains amortized\n", "neural likelihood (`ContinuousApproximator`) and ratio (`RatioApproximator`)\n", "estimators. LANfactory's `transform_bayesflow_to_onnx` exports them to the\n", - "same single-trial ONNX graph HSSM consumes from any source:\n", - "\n", - "```python\n", - "hssm.HSSM(loglik=\"model.onnx\", loglik_kind=\"approx_differentiable\")\n", - "```\n", + "same single-trial ONNX graph HSSM consumes from any source.\n", "\n", "This is the bayesflow sibling of the [sbi tutorial](../exporting_sbi_to_onnx/):\n", "same **train → export → verify** loop, with bayesflow's Keras-backed specifics.\n", @@ -202,8 +198,10 @@ "\n", "`transform_bayesflow_to_onnx` bakes the standardizer's accumulated mean/std\n", "into the graph as constants (sidestepping the dynamic-shape ops the live Keras\n", - "layer would emit) and writes a **rank-1** single-trial graph, opset 17 — the\n", - "same contract as the sbi and LAN exporters, so HSSM consumes it identically." + "layer would emit) and writes a **rank-1** single-trial graph, opset 17. Rank\n", + "1 is an exporter detail: LAN graphs may instead use a concrete `(1, D)`\n", + "input. Fully concrete single-trial dimensions are the shared contract, so\n", + "HSSM consumes both forms identically." ] }, { @@ -306,7 +304,7 @@ { "data": { "text/html": [ - "
**θ (parameters)**
**x (observation)**
" + "
**θ (parameters)**
**x (observation)**
" ] }, "metadata": {}, @@ -392,28 +390,15 @@ } }, "source": [ - "## 4. Consume it in HSSM\n", - "\n", - "The `.onnx` file drops into HSSM exactly like a LAN or sbi export:\n", + "## 4. Continue in HSSM\n", "\n", - "```python\n", - "import jax\n", - "jax.config.update(\"jax_enable_x64\", True) # if your HSSM version doesn't self-manage it\n", - "\n", - "import hssm\n", - "model = hssm.HSSM(\n", - " data=obs_data,\n", - " model=\"ddm\",\n", - " loglik_kind=\"approx_differentiable\",\n", - " loglik=\"ddm_bayesflow_nle.onnx\",\n", - " p_outlier=0,\n", - ")\n", - "idata = model.sample(sampler=\"numpyro\", draws=500, tune=500, chains=2)\n", - "```\n", + "LANfactory owns training, export, and cross-runtime verification. HSSM owns\n", + "model configuration and sampling with the resulting file. Continue with\n", + "HSSM's [single-trial ONNX contract](https://lnccbrown.github.io/HSSM/how_to/custom_onnx_likelihoods/)\n", + "and [BayesFlow NLE integration tutorial](https://lnccbrown.github.io/HSSM/tutorials/bayesflow_nle_onnx_integration/)\n", + "for model configuration and sampling.\n", "\n", - "For the consumption side end to end, see HSSM's\n", - "[Build HSSM models starting from ONNX files](https://github.com/lnccbrown/HSSM/blob/main/docs/tutorials/blackbox_contribution_onnx_example.ipynb)\n", - "tutorial. The **NRE** path is analogous: train a `RatioApproximator` and pass\n", + "The **NRE** export path is analogous: train a `RatioApproximator` and pass\n", "`mode=\"nre\"` to `transform_bayesflow_to_onnx`." ] } diff --git a/docs/tutorials/exporting_sbi_to_onnx.ipynb b/docs/tutorials/exporting_sbi_to_onnx.ipynb index d964e46..3e66b97 100644 --- a/docs/tutorials/exporting_sbi_to_onnx.ipynb +++ b/docs/tutorials/exporting_sbi_to_onnx.ipynb @@ -19,17 +19,12 @@ } }, "source": [ - "# Exporting an `sbi` model to ONNX for HSSM\n", + "# Export an `sbi` model to ONNX\n", "\n", "[`sbi`](https://github.com/sbi-dev/sbi) trains neural likelihood (NLE) and\n", "ratio (NRE) estimators. HSSM can use them as differentiable likelihoods —\n", "*if* they are exported to a single-trial ONNX graph. LANfactory's\n", - "`transform_sbi_to_onnx` does exactly that, so the user gesture into HSSM is\n", - "identical to a native LAN file:\n", - "\n", - "```python\n", - "hssm.HSSM(loglik=\"model.onnx\", loglik_kind=\"approx_differentiable\")\n", - "```\n", + "`transform_sbi_to_onnx` does exactly that.\n", "\n", "This notebook runs the full **train → export → verify** loop end to end on\n", "a tiny toy, then points you at HSSM for the consumption side. For the\n", @@ -114,29 +109,6 @@ "id": "PKri", "metadata": {}, "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\r", - " Training neural network. Epochs trained: 1\r", - " Training neural network. Epochs trained: 2\r", - " Training neural network. Epochs trained: 3\r", - " Training neural network. Epochs trained: 4\r", - " Training neural network. Epochs trained: 5\r", - " Training neural network. Epochs trained: 6\r", - " Training neural network. Epochs trained: 7\r", - " Training neural network. Epochs trained: 8\r", - " Training neural network. Epochs trained: 9\r", - " Training neural network. Epochs trained: 10\r", - " Training neural network. Epochs trained: 11\r", - " Training neural network. Epochs trained: 12\r", - " Training neural network. Epochs trained: 13\r", - " Training neural network. Epochs trained: 14\r", - " Training neural network. Epochs trained: 15\r", - " Training neural network. Epochs trained: 16" - ] - }, { "data": { "text/html": [ @@ -505,7 +477,11 @@ " low=torch.full((THETA_DIM,), -3.0),\n", " high=torch.full((THETA_DIM,), 3.0),\n", ")\n", - "_inference = NLE_A(prior=prior, density_estimator=\"maf\")\n", + "_inference = NLE_A(\n", + " prior=prior,\n", + " density_estimator=\"maf\",\n", + " show_progress_bars=False,\n", + ")\n", "_theta = prior.sample((2000,))\n", "_x = _theta + torch.randn_like(_theta) # x | θ ~ N(θ, I)\n", "\n", @@ -529,8 +505,10 @@ "## 2. Export to ONNX\n", "\n", "`transform_sbi_to_onnx` wraps the trained estimator into a **rank-1**\n", - "single-trial graph (parameters first, observations second; opset 17). The\n", - "rank-1 contract is what lets HSSM `vmap` the graph over trials." + "single-trial graph (parameters first, observations second; opset 17). Rank\n", + "1 is specific to this exporter; the shared contract is that every input\n", + "dimension is concrete. HSSM then `vmap`s the compliant single-trial graph\n", + "over trials." ] }, { @@ -616,7 +594,7 @@ { "data": { "text/html": [ - "
**θ (parameters)**
**x (observation)**
" + "
**θ (parameters)**
**x (observation)**
" ] }, "metadata": {}, @@ -713,32 +691,17 @@ } }, "source": [ - "## 4. Consume it in HSSM\n", - "\n", - "The `.onnx` file drops into HSSM exactly like a LAN export — HSSM handles\n", - "the `vmap` over trials and (recent versions) the x64 flag:\n", + "## 4. Continue in HSSM\n", "\n", - "```python\n", - "import jax\n", - "jax.config.update(\"jax_enable_x64\", True) # if your HSSM version doesn't self-manage it\n", - "\n", - "import hssm\n", - "model = hssm.HSSM(\n", - " data=obs_data, # DataFrame with rt / response columns\n", - " model=\"ddm\",\n", - " loglik_kind=\"approx_differentiable\",\n", - " loglik=\"ddm_nle.onnx\",\n", - " p_outlier=0,\n", - ")\n", - "idata = model.sample(sampler=\"numpyro\", draws=500, tune=500, chains=2)\n", - "```\n", + "LANfactory owns training, export, and cross-runtime verification. HSSM owns\n", + "model configuration and sampling with the resulting file. Continue with\n", + "HSSM's [single-trial ONNX contract](https://lnccbrown.github.io/HSSM/how_to/custom_onnx_likelihoods/)\n", + "and [sbi NRE integration tutorial](https://lnccbrown.github.io/HSSM/tutorials/sbi_nre_integration/)\n", + "for model configuration and sampling.\n", "\n", - "For the consumption side end to end — defining the likelihood, building the\n", - "model, sampling — see HSSM's\n", - "[Build HSSM models starting from ONNX files](https://github.com/lnccbrown/HSSM/blob/main/docs/tutorials/blackbox_contribution_onnx_example.ipynb)\n", - "tutorial. The **NRE** path is identical: train an sbi NRE ratio classifier\n", - "(e.g. `NRE_A`, `NRE_B`, `NRE_C`, or `BNRE`) and pass `mode=\"nre\"` to\n", - "`transform_sbi_to_onnx`." + "The **NRE** export path is identical: train an sbi NRE ratio classifier\n", + "(for example `NRE_A`, `NRE_B`, `NRE_C`, or `BNRE`) and pass `mode=\"nre\"`\n", + "to `transform_sbi_to_onnx`." ] } ], diff --git a/docs/using_huggingface.md b/docs/using_huggingface.md index d874831..5dc0eb7 100644 --- a/docs/using_huggingface.md +++ b/docs/using_huggingface.md @@ -1,118 +1,85 @@ -# Share trained networks on HuggingFace Hub +# Share trained networks on Hugging Face Hub -LANfactory provides CLI commands for uploading trained models to and downloading models from HuggingFace Hub. +Use LANfactory's Hub commands to publish a trained artifact set, make its +canonical ONNX file discoverable by released HSSM versions, or retrieve the +folder for inspection and local reuse. -## Installation +!!! info "Execution status" -HuggingFace support requires the optional `hf` dependencies: + Package tests exercise placement, manifest, overwrite, and download logic + against a fake Hub. Documentation CI never authenticates, uploads, or + downloads. Preview every publication locally with `--dry-run`, then verify + the real commit and root alias in the target repository. -```bash -pip install lanfactory[hf] -``` +## Install and authenticate -Or install all optional dependencies: +Install the optional Hub dependency: ```bash -pip install lanfactory[all] +pip install 'lanfactory[hf]' ``` -## Authentication - -Before uploading, authenticate with HuggingFace: - -```bash -# Option 1: Login interactively -huggingface-cli login - -# Option 2: Set environment variable -export HF_TOKEN="your_token_here" +From a source checkout, use `uv sync --extra hf` instead. Authenticate with +the Hugging Face CLI or provide `HF_TOKEN` through your shell or secret manager. +Avoid putting a token directly in a shared command or shell history. -# Option 3: Pass token via CLI -upload-hf ... --token "your_token_here" -``` +## Preview the publication -## Uploading Models - -### 1. Create a `model_card.yaml` file - -In your trained model folder, create a `model_card.yaml` file with model metadata: - -```yaml -# Required metadata (HuggingFace frontmatter) -tags: - - lan - - ssm - - ddm - - hssm -library_name: onnx -license: mit - -# Model information -title: "LAN Model for DDM" -description: "Likelihood Approximation Network trained on DDM (Drift Diffusion Model) simulations." - -# Optional: Network architecture (auto-extracted from network_config.pickle / train_config.pickle if not provided) -architecture: - layer_sizes: [100, 100, 1] - activations: [tanh, tanh, linear] - network_type: lan - -# Optional: Training details -training: - epochs: 20 - optimizer: adam - learning_rate: 0.001 - -# Usage example (shown in README) -usage_example: | - import hssm - model = hssm.HSSM(data=my_data, model="ddm", loglik_kind="approx_differentiable") -``` - -### 2. Upload using the CLI +Point `upload-hf` at one trained-model folder. The accepted network types are +`lan`, `cpn`, `opn`, and `gonogo`. ```bash upload-hf \ --model-folder ./networks/lan/ddm/ \ --network-type lan \ --model-name ddm \ - --commit-message "Initial upload" + --dry-run ``` -This uploads to `franklab/HSSM` (default) at path `lan/ddm/`. +By default, LANfactory plans three coordinated placements: -### CLI Options +1. the complete artifact set under `lan/ddm/`; +2. the canonical ONNX file at the repository root as `ddm.onnx`; and +3. an updated root `manifest.json` entry. -| Option | Required | Description | -|--------|----------|-------------| -| `--model-folder` | Yes | Path to folder with trained model artifacts | -| `--network-type` | Yes | Network type: `lan`, `cpn`, or `opn` | -| `--model-name` | Yes | Model name (e.g., `ddm`, `angle`) | -| `--repo-id` | No | HuggingFace repo ID (default: `franklab/HSSM`) | -| `--commit-message` | No | Git commit message (default: "Upload model") | -| `--private` | No | Create a private repository | -| `--create-repo` | No | Create repository if it doesn't exist | -| `--include-patterns` | No | Comma-separated glob patterns to include | -| `--exclude-patterns` | No | Comma-separated glob patterns to exclude | -| `--revision` | No | Branch or tag name for versioning | -| `--token` | No | HuggingFace API token | -| `--dry-run` | No | Show what would be uploaded without uploading | +The other root aliases are `{model}_cpn.onnx`, `{model}_opn.onnx`, and +`{model}_gonogo.onnx`. Released HSSM versions resolve these root filenames; +publishing only the folder does not make a network consumable by HSSM. -### Dry Run +If the folder has no unambiguous ONNX filename for the requested model, pass +`--canonical-onnx PATH`. A `model_card.yaml` is optional by default: LANfactory +can generate one from the saved configuration, using the artifact repository's +`bsd-2-clause` license metadata. Use `--require-model-card` when publication +policy requires a reviewed, hand-authored card. -To preview what will be uploaded without actually uploading: +## Publish and verify + +Remove `--dry-run` only after the plan identifies the intended artifact and +root filename: ```bash upload-hf \ --model-folder ./networks/lan/ddm/ \ --network-type lan \ --model-name ddm \ - --dry-run + --commit-message "Publish validated DDM network" ``` -## Downloading Models +The command refuses to replace an existing root network unless you explicitly +pass `--overwrite-root`. That guard is consequential: released HSSM versions +download the root file from `main` without pinning a revision. Test a candidate +in a staging repository and complete its validation before authorizing a root +replacement. -### Download using the CLI +After upload, verify that the Hub commit contains the folder, root alias, and +manifest update together. `--no-publish-root-alias` and +`--no-update-manifest` are specialized escape hatches; do not use them for a +normal HSSM-facing publication. + +## Retrieve a published folder + +`download-hf` copies the selected `{network-type}/{model-name}/` folder into a +local directory: ```bash download-hf \ @@ -121,79 +88,16 @@ download-hf \ --output-folder ./models/ddm/ ``` -This downloads from `franklab/HSSM` at path `lan/ddm/`. - -### CLI Options - -| Option | Required | Description | -|--------|----------|-------------| -| `--network-type` | Yes | Network type: `lan`, `cpn`, or `opn` | -| `--model-name` | Yes | Model name (e.g., `ddm`, `angle`) | -| `--output-folder` | Yes | Local destination folder | -| `--repo-id` | No | HuggingFace repo ID (default: `franklab/HSSM`) | -| `--revision` | No | Branch, tag, or commit to download (default: main) | -| `--include-patterns` | No | Comma-separated glob patterns to include | -| `--exclude-patterns` | No | Comma-separated glob patterns to exclude | -| `--token` | No | HuggingFace API token (for private repos) | -| `--force` | No | Overwrite existing files | - -## Repository Structure - -Models are organized in the repository using the following structure: - -``` -franklab/HSSM/ -├── lan/ -│ ├── ddm/ -│ │ ├── model.onnx -│ │ ├── network_config.pickle -│ │ ├── train_config.pickle -│ │ └── README.md -│ ├── angle/ -│ │ └── ... -│ └── weibull/ -│ └── ... -├── cpn/ -│ └── ... -└── opn/ - └── ... -``` - -## Using Downloaded Models with HSSM - -After downloading a model, you can use it with HSSM: +An existing destination is rejected unless `--force` is set. The download +command does not reconfigure HSSM. To use a downloaded ONNX file directly, +pass its local path through HSSM's +[approximate-differentiable ONNX route](https://lnccbrown.github.io/HSSM/how_to/custom_onnx_likelihoods/). +When a validated root alias is published in `franklab/HSSM`, HSSM's built-in +model configuration can resolve it from the Hub instead. -```python -import hssm +## Inspect the exact interface -# HSSM will look for models in the franklab/HSSM repository -model = hssm.HSSM( - data=my_data, - model="ddm", - loglik_kind="approx_differentiable" -) -``` - -## Programmatic Usage - -You can also use the upload/download functions directly in Python: - -```python -from pathlib import Path -from lanfactory.hf import upload_model, download_model - -# Upload -upload_model( - model_folder=Path("./networks/lan/ddm/"), - network_type="lan", - model_name="ddm", - commit_message="v1.0.0 release", -) - -# Download -download_model( - network_type="lan", - model_name="ddm", - output_folder=Path("./models/ddm/"), -) -``` +Run `upload-hf --help` and `download-hf --help` for the installed version. The +[command-line reference](api/cli.md#upload-hf) records every flag and safety +default. The [Hub Python API reference](api/hf.md) documents the public helpers +and constants owned by LANfactory. diff --git a/docs/what_are_lans.md b/docs/what_are_lans.md index 2f89265..eca105b 100644 --- a/docs/what_are_lans.md +++ b/docs/what_are_lans.md @@ -1,4 +1,4 @@ -# What LANs are, and where LANfactory fits +# Likelihood approximation networks and LANfactory Many sequential sampling models (SSMs) have no closed-form likelihood. You can *simulate* from them cheaply, but you cannot write down the density that diff --git a/mkdocs.yml b/mkdocs.yml index 71c2e33..d49aa87 100755 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -18,15 +18,17 @@ nav: - Track training runs with MLflow: using_mlflow.md - Share trained networks on HuggingFace Hub: using_huggingface.md - Explanations: - - What LANs are, and where LANfactory fits: what_are_lans.md + - Likelihood approximation networks and LANfactory: what_are_lans.md - Reference: - "Network types: LAN, CPN, OPN": network_types.md - sbi export — architectures and constraints: exporting_sbi_models.md - bayesflow export — architectures and constraints: exporting_bayesflow_models.md + - Command-line interface: api/cli.md - API: - config: api/config.md - hf: api/hf.md - onnx: api/onnx.md + - network_inspectors: api/network_inspectors.md - trainers: api/trainers.md - utils: api/utils.md diff --git a/notebooks/basic_tutorial_lan_jax.py b/notebooks/basic_tutorial_lan_jax.py new file mode 100644 index 0000000..402f282 --- /dev/null +++ b/notebooks/basic_tutorial_lan_jax.py @@ -0,0 +1,317 @@ +# marimo source for the "Train with the JAX backend" documentation how-to. +# +# Regenerate the rendered docs notebook (with outputs) after editing: +# uv run marimo export ipynb notebooks/basic_tutorial_lan_jax.py \ +# -o docs/basic_tutorial/basic_tutorial_lan_jax.ipynb --include-outputs +# +# Edit interactively with: uv run marimo edit notebooks/basic_tutorial_lan_jax.py + +import marimo + +__generated_with = "0.24.0" +app = marimo.App() + + +@app.cell +def _(): + import marimo as mo + + return (mo,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + # Train with the JAX backend + + This is the JAX/Flax companion to [Train your first LAN + (PyTorch)](../basic_tutorial_lan_torch/). Complete that learning tutorial + first: it owns the explanation of LAN training data, configuration, and the + end-to-end workflow. Here we repeat a deliberately tiny data fixture so this + how-to remains executable, then focus on the JAX-specific factory, trainer, + saved state, and inference call. + + The fixture uses [`ssm-simulators`](https://lnccbrown.github.io/ssm-simulators/) + to generate DDM data. LANfactory can train on compatible data from other + generators as well. + """) + return + + +@app.cell +def _(): + from copy import deepcopy + from pathlib import Path + + import lanfactory + import numpy as np + import ssms + + return Path, deepcopy, lanfactory, np, ssms + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## Generate a small training fixture + + These settings make two small files so the example runs quickly. For real + training, choose the simulator budget and parameter coverage described in + the PyTorch learning tutorial and the + [`ssm-simulators` data-generation documentation](https://lnccbrown.github.io/ssm-simulators/). + """) + return + + +@app.cell +def _(Path, deepcopy, ssms): + MODEL = "ddm" + OUT_FOLDER = Path("jax_nb_data") / "training_data" + MODEL_FOLDER = Path("jax_nb_data") / "jax_models" / "lan" + N_DATA_FILES = 2 + BATCH_SIZE = 1000 + OUT_FOLDER.mkdir(parents=True, exist_ok=True) + MODEL_FOLDER.mkdir(parents=True, exist_ok=True) + + generator_config = ssms.config.get_default_generator_config("lan") + generator_config["model"] = MODEL + generator_config["pipeline"]["n_parameter_sets"] = 100 + generator_config["pipeline"]["n_cpus"] = 1 + generator_config["simulator"]["n_samples"] = 200 + generator_config["training"]["n_samples_per_param"] = 200 + generator_config["output"]["folder"] = str(OUT_FOLDER) + + model_config = deepcopy(ssms.config.model_config[MODEL]) + return ( + BATCH_SIZE, + MODEL, + MODEL_FOLDER, + N_DATA_FILES, + OUT_FOLDER, + generator_config, + model_config, + ) + + +@app.cell +def _(N_DATA_FILES, generator_config, model_config, ssms): + for _i in range(N_DATA_FILES): + print(f"Generating data file {_i + 1} / {N_DATA_FILES}") + _generator = ssms.dataset_generators.lan_mlp.TrainingDataGenerator( + config=generator_config, + model_config=model_config, + ) + _generator.generate_data_training(save=True) + return + + +@app.cell +def _(deepcopy, lanfactory): + network_config = deepcopy(lanfactory.config.network_configs.network_config_mlp) + network_config["layer_sizes"] = [100, 100, 100, 1] + network_config["activations"] = ["tanh", "tanh", "tanh", "linear"] + print("Network config:") + print(network_config) + + train_config = deepcopy(lanfactory.config.network_configs.train_config_mlp) + train_config["learning_rate"] = 2e-6 + train_config["cpu_batch_size"] = 4096 + train_config["gpu_batch_size"] = 4096 + train_config["n_epochs"] = 2 + print("Train config:") + print(train_config) + return network_config, train_config + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## Reuse the shared data loader + + `make_train_valid_dataloaders` is backend-neutral at this boundary. It: + + - splits your data files into training and validation sets; + - creates the appropriate `DatasetTorch` objects; and + - wraps them in PyTorch `DataLoader` objects with sensible defaults. + + The loaders yield NumPy-compatible batches that the JAX trainer consumes. + """) + return + + +@app.cell +def _(BATCH_SIZE, OUT_FOLDER, lanfactory): + file_list_ = sorted(OUT_FOLDER.glob("*.pickle")) + + # num_workers=0 keeps data loading in-process: ssm-simulators sets the + # multiprocessing start method to "spawn" at import, which is unsafe for + # DataLoader worker processes inside a notebook. + jax_training_dataloader, jax_validation_dataloader, input_dim = lanfactory.trainers.make_train_valid_dataloaders( + file_ids=file_list_, + batch_size=BATCH_SIZE, + network_type="lan", + train_val_split=0.5, + num_workers=0, + pin_memory=False, + ) + + print(f"Training batches: {len(jax_training_dataloader)}") + print(f"Validation batches: {len(jax_validation_dataloader)}") + print(f"Input dimension: {input_dim}") + return jax_training_dataloader, jax_validation_dataloader + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## Create the JAX/Flax network + + `JaxMLPFactory` materializes the configured Flax MLP. Passing `train=True` + prepares it for the training-state lifecycle used by `ModelTrainerJaxMLP`. + """) + return + + +@app.cell +def _(lanfactory, network_config): + jax_net = lanfactory.trainers.JaxMLPFactory( + network_config=network_config, + train=True, + ) + return (jax_net,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## Train and save the JAX state + + The JAX trainer takes the same configuration and loaders as the PyTorch + path, but writes a Flax training-state artifact rather than a Torch model. + """) + return + + +@app.cell +def _( + jax_net, + jax_training_dataloader, + jax_validation_dataloader, + lanfactory, + train_config, +): + jax_trainer = lanfactory.trainers.ModelTrainerJaxMLP( + train_config=train_config, + model=jax_net, + train_dl=jax_training_dataloader, + valid_dl=jax_validation_dataloader, + pin_memory=False, + ) + return (jax_trainer,) + + +@app.cell +def _(MODEL, MODEL_FOLDER, jax_trainer): + train_state = jax_trainer.train_and_evaluate( + output_folder=MODEL_FOLDER, + output_file_id=MODEL, + run_id="jax", + mlflow_on=False, + verbose=1, + save_outputs=True, + ) + return (train_state,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## Reload the state for inference + + Recreate the factory with `train=False`, load the saved state, and request a + JIT-compiled forward function. The input width is the model parameter count + plus reaction time and response. + """) + return + + +@app.cell +def _(lanfactory, network_config): + jax_infer = lanfactory.trainers.JaxMLPFactory( + network_config=network_config, + train=False, + ) + return (jax_infer,) + + +@app.cell +def _(MODEL, MODEL_FOLDER, jax_infer, model_config, train_state): + # Establish the reactive dependency before reloading the saved state. + _ = train_state + _forward_pass, forward_pass_jitted = jax_infer.make_forward_partial( + seed=42, + input_dim=model_config["n_params"] + 2, + state=str(MODEL_FOLDER / f"jax_lan_{MODEL}__train_state.jax"), + add_jitted=True, + ) + return (forward_pass_jitted,) + + +@app.cell +def _(MODEL, deepcopy, forward_pass_jitted, np, ssms): + import jax.numpy as jnp + + theta = deepcopy(ssms.config.model_config[MODEL]["default_params"]) + sim_out = ssms.basic_simulators.simulator.simulator( + model=MODEL, + theta=theta, + n_samples=50_000, + ) + input_mat = jnp.zeros((2000, len(theta) + 2)) + for _i in range(len(theta)): + input_mat = input_mat.at[:, _i].set(jnp.ones(2000) * theta[_i]) + input_mat = input_mat.at[:, len(theta)].set( + jnp.array( + np.concatenate( + [ + np.linspace(5, 0, 1000, dtype=np.float32), + np.linspace(0, 5, 1000, dtype=np.float32), + ] + ) + ) + ) + input_mat = input_mat.at[:, len(theta) + 1].set( + jnp.array( + np.concatenate( + [np.repeat(-1.0, 1000), np.repeat(1.0, 1000)] + ).astype(np.float32) + ) + ) + net_out = forward_pass_jitted(input_mat) + return net_out, sim_out + + +@app.cell +def _(net_out, np, sim_out): + from matplotlib import pyplot as plt + + plt.plot( + np.linspace(-5, 5, 2000, dtype=np.float32), + np.exp(net_out), + label="JAX LAN likelihood", + ) + plt.hist( + sim_out["rts"] * sim_out["choices"], + bins=100, + histtype="step", + fill=None, + density=True, + label="simulated choices × RT", + ) + plt.legend() + plt.show() + return + + +if __name__ == "__main__": + app.run() diff --git a/notebooks/exporting_bayesflow_to_onnx.py b/notebooks/exporting_bayesflow_to_onnx.py index 79a75d7..5a9e2d0 100644 --- a/notebooks/exporting_bayesflow_to_onnx.py +++ b/notebooks/exporting_bayesflow_to_onnx.py @@ -22,16 +22,12 @@ def _(): @app.cell def _(mo): mo.md(r""" - # Exporting a `bayesflow` model to ONNX for HSSM + # Export a `bayesflow` model to ONNX [`bayesflow`](https://github.com/bayesflow-org/bayesflow) trains amortized neural likelihood (`ContinuousApproximator`) and ratio (`RatioApproximator`) estimators. LANfactory's `transform_bayesflow_to_onnx` exports them to the - same single-trial ONNX graph HSSM consumes from any source: - - ```python - hssm.HSSM(loglik="model.onnx", loglik_kind="approx_differentiable") - ``` + same single-trial ONNX graph HSSM consumes from any source. This is the bayesflow sibling of the [sbi tutorial](../exporting_sbi_to_onnx/): same **train → export → verify** loop, with bayesflow's Keras-backed specifics. @@ -169,8 +165,10 @@ def _(mo): `transform_bayesflow_to_onnx` bakes the standardizer's accumulated mean/std into the graph as constants (sidestepping the dynamic-shape ops the live Keras - layer would emit) and writes a **rank-1** single-trial graph, opset 17 — the - same contract as the sbi and LAN exporters, so HSSM consumes it identically. + layer would emit) and writes a **rank-1** single-trial graph, opset 17. Rank + 1 is an exporter detail: LAN graphs may instead use a concrete `(1, D)` + input. Fully concrete single-trial dimensions are the shared contract, so + HSSM consumes both forms identically. """) return @@ -290,28 +288,15 @@ def _(eval_all, mo, np, theta_ui, x_ui): @app.cell def _(mo): mo.md(r""" - ## 4. Consume it in HSSM - - The `.onnx` file drops into HSSM exactly like a LAN or sbi export: + ## 4. Continue in HSSM - ```python - import jax - jax.config.update("jax_enable_x64", True) # if your HSSM version doesn't self-manage it - - import hssm - model = hssm.HSSM( - data=obs_data, - model="ddm", - loglik_kind="approx_differentiable", - loglik="ddm_bayesflow_nle.onnx", - p_outlier=0, - ) - idata = model.sample(sampler="numpyro", draws=500, tune=500, chains=2) - ``` + LANfactory owns training, export, and cross-runtime verification. HSSM owns + model configuration and sampling with the resulting file. Continue with + HSSM's [single-trial ONNX contract](https://lnccbrown.github.io/HSSM/how_to/custom_onnx_likelihoods/) + and [BayesFlow NLE integration tutorial](https://lnccbrown.github.io/HSSM/tutorials/bayesflow_nle_onnx_integration/) + for model configuration and sampling. - For the consumption side end to end, see HSSM's - [Build HSSM models starting from ONNX files](https://github.com/lnccbrown/HSSM/blob/main/docs/tutorials/blackbox_contribution_onnx_example.ipynb) - tutorial. The **NRE** path is analogous: train a `RatioApproximator` and pass + The **NRE** export path is analogous: train a `RatioApproximator` and pass `mode="nre"` to `transform_bayesflow_to_onnx`. """) return diff --git a/notebooks/exporting_sbi_to_onnx.py b/notebooks/exporting_sbi_to_onnx.py index d2f61bf..3249cfa 100644 --- a/notebooks/exporting_sbi_to_onnx.py +++ b/notebooks/exporting_sbi_to_onnx.py @@ -22,17 +22,12 @@ def _(): @app.cell def _(mo): mo.md(r""" - # Exporting an `sbi` model to ONNX for HSSM + # Export an `sbi` model to ONNX [`sbi`](https://github.com/sbi-dev/sbi) trains neural likelihood (NLE) and ratio (NRE) estimators. HSSM can use them as differentiable likelihoods — *if* they are exported to a single-trial ONNX graph. LANfactory's - `transform_sbi_to_onnx` does exactly that, so the user gesture into HSSM is - identical to a native LAN file: - - ```python - hssm.HSSM(loglik="model.onnx", loglik_kind="approx_differentiable") - ``` + `transform_sbi_to_onnx` does exactly that. This notebook runs the full **train → export → verify** loop end to end on a tiny toy, then points you at HSSM for the consumption side. For the @@ -121,7 +116,11 @@ def _(BoxUniform, NLE_A, THETA_DIM, torch): low=torch.full((THETA_DIM,), -3.0), high=torch.full((THETA_DIM,), 3.0), ) - _inference = NLE_A(prior=prior, density_estimator="maf") + _inference = NLE_A( + prior=prior, + density_estimator="maf", + show_progress_bars=False, + ) _theta = prior.sample((2000,)) _x = _theta + torch.randn_like(_theta) # x | θ ~ N(θ, I) @@ -140,8 +139,10 @@ def _(mo): ## 2. Export to ONNX `transform_sbi_to_onnx` wraps the trained estimator into a **rank-1** - single-trial graph (parameters first, observations second; opset 17). The - rank-1 contract is what lets HSSM `vmap` the graph over trials. + single-trial graph (parameters first, observations second; opset 17). Rank + 1 is specific to this exporter; the shared contract is that every input + dimension is concrete. HSSM then `vmap`s the compliant single-trial graph + over trials. """) return @@ -252,32 +253,17 @@ def _(estimator, eval_backends, mo, np, theta_ui, torch, x_ui): @app.cell def _(mo): mo.md(r""" - ## 4. Consume it in HSSM + ## 4. Continue in HSSM - The `.onnx` file drops into HSSM exactly like a LAN export — HSSM handles - the `vmap` over trials and (recent versions) the x64 flag: + LANfactory owns training, export, and cross-runtime verification. HSSM owns + model configuration and sampling with the resulting file. Continue with + HSSM's [single-trial ONNX contract](https://lnccbrown.github.io/HSSM/how_to/custom_onnx_likelihoods/) + and [sbi NRE integration tutorial](https://lnccbrown.github.io/HSSM/tutorials/sbi_nre_integration/) + for model configuration and sampling. - ```python - import jax - jax.config.update("jax_enable_x64", True) # if your HSSM version doesn't self-manage it - - import hssm - model = hssm.HSSM( - data=obs_data, # DataFrame with rt / response columns - model="ddm", - loglik_kind="approx_differentiable", - loglik="ddm_nle.onnx", - p_outlier=0, - ) - idata = model.sample(sampler="numpyro", draws=500, tune=500, chains=2) - ``` - - For the consumption side end to end — defining the likelihood, building the - model, sampling — see HSSM's - [Build HSSM models starting from ONNX files](https://github.com/lnccbrown/HSSM/blob/main/docs/tutorials/blackbox_contribution_onnx_example.ipynb) - tutorial. The **NRE** path is identical: train an sbi NRE ratio classifier - (e.g. `NRE_A`, `NRE_B`, `NRE_C`, or `BNRE`) and pass `mode="nre"` to - `transform_sbi_to_onnx`. + The **NRE** export path is identical: train an sbi NRE ratio classifier + (for example `NRE_A`, `NRE_B`, `NRE_C`, or `BNRE`) and pass `mode="nre"` + to `transform_sbi_to_onnx`. """) return diff --git a/src/lanfactory/cli/torch_train.py b/src/lanfactory/cli/torch_train.py index dea8f60..1148eda 100644 --- a/src/lanfactory/cli/torch_train.py +++ b/src/lanfactory/cli/torch_train.py @@ -99,7 +99,7 @@ def main( autocompletion=lambda: ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], ), ): - """Train a JAX neural network using the provided configuration.""" + """Train a PyTorch neural network using the provided configuration.""" # Set up logging ------------------------------------------------ logging.basicConfig( diff --git a/src/lanfactory/cli/upload_hf.py b/src/lanfactory/cli/upload_hf.py index 849233a..e64006f 100644 --- a/src/lanfactory/cli/upload_hf.py +++ b/src/lanfactory/cli/upload_hf.py @@ -23,7 +23,7 @@ def main( model_folder: Path = typer.Option( ..., "--model-folder", - help="Path to the folder containing trained model artifacts (should contain model_card.yaml).", + help="Path to trained model artifacts; may contain model_card.yaml.", exists=True, file_okay=False, dir_okay=True, @@ -138,8 +138,10 @@ def main( This command uploads model artifacts to a HuggingFace repository at the path {network_type}/{model_name}/ (e.g., lan/ddm/). - The model folder must contain a model_card.yaml file with model metadata. - This YAML file is converted to a README.md for HuggingFace. + A model_card.yaml file is optional by default. When it is absent, LANfactory + generates metadata from the command arguments; ``--require-model-card`` + restores strict validation. Publication coordinates the network-type/model + folder, optional root alias, and repository manifest. Example: upload-hf --model-folder ./networks/lan/ddm/ --network-type lan --model-name ddm diff --git a/tests/test_docs_api_reference.py b/tests/test_docs_api_reference.py new file mode 100644 index 0000000..2e9b48f --- /dev/null +++ b/tests/test_docs_api_reference.py @@ -0,0 +1,156 @@ +"""Keep public namespace exports represented in the rendered API reference.""" + +from __future__ import annotations + +import ast +import re +import tomllib +from pathlib import Path + +ROOT = Path(__file__).parent.parent + + +def _all_exports(module_path: str) -> set[str]: + tree = ast.parse((ROOT / module_path).read_text()) + for node in tree.body: + if not isinstance(node, ast.Assign): + continue + if not any( + isinstance(target, ast.Name) and target.id == "__all__" + for target in node.targets + ): + continue + return set(ast.literal_eval(node.value)) + raise AssertionError(f"No literal __all__ found in {module_path}") + + +def _assert_documented(module_path: str, docs_path: str) -> None: + reference = (ROOT / docs_path).read_text() + missing = sorted( + name + for name in _all_exports(module_path) + if f"`{name}`" not in reference and f".{name}" not in reference + ) + assert not missing, f"{docs_path} omits public exports: {missing}" + + +def _literal_assignment(module_path: str, name: str) -> object: + tree = ast.parse((ROOT / module_path).read_text()) + for node in tree.body: + if not isinstance(node, ast.Assign): + continue + if any( + isinstance(target, ast.Name) and target.id == name + for target in node.targets + ): + return ast.literal_eval(node.value) + raise AssertionError(f"No literal assignment for {name} in {module_path}") + + +def _cli_option_flags(module_path: str) -> set[str]: + tree = ast.parse((ROOT / module_path).read_text()) + main = next( + node + for node in tree.body + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) + and node.name == "main" + ) + arguments = [*main.args.posonlyargs, *main.args.args] + defaults = [None] * (len(arguments) - len(main.args.defaults)) + list( + main.args.defaults + ) + arguments.extend(main.args.kwonlyargs) + defaults.extend(main.args.kw_defaults) + + flags: set[str] = set() + for argument, default in zip(arguments, defaults, strict=True): + if not isinstance(default, ast.Call): + continue + is_typer_option = ( + isinstance(default.func, ast.Attribute) and default.func.attr == "Option" + ) or ( + isinstance(default.func, ast.Name) + and default.func.id == "option_no_default" + ) + if not is_typer_option: + continue + explicit = { + flag + for arg in default.args + if isinstance(arg, ast.Constant) + and isinstance((value := arg.value), str) + and value.startswith("-") + for flag in value.split("/") + } + flags.update(explicit or {f"--{argument.arg.replace('_', '-')}"}) + return flags + + +def _project_scripts() -> dict[str, str]: + project = tomllib.loads((ROOT / "pyproject.toml").read_text())["project"] + return project["scripts"] + + +def _module_path(entry_point: str) -> str: + module, separator, callable_name = entry_point.partition(":") + assert separator == ":" and callable_name == "app" + return f"src/{module.replace('.', '/')}.py" + + +def _documented_command_flags(reference: str, command: str) -> set[str]: + match = re.search( + rf"^## `{re.escape(command)}`\n(?P.*?)(?=^## `|\Z)", + reference, + flags=re.MULTILINE | re.DOTALL, + ) + assert match is not None, f"docs/api/cli.md has no section for {command}" + flags: set[str] = set() + for line in match.group("body").splitlines(): + if not line.startswith("| `"): + continue + option_cell = line.split("|", maxsplit=2)[1] + for code_span in re.findall(r"`([^`]+)`", option_cell): + flag = code_span.split(maxsplit=1)[0] + if flag.startswith("-"): + flags.add(flag) + return flags + + +def test_config_exports_are_referenced() -> None: + _assert_documented("src/lanfactory/config/__init__.py", "docs/api/config.md") + + +def test_hf_exports_are_referenced() -> None: + _assert_documented("src/lanfactory/hf/__init__.py", "docs/api/hf.md") + + +def test_hf_constants_are_current() -> None: + reference = (ROOT / "docs/api/hf.md").read_text() + module = "src/lanfactory/hf/__init__.py" + assert f"`{_literal_assignment(module, 'DEFAULT_REPO_ID')}`" in reference + assert f"`{_literal_assignment(module, 'DEFAULT_LICENSE')}`" in reference + for network_type in _literal_assignment(module, "VALID_NETWORK_TYPES"): + assert f"`{network_type}`" in reference + + +def test_every_installed_cli_has_an_exact_option_reference() -> None: + reference = (ROOT / "docs/api/cli.md").read_text() + scripts = _project_scripts() + documented_commands = set(re.findall(r"^## `([^`]+)`$", reference, re.MULTILINE)) + assert documented_commands == set(scripts) + + for command, entry_point in scripts.items(): + source_flags = _cli_option_flags(_module_path(entry_point)) + documented_flags = _documented_command_flags(reference, command) + assert documented_flags == source_flags, ( + f"docs/api/cli.md flags drifted for {command}: " + f"missing={sorted(source_flags - documented_flags)}, " + f"extra={sorted(documented_flags - source_flags)}" + ) + + +def test_network_inspector_exports_are_referenced() -> None: + _assert_documented( + "src/lanfactory/network_inspectors/__init__.py", + "docs/api/network_inspectors.md", + ) diff --git a/tests/test_notebooks.py b/tests/test_notebooks.py index 276b764..5467e48 100644 --- a/tests/test_notebooks.py +++ b/tests/test_notebooks.py @@ -4,16 +4,17 @@ uv run pytest tests/test_notebooks.py --run-notebooks uv run pytest tests/test_notebooks.py --run-notebooks -k lan_torch -Two tutorial formats are covered: -- The Jupyter basic tutorials under ``docs/basic_tutorial`` are executed with - ``jupyter nbconvert --execute``. -- The marimo export tutorials under ``notebooks`` are executed by exporting them - to an ipynb (``marimo export ipynb``), which runs the whole notebook. +Two tutorial surfaces are covered: +- Every committed rendered notebook is executed with ``jupyter nbconvert``. +- Every canonical marimo source under ``notebooks`` is executed by exporting it + to ipynb (``marimo export ipynb``), which runs the whole notebook. Every notebook is executed inside a throwaway working directory so the training data / model artifacts they generate never land in the repository. """ +import json +import re import subprocess import sys import tempfile @@ -23,6 +24,7 @@ PROJECT_ROOT = Path(__file__).parent.parent BASIC_TUTORIAL_DIR = PROJECT_ROOT / "docs" / "basic_tutorial" +EXPORTED_TUTORIAL_DIR = PROJECT_ROOT / "docs" / "tutorials" MARIMO_DIR = PROJECT_ROOT / "notebooks" # Timeout for a single notebook (seconds). The tutorials generate a small amount @@ -30,20 +32,34 @@ NOTEBOOK_TIMEOUT = 1200 BASIC_NOTEBOOKS = sorted(BASIC_TUTORIAL_DIR.glob("*.ipynb")) +EXPORTED_NOTEBOOKS = sorted(EXPORTED_TUTORIAL_DIR.glob("*.ipynb")) # Fail loudly if discovery finds nothing (e.g. the dir was moved/renamed) rather # than parametrizing over an empty set and silently skipping the whole suite. assert BASIC_NOTEBOOKS, ( f"No basic-tutorial notebooks discovered in {BASIC_TUTORIAL_DIR}" ) -MARIMO_NOTEBOOKS = [ - MARIMO_DIR / "exporting_sbi_to_onnx.py", - MARIMO_DIR / "exporting_bayesflow_to_onnx.py", -] +assert EXPORTED_NOTEBOOKS, ( + f"No exported tutorial notebooks discovered in {EXPORTED_TUTORIAL_DIR}" +) +RENDERED_NOTEBOOKS = BASIC_NOTEBOOKS + EXPORTED_NOTEBOOKS +MARIMO_EXPORTS = { + MARIMO_DIR / "basic_tutorial_lan_jax.py": BASIC_TUTORIAL_DIR + / "basic_tutorial_lan_jax.ipynb", + MARIMO_DIR / "exporting_sbi_to_onnx.py": EXPORTED_TUTORIAL_DIR + / "exporting_sbi_to_onnx.ipynb", + MARIMO_DIR / "exporting_bayesflow_to_onnx.py": EXPORTED_TUTORIAL_DIR + / "exporting_bayesflow_to_onnx.ipynb", +} +MARIMO_NOTEBOOKS = list(MARIMO_EXPORTS) assert all(nb.exists() for nb in MARIMO_NOTEBOOKS), ( f"Missing marimo tutorial source(s): " f"{[str(nb) for nb in MARIMO_NOTEBOOKS if not nb.exists()]}" ) +LOCAL_OUTPUT_PATH = re.compile( + r"(?:/Users/|/home/|/private/var/folders/|/var/folders/|[A-Za-z]:[\\\\/]Users[\\\\/])" +) + def _run(cmd: list[str]) -> tuple[bool, str]: """Run ``cmd`` in a throwaway working directory; return (success, output).""" @@ -65,12 +81,37 @@ def _run(cmd: list[str]) -> tuple[bool, str]: ) +@pytest.mark.parametrize( + "notebook_path", + RENDERED_NOTEBOOKS, + ids=[nb.stem for nb in RENDERED_NOTEBOOKS], +) +def test_committed_notebook_outputs_are_portable(notebook_path: Path): + """Keep terminal progress noise and machine-local paths out of public output.""" + notebook = json.loads(notebook_path.read_text()) + for cell_index, cell in enumerate(notebook["cells"]): + for output_index, output in enumerate(cell.get("outputs", [])): + if output.get("output_type") != "stream": + continue + text = "".join(output.get("text", [])) + assert "\r" not in text, ( + f"{notebook_path}: cell {cell_index}, output {output_index} " + "contains carriage-return progress output" + ) + assert LOCAL_OUTPUT_PATH.search(text) is None, ( + f"{notebook_path}: cell {cell_index}, output {output_index} " + "contains a machine-local path" + ) + + @pytest.mark.notebooks @pytest.mark.parametrize( - "notebook_path", BASIC_NOTEBOOKS, ids=[nb.stem for nb in BASIC_NOTEBOOKS] + "notebook_path", + RENDERED_NOTEBOOKS, + ids=[nb.stem for nb in RENDERED_NOTEBOOKS], ) -def test_basic_tutorial_executes(notebook_path: Path): - """Execute a basic-tutorial Jupyter notebook via nbconvert.""" +def test_rendered_tutorial_executes(notebook_path: Path): + """Execute the exact committed documentation notebook via nbconvert.""" success, output = _run( [ sys.executable, @@ -110,3 +151,46 @@ def test_marimo_tutorial_executes(notebook_path: Path): ) if not success: pytest.fail(f"{notebook_path.name} failed to execute:\n{output[-4000:]}") + + +@pytest.mark.notebooks +@pytest.mark.parametrize( + ("source_path", "rendered_path"), + MARIMO_EXPORTS.items(), + ids=[path.stem for path in MARIMO_EXPORTS], +) +def test_marimo_source_matches_rendered_notebook( + source_path: Path, + rendered_path: Path, +): + """Keep the rendered notebook's cells synchronized with its marimo source.""" + with tempfile.TemporaryDirectory() as tmpdir: + exported_path = Path(tmpdir) / "exported.ipynb" + result = subprocess.run( + [ + sys.executable, + "-m", + "marimo", + "export", + "ipynb", + str(source_path.resolve()), + "-o", + str(exported_path), + ], + cwd=tmpdir, + capture_output=True, + text=True, + timeout=60, + check=False, + ) + assert result.returncode == 0, result.stderr[-4000:] + exported = json.loads(exported_path.read_text()) + + rendered = json.loads(rendered_path.read_text()) + + def cell_contract(notebook: dict) -> list[tuple[str, list[str]]]: + return [(cell["cell_type"], cell["source"]) for cell in notebook["cells"]] + + assert cell_contract(rendered) == cell_contract(exported), ( + f"{rendered_path} is stale; regenerate it from {source_path}" + )