Files
adas-core/adas-core.Application/Customizations/HPAZ/CalculatedObservations.cs
T
2026-06-26 10:29:23 +02:00

756 lines
35 KiB
C#
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using adas_core.Application.Services.Interfaces;
using adas_core.Domain.Enums;
using adas_core.Domain.Models;
using adas_core.Domain.Models.AppSettings;
using adas_core.Domain.Models.Pumps;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using Serilog;
namespace adas_core.Application.Customizations.HPAZ;
/// <summary>
/// Represents a service that provides calculated observations, receiving its dependencies through the supplied <see cref="IServiceProvider"/>.
/// </summary>
/// <remarks>
/// This class implements the <see cref="ICalculatedObservations"/> contract, exposing the behavior defined by that interface while relying on constructor-injected services for its operations.
/// </remarks>
public class CalculatedObservations(IServiceProvider serviceProvider) : ICalculatedObservations
{
private readonly Lazy<IAlarmService> _alarmService = serviceProvider.GetRequiredService<Lazy<IAlarmService>>();
private readonly ApiSettings
_apiSettings = serviceProvider.GetRequiredService<IOptions<ApiSettings>>().Value; //apiSettings;
private readonly IConfigObservationService _configObservationService =
serviceProvider.GetRequiredService<IConfigObservationService>(); //configObservationService;
private readonly ILogger<CalculatedObservations> _logger =
serviceProvider.GetRequiredService<ILogger<CalculatedObservations>>();
//Use GroupedObservation to avoid circular dependency with _observationService
private readonly Lazy<IObservationService> _observationService =
serviceProvider.GetRequiredService<Lazy<IObservationService>>(); //observationService;
private readonly List<string> _pressBloodArteryMean = ["TAm"];
/// <summary>
/// Asynchronously calculates the active bolus for the specified patient.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose active bolus is to be calculated.</param>
public Task CalculateActiveBolus(ObjectId patientId)
{
return Task.CompletedTask;
}
/// <summary>
/// Calculates medicine observations for the specified patient based on their active medicines.
/// </summary>
/// <param name="activeMedicines">The list of medicines currently active for the patient.</param>
/// <param name="patientId">The unique identifier of the patient whose medicine observations are being calculated.</param>
public Task CalculateMedicineObservation(List<Medicine> activeMedicines, ObjectId patientId)
{
return Task.CompletedTask;
}
/// <summary>
/// Retrieves the active treatments associated with the specified patient identifier.
/// Returns an empty collection when no active treatments are found.
/// </summary>
/// <param name="id">The unique identifier of the patient whose active treatments are being retrieved.</param>
/// <returns>A task that represents the asynchronous operation, containing a collection of active <see cref="PatientTreatment"/> records for the patient, or an empty collection if none exist.</returns>
public Task<IEnumerable<PatientTreatment?>> GetActiveTreatmentsByPatient(ObjectId id)
{
return Task.FromResult<IEnumerable<PatientTreatment?>>(new List<PatientTreatment?>());
}
/// <summary>
/// Maps the obs to an event or alarm depending on its type and inserts it if needed
/// </summary>
/// <typeparam name="T">Type</typeparam>
/// <param name="source">Observation</param>
/// <param name="onlyByName">True if the observation needs to generate an alert</param>
/// <returns>Mapped observation</returns>
public async Task<T?> Map<T>(T source, bool onlyByName) where T : BasePatientObservation
{
_logger.LogDebug("Mapping {obsName} mode observation {obs}", source.Name, source);
if (source is not BasePatientObservationValue obs) return source;
var name = obs is PatientObservationAlarm obsAlarm ? obsAlarm.Event : obs.Name;
name ??= string.Empty;
//No generamos alertas si viene onlyByName
if (name == "Resp_Mode")
obs = CalculateVentilationMode(obs);
if (name is "Sattc" or "FiO2") await CalculateSf(obs, name);
if (name is "FiO2" or "P_VAM" or "PaO2_Tidal") await CalculateOxygenationIndex(obs, name);
if (name is "FiO2" or "PaO2_Tidal") await CalculatePf(obs, name);
if (name == "TEST_ALARMA") await _alarmService.Value.CalculateAlarmTest(obs, name);
obs = await CheckObsWithSameTimeExistsAndIncrementTime(obs);
if (obs is PatientObservation pobs)
{
pobs = await CheckAlarmConfig(pobs);
return pobs as T;
}
return obs as T;
}
/// <summary>
/// Maps the specified pump observation to a new <see cref="PumpObservation"/> instance asynchronously.
/// </summary>
/// <param name="pumpObservation">The source <see cref="PumpObservation"/> to map.</param>
/// <returns>A task that represents the asynchronous mapping operation. The task result contains the mapped <see cref="PumpObservation"/>.</returns>
public async Task<PumpObservation> Map(PumpObservation pumpObservation)
{
try
{
if (pumpObservation.Status is PumpEnum.Status.Alarm or PumpEnum.Status.Warning)
{
PatientObservation? patientObservation = null;
string? name = null;
//La oclusión es para todos los tipos
var volumetricAirOcclusionAlarm = _apiSettings.VolumetricAirOcclusionAlarm ?? null;
if (volumetricAirOcclusionAlarm != null && pumpObservation.AlarmType != null &&
volumetricAirOcclusionAlarm.Contains(pumpObservation.AlarmType.ToString() ?? string.Empty))
{
name = "VolumetricAirOcclusion";
//alerta oclusión aire para bombas volumétricas
patientObservation = await CreatePumpAlarmObservation(name, pumpObservation);
}
var listInotropicMedicines = _apiSettings.InotropicMedicines ?? null;
if (listInotropicMedicines is { Count: > 0 } && pumpObservation.DrugName != null &&
listInotropicMedicines.Contains(pumpObservation.DrugName)) //INOTRÓPICOS
{
var inotropicEndInfusionAlarm = _apiSettings.InotropicEndInfusionAlarm ?? null;
if (inotropicEndInfusionAlarm != null && pumpObservation.AlarmType != null &&
inotropicEndInfusionAlarm.Contains(pumpObservation.AlarmType.ToString() ?? string.Empty))
{
//Inotrópicos próxima a fin de infusión
// Y
//proximo fin de infusión, oclusión, y presencia aire….
//Solo inotrópicos
//Está en la línea 19 del excel y abarca la que hay en la línea 9
// TODO: revisar en los logs que nombre de type (Buscar por crud xmlns)
name = "InotropicEndInfusion";
patientObservation = await CreatePumpAlarmObservation(name, pumpObservation);
}
}
else
{
var pumPressureAlarm = _apiSettings.PumPressureAlarm ?? null;
if (pumPressureAlarm != null && pumpObservation.AlarmType != null &&
pumPressureAlarm.Contains(pumpObservation.AlarmType.ToString() ?? string.Empty))
{
name = "Pressure";
//alerta de presión(la alerta de la bomba se programa + -30mmHg por encima de la presión de la línea).
patientObservation = await CreatePumpAlarmObservation(name, pumpObservation);
}
var endContinuousInfusionAlarm = _apiSettings.EndContinuousInfusionAlarm ?? null;
if (endContinuousInfusionAlarm != null && pumpObservation.AlarmType != null &&
endContinuousInfusionAlarm.Contains(pumpObservation.AlarmType.ToString() ?? string.Empty))
{
name = "EndContinuousInfusion";
//próximo a fin de infusión /fin de infusión
//información de bombas solo perfusión continua
patientObservation = await CreatePumpAlarmObservation(name, pumpObservation);
}
var volumetricAirInLineAlarm = _apiSettings.VolumetricAirInLineAlarm ?? null;
if (volumetricAirInLineAlarm != null && pumpObservation.AlarmType != null &&
volumetricAirInLineAlarm.Contains(pumpObservation.AlarmType.ToString() ?? string.Empty))
{
name = "VolumetricAirInLine";
//alerta aire para bombas volumétricas (burbujas de aire para la bomba)
patientObservation = await CreatePumpAlarmObservation(name, pumpObservation);
}
}
if (patientObservation != null && name != null)
{
_ = _observationService.Value.InsertObservation(patientObservation);
_ = _alarmService.Value.SendAlarm(patientObservation, name, AlarmEnum.Name.Pump,
AlarmEnum.Severity.None, AlarmEnum.Type.Auto);
}
}
}
catch (Exception ex)
{
_logger.LogError("Error mapping alarm pump observation. Exception: {exMessage}", ex.Message);
}
return pumpObservation;
}
/// <summary>
/// Maps a <see cref="PatientTreatment"/> instance, performing the required transformation logic.
/// </summary>
/// <param name="treatment">The <see cref="PatientTreatment"/> to be mapped.</param>
/// <returns>A task representing the asynchronous operation, containing the mapped <see cref="PatientTreatment"/>.</returns>
/// <exception cref="NotImplementedException">Thrown in all cases because the method has not yet been implemented.</exception>
public Task<PatientTreatment> Map(PatientTreatment treatment)
{
throw new NotImplementedException();
}
/// <summary>
/// Maps a <see cref="PatientDiagnosis"/> instance. This method is a placeholder and has not been implemented yet.
/// </summary>
/// <param name="diagnosis">The <see cref="PatientDiagnosis"/> to be mapped.</param>
/// <returns>A task that represents the asynchronous operation, containing the mapped <see cref="PatientDiagnosis"/>.</returns>
/// <exception cref="NotImplementedException">Thrown to indicate that the mapping logic has not been implemented.</exception>
public Task<PatientDiagnosis> Map(PatientDiagnosis diagnosis)
{
throw new NotImplementedException();
}
/// <summary>
/// Ensures chronological order of patient observations by adjusting a new observation's time to one second after the most recent observation with the same name, when its time is earlier than or equal to the previous one (compared at second precision). Returns the observation unchanged and logs an error if the observation name is null or empty.
/// </summary>
/// <param name="newObservation">The new patient observation to validate and potentially adjust for time consistency.</param>
/// <returns>The patient observation with its time corrected if a time inconsistency was detected, otherwise the original observation.</returns>
public async Task<PatientObservation?> FixTimeInconsistencyWithLast(PatientObservation newObservation)
{
if (string.IsNullOrEmpty(newObservation.Name))
{
_logger.LogError(
"Error FixTimeInconsistencyWithLast new observation name is null or empty: {newObservation}.",
newObservation);
return newObservation;
}
var lasObservations = await _observationService.Value.FindLastObservations(newObservation.PatientId, 1,
[newObservation.Name]);
var lastObservation = lasObservations.FirstOrDefault();
if (lastObservation != null && DateTime.Compare(
new DateTime(lastObservation.Time.Year, lastObservation.Time.Month,
lastObservation.Time.Day, lastObservation.Time.Hour,
lastObservation.Time.Minute, lastObservation.Time.Second),
new DateTime(newObservation.Time.Year, newObservation.Time.Month,
newObservation.Time.Day, newObservation.Time.Hour,
newObservation.Time.Minute, newObservation.Time.Second)
) >= 0)
newObservation.Time = lastObservation.Time.AddSeconds(1);
return newObservation;
}
/// <summary>
/// Pre-maps a list of patient observations by creating deep copies, applying configuration-based mapping,
/// and triggering blue code calculation. Returns the original list of patient observations unchanged.
/// </summary>
/// <param name="listToMap">The list of patient observations to process for mapping and blue code calculation.</param>
/// <returns>The original list of patient observations passed to the method.</returns>
public Task<List<PatientObservation>> PreMapList(List<PatientObservation> listToMap)
{
var mappedObsList = listToMap
.Select(obs => obs.DeepCopy())
.Select(obs => _configObservationService.Map(obs).Result)
.Where(obs => obs != null)
.ToList();
// mappedObsList contains observation mapped
// Check blue code
_ = CalculateBlueCodeList(mappedObsList);
return Task.FromResult(listToMap);
}
/// <summary>
/// Maps fields from a <see cref="PatientObservationAlarm"/> onto an existing <see cref="PatientObservation"/>, copying the alarm's EventId to Code and Event to Name when those values are provided, and always copying the alarm's Value.
/// </summary>
/// <param name="obs">The target <see cref="PatientObservation"/> instance that will be updated with values from the alarm.</param>
/// <param name="alarmToInsert">The source <see cref="PatientObservationAlarm"/> whose values are applied to <paramref name="obs"/>.</param>
/// <returns>A completed <see cref="Task{PatientObservation}"/> containing the updated <paramref name="obs"/>.</returns>
public Task<PatientObservation> MapSourceAlarm(PatientObservation obs, PatientObservationAlarm alarmToInsert)
{
if (!string.IsNullOrEmpty(alarmToInsert.EventId))
obs.Code = alarmToInsert.EventId;
if (!string.IsNullOrEmpty(alarmToInsert.Event))
obs.Name = alarmToInsert.Event;
obs.Value = alarmToInsert.Value;
return Task.FromResult(obs);
}
/// <summary>
/// Sends an alarm associated with the specified patient observation, using the provided alarm name and optional code.
/// </summary>
/// <param name="obs">The patient observation that triggers or relates to the alarm.</param>
/// <param name="name">The name of the alarm to be sent.</param>
/// <param name="code">The optional alarm code identifying the type of alarm.</param>
/// <exception cref="System.NotImplementedException">Thrown because the method is not yet implemented.</exception>
Task ICalculatedObservations.SendAlarm(PatientObservation obs, string name, AlarmEnum.Name? code)
{
throw new NotImplementedException();
}
/// <summary>
/// Asynchronously sends an alarm for the specified patient observation through the alarm service, always using a None severity.
/// </summary>
/// <param name="obs">The patient observation associated with the alarm.</param>
/// <param name="name">The name of the alarm.</param>
/// <param name="code">The optional alarm code identifier.</param>
/// <param name="type">The type of alarm to send.</param>
private async Task SendAlarm(PatientObservation obs, string name, AlarmEnum.Name? code, AlarmEnum.Type type)
{
await _alarmService.Value.SendAlarm(obs, name, code, AlarmEnum.Severity.None, type);
}
/// <summary>
/// Initial check to ignore old observations generating blue codes
/// </summary>
/// <param name="obs">Observation</param>
/// <returns>True if it needs to be ignored</returns>
private bool CheckIfObservationIsOlderAndShouldBeIgnored(PatientObservation obs)
{
if (obs.Time.ToUniversalTime().AddMinutes(_apiSettings.IgnoreCalcObservationsOlderInMinutesThan) <
DateTime.UtcNow)
{
Log.Debug(
"ignoring calc old observation: name:{obsName} obs_time:{obsTime} system_time {DateTimeNow} id:{obsId}",
obs.Name ?? "null", obs.Time, DateTime.Now, obs.Id);
return true;
}
return false;
}
/// <summary>
/// Retrieves the alarm configuration for the given patient observation by looking up a matching configuration entry based on the observation name and patient ID, and applies it to the observation. If no matching configuration is found, the alarm is set to <c>null</c>.
/// </summary>
/// <param name="pobs">The patient observation whose alarm configuration should be resolved; its <c>Name</c> and <c>PatientId</c> are used to locate the configuration.</param>
/// <returns>The same <see cref="PatientObservation"/> instance with its <c>Alarm</c> property populated from the matching configuration, or <c>null</c> when no configuration is found.</returns>
private async Task<PatientObservation> CheckAlarmConfig(PatientObservation pobs)
{
var configObs = await _configObservationService.Get(new PatientObservation
{
Name = pobs.Name,
PatientId = pobs.PatientId
});
pobs.Alarm = configObs?.Alarm ?? null;
return pobs;
}
/// <summary>
/// Creates a new <see cref="PatientObservation"/> representing an alarm, using the ADAS_ALARM coding system and prefixing the name with "Alarm_". The resulting observation inherits the value, patient identifier, timestamp, and alarm status from the provided source observation.
/// </summary>
/// <param name="name">The alarm code used to identify the type of alarm; it is set as the <c>Code</c> and used to compose the <c>Name</c>.</param>
/// <param name="pobs">The source patient observation whose value, patient identifier, time, and alarm flag are copied into the new alarm observation.</param>
/// <returns>A new <see cref="PatientObservation"/> configured as an ADAS alarm observation based on the provided source.</returns>
private static PatientObservation CreateAlarmObservation(string name, PatientObservation pobs)
{
return new PatientObservation
{
CodingSystem = "ADAS_ALARM",
Code = name,
Name = $"Alarm_{name}",
Value = pobs.Value,
PatientId = pobs.PatientId,
Time = pobs.Time,
Alarm = pobs.Alarm
};
}
/// <summary>
/// Checks if a blue code observation Lists needs to be generated and inserts it.
/// </summary>
/// <param name="obsList"></param>
public async Task CalculateBlueCodeList(List<PatientObservation?>? obsList)
{
//Las 3 observaciones vienen siempre juntas en el mismo mensaje
//Solo se comprueba si se cumplen 2 de las 3 condiciones
var condition = 0;
var patientId = obsList?.FirstOrDefault()?.PatientId;
if (patientId == null) return;
var fcObs = obsList?.FirstOrDefault(o => o is { Name: "FC" });
var sattcObs = obsList?.FirstOrDefault(o => o is { Name: "Sattc" });
var tamObs = obsList?.FirstOrDefault(o => o is { Name: "TAm" });
// FC < 60
if (fcObs != null && !CheckIfObservationIsOlderAndShouldBeIgnored(fcObs) &&
double.TryParse(fcObs.Value.ToString(), out var cardiacBeatRate) && cardiacBeatRate < 60)
{
Log.Debug("BlueCode condition: FC {fc} for patient: {obsPatientid} fc increase limit", cardiacBeatRate,
fcObs.PatientId);
condition++;
}
// SpO2 < 80 (Sattc)
if (sattcObs != null && !CheckIfObservationIsOlderAndShouldBeIgnored(sattcObs) &&
double.TryParse(sattcObs.Value.ToString(), out var spO2Value) && spO2Value < 80)
{
Log.Debug("Blue code condition: SpO2 {sp} for patient: {obsPatientid} Sattc increase limit", spO2Value,
sattcObs.PatientId);
condition++;
}
if (condition == 0) return;
// Early exit if 2 conditions are met
if (condition == 2)
{
_ = SendBlueCode(fcObs);
return;
}
// TAm actual es un 50% por debajo de la medición de 30 seg antes
if (tamObs != null && !CheckIfObservationIsOlderAndShouldBeIgnored(tamObs) &&
double.TryParse(tamObs.Value.ToString(), out var tamObsValue))
{
// Buscamos el anterior
var lastPressBloodObsList =
await _observationService.Value.FindLastObservations(tamObs.PatientId, 1, _pressBloodArteryMean);
var lastPressBloodObs = lastPressBloodObsList.FirstOrDefault();
// Diferencia entre la última observación es < 30s
if (lastPressBloodObs != null &&
double.TryParse(lastPressBloodObs.Value.ToString(), out var lastPressBloodValue) &&
(tamObs.Time - lastPressBloodObs.Time).TotalSeconds <= 35 &&
tamObsValue <= lastPressBloodValue / 2)
{
Log.Debug("Blue code condition: TAm decrease limit over 30s for patient: {obsPatientid}",
tamObs.PatientId);
condition++;
}
}
if (condition >= 2) _ = SendBlueCode(tamObs);
}
/// <summary>
/// Sends a Blue Code alarm for the given patient observation. If the observation is null, the method returns without taking any action; otherwise it creates the corresponding alarm observation, validates it against the alarm configuration, persists it, and dispatches the alarm as an automatically triggered Blue alarm.
/// </summary>
/// <param name="pobs">The patient observation that triggers the Blue Code alarm; when null, the method short-circuits without processing.</param>
private async Task SendBlueCode(PatientObservation? pobs)
{
if (pobs == null)
return;
Log.Debug("Blue code Alarm for patient: {obsPatientid}", pobs.PatientId);
var blueCodeObservation = CreateAlarmObservation("BlueCode", pobs);
blueCodeObservation = await CheckAlarmConfig(blueCodeObservation);
_ = _observationService.Value.InsertObservation(blueCodeObservation);
_ = SendAlarm(blueCodeObservation, "BlueCode", AlarmEnum.Name.Blue, AlarmEnum.Type.Auto);
}
//PRVC : donde aparezca cambiar y mostrar en su lugar: VCRP
//FLUJO ALTO: poner OAF en su lugar
/// <summary>
/// Calculates and normalizes the ventilation mode value for a patient observation.
/// Replaces occurrences of "PRVC" with "VCRP" and maps "FLUJ.ALTO" to "OAF" in the observation's value.
/// </summary>
/// <param name="obs">The base patient observation value whose ventilation mode will be calculated and normalized.</param>
/// <returns>The patient observation with the normalized ventilation mode value.</returns>
private BasePatientObservationValue CalculateVentilationMode(BasePatientObservationValue obs)
{
var value = obs is PatientObservationAlarm oAlarm ? oAlarm.Value
: obs is PatientObservation o ? o.Value : null;
if (value != null && value.ToString()!.Contains("PRVC"))
obs.Value = value.ToString()!.Replace("PRVC", "VCRP");
else if (value != null && value.ToString()!.Contains("FLUJ.ALTO")) obs.Value = "OAF";
_logger.LogDebug("Calculating patient id: {id} ventilation mode: {obsName}, value : {obsValue}", obs.PatientId,
obs.Name, obs.Value);
return obs;
}
//TODO duplicado con el calculated del 12o, sacar a un utils
/// <summary>
/// Checks whether a patient observation with the same time already exists and, if so, increments the observation time by one second to avoid a duplicate timestamp.
/// </summary>
/// <param name="obs">The patient observation to evaluate; non-<see cref="PatientObservation"/> instances are returned unchanged.</param>
/// <returns>The original observation, with its <c>Time</c> adjusted by one second when a matching observation is found.</returns>
private async Task<BasePatientObservationValue> CheckObsWithSameTimeExistsAndIncrementTime(
BasePatientObservationValue obs)
{
if (obs is not PatientObservation pobs) return obs;
var existObsWithSameTime =
await _observationService.Value.FindAnyWithSameDate(pobs.PatientId, pobs.Time, pobs.Name);
if (existObsWithSameTime != null && existObsWithSameTime.Any()) pobs.Time = pobs.Time.AddSeconds(1);
return pobs;
}
//S/F Calc ( craneal saturation / FiO2
/// <summary>
/// Calculates the SpO2/FiO2 (S/F) ratio for a patient by pairing a newly received oxygenation observation
/// (FiO2 or Sattc) with the most recent complementary value and inserting a derived "Sattc_FiO2" observation.
/// The derived value is only inserted when both observations exist, are not expired, parse as numbers, the saturation is at most 97, and FiO2 is non-zero; otherwise the calculation is logged and skipped, and any exception is caught and logged.
/// </summary>
/// <param name="obs">The newly received patient observation that triggered the calculation; its Name must be either "FiO2" or "Sattc".</param>
/// <param name="name">The name of the observation, used to determine whether <paramref name="obs"/> is the FiO2 or the Sattc value.</param>
private async Task CalculateSf(BasePatientObservationValue obs, string name)
{
try
{
PatientObservation? saturation = null;
PatientObservation? fio2 = null;
var values = await _observationService.Value.FindLastObservations(obs.PatientId, 1, ["FiO2", "Sattc"]);
switch (name)
{
case "FiO2":
fio2 = (PatientObservation)obs;
saturation = values.FirstOrDefault(o => o.Name == "Sattc");
break;
case "Sattc":
saturation = (PatientObservation)obs;
fio2 = values.FirstOrDefault(o => o.Name == "FiO2");
break;
}
if (fio2 != null && saturation != null && !fio2.Expired &&
double.TryParse(fio2.Value.ToString(), out var fio) && !saturation.Expired &&
double.TryParse(saturation.Value.ToString(), out var sat) && sat <= 97)
{
if (fio == 0)
{
_logger.LogError("Error calculating SF. FiO2: {fio}", fio);
return;
}
var sfValue = sat / fio;
sfValue = Math.Round(sfValue, 4);
var sfObs = new PatientObservation
{
PatientId = obs.PatientId,
Name = "Sattc_FiO2",
CodingSystem = "ADAS",
Value = sfValue,
Time = obs.Time
};
await _observationService.Value.InsertObservation(sfObs);
}
}
catch (Exception ex)
{
_logger.LogError("Error calculate S/F observation. Exception: {exMessage}", ex.Message);
}
}
/// <summary>
/// Oxygenation index is calculated => P_VAM x FiO2 x 100 / PaO2_Tidal
/// the method is called when it receives P_VAM or FiO2 or PaO2_Tidal and tries to take the other values to perform the
/// calculation if they are not
/// in database then does nothing.
/// </summary>
public async Task CalculateOxygenationIndex(BasePatientObservationValue obs, string name)
{
var toSearchList = new List<string>();
PatientObservation? pVam = null;
PatientObservation? fiO2 = null;
PatientObservation? paO2 = null;
toSearchList.AddRange(new List<string> { "P_VAM", "FiO2", "PaO2_Tidal" });
var values = await _observationService.Value.FindLastObservations(obs.PatientId, 1, toSearchList);
try
{
switch (name)
{
case "P_VAM":
pVam = (PatientObservation)obs;
fiO2 = values.FirstOrDefault(o => o.Name == "FiO2");
paO2 = values.FirstOrDefault(o => o.Name == "PaO2_Tidal");
break;
case "FiO2":
fiO2 = (PatientObservation)obs;
pVam = values.FirstOrDefault(o => o.Name == "P_VAM");
paO2 = values.FirstOrDefault(o => o.Name == "PaO2_Tidal");
break;
case "PaO2_Tidal":
paO2 = (PatientObservation)obs;
fiO2 = values.FirstOrDefault(o => o.Name == "FiO2");
pVam = values.FirstOrDefault(o => o.Name == "P_VAM");
break;
}
//Controlar que FiO2 no haya expirada al recogerla
if (fiO2 != null && paO2 != null && pVam != null &&
int.TryParse(pVam.Value.ToString(), out var pvam) &&
int.TryParse(fiO2.Value.ToString(), out var fio2) &&
int.TryParse(paO2.Value.ToString(), out var pao2))
{
if (pao2 == 0)
{
_logger.LogError(
"Error calculate Oxygenation Index observation. PaO2: {pao2}, observation: {obsName}", pao2,
name);
return;
}
var oxygenationIndexValue = pvam * fio2 * 100 / pao2;
var oxygenationIndexObs = new PatientObservation
{
PatientId = obs.PatientId,
Name = "Oxygenation_Index",
CodingSystem = "ADAS",
Value = oxygenationIndexValue,
Time = obs.Time
};
await _observationService.Value.InsertObservation(oxygenationIndexObs);
}
}
catch (Exception ex)
{
_logger.LogError("Error calculate Oxygenation Index observation. Exception: {exMessage}", ex.Message);
}
}
//P/F Calc ( PaO2_Tidal / FiO2
/// <summary>
/// Asynchronously calculates the PaO2/FiO2 (P/F) ratio for a patient and persists the result as a new <c>PaO2_FiO2</c> observation.
/// Supports being triggered by either an FiO2 or a PaO2_Tidal observation by pairing the provided observation with its counterpart retrieved from the most recent observations, and only persists the result when both values are present, non-expired, parseable as numeric values, and the FiO2 value is non-zero.
/// </summary>
/// <param name="obs">The current patient observation that initiated the calculation; provides the patient identifier and timestamp used for the resulting observation.</param>
/// <param name="name">The name of the observation in <paramref name="obs"/>, expected to be either <c>FiO2</c> or <c>PaO2_Tidal</c>, which determines how the counterpart value is resolved.</param>
private async Task CalculatePf(BasePatientObservationValue obs, string name)
{
try
{
PatientObservation? paO2 = null;
PatientObservation? fio2 = null;
var values = await _observationService.Value.FindLastObservations(obs.PatientId, 1,
["FiO2", "PaO2_Tidal"]);
switch (name)
{
case "FiO2":
fio2 = (PatientObservation)obs;
paO2 = values.FirstOrDefault(o => o.Name == "PaO2_Tidal");
break;
case "PaO2_Tidal":
paO2 = (PatientObservation)obs;
fio2 = values.FirstOrDefault(o => o.Name == "FiO2");
break;
}
if (fio2 != null && paO2 != null && !fio2.Expired && !paO2.Expired &&
double.TryParse(paO2.Value.ToString(), out var pa) &&
double.TryParse(fio2.Value.ToString(), out var fi))
{
if (fi == 0)
{
_logger.LogError("Error Calculating PF. FiO2 value is 0. Observation:{obs}", obs);
return;
}
var sfValue = pa / fi;
sfValue = Math.Round(sfValue, 4);
var sfObs = new PatientObservation
{
PatientId = obs.PatientId,
Name = "PaO2_FiO2",
CodingSystem = "ADAS",
Value = sfValue,
Time = obs.Time
};
await _observationService.Value.InsertObservation(sfObs);
}
}
catch (Exception ex)
{
_logger.LogError("Error calculate P/F observation. Exception: {exMessage}", ex.Message);
}
}
/// <summary>
/// Creates a <see cref="PatientObservation"/> representing a pump alarm, tagging it with the
/// ADAS_ALARM coding system and a code/name derived from the alarm <paramref name="name"/>.
/// The position label used as the observation value distinguishes main rack pumps from auxiliary rack pumps based on <see cref="PumpObservation.IsAux"/>, and falls back to the position when no drug name is available. The patient identifier is assigned when present, and the observation is then validated against the alarm configuration.
/// </summary>
/// <param name="name">The alarm identifier used to build the observation <c>Code</c> and <c>Name</c>.</param>
/// <param name="pumpObservation">The pump data source providing rack/auxiliary info, drug name, time, and optional patient id.</param>
/// <returns>A <see cref="Task{PatientObservation}"/> that yields the resulting observation, or <c>null</c> when the alarm configuration check rejects it.</returns>
private async Task<PatientObservation?> CreatePumpAlarmObservation(string name,
PumpObservation pumpObservation)
{
var positionPump = pumpObservation.IsAux != null && !pumpObservation.IsAux.Value
? $"Rack {pumpObservation.GatewayNumber} Bomba {pumpObservation.Number}"
: $"Rack Aux {pumpObservation.GatewayNumber} Bomba {pumpObservation.Number}";
PatientObservation patientObservation = new()
{
CodingSystem = "ADAS_ALARM",
Code = $"Pump_{name}",
Name = $"Alarm_Pump_{name}",
Value = pumpObservation.DrugName ?? positionPump,
Time = pumpObservation.Time
};
if (pumpObservation.PatientId != null)
patientObservation.PatientId = pumpObservation.PatientId.Value;
return await CheckAlarmConfig(patientObservation);
}
}