Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion src/main/java/org/mitre/synthea/engine/Generator.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
import org.mitre.synthea.world.agents.PayerManager;
import org.mitre.synthea.world.agents.Person;
import org.mitre.synthea.world.agents.Provider;
import org.mitre.synthea.world.agents.behaviors.providerfinder.ProviderFinderPreferOne;
import org.mitre.synthea.world.concepts.Costs;
import org.mitre.synthea.world.concepts.HealthRecord.Encounter;
import org.mitre.synthea.world.concepts.HealthRecord.EncounterType;
Expand Down Expand Up @@ -211,7 +212,13 @@ public Generator(GeneratorOptions o, Exporter.ExporterRuntimeOptions ero) {

private void init() {
if (options.state == null) {
options.state = DEFAULT_STATE;
if (ProviderFinderPreferOne.isUsingPreferredProvider()) {
Location providerLocation = Provider.findProviderLocationByNPI(ProviderFinderPreferOne.getPreferredNPI());
options.state = providerLocation.state;
options.city = providerLocation.city;
} else {
options.state = DEFAULT_STATE;
}
}
int stateIndex = Location.getIndex(options.state);
if (Config.getAsBoolean("exporter.cdw.export")) {
Expand Down Expand Up @@ -646,6 +653,7 @@ public Person createPerson(long personSeed, Map<String, Object> demoAttributes)
// Initialize person.
Person person = new Person(personSeed);
person.populationSeed = this.options.seed;

person.attributes.putAll(demoAttributes);
person.attributes.put(Person.LOCATION, this.location);
person.lastUpdated = (long) demoAttributes.get(Person.BIRTHDATE);
Expand Down
73 changes: 55 additions & 18 deletions src/main/java/org/mitre/synthea/world/agents/Provider.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import org.mitre.synthea.world.agents.behaviors.providerfinder.IProviderFinder;
import org.mitre.synthea.world.agents.behaviors.providerfinder.ProviderFinderNearest;
import org.mitre.synthea.world.agents.behaviors.providerfinder.ProviderFinderNearestMedicare;
import org.mitre.synthea.world.agents.behaviors.providerfinder.ProviderFinderPreferOne;
import org.mitre.synthea.world.agents.behaviors.providerfinder.ProviderFinderRandom;
import org.mitre.synthea.world.concepts.ClinicianSpecialty;
import org.mitre.synthea.world.concepts.HealthRecord.EncounterType;
Expand All @@ -54,6 +55,22 @@ public enum ProviderType {
public static final String RANDOM = "random";
public static final String NETWORK = "network";
public static final String MEDICARE = "medicare";
public static final String PREFER_ONE = "prefer_one";

// Provider source files
private static String HOSPITAL_FILE = Config.get("generate.providers.hospitals.default_file");
private static String IHS_HOSPITAL_FILE = Config.get("generate.providers.ihs.hospitals.default_file");
private static String VA_FILE = Config.get("generate.providers.veterans.default_file");
private static String PRIMARY_CARE_FILE = Config.get("generate.providers.primarycare.default_file");
private static String IHS_PC_FILE = Config.get("generate.providers.ihs.primarycare.default_file");
private static String URGENT_CARE_FILE = Config.get("generate.providers.urgentcare.default_file");
private static String HOME_HEALTH_FILE = Config.get("generate.providers.homehealth.default_file");
private static String HOSPICE_FILE = Config.get("generate.providers.hospice.default_file");
private static String NURSING_FILE = Config.get("generate.providers.nursing.default_file");

// NOTE that this should contain all of the used provider souce files. This is used for pulling provider location by NPI
private static String[] PROVIDER_SOURCE_FILES = {HOSPITAL_FILE, IHS_HOSPITAL_FILE, VA_FILE, PRIMARY_CARE_FILE, IHS_PC_FILE, URGENT_CARE_FILE, HOME_HEALTH_FILE, HOSPICE_FILE, NURSING_FILE};


/** Map of providers imported by UUID. */
private static Map<String, Provider> providerByUuid = new HashMap<String, Provider>();
Expand Down Expand Up @@ -164,6 +181,9 @@ private static IProviderFinder buildProviderFinder() {
case MEDICARE:
finder = new ProviderFinderNearestMedicare();
break;
case PREFER_ONE:
finder = new ProviderFinderPreferOne();
break;
case NEAREST:
default:
finder = new ProviderFinderNearest();
Expand Down Expand Up @@ -371,31 +391,25 @@ public static void loadProviders(Location location, DefaultRandomNumberGenerator
servicesProvided.add(EncounterType.OUTPATIENT);
servicesProvided.add(EncounterType.INPATIENT);

String hospitalFile = Config.get("generate.providers.hospitals.default_file");
loadProviders(location, hospitalFile, ProviderType.HOSPITAL, servicesProvided,
loadProviders(location, HOSPITAL_FILE, ProviderType.HOSPITAL, servicesProvided,
random, false);

String ihsHospitalFile = Config.get("generate.providers.ihs.hospitals.default_file");
loadProviders(location, ihsHospitalFile, ProviderType.IHS, servicesProvided,
loadProviders(location, IHS_HOSPITAL_FILE, ProviderType.IHS, servicesProvided,
random, true);

servicesProvided.add(EncounterType.WELLNESS);
String vaFile = Config.get("generate.providers.veterans.default_file");
loadProviders(location, vaFile, ProviderType.VETERAN, servicesProvided, random,
loadProviders(location, VA_FILE, ProviderType.VETERAN, servicesProvided, random,
false);

servicesProvided.clear();
servicesProvided.add(EncounterType.WELLNESS);
String primaryCareFile = Config.get("generate.providers.primarycare.default_file");
loadProviders(location, primaryCareFile, ProviderType.PRIMARY, servicesProvided,
loadProviders(location, PRIMARY_CARE_FILE, ProviderType.PRIMARY, servicesProvided,
random, false);
String ihsPCFile = Config.get("generate.providers.ihs.primarycare.default_file");
loadProviders(location, ihsPCFile, ProviderType.IHS, servicesProvided, random, true);
loadProviders(location, IHS_PC_FILE, ProviderType.IHS, servicesProvided, random, true);

servicesProvided.clear();
servicesProvided.add(EncounterType.URGENTCARE);
String urgentcareFile = Config.get("generate.providers.urgentcare.default_file");
loadProviders(location, urgentcareFile, ProviderType.URGENT, servicesProvided,
loadProviders(location, URGENT_CARE_FILE, ProviderType.URGENT, servicesProvided,
random, false);

statesLoaded.add(location.state);
Expand All @@ -411,20 +425,17 @@ public static void loadProviders(Location location, DefaultRandomNumberGenerator
Set<EncounterType> servicesProvided = new HashSet<EncounterType>();
servicesProvided.clear();
servicesProvided.add(EncounterType.HOME);
String homeHealthFile = Config.get("generate.providers.homehealth.default_file");
loadProviders(location, homeHealthFile, ProviderType.HOME_HEALTH, servicesProvided,
loadProviders(location, HOME_HEALTH_FILE, ProviderType.HOME_HEALTH, servicesProvided,
random, true);

servicesProvided.clear();
servicesProvided.add(EncounterType.HOSPICE);
String hospiceFile = Config.get("generate.providers.hospice.default_file");
loadProviders(location, hospiceFile, ProviderType.HOSPICE, servicesProvided,
loadProviders(location, HOSPICE_FILE, ProviderType.HOSPICE, servicesProvided,
random, true);

servicesProvided.clear();
servicesProvided.add(EncounterType.SNF);
String nursingFile = Config.get("generate.providers.nursing.default_file");
loadProviders(location, nursingFile, ProviderType.NURSING, servicesProvided,
loadProviders(location, NURSING_FILE, ProviderType.NURSING, servicesProvided,
random, true);
} catch (IOException e) {
System.err.println("WARNING: unable to load optional providers in: " + location.state);
Expand Down Expand Up @@ -743,6 +754,32 @@ public static List<Provider> getProviderList() {
return new ArrayList<Provider>(providerByUuid.values());
}

public static Location findProviderLocationByNPI(String npi) throws ExceptionInInitializerError {

for (String filename : PROVIDER_SOURCE_FILES) {

String resource;
try {
resource = Utilities.readResource(filename, true, true);
Iterator<? extends Map<String,String>> csv = SimpleCSV.parseLineByLine(resource);

while (csv.hasNext()) {
Map<String,String> row = csv.next();
String currNpi = row.get("npi");

if (npi.equals(currNpi) && row.get("state") != null) {
return new Location(Location.getStateName(row.get("state")), row.get("city"));
}
}
} catch (IOException e) {
throw new ExceptionInInitializerError("ERROR: unable to find state for provider by NPI: '" + npi + "' configured but provider not found in loaded list. Using demographic location." + e.getMessage());
}

}

return null;
}

void merge(Provider other) {
if (this.uuid == null) {
this.uuid = other.uuid;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package org.mitre.synthea.world.agents.behaviors.providerfinder;

import java.util.List;

import org.mitre.synthea.helpers.Config;
import org.mitre.synthea.world.agents.Person;
import org.mitre.synthea.world.agents.Provider;
import org.mitre.synthea.world.concepts.HealthRecord.EncounterType;

/**
* Finder that prioritizes a single provider based on NPI specified in configuration.
* If the preferred provider is not available or suitable, it falls back to the nearest provider.
*/
public class ProviderFinderPreferOne implements IProviderFinder {

private static final String PREFER_ONE_NPI = "generate.providers.prefer_one.npi";
private static final String PREFER_ONE_IGNORE_SUITABLE = "generate.providers.prefer_one.ignore_suitable";
// Fallback finder if the preferred one isn't suitable during encounter finding
private final IProviderFinder fallbackFinder = new ProviderFinderNearest();
// Cache for the preferred provider to avoid repeated lookups
private static Provider cachedPreferredProvider = null;
// Track the NPI that was used to find the cached provider
private static String cachedNpi = null;

public static boolean isUsingPreferredProvider() {
return Config.get("generate.providers.selection_behavior", "nearest").equals(Provider.PREFER_ONE);
}

public static boolean isIgnoringSuitable() {
return Boolean.valueOf(Config.get(PREFER_ONE_IGNORE_SUITABLE, "false"));
}

public static String getPreferredNPI() {
return isUsingPreferredProvider() ? Config.get(PREFER_ONE_NPI, null) : null;
}



@Override
public Provider find(List<Provider> providers, Person person, EncounterType service, long time) {

String preferredNpi = getPreferredNPI();

if (preferredNpi != null && !preferredNpi.isEmpty()) {
// first check the list passed in (if the states line up with the detault state, e.g., MA, then it may be found in the list)
// if it's not found in the list then look across all providers. The state will be updated on the patient and generator.
Provider provider = findPreferredProvider(providers, preferredNpi, person, service, time);
if (provider == null) provider = findPreferredProvider(Provider.getProviderList(), preferredNpi, person, service, time);
if (provider != null) return provider;
}

// If preferred NPI not set, not found in the list, or not suitable, use the fallback finder.
return fallbackFinder.find(providers, person, service, time);
}

private Provider findPreferredProvider(List<Provider> providers, String preferredNpi, Person person, EncounterType service, long time) {

if (!isUsingPreferredProvider()) return null;

if (preferredNpi == null || preferredNpi.isEmpty()) {
throw new ExceptionInInitializerError("ERROR: generate.providers.selection_behavior=PreferOne but " + PREFER_ONE_NPI + " is not set. Using demographic location.");
}

// Check if we already have a cached provider with the correct NPI
if (cachedPreferredProvider != null && preferredNpi.equals(cachedNpi)) {
return cachedPreferredProvider;
}

for (Provider provider : providers) {

// Check if this provider matches the preferred NPI
if (preferredNpi.equals(provider.npi)) {

cachedPreferredProvider = provider;
cachedNpi = provider.npi;

// Check if the preferred provider offers the service and accepts the patient
if (provider.hasService(service) && provider.accepts(person, time)) {
return provider;
} else {
if (isIgnoringSuitable()) return provider;
// Preferred provider found but is not suitable (doesn't offer service or accept patient)
// Break the loop and proceed to fallback.
break;
}
}
}
cachedPreferredProvider = null;
cachedNpi = null;
return null;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.commons.lang3.ArrayUtils;
import org.mitre.synthea.export.JSONSkip;
Expand Down Expand Up @@ -437,7 +438,7 @@ public static String getAbbreviation(String state) {
return stateAbbreviations.get(state);
}

/**
/**
* Get the index for a state. This maybe useful for
* exporters where you want to generate a list of unique
* identifiers that do not collide across state-boundaries.
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 @@ -252,7 +252,13 @@ generate.providers.ihs.primarycare.default_file = providers/ihs_centers.csv
# random - select randomly.
# network - select a random provider in your insurance network. same as random except it changes every time the patient switches insurance provider.
# medicare - select the nearest provider that can bill Medicare. If no Medicare provider is found, it defaults back to "nearest".
# prefer_one - select a specific provider by NPI (generate.providers.prefer_one.npi), falling back to nearest if unavailable/unsuitable.
generate.providers.selection_behavior = nearest
# NPI of the provider to prefer when generate.providers.selection_behavior = prefer_one
generate.providers.prefer_one.npi =
# only select the provider to prefer when generate.providers.selection_behavior = prefer_one
# if set to true then the prefered provider is always used even if not suitable for the encounter type
generate.providers.prefer_one.ignore_suitable=true

# if a provider cannot be found for a certain type of service,
# this will default to the nearest hospital.
Expand Down
Loading