From f928250e0407c6d92e79a9105d10293fb5948c34 Mon Sep 17 00:00:00 2001 From: Jason Walonoski Date: Tue, 21 Jul 2020 14:56:27 -0400 Subject: [PATCH 1/2] Constrict randomness and add reference date. --- src/main/java/App.java | 8 + .../editors/GrowthDataErrorsEditor.java | 50 ++-- .../org/mitre/synthea/engine/Generator.java | 35 +-- .../synthea/engine/HealthRecordEditor.java | 4 +- .../synthea/engine/HealthRecordEditors.java | 8 +- .../java/org/mitre/synthea/engine/Module.java | 21 +- .../java/org/mitre/synthea/engine/State.java | 41 ++-- .../org/mitre/synthea/export/CDWExporter.java | 2 +- .../synthea/helpers/RandomCollection.java | 19 +- .../synthea/helpers/RandomValueGenerator.java | 6 +- .../helpers/TrendingValueGenerator.java | 2 +- .../org/mitre/synthea/helpers/Utilities.java | 14 +- .../modules/CardiovascularDiseaseModule.java | 4 + .../synthea/modules/EncounterModule.java | 6 +- .../modules/HealthInsuranceModule.java | 4 + .../synthea/modules/LifecycleModule.java | 56 ++--- .../synthea/modules/QualityOfLifeModule.java | 4 + .../synthea/modules/WeightLossModule.java | 9 +- .../mitre/synthea/world/agents/Person.java | 40 +++- .../mitre/synthea/world/agents/Provider.java | 15 +- .../world/agents/behaviors/IPayerFinder.java | 6 +- .../agents/behaviors/PayerFinderRandom.java | 2 +- .../world/concepts/BirthStatistics.java | 2 +- .../mitre/synthea/world/concepts/Costs.java | 7 +- .../synthea/world/concepts/HealthRecord.java | 25 +- .../synthea/world/concepts/NHANESSample.java | 5 + .../concepts/PediatricGrowthTrajectory.java | 50 ++-- .../synthea/world/geography/Location.java | 61 +++-- .../editors/GrowthDataErrorsEditorTest.java | 12 +- .../engine/HealthRecordEditorsTest.java | 8 +- .../org/mitre/synthea/engine/ModuleTest.java | 119 +++++++++- .../export/CodeResolveAndExportTest.java | 2 +- .../mitre/synthea/export/ExporterTest.java | 2 +- .../synthea/export/FHIRDSTU2ExporterTest.java | 2 +- .../synthea/export/FHIRR4ExporterTest.java | 4 +- .../synthea/export/FHIRSTU3ExporterTest.java | 2 +- .../export/ValueSetCodeResolverTest.java | 2 +- .../synthea/modules/EncounterModuleTest.java | 2 +- .../synthea/world/agents/PersonTest.java | 220 +++++++++++++++++- .../synthea/world/agents/ProviderTest.java | 22 +- .../world/concepts/NHANESSampleTest.java | 63 +++++ .../PediatricGrowthTrajectoryTest.java | 4 +- .../synthea/world/geography/LocationTest.java | 21 +- 43 files changed, 760 insertions(+), 231 deletions(-) diff --git a/src/main/java/App.java b/src/main/java/App.java index a1dfd3c05b..fbc61d7a51 100644 --- a/src/main/java/App.java +++ b/src/main/java/App.java @@ -1,9 +1,11 @@ import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; +import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.LinkedList; import java.util.Queue; +import java.util.TimeZone; import org.mitre.synthea.engine.Generator; import org.mitre.synthea.engine.Module; @@ -20,6 +22,7 @@ public class App { public static void usage() { System.out.println("Usage: run_synthea [options] [state [city]]"); System.out.println("Options: [-s seed] [-cs clinicianSeed] [-p populationSize]"); + System.out.println(" [-r referenceDate as YYYYMMDD]"); System.out.println(" [-g gender] [-a minAge-maxAge]"); System.out.println(" [-o overflowPopulation]"); System.out.println(" [-m moduleFileWildcardList]"); @@ -69,6 +72,11 @@ public static void main(String[] args) throws Exception { } else if (currArg.equalsIgnoreCase("-cs")) { String value = argsQ.poll(); options.clinicianSeed = Long.parseLong(value); + } else if (currArg.equalsIgnoreCase("-r")) { + String value = argsQ.poll(); + SimpleDateFormat format = new SimpleDateFormat("YYYYMMDD"); + format.setTimeZone(TimeZone.getTimeZone("UTC")); + options.referenceTime = format.parse(value).getTime(); } else if (currArg.equalsIgnoreCase("-p")) { String value = argsQ.poll(); options.population = Integer.parseInt(value); diff --git a/src/main/java/org/mitre/synthea/editors/GrowthDataErrorsEditor.java b/src/main/java/org/mitre/synthea/editors/GrowthDataErrorsEditor.java index 4e0cd435b1..0d504665d8 100644 --- a/src/main/java/org/mitre/synthea/editors/GrowthDataErrorsEditor.java +++ b/src/main/java/org/mitre/synthea/editors/GrowthDataErrorsEditor.java @@ -3,7 +3,6 @@ import com.google.gson.Gson; import java.util.List; -import java.util.Random; import java.util.stream.Collectors; import org.mitre.synthea.engine.HealthRecordEditor; @@ -86,34 +85,32 @@ public boolean shouldRun(Person person, HealthRecord record, long time) { * @param person The Synthea person to check on whether the module should be run * @param encounters The encounters that took place during the last time step of the simulation * @param time The current time in the simulation - * @param random Random generator that should be used when randomness is needed */ - public void process(Person person, List encounters, long time, - Random random) { + public void process(Person person, List encounters, long time) { List encountersWithWeights = encountersWithObservationsOfCode(encounters, WEIGHT_LOINC_CODE); encountersWithWeights.forEach(e -> { - if (random.nextDouble() <= config.weightUnitErrorRate) { + if (person.rand() <= config.weightUnitErrorRate) { introduceWeightUnitError(e); recalculateBMI(e); } - if (random.nextDouble() <= config.weightTransposeErrorRate) { + if (person.rand() <= config.weightTransposeErrorRate) { introduceTransposeError(e, "weight"); recalculateBMI(e); } - if (random.nextDouble() <= config.weightSwitchErrorRate) { + if (person.rand() <= config.weightSwitchErrorRate) { introduceWeightSwitchError(e); recalculateBMI(e); } - if (random.nextDouble() <= config.weightExtremeErrorRate) { + if (person.rand() <= config.weightExtremeErrorRate) { introduceWeightExtremeError(e); recalculateBMI(e); } - if (random.nextDouble() <= config.weightDuplicateErrorRate) { - introduceWeightDuplicateError(e, random); + if (person.rand() <= config.weightDuplicateErrorRate) { + introduceWeightDuplicateError(e, person); recalculateBMI(e); } - if (random.nextDouble() <= config.weightCarriedForwardErrorRate) { + if (person.rand() <= config.weightCarriedForwardErrorRate) { introduceWeightCarriedForwardError(e); recalculateBMI(e); } @@ -122,31 +119,31 @@ public void process(Person person, List encounters, long List encountersWithHeights = encountersWithObservationsOfCode(encounters, HEIGHT_LOINC_CODE); encountersWithHeights.forEach(e -> { - if (random.nextDouble() <= config.heightUnitErrorRate) { + if (person.rand() <= config.heightUnitErrorRate) { introduceHeightUnitError(e); recalculateBMI(e); } - if (random.nextDouble() <= config.heightTransposeErrorRate) { + if (person.rand() <= config.heightTransposeErrorRate) { introduceTransposeError(e, "height"); recalculateBMI(e); } - if (random.nextDouble() <= config.heightSwitchErrorRate) { + if (person.rand() <= config.heightSwitchErrorRate) { introduceHeightSwitchError(e); recalculateBMI(e); } - if (random.nextDouble() <= config.heightExtremeErrorRate) { + if (person.rand() <= config.heightExtremeErrorRate) { introduceHeightExtremeError(e); recalculateBMI(e); } - if (random.nextDouble() <= config.heightAbsoluteErrorRate) { - introduceHeightAbsoluteError(e, random); + if (person.rand() <= config.heightAbsoluteErrorRate) { + introduceHeightAbsoluteError(e, person); recalculateBMI(e); } - if (random.nextDouble() <= config.heightDuplicateErrorRate) { - introduceHeightDuplicateError(e, random); + if (person.rand() <= config.heightDuplicateErrorRate) { + introduceHeightDuplicateError(e, person); recalculateBMI(e); } - if (random.nextDouble() <= config.heightCarriedForwardErrorRate) { + if (person.rand() <= config.heightCarriedForwardErrorRate) { introduceHeightCarriedForwardError(e); recalculateBMI(e); } @@ -273,10 +270,11 @@ public static void introduceHeightExtremeError(HealthRecord.Encounter encounter) * shoes before getting measured. * @param encounter The encounter that contains the observation */ - public static void introduceHeightAbsoluteError(HealthRecord.Encounter encounter, Random random) { + public static void introduceHeightAbsoluteError(HealthRecord.Encounter encounter, + Person person) { HealthRecord.Observation htObs = heightObservation(encounter); double heightValue = (Double) htObs.value; - double additionalAbsolute = random.nextDouble() * 3; + double additionalAbsolute = person.rand() * 3; htObs.value = heightValue - (3 + additionalAbsolute); } @@ -285,10 +283,10 @@ public static void introduceHeightAbsoluteError(HealthRecord.Encounter encounter * @param encounter The encounter that contains the observation */ public static void introduceWeightDuplicateError(HealthRecord.Encounter encounter, - Random random) { + Person person) { HealthRecord.Observation wtObs = weightObservation(encounter); double weightValue = (Double) wtObs.value; - double jitter = random.nextDouble() - 0.5; + double jitter = person.rand() - 0.5; HealthRecord.Observation newObs = encounter.addObservation(wtObs.start, wtObs.type, weightValue + jitter, "Body Weight"); newObs.category = "vital-signs"; @@ -300,10 +298,10 @@ public static void introduceWeightDuplicateError(HealthRecord.Encounter encounte * @param encounter The encounter that contains the observation */ public static void introduceHeightDuplicateError(HealthRecord.Encounter encounter, - Random random) { + Person person) { HealthRecord.Observation htObs = heightObservation(encounter); double heightValue = (Double) htObs.value; - double jitter = random.nextDouble() - 0.5; + double jitter = person.rand() - 0.5; HealthRecord.Observation newObs = encounter.addObservation(htObs.start, htObs.type, heightValue + jitter, "Body Height"); newObs.category = "vital-signs"; diff --git a/src/main/java/org/mitre/synthea/engine/Generator.java b/src/main/java/org/mitre/synthea/engine/Generator.java index 6630d656f3..e619e6fca9 100644 --- a/src/main/java/org/mitre/synthea/engine/Generator.java +++ b/src/main/java/org/mitre/synthea/engine/Generator.java @@ -52,6 +52,7 @@ public class Generator { private Random random; public long timestep; public long stop; + public long referenceTime; public Map stats; public Location location; private AtomicInteger totalGeneratedPopulation; @@ -108,6 +109,8 @@ public static class GeneratorOptions { * value of -1 will evolve the population to the current system time. */ public int daysToTravelForward = -1; + /** Reference Time when to start Synthea. By default equal to the current system time. */ + public long referenceTime = seed; } /** @@ -197,6 +200,7 @@ private void init() { this.random = new Random(options.seed); this.timestep = Long.parseLong(Config.get("generate.timestep")); this.stop = System.currentTimeMillis(); + this.referenceTime = options.referenceTime; this.location = new Location(options.state, options.city); @@ -241,8 +245,10 @@ private void init() { locationName = options.city + ", " + options.state; } System.out.println("Running with options:"); - System.out.println(String.format("Population: %d\nSeed: %d\nProvider Seed:%d\nLocation: %s", - options.population, options.seed, options.clinicianSeed, locationName)); + System.out.println(String.format( + "Population: %d\nSeed: %d\nProvider Seed:%d\nReference Time: %d\nLocation: %s", + options.population, options.seed, options.clinicianSeed, options.referenceTime, + locationName)); System.out.println(String.format("Min Age: %d\nMax Age: %d", options.minAge, options.maxAge)); if (options.gender != null) { @@ -393,7 +399,7 @@ public Person generatePerson(int index, long personSeed) { if (isAlive && onlyDeadPatients) { // rotate the seed so the next attempt gets a consistent but different one - personSeed = new Random(personSeed).nextLong(); + personSeed = randomForDemographics.nextLong(); continue; // skip the other stuff if the patient is alive and we only want dead patients // note that this skips ahead to the while check and doesn't automatically re-loop @@ -401,7 +407,7 @@ public Person generatePerson(int index, long personSeed) { if (!isAlive && onlyAlivePatients) { // rotate the seed so the next attempt gets a consistent but different one - personSeed = new Random(personSeed).nextLong(); + personSeed = randomForDemographics.nextLong(); continue; // skip the other stuff if the patient is dead and we only want alive patients // note that this skips ahead to the while check and doesn't automatically re-loop @@ -412,7 +418,7 @@ public Person generatePerson(int index, long personSeed) { tryNumber++; if (!isAlive) { // rotate the seed so the next attempt gets a consistent but different one - personSeed = new Random(personSeed).nextLong(); + personSeed = randomForDemographics.nextLong(); // if we've tried and failed > 10 times to generate someone over age 90 // and the options allow for ages as low as 85 @@ -470,23 +476,20 @@ public void updatePerson(Person person) { long time = person.lastUpdated; while (person.alive(time) && time < stop) { - healthInsuranceModule.process(person, time + timestep); encounterModule.process(person, time); Iterator iter = person.currentModules.iterator(); while (iter.hasNext()) { Module module = iter.next(); - // System.out.format("Processing module %s\n", module.name); + if (module.process(person, time)) { - // System.out.format("Removing module %s\n", module.name); iter.remove(); // this module has completed/terminated. } } encounterModule.endEncounterModuleEncounters(person, time); person.lastUpdated = time; - HealthRecordEditors.getInstance().executeAll( - person, person.record, time, timestep, person.random); + HealthRecordEditors.getInstance().executeAll(person, person.record, time, timestep); time += timestep; } @@ -517,12 +520,12 @@ public Person createPerson(long personSeed, Map demoAttributes) /** * Create a set of random demographics. - * @param seed The random seed to use + * @param random The random number generator to use. * @return demographics */ - public Map randomDemographics(Random seed) { - Demographics city = location.randomCity(seed); - Map demoAttributes = pickDemographics(seed, city); + public Map randomDemographics(Random random) { + Demographics city = location.randomCity(random); + Map demoAttributes = pickDemographics(random, city); return demoAttributes; } @@ -650,8 +653,8 @@ private Map pickDemographics(Random random, Demographics city) { } private long birthdateFromTargetAge(long targetAge, Random random) { - long earliestBirthdate = stop - TimeUnit.DAYS.toMillis((targetAge + 1) * 365L + 1); - long latestBirthdate = stop - TimeUnit.DAYS.toMillis(targetAge * 365L); + long earliestBirthdate = referenceTime - TimeUnit.DAYS.toMillis((targetAge + 1) * 365L + 1); + long latestBirthdate = referenceTime - TimeUnit.DAYS.toMillis(targetAge * 365L); return (long) (earliestBirthdate + ((latestBirthdate - earliestBirthdate) * random.nextDouble())); } diff --git a/src/main/java/org/mitre/synthea/engine/HealthRecordEditor.java b/src/main/java/org/mitre/synthea/engine/HealthRecordEditor.java index 7ed7cd073d..e261e23671 100644 --- a/src/main/java/org/mitre/synthea/engine/HealthRecordEditor.java +++ b/src/main/java/org/mitre/synthea/engine/HealthRecordEditor.java @@ -1,7 +1,6 @@ package org.mitre.synthea.engine; import java.util.List; -import java.util.Random; import org.mitre.synthea.world.agents.Person; import org.mitre.synthea.world.concepts.HealthRecord; @@ -45,7 +44,6 @@ public interface HealthRecordEditor { * @param person The Synthea person that will have their HealthRecord edited * @param encounters The encounters that took place during the last time step of the simulation * @param time The current time in the simulation - * @param random Random generator that should be used when randomness is needed */ - void process(Person person, List encounters, long time, Random random); + void process(Person person, List encounters, long time); } diff --git a/src/main/java/org/mitre/synthea/engine/HealthRecordEditors.java b/src/main/java/org/mitre/synthea/engine/HealthRecordEditors.java index a2c98a06a2..b2511418df 100644 --- a/src/main/java/org/mitre/synthea/engine/HealthRecordEditors.java +++ b/src/main/java/org/mitre/synthea/engine/HealthRecordEditors.java @@ -2,7 +2,6 @@ import java.util.ArrayList; import java.util.List; -import java.util.Random; import java.util.stream.Collectors; import org.mitre.synthea.world.agents.Person; @@ -45,13 +44,12 @@ public void registerEditor(HealthRecordEditor editor) { *

* It's unlikely that this method should be called by anything outside of Generator. *

- * @param person The Person to run on + * @param person The Person to run on, and the source of randomness * @param record The HealthRecord to potentially modify * @param time The current time in the simulation * @param step The time step for the simulation - * @param random The source of randomness that modules should use */ - public void executeAll(Person person, HealthRecord record, long time, long step, Random random) { + public void executeAll(Person person, HealthRecord record, long time, long step) { if (this.registeredEditors.size() > 0) { long start = time - step; List encountersThisStep = record.encounters.stream() @@ -59,7 +57,7 @@ public void executeAll(Person person, HealthRecord record, long time, long step, .collect(Collectors.toList()); this.registeredEditors.forEach(m -> { if (m.shouldRun(person, record, time)) { - m.process(person, encountersThisStep, time, random); + m.process(person, encountersThisStep, time); } }); } diff --git a/src/main/java/org/mitre/synthea/engine/Module.java b/src/main/java/org/mitre/synthea/engine/Module.java index ed57f3b820..dbf04b49ab 100644 --- a/src/main/java/org/mitre/synthea/engine/Module.java +++ b/src/main/java/org/mitre/synthea/engine/Module.java @@ -52,7 +52,7 @@ * across the population, it is important that States are cloned before they are executed. * This keeps the "master" copy of the module clean. */ -public class Module implements Serializable { +public class Module implements Cloneable, Serializable { private static final Configuration JSON_PATH_CONFIG = Configuration.builder() .jsonProvider(new GsonJsonProvider()) @@ -284,6 +284,23 @@ public Module(JsonObject definition, boolean submodule) throws Exception { } } + /** + * Clone this module. Never provide the original. + */ + public Module clone() { + Module clone = new Module(); + clone.name = this.name; + clone.submodule = this.submodule; + clone.remarks = this.remarks; + clone.states = new ConcurrentHashMap(); + if (this.states != null) { + for (String key : this.states.keySet()) { + clone.states.put(key, this.states.get(key).clone()); + } + } + return clone; + } + /** * Process this Module with the given Person at the specified time within the simulation. * @@ -423,7 +440,7 @@ public synchronized Module get() { if (fault != null) { throw new RuntimeException(fault); } - return module; + return module.clone(); } } } diff --git a/src/main/java/org/mitre/synthea/engine/State.java b/src/main/java/org/mitre/synthea/engine/State.java index 708cfc5e7b..dd712ef676 100644 --- a/src/main/java/org/mitre/synthea/engine/State.java +++ b/src/main/java/org/mitre/synthea/engine/State.java @@ -168,7 +168,6 @@ public Transition getTransition() { * should halt for this time step. */ public boolean run(Person person, long time) { - // System.out.format("State: %s\n", this.name); if (!person.alive(time)) { return false; } @@ -337,7 +336,6 @@ private void setup() { @Override public Physiology clone() { - super.clone(); Physiology clone = (Physiology) super.clone(); clone.model = model; clone.solver = solver; @@ -345,6 +343,7 @@ public Physiology clone() { clone.simDuration = simDuration; clone.leadTime = leadTime; clone.altDirectTransition = altDirectTransition; + clone.altTransition = altTransition; List inputList = new ArrayList(inputs.size()); for (IoMapper mapper : inputs) { @@ -357,9 +356,11 @@ public Physiology clone() { outputList.add(new IoMapper(mapper)); } clone.outputs = outputList; - - clone.setup(); - + + if (ENABLE_PHYSIOLOGY_STATE) { + clone.setup(); + } + return clone; } @@ -470,7 +471,8 @@ public boolean process(Person person, long time) { } else if (range != null) { // use a range this.next = - time + Utilities.convertTime(range.unit, (long) person.rand(range.low, range.high)); + time + Utilities.convertTime(range.unit, + (long) person.rand(range.low, range.high)); } else { throw new RuntimeException("Delay state has no exact or range: " + this); } @@ -570,7 +572,8 @@ public SetAttribute clone() { clone.value = value; clone.range = range; clone.expression = expression; - clone.threadExpProcessor = threadExpProcessor; + // We shouldn't clone thread local objects since the application is multi-threaded. + // clone.threadExpProcessor = threadExpProcessor; clone.seriesData = seriesData; clone.period = period; return clone; @@ -1370,7 +1373,8 @@ public boolean process(Person person, long time) { } if (duration != null) { double durationVal = person.rand(duration.low, duration.high); - procedure.stop = procedure.start + Utilities.convertTime(duration.unit, (long) durationVal); + procedure.stop = procedure.start + + Utilities.convertTime(duration.unit, (long) durationVal); } // increment number of procedures by respective hospital Provider provider; @@ -1433,7 +1437,8 @@ public VitalSign clone() { clone.vitalSign = vitalSign; clone.unit = unit; clone.expression = expression; - clone.threadExpProcessor = threadExpProcessor; + // We shouldn't clone thread local objects since the application is multi-threaded. + // clone.threadExpProcessor = threadExpProcessor; return clone; } @@ -1533,7 +1538,8 @@ public Observation clone() { clone.category = category; clone.unit = unit; clone.expression = expression; - clone.threadExpProcessor = threadExpProcessor; + // We shouldn't clone thread local objects since the application is multi-threaded. + // clone.threadExpProcessor = threadExpProcessor; clone.attachment = attachment; return clone; } @@ -1691,8 +1697,8 @@ public ImagingStudy clone() { @Override public boolean process(Person person, long time) { // Randomly pick number of series and instances if bounds were provided - duplicateSeries(person); - duplicateInstances(person); + duplicateSeries(person, time); + duplicateInstances(person, time); // The modality code of the first series is a good approximation // of the type of ImagingStudy this is @@ -1708,7 +1714,7 @@ public boolean process(Person person, long time) { return true; } - private void duplicateSeries(Person person) { + private void duplicateSeries(Person person, long time) { if (minNumberSeries > 0 && maxNumberSeries >= minNumberSeries && series.size() > 0) { @@ -1720,7 +1726,7 @@ private void duplicateSeries(Person person) { // Create the new series with random series UID for (int i = 0; i < numberOfSeries; i++) { HealthRecord.ImagingStudy.Series newSeries = referenceSeries.clone(); - newSeries.dicomUid = Utilities.randomDicomUid(i + 1, 0); + newSeries.dicomUid = Utilities.randomDicomUid(person, time, i + 1, 0); series.add(newSeries); } } else { @@ -1734,21 +1740,22 @@ private void duplicateSeries(Person person) { } } - private void duplicateInstances(Person person) { + private void duplicateInstances(Person person, long time) { for (int i = 0; i < series.size(); i++) { HealthRecord.ImagingStudy.Series s = series.get(i); if (s.minNumberInstances > 0 && s.maxNumberInstances >= s.minNumberInstances && s.instances.size() > 0) { // Randomly pick the number of instances in this series - int numberOfInstances = (int) person.rand(s.minNumberInstances, s.maxNumberInstances + 1); + int numberOfInstances = + (int) person.rand(s.minNumberInstances, s.maxNumberInstances + 1); HealthRecord.ImagingStudy.Instance referenceInstance = s.instances.get(0); s.instances = new ArrayList(); // Create the new instances with random instance UIDs for (int j = 0; j < numberOfInstances; j++) { HealthRecord.ImagingStudy.Instance newInstance = referenceInstance.clone(); - newInstance.dicomUid = Utilities.randomDicomUid(i + 1, j + 1); + newInstance.dicomUid = Utilities.randomDicomUid(person, time, i + 1, j + 1); s.instances.add(newInstance); } } diff --git a/src/main/java/org/mitre/synthea/export/CDWExporter.java b/src/main/java/org/mitre/synthea/export/CDWExporter.java index aef7a4eef0..f52356d90e 100644 --- a/src/main/java/org/mitre/synthea/export/CDWExporter.java +++ b/src/main/java/org/mitre/synthea/export/CDWExporter.java @@ -722,7 +722,7 @@ private int patient(Person person, int sta3n, long time) throws IOException { s.append(NEWLINE); write(s.toString(), spatientphone); - if (person.random.nextBoolean()) { + if (person.randBoolean()) { // Add an email address s.setLength(0); s.append(getNextKey(spatientphone)).append(','); diff --git a/src/main/java/org/mitre/synthea/helpers/RandomCollection.java b/src/main/java/org/mitre/synthea/helpers/RandomCollection.java index c32319eb58..66ae1a87d2 100644 --- a/src/main/java/org/mitre/synthea/helpers/RandomCollection.java +++ b/src/main/java/org/mitre/synthea/helpers/RandomCollection.java @@ -6,6 +6,8 @@ import java.util.Random; import java.util.TreeMap; +import org.mitre.synthea.world.agents.Person; + /** * Random collection of objects, with weightings. Intended to be an equivalent to the ruby Pickup * gem. Adapted from https://stackoverflow.com/a/6409791/630384 @@ -38,7 +40,22 @@ public void add(double weight, E result) { * @return a random item from the collection weighted by the item weights. */ public E next(Random random) { - double value = random.nextDouble() * total; + return next(random.nextDouble() * total); + } + + /** + * Select an item from the collection at random by the weight of the items. + * Selecting an item from one draw, does not remove the item from the collection + * for subsequent draws. In other words, an item can be selected repeatedly if + * the weights are severely imbalanced. + * @param person - person object, and the source of the random number generator. + * @return a random item from the collection weighted by the item weights. + */ + public E next(Person person) { + return next(person.rand() * total); + } + + private E next(double value) { Entry entry = map.higherEntry(value); if (entry == null) { entry = map.lastEntry(); diff --git a/src/main/java/org/mitre/synthea/helpers/RandomValueGenerator.java b/src/main/java/org/mitre/synthea/helpers/RandomValueGenerator.java index 351eae5359..d75954fc04 100644 --- a/src/main/java/org/mitre/synthea/helpers/RandomValueGenerator.java +++ b/src/main/java/org/mitre/synthea/helpers/RandomValueGenerator.java @@ -10,7 +10,7 @@ public class RandomValueGenerator extends ValueGenerator { private double high; /** - * Createa new RandomValueGenerator. + * Create a new RandomValueGenerator. * @param person The person to generate data for. * @param low The lower bound for the generator. * @param high The upper bound for the generator. @@ -23,8 +23,6 @@ public RandomValueGenerator(Person person, double low, double high) { @Override public double getValue(long time) { - // TODO: Using the person.random could return a different value for the same timepoint. - // Use the time as seed instead for repeatability? - return person.rand(low, high); + return person.rand(low, high); } } diff --git a/src/main/java/org/mitre/synthea/helpers/TrendingValueGenerator.java b/src/main/java/org/mitre/synthea/helpers/TrendingValueGenerator.java index 9a21846d56..23b86aaa0d 100644 --- a/src/main/java/org/mitre/synthea/helpers/TrendingValueGenerator.java +++ b/src/main/java/org/mitre/synthea/helpers/TrendingValueGenerator.java @@ -92,7 +92,7 @@ private double nextValue(double mean) { double nextValue; do { - nextValue = person.random.nextGaussian() * standardDeviation + mean; + nextValue = person.randGaussian() * standardDeviation + mean; if ((minimumValue == null || nextValue >= minimumValue) && (maximumValue == null || nextValue <= maximumValue)) { diff --git a/src/main/java/org/mitre/synthea/helpers/Utilities.java b/src/main/java/org/mitre/synthea/helpers/Utilities.java index 375ea7ec3b..73ebca1b69 100644 --- a/src/main/java/org/mitre/synthea/helpers/Utilities.java +++ b/src/main/java/org/mitre/synthea/helpers/Utilities.java @@ -13,7 +13,6 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.Calendar; -import java.util.Random; import java.util.TimeZone; import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; @@ -21,6 +20,7 @@ import org.mitre.synthea.engine.Logic; import org.mitre.synthea.engine.State; +import org.mitre.synthea.world.agents.Person; import org.mitre.synthea.world.concepts.HealthRecord.Code; public class Utilities { @@ -331,12 +331,12 @@ public static Gson getGson() { * * @return a String DICOM UID */ - public static String randomDicomUid(int seriesNo, int instanceNo) { + public static String randomDicomUid(Person person, long time, int seriesNo, int instanceNo) { // Add a random salt to increase uniqueness - String salt = randomDicomUidSalt(); + String salt = randomDicomUidSalt(person); - String now = String.valueOf(System.currentTimeMillis()); + String now = String.valueOf(time); String uid = "1.2.840.99999999"; // 99999999 is an arbitrary organizational identifier if (seriesNo > 0) { @@ -354,13 +354,11 @@ public static String randomDicomUid(int seriesNo, int instanceNo) { * Generates a random string of 8 numbers to use as a salt for DICOM UIDs. * @return The 8-digit numeric salt, as a String */ - private static String randomDicomUidSalt() { - + private static String randomDicomUidSalt(Person person) { final int MIN = 10000000; final int MAX = 99999999; - Random rand = new Random(); - int saltInt = rand.nextInt(MAX - MIN + 1) + MIN; + int saltInt = person.randInt(MAX - MIN + 1) + MIN; return String.valueOf(saltInt); } diff --git a/src/main/java/org/mitre/synthea/modules/CardiovascularDiseaseModule.java b/src/main/java/org/mitre/synthea/modules/CardiovascularDiseaseModule.java index 1d0ce015dc..73aa104f03 100644 --- a/src/main/java/org/mitre/synthea/modules/CardiovascularDiseaseModule.java +++ b/src/main/java/org/mitre/synthea/modules/CardiovascularDiseaseModule.java @@ -28,6 +28,10 @@ public CardiovascularDiseaseModule() { this.name = "Cardiovascular Disease"; } + public Module clone() { + return this; + } + @Override public boolean process(Person person, long time) { if (!person.alive(time)) { diff --git a/src/main/java/org/mitre/synthea/modules/EncounterModule.java b/src/main/java/org/mitre/synthea/modules/EncounterModule.java index 0193f342db..c1bc46dfff 100644 --- a/src/main/java/org/mitre/synthea/modules/EncounterModule.java +++ b/src/main/java/org/mitre/synthea/modules/EncounterModule.java @@ -49,6 +49,10 @@ public EncounterModule() { this.name = "Encounter"; } + public Module clone() { + return this; + } + @Override public boolean process(Person person, long time) { if (!person.alive(time)) { @@ -144,7 +148,7 @@ public static Encounter createEncounter(Person person, long time, EncounterType prov.incrementEncounters(type, year); encounter.provider = prov; // assign a clinician - encounter.clinician = prov.chooseClinicianList(specialty, person.random); + encounter.clinician = prov.chooseClinicianList(specialty, person); return encounter; } diff --git a/src/main/java/org/mitre/synthea/modules/HealthInsuranceModule.java b/src/main/java/org/mitre/synthea/modules/HealthInsuranceModule.java index f340d0244c..8fb4e7c582 100644 --- a/src/main/java/org/mitre/synthea/modules/HealthInsuranceModule.java +++ b/src/main/java/org/mitre/synthea/modules/HealthInsuranceModule.java @@ -33,6 +33,10 @@ public class HealthInsuranceModule extends Module { */ public HealthInsuranceModule() {} + public Module clone() { + return this; + } + /** * Process this HealthInsuranceModule with the given Person at the specified * time within the simulation. diff --git a/src/main/java/org/mitre/synthea/modules/LifecycleModule.java b/src/main/java/org/mitre/synthea/modules/LifecycleModule.java index 83b9c4ebfc..2d30979e10 100644 --- a/src/main/java/org/mitre/synthea/modules/LifecycleModule.java +++ b/src/main/java/org/mitre/synthea/modules/LifecycleModule.java @@ -7,7 +7,6 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Random; import java.util.UUID; import java.util.concurrent.TimeUnit; @@ -95,6 +94,10 @@ private static RandomCollection loadSexualOrientationData() { return soDistribution; } + public Module clone() { + return this; + } + @Override public boolean process(Person person, long time) { if (!person.alive(time)) { @@ -133,8 +136,8 @@ public static void birth(Person person, long time) { attributes.put(Person.BIRTHDATE, time); String gender = (String) attributes.get(Person.GENDER); String language = (String) attributes.get(Person.FIRST_LANGUAGE); - String firstName = fakeFirstName(gender, language, person.random); - String lastName = fakeLastName(language, person.random); + String firstName = fakeFirstName(gender, language, person); + String lastName = fakeLastName(language, person); if (appendNumbersToNames) { firstName = addHash(firstName); lastName = addHash(lastName); @@ -143,15 +146,15 @@ public static void birth(Person person, long time) { attributes.put(Person.LAST_NAME, lastName); attributes.put(Person.NAME, firstName + " " + lastName); - String motherFirstName = fakeFirstName("F", language, person.random); - String motherLastName = fakeLastName(language, person.random); + String motherFirstName = fakeFirstName("F", language, person); + String motherLastName = fakeLastName(language, person); if (appendNumbersToNames) { motherFirstName = addHash(motherFirstName); motherLastName = addHash(motherLastName); } attributes.put(Person.NAME_MOTHER, motherFirstName + " " + motherLastName); - String fatherFirstName = fakeFirstName("M", language, person.random); + String fatherFirstName = fakeFirstName("M", language, person); if (appendNumbersToNames) { fatherFirstName = addHash(fatherFirstName); } @@ -180,10 +183,10 @@ public static void birth(Person person, long time) { person.attributes.put(Person.ZIP, location.getZipCode(city, person)); String[] birthPlace; if ("english".equalsIgnoreCase((String) attributes.get(Person.FIRST_LANGUAGE))) { - birthPlace = location.randomBirthPlace(person.random); + birthPlace = location.randomBirthPlace(person); } else { birthPlace = location.randomBirthplaceByLanguage( - person.random, (String) person.attributes.get(Person.FIRST_LANGUAGE)); + person, (String) person.attributes.get(Person.FIRST_LANGUAGE)); } attributes.put(Person.BIRTH_CITY, birthPlace[0]); attributes.put(Person.BIRTH_STATE, birthPlace[1]); @@ -193,7 +196,7 @@ public static void birth(Person person, long time) { } boolean hasStreetAddress2 = person.rand() < 0.5; - attributes.put(Person.ADDRESS, fakeAddress(hasStreetAddress2, person.random)); + attributes.put(Person.ADDRESS, fakeAddress(hasStreetAddress2, person)); attributes.put(Person.ACTIVE_WEIGHT_MANAGEMENT, false); // TODO: Why are the percentiles a vital sign? Sounds more like an attribute? @@ -205,7 +208,7 @@ public static void birth(Person person, long time) { person.attributes.put(Person.GROWTH_TRAJECTORY, pgt); // Temporarily generate a mother - Person mother = new Person(person.random.nextLong()); + Person mother = new Person(person.randLong()); mother.attributes.put(Person.GENDER, "F"); mother.attributes.put("pregnant", true); mother.attributes.put(Person.RACE, person.attributes.get(Person.RACE)); @@ -232,7 +235,7 @@ public static void birth(Person person, long time) { grow(person, time); // set initial height and weight from percentiles calculateVitalSigns(person, time); // Set initial values for many vital signs. - String orientation = sexualOrientationData.next(person.random); + String orientation = sexualOrientationData.next(person); attributes.put(Person.SEXUAL_ORIENTATION, orientation); // Setup vital signs which follow the generator approach @@ -263,11 +266,11 @@ private static void setupVitalSignGenerators(Person person) { * Generate a first name appropriate for a given gender and language. * @param gender Gender of the name, "M" or "F" * @param language Origin language of the name, "english", "spanish" - * @param random Random number generator to use. + * @param person person to generate a name for. * @return First name. */ @SuppressWarnings("unchecked") - public static String fakeFirstName(String gender, String language, Random random) { + public static String fakeFirstName(String gender, String language, Person person) { List choices; if ("spanish".equalsIgnoreCase(language)) { choices = (List) names.get("spanish." + gender); @@ -275,17 +278,17 @@ public static String fakeFirstName(String gender, String language, Random random choices = (List) names.get("english." + gender); } // pick a random item from the list - return choices.get(random.nextInt(choices.size())); + return choices.get(person.randInt(choices.size())); } /** * Generate a surname appropriate for a given language. * @param language Origin language of the name, "english", "spanish" - * @param random Random number generator to use. + * @param person person to generate a name for. * @return Surname or Family Name. */ @SuppressWarnings("unchecked") - public static String fakeLastName(String language, Random random) { + public static String fakeLastName(String language, Person person) { List choices; if ("spanish".equalsIgnoreCase(language)) { choices = (List) names.get("spanish.family"); @@ -293,30 +296,30 @@ public static String fakeLastName(String language, Random random) { choices = (List) names.get("english.family"); } // pick a random item from the list - return choices.get(random.nextInt(choices.size())); + return choices.get(person.randInt(choices.size())); } /** * Generate a Street Address. * @param includeLine2 Whether or not the address should have a second line, * which can take the form of an apartment, unit, or suite number. - * @param random Random number generator to use. + * @param person person to generate an address for. * @return First name. */ @SuppressWarnings("unchecked") - public static String fakeAddress(boolean includeLine2, Random random) { - int number = random.nextInt(1000) + 100; + public static String fakeAddress(boolean includeLine2, Person person) { + int number = person.randInt(1000) + 100; List n = (List)names.get("english.family"); // for now just use family names as the street name. // could expand with a few more but probably not worth it - String streetName = n.get(random.nextInt(n.size())); + String streetName = n.get(person.randInt(n.size())); List a = (List)names.get("street.type"); - String streetType = a.get(random.nextInt(a.size())); + String streetType = a.get(person.randInt(a.size())); if (includeLine2) { - int addtlNum = random.nextInt(100); + int addtlNum = person.randInt(100); List s = (List)names.get("street.secondary"); - String addtlType = s.get(random.nextInt(s.size())); + String addtlType = s.get(person.randInt(s.size())); return number + " " + streetName + " " + streetType + " " + addtlType + " " + addtlNum; } else { return number + " " + streetName + " " + streetType; @@ -352,7 +355,6 @@ private static boolean age(Person person, long time) { int newAgeMos = person.ageInMonths(time); person.attributes.put(AGE, newAge); person.attributes.put(AGE_MONTHS, newAgeMos); - switch (newAge) { case 16: // driver's license @@ -404,7 +406,7 @@ private static boolean age(Person person, long time) { person.attributes.put(Person.MAIDEN_NAME, person.attributes.get(Person.LAST_NAME)); String firstName = ((String) person.attributes.get(Person.FIRST_NAME)); String language = (String) person.attributes.get(Person.FIRST_LANGUAGE); - String newLastName = fakeLastName(language, person.random); + String newLastName = fakeLastName(language, person); if (appendNumbersToNames) { newLastName = addHash(newLastName); } @@ -512,7 +514,7 @@ private static double adjustWeight(Person person, long time) { weight = lookupGrowthChart("weight", gender, ageInMonths, person.getVitalSign(VitalSign.WEIGHT_PERCENTILE, time)); } else if (age < 20) { - double currentBMI = pgt.currentBMI(person, time, person.random); + double currentBMI = pgt.currentBMI(person, time); double height = growthChart.get(GrowthChart.ChartType.HEIGHT).lookUp(ageInMonths, gender, heightPercentile); weight = BMI.weightForHeightAndBMI(height, currentBMI); diff --git a/src/main/java/org/mitre/synthea/modules/QualityOfLifeModule.java b/src/main/java/org/mitre/synthea/modules/QualityOfLifeModule.java index 4b0e50eebc..48fc85bde5 100644 --- a/src/main/java/org/mitre/synthea/modules/QualityOfLifeModule.java +++ b/src/main/java/org/mitre/synthea/modules/QualityOfLifeModule.java @@ -36,6 +36,10 @@ public QualityOfLifeModule() { this.name = "Quality of Life"; } + public Module clone() { + return this; + } + @SuppressWarnings("unchecked") @Override public boolean process(Person person, long time) { diff --git a/src/main/java/org/mitre/synthea/modules/WeightLossModule.java b/src/main/java/org/mitre/synthea/modules/WeightLossModule.java index 4227173bce..8bb6fe7fb5 100644 --- a/src/main/java/org/mitre/synthea/modules/WeightLossModule.java +++ b/src/main/java/org/mitre/synthea/modules/WeightLossModule.java @@ -57,6 +57,9 @@ public WeightLossModule() { public static final double maxPedPercentileChange = (double) BiometricsConfig.get("max_ped_percentile_change", 0.1); + public Module clone() { + return this; + } @Override public boolean process(Person person, long time) { @@ -238,7 +241,7 @@ public void pediatricRegression(Person person, long time) { if (time + ONE_YEAR > pgt.tail().timeInSimulation) { GrowthChart bmiChart = growthChart.get(GrowthChart.ChartType.BMI); String gender = (String) person.attributes.get(Person.GENDER); - double bmiAtStart = pgt.currentBMI(person, start, person.random); + double bmiAtStart = pgt.currentBMI(person, start); double originalPercentile = bmiChart.percentileFor(startAgeInMonths, gender, bmiAtStart); double percentileChange = (double) person.attributes.get(WEIGHT_LOSS_BMI_PERCENTILE_CHANGE); int nextAgeInMonths = pgt.tail().ageInMonths + 12; @@ -265,7 +268,7 @@ public double transitionRegression(Person person, long time) { (PediatricGrowthTrajectory) person.attributes.get(Person.GROWTH_TRAJECTORY); long start = (long) person.attributes.get(WEIGHT_MANAGEMENT_START); int startAgeInMonths = person.ageInMonths(start); - double bmiAtStart = pgt.currentBMI(person, start, person.random); + double bmiAtStart = pgt.currentBMI(person, start); String gender = (String) person.attributes.get(Person.GENDER); double originalPercentile = bmiChart.percentileFor(startAgeInMonths, gender, bmiAtStart); double bmiForPercentileAtTwenty = bmiChart.lookUp(240, gender, originalPercentile); @@ -297,7 +300,7 @@ public void adjustBMIVectorForSuccessfulManagement(Person person) { PediatricGrowthTrajectory pgt = (PediatricGrowthTrajectory) person.attributes.get(Person.GROWTH_TRAJECTORY); double percentileChange = (double) person.attributes.get(WEIGHT_LOSS_BMI_PERCENTILE_CHANGE); - double bmiAtStart = pgt.currentBMI(person, start, person.random); + double bmiAtStart = pgt.currentBMI(person, start); double startPercentile = bmiChart.percentileFor(startAgeInMonths, gender, bmiAtStart); int currentTailAge = pgt.tail().ageInMonths; double currentTailBMI = pgt.tail().bmi; diff --git a/src/main/java/org/mitre/synthea/world/agents/Person.java b/src/main/java/org/mitre/synthea/world/agents/Person.java index 6e0a5bd96e..8ea2ea055a 100644 --- a/src/main/java/org/mitre/synthea/world/agents/Person.java +++ b/src/main/java/org/mitre/synthea/world/agents/Person.java @@ -13,10 +13,10 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Random; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import org.apache.commons.math3.random.JDKRandomGenerator; import org.mitre.synthea.engine.ExpressedConditionRecord; import org.mitre.synthea.engine.ExpressedSymptom; import org.mitre.synthea.engine.Module; @@ -87,7 +87,7 @@ public class Person implements Serializable, QuadTreeElement { private static final String DEDUCTIBLE = "deductible"; private static final String LAST_MONTH_PAID = "last_month_paid"; - public final JDKRandomGenerator random; + private final Random random; public final long seed; public long populationSeed; /** @@ -139,13 +139,13 @@ public class Person implements Serializable, QuadTreeElement { * Person constructor. */ public Person(long seed) { - this.seed = seed; // keep track of seed so it can be exported later - random = new JDKRandomGenerator((int) seed); + this.seed = seed; + random = new Random(seed); attributes = new ConcurrentHashMap(); vitalSigns = new ConcurrentHashMap(); - symptoms = new ConcurrentHashMap(); + symptoms = new ConcurrentHashMap(); /* initialized the onsetConditions field */ - onsetConditionRecord = new ExpressedConditionRecord(this); + onsetConditionRecord = new ExpressedConditionRecord(this); /* Chronic Medications which will be renewed at each Wellness Encounter */ chronicMedications = new ConcurrentHashMap(); hasMultipleRecords = @@ -168,7 +168,7 @@ record = defaultRecord; } /** - * Retuns a random double. + * Returns a random double. */ public double rand() { return random.nextDouble(); @@ -178,7 +178,7 @@ public double rand() { * Returns a random double in the given range. */ public double rand(double low, double high) { - return (low + ((high - low) * random.nextDouble())); + return (low + ((high - low) * rand())); } /** @@ -221,7 +221,8 @@ public double rand(double[] range) { * @return One of the options randomly selected. */ public String rand(String[] choices) { - return choices[random.nextInt(choices.length)]; + int value = random.nextInt(choices.length); + return choices[value]; } /** @@ -245,6 +246,13 @@ public double rand(int[] range) { return rand(range[0], range[1]); } + /** + * Returns a random boolean. + */ + public boolean randBoolean() { + return random.nextBoolean(); + } + /** * Returns a random integer. */ @@ -259,6 +267,20 @@ public int randInt(int bound) { return random.nextInt(bound); } + /** + * Returns a double from a normal distribution. + */ + public double randGaussian() { + return random.nextGaussian(); + } + + /** + * Return a random long. + */ + public long randLong() { + return random.nextLong(); + } + /** * Returns a person's age in Period form. */ diff --git a/src/main/java/org/mitre/synthea/world/agents/Provider.java b/src/main/java/org/mitre/synthea/world/agents/Provider.java index 449f651f2b..b61f734966 100644 --- a/src/main/java/org/mitre/synthea/world/agents/Provider.java +++ b/src/main/java/org/mitre/synthea/world/agents/Provider.java @@ -439,7 +439,8 @@ private Clinician generateClinician(long clinicianSeed, Random clinicianRand, long clinicianIdentifier, Provider provider) { Clinician clinician = null; try { - Demographics city = location.randomCity(clinicianRand); + Person doc = new Person(clinicianIdentifier); + Demographics city = location.randomCity(doc); Map out = new HashMap<>(); String race = city.pickRace(clinicianRand); @@ -464,8 +465,8 @@ private Clinician generateClinician(long clinicianSeed, Random clinicianRand, clinician.attributes.put(Person.ZIP, provider.zip); clinician.attributes.put(Person.COORDINATE, provider.coordinates); - String firstName = LifecycleModule.fakeFirstName(gender, language, clinician.random); - String lastName = LifecycleModule.fakeLastName(language, clinician.random); + String firstName = LifecycleModule.fakeFirstName(gender, language, doc); + String lastName = LifecycleModule.fakeLastName(language, doc); if (LifecycleModule.appendNumbersToNames) { firstName = LifecycleModule.addHash(firstName); @@ -486,13 +487,13 @@ private Clinician generateClinician(long clinicianSeed, Random clinicianRand, /** * Randomly chooses a clinician out of a given clinician list. - * @param specialty - the specialty to choose from - * @param random - random to help choose clinician + * @param specialty - the specialty to choose from. + * @param person - the patient. * @return A clinician with the required specialty. */ - public Clinician chooseClinicianList(String specialty, Random random) { + public Clinician chooseClinicianList(String specialty, Person person) { ArrayList clinicians = this.clinicianMap.get(specialty); - Clinician doc = clinicians.get(random.nextInt(clinicians.size())); + Clinician doc = clinicians.get(person.randInt(clinicians.size())); doc.incrementEncounters(); return doc; } diff --git a/src/main/java/org/mitre/synthea/world/agents/behaviors/IPayerFinder.java b/src/main/java/org/mitre/synthea/world/agents/behaviors/IPayerFinder.java index ce4edfb6b1..424c565124 100644 --- a/src/main/java/org/mitre/synthea/world/agents/behaviors/IPayerFinder.java +++ b/src/main/java/org/mitre/synthea/world/agents/behaviors/IPayerFinder.java @@ -1,7 +1,6 @@ package org.mitre.synthea.world.agents.behaviors; import java.util.List; -import java.util.Random; import org.mitre.synthea.modules.HealthInsuranceModule; import org.mitre.synthea.world.agents.Payer; @@ -52,15 +51,14 @@ public static boolean meetsBasicRequirements( * @param options the list of acceptable payer options that the person can recieve. * @return a random payer from the given list of options. */ - public default Payer chooseRandomlyFromList(List options) { + public default Payer chooseRandomlyFromList(List options, Person person) { if (options.isEmpty()) { return Payer.noInsurance; } else if (options.size() == 1) { return options.get(0); } else { // There are a few equally good options, pick one randomly. - Random r = new Random(); - return options.get(r.nextInt(options.size())); + return options.get(person.randInt(options.size())); } } } \ No newline at end of file diff --git a/src/main/java/org/mitre/synthea/world/agents/behaviors/PayerFinderRandom.java b/src/main/java/org/mitre/synthea/world/agents/behaviors/PayerFinderRandom.java index e7333ea705..dc4ba98b69 100644 --- a/src/main/java/org/mitre/synthea/world/agents/behaviors/PayerFinderRandom.java +++ b/src/main/java/org/mitre/synthea/world/agents/behaviors/PayerFinderRandom.java @@ -30,6 +30,6 @@ public Payer find(List payers, Person person, EncounterType service, long } } // Choose a payer from the list of options. - return chooseRandomlyFromList(options); + return chooseRandomlyFromList(options, person); } } \ No newline at end of file diff --git a/src/main/java/org/mitre/synthea/world/concepts/BirthStatistics.java b/src/main/java/org/mitre/synthea/world/concepts/BirthStatistics.java index 42582e1290..a3d0508aa1 100644 --- a/src/main/java/org/mitre/synthea/world/concepts/BirthStatistics.java +++ b/src/main/java/org/mitre/synthea/world/concepts/BirthStatistics.java @@ -104,7 +104,7 @@ public static void setBirthStatistics(Person mother, long time) { if (mother.attributes.containsKey(BIRTH_SEX)) { babySex = (String) mother.attributes.get(BIRTH_SEX); } else { - if (mother.random.nextBoolean()) { + if (mother.randBoolean()) { babySex = "M"; } else { babySex = "F"; diff --git a/src/main/java/org/mitre/synthea/world/concepts/Costs.java b/src/main/java/org/mitre/synthea/world/concepts/Costs.java index 1e83303222..c70b61cc0d 100644 --- a/src/main/java/org/mitre/synthea/world/concepts/Costs.java +++ b/src/main/java/org/mitre/synthea/world/concepts/Costs.java @@ -5,7 +5,6 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Random; import org.mitre.synthea.helpers.Config; import org.mitre.synthea.helpers.SimpleCSV; @@ -70,7 +69,7 @@ public static double determineCostOfEntry(Entry entry, Person person) { // Retrieve the base cost based on the code. double baseCost; if (costs != null && costs.containsKey(code)) { - baseCost = costs.get(code).chooseCost(person.random); + baseCost = costs.get(code).chooseCost(person); } else { baseCost = defaultCost; } @@ -193,8 +192,8 @@ private CostData(double min, double mode, double max) { * @param random Source of randomness * @return Single cost within the range this set of cost data represents */ - private double chooseCost(Random random) { - return triangularDistribution(min, max, mode, random.nextDouble()); + private double chooseCost(Person person) { + return triangularDistribution(min, max, mode, person.rand()); } /** diff --git a/src/main/java/org/mitre/synthea/world/concepts/HealthRecord.java b/src/main/java/org/mitre/synthea/world/concepts/HealthRecord.java index 836750c294..bc5bad8796 100644 --- a/src/main/java/org/mitre/synthea/world/concepts/HealthRecord.java +++ b/src/main/java/org/mitre/synthea/world/concepts/HealthRecord.java @@ -317,9 +317,9 @@ public class ImagingStudy extends Entry { /** * Constructor for ImagingStudy HealthRecord Entry. */ - public ImagingStudy(long time, String type) { + public ImagingStudy(Person person, long time, String type) { super(time, type); - this.dicomUid = Utilities.randomDicomUid(0, 0); + this.dicomUid = Utilities.randomDicomUid(person, time, 0, 0); this.series = new ArrayList(); } @@ -411,11 +411,11 @@ public Device(long start, String type) { * @param person The person who owns or contains the device. */ public void generateUDI(Person person) { - deviceIdentifier = trimLong(person.random.nextLong(), 14); + deviceIdentifier = trimLong(person.randLong(), 14); manufactureTime = start - Utilities.convertTime("weeks", 3); expirationTime = start + Utilities.convertTime("years", 25); - lotNumber = trimLong(person.random.nextLong(), (int) person.rand(4, 20)); - serialNumber = trimLong(person.random.nextLong(), (int) person.rand(4, 20)); + lotNumber = trimLong(person.randLong(), (int) person.rand(4, 20)); + serialNumber = trimLong(person.randLong(), (int) person.rand(4, 20)); udi = "(01)" + deviceIdentifier; udi += "(11)" + udiDate(manufactureTime); @@ -1175,10 +1175,11 @@ public boolean careplanActive(String type) { * @param series the series associated with the study. * @return */ - public ImagingStudy imagingStudy(long time, String type, List series) { - ImagingStudy study = new ImagingStudy(time, type); + public ImagingStudy imagingStudy(long time, String type, + List series) { + ImagingStudy study = new ImagingStudy(this.person, time, type); study.series = series; - assignImagingStudyDicomUids(study); + assignImagingStudyDicomUids(time, study); currentEncounter(time).imagingStudies.add(study); return study; } @@ -1186,18 +1187,18 @@ public ImagingStudy imagingStudy(long time, String type, List yearCorrelations = loadCorrelations(); - private static NormalDistribution normalDistribution = new NormalDistribution(); + private static NormalDistribution normalDistribution = new NormalDistribution(null, 0, 1); private static EnumeratedDistribution nhanesSamples = NHANESSample.loadDistribution(); @@ -89,11 +88,15 @@ public class Point implements Serializable { public int ageInMonths; public long timeInSimulation; public double bmi; + + public String toString() { + return String.format("{Age: %d, Time: %d, BMI: %f}", ageInMonths, timeInSimulation, bmi); + } } private NHANESSample initialSample; private List trajectory; - + private long seed; /** * Starts a new pediatric growth trajectory. Selects a start BMI between 2 and 3 years old by @@ -104,8 +107,8 @@ public class Point implements Serializable { */ public PediatricGrowthTrajectory(long personSeed, long birthTime) { // TODO: Make the selection sex specific - nhanesSamples.reseedRandomGenerator(personSeed); - this.initialSample = nhanesSamples.sample(); + this.seed = personSeed; + this.initialSample = getSample(personSeed); this.trajectory = new LinkedList(); Point p = new Point(); p.ageInMonths = this.initialSample.agem; @@ -114,6 +117,13 @@ public PediatricGrowthTrajectory(long personSeed, long birthTime) { this.trajectory.add(p); } + private static NHANESSample getSample(long personSeed) { + synchronized (nhanesSamples) { + nhanesSamples.reseedRandomGenerator(personSeed); + return nhanesSamples.sample(); + } + } + /** * Given the sex, BMI and height percentile at age 2, calculate the correct weight percentile. * @param sex of the person to get the weight percentile for @@ -135,10 +145,8 @@ public double reverseWeightPercentile(String sex, double heightPercentile) { * consideration people at or above the 95th percentile, as the growth charts start to break down. * @param person to generate the new BMI for * @param time current time - * @param randomGenerator Apache Commons Math random thingy needed to sample a value */ - public void generateNextYearBMI(Person person, long time, - JDKRandomGenerator randomGenerator) { + public void generateNextYearBMI(Person person, long time) { double age = person.ageInDecimalYears(time); double nextAgeYear = age + 1; String sex = (String) person.attributes.get(Person.GENDER); @@ -150,8 +158,7 @@ public void generateNextYearBMI(Person person, long time, double ezscore = extendedZScore(currentBMI, lastPoint.ageInMonths, sex, sigma); double mean = yi.correlation * ezscore + yi.diff; double sd = Math.sqrt(1 - Math.pow(yi.correlation, 2)); - NormalDistribution nextZDistro = new NormalDistribution(randomGenerator, mean, sd); - double nextYearZscore = nextZDistro.sample(); + double nextYearZscore = (person.randGaussian() * sd) + mean; double nextYearPercentile = GrowthChart.zscoreToPercentile(nextYearZscore); double nextPointBMI = percentileToBMI(nextYearPercentile, lastPoint.ageInMonths + 12, sex, sigma(sex, nextAgeYear)); @@ -254,16 +261,15 @@ public void addPointFromPercentile(int ageInMonths, long timeInSimulation, doubl * will happen before the person is 20 years old. * @param person to get the BMI for * @param time the time at which you want the BMI - * @param randomGenerator Apache Commons Math random thingy needed to sample a value * @return a BMI value */ - public double currentBMI(Person person, long time, JDKRandomGenerator randomGenerator) { + public double currentBMI(Person person, long time) { Point lastPoint = tail(); if (lastPoint.timeInSimulation <= time) { if (lastPoint.ageInMonths > NINETEEN_YEARS_IN_MONTHS) { return lastPoint.bmi; } - generateNextYearBMI(person, time, randomGenerator); + generateNextYearBMI(person, time); } Point previous = justBefore(time); Point next = justAfter(time); @@ -274,6 +280,18 @@ public double currentBMI(Person person, long time, JDKRandomGenerator randomGene return previous.bmi + (bmiDifference * percentOfTimeBetweenPointsElapsed); } + private static double cumulativeProbability(double value) { + synchronized (normalDistribution) { + return normalDistribution.cumulativeProbability(value); + } + } + + private static double inverseCumulativeProbability(double value) { + synchronized (normalDistribution) { + return normalDistribution.inverseCumulativeProbability(value); + } + } + /** * Uses extended percentiles when calculating a BMI greater than or equal to the 95th * percentile. @@ -292,7 +310,7 @@ public static double percentileToBMI(double percentile, int ageInMonths, String double ninetyFifth = growthChart.get(GrowthChart.ChartType.BMI) .lookUp(ageInMonths, sex, 0.95); return ninetyFifth - + normalDistribution.inverseCumulativeProbability((percentile - 0.9) * 10) * sigma; + + inverseCumulativeProbability((percentile - 0.9) * 10) * sigma; } } @@ -314,8 +332,8 @@ public static double extendedZScore(double bmi, int ageInMonths, String sex, dou double ninetyFifth = growthChart.get(GrowthChart.ChartType.BMI) .lookUp(ageInMonths, sex, 0.95); double ebmiPercentile = 90 + 10 - * normalDistribution.cumulativeProbability((bmi - ninetyFifth) / sigma); - return normalDistribution.inverseCumulativeProbability(ebmiPercentile / 100); + * cumulativeProbability((bmi - ninetyFifth) / sigma); + return inverseCumulativeProbability(ebmiPercentile / 100); } } diff --git a/src/main/java/org/mitre/synthea/world/geography/Location.java b/src/main/java/org/mitre/synthea/world/geography/Location.java index 18ccdb06e8..c878b0f868 100644 --- a/src/main/java/org/mitre/synthea/world/geography/Location.java +++ b/src/main/java/org/mitre/synthea/world/geography/Location.java @@ -170,7 +170,25 @@ public long getPopulation(String cityName) { * Pick the name of a random city from the current "world". * If only one city was selected, this will return that one city. * - * @param random Source of randomness + * @param person The person and source of randomness + * @return Demographics of a random city. + */ + public Demographics randomCity(Person person) { + if (city != null) { + // if we're only generating one city at a time, just use the largest entry for that one city + if (fixedCity == null) { + fixedCity = demographics.values().stream() + .filter(d -> d.city.equalsIgnoreCase(city)) + .sorted().findFirst().get(); + } + return fixedCity; + } + return demographics.get(randomCityId(person)); + } + + /** + * Pick the name of a random city from the current "world"? + * @param random The source of randomness. * @return Demographics of a random city. */ public Demographics randomCity(Random random) { @@ -185,22 +203,37 @@ public Demographics randomCity(Random random) { } return demographics.get(randomCityId(random)); } - + /** * Pick a random city name, weighted by population. - * @param random Source of randomness + * @param person The person and source of randomness * @return a city name */ - public String randomCityName(Random random) { - String cityId = randomCityId(random); + public String randomCityName(Person person) { + String cityId = randomCityId(person); return demographics.get(cityId).city; } /** * Pick a random city id, weighted by population. - * @param random Source of randomness + * @param person The person and source of randomness * @return a city id */ + private String randomCityId(Person person) { + long targetPop = (long) (person.rand() * totalPopulation); + + for (Map.Entry city : populationByCityId.entrySet()) { + targetPop -= city.getValue(); + + if (targetPop < 0) { + return city.getKey(); + } + } + + // should never happen + throw new RuntimeException("Unable to select a random city id."); + } + private String randomCityId(Random random) { long targetPop = (long) (random.nextDouble() * totalPopulation); @@ -218,12 +251,12 @@ private String randomCityId(Random random) { /** * Pick a random birth place, weighted by population. - * @param random Source of randomness + * @param person The person and source of randomness * @return Array of Strings: [city, state, country, "city, state, country"] */ - public String[] randomBirthPlace(Random random) { + public String[] randomBirthPlace(Person person) { String[] birthPlace = new String[4]; - birthPlace[0] = randomCityName(random); + birthPlace[0] = randomCityName(person); birthPlace[1] = this.state; birthPlace[2] = COUNTRY_CODE; birthPlace[3] = birthPlace[0] + ", " + birthPlace[1] + ", " + birthPlace[2]; @@ -236,23 +269,23 @@ public String[] randomBirthPlace(Random random) { * In the case an language is not present the method returns the value from a call to * randomCityName(). * - * @param random the Random to base our city selection on + * @param person The person and source of randomness * @param language the language to look for cities in * @return A String representing the place of birth */ - public String[] randomBirthplaceByLanguage(Random random, String language) { + public String[] randomBirthplaceByLanguage(Person person, String language) { String[] birthPlace; List cities = foreignPlacesOfBirth.get(language.toLowerCase()); if (cities != null && cities.size() > 0) { int upperBound = cities.size(); - String randomBirthPlace = cities.get(random.nextInt(upperBound)); + String randomBirthPlace = cities.get(person.randInt(upperBound)); String[] split = randomBirthPlace.split(","); // make sure we have exactly 3 elements (city, state, country_abbr) // if not fallback to some random US location if (split.length != 3) { - birthPlace = randomBirthPlace(random); + birthPlace = randomBirthPlace(person); } else { //concatenate all the results together, adding spaces behind commas for readability birthPlace = ArrayUtils.addAll(split, @@ -260,7 +293,7 @@ public String[] randomBirthplaceByLanguage(Random random, String language) { } } else { //if we can't find a foreign city at least return something - birthPlace = randomBirthPlace(random); + birthPlace = randomBirthPlace(person); } return birthPlace; diff --git a/src/test/java/org/mitre/synthea/editors/GrowthDataErrorsEditorTest.java b/src/test/java/org/mitre/synthea/editors/GrowthDataErrorsEditorTest.java index 2cf6f2ab8f..9168ef35ed 100644 --- a/src/test/java/org/mitre/synthea/editors/GrowthDataErrorsEditorTest.java +++ b/src/test/java/org/mitre/synthea/editors/GrowthDataErrorsEditorTest.java @@ -4,7 +4,6 @@ import static org.junit.Assert.assertTrue; import java.util.List; -import java.util.Random; import org.junit.Before; import org.junit.Test; @@ -47,7 +46,7 @@ record = new HealthRecord(new Person(1)); @Test public void process() { GrowthDataErrorsEditor m = new GrowthDataErrorsEditor(); - m.process(new Person(1), record.encounters, 100000, new Random()); + m.process(new Person(1), record.encounters, 100000); } @@ -106,7 +105,8 @@ public void introduceHeightExtremeError() { @Test public void introduceHeightAbsoluteError() { - GrowthDataErrorsEditor.introduceHeightAbsoluteError(first, new Random()); + Person person = new Person(0L); + GrowthDataErrorsEditor.introduceHeightAbsoluteError(first, person); assertTrue((Double) first.findObservation( GrowthDataErrorsEditor.HEIGHT_LOINC_CODE).value <= 147d); assertTrue((Double) first.findObservation( @@ -115,7 +115,8 @@ public void introduceHeightAbsoluteError() { @Test public void introduceWeightDuplicateError() { - GrowthDataErrorsEditor.introduceWeightDuplicateError(first, new Random()); + Person person = new Person(0L); + GrowthDataErrorsEditor.introduceWeightDuplicateError(first, person); long obsCount = first.observations.stream() .filter(o -> o.type.equals(GrowthDataErrorsEditor.WEIGHT_LOINC_CODE)) .count(); @@ -124,7 +125,8 @@ public void introduceWeightDuplicateError() { @Test public void introduceHeightDuplicateError() { - GrowthDataErrorsEditor.introduceHeightDuplicateError(first, new Random()); + Person person = new Person(0L); + GrowthDataErrorsEditor.introduceHeightDuplicateError(first, person); long obsCount = first.observations.stream() .filter(o -> o.type.equals(GrowthDataErrorsEditor.HEIGHT_LOINC_CODE)) .count(); diff --git a/src/test/java/org/mitre/synthea/engine/HealthRecordEditorsTest.java b/src/test/java/org/mitre/synthea/engine/HealthRecordEditorsTest.java index e280117996..e5dee933c9 100644 --- a/src/test/java/org/mitre/synthea/engine/HealthRecordEditorsTest.java +++ b/src/test/java/org/mitre/synthea/engine/HealthRecordEditorsTest.java @@ -5,7 +5,6 @@ import static org.junit.Assert.assertNull; import java.util.List; -import java.util.Random; import org.junit.Test; import org.mitre.synthea.world.agents.Person; @@ -20,8 +19,7 @@ public boolean shouldRun(Person person, HealthRecord record, long time) { } @Override - public void process(Person person, List encounters, long time, - Random random) { + public void process(Person person, List encounters, long time) { person.attributes.put(Person.ZIP, "01730"); } } @@ -36,9 +34,9 @@ public void executeAll() { HealthRecordEditors hrm = HealthRecordEditors.getInstance(); hrm.registerEditor(new Dummy()); Person p = new Person(1); - hrm.executeAll(p, new HealthRecord(p), 1, 1, new Random()); + hrm.executeAll(p, new HealthRecord(p), 1, 1); assertNull(p.attributes.get(Person.ZIP)); - hrm.executeAll(p, new HealthRecord(p), 1100, 1, new Random()); + hrm.executeAll(p, new HealthRecord(p), 1100, 1); assertEquals("01730", p.attributes.get(Person.ZIP)); hrm.resetEditors(); } diff --git a/src/test/java/org/mitre/synthea/engine/ModuleTest.java b/src/test/java/org/mitre/synthea/engine/ModuleTest.java index 7eafa2cf9e..458e902e9a 100644 --- a/src/test/java/org/mitre/synthea/engine/ModuleTest.java +++ b/src/test/java/org/mitre/synthea/engine/ModuleTest.java @@ -16,9 +16,15 @@ import java.io.FileReader; import java.io.IOException; import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Random; import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Predicate; @@ -33,8 +39,8 @@ public void getModules() { List allModules = Module.getModules(); List someModules = Module.getModules(path -> path.contains("ti")); - assertTrue(allModules.containsAll(someModules)); - assertFalse(someModules.containsAll(allModules)); + assertTrue(contains(allModules, someModules)); + assertFalse(contains(someModules, allModules)); assertTrue(allModules.size() > someModules.size()); assertTrue(someModules.size() > 0); @@ -43,6 +49,115 @@ public void getModules() { assertFalse(someModules.stream().anyMatch(filterOnModuleName("COPD"))); } + /** Manually compare lists since the Modules are clones and not originals. */ + private boolean contains(List superset, List subset) { + for (Module subsetModule : subset) { + boolean found = false; + for (Module supersetModule : superset) { + if (supersetModule.name.equals(subsetModule.name) + && supersetModule.submodule == subsetModule.submodule + && ((supersetModule.remarks == null && subsetModule.remarks == null) + || supersetModule.remarks.equals(subsetModule.remarks))) { + found = true; + } + } + if (!found) { + return false; + } + } + return true; + } + + @Test + public void getModulesInPredictableOrder() { + List modulesA = Module.getModules(); + List modulesB = Module.getModules(); + + // verify with list + assertEquals(modulesA.size(), modulesB.size()); + for (int i = 0; i < modulesA.size(); i++) { + assertEquals(modulesA.get(i).name, modulesB.get(i).name); + assertEquals(modulesA.get(i).submodule, modulesB.get(i).submodule); + assertEquals(modulesA.get(i).getStateNames(), modulesB.get(i).getStateNames()); + } + + // verify with iterator + Iterator iterA = modulesA.iterator(); + Iterator iterB = modulesB.iterator(); + while (iterA.hasNext()) { + Module modA = iterA.next(); + Module modB = iterB.next(); + assertEquals(modA.name, modB.name); + assertEquals(modA.submodule, modB.submodule); + assertEquals(modA.getStateNames(), modB.getStateNames()); + } + } + + @Test + public void getModulesInPredictableOrderThreadPool() { + ExecutorService threadPool = Executors.newFixedThreadPool(8); + + List modules = Module.getModules(); + + for (int i = 0; i < 1000; i++) { + threadPool.submit(() -> { + List localModules = Module.getModules(); + assertEquals(modules.size(), localModules.size()); + for (int j = 0; j < modules.size(); j++) { + assertEquals(modules.get(j), localModules.get(j)); + } + }); + } + + try { + threadPool.shutdown(); + while (!threadPool.awaitTermination(30, TimeUnit.SECONDS)) { + System.out.println("Waiting for threads to finish... " + threadPool); + } + } catch (InterruptedException e) { + System.out.println("Test interrupted. Attempting to shut down associated thread pool."); + threadPool.shutdownNow(); + } + } + + @Test + public void getModulesInPredictableOrderWithRemoval() { + List resultsA = new ArrayList(); + List resultsB = new ArrayList(); + + Random randA = new Random(9L); + Random randB = new Random(9L); + + List modulesA = Module.getModules(); + while (!modulesA.isEmpty()) { + Iterator iter = modulesA.iterator(); + while (iter.hasNext()) { + Module mod = iter.next(); + resultsA.add(mod.name); + if (randA.nextDouble() < 0.1) { + iter.remove(); + } + } + } + + List modulesB = Module.getModules(); + while (!modulesB.isEmpty()) { + Iterator iter = modulesB.iterator(); + while (iter.hasNext()) { + Module mod = iter.next(); + resultsB.add(mod.name); + if (randB.nextDouble() < 0.1) { + iter.remove(); + } + } + } + + assertEquals(resultsA.size(), resultsB.size()); + for (int i = 0; i < resultsA.size(); i++) { + assertEquals(resultsA.get(i), resultsB.get(i)); + } + } + @Test public void getModuleByPath() { Module module = Module.getModuleByPath("copd"); diff --git a/src/test/java/org/mitre/synthea/export/CodeResolveAndExportTest.java b/src/test/java/org/mitre/synthea/export/CodeResolveAndExportTest.java index 8acbe49665..5104fee965 100644 --- a/src/test/java/org/mitre/synthea/export/CodeResolveAndExportTest.java +++ b/src/test/java/org/mitre/synthea/export/CodeResolveAndExportTest.java @@ -123,7 +123,7 @@ public void setUp() throws Exception { TestHelper.loadTestProperties(); Generator.DEFAULT_STATE = Config.get("test_state.default", "Massachusetts"); Location location = new Location(Generator.DEFAULT_STATE, null); - location.assignPoint(person, location.randomCityName(person.random)); + location.assignPoint(person, location.randomCityName(person)); Provider.loadProviders(location, 1L); Payer.clear(); diff --git a/src/test/java/org/mitre/synthea/export/ExporterTest.java b/src/test/java/org/mitre/synthea/export/ExporterTest.java index ec96f34131..2f8e54316d 100644 --- a/src/test/java/org/mitre/synthea/export/ExporterTest.java +++ b/src/test/java/org/mitre/synthea/export/ExporterTest.java @@ -46,7 +46,7 @@ public void setup() throws Exception { TestHelper.loadTestProperties(); Generator.DEFAULT_STATE = Config.get("test_state.default", "Massachusetts"); Location location = new Location(Generator.DEFAULT_STATE, null); - location.assignPoint(patient, location.randomCityName(patient.random)); + location.assignPoint(patient, location.randomCityName(patient)); Provider.loadProviders(location, 1L); record = patient.record; // Ensure Person's Payer is not null. diff --git a/src/test/java/org/mitre/synthea/export/FHIRDSTU2ExporterTest.java b/src/test/java/org/mitre/synthea/export/FHIRDSTU2ExporterTest.java index 6805fb8ef4..a708733325 100644 --- a/src/test/java/org/mitre/synthea/export/FHIRDSTU2ExporterTest.java +++ b/src/test/java/org/mitre/synthea/export/FHIRDSTU2ExporterTest.java @@ -116,7 +116,7 @@ public void testFHIRDSTU2Export() throws Exception { TestHelper.exportOff(); Person person = generator.generatePerson(i); Config.set("exporter.fhir_dstu2.export", "true"); - FhirDstu2.TRANSACTION_BUNDLE = person.random.nextBoolean(); + FhirDstu2.TRANSACTION_BUNDLE = person.randBoolean(); String fhirJson = FhirDstu2.convertToFHIRJson(person, System.currentTimeMillis()); // Check that the fhirJSON doesn't contain unresolved SNOMED-CT strings // (these should have been converted into URIs) diff --git a/src/test/java/org/mitre/synthea/export/FHIRR4ExporterTest.java b/src/test/java/org/mitre/synthea/export/FHIRR4ExporterTest.java index bc3ac832c1..a29cd4520c 100644 --- a/src/test/java/org/mitre/synthea/export/FHIRR4ExporterTest.java +++ b/src/test/java/org/mitre/synthea/export/FHIRR4ExporterTest.java @@ -111,8 +111,8 @@ public void testFHIRR4Export() throws Exception { int x = validationErrors.size(); TestHelper.exportOff(); Person person = generator.generatePerson(i); - FhirR4.TRANSACTION_BUNDLE = person.random.nextBoolean(); - FhirR4.USE_US_CORE_IG = person.random.nextBoolean(); + FhirR4.TRANSACTION_BUNDLE = person.randBoolean(); + FhirR4.USE_US_CORE_IG = person.randBoolean(); FhirR4.USE_SHR_EXTENSIONS = false; String fhirJson = FhirR4.convertToFHIRJson(person, System.currentTimeMillis()); // Check that the fhirJSON doesn't contain unresolved SNOMED-CT strings diff --git a/src/test/java/org/mitre/synthea/export/FHIRSTU3ExporterTest.java b/src/test/java/org/mitre/synthea/export/FHIRSTU3ExporterTest.java index d257b1ba3d..21b4e910ed 100644 --- a/src/test/java/org/mitre/synthea/export/FHIRSTU3ExporterTest.java +++ b/src/test/java/org/mitre/synthea/export/FHIRSTU3ExporterTest.java @@ -120,7 +120,7 @@ public void testFHIRSTU3Export() throws Exception { Person person = generator.generatePerson(i); Config.set("exporter.fhir_stu3.export", "true"); Config.set("exporter.fhir.use_shr_extensions", "true"); - FhirStu3.TRANSACTION_BUNDLE = person.random.nextBoolean(); + FhirStu3.TRANSACTION_BUNDLE = person.randBoolean(); String fhirJson = FhirStu3.convertToFHIRJson(person, System.currentTimeMillis()); // Check that the fhirJSON doesn't contain unresolved SNOMED-CT strings // (these should have been converted into URIs) diff --git a/src/test/java/org/mitre/synthea/export/ValueSetCodeResolverTest.java b/src/test/java/org/mitre/synthea/export/ValueSetCodeResolverTest.java index 7b45f4aa25..913ed1d6c0 100644 --- a/src/test/java/org/mitre/synthea/export/ValueSetCodeResolverTest.java +++ b/src/test/java/org/mitre/synthea/export/ValueSetCodeResolverTest.java @@ -68,7 +68,7 @@ public void setUp() throws Exception { TestHelper.loadTestProperties(); Generator.DEFAULT_STATE = Config.get("test_state.default", "Massachusetts"); Location location = new Location(Generator.DEFAULT_STATE, null); - location.assignPoint(person, location.randomCityName(person.random)); + location.assignPoint(person, location.randomCityName(person)); Provider.loadProviders(location, 1L); Payer.clear(); diff --git a/src/test/java/org/mitre/synthea/modules/EncounterModuleTest.java b/src/test/java/org/mitre/synthea/modules/EncounterModuleTest.java index e02c1e382a..718b1bebbb 100644 --- a/src/test/java/org/mitre/synthea/modules/EncounterModuleTest.java +++ b/src/test/java/org/mitre/synthea/modules/EncounterModuleTest.java @@ -33,7 +33,7 @@ public static void setup() throws Exception { TestHelper.loadTestProperties(); String testState = Config.get("test_state.default", "Massachusetts"); location = new Location(testState, null); - location.assignPoint(person, location.randomCityName(person.random)); + location.assignPoint(person, location.randomCityName(person)); Provider.loadProviders(location, 1L); module = new EncounterModule(); // Ensure Person's Payer is not null. diff --git a/src/test/java/org/mitre/synthea/world/agents/PersonTest.java b/src/test/java/org/mitre/synthea/world/agents/PersonTest.java index 8ca4e7afcf..7d6f06e762 100644 --- a/src/test/java/org/mitre/synthea/world/agents/PersonTest.java +++ b/src/test/java/org/mitre/synthea/world/agents/PersonTest.java @@ -10,21 +10,36 @@ import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; +import java.nio.file.Files; +import java.text.SimpleDateFormat; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import java.util.Map; import java.util.Random; +import java.util.TimeZone; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; - +import org.junit.rules.TemporaryFolder; import org.mitre.synthea.TestHelper; import org.mitre.synthea.engine.Generator; +import org.mitre.synthea.engine.Generator.GeneratorOptions; import org.mitre.synthea.helpers.Config; import org.mitre.synthea.world.concepts.VitalSign; public class PersonTest { private Person person; - + /** + * Temporary folder for any exported files, guaranteed to be deleted at the end of the test. + */ + @Rule + public TemporaryFolder tempFolder = new TemporaryFolder(); + /** * Create a person for use in each test. * @throws IOException if something goes wrong. @@ -87,7 +102,7 @@ public void testSerializationAndDeserialization() throws Exception { Person rehydrated = serializeAndDeserialize(original); // Compare the original to the serialized+deserialized version - assertEquals(original.random.nextInt(), rehydrated.random.nextInt()); + assertEquals(original.randInt(), rehydrated.randInt()); assertEquals(original.seed, rehydrated.seed); assertEquals(original.populationSeed, rehydrated.populationSeed); assertEquals(original.symptoms.keySet(), rehydrated.symptoms.keySet()); @@ -165,4 +180,203 @@ public void testVitalSignInfinity() { public void testVitalSign() { person.setVitalSign(VitalSign.HEIGHT, 6.02); } + + @Test() + public void testPersonRecreationSerialDifferentGenerator() throws Exception { + TestHelper.loadTestProperties(); + TestHelper.exportOff(); + Generator.DEFAULT_STATE = Config.get("test_state.default", "Massachusetts"); + File tempOutputFolder = tempFolder.newFolder(); + Config.set("exporter.baseDirectory", tempOutputFolder.toString()); + Config.set("exporter.text.export", "true"); + + SimpleDateFormat format = new SimpleDateFormat("YYYYMMDD"); + format.setTimeZone(TimeZone.getTimeZone("UTC")); + + GeneratorOptions options = new GeneratorOptions(); + options.clinicianSeed = 9L; + options.seed = 9L; + options.referenceTime = format.parse("20200704").getTime(); + options.overflow = false; + + // Generate the first patient... + Generator generator = new Generator(options); + generator.generatePerson(0, 42L); + + // Generate what should be an identical clone... + generator = new Generator(options); + generator.generatePerson(0, 42L); + + // Check that the output files exist + File expectedExportFolder = tempOutputFolder.toPath().resolve("text").toFile(); + assertTrue(expectedExportFolder.exists() && expectedExportFolder.isDirectory()); + + // Check that there are exactly two files + List> fileContents = new ArrayList>(); + for (File txtFile : expectedExportFolder.listFiles()) { + if (!txtFile.getName().endsWith(".txt")) { + continue; + } + fileContents.add(Files.readAllLines(txtFile.toPath())); + } + assertEquals("Expected 2 files in the output directory, found " + fileContents.size(), 2, + fileContents.size()); + + // Check that the two files are identical + for (int i = 0; i < fileContents.get(0).size(); i++) { + assertEquals(fileContents.get(0).get(i), fileContents.get(1).get(i)); + } + } + + @Test() + public void testPersonRecreationSerialSameGenerator() throws Exception { + TestHelper.loadTestProperties(); + TestHelper.exportOff(); + Generator.DEFAULT_STATE = Config.get("test_state.default", "Massachusetts"); + File tempOutputFolder = tempFolder.newFolder(); + Config.set("exporter.baseDirectory", tempOutputFolder.toString()); + Config.set("exporter.text.export", "true"); + + SimpleDateFormat format = new SimpleDateFormat("YYYYMMDD"); + format.setTimeZone(TimeZone.getTimeZone("UTC")); + + GeneratorOptions options = new GeneratorOptions(); + options.clinicianSeed = 9L; + options.seed = 9L; + options.referenceTime = format.parse("20200704").getTime(); + options.overflow = false; + + // Generate the first patient... + Generator generator = new Generator(options); + generator.generatePerson(0, 42L); + + // Generate what should be an identical clone... + generator.generatePerson(0, 42L); + + // Check that the output files exist + File expectedExportFolder = tempOutputFolder.toPath().resolve("text").toFile(); + assertTrue(expectedExportFolder.exists() && expectedExportFolder.isDirectory()); + + // Check that there are exactly two files + List> fileContents = new ArrayList>(); + for (File txtFile : expectedExportFolder.listFiles()) { + if (!txtFile.getName().endsWith(".txt")) { + continue; + } + fileContents.add(Files.readAllLines(txtFile.toPath())); + } + assertEquals("Expected 2 files in the output directory, found " + fileContents.size(), 2, + fileContents.size()); + + // Check that the two files are identical + for (int i = 0; i < fileContents.get(0).size(); i++) { + assertEquals(fileContents.get(0).get(i), fileContents.get(1).get(i)); + } + } + + @Test() + public void testPersonRecreationParallel() throws Exception { + TestHelper.loadTestProperties(); + TestHelper.exportOff(); + Generator.DEFAULT_STATE = Config.get("test_state.default", "Massachusetts"); + File tempOutputFolder = tempFolder.newFolder(); + Config.set("exporter.baseDirectory", tempOutputFolder.toString()); + Config.set("exporter.text.export", "true"); + + SimpleDateFormat format = new SimpleDateFormat("YYYYMMDD"); + format.setTimeZone(TimeZone.getTimeZone("UTC")); + + GeneratorOptions options = new GeneratorOptions(); + options.clinicianSeed = 9L; + options.seed = 9L; + options.referenceTime = format.parse("20200704").getTime(); + options.overflow = false; + Generator generator = new Generator(options); + + // Generate the patients... + ExecutorService threadPool = Executors.newFixedThreadPool(8); + for (int i = 0; i < 10; i++) { + threadPool.submit(() -> generator.generatePerson(0, 42L)); + } + threadPool.shutdown(); + while (!threadPool.awaitTermination(30, TimeUnit.SECONDS)) { + /* do nothing */ + } + + // Check that the output files exist + File expectedExportFolder = tempOutputFolder.toPath().resolve("text").toFile(); + assertTrue(expectedExportFolder.exists() && expectedExportFolder.isDirectory()); + + // Check that there are exactly two files + List> fileContents = new ArrayList>(); + for (File txtFile : expectedExportFolder.listFiles()) { + if (!txtFile.getName().endsWith(".txt")) { + continue; + } + fileContents.add(Files.readAllLines(txtFile.toPath())); + } + assertEquals("Expected 10 files in the output directory, found " + fileContents.size(), 10, + fileContents.size()); + + // Check that all files are identical + for (int f = 1; f < fileContents.size(); f++) { + for (int l = 0; l < fileContents.get(f).size(); l++) { + assertEquals(fileContents.get(f - 1).get(l), fileContents.get(f).get(l)); + } + } + } + + @Test() + public void testPersonRandomStability() { + Person personA = new Person(0L); + Person personB = new Person(0L); + + double[] doubleRange = { 3.14159d, 42.4233d }; + int[] intRange = { 33, 333 }; + String[] choices = { "foo", "bar", "baz" }; + + List resultsA = new ArrayList(); + List resultsB = new ArrayList(); + + int mode = 0; + for (int i = 0; i < 1000; i++) { + if (mode == 0) { + resultsA.add("" + personA.rand()); + resultsB.add("" + personB.rand()); + } else if (mode == 1) { + resultsA.add("" + personA.rand(doubleRange)); + resultsB.add("" + personB.rand(doubleRange)); + } else if (mode == 2) { + resultsA.add("" + personA.rand(intRange)); + resultsB.add("" + personB.rand(intRange)); + } else if (mode == 3) { + resultsA.add("" + personA.rand(choices)); + resultsB.add("" + personB.rand(choices)); + } else if (mode == 4) { + resultsA.add("" + personA.rand(101.101d, 333.333d)); + resultsB.add("" + personB.rand(101.101d, 333.333d)); + } else if (mode == 5) { + resultsA.add("" + personA.rand(42.2345d, 98.7654d, 3)); + resultsB.add("" + personB.rand(42.2345d, 98.7654d, 3)); + } else if (mode == 6) { + resultsA.add("" + personA.randInt()); + resultsB.add("" + personB.randInt()); + } else if (mode == 7) { + resultsA.add("" + personA.randInt(3333)); + resultsB.add("" + personB.randInt(3333)); + } else if (mode == 8) { + resultsA.add("" + personA.randGaussian()); + resultsB.add("" + personB.randGaussian()); + } + mode += 1; + if (mode > 8) { + mode = 0; + } + } + + assertEquals(resultsA.size(), resultsB.size()); + for (int i = 0; i < resultsA.size(); i++) { + assertEquals(resultsA.get(i), resultsB.get(i)); + } + } } diff --git a/src/test/java/org/mitre/synthea/world/agents/ProviderTest.java b/src/test/java/org/mitre/synthea/world/agents/ProviderTest.java index 7fd520f955..8f912003bf 100644 --- a/src/test/java/org/mitre/synthea/world/agents/ProviderTest.java +++ b/src/test/java/org/mitre/synthea/world/agents/ProviderTest.java @@ -92,7 +92,7 @@ public void testAllFacilitiesHaveAnId() { public void testNearestInpatientInState() { Provider.loadProviders(location, 1L); Person person = new Person(0L); - location.assignPoint(person, location.randomCityName(person.random)); + location.assignPoint(person, location.randomCityName(person)); Provider provider = Provider.findService(person, EncounterType.INPATIENT, 0); Assert.assertNotNull(provider); } @@ -101,7 +101,7 @@ public void testNearestInpatientInState() { public void testNearestAmbulatoryInState() { Provider.loadProviders(location, 1L); Person person = new Person(0L); - location.assignPoint(person, location.randomCityName(person.random)); + location.assignPoint(person, location.randomCityName(person)); Provider provider = Provider.findService(person, EncounterType.AMBULATORY, 0); Assert.assertNotNull(provider); } @@ -110,7 +110,7 @@ public void testNearestAmbulatoryInState() { public void testNearestWellnessInState() { Provider.loadProviders(location, 1L); Person person = new Person(0L); - location.assignPoint(person, location.randomCityName(person.random)); + location.assignPoint(person, location.randomCityName(person)); Provider provider = Provider.findService(person, EncounterType.WELLNESS, 0); Assert.assertNotNull(provider); } @@ -119,7 +119,7 @@ public void testNearestWellnessInState() { public void testNearestEmergencyInState() { Provider.loadProviders(location, 1L); Person person = new Person(0L); - location.assignPoint(person, location.randomCityName(person.random)); + location.assignPoint(person, location.randomCityName(person)); Provider provider = Provider.findService(person, EncounterType.EMERGENCY, 0); Assert.assertNotNull(provider); } @@ -133,7 +133,7 @@ public void testNearestEmergencyInDC() { Location capital = new Location("District of Columbia", null); Provider.loadProviders(capital, 1L); Person person = new Person(0L); - capital.assignPoint(person, capital.randomCityName(person.random)); + capital.assignPoint(person, capital.randomCityName(person)); Provider provider = Provider.findService(person, EncounterType.EMERGENCY, 0); Assert.assertNotNull(provider); } @@ -142,7 +142,7 @@ public void testNearestEmergencyInDC() { public void testNearestUrgentCareInState() { Provider.loadProviders(location, 1L); Person person = new Person(0L); - location.assignPoint(person, location.randomCityName(person.random)); + location.assignPoint(person, location.randomCityName(person)); Provider provider = Provider.findService(person, EncounterType.URGENTCARE, 0); Assert.assertNotNull(provider); } @@ -151,7 +151,7 @@ public void testNearestUrgentCareInState() { public void testNearestInpatientInCity() { Provider.loadProviders(city, 1L); Person person = new Person(0L); - city.assignPoint(person, city.randomCityName(person.random)); + city.assignPoint(person, city.randomCityName(person)); Provider provider = Provider.findService(person, EncounterType.INPATIENT, 0); Assert.assertNotNull(provider); } @@ -160,7 +160,7 @@ public void testNearestInpatientInCity() { public void testNearestAmbulatoryInCity() { Provider.loadProviders(city, 1L); Person person = new Person(0L); - city.assignPoint(person, city.randomCityName(person.random)); + city.assignPoint(person, city.randomCityName(person)); Provider provider = Provider.findService(person, EncounterType.AMBULATORY, 0); Assert.assertNotNull(provider); } @@ -169,7 +169,7 @@ public void testNearestAmbulatoryInCity() { public void testNearestWellnessInCity() { Provider.loadProviders(city, 1L); Person person = new Person(0L); - city.assignPoint(person, city.randomCityName(person.random)); + city.assignPoint(person, city.randomCityName(person)); Provider provider = Provider.findService(person, EncounterType.WELLNESS, 0); Assert.assertNotNull(provider); } @@ -178,7 +178,7 @@ public void testNearestWellnessInCity() { public void testNearestEmergencyInCity() { Provider.loadProviders(city, 1L); Person person = new Person(0L); - city.assignPoint(person, city.randomCityName(person.random)); + city.assignPoint(person, city.randomCityName(person)); Provider provider = Provider.findService(person, EncounterType.EMERGENCY, 0); Assert.assertNotNull(provider); } @@ -187,7 +187,7 @@ public void testNearestEmergencyInCity() { public void testNearestUrgentCareInCity() { Provider.loadProviders(city, 1L); Person person = new Person(0L); - city.assignPoint(person, city.randomCityName(person.random)); + city.assignPoint(person, city.randomCityName(person)); Provider provider = Provider.findService(person, EncounterType.URGENTCARE, 0); Assert.assertNotNull(provider); } diff --git a/src/test/java/org/mitre/synthea/world/concepts/NHANESSampleTest.java b/src/test/java/org/mitre/synthea/world/concepts/NHANESSampleTest.java index ef54dc6c06..7a05d24bd6 100644 --- a/src/test/java/org/mitre/synthea/world/concepts/NHANESSampleTest.java +++ b/src/test/java/org/mitre/synthea/world/concepts/NHANESSampleTest.java @@ -2,7 +2,13 @@ import static org.junit.Assert.assertEquals; +import java.util.ArrayList; import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.math3.distribution.EnumeratedDistribution; import org.junit.Test; public class NHANESSampleTest { @@ -13,4 +19,61 @@ public void loadSamples() { NHANESSample first = list.get(0); assertEquals(11.9, first.wt, 0.001); } + + @Test + public void predictableSamplesSingleThread() { + EnumeratedDistribution nhanesSamples = NHANESSample.loadDistribution(); + + List foo = new ArrayList(); + List bar = new ArrayList(); + + for (int i = 0; i < 10; i++) { + nhanesSamples.reseedRandomGenerator(0L); + foo.add(nhanesSamples.sample()); + + nhanesSamples.reseedRandomGenerator(9L); + bar.add(nhanesSamples.sample()); + } + + for (int j = 1; j < foo.size(); j++) { + assertEquals(foo.get(j - 1).toString(), foo.get(j).toString()); + assertEquals(bar.get(j - 1).toString(), bar.get(j).toString()); + } + } + + @Test + public void predictableSamplesMultiThread() throws InterruptedException { + EnumeratedDistribution nhanesSamples = NHANESSample.loadDistribution(); + ExecutorService threadPool = Executors.newFixedThreadPool(8); + List> results = new ArrayList>(); + + for (int i = 0; i < 10; i++) { + results.add(new ArrayList()); + } + + for (int i = 0; i < 10; i++) { + final int k = i; + threadPool.submit(() -> { + for (int j = 0; j < 1000; j++) { + results.get(k).add(threadSample(nhanesSamples, j)); + } + }); + } + + threadPool.shutdown(); + while (!threadPool.awaitTermination(30, TimeUnit.SECONDS)) { /* wait */ } + + for (int i = 1; i < results.size(); i++) { + for (int j = 0; j < results.get(i).size(); j++) { + assertEquals(results.get(i - 1).get(j).toString(), results.get(i).get(j).toString()); + } + } + } + + private NHANESSample threadSample(EnumeratedDistribution nhanesSamples, long seed) { + synchronized (nhanesSamples) { + nhanesSamples.reseedRandomGenerator(seed); + return nhanesSamples.sample(); + } + } } \ No newline at end of file diff --git a/src/test/java/org/mitre/synthea/world/concepts/PediatricGrowthTrajectoryTest.java b/src/test/java/org/mitre/synthea/world/concepts/PediatricGrowthTrajectoryTest.java index afcdc86999..7ec65655fa 100644 --- a/src/test/java/org/mitre/synthea/world/concepts/PediatricGrowthTrajectoryTest.java +++ b/src/test/java/org/mitre/synthea/world/concepts/PediatricGrowthTrajectoryTest.java @@ -3,7 +3,6 @@ import static org.junit.Assert.assertEquals; import java.util.Map; -import org.apache.commons.math3.random.JDKRandomGenerator; import org.junit.Test; import org.mitre.synthea.TestHelper; import org.mitre.synthea.helpers.Utilities; @@ -17,14 +16,13 @@ public void generateNextYearBMI() { Person person = new Person(0L); person.attributes.put(Person.BIRTHDATE, birthDay); person.attributes.put(Person.GENDER, "M"); - JDKRandomGenerator random = new JDKRandomGenerator(6454); PediatricGrowthTrajectory pgt = new PediatricGrowthTrajectory(0L, birthDay); // This will be the initial NHANES Sample long sampleSimulationTime = pgt.tail().timeInSimulation; double initialBMI = pgt.tail().bmi; long sixMonthsAfterInitial = sampleSimulationTime + Utilities.convertTime("months", 6); // Will cause generateNextYearBMI to be run - double sixMonthLaterBMI = pgt.currentBMI(person, sixMonthsAfterInitial, random); + double sixMonthLaterBMI = pgt.currentBMI(person, sixMonthsAfterInitial); double oneYearLaterBMI = pgt.tail().bmi; double bmiDiff = oneYearLaterBMI - initialBMI; assertEquals(initialBMI + (0.5 * bmiDiff), sixMonthLaterBMI, 0.01); diff --git a/src/test/java/org/mitre/synthea/world/geography/LocationTest.java b/src/test/java/org/mitre/synthea/world/geography/LocationTest.java index 4b2a5d94c7..a43ab59c2b 100644 --- a/src/test/java/org/mitre/synthea/world/geography/LocationTest.java +++ b/src/test/java/org/mitre/synthea/world/geography/LocationTest.java @@ -8,7 +8,6 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Random; import java.util.Set; import org.junit.Assert; @@ -146,8 +145,8 @@ public void testMalformedForeignPlaceOfBirthFileLoad() { @Test public void testGetForeignPlaceOfBirth_HappyPath() { - Random random = new Random(4L); - String[] placeOfBirth = location.randomBirthplaceByLanguage(random, "german"); + Person person = new Person(4L); + String[] placeOfBirth = location.randomBirthplaceByLanguage(person, "german"); for (String part : placeOfBirth) { Assert.assertNotNull(part); Assert.assertTrue(placeOfBirth[placeOfBirth.length - 1].contains(part)); @@ -156,8 +155,8 @@ public void testGetForeignPlaceOfBirth_HappyPath() { @Test public void testGetForeignPlaceOfBirth_ValidStringInvalidFormat_1() { - Random random = new Random(0L); - String[] placeOfBirth = location.randomBirthplaceByLanguage(random, "too_many_elements"); + Person person = new Person(0L); + String[] placeOfBirth = location.randomBirthplaceByLanguage(person, "too_many_elements"); for (String part : placeOfBirth) { Assert.assertNotNull(part); Assert.assertTrue(placeOfBirth[placeOfBirth.length - 1].contains(part)); @@ -166,8 +165,8 @@ public void testGetForeignPlaceOfBirth_ValidStringInvalidFormat_1() { @Test public void testGetForeignPlaceOfBirth_ValidStringInvalidFormat_2() { - Random random = new Random(0L); - String[] placeOfBirth = location.randomBirthplaceByLanguage(random, "not_enough_elements"); + Person person = new Person(0L); + String[] placeOfBirth = location.randomBirthplaceByLanguage(person, "not_enough_elements"); for (String part : placeOfBirth) { Assert.assertNotNull(part); Assert.assertTrue(placeOfBirth[placeOfBirth.length - 1].contains(part)); @@ -176,8 +175,8 @@ public void testGetForeignPlaceOfBirth_ValidStringInvalidFormat_2() { @Test public void testGetForeignPlaceOfBirth_MissingValue() { - Random random = new Random(0L); - String[] placeOfBirth = location.randomBirthplaceByLanguage(random, "unknown_ethnicity"); + Person person = new Person(0L); + String[] placeOfBirth = location.randomBirthplaceByLanguage(person, "unknown_ethnicity"); for (String part : placeOfBirth) { Assert.assertNotNull(part); Assert.assertTrue(placeOfBirth[placeOfBirth.length - 1].contains(part)); @@ -186,8 +185,8 @@ public void testGetForeignPlaceOfBirth_MissingValue() { @Test public void testGetForeignPlaceOfBirth_EmptyValue() { - Random random = new Random(0L); - String[] placeOfBirth = location.randomBirthplaceByLanguage(random, "empty_ethnicity"); + Person person = new Person(0L); + String[] placeOfBirth = location.randomBirthplaceByLanguage(person, "empty_ethnicity"); for (String part : placeOfBirth) { Assert.assertNotNull(part); Assert.assertTrue(placeOfBirth[placeOfBirth.length - 1].contains(part)); From b56a7c1eff0a463cbbf249d6c76fd9e4bcb68cc8 Mon Sep 17 00:00:00 2001 From: Jason Walonoski Date: Fri, 24 Jul 2020 09:24:10 -0400 Subject: [PATCH 2/2] Refactor with RandomNumberGenerator interface. --- .../editors/GrowthDataErrorsEditor.java | 13 +-- .../java/org/mitre/synthea/engine/Module.java | 2 +- .../java/org/mitre/synthea/engine/State.java | 13 +-- .../synthea/helpers/RandomCollection.java | 8 +- .../helpers/RandomNumberGenerator.java | 100 ++++++++++++++++++ .../org/mitre/synthea/helpers/Utilities.java | 11 +- .../mitre/synthea/world/agents/Clinician.java | 4 - .../mitre/synthea/world/agents/Person.java | 76 +------------ .../mitre/synthea/world/agents/Provider.java | 7 +- .../world/agents/behaviors/IPayerFinder.java | 5 +- .../mitre/synthea/world/concepts/Costs.java | 9 +- .../synthea/world/concepts/HealthRecord.java | 11 +- .../synthea/world/geography/Location.java | 43 ++++---- 13 files changed, 166 insertions(+), 136 deletions(-) create mode 100644 src/main/java/org/mitre/synthea/helpers/RandomNumberGenerator.java diff --git a/src/main/java/org/mitre/synthea/editors/GrowthDataErrorsEditor.java b/src/main/java/org/mitre/synthea/editors/GrowthDataErrorsEditor.java index 0d504665d8..bcfa1afc94 100644 --- a/src/main/java/org/mitre/synthea/editors/GrowthDataErrorsEditor.java +++ b/src/main/java/org/mitre/synthea/editors/GrowthDataErrorsEditor.java @@ -6,6 +6,7 @@ import java.util.stream.Collectors; import org.mitre.synthea.engine.HealthRecordEditor; +import org.mitre.synthea.helpers.RandomNumberGenerator; import org.mitre.synthea.helpers.Utilities; import org.mitre.synthea.world.agents.Person; import org.mitre.synthea.world.concepts.HealthRecord; @@ -271,10 +272,10 @@ public static void introduceHeightExtremeError(HealthRecord.Encounter encounter) * @param encounter The encounter that contains the observation */ public static void introduceHeightAbsoluteError(HealthRecord.Encounter encounter, - Person person) { + RandomNumberGenerator random) { HealthRecord.Observation htObs = heightObservation(encounter); double heightValue = (Double) htObs.value; - double additionalAbsolute = person.rand() * 3; + double additionalAbsolute = random.rand() * 3; htObs.value = heightValue - (3 + additionalAbsolute); } @@ -283,10 +284,10 @@ public static void introduceHeightAbsoluteError(HealthRecord.Encounter encounter * @param encounter The encounter that contains the observation */ public static void introduceWeightDuplicateError(HealthRecord.Encounter encounter, - Person person) { + RandomNumberGenerator random) { HealthRecord.Observation wtObs = weightObservation(encounter); double weightValue = (Double) wtObs.value; - double jitter = person.rand() - 0.5; + double jitter = random.rand() - 0.5; HealthRecord.Observation newObs = encounter.addObservation(wtObs.start, wtObs.type, weightValue + jitter, "Body Weight"); newObs.category = "vital-signs"; @@ -298,10 +299,10 @@ public static void introduceWeightDuplicateError(HealthRecord.Encounter encounte * @param encounter The encounter that contains the observation */ public static void introduceHeightDuplicateError(HealthRecord.Encounter encounter, - Person person) { + RandomNumberGenerator random) { HealthRecord.Observation htObs = heightObservation(encounter); double heightValue = (Double) htObs.value; - double jitter = person.rand() - 0.5; + double jitter = random.rand() - 0.5; HealthRecord.Observation newObs = encounter.addObservation(htObs.start, htObs.type, heightValue + jitter, "Body Height"); newObs.category = "vital-signs"; diff --git a/src/main/java/org/mitre/synthea/engine/Module.java b/src/main/java/org/mitre/synthea/engine/Module.java index dbf04b49ab..1619c66f76 100644 --- a/src/main/java/org/mitre/synthea/engine/Module.java +++ b/src/main/java/org/mitre/synthea/engine/Module.java @@ -292,8 +292,8 @@ public Module clone() { clone.name = this.name; clone.submodule = this.submodule; clone.remarks = this.remarks; - clone.states = new ConcurrentHashMap(); if (this.states != null) { + clone.states = new ConcurrentHashMap(); for (String key : this.states.keySet()) { clone.states.put(key, this.states.get(key).clone()); } diff --git a/src/main/java/org/mitre/synthea/engine/State.java b/src/main/java/org/mitre/synthea/engine/State.java index dd712ef676..acfb523fcb 100644 --- a/src/main/java/org/mitre/synthea/engine/State.java +++ b/src/main/java/org/mitre/synthea/engine/State.java @@ -34,6 +34,7 @@ import org.mitre.synthea.helpers.Config; import org.mitre.synthea.helpers.ConstantValueGenerator; import org.mitre.synthea.helpers.ExpressionProcessor; +import org.mitre.synthea.helpers.RandomNumberGenerator; import org.mitre.synthea.helpers.RandomValueGenerator; import org.mitre.synthea.helpers.TimeSeriesData; import org.mitre.synthea.helpers.Utilities; @@ -1714,19 +1715,19 @@ public boolean process(Person person, long time) { return true; } - private void duplicateSeries(Person person, long time) { + private void duplicateSeries(RandomNumberGenerator random, long time) { if (minNumberSeries > 0 && maxNumberSeries >= minNumberSeries && series.size() > 0) { // Randomly pick the number of series in this study - int numberOfSeries = (int) person.rand(minNumberSeries, maxNumberSeries + 1); + int numberOfSeries = (int) random.rand(minNumberSeries, maxNumberSeries + 1); HealthRecord.ImagingStudy.Series referenceSeries = series.get(0); series = new ArrayList(); // Create the new series with random series UID for (int i = 0; i < numberOfSeries; i++) { HealthRecord.ImagingStudy.Series newSeries = referenceSeries.clone(); - newSeries.dicomUid = Utilities.randomDicomUid(person, time, i + 1, 0); + newSeries.dicomUid = Utilities.randomDicomUid(random, time, i + 1, 0); series.add(newSeries); } } else { @@ -1740,7 +1741,7 @@ private void duplicateSeries(Person person, long time) { } } - private void duplicateInstances(Person person, long time) { + private void duplicateInstances(RandomNumberGenerator random, long time) { for (int i = 0; i < series.size(); i++) { HealthRecord.ImagingStudy.Series s = series.get(i); if (s.minNumberInstances > 0 && s.maxNumberInstances >= s.minNumberInstances @@ -1748,14 +1749,14 @@ private void duplicateInstances(Person person, long time) { // Randomly pick the number of instances in this series int numberOfInstances = - (int) person.rand(s.minNumberInstances, s.maxNumberInstances + 1); + (int) random.rand(s.minNumberInstances, s.maxNumberInstances + 1); HealthRecord.ImagingStudy.Instance referenceInstance = s.instances.get(0); s.instances = new ArrayList(); // Create the new instances with random instance UIDs for (int j = 0; j < numberOfInstances; j++) { HealthRecord.ImagingStudy.Instance newInstance = referenceInstance.clone(); - newInstance.dicomUid = Utilities.randomDicomUid(person, time, i + 1, j + 1); + newInstance.dicomUid = Utilities.randomDicomUid(random, time, i + 1, j + 1); s.instances.add(newInstance); } } diff --git a/src/main/java/org/mitre/synthea/helpers/RandomCollection.java b/src/main/java/org/mitre/synthea/helpers/RandomCollection.java index 66ae1a87d2..b857069190 100644 --- a/src/main/java/org/mitre/synthea/helpers/RandomCollection.java +++ b/src/main/java/org/mitre/synthea/helpers/RandomCollection.java @@ -6,8 +6,6 @@ import java.util.Random; import java.util.TreeMap; -import org.mitre.synthea.world.agents.Person; - /** * Random collection of objects, with weightings. Intended to be an equivalent to the ruby Pickup * gem. Adapted from https://stackoverflow.com/a/6409791/630384 @@ -48,11 +46,11 @@ public E next(Random random) { * Selecting an item from one draw, does not remove the item from the collection * for subsequent draws. In other words, an item can be selected repeatedly if * the weights are severely imbalanced. - * @param person - person object, and the source of the random number generator. + * @param random the random number generator. * @return a random item from the collection weighted by the item weights. */ - public E next(Person person) { - return next(person.rand() * total); + public E next(RandomNumberGenerator random) { + return next(random.rand() * total); } private E next(double value) { diff --git a/src/main/java/org/mitre/synthea/helpers/RandomNumberGenerator.java b/src/main/java/org/mitre/synthea/helpers/RandomNumberGenerator.java new file mode 100644 index 0000000000..3b3d2c1d0b --- /dev/null +++ b/src/main/java/org/mitre/synthea/helpers/RandomNumberGenerator.java @@ -0,0 +1,100 @@ +package org.mitre.synthea.helpers; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.Arrays; + +public interface RandomNumberGenerator { + /** Returns a double between 0-1 from a uniform distribution. */ + public double rand(); + + /** + * Returns a random double in the given range. + */ + public default double rand(double low, double high) { + return (low + ((high - low) * rand())); + } + + /** + * Returns a random double in the given range with no more that the specified + * number of decimal places. + */ + public default double rand(double low, double high, Integer decimals) { + double value = rand(low, high); + if (decimals != null) { + value = BigDecimal.valueOf(value).setScale(decimals, RoundingMode.HALF_UP).doubleValue(); + } + return value; + } + + /** + * Helper function to get a random number based on an array of [min, max]. This + * should be used primarily when pulling ranges from YML. + * + * @param range array [min, max] + * @return random double between min and max + */ + public default double rand(double[] range) { + if (range == null || range.length != 2) { + throw new IllegalArgumentException( + "input range must be of length 2 -- got " + Arrays.toString(range)); + } + + if (range[0] > range[1]) { + throw new IllegalArgumentException( + "range must be of the form {low, high} -- got " + Arrays.toString(range)); + } + + return rand(range[0], range[1]); + } + + /** + * Return one of the options randomly with uniform distribution. + * + * @param choices The options to be returned. + * @return One of the options randomly selected. + */ + public default String rand(String[] choices) { + int value = randInt(choices.length); + return choices[value]; + } + + /** + * Helper function to get a random number based on an integer array of [min, + * max]. This should be used primarily when pulling ranges from YML. + * + * @param range array [min, max] + * @return random double between min and max + */ + public default double rand(int[] range) { + if (range == null || range.length != 2) { + throw new IllegalArgumentException( + "input range must be of length 2 -- got " + Arrays.toString(range)); + } + + if (range[0] > range[1]) { + throw new IllegalArgumentException( + "range must be of the form {low, high} -- got " + Arrays.toString(range)); + } + + return rand(range[0], range[1]); + } + + /** Returns a random boolean. */ + public boolean randBoolean(); + + /** + * Returns a double between from a normal distribution + * with mean of 0 and standard deviation of 1. + */ + public double randGaussian(); + + /** Returns a random integer. */ + public int randInt(); + + /** Returns a random integer in the given bound. */ + public int randInt(int bound); + + /** Return a random long. */ + public long randLong(); +} diff --git a/src/main/java/org/mitre/synthea/helpers/Utilities.java b/src/main/java/org/mitre/synthea/helpers/Utilities.java index 73ebca1b69..1807e059f7 100644 --- a/src/main/java/org/mitre/synthea/helpers/Utilities.java +++ b/src/main/java/org/mitre/synthea/helpers/Utilities.java @@ -20,7 +20,6 @@ import org.mitre.synthea.engine.Logic; import org.mitre.synthea.engine.State; -import org.mitre.synthea.world.agents.Person; import org.mitre.synthea.world.concepts.HealthRecord.Code; public class Utilities { @@ -331,10 +330,11 @@ public static Gson getGson() { * * @return a String DICOM UID */ - public static String randomDicomUid(Person person, long time, int seriesNo, int instanceNo) { + public static String randomDicomUid(RandomNumberGenerator random, + long time, int seriesNo, int instanceNo) { // Add a random salt to increase uniqueness - String salt = randomDicomUidSalt(person); + String salt = randomDicomUidSalt(random); String now = String.valueOf(time); String uid = "1.2.840.99999999"; // 99999999 is an arbitrary organizational identifier @@ -352,13 +352,14 @@ public static String randomDicomUid(Person person, long time, int seriesNo, int /** * Generates a random string of 8 numbers to use as a salt for DICOM UIDs. + * @param random the source of randomness * @return The 8-digit numeric salt, as a String */ - private static String randomDicomUidSalt(Person person) { + private static String randomDicomUidSalt(RandomNumberGenerator random) { final int MIN = 10000000; final int MAX = 99999999; - int saltInt = person.randInt(MAX - MIN + 1) + MIN; + int saltInt = random.randInt(MAX - MIN + 1) + MIN; return String.valueOf(saltInt); } diff --git a/src/main/java/org/mitre/synthea/world/agents/Clinician.java b/src/main/java/org/mitre/synthea/world/agents/Clinician.java index 3a752f63dd..cdebabedd3 100644 --- a/src/main/java/org/mitre/synthea/world/agents/Clinician.java +++ b/src/main/java/org/mitre/synthea/world/agents/Clinician.java @@ -117,10 +117,6 @@ public synchronized int incrementEncounters() { public int getEncounterCount() { return encounters; } - - public int randInt() { - return random.nextInt(); - } public int randInt(int bound) { return random.nextInt(bound); diff --git a/src/main/java/org/mitre/synthea/world/agents/Person.java b/src/main/java/org/mitre/synthea/world/agents/Person.java index 8ea2ea055a..74361183d4 100644 --- a/src/main/java/org/mitre/synthea/world/agents/Person.java +++ b/src/main/java/org/mitre/synthea/world/agents/Person.java @@ -8,7 +8,6 @@ import java.time.LocalDate; import java.time.Period; import java.time.ZoneId; -import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -23,6 +22,7 @@ import org.mitre.synthea.engine.State; import org.mitre.synthea.helpers.Config; import org.mitre.synthea.helpers.ConstantValueGenerator; +import org.mitre.synthea.helpers.RandomNumberGenerator; import org.mitre.synthea.helpers.Utilities; import org.mitre.synthea.helpers.ValueGenerator; import org.mitre.synthea.modules.QualityOfLifeModule; @@ -33,7 +33,7 @@ import org.mitre.synthea.world.concepts.VitalSign; import org.mitre.synthea.world.geography.quadtree.QuadTreeElement; -public class Person implements Serializable, QuadTreeElement { +public class Person implements Serializable, RandomNumberGenerator, QuadTreeElement { private static final long serialVersionUID = 4322116644425686379L; private static final ZoneId timeZone = ZoneId.systemDefault(); @@ -174,78 +174,6 @@ public double rand() { return random.nextDouble(); } - /** - * Returns a random double in the given range. - */ - public double rand(double low, double high) { - return (low + ((high - low) * rand())); - } - - /** - * Returns a random double in the given range with no more that the specified - * number of decimal places. - */ - public double rand(double low, double high, Integer decimals) { - double value = rand(low, high); - if (decimals != null) { - value = BigDecimal.valueOf(value).setScale(decimals, RoundingMode.HALF_UP).doubleValue(); - } - return value; - } - - /** - * Helper function to get a random number based on an array of [min, max]. This - * should be used primarily when pulling ranges from YML. - * - * @param range array [min, max] - * @return random double between min and max - */ - public double rand(double[] range) { - if (range == null || range.length != 2) { - throw new IllegalArgumentException( - "input range must be of length 2 -- got " + Arrays.toString(range)); - } - - if (range[0] > range[1]) { - throw new IllegalArgumentException( - "range must be of the form {low, high} -- got " + Arrays.toString(range)); - } - - return rand(range[0], range[1]); - } - - /** - * Return one of the options randomly with uniform distribution. - * - * @param choices The options to be returned. - * @return One of the options randomly selected. - */ - public String rand(String[] choices) { - int value = random.nextInt(choices.length); - return choices[value]; - } - - /** - * Helper function to get a random number based on an integer array of [min, - * max]. This should be used primarily when pulling ranges from YML. - * - * @param range array [min, max] - * @return random double between min and max - */ - public double rand(int[] range) { - if (range == null || range.length != 2) { - throw new IllegalArgumentException( - "input range must be of length 2 -- got " + Arrays.toString(range)); - } - - if (range[0] > range[1]) { - throw new IllegalArgumentException( - "range must be of the form {low, high} -- got " + Arrays.toString(range)); - } - - return rand(range[0], range[1]); - } - /** * Returns a random boolean. */ diff --git a/src/main/java/org/mitre/synthea/world/agents/Provider.java b/src/main/java/org/mitre/synthea/world/agents/Provider.java index b61f734966..c9f4cc5d4a 100644 --- a/src/main/java/org/mitre/synthea/world/agents/Provider.java +++ b/src/main/java/org/mitre/synthea/world/agents/Provider.java @@ -21,6 +21,7 @@ import java.util.concurrent.atomic.AtomicInteger; import org.mitre.synthea.helpers.Config; +import org.mitre.synthea.helpers.RandomNumberGenerator; import org.mitre.synthea.helpers.SimpleCSV; import org.mitre.synthea.helpers.Utilities; import org.mitre.synthea.modules.LifecycleModule; @@ -488,12 +489,12 @@ private Clinician generateClinician(long clinicianSeed, Random clinicianRand, /** * Randomly chooses a clinician out of a given clinician list. * @param specialty - the specialty to choose from. - * @param person - the patient. + * @param rand - random number generator. * @return A clinician with the required specialty. */ - public Clinician chooseClinicianList(String specialty, Person person) { + public Clinician chooseClinicianList(String specialty, RandomNumberGenerator rand) { ArrayList clinicians = this.clinicianMap.get(specialty); - Clinician doc = clinicians.get(person.randInt(clinicians.size())); + Clinician doc = clinicians.get(rand.randInt(clinicians.size())); doc.incrementEncounters(); return doc; } diff --git a/src/main/java/org/mitre/synthea/world/agents/behaviors/IPayerFinder.java b/src/main/java/org/mitre/synthea/world/agents/behaviors/IPayerFinder.java index 424c565124..4ee52ef2f3 100644 --- a/src/main/java/org/mitre/synthea/world/agents/behaviors/IPayerFinder.java +++ b/src/main/java/org/mitre/synthea/world/agents/behaviors/IPayerFinder.java @@ -2,6 +2,7 @@ import java.util.List; +import org.mitre.synthea.helpers.RandomNumberGenerator; import org.mitre.synthea.modules.HealthInsuranceModule; import org.mitre.synthea.world.agents.Payer; import org.mitre.synthea.world.agents.Person; @@ -51,14 +52,14 @@ public static boolean meetsBasicRequirements( * @param options the list of acceptable payer options that the person can recieve. * @return a random payer from the given list of options. */ - public default Payer chooseRandomlyFromList(List options, Person person) { + public default Payer chooseRandomlyFromList(List options, RandomNumberGenerator rand) { if (options.isEmpty()) { return Payer.noInsurance; } else if (options.size() == 1) { return options.get(0); } else { // There are a few equally good options, pick one randomly. - return options.get(person.randInt(options.size())); + return options.get(rand.randInt(options.size())); } } } \ No newline at end of file diff --git a/src/main/java/org/mitre/synthea/world/concepts/Costs.java b/src/main/java/org/mitre/synthea/world/concepts/Costs.java index c70b61cc0d..8e715f73c2 100644 --- a/src/main/java/org/mitre/synthea/world/concepts/Costs.java +++ b/src/main/java/org/mitre/synthea/world/concepts/Costs.java @@ -7,6 +7,7 @@ import java.util.Map; import org.mitre.synthea.helpers.Config; +import org.mitre.synthea.helpers.RandomNumberGenerator; import org.mitre.synthea.helpers.SimpleCSV; import org.mitre.synthea.helpers.Utilities; import org.mitre.synthea.world.agents.Person; @@ -173,7 +174,7 @@ public static boolean hasCost(Entry entry) { * Helper class to store a grouping of cost data for a single concept. Currently * cost data includes a minimum, maximum, and mode (most common value). * Selection of individual prices based on this cost data should be done using - * the chooseCost(Random) method. + * the chooseCost method. */ private static class CostData { private double min; @@ -189,11 +190,11 @@ private CostData(double min, double mode, double max) { /** * Select an individual cost based on this cost data. Uses a triangular * distribution to pick a randomized value. - * @param random Source of randomness + * @param rand Source of randomness * @return Single cost within the range this set of cost data represents */ - private double chooseCost(Person person) { - return triangularDistribution(min, max, mode, person.rand()); + private double chooseCost(RandomNumberGenerator rand) { + return triangularDistribution(min, max, mode, rand.rand()); } /** diff --git a/src/main/java/org/mitre/synthea/world/concepts/HealthRecord.java b/src/main/java/org/mitre/synthea/world/concepts/HealthRecord.java index bc5bad8796..82c7bd7004 100644 --- a/src/main/java/org/mitre/synthea/world/concepts/HealthRecord.java +++ b/src/main/java/org/mitre/synthea/world/concepts/HealthRecord.java @@ -22,6 +22,7 @@ import java.util.Set; import java.util.concurrent.TimeUnit; +import org.mitre.synthea.helpers.RandomNumberGenerator; import org.mitre.synthea.helpers.Utilities; import org.mitre.synthea.world.agents.Clinician; import org.mitre.synthea.world.agents.Person; @@ -408,14 +409,14 @@ public Device(long start, String type) { /** * Set the human readable form of the UDI for this Person's device. - * @param person The person who owns or contains the device. + * @param random the random number generator */ - public void generateUDI(Person person) { - deviceIdentifier = trimLong(person.randLong(), 14); + public void generateUDI(RandomNumberGenerator random) { + deviceIdentifier = trimLong(random.randLong(), 14); manufactureTime = start - Utilities.convertTime("weeks", 3); expirationTime = start + Utilities.convertTime("years", 25); - lotNumber = trimLong(person.randLong(), (int) person.rand(4, 20)); - serialNumber = trimLong(person.randLong(), (int) person.rand(4, 20)); + lotNumber = trimLong(random.randLong(), (int) random.rand(4, 20)); + serialNumber = trimLong(random.randLong(), (int) random.rand(4, 20)); udi = "(01)" + deviceIdentifier; udi += "(11)" + udiDate(manufactureTime); diff --git a/src/main/java/org/mitre/synthea/world/geography/Location.java b/src/main/java/org/mitre/synthea/world/geography/Location.java index c878b0f868..2e9799bae9 100644 --- a/src/main/java/org/mitre/synthea/world/geography/Location.java +++ b/src/main/java/org/mitre/synthea/world/geography/Location.java @@ -15,6 +15,7 @@ import org.apache.commons.lang3.ArrayUtils; import org.mitre.synthea.helpers.Config; +import org.mitre.synthea.helpers.RandomNumberGenerator; import org.mitre.synthea.helpers.SimpleCSV; import org.mitre.synthea.helpers.Utilities; import org.mitre.synthea.world.agents.Clinician; @@ -124,14 +125,14 @@ public Location(String state, String city) { * If a city has more than one zip code, this picks a random one. * * @param cityName Name of the city - * @param person Used for a source of repeatable randomness when selecting a zipcode when multiple - * exist for a location + * @param random Used for a source of repeatable randomness when selecting + * a zipcode when multiple exist for a location * @return a zip code for the given city */ - public String getZipCode(String cityName, Person person) { + public String getZipCode(String cityName, RandomNumberGenerator random) { List zipsForCity = getZipCodes(cityName); if (zipsForCity.size() > 1) { - int randomChoice = person.randInt(zipsForCity.size()); + int randomChoice = random.randInt(zipsForCity.size()); return zipsForCity.get(randomChoice); } else { return zipsForCity.get(0); @@ -170,10 +171,10 @@ public long getPopulation(String cityName) { * Pick the name of a random city from the current "world". * If only one city was selected, this will return that one city. * - * @param person The person and source of randomness + * @param random The source of randomness. * @return Demographics of a random city. */ - public Demographics randomCity(Person person) { + public Demographics randomCity(RandomNumberGenerator random) { if (city != null) { // if we're only generating one city at a time, just use the largest entry for that one city if (fixedCity == null) { @@ -183,7 +184,7 @@ public Demographics randomCity(Person person) { } return fixedCity; } - return demographics.get(randomCityId(person)); + return demographics.get(randomCityId(random)); } /** @@ -206,21 +207,21 @@ public Demographics randomCity(Random random) { /** * Pick a random city name, weighted by population. - * @param person The person and source of randomness + * @param random the source of randomness * @return a city name */ - public String randomCityName(Person person) { - String cityId = randomCityId(person); + public String randomCityName(RandomNumberGenerator random) { + String cityId = randomCityId(random); return demographics.get(cityId).city; } /** * Pick a random city id, weighted by population. - * @param person The person and source of randomness + * @param random the source of randomness * @return a city id */ - private String randomCityId(Person person) { - long targetPop = (long) (person.rand() * totalPopulation); + private String randomCityId(RandomNumberGenerator random) { + long targetPop = (long) (random.rand() * totalPopulation); for (Map.Entry city : populationByCityId.entrySet()) { targetPop -= city.getValue(); @@ -251,12 +252,12 @@ private String randomCityId(Random random) { /** * Pick a random birth place, weighted by population. - * @param person The person and source of randomness + * @param random the source of randomness * @return Array of Strings: [city, state, country, "city, state, country"] */ - public String[] randomBirthPlace(Person person) { + public String[] randomBirthPlace(RandomNumberGenerator random) { String[] birthPlace = new String[4]; - birthPlace[0] = randomCityName(person); + birthPlace[0] = randomCityName(random); birthPlace[1] = this.state; birthPlace[2] = COUNTRY_CODE; birthPlace[3] = birthPlace[0] + ", " + birthPlace[1] + ", " + birthPlace[2]; @@ -269,23 +270,23 @@ public String[] randomBirthPlace(Person person) { * In the case an language is not present the method returns the value from a call to * randomCityName(). * - * @param person The person and source of randomness + * @param random the source of randomness * @param language the language to look for cities in * @return A String representing the place of birth */ - public String[] randomBirthplaceByLanguage(Person person, String language) { + public String[] randomBirthplaceByLanguage(RandomNumberGenerator random, String language) { String[] birthPlace; List cities = foreignPlacesOfBirth.get(language.toLowerCase()); if (cities != null && cities.size() > 0) { int upperBound = cities.size(); - String randomBirthPlace = cities.get(person.randInt(upperBound)); + String randomBirthPlace = cities.get(random.randInt(upperBound)); String[] split = randomBirthPlace.split(","); // make sure we have exactly 3 elements (city, state, country_abbr) // if not fallback to some random US location if (split.length != 3) { - birthPlace = randomBirthPlace(person); + birthPlace = randomBirthPlace(random); } else { //concatenate all the results together, adding spaces behind commas for readability birthPlace = ArrayUtils.addAll(split, @@ -293,7 +294,7 @@ public String[] randomBirthplaceByLanguage(Person person, String language) { } } else { //if we can't find a foreign city at least return something - birthPlace = randomBirthPlace(person); + birthPlace = randomBirthPlace(random); } return birthPlace;