Skip to content

Commit

Permalink
Add support for mapping condition codes to ICD-10-CM in the FHIR R4 e…
Browse files Browse the repository at this point in the history
…xporter. Fix some checkstyle warnings.
  • Loading branch information
hadleynet committed Oct 15, 2024
1 parent 76f2925 commit 3120e50
Show file tree
Hide file tree
Showing 8 changed files with 157 additions and 11 deletions.
15 changes: 8 additions & 7 deletions src/main/java/App.java
Original file line number Diff line number Diff line change
Expand Up @@ -280,14 +280,15 @@ public static void main(String[] args) throws Exception {
* Reset the fields of the provided options to the current values in the Config.
*/
private static void resetOptionsFromConfig(Generator.GeneratorOptions options,
Exporter.ExporterRuntimeOptions exportOptions) {
// Any options that are automatically set by reading the configuration
// file during options initialization need to be reset here.
options.population = Config.getAsInteger("generate.default_population", 1);
options.threadPoolSize = Config.getAsInteger("generate.thread_pool_size", -1);
Exporter.ExporterRuntimeOptions exportOptions) {
// Any options that are automatically set by reading the configuration
// file during options initialization need to be reset here.
options.population = Config.getAsInteger("generate.default_population", 1);
options.threadPoolSize = Config.getAsInteger("generate.thread_pool_size", -1);

exportOptions.yearsOfHistory = Config.getAsInteger("exporter.years_of_history", 10);
exportOptions.terminologyService = !Config.get("generate.terminology_service_url", "").isEmpty();
exportOptions.yearsOfHistory = Config.getAsInteger("exporter.years_of_history", 10);
exportOptions.terminologyService = !Config.get("generate.terminology_service_url", "")
.isEmpty();
}

private static boolean validateConfig(Generator.GeneratorOptions options,
Expand Down
1 change: 1 addition & 0 deletions src/main/java/org/mitre/synthea/engine/Generator.java
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ private void init() {
CDWExporter.getInstance().setKeyStart((stateIndex * 1_000_000) + 1);
}
Exporter.loadCustomExporters();
Exporter.loadCodeMappers();

this.populationRandom = new DefaultRandomNumberGenerator(options.seed);
this.clinicianRandom = new DefaultRandomNumberGenerator(options.clinicianSeed);
Expand Down
44 changes: 44 additions & 0 deletions src/main/java/org/mitre/synthea/export/Exporter.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,17 @@
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
Expand All @@ -34,6 +37,7 @@
import org.mitre.synthea.export.flexporter.FlexporterJavascriptContext;
import org.mitre.synthea.export.flexporter.Mapping;
import org.mitre.synthea.export.rif.BB2RIFExporter;
import org.mitre.synthea.export.rif.CodeMapper;
import org.mitre.synthea.helpers.Config;
import org.mitre.synthea.helpers.TransitionMetrics;
import org.mitre.synthea.helpers.Utilities;
Expand Down Expand Up @@ -69,6 +73,7 @@ public enum SupportedFhirVersion {

private static List<PatientExporter> patientExporters;
private static List<PostCompletionExporter> postCompletionExporters;
private static Map<String, CodeMapper> codeMappers;

/**
* If the config setting "exporter.enable_custom_exporters" is enabled,
Expand All @@ -95,6 +100,45 @@ public static void loadCustomExporters() {
}
}

/**
* Load any configured code mappers. Code mappers are configured via the
* synthea.properties file and a sample configuration is shown below:
* <pre>
* exporter.code_map.icd_10=export/anti_amyloid_code_map.json
* exporter.code_map.cpt=export/phlebotomy_code_map.json,export/neurology_code_map.json
* </pre>
* The above define a single code map for ICD-10 codes and two code maps for CPT codes.
*/
public static void loadCodeMappers() {
codeMappers = new HashMap<String, CodeMapper>();
List<String> codeSystemProperties = Config.allPropertyNames()
.stream()
.filter((key) -> key.startsWith("exporter.code_map"))
.collect(Collectors.toList());
codeSystemProperties.forEach(codeSystemProperty -> {
String codeSystem = codeSystemProperty.strip().replace(
"exporter.code_map.", "").toUpperCase();
String[] resources = Config.get(codeSystemProperty).split(",");
for (String resource: resources) {
CodeMapper mapper = new CodeMapper(resource);
if (codeMappers.containsKey(codeSystem)) {
codeMappers.get(codeSystem).merge(mapper);
} else {
codeMappers.put(codeSystem, mapper);
}
}
});
}

/**
* Get the code mapper for the supplied code system.
* @param codeSystem the code system
* @return the corresponding code mapper or null if none configured
*/
public static CodeMapper getCodeMapper(String codeSystem) {
return codeMappers.get(codeSystem);
}

/**
* Runtime configuration of the record exporter.
*/
Expand Down
29 changes: 27 additions & 2 deletions src/main/java/org/mitre/synthea/export/FhirR4.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

import java.awt.geom.Point2D;
import java.io.IOException;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
Expand Down Expand Up @@ -139,7 +140,10 @@

import org.mitre.synthea.engine.Components;
import org.mitre.synthea.engine.Components.Attachment;
import org.mitre.synthea.export.rif.CodeMapper;
import org.mitre.synthea.helpers.Config;
import org.mitre.synthea.helpers.RandomNumberGenerator;
import org.mitre.synthea.helpers.RandomValueGenerator;
import org.mitre.synthea.helpers.SimpleCSV;
import org.mitre.synthea.helpers.Utilities;
import org.mitre.synthea.identity.Entity;
Expand Down Expand Up @@ -404,7 +408,7 @@ public static Bundle convertToFHIR(Person person, long stopTime) {

if (shouldExport(Condition.class)) {
for (HealthRecord.Entry condition : encounter.conditions) {
condition(personEntry, bundle, encounterEntry, condition);
condition(person, personEntry, bundle, encounterEntry, condition);
}
}

Expand Down Expand Up @@ -894,6 +898,16 @@ private static BundleEntryComponent encounter(Person person, BundleEntryComponen
if (encounter.reason != null) {
encounterResource.addReasonCode().addCoding().setCode(encounter.reason.code)
.setDisplay(encounter.reason.display).setSystem(SNOMED_URI);
CodeMapper mapper = Exporter.getCodeMapper("ICD10-CM");
if (mapper != null && mapper.canMap(encounter.reason.code)) {
Coding coding = new Coding();
Map.Entry<String, String> mappedCode = mapper.mapToCodeAndDescription(
encounter.reason, person);
coding.setCode(mappedCode.getKey());
coding.setDisplay(mappedCode.getValue());
coding.setSystem(ExportHelper.getSystemURI("ICD10-CM"));
encounterResource.getReasonCodeFirstRep().addCoding(coding);
}
}

Provider provider = encounter.provider;
Expand Down Expand Up @@ -1607,6 +1621,7 @@ private static BundleEntryComponent explanationOfBenefit(BundleEntryComponent pe
* @return The added Entry
*/
private static BundleEntryComponent condition(
RandomNumberGenerator rand,
BundleEntryComponent personEntry, Bundle bundle, BundleEntryComponent encounterEntry,
HealthRecord.Entry condition) {
Condition conditionResource = new Condition();
Expand All @@ -1630,7 +1645,17 @@ private static BundleEntryComponent condition(
conditionResource.setEncounter(new Reference(encounterEntry.getFullUrl()));

Code code = condition.codes.get(0);
conditionResource.setCode(mapCodeToCodeableConcept(code, SNOMED_URI));
CodeableConcept concept = mapCodeToCodeableConcept(code, SNOMED_URI);
CodeMapper mapper = Exporter.getCodeMapper("ICD10-CM");
if (mapper != null && mapper.canMap(code)) {
Coding coding = new Coding();
Map.Entry<String, String> mappedCode = mapper.mapToCodeAndDescription(code, rand);
coding.setCode(mappedCode.getKey());
coding.setDisplay(mappedCode.getValue());
coding.setSystem(ExportHelper.getSystemURI("ICD10-CM"));
concept.addCoding(coding);
}
conditionResource.setCode(concept);

CodeableConcept verification = new CodeableConcept();
verification.getCodingFirstRep()
Expand Down
39 changes: 39 additions & 0 deletions src/main/java/org/mitre/synthea/export/rif/CodeMapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.google.gson.reflect.TypeToken;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.AbstractMap.SimpleEntry;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
Expand Down Expand Up @@ -86,6 +87,26 @@ public CodeMapper(String jsonMapResource) {
}
}

/**
* Merge the mappings from the supplied code mapper. This method performs a deep
* merge such that destination codes will be merged if this and other contain
* mappings for the same source code. Care is required with the use of weights
* such that this and other use the same weighting scale (e.g. 0-1) otherwise one
* set of mappings could overwhelm the other.
* @param other the other code mapper from which mappings will be copied.
*/
public void merge(CodeMapper other) {
if (other.hasMap()) {
other.map.forEach((source, mapped) -> {
if (map.containsKey(source)) {
map.get(source).addAll(mapped);
} else {
map.put(source, mapped);
}
});
}
}

/**
* Determines whether this mapper has an entry for the supplied code.
* @param codeToMap the Synthea code to look for
Expand Down Expand Up @@ -249,6 +270,24 @@ public String map(Code codeToMap, String bfdCodeType, RandomNumberGenerator rand
return map(codeToMap.code, bfdCodeType, rand, stripDots);
}

/**
* Get one of the BFD codes for the supplied Synthea code. Equivalent to
* {@code map(codeToMap, "code", rand)}.
* @param codeToMap the Synthea code to look for
* @param rand a source of random numbers used to pick one of the list of BFD codes
* @return the BFD code and display string or null if the code can't be mapped
*/
public SimpleEntry<String, String> mapToCodeAndDescription(Code codeToMap,
RandomNumberGenerator rand) {
if (!canMap(codeToMap)) {
return null;
}
RandomCollection<Map<String, String>> options = map.get(codeToMap.code);
Map<String, String> mappedCode = options.next(rand);
return new SimpleEntry<>(mappedCode.get("code"),
mappedCode.get("description"));
}

/**
* Get the missing code as a list of maps, where each map includes the mapper name, a missing
* code, a description, and the count of times the code was requested.
Expand Down
15 changes: 13 additions & 2 deletions src/main/java/org/mitre/synthea/helpers/RandomCollection.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@

import java.io.Serializable;
import java.util.Map.Entry;
import java.util.NavigableMap;
import java.util.TreeMap;

/**
* Random collection of objects, with weightings. Intended to be an equivalent to the ruby Pickup
* gem. Adapted from https://stackoverflow.com/a/6409791/630384
*/
public class RandomCollection<E> implements Serializable {
private final NavigableMap<Double, E> map = new TreeMap<Double, E>();
private final TreeMap<Double, E> map = new TreeMap<Double, E>();
private double total = 0;

/**
Expand All @@ -28,6 +27,18 @@ public void add(double weight, E result) {
map.put(total, result);
}

/**
* Add all of the entries from the supplied RandomCollection.
* @param other the collection from which to copy entries.
*/
public void addAll(RandomCollection<E> other) {
Double weightAdj = 0.0;
for (Entry<Double, E> e: other.map.entrySet()) {
add(e.getKey() - weightAdj, e.getValue());
weightAdj = e.getKey();
}
}

/**
* 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
Expand Down
6 changes: 6 additions & 0 deletions src/main/resources/synthea.properties
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,12 @@ exporter.symptoms.text.export = false
# enable searching for custom exporter implementations
exporter.enable_custom_exporters = true

# enable mapping of Synthea-native code systems to others
# each key is for a specific code system and the values are comma separated paths to
# JSON mapping files
# see the CodeMapper class for code mapping file format and functionality
#exporter.code_map.icd10-cm=export/anti_amyloid_code_map.json

# the number of patients to generate, by default
# this can be overridden by passing a different value to the Generator constructor
generate.default_population = 1
Expand Down
19 changes: 19 additions & 0 deletions src/test/java/org/mitre/synthea/export/rif/CodeMapperTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,25 @@ public void testWeightedCodeMapper() {
assertTrue(defCount > abcCount);
}

@Test
public void testCodeMapperMerge() {
Config.set("exporter.bfd.require_code_maps", "true");
CodeMapper mapper = new CodeMapper("export/unweighted_code_map.json");
mapper.merge(new CodeMapper("export/weighted_code_map.json"));
assertTrue(mapper.canMap("10509002"));
int abcCount = 0;
int defCount = 0;
for (int i = 0; i < 10; i++) {
String code = mapper.map("10509002", random);
if (code.equals("ABC20.9")) {
abcCount++;
} else if (code.equals("DEF20.9")) {
defCount++;
}
}
assertTrue(defCount > abcCount);
}

@Test(expected = MissingResourceException.class)
public void testThrowsExceptionWhenMapFileMissing() {
Config.set("exporter.bfd.require_code_maps", "true");
Expand Down

0 comments on commit 3120e50

Please sign in to comment.