Synthetic dataset generation and evaluation harness for paradigm, Paralia's scan-processing algorithm — a test bench for debugging and improving paradigm's scan-to-heatmap pipeline against known-ground-truth synthetic data instead of real (unlabeled) patient scans.
parable is the one open-source piece of the Paralia platform, built on a real port of OpenLimbTT's statistical shape model — see Open source & licensing below. For the platform's mission, how parable and paradigm fit together, and deeper write-ups of what's below, see paralia.io/docs.
- Generate anatomically plausible synthetic limb cases via a real port of OpenLimbTT's PCA statistical shape model.
- Apply controlled, cumulative perturbations — pose (random rotation + translation), mesh decimation, mesh discontinuities and duplicate boundaries (topology defects), and gaussian surface noise — each independently enabled and each sampling its own severity per case from a
min/maxrange. - Store ground truth for every case (true origin/axis, the transform applied, the PCA coefficients used to generate the shape).
- Run
paradigm(a proprietary, separately-hosted service) over its network API, via theparalia_sdkclient. - Compute origin/axis recovery metrics, run severity sweeps, and review results in a built-in browser viewer — see Analysis tools below.
flowchart TD
subgraph Generation["1 . Generate clean case"]
A[seed] --> B["generators/openlimbtt/model.py:\nsample 32 calibrated PCA coeffs\n-> LR.pkl -> reconstruct\n(against paradigm-aligned mean)"]
B --> C["clean_mesh.stl (+ bone_mesh.stl if save_bone_mesh)\nground truth: origin=(0,0,0), axis=(0,0,1),\nPCA coefficients"]
end
subgraph Perturb["2 . Perturb (cumulative, in-memory: pose -> decimation -> discontinuity -> duplicate boundary -> noise)"]
C --> D1["apply_pose_perturbation:\nrandom rotation + translation\n(rigid transform)"]
D1 --> D3["apply_mesh_decimation_perturbation:\nrandom face-count reduction\n(fast_simplification; non-rigid)"]
D3 --> D4["apply_mesh_discontinuity_perturbation:\nrandomized flood-fill discontinuity growth\n(non-rigid; boundary_edges reported\nfor 3D-viewer highlighting)"]
D4 --> D5["apply_gaussian_surface_noise_perturbation:\nrandom per-vertex noise\n(non-rigid — ground truth unchanged)"]
D5 --> E["perturbed_mesh.stl (only mesh written\nby default — see Performance below)\nupdated ground truth: origin_after, axis_after\ndefect_report: {pose,\nmesh_decimation, mesh_discontinuity, mesh_duplicate_boundary, gaussian_surface_noise}"]
Fx["future: topology defects beyond discontinuities/duplicate boundaries\n(self-intersections, orphan fragments,\nboundary rim tilt, ...)\n— not implemented yet"] -.-> D4
end
subgraph Run["3 . Run paradigm"]
E --> G["paralia_sdk.ParadigmClient.run_pipeline\n(mesh, case_id) -- one HTTP call"]
G --> H{"paradigm batch API reachable?"}
H -->|"yes"| I["ParadigmRunResult:\norigin, primary_axis, cap info,\ndr_min/max, debug"]
H -->|"no, or pipeline failed"| J["ParadigmRunResult:\nsuccess=False\nfailure_stage=... (e.g. 'adapter')\n(clean failure, not a crash)"]
end
subgraph Score["4 . Evaluate"]
I --> K["evaluate_case:\norigin_error_mm, axis_error_degrees, cap_detected,\ndr_accuracy_pct field self-consistency (mode=api),\nsampled determinism check -> EvaluationResult"]
J --> K
end
subgraph Store["5 . Persist"]
K --> L["case.json, metrics.json,\nparadigm_result.json,\nmanifest.csv row"]
end
subgraph Review["6 . Review & generate"]
L --> M["parable viewer:\nRun overview -> Case grid -> Case detail,\nvariant-card gallery, filter/sort,\ntabs per open run"]
M --> N["spot failure clusters —\ntoday's perturbation stages (pose,\ndecimation, discontinuities, duplicate boundaries, noise)\nplus a magenta boundary-edge highlight\nfor mesh_discontinuity cases in the 3D viewer"]
O["+ Generate a run\n(configure num_cases/seed/perturbations\nin the browser, jobs.py::RunRegistry\nruns it, one at a time)"] -.-> A
end
Boxes 1–6 are implemented and working today. Box Fx (topology defects beyond discontinuities/duplicate boundaries) is the one remaining item not yet built — see paralia.io/docs/parable for the full roadmap rationale.
parable/
pyproject.toml
requirements.txt # pip deps for the hosted API's Docker image (mirrors paradigm's own)
Dockerfile # hosted API, deployed as its own Railway service (see io/api.py's docstring)
railway.toml # Railway service config for the Dockerfile above
README.md
examples/
basic_run.yaml
viewer/ # the review viewer app (served by `parable viewer`)
index.html # 3-level UI + tabs per open run + a generate form
parable/ # the importable package
cli.py # run_config(): the core generate->perturb->paradigm->evaluate->manifest loop;
# run() (the CLI entrypoint) is a thin wrapper loading a YAML config
config.py # pydantic RunConfig — see "Configuration" below
types.py # GroundTruth, CasePaths, SyntheticCase, ParadigmResult dataclasses
math_utils.py # normalize, random_unit_vector, rotation_matrix, axis_angle_error_degrees
jobs.py # RunRegistry: the one shared job queue/progress/cancel implementation,
# used by both viewer/server.py (local) and io/api.py (hosted)
generators/
openlimbtt_adapter.py # thin call-through — the ONLY file outside the sandbox that touches it
openlimbtt/ # SANDBOXED — see its own NOTICE.md/LICENSE (CC BY-SA 4.0 data)
model.py # ported reconstruction logic; scales output to mm at the source
data/ # vendored Mean-Limb.stl, Components16.npy, LR.pkl
perturbations/ # every apply_x_perturbation(mesh, ground_truth, rng, config) is a pure
# in-memory Mesh -> Mesh transform — cli.py owns all disk I/O (see Performance)
pose.py # apply_pose_perturbation: random rotation + translation (rigid transform)
mesh_decimation.py # apply_mesh_decimation_perturbation: random face-count reduction via the
# fast_simplification library (non-rigid — ground truth left unchanged)
mesh_discontinuity.py # apply_mesh_discontinuity_perturbation: randomized flood-fill discontinuity
# growth, boundary edges reported for 3D-viewer highlighting (non-rigid)
mesh_duplicate_boundary.py # apply_mesh_duplicate_boundary_perturbation: randomized flood-fill patch
# duplication, boundary edges reported for 3D-viewer highlighting (non-rigid)
gaussian_surface_noise.py # apply_gaussian_surface_noise_perturbation: random per-vertex noise
# (non-rigid — ground truth left unchanged)
runners/
paradigm_adapter.py # the ONLY file that talks to paradigm (over HTTP, via paralia_sdk)
evaluation/
metrics.py # evaluate_pose_recovery: origin_error_mm, axis_error_degrees
storage/
io.py # write_json, append_manifest_row
viewer/
README.md # viewer usage + architecture notes
server.py # stdlib server backing `parable viewer` — static files + /api/runs* (local)
io/
api.py # FastAPI transport for the hosted deployment — same RunRegistry as the
# local devserver, gated by a shared-secret header
cd parable
python -m venv .venv
source .venv/bin/activate
pip install -e . # pulls paralia_sdk straight from github.com/Paralia-Labs/paralia_sdk
parable run --config examples/basic_run.yaml
parable viewer --run runs/dev_run # or just `parable viewer` — generate directly from the browsermode="api" runs (below) additionally need a reachable paradigm batch API — set
PARADIGM_BATCH_API_URL/API_INTERNAL_SECRET to point at one (paradigm itself is a
proprietary, closed-source service; mode="stub", the default, works fully offline
with no paradigm access at all).
The default run writes to:
runs/dev_run/
manifest.csv
cases/case_000001/
case.json
clean_mesh.stl
perturbed_mesh.stl (the only mesh written by default — see Performance below)
perturbed_pose.stl, perturbed_decimation.stl, perturbed_mesh_discontinuity.stl, perturbed_mesh_duplicate_boundary.stl
(if shape.save_perturbation_intermediates is enabled)
bone_mesh.stl (if save_bone_mesh is enabled)
paradigm_result.json
metrics.json
paradigm_intermediates/ (if paradigm.save_intermediates and mode=api)
If a single case raises unexpectedly, cli.py catches it and records a failure_stage="harness" manifest row rather than aborting the rest of the run.
run_id: dev_run
output_root: runs
num_cases: 10
base_seed: 12345
shape:
generator: openlimbtt # only "openlimbtt" is real; the name is kept for future generators
save_bone_mesh: false
save_perturbation_intermediates: false
perturbations:
# Every perturbation follows enabled + min_* + max_* — magnitude is sampled
# once per case from [min, max], not read as a fixed constant.
translation: { enabled: true, min_abs_mm: 0.0, max_abs_mm: 100.0 }
rotation: { enabled: true, min_degrees: 0.0, max_degrees: 30.0 }
mesh_decimation: { enabled: false, min_reduction: 0.1, max_reduction: 0.5 }
mesh_discontinuity: { enabled: false, min_count: 0, max_count: 3, max_length_mm: 10.0 }
mesh_duplicate_boundary: { enabled: false, min_count: 0, max_count: 3, max_length_mm: 10.0 }
gaussian_surface_noise: { enabled: false, min_sigma_mm: 0.0, max_sigma_mm: 2.0, clip_mm: 2.0 }
paradigm:
mode: stub # "stub" (fake result, no paradigm dependency) or "api" (the real thing)
save_intermediates: trueEach case gets its own seed per generation/perturbation stage, all derived from base_seed via one master_rng, drawn in the same fixed order every case — a full run is reproducible end to end.
paradigm is a separate, proprietary, closed-source service — parable never imports its code, only calls its network API through paralia_sdk's ParadigmClient. parable/runners/paradigm_adapter.py is the only file in this codebase that talks to paradigm at all.
from paralia_sdk import ParadigmClient
client = ParadigmClient(base_url=PARADIGM_BATCH_API_URL, internal_secret=API_INTERNAL_SECRET)
result = client.run_pipeline(mesh, case_id=case.case_id)mode="stub" (the config default) exercises the rest of the harness with no paradigm access at all — a deliberate option for anyone who doesn't have (or doesn't want to run) a paradigm batch API instance. mode="api" never raises either: every case just lands as success=False in the manifest instead of crashing the run.
Beyond a plain run, parable ships two ways to answer "where does paradigm actually break?":
- Severity sweeps (
parable sweep --config <path>, or the viewer's Generate form) test one perturbation at a time across a range, producing dose-response curves against a configurable pass threshold — the answer to at what severity does accuracy start to fail. Seesweep_config.py::SweepConfigfor the full schema. - Defect similarity is a UMAP scatter (in the viewer, on every run) positioning each case by defect-severity similarity, colored by accuracy — surfaces which perturbations or combinations of perturbations correlate most with failure, and whether failure modes cluster.
Full write-ups of both, with screenshots and interpretation guidance, live at paralia.io/docs/parable.
Dockerfile and railway.toml at this repo's root deploy io/api.py (the hosted FastAPI transport) as a standalone service — Root Directory is this repo's own root, since paralia_sdk installs directly from its own public repo during the build. Required environment variables: API_INTERNAL_SECRET, ALLOWED_ORIGINS, and optionally PARABLE_RUNS_DIR. Runs on the hosted service are ephemeral (lost on restart/redeploy) and shared across every authenticated caller by design — see io/api.py's module docstring.
parable is BSD-2-Clause, except generators/openlimbtt/, which is CC BY-SA 4.0 (vendored data + a ported reconstruction algorithm from OpenLimbTT — see generators/openlimbtt/LICENSE and NOTICE.md). This is the one open-source piece of the Paralia platform today, and it's open specifically because of that embedded dependency: rather than build around OpenLimbTT's license, the whole tool was open-sourced — which doubles as a standalone resource for researchers who want to generate and evaluate large synthetic transtibial-limb datasets, independent of paradigm.
Development setup, testing, and contribution guidelines: see TESTING.md. Deeper technical write-ups — the OpenLimbTT port's implementation details, in-memory perturbation performance, and the full "where this is headed" roadmap — live at paralia.io/docs/parable rather than here, to keep this README focused on getting you running.