Skip to content
Paralia-LabsPublic

About

Synthetic prosthetic-scan data generator and evaluation harness, built to stress-test a clinical scan-deviation pipeline.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Paralia

parable

License: BSD-2-Clause (see LICENSE — generators/openlimbtt/ is CC BY-SA 4.0) Python 3.10+ Built on OpenLimbTT

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.

Current scope

  • 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/max range.
  • 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 the paralia_sdk client.
  • Compute origin/axis recovery metrics, run severity sweeps, and review results in a built-in browser viewer — see Analysis tools below.

How this fits together

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
Loading

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.

Layout

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

Quick start

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 browser

mode="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.

Configuration (config.py::RunConfig)

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: true

Each 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.

Boundary with paradigm

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.

Analysis tools

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. See sweep_config.py::SweepConfig for 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.

Hosted deployment

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.

Open source & licensing

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.

About

Synthetic prosthetic-scan data generator and evaluation harness, built to stress-test a clinical scan-deviation pipeline.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages