Skip to content

Commit 6e4507a

Browse files
authored
Merge pull request #760 from synthetichealth/id-reproducibility
Reproducible IDs for FHIR resources and CCDA elements.
2 parents 89aa966 + 38bd88d commit 6e4507a

25 files changed

+710
-329
lines changed

src/main/java/org/mitre/synthea/datastore/DataStore.java

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import java.sql.SQLException;
1010
import java.util.Collection;
1111
import java.util.Map;
12-
import java.util.UUID;
1312
import java.util.concurrent.atomic.AtomicInteger;
1413

1514
import org.mitre.synthea.helpers.Utilities;
@@ -283,7 +282,7 @@ public boolean store(Person p) {
283282
stmt.executeBatch();
284283

285284
for (Encounter encounter : p.record.encounters) {
286-
String encounterID = UUID.randomUUID().toString();
285+
String encounterID = p.randUUID().toString();
287286

288287
String providerID = null;
289288

@@ -343,7 +342,7 @@ public boolean store(Person p) {
343342
}
344343

345344
for (Report report : encounter.reports) {
346-
String reportID = UUID.randomUUID().toString();
345+
String reportID = p.randUUID().toString();
347346

348347
// CREATE TABLE IF NOT EXISTS REPORT (id varchar, person_id varchar, encounter_id varchar,
349348
// name varchar, type varchar, start bigint, code varchar, display varchar, system
@@ -475,7 +474,7 @@ public boolean store(Person p) {
475474
"INSERT INTO MEDICATION "
476475
+ "(id, person_id, provider_id, name, type, start, stop, code, display, system) "
477476
+ "VALUES (?,?,?,?,?,?,?,?,?,?);");
478-
String medicationID = UUID.randomUUID().toString();
477+
String medicationID = p.randUUID().toString();
479478
stmt.setString(1, medicationID);
480479
stmt.setString(2, personID);
481480
stmt.setString(3, providerID);
@@ -501,7 +500,7 @@ public boolean store(Person p) {
501500
"INSERT INTO CLAIM "
502501
+ "(id, person_id, encounter_id, medication_id, time, cost) "
503502
+ "VALUES (?,?,?,?,?,?)");
504-
stmt.setString(1, UUID.randomUUID().toString());
503+
stmt.setString(1, p.randUUID().toString());
505504
stmt.setString(2, personID);
506505
stmt.setString(3, encounterID);
507506
stmt.setString(4, medicationID);
@@ -544,7 +543,7 @@ public boolean store(Person p) {
544543
"INSERT INTO careplan "
545544
+ "(id, person_id, provider_id, name, type, start, stop, code, display, system) "
546545
+ "VALUES (?,?,?,?,?,?,?,?,?,?);");
547-
stmt.setString(1, UUID.randomUUID().toString());
546+
stmt.setString(1, p.randUUID().toString());
548547
stmt.setString(2, personID);
549548
if (encounter.provider == null) {
550549
stmt.setString(3, null);
@@ -580,7 +579,7 @@ public boolean store(Person p) {
580579
+ "(id, uid, person_id, encounter_id, start, modality_code, modality_display, "
581580
+ "modality_system, bodysite_code, bodysite_display, bodysite_system, sop_class) "
582581
+ "VALUES (?,?,?,?,?,?,?,?,?,?,?,?);");
583-
stmt.setString(1, UUID.randomUUID().toString());
582+
stmt.setString(1, p.randUUID().toString());
584583
stmt.setString(2, imagingStudy.dicomUid);
585584
stmt.setString(3, personID);
586585
stmt.setString(4, encounterID);
@@ -608,7 +607,7 @@ public boolean store(Person p) {
608607
"INSERT INTO CLAIM "
609608
+ "(id, person_id, encounter_id, medication_id, time, cost) "
610609
+ "VALUES (?,?,?,?,?,?)");
611-
stmt.setString(1, UUID.randomUUID().toString());
610+
stmt.setString(1, p.randUUID().toString());
612611
stmt.setString(2, personID);
613612
stmt.setString(3, encounterID);
614613
stmt.setString(4, null);

src/main/java/org/mitre/synthea/engine/Generator.java

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.mitre.synthea.export.CDWExporter;
2929
import org.mitre.synthea.export.Exporter;
3030
import org.mitre.synthea.helpers.Config;
31+
import org.mitre.synthea.helpers.RandomNumberGenerator;
3132
import org.mitre.synthea.helpers.TransitionMetrics;
3233
import org.mitre.synthea.helpers.Utilities;
3334
import org.mitre.synthea.modules.DeathModule;
@@ -45,7 +46,7 @@
4546
/**
4647
* Generator creates a population by running the generic modules each timestep per Person.
4748
*/
48-
public class Generator {
49+
public class Generator implements RandomNumberGenerator {
4950

5051
public DataStore database;
5152
public GeneratorOptions options;
@@ -360,6 +361,7 @@ public void run() {
360361
* simulation. This means that if in the course of the simulation the person dies, a new person
361362
* will be started to replace them.
362363
* The seed used to generate the person is randomized as well.
364+
* Note that this method is only used by unit tests.
363365
*
364366
* @param index Target index in the whole set of people to generate
365367
* @return generated Person
@@ -667,4 +669,54 @@ private Predicate<String> getModulePredicate() {
667669
IOCase.INSENSITIVE);
668670
return path -> filenameFilter.accept(null, path);
669671
}
672+
673+
/**
674+
* Returns a random double.
675+
*/
676+
public double rand() {
677+
return random.nextDouble();
678+
}
679+
680+
/**
681+
* Returns a random boolean.
682+
*/
683+
public boolean randBoolean() {
684+
return random.nextBoolean();
685+
}
686+
687+
/**
688+
* Returns a random integer.
689+
*/
690+
public int randInt() {
691+
return random.nextInt();
692+
}
693+
694+
/**
695+
* Returns a random integer in the given bound.
696+
*/
697+
public int randInt(int bound) {
698+
return random.nextInt(bound);
699+
}
700+
701+
/**
702+
* Returns a double from a normal distribution.
703+
*/
704+
public double randGaussian() {
705+
return random.nextGaussian();
706+
}
707+
708+
/**
709+
* Return a random long.
710+
*/
711+
public long randLong() {
712+
return random.nextLong();
713+
}
714+
715+
/**
716+
* Return a random UUID.
717+
*/
718+
public UUID randUUID() {
719+
return new UUID(randLong(), randLong());
720+
}
721+
670722
}

src/main/java/org/mitre/synthea/export/CCDAExporter.java

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import java.io.Serializable;
88
import java.io.StringWriter;
9-
import java.util.UUID;
9+
import org.mitre.synthea.helpers.RandomNumberGenerator;
1010

1111
import org.mitre.synthea.world.agents.Person;
1212
import org.mitre.synthea.world.concepts.HealthRecord.Encounter;
@@ -20,18 +20,21 @@ public class CCDAExporter {
2020
private static final Configuration TEMPLATES = templateConfiguration();
2121

2222
/**
23-
* This is a dummy class and object for FreeMarker, because the library cannot access static
24-
* class methods such as UUID.randomUUID()
23+
* This is a dummy class and object for FreeMarker templates that create IDs.
2524
*/
2625
private static class UUIDGenerator implements Serializable {
26+
private RandomNumberGenerator rand;
27+
28+
public UUIDGenerator(RandomNumberGenerator rand) {
29+
this.rand = rand;
30+
}
31+
2732
@Override
2833
public String toString() {
29-
return UUID.randomUUID().toString();
34+
return rand.randUUID().toString();
3035
}
3136
}
3237

33-
private static final Object UUID_GEN = new UUIDGenerator();
34-
3538
private static Configuration templateConfiguration() {
3639
Configuration configuration = new Configuration(Configuration.VERSION_2_3_26);
3740
configuration.setDefaultEncoding("UTF-8");
@@ -82,7 +85,7 @@ public static String export(Person person, long time) {
8285

8386
// The export templates fill in the record by accessing the attributes
8487
// of the Person, so we add a few attributes just for the purposes of export.
85-
person.attributes.put("UUID", UUID_GEN);
88+
person.attributes.put("UUID", new UUIDGenerator(person));
8689
person.attributes.put("ehr_encounters", person.record.encounters);
8790
person.attributes.put("ehr_observations", superEncounter.observations);
8891
person.attributes.put("ehr_reports", superEncounter.reports);

src/main/java/org/mitre/synthea/export/CPCDSExporter.java

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import java.util.UUID;
1919

2020
import org.mitre.synthea.helpers.Config;
21+
import org.mitre.synthea.helpers.RandomNumberGenerator;
2122
import org.mitre.synthea.world.agents.Person;
2223
import org.mitre.synthea.world.concepts.HealthRecord.CarePlan;
2324
import org.mitre.synthea.world.concepts.HealthRecord.Code;
@@ -232,8 +233,8 @@ public void export(Person person, long time) throws IOException {
232233
long end = 0;
233234

234235
for (Encounter encounter : person.record.encounters) {
235-
String encounterID = UUID.randomUUID().toString();
236-
UUID medRecordNumber = UUID.randomUUID();
236+
String encounterID = person.randUUID().toString();
237+
UUID medRecordNumber = person.randUUID();
237238
CPCDSAttributes encounterAttributes = new CPCDSAttributes(encounter);
238239

239240

@@ -256,9 +257,9 @@ public void export(Person person, long time) throws IOException {
256257
if (start == 999999999999999999L) {
257258
start = end;
258259
}
259-
String coverageID = coverage(personID, start, end, payerId, type, groupId, groupName,
260+
String coverageID = coverage(person, personID, start, end, payerId, type, groupId, groupName,
260261
planName, planId);
261-
claim(encounter, personID, encounterID, medRecordNumber, encounterAttributes, payerId,
262+
claim(person, encounter, personID, encounterID, medRecordNumber, encounterAttributes, payerId,
262263
coverageID);
263264
hospital(encounter, encounterAttributes, payerName);
264265
}
@@ -326,16 +327,18 @@ private String patient(Person person, long time) throws IOException {
326327
/**
327328
* Write a single Coverage CPCDS file.
328329
*
330+
* @param rand Source of randomness to use when generating ids etc
329331
* @param personID ID of the person prescribed the careplan.
330332
* @param encounterID ID of the encounter where the careplan was prescribed
331333
* @param careplan The careplan itself
332334
* @throws IOException if any IO error occurs
333335
*/
334-
private String coverage(String personID, long start, long stop, String payerId, String type,
335-
UUID groupId, String groupName, String name, String planId) throws IOException {
336+
private String coverage(RandomNumberGenerator rand, String personID, long start, long stop,
337+
String payerId, String type, UUID groupId, String groupName, String name,
338+
String planId) throws IOException {
336339

337340
StringBuilder s = new StringBuilder();
338-
String coverageID = UUID.randomUUID().toString();
341+
String coverageID = rand.randUUID().toString();
339342
s.append(coverageID).append(',');
340343
s.append(personID).append(',');
341344
s.append(personID).append(',');
@@ -370,6 +373,7 @@ private String coverage(String personID, long start, long stop, String payerId,
370373
* Method to write a single Claims file. Take an encounter in the parameters and
371374
* processes Diagnoses, Procedures, and Pharmacy claims for each one, in order.
372375
*
376+
* @param rand Source of randomness to use when generating ids etc
373377
* @param encounter The encounter object itself
374378
* @param personID The Id of the involved patient
375379
* @param encounterID The Id of the encounter
@@ -378,8 +382,9 @@ private String coverage(String personID, long start, long stop, String payerId,
378382
* @param payerId The Id of the payer
379383
* @throws IOException Throws this exception
380384
*/
381-
private void claim(Encounter encounter, String personID, String encounterID, UUID medRecordNumber,
382-
CPCDSAttributes attributes, String payerId, String coverageID) throws IOException {
385+
private void claim(RandomNumberGenerator rand, Encounter encounter, String personID,
386+
String encounterID, UUID medRecordNumber, CPCDSAttributes attributes, String payerId,
387+
String coverageID) throws IOException {
383388

384389
StringBuilder s = new StringBuilder();
385390

@@ -742,7 +747,7 @@ private void claim(Encounter encounter, String personID, String encounterID, UUI
742747
* dayMultiplier.get(duration.get("unit").getAsString());
743748
}
744749

745-
UUID rxRef = UUID.randomUUID();
750+
UUID rxRef = rand.randUUID();
746751

747752
String[] serviceTypeList = { "01", "04", "06" };
748753
String serviceType = serviceTypeList[(int) randomLongWithBounds(0, 2)];

src/main/java/org/mitre/synthea/export/CSVExporter.java

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@
1919
import java.util.GregorianCalendar;
2020
import java.util.Locale;
2121
import java.util.Map;
22-
import java.util.UUID;
2322
import java.util.concurrent.atomic.AtomicInteger;
2423

2524
import org.mitre.synthea.helpers.Config;
25+
import org.mitre.synthea.helpers.RandomNumberGenerator;
2626
import org.mitre.synthea.helpers.Utilities;
2727
import org.mitre.synthea.modules.QualityOfLifeModule;
2828
import org.mitre.synthea.world.agents.Clinician;
@@ -379,7 +379,7 @@ public void export(Person person, long time) throws IOException {
379379

380380
for (Encounter encounter : person.record.encounters) {
381381

382-
String encounterID = encounter(personID, encounter);
382+
String encounterID = encounter(person, personID, encounter);
383383
String payerID = encounter.claim.payer.uuid;
384384

385385
for (HealthRecord.Entry condition : encounter.conditions) {
@@ -407,11 +407,11 @@ public void export(Person person, long time) throws IOException {
407407
}
408408

409409
for (CarePlan careplan : encounter.careplans) {
410-
careplan(personID, encounterID, careplan);
410+
careplan(person, personID, encounterID, careplan);
411411
}
412412

413413
for (ImagingStudy imagingStudy : encounter.imagingStudies) {
414-
imagingStudy(personID, encounterID, imagingStudy);
414+
imagingStudy(person, personID, encounterID, imagingStudy);
415415
}
416416

417417
for (Device device : encounter.devices) {
@@ -544,17 +544,19 @@ private String patient(Person person, long time) throws IOException {
544544
/**
545545
* Write a single Encounter line to encounters.csv.
546546
*
547+
* @param rand Source of randomness to use when generating ids etc
547548
* @param personID The ID of the person that had this encounter
548549
* @param encounter The encounter itself
549550
* @return The encounter ID, to be referenced as a "foreign key" if necessary
550551
* @throws IOException if any IO error occurs
551552
*/
552-
private String encounter(String personID, Encounter encounter) throws IOException {
553+
private String encounter(RandomNumberGenerator rand, String personID,
554+
Encounter encounter) throws IOException {
553555
// Id,START,STOP,PATIENT,ORGANIZATION,PROVIDER,PAYER,ENCOUNTERCLASS,CODE,DESCRIPTION,
554556
// BASE_ENCOUNTER_COST,TOTAL_CLAIM_COST,PAYER_COVERAGE,REASONCODE,REASONDESCRIPTION
555557
StringBuilder s = new StringBuilder();
556558

557-
String encounterID = UUID.randomUUID().toString();
559+
String encounterID = rand.randUUID().toString();
558560
// ID
559561
s.append(encounterID).append(',');
560562
// START
@@ -873,17 +875,18 @@ private void immunization(String personID, String encounterID,
873875
/**
874876
* Write a single CarePlan to careplans.csv.
875877
*
878+
* @param rand Source of randomness to use when generating ids etc
876879
* @param personID ID of the person prescribed the careplan.
877880
* @param encounterID ID of the encounter where the careplan was prescribed
878881
* @param careplan The careplan itself
879882
* @throws IOException if any IO error occurs
880883
*/
881-
private String careplan(String personID, String encounterID,
884+
private String careplan(RandomNumberGenerator rand, String personID, String encounterID,
882885
CarePlan careplan) throws IOException {
883886
// Id,START,STOP,PATIENT,ENCOUNTER,CODE,DESCRIPTION,REASONCODE,REASONDESCRIPTION
884887
StringBuilder s = new StringBuilder();
885888

886-
String careplanID = UUID.randomUUID().toString();
889+
String careplanID = rand.randUUID().toString();
887890
s.append(careplanID).append(',');
888891
s.append(dateFromTimestamp(careplan.start)).append(',');
889892
if (careplan.stop != 0L) {
@@ -915,18 +918,19 @@ private String careplan(String personID, String encounterID,
915918
/**
916919
* Write a single ImagingStudy to imaging_studies.csv.
917920
*
921+
* @param rand Source of randomness to use when generating ids etc
918922
* @param personID ID of the person the ImagingStudy was taken of.
919923
* @param encounterID ID of the encounter where the ImagingStudy was performed
920924
* @param imagingStudy The ImagingStudy itself
921925
* @throws IOException if any IO error occurs
922926
*/
923-
private String imagingStudy(String personID, String encounterID,
927+
private String imagingStudy(RandomNumberGenerator rand, String personID, String encounterID,
924928
ImagingStudy imagingStudy) throws IOException {
925929
// Id,DATE,PATIENT,ENCOUNTER,BODYSITE_CODE,BODYSITE_DESCRIPTION,
926930
// MODALITY_CODE,MODALITY_DESCRIPTION,SOP_CODE,SOP_DESCRIPTION
927931
StringBuilder s = new StringBuilder();
928932

929-
String studyID = UUID.randomUUID().toString();
933+
String studyID = rand.randUUID().toString();
930934
s.append(studyID).append(',');
931935
s.append(iso8601Timestamp(imagingStudy.start)).append(',');
932936
s.append(personID).append(',');

0 commit comments

Comments
 (0)