Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
11 changes: 10 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,11 @@ public Generator(GeneratorOptions o, Exporter.ExporterRuntimeOptions ero) {

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

// Check if we need to override demographics based on preferred provider setting
ProviderFinderPreferOne.overrideDemographicsIfPreferredProvider(demoAttributes);

Comment thread
andrequina marked this conversation as resolved.
Outdated
person.attributes.putAll(demoAttributes);
person.attributes.put(Person.LOCATION, this.location);
person.lastUpdated = (long) demoAttributes.get(Person.BIRTHDATE);
Expand Down
73 changes: 54 additions & 19 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,30 @@ public static List<Provider> getProviderList() {
return new ArrayList<Provider>(providerByUuid.values());
}

public static String findProviderStateByNPI(String npi) {

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 Location.getStateNameFromAbbreviation(row.get("state"));
}
} catch (IOException e) {
System.err.println("ERROR: unable to find state for provider by NPI: " + e.getMessage());
}
Comment thread
andrequina marked this conversation as resolved.

}

return null;
}

void merge(Provider other) {
if (this.uuid == null) {
this.uuid = other.uuid;
Expand Down Expand Up @@ -826,6 +861,6 @@ public double getY() {
}

public Point2D.Double getLonLat() {
return coordinates;
return coordinates;
Comment thread
andrequina marked this conversation as resolved.
Outdated
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package org.mitre.synthea.world.agents.behaviors.providerfinder;

import java.util.HashSet;
import java.util.List;

import org.mitre.synthea.helpers.Config;
import org.mitre.synthea.world.agents.Person;
import java.util.Map;
import java.util.Set;

import org.mitre.synthea.helpers.Config;
Comment thread
jawalonoski marked this conversation as resolved.
import org.mitre.synthea.world.agents.Person;
Comment thread
jawalonoski marked this conversation as resolved.
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.
* Also provides a static method to override initial person demographics based on this preference.
* 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();

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 Config.get(PREFER_ONE_NPI, null);
Comment thread
andrequina marked this conversation as resolved.
Outdated
}

public static Provider getPreferredProvider() {

if (!isUsingPreferredProvider()) return null;


String preferredNpi = getPreferredNPI();
if (preferredNpi == null || preferredNpi.isEmpty()) {
System.err.println("WARNING: generate.providers.selection_behavior=PreferOne but " + PREFER_ONE_NPI + " is not set. Using demographic location.");
return null; // NPI not configured, do nothing.
}

Provider preferredProvider = null;
// Find the preferred provider by NPI from the loaded list
// Note: This assumes the provider is within the initially loaded set.
for (Provider p : Provider.getProviderList()) {
if (preferredNpi.equals(p.npi)) {
preferredProvider = p;
break;
}
}
Comment thread
andrequina marked this conversation as resolved.
Outdated
return preferredProvider;
}

/**
* Checks configuration for the PreferOne provider setting. If enabled and the
* preferred provider is found, overrides the City, State, and Coordinate
* entries in the provided demographics map with the provider's location.
* Logs warnings if the provider or its location data is not found.
*
* @param demoAttributes The map of demographic attributes to potentially modify.
*/
public static void overrideDemographicsIfPreferredProvider(Map<String, Object> demoAttributes) {

Provider preferredProvider = getPreferredProvider();

if (preferredProvider != null) {
// Override demographics with preferred provider's location data
boolean cityOverridden = false;
boolean stateOverridden = false;
boolean coordsOverridden = false;

if (preferredProvider.city != null && !preferredProvider.city.isEmpty()) {
demoAttributes.put(Person.CITY, preferredProvider.city);
cityOverridden = true;
}
if (preferredProvider.state != null && !preferredProvider.state.isEmpty()) {
demoAttributes.put(Person.STATE, preferredProvider.state);
stateOverridden = true;
}
// IMPORTANT: Update coordinates as well for provider finding logic
java.awt.geom.Point2D.Double providerCoords = preferredProvider.getLonLat();
if (providerCoords != null) {
// Create a new Point2D object to avoid modifying the provider's instance
demoAttributes.put(Person.COORDINATE,
new java.awt.geom.Point2D.Double(providerCoords.getX(), providerCoords.getY()));
coordsOverridden = true;
}

if (!cityOverridden || !stateOverridden || !coordsOverridden) {
System.err.println("WARNING: Preferred provider NPI '" + preferredProvider.npi
+ "' found, but missing location data (City: " + preferredProvider.city
+ ", State: " + preferredProvider.state + ", Coords: " + providerCoords
+ "). Not all location attributes were overridden.");
}
// TODO: Consider updating Person.COUNTY if available? Provider doesn't store it.

} else {
// Log a warning if the provider wasn't found
System.err.println("WARNING: Preferred provider NPI '" + getPreferredNPI() + "' configured but provider not found in loaded list. Using demographic location.");
}
}


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

String preferredNpi = Config.get(PREFER_ONE_NPI, null);

System.out.println("PreferredNpi: " + preferredNpi);
Comment thread
andrequina marked this conversation as resolved.
Outdated

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);
Comment thread
andrequina marked this conversation as resolved.
}

private Provider findPreferredProvider(List<Provider> providers, String preferredNpi, Person person, EncounterType service, long time) {
Comment thread
andrequina marked this conversation as resolved.
for (Provider provider : providers) { // Iterate the passed-in list
// Check if this provider matches the preferred NPI
if (preferredNpi.equals(provider.npi)) {
System.out.println("FOUND PROVIDER: " + provider.npi + " -- " + preferredNpi);

// 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;
}
}
}
return null;
}

}
13 changes: 13 additions & 0 deletions src/main/java/org/mitre/synthea/world/geography/Location.java
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,6 +438,18 @@ public static String getAbbreviation(String state) {
return stateAbbreviations.get(state);
}

/**
* Get the state for an abbreviation.
* @param abbreviation State abbreviation. e.g. "MA"
* @return state name. e.g. "Massachusetts"
*/
public static String getStateNameFromAbbreviation(String abbreviation) {
for(Entry<String, String> entry : stateAbbreviations.entrySet()) {
if (entry.getValue().equals(abbreviation)) return entry.getKey();
}
return null;
}

Comment thread
andrequina marked this conversation as resolved.
Outdated
/**
* Get the index for a state. This maybe useful for
* exporters where you want to generate a list of unique
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