BotShelf Vampire BOTSHELF VAMPIRE Register

Healthcare & medical robotics: simulate, measure, learn

Build simulation and data-handling exercises with open tools: MuJoCo probe positioning, SOFA indentation, DICOM/FHIR hygiene, OpenFDA and ClinicalTrials.gov discovery, plus 3D models from public sample scans. Research and education only.

Research, education and simulation only. Nothing here is a medical device, a clinical method or advice for patient care, and none of it has been validated for clinical use.

Interactive tool, runs in your browser (English / Japanese)
Medical robotics pilot planner →
Turn a use case into a research or simulation validation plan.

Build recipes

Step-by-step workflows written by BSV. Each one combines real open tools into something you can make today.

Measure probe-tip positioning accuracy in MuJoCo

Tested by BSV

Model a small two-link arm, send its tip to target points and log how fast and how precisely it arrives — a first simulation check for a robot-assisted positioning idea.

Input
Target points in millimetres (in reach: 20–220 mm from the base), controller gain kp, tolerance in mm.
Output
probe_reach.csv — one row per target: time to first reach the tolerance, time it settles inside it, final error, how far the tip went past the target, reached yes/no.
Prerequisites
Python 3.10 or later and pip install mujoco numpy. No GPU and no robot hardware needed.
Steps
  1. Save the code below as mujoco_probe_reach.py (it contains the arm model, closed-form inverse kinematics and the logger).
  2. Run python mujoco_probe_reach.py to use the four default targets.
  3. Open probe_reach.csv and read time_to_tol_s, settled_at_s, final_err_mm and overshoot_mm side by side.
  4. Run again with a softer controller: python mujoco_probe_reach.py "150,60;120,-80;180,0;90,120" 20, and compare the two files.
  5. Replace the targets with points that matter for your idea and keep each run's CSV, named by its settings.
Expected result
Every target is reached and the final error is close to zero. A lower gain arrives later. Because the arm moves in joint space, the tip path curves, so it can travel well past the target along the straight line before settling — exactly the kind of behaviour worth logging before anyone trusts a positioning idea.
Next step
Swap in your own robot's MJCF/URDF model and add a third joint, then practise teleoperation in BSV's Robot Pilot Academy (simulation first). Open
Code (BSV original, MIT licence)

Download .py · mujoco_probe_reach.py

#!/usr/bin/env python3
"""BSV recipe: probe-tip positioning test for a 2-link arm in MuJoCo, with logged accuracy metrics.

Research / education / simulation only. Not a medical device, not clinical guidance.

Input : target points in mm (default four points in the arm's workspace), controller gain kp (default 60),
        tolerance in mm (default 1.0).
Output: probe_reach.csv with, per target: time to first enter the tolerance, time it stays inside, final error, peak overshoot past
        the target and whether the target was reached within 3 s of simulated time.
Original BSV code (model and controller), MIT. Library: MuJoCo (Apache-2.0).
"""
import csv, sys
import mujoco
import numpy as np

XML = """
<mujoco model="bsv_probe_arm">
  <option timestep="0.002" gravity="0 0 0"/>
  <worldbody>
    <body name="link1" pos="0 0 0">
      <joint name="j1" type="hinge" axis="0 0 1" damping="2.0"/>
      <geom type="capsule" fromto="0 0 0 0.12 0 0" size="0.008" mass="0.15"/>
      <body name="link2" pos="0.12 0 0">
        <joint name="j2" type="hinge" axis="0 0 1" damping="1.2"/>
        <geom type="capsule" fromto="0 0 0 0.10 0 0" size="0.006" mass="0.08"/>
        <site name="tip" pos="0.10 0 0" size="0.003"/>
      </body>
    </body>
  </worldbody>
  <actuator>
    <position name="a1" joint="j1" kp="KP"/>
    <position name="a2" joint="j2" kp="KP"/>
  </actuator>
</mujoco>
"""
L1, L2 = 0.12, 0.10

def ik(x, y):
    """Closed-form inverse kinematics of a planar 2-link arm (elbow-down)."""
    c2 = (x * x + y * y - L1 * L1 - L2 * L2) / (2 * L1 * L2)
    if abs(c2) > 1:
        raise ValueError(f"target ({x:.3f}, {y:.3f}) is outside the workspace")
    q2 = np.arccos(c2)
    q1 = np.arctan2(y, x) - np.arctan2(L2 * np.sin(q2), L1 + L2 * np.cos(q2))
    return q1, q2

def main(targets="150,60;120,-80;180,0;90,120", kp="60", tol_mm="1.0"):
    model = mujoco.MjModel.from_xml_string(XML.replace("KP", str(float(kp))))
    data = mujoco.MjData(model)
    tip = model.site("tip").id
    rows = []
    for t in targets.split(";"):
        tx, ty = (float(v) / 1000 for v in t.split(","))
        q1, q2 = ik(tx, ty)
        start = data.site_xpos[tip][:2].copy()
        data.ctrl[:] = [q1, q2]
        t_in, t_settle, peak_over = None, None, 0.0
        direction = np.array([tx, ty]) - start
        direction = direction / (np.linalg.norm(direction) or 1)
        for step in range(int(3.0 / model.opt.timestep)):
            mujoco.mj_step(model, data)
            p = data.site_xpos[tip][:2]
            err = np.linalg.norm(p - [tx, ty]) * 1000
            peak_over = max(peak_over, float(np.dot(p - [tx, ty], direction)) * 1000)
            now = (step + 1) * model.opt.timestep
            if t_in is None and err <= float(tol_mm):
                t_in = now
            if err > float(tol_mm):
                t_settle = None
            elif t_settle is None:
                t_settle = now
        rows.append({"target_mm": t, "time_to_tol_s": round(t_in, 3) if t_in else "",
                     "settled_at_s": round(t_settle, 3) if t_settle else "", "final_err_mm": round(float(err), 3),
                     "overshoot_mm": round(peak_over, 3), "reached": t_in is not None})
    with open("probe_reach.csv", "w", newline="") as f:
        w = csv.DictWriter(f, fieldnames=list(rows[0])); w.writeheader(); w.writerows(rows)
    for r in rows:
        print(r)

if __name__ == "__main__":
    main(*sys.argv[1:])
Output of BSV's own test run

Run on 2026-10-06 23:37 JST · Python 3.13.5, mujoco 3.15.0, numpy 2.5.3

{'target_mm': '150,60', 'time_to_tol_s': 0.122, 'settled_at_s': 0.122, 'final_err_mm': 0.0, 'overshoot_mm': 47.004, 'reached': True}
{'target_mm': '120,-80', 'time_to_tol_s': 0.16, 'settled_at_s': 0.16, 'final_err_mm': 0.0, 'overshoot_mm': 0.0, 'reached': True}
{'target_mm': '180,0', 'time_to_tol_s': 0.158, 'settled_at_s': 0.158, 'final_err_mm': 0.0, 'overshoot_mm': 0.0, 'reached': True}
{'target_mm': '90,120', 'time_to_tol_s': 0.152, 'settled_at_s': 0.152, 'final_err_mm': 0.0, 'overshoot_mm': 0.0, 'reached': True}

Soft-tissue indentation in SOFA: log force against displacement

Not run by BSV

A warm-up for needle-insertion studies: press on a finite-element tissue block, ramp the force and record how far the surface moves.

Input
Block size, Young's modulus and Poisson ratio (the defaults are example values for a soft gel phantom), maximum probe force.
Output
indent_log.csv — step, applied force (N), downward displacement of the top-centre node (mm).
Prerequisites
SOFA v24.06 or later with the SofaPython3 plugin (binary release from the SOFA GitHub page) and the Python version that build expects. Basic familiarity with runSofa.
Steps
  1. Install a SOFA binary release and confirm that runSofa opens one of the bundled examples.
  2. Save the code below as sofa_tissue_indent.py. Component names follow SOFA v24.06; if your version reports an unknown component, look up its current name in the SOFA documentation.
  3. Run runSofa -l SofaPython3 sofa_tissue_indent.py and press Animate. Let it run for at least 400 steps (the force ramp).
  4. Close runSofa and open indent_log.csv. Plot displacement_mm against force_N.
  5. Change YOUNG (for example 2500 and 10000) and compare the curves.
Expected result
A displacement curve that rises with the force and levels off once the ramp ends; a stiffer block moves less for the same force. This is linear-elastic indentation only — puncture, friction and needle bending need further models that this recipe deliberately leaves out.
Next step
Read the SofaPython3 documentation on controllers, then look at how published needle-insertion work adds puncture and friction before you extend the scene. Open
Code (BSV original, MIT licence)

Download .py · sofa_tissue_indent.py

#!/usr/bin/env python3
"""BSV recipe (NOT RUN BY BSV): soft-tissue block indentation in SOFA, logging force vs. displacement.

Research / education / simulation only. A warm-up for needle-insertion studies: it models only linear-elastic
indentation of a block, not puncture, friction or needle bending.

Run   : runSofa -l SofaPython3 sofa_tissue_indent.py   (SOFA v24.06 or later with the SofaPython3 plugin)
Output: indent_log.csv  (step, applied force in N, downward displacement of the top-centre node in mm)
Component names follow SOFA v24.06; check them against the SOFA version you install.
Original BSV code, MIT. SOFA and SofaPython3 are LGPL-2.1.
"""
import csv
import Sofa
import Sofa.Core

NX, NY, NZ = 7, 7, 4                      # grid nodes; odd NX/NY gives an exact top-centre node
SIZE = (0.04, 0.04, 0.02)                 # block size in metres (4 x 4 x 2 cm)
YOUNG = 5000.0                            # Pa - example value for a soft gel phantom; change and compare
POISSON = 0.45
TOP_CENTRE = (NX // 2) + NX * ((NY // 2) + NY * (NZ - 1))
MAX_FORCE = 0.5                           # N, reached after RAMP_STEPS
RAMP_STEPS = 400


class IndentLogger(Sofa.Core.Controller):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.mo, self.ff = kwargs["mo"], kwargs["ff"]
        self.step, self.z0, self.rows = 0, None, []

    def onAnimateBeginEvent(self, event):
        self.step += 1
        f = MAX_FORCE * min(self.step / RAMP_STEPS, 1.0)
        self.ff.forces.value = [[0.0, 0.0, -f]]
        z = self.mo.position.value[TOP_CENTRE][2]
        self.z0 = z if self.z0 is None else self.z0
        self.rows.append((self.step, round(f, 5), round((self.z0 - z) * 1000, 4)))
        if self.step % 50 == 0:
            with open("indent_log.csv", "w", newline="") as fh:
                w = csv.writer(fh); w.writerow(["step", "force_N", "displacement_mm"]); w.writerows(self.rows)


def createScene(root):
    root.gravity = [0, 0, 0]
    root.dt = 0.005
    root.addObject("RequiredPlugin", pluginName=[
        "Sofa.Component.ODESolver.Backward", "Sofa.Component.LinearSolver.Iterative",
        "Sofa.Component.StateContainer", "Sofa.Component.Topology.Container.Grid",
        "Sofa.Component.SolidMechanics.FEM.Elastic", "Sofa.Component.Mass",
        "Sofa.Component.Engine.Select", "Sofa.Component.Constraint.Projective",
        "Sofa.Component.MechanicalLoad", "Sofa.Component.Visual"])
    root.addObject("DefaultAnimationLoop")
    root.addObject("VisualStyle", displayFlags="showBehaviorModels showForceFields")
    tissue = root.addChild("Tissue")
    tissue.addObject("EulerImplicitSolver", rayleighStiffness=0.1, rayleighMass=0.1)
    tissue.addObject("CGLinearSolver", iterations=50, tolerance=1e-9, threshold=1e-9)
    tissue.addObject("RegularGridTopology", name="grid", n=[NX, NY, NZ], min=[0, 0, 0], max=list(SIZE))
    mo = tissue.addObject("MechanicalObject", name="dofs")
    tissue.addObject("UniformMass", totalMass=0.035)
    tissue.addObject("HexahedronFEMForceField", youngModulus=YOUNG, poissonRatio=POISSON, method="large")
    tissue.addObject("BoxROI", name="base", box=[-0.001, -0.001, -0.001, SIZE[0] + 0.001, SIZE[1] + 0.001, 0.001])
    tissue.addObject("FixedProjectiveConstraint", indices="@base.indices")
    ff = tissue.addObject("ConstantForceField", name="probe", indices=[TOP_CENTRE], forces=[[0, 0, 0]])
    tissue.addObject(IndentLogger(name="logger", mo=mo, ff=ff))
    return root

BSV has not run this one, so there is no test output to show. Use the steps as a guide and check them against the official documentation.

Check and blank identifying DICOM fields before you share a teaching file

Tested by BSV

Read a DICOM file, report which identifying fields are present, blank or remove them, strip private tags and save a copy — without ever printing the values.

Input
A DICOM file. The default is CT_small.dcm, a public test file that ships with pydicom.
Output
CT_small_deid.dcm (the cleaned copy) and deid_report.csv (field, present before, action taken).
Prerequisites
Python 3.10 or later and pip install pydicom. Use only files you are allowed to handle.
Steps
  1. Save the code below as dicom_deid_audit.py.
  2. Run python dicom_deid_audit.py to process the bundled public test file.
  3. Open deid_report.csv: every field in the list shows whether it was present and what was done.
  4. Open CT_small_deid.dcm in a viewer (3D Slicer, for example) and confirm the image is intact.
  5. Compare the field list with the DICOM PS3.15 Annex E profile and note what it does not cover yet — dates, UIDs and burned-in text in the pixels.
Expected result
A report and a cleaned copy whose image is unchanged. This is a learning exercise with a short field list, not a complete de-identification tool; real data sharing follows the PS3.15 profiles and your institution's rules.
Next step
Read the DICOM standard's confidentiality profiles and extend the field list; then open the cleaned file in 3D Slicer (next recipe). Open
Code (BSV original, MIT licence)

Download .py · dicom_deid_audit.py

#!/usr/bin/env python3
"""BSV recipe: audit and blank identifying fields in a DICOM file before you share it for research or teaching.

Education only. This is a learning exercise based on a small field list, NOT a complete de-identification
tool and not a substitute for the DICOM PS3.15 Annex E confidentiality profiles or your institution's rules.

Input : path to a DICOM file (default: pydicom's bundled public test file CT_small.dcm).
Output: <name>_deid.dcm and deid_report.csv (field, present before, action). Values are never printed.
Original BSV code, MIT. Library: pydicom (MIT).
"""
import csv, sys
import pydicom
from pydicom.data import get_testdata_file

FIELDS = {  # keyword -> action ("blank" keeps the element with an empty value, "remove" deletes it)
    "PatientName": "blank", "PatientID": "blank", "PatientBirthDate": "blank", "PatientSex": "keep",
    "PatientAge": "keep", "PatientAddress": "remove", "OtherPatientIDs": "remove",
    "InstitutionName": "remove", "InstitutionAddress": "remove", "ReferringPhysicianName": "blank",
    "PerformingPhysicianName": "remove", "OperatorsName": "remove", "AccessionNumber": "blank",
    "StudyID": "blank", "StationName": "remove", "DeviceSerialNumber": "remove",
}

def main(path=None):
    path = path or get_testdata_file("CT_small.dcm")
    ds = pydicom.dcmread(path)
    rows = []
    for kw, action in FIELDS.items():
        present = kw in ds and str(ds.data_element(kw).value) != ""
        if present and action == "blank":
            ds.data_element(kw).value = ""
        elif present and action == "remove":
            delattr(ds, kw)
        rows.append({"field": kw, "present_before": present, "action": action if present else "none"})
    n_private = sum(1 for el in ds if el.tag.is_private)
    ds.remove_private_tags()
    rows.append({"field": "private tags", "present_before": n_private > 0, "action": f"removed {n_private}"})
    ds.PatientIdentityRemoved = "YES"
    ds.DeidentificationMethod = "BSV recipe field list (education); not PS3.15 complete"
    out = path.rsplit("/", 1)[-1].replace(".dcm", "") + "_deid.dcm"
    ds.save_as(out)
    with open("deid_report.csv", "w", newline="") as f:
        w = csv.DictWriter(f, fieldnames=["field", "present_before", "action"]); w.writeheader(); w.writerows(rows)
    changed = sum(1 for r in rows if r["action"] not in ("none", "keep"))
    print(f"{out}: {changed} fields changed, {n_private} private tags removed; dates/UIDs untouched (see next step)")
    print(f"pixel data kept: {'PixelData' in ds}, image {ds.Rows}x{ds.Columns}")

if __name__ == "__main__":
    main(*sys.argv[1:])
Output of BSV's own test run

Run on 2026-10-06 23:37 JST · Python 3.13.5, pydicom 3.0.2

CT_small_deid.dcm: 6 fields changed, 179 private tags removed; dates/UIDs untouched (see next step)
pixel data kept: True, image 128x128

From a public CT sample to a 3D model in 3D Slicer

Not run by BSV

Load a sample scan that ships with 3D Slicer, segment bone with a threshold, clean it up and export an STL you can print or load into a simulator.

Input
The CTChest sample data set from Slicer's Sample Data module (public sample data).
Output
An STL surface mesh of the segmented bone.
Prerequisites
3D Slicer (current stable release) on Windows, macOS or Linux. A mouse helps.
Steps
  1. Install the stable release from slicer.org and open it.
  2. Open the Sample Data module and load CTChest.
  3. Open Segment Editor, add a segment and choose the Threshold effect. Start from a lower bound around bone intensity, adjust until bone is highlighted, then Apply.
  4. Use Islands → Keep largest island, then Smoothing (median) to remove specks.
  5. Click Show 3D to check the surface.
  6. Export the segment to an STL file (Segmentations → Export to files).
Expected result
A rib-cage and spine surface in the 3D view and an STL file on disk. The threshold is a visual choice on sample data, not a measurement.
Next step
Flatten a synthetic FHIR Bundle next, or inventory DICOM tags on the sample file. Open

BSV has not run this one, so there is no test output to show. Use the steps as a guide and check them against the official documentation.

Flatten a synthetic FHIR Bundle

Tested by BSV

Turn a small Patient+Observation Bundle into fhir_patients.csv and fhir_observations.csv. Default input is a labeled synthetic sample — not a real patient.

Input
Optional FHIR Bundle JSON path. Default: writes sample_fhir_bundle_labeled.json.
Output
fhir_patients.csv and fhir_observations.csv.
Prerequisites
Python 3 stdlib only (csv, json).
Steps
  1. Save the script.
  2. Run: python fhir_bundle_flatten.py
  3. Confirm the synthetic ids stay labeled example-* before you swap in any institutional export.
Expected result
One patient row and a few observation rows from the sample. Not a production FHIR store.
Next step
Inventory DICOM tags on the public CT sample next. Open
Code (BSV original, MIT licence)

Download .py · fhir_bundle_flatten.py

#!/usr/bin/env python3
"""BSV recipe: flatten a small FHIR R4 Bundle (Patient + Observations) into CSVs.

Education / research data-handling only. Synthetic bundle is labeled example data — not a real patient.
Not a clinical system, not PHI handling guidance, and not a claim of FHIR conformance for production.

Input : optional path to a FHIR Bundle JSON (default: writes a labeled synthetic sample).
Output: fhir_patients.csv + fhir_observations.csv
Original BSV code, MIT.
"""
import csv, json, sys

SAMPLE = {
  "resourceType": "Bundle", "type": "collection", "entry": [
    {"resource": {"resourceType": "Patient", "id": "example-1",
                  "name": [{"family": "Example", "given": ["Practice"]}],
                  "gender": "unknown", "birthDate": "1970-01-01"}},
    {"resource": {"resourceType": "Observation", "id": "obs-1", "status": "final",
                  "code": {"text": "example-heart-rate"}, "subject": {"reference": "Patient/example-1"},
                  "valueQuantity": {"value": 72, "unit": "beats/min"}}},
    {"resource": {"resourceType": "Observation", "id": "obs-2", "status": "final",
                  "code": {"text": "example-temp-c"}, "subject": {"reference": "Patient/example-1"},
                  "valueQuantity": {"value": 36.6, "unit": "Cel"}}},
  ]
}

def main(path=None):
    if not path:
        path = "sample_fhir_bundle_labeled.json"
        open(path, "w", encoding="utf-8").write(json.dumps(SAMPLE, indent=2) + "\n")
        print("wrote labeled synthetic FHIR Bundle:", path)
    bundle = json.load(open(path, encoding="utf-8"))
    patients, obs = [], []
    for e in bundle.get("entry") or []:
        r = e.get("resource") or {}
        rt = r.get("resourceType")
        if rt == "Patient":
            nm = (r.get("name") or [{}])[0]
            patients.append({"id": r.get("id"), "family": nm.get("family"), "given": " ".join(nm.get("given") or []),
                             "gender": r.get("gender"), "birthDate": r.get("birthDate")})
        elif rt == "Observation":
            vq = r.get("valueQuantity") or {}
            patients_ref = (r.get("subject") or {}).get("reference")
            obs.append({"id": r.get("id"), "status": r.get("status"), "code": (r.get("code") or {}).get("text"),
                        "subject": patients_ref, "value": vq.get("value"), "unit": vq.get("unit")})
    with open("fhir_patients.csv", "w", newline="", encoding="utf-8") as f:
        w = csv.DictWriter(f, fieldnames=["id", "family", "given", "gender", "birthDate"])
        w.writeheader(); w.writerows(patients)
    with open("fhir_observations.csv", "w", newline="", encoding="utf-8") as f:
        w = csv.DictWriter(f, fieldnames=["id", "status", "code", "subject", "value", "unit"])
        w.writeheader(); w.writerows(obs)
    print(f"flattened {len(patients)} patients, {len(obs)} observations -> fhir_patients.csv, fhir_observations.csv")

if __name__ == "__main__":
    main(*sys.argv[1:2])
Output of BSV's own test run

Run on 2026-10-07 02:16 JST · Python 3.13.5

wrote labeled synthetic FHIR Bundle: sample_fhir_bundle_labeled.json
flattened 1 patients, 2 observations -> fhir_patients.csv, fhir_observations.csv

Inventory DICOM tags in a sample file

Tested by BSV

List keyword, VR, VM and a short preview for every element in CT_small.dcm (or your path). Long values are truncated in the CSV.

Input
Optional DICOM path. Default: pydicom public CT_small.dcm.
Output
dicom_tags.csv.
Prerequisites
Python 3 + pydicom (BSV fieldsvenv).
Steps
  1. Save the script.
  2. Run: python dicom_tag_inventory.py
  3. Handle real clinical files under your institution rules — this recipe does not de-identify.
Expected result
Hundreds of rows for CT_small. Education inventory only.
Next step
Pull one OpenFDA drug label for a research citation practice. Open
Code (BSV original, MIT licence)

Download .py · dicom_tag_inventory.py

#!/usr/bin/env python3
"""BSV recipe: inventory DICOM data elements in a sample file (keyword, VR, VM, value length).

Research / education only. Does not de-identify. Values longer than 40 chars are truncated in the CSV
so accidental PHI is less likely to be pasted into chats — still handle real clinical files under institutional rules.

Input : optional DICOM path (default: pydicom public test file CT_small.dcm).
Output: dicom_tags.csv
Original BSV code, MIT. Library: pydicom (MIT).
"""
import csv, sys
import pydicom
from pydicom.data import get_testdata_file

def main(path=None):
    path = path or get_testdata_file("CT_small.dcm")
    ds = pydicom.dcmread(path)
    rows = []
    for el in ds.iterall():
        if el.VR == "SQ":
            val_len = f"seq({len(el.value)})"
            preview = ""
        else:
            s = str(el.value)
            val_len = len(s)
            preview = s if len(s) <= 40 else s[:37] + "..."
        rows.append({"tag": str(el.tag), "keyword": el.keyword or "", "VR": el.VR, "VM": el.VM,
                     "value_len": val_len, "preview": preview, "private": el.tag.is_private})
    with open("dicom_tags.csv", "w", newline="", encoding="utf-8") as f:
        w = csv.DictWriter(f, fieldnames=["tag", "keyword", "VR", "VM", "value_len", "preview", "private"])
        w.writeheader(); w.writerows(rows)
    print(f"inventoried {len(rows)} elements from {path.rsplit('/',1)[-1]} -> dicom_tags.csv")

if __name__ == "__main__":
    main(*sys.argv[1:2])
Output of BSV's own test run

Run on 2026-10-07 02:16 JST · Python 3.13.5, pydicom 3.0.2

inventoried 262 elements from CT_small.dcm -> dicom_tags.csv

Pull an OpenFDA drug label (research)

Tested by BSV

Download one label hit into markdown plus openfda_index.csv. Not medical advice — OpenFDA is incomplete and not for care decisions.

Input
Brand/generic query (default aspirin) and limit 1–5 (default 1).
Output
openfda_label_<query>_N.md and openfda_index.csv.
Prerequisites
Python 3 stdlib + network access to api.fda.gov.
Steps
  1. Save the script.
  2. Run: python open_fda_label_pull.py aspirin 1
  3. Cite open.fda.gov if you republish; do not use the text for care decisions.
Expected result
One short markdown file and an index row. Truncated fields by design.
Next step
List a few ClinicalTrials.gov studies about surgical robots. Open
Code (BSV original, MIT licence)

Download .py · open_fda_label_pull.py

#!/usr/bin/env python3
"""BSV recipe: pull one OpenFDA drug label result into a short markdown + CSV index.

Research / education only. OpenFDA is not a complete archive and is not medical advice.
Do not use this output for prescribing, diagnosis, or treatment decisions.

Input : brand or generic name query (default: aspirin) and limit (default 1, max 5).
Output: openfda_label_<query>.md and openfda_index.csv
Original BSV code, MIT. Source: open.fda.gov (public domain / CC0 attribution as published).
"""
import csv, json, sys, urllib.parse, urllib.request

UA = {"User-Agent": "BSV-fields-recipe/1.0 (research education; +https://botshelfvampire.com)"}

def main(name="aspirin", limit="1"):
    name = name.strip() or "aspirin"
    limit = max(1, min(5, int(limit)))
    # Prefer brand_name match; fall back to bare term if needed.
    search = urllib.parse.quote(f"openfda.brand_name:{name}")
    url = f"https://api.fda.gov/drug/label.json?search={search}&limit={limit}"
    req = urllib.request.Request(url, headers=UA)
    with urllib.request.urlopen(req, timeout=45) as r:
        data = json.loads(r.read().decode("utf-8"))
    results = data.get("results") or []
    if not results:
        raise SystemExit(f"no OpenFDA label results for {name!r}")
    rows = []
    for i, res in enumerate(results):
        of = res.get("openfda") or {}
        brand = ",".join(of.get("brand_name") or []) or name
        generic = ",".join(of.get("generic_name") or [])
        mfr = ",".join(of.get("manufacturer_name") or [])
        purpose = " ".join(res.get("purpose") or res.get("indications_and_usage") or [])[:400]
        warnings = " ".join(res.get("warnings") or [])[:400]
        md = [f"# OpenFDA label pull — {brand}", "",
              f"Query: `{name}`  Result: {i+1}/{len(results)}", "",
              "**Research/education only. Not clinical advice.**", "",
              f"- generic: {generic}", f"- manufacturer: {mfr}",
              f"- product_ndc: {','.join(of.get('product_ndc') or [])}", "",
              "## Purpose / indications (truncated from API)", purpose or "(none in this record)", "",
              "## Warnings (truncated)", warnings or "(none in this record)", "",
              "Source: https://open.fda.gov/ — keep attribution if you republish.", ""]
        out = f"openfda_label_{name.replace(' ','_')}_{i+1}.md"
        open(out, "w", encoding="utf-8").write("\n".join(md) + "\n")
        rows.append({"query": name, "brand": brand, "generic": generic, "manufacturer": mfr, "md_file": out})
    with open("openfda_index.csv", "w", newline="", encoding="utf-8") as f:
        w = csv.DictWriter(f, fieldnames=["query", "brand", "generic", "manufacturer", "md_file"])
        w.writeheader(); w.writerows(rows)
    print(f"wrote {len(rows)} OpenFDA label md file(s) + openfda_index.csv for query={name!r}")

if __name__ == "__main__":
    main(*sys.argv[1:3])
Output of BSV's own test run

Run on 2026-10-07 02:16 JST · Python 3.13.5

wrote 1 OpenFDA label md file(s) + openfda_index.csv for query='aspirin'

Brief ClinicalTrials.gov list (surgical robot)

Tested by BSV

Query API v2 and write trials_brief.csv (NCT ID, title, status, type, URL). Discovery only — not recruitment.

Input
Query term (default "surgical robot") and page size 1–10 (default 5).
Output
trials_brief.csv.
Prerequisites
Python 3 stdlib + network access to clinicaltrials.gov.
Steps
  1. Save the script.
  2. Run: python clinicaltrials_robot_brief.py "surgical robot" 5
  3. Open each NCT URL on ClinicalTrials.gov for the authoritative record.
Expected result
A handful of registry rows as published by the API that day.
Next step
Sketch a teleop HIL latency budget next. Open
Code (BSV original, MIT licence)

Download .py · clinicaltrials_robot_brief.py

#!/usr/bin/env python3
"""BSV recipe: list a few ClinicalTrials.gov studies matching a research query (default: surgical robot).

Research / education discovery only. Not recruitment, not enrollment advice, not a complete registry dump.
Titles and NCT IDs come from ClinicalTrials.gov API v2 as published.

Input : query string (default "surgical robot") and page size (default 5, max 10).
Output: trials_brief.csv
Original BSV code, MIT.
"""
import csv, json, sys, urllib.parse, urllib.request

UA = {"User-Agent": "BSV-fields-recipe/1.0 (research education; +https://botshelfvampire.com)"}

def main(term="surgical robot", page_size="5"):
    term = term.strip() or "surgical robot"
    n = max(1, min(10, int(page_size)))
    q = urllib.parse.urlencode({"query.term": term, "pageSize": n, "format": "json"})
    url = f"https://clinicaltrials.gov/api/v2/studies?{q}"
    req = urllib.request.Request(url, headers=UA)
    with urllib.request.urlopen(req, timeout=45) as r:
        data = json.loads(r.read().decode("utf-8"))
    rows = []
    for st in data.get("studies") or []:
        proto = st.get("protocolSection") or {}
        ident = proto.get("identificationModule") or {}
        status = (proto.get("statusModule") or {})
        design = (proto.get("designModule") or {})
        rows.append({
            "nct_id": ident.get("nctId"),
            "brief_title": ident.get("briefTitle"),
            "overall_status": status.get("overallStatus"),
            "study_type": design.get("studyType"),
            "url": f"https://clinicaltrials.gov/study/{ident.get('nctId')}" if ident.get("nctId") else "",
        })
    with open("trials_brief.csv", "w", newline="", encoding="utf-8") as f:
        w = csv.DictWriter(f, fieldnames=["nct_id", "brief_title", "overall_status", "study_type", "url"])
        w.writeheader(); w.writerows(rows)
    print(f"wrote {len(rows)} trials for term={term!r} -> trials_brief.csv (ClinicalTrials.gov API v2)")

if __name__ == "__main__":
    main(*sys.argv[1:3])
Output of BSV's own test run

Run on 2026-10-07 02:16 JST · Python 3.13.5

wrote 5 trials for term='surgical robot' -> trials_brief.csv (ClinicalTrials.gov API v2)

Teleop HIL latency budget (cartoon)

Tested by BSV

Sum one-way + encode/decode/display delays into a toy RTT. Research/sim literacy only — not clinical or safety analysis.

Input
one_way encode decode display ms (defaults 40 8 8 16).
Output
hil_latency.csv.
Prerequisites
Python 3 stdlib.
Steps
  1. Save the script.
  2. Run: python hil_latency_budget.py
  3. Read round_trip_ms.
Expected result
One row with a toy RTT sum.
Next step
Bin synthetic FHIR patient ages next. Open
Code (BSV original, MIT licence)

Download .py · hil_latency_budget.py

#!/usr/bin/env python3
"""BSV recipe: teleop HIL latency budget (one-way + round-trip cartoon).

Research / education / simulation literacy only — not a clinical or safety analysis.
Input : one_way_ms, encode_ms, decode_ms, display_ms (defaults 40 8 8 16), samples (default 1).
Output: hil_latency.csv with components and round_trip_ms.
Original BSV code, MIT. Dependency: none.
"""
import csv, sys

def main(one_way="40", encode="8", decode="8", display="16"):
    a,b,c,d = map(float, (one_way, encode, decode, display))
    rtt = 2 * a + b + c + d
    row = {"one_way_ms": a, "encode_ms": b, "decode_ms": c, "display_ms": d,
           "round_trip_ms": rtt, "note": "toy sum — not a measured system"}
    with open("hil_latency.csv", "w", newline="", encoding="utf-8") as f:
        w = csv.DictWriter(f, fieldnames=list(row.keys())); w.writeheader(); w.writerow(row)
    print(f"RTT cartoon={rtt:.1f}ms -> hil_latency.csv (research/sim only)")

if __name__ == "__main__":
    main(*sys.argv[1:5])
Output of BSV's own test run

Run on 2026-10-07 03:20 JST · Python 3.13.5

RTT cartoon=112.0ms -> hil_latency.csv (research/sim only)

FHIR age bins from a synthetic Bundle

Tested by BSV

Decade-bin Patient birthDates from an embedded synthetic Bundle. No PHI — not clinical.

Input
None.
Output
fhir_age_bins.csv.
Prerequisites
Python 3 stdlib.
Steps
  1. Save the script.
  2. Run: python fhir_age_bins_sample.py
  3. Inspect decade counts.
Expected result
A few decade bins from the embedded Bundle.
Next step
Sample openFDA device events next. Open
Code (BSV original, MIT licence)

Download .py · fhir_age_bins_sample.py

#!/usr/bin/env python3
"""BSV recipe: bin Patient birth years from a tiny embedded FHIR Bundle (no network).

Research / education only — synthetic data, not real PHI, not clinical.
Input : none.
Output: fhir_age_bins.csv with decade bucket counts.
Original BSV code, MIT. Dependency: none.
"""
import csv, json, sys
from datetime import date

BUNDLE = {
  "resourceType": "Bundle", "type": "collection",
  "entry": [
    {"resource": {"resourceType": "Patient", "id": "a", "birthDate": "1980-05-01"}},
    {"resource": {"resourceType": "Patient", "id": "b", "birthDate": "1992-11-12"}},
    {"resource": {"resourceType": "Patient", "id": "c", "birthDate": "1975-01-20"}},
    {"resource": {"resourceType": "Patient", "id": "d", "birthDate": "2001-07-04"}},
    {"resource": {"resourceType": "Patient", "id": "e", "birthDate": "1988-03-15"}},
    {"resource": {"resourceType": "Patient", "id": "f", "birthDate": "1969-09-09"}},
  ]
}

def main():
    today = date(2026, 10, 7)
    bins = {}
    for e in BUNDLE["entry"]:
        bd = e["resource"].get("birthDate")
        if not bd: continue
        y = int(bd[:4]); age = today.year - y
        bucket = f"{(age // 10) * 10}s"
        bins[bucket] = bins.get(bucket, 0) + 1
    rows = [{"age_decade": k, "count": v, "note": "synthetic FHIR only"} for k, v in sorted(bins.items())]
    with open("fhir_age_bins.csv", "w", newline="", encoding="utf-8") as f:
        w = csv.DictWriter(f, fieldnames=["age_decade", "count", "note"]); w.writeheader(); w.writerows(rows)
    print(f"wrote {len(rows)} decade bins from synthetic Bundle -> fhir_age_bins.csv")

if __name__ == "__main__":
    main()
Output of BSV's own test run

Run on 2026-10-07 03:20 JST · Python 3.13.5

wrote 4 decade bins from synthetic Bundle -> fhir_age_bins.csv

openFDA device event sample

Tested by BSV

Pull a few device adverse-event rows by generic name. Research/education only — not pharmacovigilance.

Input
generic_name limit (defaults endoscope 5).
Output
openfda_device_events.csv.
Prerequisites
Python 3 stdlib + network access to api.fda.gov.
Steps
  1. Save the script.
  2. Run: python open_fda_device_event_brief.py endoscope 5
  3. Skim report_number dates.
Expected result
Up to limit rows when openFDA answers.
Next step
Histogram ClinicalTrials.gov phases next. Open
Code (BSV original, MIT licence)

Download .py · open_fda_device_event_brief.py

#!/usr/bin/env python3
"""BSV recipe: brief openFDA device adverse-event counts for a device generic name.

Research / education only — not pharmacovigilance, not clinical advice.
Input : generic_name query (default endoscope), limit (default 5).
Output: openfda_device_events.csv with report counts sample.
Original BSV code, MIT. Data: api.fda.gov (openFDA terms).
"""
import csv, json, sys, urllib.parse, urllib.request
UA = {"User-Agent": "BSV-fields-recipe/1.0 (research-education; +https://botshelfvampire.com)"}

def main(name="endoscope", limit="5"):
    limit = int(limit)
    q = urllib.parse.quote(f'device.generic_name:"{name}"')
    url = f"https://api.fda.gov/device/event.json?search={q}&limit={limit}"
    req = urllib.request.Request(url, headers=UA)
    with urllib.request.urlopen(req, timeout=45) as r:
        d = json.loads(r.read().decode())
    rows = []
    for ev in d.get("results") or []:
        device = (ev.get("device") or [{}])[0]
        rows.append({
            "report_number": ev.get("report_number"),
            "date_received": ev.get("date_received"),
            "generic_name": device.get("generic_name"),
            "brand_name": device.get("brand_name"),
            "event_type": ev.get("event_type"),
        })
    if not rows:
        raise SystemExit("no results — try another generic_name")
    with open("openfda_device_events.csv", "w", newline="", encoding="utf-8") as f:
        w = csv.DictWriter(f, fieldnames=list(rows[0].keys())); w.writeheader(); w.writerows(rows)
    meta = d.get("meta", {}).get("results", {})
    print(f"wrote {len(rows)} sample rows (openFDA total≈{meta.get('total','?')}) -> openfda_device_events.csv")

if __name__ == "__main__":
    main(*sys.argv[1:3])
Output of BSV's own test run

Run on 2026-10-07 03:20 JST · Python 3.13.5

wrote 5 sample rows (openFDA total≈20313) -> openfda_device_events.csv

ClinicalTrials.gov phase histogram

Tested by BSV

Count phases in one page of studies. Research/education only — not a care recommendation.

Input
query pageSize (defaults robotics-ish query 50).
Output
trials_phase_hist.csv.
Prerequisites
Python 3 stdlib + network access to clinicaltrials.gov.
Steps
  1. Save the script.
  2. Run: python clinicaltrials_phase_hist.py
  3. Read phase counts.
Expected result
One row per distinct phase in the page.
Next step
Return to MuJoCo probe positioning, or open Robot Pilot for teleop practice. Open
Code (BSV original, MIT licence)

Download .py · clinicaltrials_phase_hist.py

#!/usr/bin/env python3
"""BSV recipe: histogram ClinicalTrials.gov phases for a query (robot OR surgical).

Research / education only — not a clinical recommendation.
Input : query expression (default AREA[ConditionSearch]robotics), pageSize (default 50).
Output: trials_phase_hist.csv.
Original BSV code, MIT. Data: clinicaltrials.gov API v2.
"""
import csv, json, sys, urllib.parse, urllib.request
UA = {"User-Agent": "BSV-fields-recipe/1.0 (research-education; +https://botshelfvampire.com)"}

def main(query="AREA[ConditionSearch]robotics", page_size="50"):
    page_size = int(page_size)
    q = urllib.parse.urlencode({"query.term": query, "pageSize": page_size, "format": "json"})
    url = f"https://clinicaltrials.gov/api/v2/studies?{q}"
    req = urllib.request.Request(url, headers=UA)
    with urllib.request.urlopen(req, timeout=60) as r:
        d = json.loads(r.read().decode())
    hist = {}
    for st in d.get("studies") or []:
        phases = (((st.get("protocolSection") or {}).get("designModule") or {}).get("phases")) or ["NA"]
        for p in phases:
            hist[p] = hist.get(p, 0) + 1
    rows = [{"phase": k, "count": v, "query": query, "page_size": page_size} for k, v in sorted(hist.items())]
    if not rows:
        raise SystemExit("no studies returned")
    with open("trials_phase_hist.csv", "w", newline="", encoding="utf-8") as f:
        w = csv.DictWriter(f, fieldnames=["phase", "count", "query", "page_size"]); w.writeheader(); w.writerows(rows)
    print(f"wrote {len(rows)} phase bins from {page_size} studies -> trials_phase_hist.csv")

if __name__ == "__main__":
    main(*sys.argv[1:3])
Output of BSV's own test run

Run on 2026-10-07 03:20 JST · Python 3.13.5

wrote 5 phase bins from 50 studies -> trials_phase_hist.csv

Compare the tools

Difficulty is BSV's own rating for a first project. Check each licence on the official page before you ship anything.

ToolJobLicenceDifficultyLocal / cloud
SOFASoft-tissue and surgical simulation (FEM, contact, haptics)LGPL-2.1AdvancedLocal
MuJoCoFast rigid-body physics for robot control and learningApache-2.0IntermediateLocal
PyBullet / BulletRobot simulation and reinforcement-learning prototypeszlibIntermediateLocal
3D SlicerMedical image viewing, segmentation and 3D models (research)BSD-style (3D Slicer License)IntermediateLocal
OpenFDA drug label APIPublic drug label endpoint (not medical advice)Public domain / CC0 as publishedBeginnerCloud / web API
ClinicalTrials.gov API v2Registry search for research discoveryPublic (NLM)BeginnerCloud / web API
HL7 FHIR R4 overviewFHIR resource definitions (education)CC0 (FHIR spec)IntermediateCloud / web API
MONAIDeep learning for medical-imaging research (PyTorch)Apache-2.0AdvancedLocal (GPU helps)
pydicomRead, inspect and write DICOM files in PythonMITBeginnerLocal

Starter stack: verified sources

Official pages only. BSV opened each link and recorded the HTTP status and date shown. We link out and summarise; we do not copy or rehost their code.

Related on BSV