Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
abb496b
Update .gitignore to exclude data directory and VSCode settings
cpaniaguam Jul 22, 2026
716c191
Add Streamlit dependency and update network inspectors UI script
cpaniaguam Jul 22, 2026
7e1682e
Add styles for network inspectors UI components
cpaniaguam Jul 22, 2026
b0891dd
Refactor KDE-vs-LAN plotting functions for improved data handling and…
cpaniaguam Jul 22, 2026
6618435
Add shared result contracts for network inspector compute and plottin…
cpaniaguam Jul 22, 2026
42a622b
Refactor imports in network inspectors module for improved organizati…
cpaniaguam Jul 22, 2026
6f7db6e
Add launcher for network inspectors Streamlit app
cpaniaguam Jul 22, 2026
cc2b7ac
Refactor likelihood computation functions for improved validation and…
cpaniaguam Jul 22, 2026
0f66cee
Add Streamlit UI for LAN network inspector workflows
cpaniaguam Jul 22, 2026
434d191
Add script for batch training of multiple torch LAN models
cpaniaguam Jul 22, 2026
ebdc934
Add Streamlit UI section for network inspection workflows and batch t…
cpaniaguam Jul 22, 2026
903de37
Add tests for compute_kde_vs_lan and compute_lan_manifold functions
cpaniaguam Jul 22, 2026
51a5659
Add tests for network inspector plotting functions and update imports
cpaniaguam Jul 22, 2026
2b12e86
Reformat
cpaniaguam Jul 31, 2026
faaa84e
Reformat
cpaniaguam Jul 31, 2026
7a9eefd
Close figure
cpaniaguam Jul 31, 2026
6d21e7f
Add error handling
cpaniaguam Jul 31, 2026
3e1113c
Reformat
cpaniaguam Jul 31, 2026
f603c4c
Update sh script
cpaniaguam Jul 31, 2026
43f89e8
Merge branch '102-separate-linting-workflow-in-ci' into add-ui-networ…
cpaniaguam Aug 3, 2026
fe1d7d6
Fix linting
cpaniaguam Aug 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,23 @@ Necessary dependency should be installed automatically in the process.

Check the basic tutorial [here](docs/basic_tutorial/basic_tutorial_lan_torch.ipynb).

### Network Inspectors UI

LANfactory includes a Streamlit interface for interactive network inspection
workflows (KDE vs LAN likelihoods and LAN manifold plots).

Install the UI dependencies:

```bash
uv sync --extra ui
```

Launch the app:

```bash
uv run network-inspectors-ui
```

### Command Line Interface

LANfactory includes a command line interface with the commands `jaxtrain` and `torchtrain`, which train neural networks using `jax` and `torch` as backends, respectively.
Expand Down Expand Up @@ -133,6 +150,30 @@ To make your own configuration file, you can copy the example above into a new `

If you are using `uv`, you can also use the `uv run` command to run `jaxtrain` or `torchtrain` from the command line

### Batch Training Multiple Torch Models

To generate multiple torch models (for example, `angle` and `ddm`) in one command,
use the helper script:

```sh
scripts/train_torch_models_batch.sh \
--training-data-base data/data \
--networks-path-base data/torch_models \
--models angle,ddm \
--network-ids 0,1,2
```

If all models should use the same training-data folder, use:

```sh
scripts/train_torch_models_batch.sh \
--training-data-folder data/data/angle \
--models angle \
--network-ids 0,1
```

You can validate setup without training via `--dry-run`.

Comment thread
cpaniaguam marked this conversation as resolved.
### TorchMLP to ONNX Converter

Once you have trained your model, you can convert it to the ONNX format using the provided `transform-onnx` command.
Expand Down
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,15 @@ mlflow = ["mlflow>=3.14.0"]
hf = ["huggingface-hub>=0.20.0"]
sbi = ["sbi>=0.26", "nflows>=0.14"]
bayesflow = ["bayesflow>=2.0.8", "keras>=3.12"]
ui = ["streamlit>=1.40.0"]
all = [
"mlflow>=3.14.0",
"huggingface-hub>=0.20.0",
"sbi>=0.26",
"nflows>=0.14",
"bayesflow>=2.0.8",
"keras>=3.12",
"streamlit>=1.40.0",
]

[dependency-groups]
Expand Down Expand Up @@ -164,6 +166,8 @@ torchtrain = "lanfactory.cli.torch_train:app"
transform-onnx = "lanfactory.onnx.transform_onnx:app"
upload-hf = "lanfactory.cli.upload_hf:app"
download-hf = "lanfactory.cli.download_hf:app"
network-inspectors-ui = "lanfactory.cli.network_inspectors_ui:app"

[tool.setuptools.package-data]
"lanfactory.cli" = ["config_network_training_lan.yaml"]
"lanfactory.network_inspectors" = ["styles.css"]
294 changes: 294 additions & 0 deletions scripts/train_torch_models_batch.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,294 @@
#!/usr/bin/env bash
set -euo pipefail

# Train multiple torch LAN models (for example angle and ddm) in one command.
# Uses uv + torchtrain and writes outputs into the chosen networks path.

CONFIG_TEMPLATE="src/lanfactory/cli/config_network_training_lan.yaml"
TRAINING_DATA_FOLDER=""
TRAINING_DATA_BASE=""
NETWORKS_PATH_BASE="data/torch_models"
MODELS="angle,ddm"
NETWORK_IDS="0,1,2"
DL_WORKERS="1"
LOG_LEVEL="INFO"
DRY_RUN="0"

usage() {
cat <<'EOF'
Usage:
scripts/train_torch_models_batch.sh [options]

Options:
--config-template PATH YAML template with a MODEL field.
Default: src/lanfactory/cli/config_network_training_lan.yaml
--training-data-folder PATH Shared training data folder used for all models.
--training-data-base PATH Base folder where each model has its own subfolder.
Example: <base>/angle and <base>/ddm
--networks-path-base PATH Base output path for trained models.
Default: data/torch_models
--models CSV Comma-separated model names.
Default: angle,ddm
--network-ids CSV Comma-separated network IDs.
Default: 0,1,2
--dl-workers N DataLoader workers passed to torchtrain.
Default: 1
--log-level LEVEL Logging level for torchtrain.
Default: INFO
--dry-run Validate commands without training.
--help Show this help message.

Examples:
scripts/train_torch_models_batch.sh \
--training-data-base data/data \
--networks-path-base data/torch_models \
--models angle,ddm \
--network-ids 0,1,2,3

scripts/train_torch_models_batch.sh \
--training-data-folder data/data/angle \
--models angle \
--network-ids 0,1
EOF
}

while [[ $# -gt 0 ]]; do
case "$1" in
--config-template)
CONFIG_TEMPLATE="$2"
shift 2
;;
--training-data-folder)
TRAINING_DATA_FOLDER="$2"
shift 2
;;
--training-data-base)
TRAINING_DATA_BASE="$2"
shift 2
;;
--networks-path-base)
NETWORKS_PATH_BASE="$2"
shift 2
;;
--models)
MODELS="$2"
shift 2
;;
--network-ids)
NETWORK_IDS="$2"
shift 2
;;
--dl-workers)
DL_WORKERS="$2"
shift 2
;;
--log-level)
LOG_LEVEL="$2"
shift 2
;;
--dry-run)
DRY_RUN="1"
shift
;;
--help)
usage
exit 0
;;
*)
echo "Unknown option: $1" >&2
usage
exit 1
;;
esac
done

if [[ ! -f "$CONFIG_TEMPLATE" ]]; then
echo "Config template not found: $CONFIG_TEMPLATE" >&2
exit 1
fi

if [[ -z "$TRAINING_DATA_FOLDER" && -z "$TRAINING_DATA_BASE" ]]; then
echo "Provide either --training-data-folder or --training-data-base" >&2
exit 1
fi

IFS=',' read -r -a MODELS_ARR <<< "$MODELS"
IFS=',' read -r -a IDS_ARR <<< "$NETWORK_IDS"

TMP_DIR="$(mktemp -d)"
trap 'rm -rf "$TMP_DIR"' EXIT

make_model_config() {
local template="$1"
local model="$2"
local out_file="$3"

# Replace MODEL: ... in YAML while preserving all other lines.
awk -v model="$model" '
BEGIN { replaced = 0 }
/^[[:space:]]*MODEL[[:space:]]*:/ && replaced == 0 {
print "MODEL: \"" model "\""
replaced = 1
next
}
{ print }
END {
if (replaced == 0) {
print "MODEL: \"" model "\""
}
}
' "$template" > "$out_file"
}

resolve_model_training_folder() {
local candidate_folder="$1"
local model="$2"

if [[ ! -d "$candidate_folder" ]]; then
echo "$candidate_folder"
return
fi

# Preferred: pickle shards directly in the provided folder.
if find "$candidate_folder" -maxdepth 1 -type f -name '*.pickle' | grep -q .; then
echo "$candidate_folder"
return
fi

# Common layout: one subfolder per model.
if [[ -d "$candidate_folder/$model" ]] && find "$candidate_folder/$model" -maxdepth 1 -type f -name '*.pickle' | grep -q .; then
echo "$candidate_folder/$model"
return
fi

# Fallback: if exactly one immediate subfolder contains pickle shards, use it.
local subdir_count
subdir_count="$(find "$candidate_folder" -mindepth 1 -maxdepth 1 -type d | wc -l | tr -d ' ')"
if [[ "$subdir_count" == "1" ]]; then
local only_subdir
only_subdir="$(find "$candidate_folder" -mindepth 1 -maxdepth 1 -type d | head -n 1)"
if find "$only_subdir" -maxdepth 1 -type f -name '*.pickle' | grep -q .; then
echo "$only_subdir"
return
fi
fi

echo "$candidate_folder"
}
Comment on lines +143 to +177

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Uninformative error message when the candidate folder is missing.

resolve_model_training_folder echoes "" at line 148 when $candidate_folder does not exist. At line 257 this produces Training data folder not found for model 'x': with a blank path, which does not tell the user what path was actually attempted. Echo the candidate path instead so the failing path shows up in the error message.

🐛 Proposed fix
     if [[ ! -d "$candidate_folder" ]]; then
-        echo ""
+        echo "$candidate_folder"
         return
     fi

Also applies to: 256-259

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/train_torch_models_batch.sh` around lines 143 - 177, Update
resolve_model_training_folder so the missing-directory branch echoes
candidate_folder instead of an empty string, allowing the caller’s “Training
data folder not found” message to include the attempted path. Preserve the
existing resolution behavior for valid directories.


validate_training_folder() {
local folder="$1"
local model="$2"

local n_pickles
n_pickles="$(find "$folder" -maxdepth 1 -type f -name '*.pickle' | wc -l | tr -d ' ')"

if [[ "$n_pickles" == "0" ]]; then
echo "No .pickle files found for model '$model' in: $folder" >&2
echo "Expected either:" >&2
echo " 1) <folder>/*.pickle" >&2
echo " 2) <folder>/$model/*.pickle" >&2
exit 1
fi

local validation_out
if ! validation_out="$((uv run python - "$folder" <<'PY'
import glob
import os
import pickle
import sys

folder = sys.argv[1]
files = sorted(glob.glob(os.path.join(folder, "*.pickle")))
if not files:
print("ERR_NO_PICKLES")
raise SystemExit(2)

with open(files[0], "rb") as f:
obj = pickle.load(f)

if not isinstance(obj, dict):
print("ERR_NOT_DICT")
raise SystemExit(3)

keys = set(obj.keys())
required = {"lan_data", "lan_labels"}
if not required.issubset(keys):
print("ERR_BAD_KEYS")
print(",".join(sorted(keys)))
raise SystemExit(4)

print("OK_KEYS")
PY
))"; then
Comment on lines +194 to +223

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Fix the $(( command-substitution ambiguity flagged by shellcheck.

Line 195 writes validation_out="$((uv run python - "$folder" <<'PY' ... ))". The double opening parenthesis after $( is unintentional: the goal is a plain command substitution, not a nested subshell or arithmetic expression. Shellcheck reports this as SC1102 (error level) because shells disambiguate $(( differently: some parse it as arithmetic and fail, others fall back to command substitution. Remove the redundant parentheses so the intent is unambiguous.

🐛 Proposed fix
-    if ! validation_out="$((uv run python - "$folder" <<'PY'
+    if ! validation_out="$(uv run python - "$folder" <<'PY'
 import glob
 import os
 import pickle
 import sys
@@
 print("OK_KEYS")
 PY
-))"; then
+)"; then
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
local validation_out
if ! validation_out="$((uv run python - "$folder" <<'PY'
import glob
import os
import pickle
import sys
folder = sys.argv[1]
files = sorted(glob.glob(os.path.join(folder, "*.pickle")))
if not files:
print("ERR_NO_PICKLES")
raise SystemExit(2)
with open(files[0], "rb") as f:
obj = pickle.load(f)
if not isinstance(obj, dict):
print("ERR_NOT_DICT")
raise SystemExit(3)
keys = set(obj.keys())
required = {"lan_data", "lan_labels"}
if not required.issubset(keys):
print("ERR_BAD_KEYS")
print(",".join(sorted(keys)))
raise SystemExit(4)
print("OK_KEYS")
PY
))"; then
local validation_out
if ! validation_out="$(uv run python - "$folder" <<'PY'
import glob
import os
import pickle
import sys
folder = sys.argv[1]
files = sorted(glob.glob(os.path.join(folder, "*.pickle")))
if not files:
print("ERR_NO_PICKLES")
raise SystemExit(2)
with open(files[0], "rb") as f:
obj = pickle.load(f)
if not isinstance(obj, dict):
print("ERR_NOT_DICT")
raise SystemExit(3)
keys = set(obj.keys())
required = {"lan_data", "lan_labels"}
if not required.issubset(keys):
print("ERR_BAD_KEYS")
print(",".join(sorted(keys)))
raise SystemExit(4)
print("OK_KEYS")
PY
)"; then
🧰 Tools
🪛 Shellcheck (0.11.0)

[error] 195-195: Shells disambiguate $(( differently or not at all. For $(command substitution), add space after $( . For $((arithmetics)), fix parsing errors.

(SC1102)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/train_torch_models_batch.sh` around lines 194 - 223, Update the
validation_out assignment in the batch validation flow to use an unambiguous
plain command substitution by removing the extra opening parenthesis after $(
and its matching closing parenthesis. Preserve the existing uv run python
heredoc and validation behavior unchanged.

Source: Linters/SAST tools

Comment on lines +195 to +223
if grep -q "ERR_BAD_KEYS" <<<"$validation_out"; then
echo "Training data format mismatch for model '$model' in: $folder" >&2
echo "Expected pickle keys: lan_data and lan_labels" >&2
echo "Found keys:" >&2
echo "$(tail -n 1 <<<"$validation_out")" >&2
exit 1
fi

if grep -q "ERR_NOT_DICT" <<<"$validation_out"; then
echo "Unexpected pickle format in: $folder" >&2
echo "Expected each shard to be a dict with lan_data/lan_labels." >&2
exit 1
fi

echo "Failed to validate training folder '$folder'." >&2
echo "$validation_out" >&2
exit 1
fi
}

for model in "${MODELS_ARR[@]}"; do
model="${model//[[:space:]]/}"
if [[ -z "$model" ]]; then
continue
fi

if [[ -n "$TRAINING_DATA_FOLDER" ]]; then
model_training_folder="$(resolve_model_training_folder "$TRAINING_DATA_FOLDER" "$model")"
else
model_training_folder="$(resolve_model_training_folder "$TRAINING_DATA_BASE/$model" "$model")"
fi

if [[ ! -d "$model_training_folder" ]]; then
echo "Training data folder not found for model '$model': $model_training_folder" >&2
exit 1
fi

validate_training_folder "$model_training_folder" "$model"

model_cfg="$TMP_DIR/config_${model}.yaml"
make_model_config "$CONFIG_TEMPLATE" "$model" "$model_cfg"

for net_id in "${IDS_ARR[@]}"; do
net_id="${net_id//[[:space:]]/}"
if [[ -z "$net_id" ]]; then
continue
fi

cmd=(
uv run torchtrain
--config-path "$model_cfg"
--training-data-folder "$model_training_folder"
--networks-path-base "$NETWORKS_PATH_BASE"
--network-id "$net_id"
--dl-workers "$DL_WORKERS"
--log-level "$LOG_LEVEL"
)

if [[ "$DRY_RUN" == "1" ]]; then
cmd+=(--dry-run)
fi

echo ""
echo "=== Training model=$model network_id=$net_id ==="
echo "Training data: $model_training_folder"
"${cmd[@]}"
done
done

echo ""
echo "Batch training complete."
23 changes: 23 additions & 0 deletions src/lanfactory/cli/network_inspectors_ui.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""Launcher for the network inspectors Streamlit app."""

from __future__ import annotations

from pathlib import Path
import sys


def app() -> None:
"""Start the LANfactory network inspectors UI via Streamlit."""
try:
from streamlit.web import cli as stcli
except ImportError as exc: # pragma: no cover
raise ImportError(
"Streamlit is required for the network-inspectors-ui command. "
"Install optional dependencies with: uv sync --extra ui"
) from exc

app_path = (
Path(__file__).resolve().parents[1] / "network_inspectors" / "streamlit_app.py"
)
sys.argv = ["streamlit", "run", str(app_path)]
raise SystemExit(stcli.main())
Loading
Loading