From 90054905243ddb9bbca3637c4e657a47cd0d27c4 Mon Sep 17 00:00:00 2001 From: Jeremy Stein Date: Mon, 8 Dec 2025 17:44:17 +0000 Subject: [PATCH 1/9] Generate synthetic Interchange ADT messages on the hl7Queue queue so downstream processes have some location visits to look up against. Not generating ADT HL7 because they can only be processed by the hl7-reader, not waveform-reader, and I don't fancy relying on the fake IDS here. --- .../springconfig/EmapRabbitMqRoute.java | 5 +- .../datasources/waveform/LocationMapping.java | 4 +- waveform-generator/pom.xml | 18 ++ .../waveform_generator/Application.java | 1 + .../waveform_generator/Config.java | 20 +++ .../waveform_generator/Hl7Generator.java | 52 +++++- .../patient_model/PatientDetails.java | 147 +++++++++++++++++ .../PatientLocationChangeSet.java | 9 + .../patient_model/PatientLocationModel.java | 155 ++++++++++++++++++ .../patient_model/package-info.java | 4 + .../waveform-generator-config-envs.EXAMPLE | 4 + 11 files changed, 412 insertions(+), 7 deletions(-) create mode 100644 waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/Config.java create mode 100644 waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/patient_model/PatientDetails.java create mode 100644 waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/patient_model/PatientLocationChangeSet.java create mode 100644 waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/patient_model/PatientLocationModel.java create mode 100644 waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/patient_model/package-info.java diff --git a/emap-interchange/src/main/java/uk/ac/ucl/rits/inform/interchange/springconfig/EmapRabbitMqRoute.java b/emap-interchange/src/main/java/uk/ac/ucl/rits/inform/interchange/springconfig/EmapRabbitMqRoute.java index 78deebd21..590865865 100644 --- a/emap-interchange/src/main/java/uk/ac/ucl/rits/inform/interchange/springconfig/EmapRabbitMqRoute.java +++ b/emap-interchange/src/main/java/uk/ac/ucl/rits/inform/interchange/springconfig/EmapRabbitMqRoute.java @@ -10,7 +10,10 @@ public record EmapRabbitMqRoute(EmapDataSourceQueue queueName, EmapDataSourceExchange exchangeName) { public enum EmapDataSourceQueue { /** - * The message queue from the HL7 (IDS) feed. + * The message queue derived from the HL7 ADT (IDS) feed. + * Bit of a misnomer now that there are other HL7 inputs to + * Emap (Waveform HL7s), and of course this queue never + * contained HL7 messages anyway. */ HL7_QUEUE("hl7Queue"), /** diff --git a/emap-utils/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform/LocationMapping.java b/emap-utils/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform/LocationMapping.java index 57509100e..0aff30dbb 100644 --- a/emap-utils/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform/LocationMapping.java +++ b/emap-utils/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform/LocationMapping.java @@ -24,7 +24,7 @@ public class LocationMapping { 5, List.of(33, 34, 35, 36)); private final Map bayFromBed = new HashMap<>(); - LocationMapping() { + public LocationMapping() { for (var bayToBeds: bayToBeds.entrySet()) { Integer bay = bayToBeds.getKey(); List beds = bayToBeds.getValue(); @@ -40,7 +40,7 @@ public class LocationMapping { } } - String hl7AdtLocationFromCapsuleLocation(String capsuleLocation) { + public String hl7AdtLocationFromCapsuleLocation(String capsuleLocation) { final Pattern sideroomPattern = Pattern.compile("UCHT03ICURM(\\d+)"); Matcher sideroomMatcher = sideroomPattern.matcher(capsuleLocation); if (sideroomMatcher.find()) { diff --git a/waveform-generator/pom.xml b/waveform-generator/pom.xml index 96f807c21..93b12abb8 100644 --- a/waveform-generator/pom.xml +++ b/waveform-generator/pom.xml @@ -22,6 +22,7 @@ 10.3.1 3.3.0 3.2.1 + 2.7 3.0.1u2 uk.ac.ucl.rits.inform.datasources.waveform_generator.Application 2.0.1.Final @@ -42,6 +43,17 @@ provided + + uk.ac.ucl.rits.inform + emap-interchange + ${emap-interchange.version} + + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + org.springframework.boot spring-boot-starter-test @@ -60,6 +72,12 @@ 1.12.0 + + uk.ac.ucl.rits.inform + emap-utils + 2.7 + + org.junit.jupiter junit-jupiter-engine diff --git a/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/Application.java b/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/Application.java index 1dc379671..aeb08d8ed 100644 --- a/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/Application.java +++ b/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/Application.java @@ -12,6 +12,7 @@ */ @SpringBootApplication(scanBasePackages = { "uk.ac.ucl.rits.inform.datasources.waveform_generator", + "uk.ac.ucl.rits.inform.interchange" }) @EnableScheduling public class Application { diff --git a/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/Config.java b/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/Config.java new file mode 100644 index 000000000..416eff218 --- /dev/null +++ b/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/Config.java @@ -0,0 +1,20 @@ +package uk.ac.ucl.rits.inform.datasources.waveform_generator; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import uk.ac.ucl.rits.inform.interchange.springconfig.EmapRabbitMqRoute; + +@Configuration +public class Config { + /** + * Publish synthetic ADT messages to the standard ADT queue. + * @return config bean + */ + @Bean + public EmapRabbitMqRoute getHl7DataSource() { + return new EmapRabbitMqRoute( + EmapRabbitMqRoute.EmapDataSourceQueue.HL7_QUEUE, + EmapRabbitMqRoute.EmapDataSourceExchange.DEFAULT_EXCHANGE + ); + } +} diff --git a/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/Hl7Generator.java b/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/Hl7Generator.java index 155a3b432..4d6b4f5bb 100644 --- a/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/Hl7Generator.java +++ b/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/Hl7Generator.java @@ -10,6 +10,11 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; +import uk.ac.ucl.rits.inform.datasources.waveform.LocationMapping; +import uk.ac.ucl.rits.inform.datasources.waveform_generator.patient_model.PatientLocationModel; +import uk.ac.ucl.rits.inform.interchange.EmapOperationMessage; +import uk.ac.ucl.rits.inform.interchange.adt.AdtMessage; +import uk.ac.ucl.rits.inform.interchange.messaging.Publisher; import javax.annotation.PostConstruct; import java.io.IOException; @@ -30,6 +35,8 @@ public class Hl7Generator { private final Logger logger = LoggerFactory.getLogger(Hl7Generator.class); + private final Publisher publisher; + @Value("${waveform.synthetic.num_patients:30}") private int numPatients; @@ -66,6 +73,8 @@ private Instant getExpectedProgressDatetime() { ChronoUnit.NANOS); } + private final LocationMapping locationMapping = new LocationMapping(); + // system time (not observation time) when we started running private Long monotonicStartTimeNanos = null; @Value("${waveform.synthetic.end_datetime:#{null}}") @@ -90,15 +99,19 @@ public void setComputedDefaults() { } else { progressDatetime = startDatetime; } + // need to initialise with the fact time so ADT correlates with waveform times + patientLocationModel = new PatientLocationModel(possibleLocations, progressDatetime); } private final Hl7TcpClientFactory hl7TcpClientFactory; /** * @param hl7TcpClientFactory for sending generated messages + * @param publisher for sending synthetic ADT messages */ - public Hl7Generator(Hl7TcpClientFactory hl7TcpClientFactory) { + public Hl7Generator(Hl7TcpClientFactory hl7TcpClientFactory, Publisher publisher) { this.hl7TcpClientFactory = hl7TcpClientFactory; + this.publisher = publisher; } @@ -196,7 +209,6 @@ private String applyHl7Template(long samplingRate, String locationId, Instant ob parameters.put("obsDatetime", obsDatetime); parameters.put("messageDatetime", messageDatetimeStr); parameters.put("messageId", messageId); - StringSubstitutor stringSubstitutor = new StringSubstitutor(parameters); StringBuilder obrMsg = new StringBuilder(stringSubstitutor.replace(templateStr)); for (int obxI = 0; obxI < valuesByStreamId.size(); obxI++) { @@ -283,6 +295,8 @@ private List makeSyntheticWaveformMsgs(final String locationId, "UCHT03ICUBED31", "UCHT03ICUBED32", "UCHT03ICUBED33", "UCHT03ICUBED34", "UCHT03ICUBED35", "UCHT03ICUBED36" ); + private PatientLocationModel patientLocationModel = null; + /** * Generate synthetic waveform data for numPatients patients to cover a period of * numMillis milliseconds. @@ -299,8 +313,16 @@ public List makeSyntheticWaveformMsgsAllPatients( new SyntheticStream("52912", 50, 0.3, 5), // airway volume new SyntheticStream("27", 300, 1.2, 10) // ECG ); + List locationChangeMessages = patientLocationModel.makeModifications(startTime); + submitBatch(locationChangeMessages); + + List empties = new ArrayList<>(); for (int p = 0; p < numPatients; p++) { - var location = possibleLocations.get(p); + String location = possibleLocations.get(p); + if (patientLocationModel.getPatientForLocation(location) == null) { + empties.add(location); + continue; + } int sizeBefore = waveformMsgs.size(); // each bed has a slightly different frequency double frequencyFactor = 0.95 + 0.1 * p / possibleLocations.size(); @@ -317,12 +339,34 @@ public List makeSyntheticWaveformMsgsAllPatients( stream.baselineSignalFrequency * frequencyFactor, numMillis, startTime, stream.maxSamplesPerMessage)); } int sizeAfter = waveformMsgs.size(); - logger.debug("Patient {} (location {}), generated {} messages", p, location, sizeAfter - sizeBefore); + logger.debug("Patient {} (location {}), generated {} messages (incl ADT)", p, location, sizeAfter - sizeBefore); } + logger.info("Not generating data for empty locations: {}", empties); return waveformMsgs; } + private void submitBatch(List adtMsgs) { + List> batch = new ArrayList<>(); + int i = 0; + for (var adt: adtMsgs) { + i++; + batch.add(new ImmutablePair<>(adt, String.format("%s_message_%d", + Instant.now().toEpochMilli(), i))); + } + if (batch.isEmpty()) { + return; + } + try { + String batchId = batch.get(0).getRight(); + publisher.submit(batch, batchId, () -> { + logger.info("Successfully submitted batch {} (size {})", batchId, batch.size()); + }); + } catch (InterruptedException e) { + logger.error("submit interrupted", e); + } + } + record SyntheticStream(String streamId, int samplingRate, double baselineSignalFrequency, int maxSamplesPerMessage) { } diff --git a/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/patient_model/PatientDetails.java b/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/patient_model/PatientDetails.java new file mode 100644 index 000000000..b755e4628 --- /dev/null +++ b/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/patient_model/PatientDetails.java @@ -0,0 +1,147 @@ +package uk.ac.ucl.rits.inform.datasources.waveform_generator.patient_model; + +import lombok.Getter; +import lombok.Setter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import uk.ac.ucl.rits.inform.datasources.waveform.LocationMapping; +import uk.ac.ucl.rits.inform.interchange.InterchangeValue; +import uk.ac.ucl.rits.inform.interchange.adt.AdmitPatient; +import uk.ac.ucl.rits.inform.interchange.adt.AdtMessage; +import uk.ac.ucl.rits.inform.interchange.adt.DischargePatient; +import uk.ac.ucl.rits.inform.interchange.adt.TransferPatient; + +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.Random; + +public class PatientDetails { + private final Logger logger = LoggerFactory.getLogger(getClass()); + // try to get the same numbers each time + private final Random random = new Random(123); + + // some values are just opaque strings because we don't need to do any processing on them + @Getter + private final LocalDate dob; + @Getter + private final String mrn; + @Getter + private final String csn; + // need to track admit time as non-admit HL7 ADT messages require it + @Getter + private final Instant admitDatetime; + + // datetime for latest event (transfer, discharge, etc) + @Getter @Setter + private Instant eventDatetime = null; + + private final LocationMapping locationMapping = new LocationMapping(); + + /** + * Current location if being discharged, new location for admit and transfer. + */ + @Getter @Setter + private String location = null; + + /** + * @return location as it would be represented in our HL7 ADT feed. + */ + public String getAdtLocation() { + if (location == null) { + // just use something, it doesn't really matter + return "UNKNOWN_ADT_LOCATION"; + } + return locationMapping.hl7AdtLocationFromCapsuleLocation(location); + } + + /** + * Create new synthetic patient. + * @param admitDatetime admit time to use for patient + */ + public PatientDetails(Instant admitDatetime) { + this.admitDatetime = admitDatetime; + this.dob = LocalDate.parse("1980-01-01"); + this.mrn = makeFakeMrn(); + this.csn = makeFakeCsn(); + } + + private String makeFakeMrn() { + // make fake data look obviously fake + return String.format("FAKE%07d", random.nextInt(10_000_000)); + } + + private String makeFakeCsn() { + // make fake data look obviously fake + return String.format("FAKE%010d", random.nextLong(10_000_000_000L)); + } + + /** + * Consumer should call to mark last event as having been processed. + */ + public void clearLastEvent() { + this.eventDatetime = null; + this.location = null; + } + + private String hl7Datetime(Instant instant) { + LocalDateTime localDateTime = instant.atZone(ZoneId.systemDefault()).toLocalDateTime(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss"); + return localDateTime.format(formatter); + } + + private void setGenericAdtFields(AdtMessage adtMessage) { + adtMessage.setSourceMessageId(String.format( + "ADT_%s_%s", admitDatetime.toEpochMilli(), Instant.now().toEpochMilli())); + adtMessage.setSourceSystem("synthetic_waveform_generator_ADT"); + adtMessage.setEventOccurredDateTime(getEventDatetime()); + adtMessage.setMrn(getMrn()); + adtMessage.setPatientBirthDate(new InterchangeValue<>(getDob())); + adtMessage.setVisitNumber(getCsn()); + adtMessage.setRecordedDateTime(getEventDatetime()); + adtMessage.setNhsNumber("111111111"); + + logger.info("Generic ADT Fields: {}", adtMessage); + } + + /** + * Interpret this set of details as if a transfer had just happened. + * @return a transfer interchange message representing the transfer + */ + public TransferPatient makeTransferMessage() { + TransferPatient tr = new TransferPatient(); + setGenericAdtFields(tr); + tr.setAdmissionDateTime(new InterchangeValue<>(getAdmitDatetime())); + tr.setFullLocationString(new InterchangeValue<>(getAdtLocation())); + return tr; + } + + /** + * Interpret this set of patient details as if a discharge had just happened. + * @return a discharge interchange message representing the discharge + */ + public DischargePatient makeDischargeMessage() { + DischargePatient disch = new DischargePatient(); + setGenericAdtFields(disch); + + disch.setAdmissionDateTime(new InterchangeValue<>(getAdmitDatetime())); + disch.setDischargeLocation(getLocation()); + disch.setDischargeDateTime(getEventDatetime()); + return disch; + } + + /** + * Interpret this set of patient details as if an admit had just happened. + * @return an admit interchange message representing the admission + */ + public AdmitPatient makeAdmitMessage() { + AdmitPatient admitPatient = new AdmitPatient(); + setGenericAdtFields(admitPatient); + admitPatient.setFullLocationString(new InterchangeValue<>(getAdtLocation())); + admitPatient.setAdmissionDateTime(new InterchangeValue<>(getAdmitDatetime())); + + return admitPatient; + } +} diff --git a/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/patient_model/PatientLocationChangeSet.java b/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/patient_model/PatientLocationChangeSet.java new file mode 100644 index 000000000..6e3ba6861 --- /dev/null +++ b/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/patient_model/PatientLocationChangeSet.java @@ -0,0 +1,9 @@ +package uk.ac.ucl.rits.inform.datasources.waveform_generator.patient_model; + +import java.util.List; + +public record PatientLocationChangeSet( + List admitList, + List dischargeList, + List transferList) { +} diff --git a/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/patient_model/PatientLocationModel.java b/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/patient_model/PatientLocationModel.java new file mode 100644 index 000000000..2ea0d9eb2 --- /dev/null +++ b/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/patient_model/PatientLocationModel.java @@ -0,0 +1,155 @@ +package uk.ac.ucl.rits.inform.datasources.waveform_generator.patient_model; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import uk.ac.ucl.rits.inform.datasources.waveform.LocationMapping; +import uk.ac.ucl.rits.inform.interchange.adt.AdtMessage; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Random; + +public class PatientLocationModel { + private final Logger logger = LoggerFactory.getLogger(PatientLocationModel.class.getName()); + private final List allPossibleLocations; + private final Random random = new Random(234); + private final LocationMapping locationMapping = new LocationMapping(); + + // patients who we have transferred out but we might want to transfer back in again + private final List patientsInOtherHospitalLocations = new ArrayList<>(); + + // initialise all locations to have a patient + private final Map locationToPatient = new HashMap<>(); + + /** + * Start a simulation of all patients. + * @param allPossibleLocations all locations patients can inhabit (Capsule format) + * @param nowTime Initial admit time for the first batch of patients. + */ + public PatientLocationModel(final List allPossibleLocations, Instant nowTime) { + this.allPossibleLocations = new ArrayList<>(allPossibleLocations); + allPossibleLocations.forEach(location -> locationToPatient.put(location, new PatientDetails(nowTime))); + } + + /** + * Randomly change stuff and return the changes such that the caller can generate the appropriate + * ADT messages. + * @param nowTime time at which ADT changes will be simulated to happen + * @return description of changes made + */ + public List makeModifications(Instant nowTime) { + List admitList = new ArrayList<>(); + List dischargeList = new ArrayList<>(); + List transferList = new ArrayList<>(); + + // probability of each patient being discharged/transferred at each time step + final float probOfDischarge = 0.01F; + final float probOfTransferOut = 0.01F; + final float probOfTransferWithin = 0.01F; + final float probOfTransferIn = 0.01F; + + // go through all locations and randomly move some patients out + for (int i = 0; i < allPossibleLocations.size(); i++) { + var thisLocation = allPossibleLocations.get(i); + var thisPatient = locationToPatient.get(thisLocation); + if (thisPatient == null) { + // no patient to move + continue; + } + if (random.nextFloat() < probOfDischarge) { + // discharge, never to be seen again + thisPatient.setLocation(thisLocation); + thisPatient.setEventDatetime(nowTime); + dischargeList.add(thisPatient); + locationToPatient.put(thisLocation, null); + logger.info("ADT: Discharge from location {} to external", thisLocation); + } else if (random.nextFloat() < probOfTransferOut) { + // discharge from ICU, with possibility of coming back + thisPatient.setLocation("SomewhereNotIcu"); + thisPatient.setEventDatetime(nowTime); + transferList.add(thisPatient); + locationToPatient.put(thisLocation, null); + logger.info("ADT: Discharge from location {} to other hospital location", thisLocation); + } + } + // Now that it's slightly emptier, randomly move patients around/in + // Move each patient to another ICU location with certain probability. + for (int i = 0; i < allPossibleLocations.size(); i++) { + var thisLocation = allPossibleLocations.get(i); + var thisPatient = locationToPatient.get(thisLocation); + if (thisPatient != null) { + if (random.nextFloat() < probOfTransferWithin) { + Optional randomEmptyLocation = findRandomEmptyLocation(); + // if all ICU locations are full then just don't move them + if (randomEmptyLocation.isPresent()) { + String newLocation = randomEmptyLocation.get(); + thisPatient.setEventDatetime(nowTime); + thisPatient.setLocation(newLocation); + transferList.add(thisPatient); + locationToPatient.put(newLocation, thisPatient); + locationToPatient.put(thisLocation, null); + logger.info("ADT: Move from location {} to location {}", thisLocation, newLocation); + } + } + } + } + + // add patients + for (int i = 0; i < allPossibleLocations.size(); i++) { + var thisLocation = allPossibleLocations.get(i); + var thisPatient = locationToPatient.get(thisLocation); + if (thisPatient == null) { + // maybe fill this slot with a new patient or one in another part of the hospital + if (random.nextFloat() < probOfTransferIn) { + if (!patientsInOtherHospitalLocations.isEmpty()) { + PatientDetails incomingPatient = patientsInOtherHospitalLocations.remove(0); + // they are now back under our control so we know where they are + incomingPatient.clearLastEvent(); + incomingPatient.setEventDatetime(nowTime); + incomingPatient.setLocation(thisLocation); + admitList.add(incomingPatient); + locationToPatient.put(thisLocation, incomingPatient); + logger.info("ADT: Move patient into location {} from other hospital location", thisLocation); + } + } else if (random.nextFloat() < probOfTransferIn) { + PatientDetails newPatient = new PatientDetails(nowTime); + newPatient.setEventDatetime(nowTime); + newPatient.setLocation(thisLocation); + locationToPatient.put(thisLocation, newPatient); + admitList.add(newPatient); + logger.info("ADT: Move brand new patient into location {}", thisLocation); + } + } + } + List mods = new ArrayList<>(); + admitList.forEach(adm -> mods.add(adm.makeAdmitMessage())); + transferList.forEach(tr -> mods.add(tr.makeTransferMessage())); + dischargeList.forEach(disch -> mods.add(disch.makeDischargeMessage())); + return mods; + } + + private Optional findRandomEmptyLocation() { + var allEmpties = locationToPatient.entrySet().stream() + .filter(entry -> entry.getValue() == null) + .toList(); + if (allEmpties.isEmpty()) { + return Optional.empty(); + } + int randomIndex = random.nextInt(allEmpties.size()); + return Optional.of(allEmpties.get(randomIndex).getKey()); + } + + /** + * Get patient details at the given location. + * @param location in Capsule format + * @return patient details + */ + public PatientDetails getPatientForLocation(String location) { + // Get current. Consider some sort of "get only if changed" option + return locationToPatient.get(location); + } +} diff --git a/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/patient_model/package-info.java b/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/patient_model/package-info.java new file mode 100644 index 000000000..9b895c0c1 --- /dev/null +++ b/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/patient_model/package-info.java @@ -0,0 +1,4 @@ +/** + * Underlying model used for generating ADT messages for synthetic Waveform patients. + */ +package uk.ac.ucl.rits.inform.datasources.waveform_generator.patient_model; diff --git a/waveform-generator/waveform-generator-config-envs.EXAMPLE b/waveform-generator/waveform-generator-config-envs.EXAMPLE index c75a80359..ec59a5b18 100644 --- a/waveform-generator/waveform-generator-config-envs.EXAMPLE +++ b/waveform-generator/waveform-generator-config-envs.EXAMPLE @@ -1,3 +1,7 @@ +SPRING_RABBITMQ_HOST= +SPRING_RABBITMQ_PORT= +SPRING_RABBITMQ_USERNAME= +SPRING_RABBITMQ_PASSWORD= WAVEFORM_SYNTHETIC_NUM_PATIENTS= WAVEFORM_SYNTHETIC_WARP_FACTOR= WAVEFORM_SYNTHETIC_START_DATETIME= From 0af4d2dc4a2922f180b5a2f2b932b6cc63a94e88 Mon Sep 17 00:00:00 2001 From: Jeremy Stein Date: Wed, 10 Dec 2025 17:56:51 +0000 Subject: [PATCH 2/9] Random seed was too fixed and duplicate data was being generated. --- .../waveform_generator/patient_model/PatientDetails.java | 7 +++++-- .../patient_model/PatientLocationModel.java | 8 +++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/patient_model/PatientDetails.java b/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/patient_model/PatientDetails.java index b755e4628..7ad62856a 100644 --- a/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/patient_model/PatientDetails.java +++ b/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/patient_model/PatientDetails.java @@ -21,7 +21,7 @@ public class PatientDetails { private final Logger logger = LoggerFactory.getLogger(getClass()); // try to get the same numbers each time - private final Random random = new Random(123); + private final Random random; // some values are just opaque strings because we don't need to do any processing on them @Getter @@ -59,10 +59,13 @@ public String getAdtLocation() { /** * Create new synthetic patient. + * * @param admitDatetime admit time to use for patient + * @param random random object to use */ - public PatientDetails(Instant admitDatetime) { + public PatientDetails(Instant admitDatetime, Random random) { this.admitDatetime = admitDatetime; + this.random = random; this.dob = LocalDate.parse("1980-01-01"); this.mrn = makeFakeMrn(); this.csn = makeFakeCsn(); diff --git a/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/patient_model/PatientLocationModel.java b/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/patient_model/PatientLocationModel.java index 2ea0d9eb2..efe5a01ee 100644 --- a/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/patient_model/PatientLocationModel.java +++ b/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/patient_model/PatientLocationModel.java @@ -16,7 +16,7 @@ public class PatientLocationModel { private final Logger logger = LoggerFactory.getLogger(PatientLocationModel.class.getName()); private final List allPossibleLocations; - private final Random random = new Random(234); + private final Random random; private final LocationMapping locationMapping = new LocationMapping(); // patients who we have transferred out but we might want to transfer back in again @@ -32,7 +32,9 @@ public class PatientLocationModel { */ public PatientLocationModel(final List allPossibleLocations, Instant nowTime) { this.allPossibleLocations = new ArrayList<>(allPossibleLocations); - allPossibleLocations.forEach(location -> locationToPatient.put(location, new PatientDetails(nowTime))); + this.random = new Random(nowTime.hashCode()); + allPossibleLocations.forEach(location -> locationToPatient.put(location, new PatientDetails(nowTime, random))); + // same time generates same data (ish) } /** @@ -116,7 +118,7 @@ public List makeModifications(Instant nowTime) { logger.info("ADT: Move patient into location {} from other hospital location", thisLocation); } } else if (random.nextFloat() < probOfTransferIn) { - PatientDetails newPatient = new PatientDetails(nowTime); + PatientDetails newPatient = new PatientDetails(nowTime, random); newPatient.setEventDatetime(nowTime); newPatient.setLocation(thisLocation); locationToPatient.put(thisLocation, newPatient); From bb1442460270fb0c4b19ffd69bca2822ef84bf9a Mon Sep 17 00:00:00 2001 From: Jeremy Stein Date: Wed, 10 Dec 2025 19:11:06 +0000 Subject: [PATCH 3/9] Delete redundant --- .../waveform_generator/patient_model/PatientDetails.java | 9 --------- .../patient_model/PatientLocationChangeSet.java | 9 --------- 2 files changed, 18 deletions(-) delete mode 100644 waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/patient_model/PatientLocationChangeSet.java diff --git a/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/patient_model/PatientDetails.java b/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/patient_model/PatientDetails.java index 7ad62856a..069ed0703 100644 --- a/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/patient_model/PatientDetails.java +++ b/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/patient_model/PatientDetails.java @@ -13,9 +13,6 @@ import java.time.Instant; import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; import java.util.Random; public class PatientDetails { @@ -89,12 +86,6 @@ public void clearLastEvent() { this.location = null; } - private String hl7Datetime(Instant instant) { - LocalDateTime localDateTime = instant.atZone(ZoneId.systemDefault()).toLocalDateTime(); - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss"); - return localDateTime.format(formatter); - } - private void setGenericAdtFields(AdtMessage adtMessage) { adtMessage.setSourceMessageId(String.format( "ADT_%s_%s", admitDatetime.toEpochMilli(), Instant.now().toEpochMilli())); diff --git a/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/patient_model/PatientLocationChangeSet.java b/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/patient_model/PatientLocationChangeSet.java deleted file mode 100644 index 6e3ba6861..000000000 --- a/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/patient_model/PatientLocationChangeSet.java +++ /dev/null @@ -1,9 +0,0 @@ -package uk.ac.ucl.rits.inform.datasources.waveform_generator.patient_model; - -import java.util.List; - -public record PatientLocationChangeSet( - List admitList, - List dischargeList, - List transferList) { -} From 93851b060a5f040ec840920f8debbbf1fe0fa11b Mon Sep 17 00:00:00 2001 From: Jeremy Stein Date: Mon, 15 Dec 2025 16:00:33 +0000 Subject: [PATCH 4/9] Forgot to create admit messages for the initial patients! --- .../waveform_generator/Hl7Generator.java | 13 ++++++++++++- .../patient_model/PatientLocationModel.java | 18 ++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/Hl7Generator.java b/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/Hl7Generator.java index 4d6b4f5bb..955fc3608 100644 --- a/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/Hl7Generator.java +++ b/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/Hl7Generator.java @@ -13,6 +13,7 @@ import uk.ac.ucl.rits.inform.datasources.waveform.LocationMapping; import uk.ac.ucl.rits.inform.datasources.waveform_generator.patient_model.PatientLocationModel; import uk.ac.ucl.rits.inform.interchange.EmapOperationMessage; +import uk.ac.ucl.rits.inform.interchange.adt.AdmitPatient; import uk.ac.ucl.rits.inform.interchange.adt.AdtMessage; import uk.ac.ucl.rits.inform.interchange.messaging.Publisher; @@ -61,6 +62,9 @@ public class Hl7Generator { * Where we are up to in generating data (observation time). */ private Instant progressDatetime; + + private boolean haveInitialised = false; + /** * @return Where we want to be up to in generating data (observation time). * This value is used to generate data at the correct rate. @@ -122,6 +126,13 @@ public Hl7Generator(Hl7TcpClientFactory hl7TcpClientFactory, Publisher publisher */ @Scheduled(fixedDelay = 1000) public void generateMessages() throws IOException { + if (!haveInitialised) { + haveInitialised = true; + List initialAdmits = patientLocationModel.getInitialLocations(); + logger.info("First scheduled run, perform initial admits: {} messages", initialAdmits.size()); + submitBatch(initialAdmits); + } + var start = Instant.now(); // The warp factor is the main mechanism used to control how much data to put in now. // Although the scheduling interval and chunk size/count will affect what warp factor is achievable. @@ -346,7 +357,7 @@ public List makeSyntheticWaveformMsgsAllPatients( return waveformMsgs; } - private void submitBatch(List adtMsgs) { + private void submitBatch(List adtMsgs) { List> batch = new ArrayList<>(); int i = 0; for (var adt: adtMsgs) { diff --git a/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/patient_model/PatientLocationModel.java b/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/patient_model/PatientLocationModel.java index efe5a01ee..337e71fd8 100644 --- a/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/patient_model/PatientLocationModel.java +++ b/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/patient_model/PatientLocationModel.java @@ -3,6 +3,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import uk.ac.ucl.rits.inform.datasources.waveform.LocationMapping; +import uk.ac.ucl.rits.inform.interchange.adt.AdmitPatient; import uk.ac.ucl.rits.inform.interchange.adt.AdtMessage; import java.time.Instant; @@ -37,6 +38,23 @@ public PatientLocationModel(final List allPossibleLocations, Instant now // same time generates same data (ish) } + /** + * Make initial set of admit messages, as if all the patients in locationToPatient have just been admitted. + * @return one admit message per existing patient + */ + public List getInitialLocations() { + List admitMsgs = new ArrayList<>(); + locationToPatient.entrySet().stream().forEach(entry -> { + PatientDetails initialPatient = entry.getValue(); + String location = entry.getKey(); + initialPatient.setEventDatetime(initialPatient.getAdmitDatetime()); + logger.info("Initial stats: {}, {}, {}", location, initialPatient.getAdmitDatetime(), initialPatient.getEventDatetime()); + initialPatient.setLocation(location); + admitMsgs.add(initialPatient.makeAdmitMessage()); + }); + return admitMsgs; + } + /** * Randomly change stuff and return the changes such that the caller can generate the appropriate * ADT messages. From 884cfa67467b210a1da402c0d591aed809bd22e5 Mon Sep 17 00:00:00 2001 From: Jeremy Stein Date: Mon, 15 Dec 2025 16:00:52 +0000 Subject: [PATCH 5/9] Was generating invalid beds. There is no bed 32, but there is a 22. --- .../inform/datasources/waveform_generator/Hl7Generator.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/Hl7Generator.java b/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/Hl7Generator.java index 955fc3608..729c840d4 100644 --- a/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/Hl7Generator.java +++ b/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/Hl7Generator.java @@ -301,9 +301,9 @@ private List makeSyntheticWaveformMsgs(final String locationId, "UCHT03ICURM01", "UCHT03ICURM02", "UCHT03ICURM03", "UCHT03ICURM04", "UCHT03ICURM05", "UCHT03ICURM06", "UCHT03ICURM07", "UCHT03ICURM08", "UCHT03ICURM09", "UCHT03ICURM10", "UCHT03ICURM32", "UCHT03ICUBED11", "UCHT03ICUBED12", "UCHT03ICUBED14", "UCHT03ICUBED15", "UCHT03ICUBED16", "UCHT03ICUBED17", - "UCHT03ICUBED18", "UCHT03ICUBED19", "UCHT03ICUBED20", "UCHT03ICUBED21", "UCHT03ICUBED23", "UCHT03ICUBED24", - "UCHT03ICUBED25", "UCHT03ICUBED26", "UCHT03ICUBED27", "UCHT03ICUBED28", "UCHT03ICUBED29", "UCHT03ICUBED30", - "UCHT03ICUBED31", "UCHT03ICUBED32", "UCHT03ICUBED33", "UCHT03ICUBED34", "UCHT03ICUBED35", "UCHT03ICUBED36" + "UCHT03ICUBED18", "UCHT03ICUBED19", "UCHT03ICUBED20", "UCHT03ICUBED21", "UCHT03ICUBED22", "UCHT03ICUBED23", + "UCHT03ICUBED24", "UCHT03ICUBED25", "UCHT03ICUBED26", "UCHT03ICUBED27", "UCHT03ICUBED28", "UCHT03ICUBED29", + "UCHT03ICUBED30", "UCHT03ICUBED31", "UCHT03ICUBED33", "UCHT03ICUBED34", "UCHT03ICUBED35", "UCHT03ICUBED36" ); private PatientLocationModel patientLocationModel = null; From 7aebe663a784609df116f753483dee798f70f80b Mon Sep 17 00:00:00 2001 From: Jeremy Stein Date: Mon, 15 Dec 2025 16:21:22 +0000 Subject: [PATCH 6/9] Generate fake NHS numbers --- .../patient_model/PatientDetails.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/patient_model/PatientDetails.java b/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/patient_model/PatientDetails.java index 069ed0703..24cdb141d 100644 --- a/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/patient_model/PatientDetails.java +++ b/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/patient_model/PatientDetails.java @@ -27,6 +27,8 @@ public class PatientDetails { private final String mrn; @Getter private final String csn; + @Getter + private final String nhsNumber; // need to track admit time as non-admit HL7 ADT messages require it @Getter private final Instant admitDatetime; @@ -66,6 +68,12 @@ public PatientDetails(Instant admitDatetime, Random random) { this.dob = LocalDate.parse("1980-01-01"); this.mrn = makeFakeMrn(); this.csn = makeFakeCsn(); + this.nhsNumber = makeFakeNhsNumber(); + } + + private String makeFakeNhsNumber() { + // Synthetic ones start with 999. Don't bother getting the checksum right + return String.format("FAKE999%07d", random.nextInt(10_000_000)); } private String makeFakeMrn() { @@ -95,7 +103,7 @@ private void setGenericAdtFields(AdtMessage adtMessage) { adtMessage.setPatientBirthDate(new InterchangeValue<>(getDob())); adtMessage.setVisitNumber(getCsn()); adtMessage.setRecordedDateTime(getEventDatetime()); - adtMessage.setNhsNumber("111111111"); + adtMessage.setNhsNumber(getNhsNumber()); logger.info("Generic ADT Fields: {}", adtMessage); } From 7686ccbed8490e40077de5c673547565e6fa807d Mon Sep 17 00:00:00 2001 From: Jeremy Stein Date: Thu, 18 Dec 2025 12:37:02 +0000 Subject: [PATCH 7/9] Allow special values to be passed through as-is, rather than failing mapping. --- .../patient_model/PatientDetails.java | 21 +++++++++++++++++-- .../patient_model/PatientLocationModel.java | 2 +- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/patient_model/PatientDetails.java b/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/patient_model/PatientDetails.java index 24cdb141d..f6117a613 100644 --- a/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/patient_model/PatientDetails.java +++ b/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/patient_model/PatientDetails.java @@ -17,6 +17,11 @@ public class PatientDetails { private final Logger logger = LoggerFactory.getLogger(getClass()); + + // some special values that we allow to be unmappable without it being an error + static final String UNKNOWN_ADT_LOCATION = "UNKNOWN_ADT_LOCATION"; + static final String SOMEWHERE_NOT_ICU = "SOMEWHERE_NOT_ICU"; + // try to get the same numbers each time private final Random random; @@ -50,10 +55,22 @@ public class PatientDetails { */ public String getAdtLocation() { if (location == null) { + logger.error("Null location for patient with CSN {}", csn); // just use something, it doesn't really matter - return "UNKNOWN_ADT_LOCATION"; + return UNKNOWN_ADT_LOCATION; + } + String hl7AdtLocation = locationMapping.hl7AdtLocationFromCapsuleLocation(location); + if (hl7AdtLocation != null) { + // successful map + return hl7AdtLocation; + } else { + // pass it through unmapped in certain special cases, not an error + if (location.equals(SOMEWHERE_NOT_ICU)) { + return location; + } } - return locationMapping.hl7AdtLocationFromCapsuleLocation(location); + logger.error("Unmappable location {} for CSN {}", location, csn); + return null; } /** diff --git a/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/patient_model/PatientLocationModel.java b/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/patient_model/PatientLocationModel.java index 337e71fd8..d498b2101 100644 --- a/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/patient_model/PatientLocationModel.java +++ b/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/patient_model/PatientLocationModel.java @@ -89,7 +89,7 @@ public List makeModifications(Instant nowTime) { logger.info("ADT: Discharge from location {} to external", thisLocation); } else if (random.nextFloat() < probOfTransferOut) { // discharge from ICU, with possibility of coming back - thisPatient.setLocation("SomewhereNotIcu"); + thisPatient.setLocation(PatientDetails.SOMEWHERE_NOT_ICU); thisPatient.setEventDatetime(nowTime); transferList.add(thisPatient); locationToPatient.put(thisLocation, null); From 7d0fde8e71664e38cc60725119e0966a7dc9f17c Mon Sep 17 00:00:00 2001 From: Jeremy Stein Date: Thu, 18 Dec 2025 16:06:48 +0000 Subject: [PATCH 8/9] We were not setting the previous and discharge location correctly for a discharge --- .../waveform_generator/patient_model/PatientDetails.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/patient_model/PatientDetails.java b/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/patient_model/PatientDetails.java index f6117a613..0f8723320 100644 --- a/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/patient_model/PatientDetails.java +++ b/waveform-generator/src/main/java/uk/ac/ucl/rits/inform/datasources/waveform_generator/patient_model/PatientDetails.java @@ -139,6 +139,7 @@ public TransferPatient makeTransferMessage() { /** * Interpret this set of patient details as if a discharge had just happened. + * By discharge, here we mean a discharge outside the hospital, not to return. * @return a discharge interchange message representing the discharge */ public DischargePatient makeDischargeMessage() { @@ -146,7 +147,11 @@ public DischargePatient makeDischargeMessage() { setGenericAdtFields(disch); disch.setAdmissionDateTime(new InterchangeValue<>(getAdmitDatetime())); - disch.setDischargeLocation(getLocation()); + logger.info("Discharge location for {}: {} ({})", getCsn(), getAdtLocation(), getLocation()); + disch.setPreviousLocationString(new InterchangeValue<>(getAdtLocation())); + disch.setFullLocationString(new InterchangeValue<>(getAdtLocation())); + // they're not coming back, so location doesn't matter + disch.setDischargeLocation("home"); disch.setDischargeDateTime(getEventDatetime()); return disch; } From bfabafe2e1e0139dfe7a6e50d3e8462e7b4decc8 Mon Sep 17 00:00:00 2001 From: Jeremy Stein Date: Thu, 18 Dec 2025 16:14:26 +0000 Subject: [PATCH 9/9] Don't discard waveform messages just because their assignment to a person is ambiguous. Better to keep the data and allow it to be resolved later. --- .../emapstar/controllers/WaveformController.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/uk/ac/ucl/rits/inform/datasinks/emapstar/controllers/WaveformController.java b/core/src/main/java/uk/ac/ucl/rits/inform/datasinks/emapstar/controllers/WaveformController.java index 5ce48e6ff..ffc9e1a4c 100644 --- a/core/src/main/java/uk/ac/ucl/rits/inform/datasinks/emapstar/controllers/WaveformController.java +++ b/core/src/main/java/uk/ac/ucl/rits/inform/datasinks/emapstar/controllers/WaveformController.java @@ -2,6 +2,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import uk.ac.ucl.rits.inform.datasinks.emapstar.exceptions.MessageIgnoredException; @@ -57,8 +58,15 @@ public void processWaveform( List numericValues = interchangeValue.get(); Instant observationTime = msg.getObservationTime(); // Try to find the visit. We don't have enough information to create the visit if it doesn't already exist. - Optional inferredLocationVisit = - locationVisitRepository.findLocationVisitByLocationAndTime(observationTime, msg.getMappedLocationString()); + Optional inferredLocationVisit; + try { + inferredLocationVisit = locationVisitRepository.findLocationVisitByLocationAndTime( + observationTime, msg.getMappedLocationString()); + } catch (IncorrectResultSizeDataAccessException e) { + logger.error("Multiple location visits found for {} at {}; waveform stored without location visit link. Message: {}", + msg.getMappedLocationString(), observationTime, e.getMessage()); + inferredLocationVisit = Optional.empty(); + } // XXX: will have to do some sanity checks here to be sure that the HL7 feed hasn't gone down. // See issue #36, and here for discussion: // https://github.com/SAFEHR-data/emap/blob/develop/docs/dev/features/waveform_hf_data.md#core-processor-logic-orphan-data-problem