Persona-driven synthetic longitudinal health data generator
Project description
vitalforge
Persona-driven synthetic longitudinal health data generator. Produces deterministic, seeded, distributionally plausible health signal and clinical record data for demos, tests, and ML fixtures. Pure-math/template generation: no network access, no LLMs, no real patient data.
What it generates
Daily signals (vitalforge generate)
| Signal | Model | Unit |
|---|---|---|
heart_rate |
Circadian-aware sampling: sleep distribution 22:00-06:00, resting distribution shifted by a sinusoidal circadian factor, probabilistic exercise bursts | bpm |
blood_pressure |
Linear start-to-end trend with gaussian noise; morning/evening readings | mmHg |
steps |
Daily totals with separate weekday/weekend distributions | steps |
sleep |
Nightly episodes: sampled bedtime and duration, 5-20 min onset latency, ~90-minute light/deep/REM cycles that exactly partition the sleep window | min |
glucose |
Fasting (07:00) and postprandial (13:00, 19:00) readings with an optional linear trend over the period | mg/dL |
weight |
Linear interpolation between start and end weight, N readings per week | kg |
activity_energy |
Daily totals with weekday/weekend distributions | kcal |
Clinical records
Conditions, medications, allergies, immunizations, lab results, and encounters, as declared in the persona file, with terminology codings (SNOMED CT, ICD-10, RxNorm, LOINC, CVX).
Multi-year trajectories (vitalforge trajectory)
Yearly health snapshots interpolated from anchor points: blood-pressure
phases, arbitrary named metric series (e.g. weight_kg, resting_hr),
milestones, encounters, and medication periods.
Install
pip install -e .
# with test dependencies:
pip install -e ".[dev]"
Requires Python >= 3.10. Runtime dependency: PyYAML.
CLI usage
# 30 days of daily signals + clinical records, JSONL
vitalforge generate --persona examples/personas/alex_rivera.yaml \
--days 30 --seed 42 --out ./out --format jsonl
# 10-year trajectory, JSON
vitalforge trajectory --persona examples/personas/jordan_kim.yaml \
--years 10 --seed 42 --out ./out --format json
generate options: --persona (required), --days (default 30), --seed
(default 42), --start-date (overrides the persona's start_date), --out
(default ./out), --format (json | jsonl | csv | fhir, default
json).
trajectory options: --persona (required), --years (default 10),
--seed, --out, --format (json | jsonl | csv).
Library usage
from vitalforge import load_persona, build_dataset, build_trajectory_dataset
from vitalforge.output import to_fhir_bundle, write_dataset
persona = load_persona("examples/personas/alex_rivera.yaml")
dataset = build_dataset(persona, days=30, seed=42)
bundle = to_fhir_bundle(dataset) # FHIR R4 Bundle dict
write_dataset(dataset, out_dir, "jsonl") # files on disk
trajectory = build_trajectory_dataset(
load_persona("examples/personas/jordan_kim.yaml"), years=10, seed=42
)
Determinism
The same persona file, parameters (--days/--years, --start-date), and
--seed always produce byte-identical output files:
- All randomness flows through a single
random.Random(seed)consumed in a fixed generation order. - Timestamps derive from the persona's
start_date(or the--start-dateoverride), never from the current clock. - Record ids are UUIDv5 values computed from a fixed namespace and stable keys.
Changing the seed changes sampled values but not record structure.
Persona schema
Personas are YAML or JSON documents (schema_version: 1):
schema_version: 1
profile: # required
name: Alex Rivera # required
birth_date: "1989-04-12" # required, ISO date
sex: male # required: female | male | other | unknown
height_cm: 178 # optional
start_date: "2025-03-01" # required; day 0 / year 0 of generation
baseline: # optional; only listed signals are generated
heart_rate:
resting_mean: 74 # bpm, daytime baseline (circadian factor adds up to +10)
resting_std: 4
active_mean: 140 # exercise-burst distribution
active_std: 15
sleep_mean: 62 # 22:00-06:00 distribution
sleep_std: 3
samples_per_day: 72 # optional, default 72
exercise_probability: 0.08 # optional, default 0.08
blood_pressure:
systolic_start: 146 # day-0 baseline
systolic_end: 132 # final-day baseline (linear trend between)
diastolic_start: 94
diastolic_end: 84
std: 5
readings_per_day: 2 # optional, default 2
steps:
weekday_mean: 7200
weekday_std: 1800
weekend_mean: 9800
weekend_std: 2600
sleep:
bedtime_hour: 23.25 # decimal hours after the day's midnight
bedtime_std: 0.5
duration_mean_hours: 6.7
duration_std_hours: 0.6
glucose:
fasting_mean: 110 # mg/dL
fasting_std: 8
postprandial_mean: 144
postprandial_std: 12
trend_mg_dl: -6 # optional linear shift over the period, default 0
weight:
start_kg: 88.5
end_kg: 87.2
std: 0.3
readings_per_week: 2 # optional, default 2
activity_energy:
weekday_mean_kcal: 330
weekday_std: 80
weekend_mean_kcal: 470
weekend_std: 110
conditions: # optional clinical history sections
- text: Essential hypertension
codes: # system: SNOMED | ICD-10 | RxNorm | LOINC | CVX | <full URI>
- { system: SNOMED, code: "59621000", display: Essential hypertension }
onset_date: "2025-01-20"
medications:
- text: Lisinopril 10 mg oral tablet, once daily
codes: [{ system: RxNorm, code: "314076" }]
dose: { quantity: 10, unit: mg, frequency: daily }
start_date: "2025-01-20" # end_date optional
allergies: # text, codes, onset_date
immunizations: # text, codes, date
labs: # text, codes, value, unit, reference_range, date
encounters:
- kind: outpatient # required
modality: in_person # optional, default in_person
service: Primary care — annual physical examination # required
reason: Annual physical examination
providers: [{ role: attending, name: Dr. Naomi Feld, specialty: Internal Medicine }]
location: Lakeside Primary Care
time: "2025-01-20T15:00:00Z" # required
summary: New diagnosis of essential hypertension.
trajectory: # optional; required for `vitalforge trajectory`
blood_pressure:
phases: # piecewise-linear systolic/diastolic ranges
- { year_start: 0, year_end: 5, systolic: [118, 116], diastolic: [76, 74] }
metrics: # name -> anchor points, linearly interpolated
weight_kg: # points use `value` (exact) ...
- { year: 0, value: 64.0 }
- { year: 10, value: 63.5 }
resting_hr: # ... or `mean` + optional `std` (sampled with noise)
- { year: 0, mean: 62, std: 3 }
- { year: 10, mean: 55, std: 3 }
milestones:
- { year: 1, month: 5, event: Completed first marathon., kind: lifestyle }
encounters:
- { year: 0, quarter: 1, kind: outpatient, service: Annual physical,
provider: Dr. Priya Anand, specialty: Family Medicine,
location: Hillcrest Family Health, outcome: All vitals within normal range }
medications: # active for start_year <= year < end_year
- { name: Lisinopril, start_year: 0, end_year: 3, detail: 10 mg daily }
Example personas: examples/personas/alex_rivera.yaml
(hypertension + prediabetes, 30-day treatment-response window) and
examples/personas/jordan_kim.yaml
(healthy adult with a 10-year trajectory).
Output formats
- json — single document:
generatormetadata,persona, and record arrays (signals+clinical, ortrajectory). - jsonl — one record per line, preceded by a
metadatarecord. - csv — one file per section:
<slug>_signals.csv,<slug>_clinical.csv,<slug>_trajectory.csv(flat rows; BP expands to systolic/diastolic rows, sleep to per-episode rows). - fhir — FHIR R4
Bundle(typecollection), daily datasets only:Patient,Condition,MedicationStatement,AllergyIntolerance,Immunization,Encounter, andObservation(labs).- Signal observations: blood pressure (LOINC 85354-9 with 8480-6/8462-4 components), glucose (2339-0), and weight (29463-7) are one Observation per reading; heart rate (8867-4, daily mean), steps (41950-7), active energy (41981-2), and sleep duration (93832-4) are one daily-summary Observation per day.
Record shapes
Signal record (all signals except sleep):
{
"record_type": "signal",
"signal": "glucose",
"id": "…",
"start": "2025-03-01T00:00:00Z",
"end": "2025-03-02T00:00:00Z",
"unit": "mg/dL",
"samples": [{ "t": "2025-03-01T07:12:00Z", "value": 109.4, "context": "fasting" }]
}
Sleep records carry episodes (stage, start, end, duration_min),
sleep_onset, latency_min, and total_sleep_min instead of samples.
Blood-pressure samples carry systolic/diastolic instead of value.
Tests
pip install -e ".[dev]"
pytest
Tests cover determinism (byte-identical re-runs), distribution sanity (physiologic bounds, circadian dip, sleep-cycle accounting), persona schema validation, output formats, and CLI behavior. All tests run offline.
License
Apache-2.0. See LICENSE.
Disclaimer
All generated data is synthetic. It encodes simplified statistical models and is not suitable for clinical decision-making, medical research conclusions, or as a substitute for real-world evidence.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file vitalforge-0.1.0.tar.gz.
File metadata
- Download URL: vitalforge-0.1.0.tar.gz
- Upload date:
- Size: 32.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5cbbde406d2c8670357a60a6c92c52391d65599e457a6c2fa6b61aaae468272c
|
|
| MD5 |
96d3884c9b57c7e21c1b10bdf814ffed
|
|
| BLAKE2b-256 |
de626f3b1eae38f40cc4a7eb6c7a6d79664df3dc804f9ac629b402125da8059b
|
Provenance
The following attestation bundles were made for vitalforge-0.1.0.tar.gz:
Publisher:
publish.yml on blockhaven-ai/vitalforge
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
vitalforge-0.1.0.tar.gz -
Subject digest:
5cbbde406d2c8670357a60a6c92c52391d65599e457a6c2fa6b61aaae468272c - Sigstore transparency entry: 1807520851
- Sigstore integration time:
-
Permalink:
blockhaven-ai/vitalforge@2d7b2beac0b7286ff84b1f81213b905890d0d99a -
Branch / Tag:
refs/heads/main - Owner: https://github.com/blockhaven-ai
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@2d7b2beac0b7286ff84b1f81213b905890d0d99a -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file vitalforge-0.1.0-py3-none-any.whl.
File metadata
- Download URL: vitalforge-0.1.0-py3-none-any.whl
- Upload date:
- Size: 28.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
76e479a6238224f1fe09c7c39909227813dfb7765dbb2be8cb7c1047b025aca1
|
|
| MD5 |
10bef02538da386086078aa98c061f0c
|
|
| BLAKE2b-256 |
93bb29fd6384e5ecafd8966620b27ea234a36a11557898d1f83b9e22b553dd52
|
Provenance
The following attestation bundles were made for vitalforge-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on blockhaven-ai/vitalforge
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
vitalforge-0.1.0-py3-none-any.whl -
Subject digest:
76e479a6238224f1fe09c7c39909227813dfb7765dbb2be8cb7c1047b025aca1 - Sigstore transparency entry: 1807520874
- Sigstore integration time:
-
Permalink:
blockhaven-ai/vitalforge@2d7b2beac0b7286ff84b1f81213b905890d0d99a -
Branch / Tag:
refs/heads/main - Owner: https://github.com/blockhaven-ai
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@2d7b2beac0b7286ff84b1f81213b905890d0d99a -
Trigger Event:
workflow_dispatch
-
Statement type: