feature: Simulator Scenarios + Clinical Validation: SOFA/GCS
This commit is contained in:
@@ -11,13 +11,11 @@ public class GcsController : ControllerBase
|
||||
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(ApiResponse<GcsScoreResponse>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Current(Guid encounterId)
|
||||
{
|
||||
var score = await _gcs.GetCurrentAsync(encounterId);
|
||||
if (score is null)
|
||||
return NotFound(ApiResponse<object>.Fail(
|
||||
404, "No GCS score computed for this encounter.", "NO_GCS_SCORE"));
|
||||
return Ok(ApiResponse<GcsScoreResponse?>.Ok(null));
|
||||
|
||||
return Ok(ApiResponse<GcsScoreResponse>.Ok(new GcsScoreResponse(
|
||||
score.EyeScore, score.VerbalScore, score.MotorScore,
|
||||
|
||||
@@ -18,12 +18,11 @@ public class News2Controller : ControllerBase
|
||||
/// </summary>
|
||||
[HttpGet("current")]
|
||||
[ProducesResponseType(typeof(ApiResponse<News2Score>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Current(Guid encounterId)
|
||||
{
|
||||
var score = await _news2.GetCurrentAsync(encounterId);
|
||||
if (score is null)
|
||||
return NotFound(ApiResponse<object>.Fail(404, "No NEWS2 score computed for this encounter.", "NO_NEWS2_SCORE"));
|
||||
return Ok(ApiResponse<News2Score?>.Ok(null));
|
||||
return Ok(ApiResponse<News2Score>.Ok(score));
|
||||
}
|
||||
|
||||
|
||||
@@ -17,12 +17,11 @@ public class SepsisBundlesController : ControllerBase
|
||||
/// <param name="encounterId">Encounter id.</param>
|
||||
[HttpGet("api/v1/encounters/{encounterId:guid}/sepsis-bundle/current")]
|
||||
[ProducesResponseType(typeof(ApiResponse<SepsisBundle>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> GetCurrentByEncounter(Guid encounterId)
|
||||
{
|
||||
var bundle = await _bundles.GetCurrentByEncounterAsync(encounterId);
|
||||
if (bundle is null)
|
||||
return NotFound(ApiResponse<object>.Fail(404, "No sepsis bundle exists for this encounter.", "BUNDLE_NOT_FOUND"));
|
||||
return Ok(ApiResponse<SepsisBundle?>.Ok(null));
|
||||
|
||||
return Ok(ApiResponse<SepsisBundle>.Ok(bundle));
|
||||
}
|
||||
|
||||
@@ -12,13 +12,11 @@ public class SofaController : ControllerBase
|
||||
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(ApiResponse<SofaScoreResponse>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Current(Guid encounterId)
|
||||
{
|
||||
var score = await _sofa.GetCurrentAsync(encounterId);
|
||||
if (score is null)
|
||||
return NotFound(ApiResponse<object>.Fail(
|
||||
404, "No SOFA score computed for this encounter.", "NO_SOFA_SCORE"));
|
||||
return Ok(ApiResponse<SofaScoreResponse?>.Ok(null));
|
||||
|
||||
return Ok(ApiResponse<SofaScoreResponse>.Ok(MapResponse(score)));
|
||||
}
|
||||
|
||||
@@ -48,150 +48,7 @@ public static class DataSeeder
|
||||
};
|
||||
db.Encounters.AddRange(encounter1, encounter2);
|
||||
|
||||
// Four alert thresholds
|
||||
var thresholds = new List<AlertThreshold>
|
||||
{
|
||||
new() {
|
||||
Id = Guid.NewGuid(), ObservationCode = "HEART_RATE", DisplayName = "Heart Rate",
|
||||
Unit = "bpm", CriticalLow = 30, WarningLow = 50, WarningHigh = 100, CriticalHigh = 150,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
},
|
||||
new() {
|
||||
Id = Guid.NewGuid(), ObservationCode = "TEMP_C", DisplayName = "Body Temperature",
|
||||
Unit = "°C", CriticalLow = 35.0m, WarningLow = 36.0m, WarningHigh = 38.3m, CriticalHigh = 40.0m,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
},
|
||||
new() {
|
||||
Id = Guid.NewGuid(), ObservationCode = "POTASSIUM_MEQ_L", DisplayName = "Serum Potassium",
|
||||
Unit = "mEq/L", CriticalLow = 2.5m, WarningLow = 3.5m, WarningHigh = 5.0m, CriticalHigh = 6.5m,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
},
|
||||
new() {
|
||||
Id = Guid.NewGuid(), ObservationCode = "SPO2", DisplayName = "Oxygen Saturation",
|
||||
Unit = "%", CriticalLow = 88, WarningLow = 92, WarningHigh = null, CriticalHigh = null,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
},
|
||||
new AlertThreshold
|
||||
{
|
||||
Id = Guid.NewGuid(), ObservationCode = "RESP_RATE",
|
||||
DisplayName = "Respiratory Rate", Unit = "breaths/min",
|
||||
CriticalLow = null, WarningLow = 12m, WarningHigh = 20m, CriticalHigh = 30m,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
},
|
||||
new AlertThreshold
|
||||
{
|
||||
Id = Guid.NewGuid(), ObservationCode = "WBC_K_UL",
|
||||
DisplayName = "White Blood Cell Count", Unit = "k/µL",
|
||||
CriticalLow = 2.0m, WarningLow = 4.0m, WarningHigh = 12.0m, CriticalHigh = 20.0m,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
},
|
||||
new AlertThreshold
|
||||
{
|
||||
Id = Guid.NewGuid(), ObservationCode = "SYSTOLIC_BP",
|
||||
DisplayName = "Systolic Blood Pressure", Unit = "mmHg",
|
||||
CriticalLow = 70m, WarningLow = 90m, WarningHigh = 160m, CriticalHigh = 180m,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
},
|
||||
new AlertThreshold
|
||||
{
|
||||
Id = Guid.NewGuid(), ObservationCode = "DIASTOLIC_BP",
|
||||
DisplayName = "Diastolic Blood Pressure", Unit = "mmHg",
|
||||
CriticalLow = 40m, WarningLow = 60m, WarningHigh = 90m, CriticalHigh = 110m,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
},
|
||||
new AlertThreshold
|
||||
{
|
||||
Id = Guid.NewGuid(), ObservationCode = "LACTATE_MMOL_L",
|
||||
DisplayName = "Serum Lactate", Unit = "mmol/L",
|
||||
CriticalLow = null, WarningLow = null, WarningHigh = 2.0m, CriticalHigh = 4.0m,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
},
|
||||
new AlertThreshold
|
||||
{
|
||||
Id = Guid.NewGuid(), ObservationCode = "AVPU",
|
||||
DisplayName = "AVPU Consciousness Level", Unit = "score",
|
||||
CriticalLow = null, WarningLow = null, WarningHigh = null, CriticalHigh = 2m,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
},
|
||||
new AlertThreshold
|
||||
{
|
||||
Id = Guid.NewGuid(), ObservationCode = "SUPPLEMENTAL_O2",
|
||||
DisplayName = "Supplemental Oxygen", Unit = "flag",
|
||||
CriticalLow = null, WarningLow = null, WarningHigh = null, CriticalHigh = null,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
},
|
||||
new AlertThreshold
|
||||
{
|
||||
Id = Guid.NewGuid(), ObservationCode = "GLUCOSE_MG_DL",
|
||||
DisplayName = "Blood Glucose", Unit = "mg/dL",
|
||||
CriticalLow = 40m, WarningLow = 70m, WarningHigh = 180m, CriticalHigh = 400m,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
},
|
||||
// GCS components — registered for ingestion; thresholds are null (alerting is on computed total in GcsDetector)
|
||||
new AlertThreshold
|
||||
{
|
||||
Id = Guid.NewGuid(), ObservationCode = "GCS_EYE",
|
||||
DisplayName = "GCS Eye Response", Unit = "score",
|
||||
CriticalLow = null, WarningLow = null, WarningHigh = null, CriticalHigh = null,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
},
|
||||
new AlertThreshold
|
||||
{
|
||||
Id = Guid.NewGuid(), ObservationCode = "GCS_VERBAL",
|
||||
DisplayName = "GCS Verbal Response", Unit = "score",
|
||||
CriticalLow = null, WarningLow = null, WarningHigh = null, CriticalHigh = null,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
},
|
||||
new AlertThreshold
|
||||
{
|
||||
Id = Guid.NewGuid(), ObservationCode = "GCS_MOTOR",
|
||||
DisplayName = "GCS Motor Response", Unit = "score",
|
||||
CriticalLow = null, WarningLow = null, WarningHigh = null, CriticalHigh = null,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
},
|
||||
new AlertThreshold
|
||||
{
|
||||
Id = Guid.NewGuid(), ObservationCode = "PAO2_MMHG",
|
||||
DisplayName = "Partial Pressure O2 (Arterial)", Unit = "mmHg",
|
||||
CriticalLow = 60m, WarningLow = 80m, WarningHigh = null, CriticalHigh = null,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
},
|
||||
new AlertThreshold
|
||||
{
|
||||
Id = Guid.NewGuid(), ObservationCode = "FIO2_PCT",
|
||||
DisplayName = "Fraction of Inspired O2", Unit = "%",
|
||||
CriticalLow = null, WarningLow = null, WarningHigh = null, CriticalHigh = null,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
},
|
||||
new AlertThreshold
|
||||
{
|
||||
Id = Guid.NewGuid(), ObservationCode = "PLATELET_K_UL",
|
||||
DisplayName = "Platelet Count", Unit = "k/µL",
|
||||
CriticalLow = 20m, WarningLow = 50m, WarningHigh = null, CriticalHigh = null,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
},
|
||||
new AlertThreshold
|
||||
{
|
||||
Id = Guid.NewGuid(), ObservationCode = "BILIRUBIN_MG_DL",
|
||||
DisplayName = "Total Bilirubin", Unit = "mg/dL",
|
||||
CriticalLow = null, WarningLow = null, WarningHigh = 2.0m, CriticalHigh = 6.0m,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
},
|
||||
new AlertThreshold
|
||||
{
|
||||
Id = Guid.NewGuid(), ObservationCode = "CREATININE_MG_DL",
|
||||
DisplayName = "Serum Creatinine", Unit = "mg/dL",
|
||||
CriticalLow = null, WarningLow = null, WarningHigh = 2.0m, CriticalHigh = 3.5m,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
},
|
||||
new AlertThreshold
|
||||
{
|
||||
Id = Guid.NewGuid(), ObservationCode = "URINE_OUTPUT_ML_H",
|
||||
DisplayName = "Urine Output", Unit = "mL/h",
|
||||
CriticalLow = null, WarningLow = null, WarningHigh = null, CriticalHigh = null,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
},
|
||||
};
|
||||
var thresholds = BuildDefaultThresholds();
|
||||
db.AlertThresholds.AddRange(thresholds);
|
||||
|
||||
// Observations spanning normal, warning, and critical ranges for encounter1
|
||||
@@ -235,8 +92,163 @@ public static class DataSeeder
|
||||
db.Observations.AddRange(observations);
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
await CacheThresholdsAsync(redis, thresholds);
|
||||
}
|
||||
|
||||
// Populate Redis cache with the seeded thresholds
|
||||
public static async Task SeedThresholdsOnlyAsync(AppDbContext db, IConnectionMultiplexer redis)
|
||||
{
|
||||
var thresholds = BuildDefaultThresholds();
|
||||
db.AlertThresholds.AddRange(thresholds);
|
||||
await db.SaveChangesAsync();
|
||||
await CacheThresholdsAsync(redis, thresholds);
|
||||
}
|
||||
|
||||
public static List<AlertThreshold> BuildDefaultThresholds() => new()
|
||||
{
|
||||
new() {
|
||||
Id = Guid.NewGuid(), ObservationCode = "HEART_RATE", DisplayName = "Heart Rate",
|
||||
Unit = "bpm", CriticalLow = 30, WarningLow = 50, WarningHigh = 100, CriticalHigh = 150,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
},
|
||||
new() {
|
||||
Id = Guid.NewGuid(), ObservationCode = "TEMP_C", DisplayName = "Body Temperature",
|
||||
Unit = "°C", CriticalLow = 35.0m, WarningLow = 36.0m, WarningHigh = 38.3m, CriticalHigh = 40.0m,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
},
|
||||
new() {
|
||||
Id = Guid.NewGuid(), ObservationCode = "POTASSIUM_MEQ_L", DisplayName = "Serum Potassium",
|
||||
Unit = "mEq/L", CriticalLow = 2.5m, WarningLow = 3.5m, WarningHigh = 5.0m, CriticalHigh = 6.5m,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
},
|
||||
new() {
|
||||
Id = Guid.NewGuid(), ObservationCode = "SPO2", DisplayName = "Oxygen Saturation",
|
||||
Unit = "%", CriticalLow = 88, WarningLow = 92, WarningHigh = null, CriticalHigh = null,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
},
|
||||
new AlertThreshold
|
||||
{
|
||||
Id = Guid.NewGuid(), ObservationCode = "RESP_RATE",
|
||||
DisplayName = "Respiratory Rate", Unit = "breaths/min",
|
||||
CriticalLow = null, WarningLow = 12m, WarningHigh = 20m, CriticalHigh = 30m,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
},
|
||||
new AlertThreshold
|
||||
{
|
||||
Id = Guid.NewGuid(), ObservationCode = "WBC_K_UL",
|
||||
DisplayName = "White Blood Cell Count", Unit = "k/µL",
|
||||
CriticalLow = 2.0m, WarningLow = 4.0m, WarningHigh = 12.0m, CriticalHigh = 20.0m,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
},
|
||||
new AlertThreshold
|
||||
{
|
||||
Id = Guid.NewGuid(), ObservationCode = "SYSTOLIC_BP",
|
||||
DisplayName = "Systolic Blood Pressure", Unit = "mmHg",
|
||||
CriticalLow = 70m, WarningLow = 90m, WarningHigh = 160m, CriticalHigh = 180m,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
},
|
||||
new AlertThreshold
|
||||
{
|
||||
Id = Guid.NewGuid(), ObservationCode = "DIASTOLIC_BP",
|
||||
DisplayName = "Diastolic Blood Pressure", Unit = "mmHg",
|
||||
CriticalLow = 40m, WarningLow = 60m, WarningHigh = 90m, CriticalHigh = 110m,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
},
|
||||
new AlertThreshold
|
||||
{
|
||||
Id = Guid.NewGuid(), ObservationCode = "LACTATE_MMOL_L",
|
||||
DisplayName = "Serum Lactate", Unit = "mmol/L",
|
||||
CriticalLow = null, WarningLow = null, WarningHigh = 2.0m, CriticalHigh = 4.0m,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
},
|
||||
new AlertThreshold
|
||||
{
|
||||
Id = Guid.NewGuid(), ObservationCode = "AVPU",
|
||||
DisplayName = "AVPU Consciousness Level", Unit = "score",
|
||||
CriticalLow = null, WarningLow = null, WarningHigh = null, CriticalHigh = 2m,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
},
|
||||
new AlertThreshold
|
||||
{
|
||||
Id = Guid.NewGuid(), ObservationCode = "SUPPLEMENTAL_O2",
|
||||
DisplayName = "Supplemental Oxygen", Unit = "flag",
|
||||
CriticalLow = null, WarningLow = null, WarningHigh = null, CriticalHigh = null,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
},
|
||||
new AlertThreshold
|
||||
{
|
||||
Id = Guid.NewGuid(), ObservationCode = "GLUCOSE_MG_DL",
|
||||
DisplayName = "Blood Glucose", Unit = "mg/dL",
|
||||
CriticalLow = 40m, WarningLow = 70m, WarningHigh = 180m, CriticalHigh = 400m,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
},
|
||||
new AlertThreshold
|
||||
{
|
||||
Id = Guid.NewGuid(), ObservationCode = "GCS_EYE",
|
||||
DisplayName = "GCS Eye Response", Unit = "score",
|
||||
CriticalLow = null, WarningLow = null, WarningHigh = null, CriticalHigh = null,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
},
|
||||
new AlertThreshold
|
||||
{
|
||||
Id = Guid.NewGuid(), ObservationCode = "GCS_VERBAL",
|
||||
DisplayName = "GCS Verbal Response", Unit = "score",
|
||||
CriticalLow = null, WarningLow = null, WarningHigh = null, CriticalHigh = null,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
},
|
||||
new AlertThreshold
|
||||
{
|
||||
Id = Guid.NewGuid(), ObservationCode = "GCS_MOTOR",
|
||||
DisplayName = "GCS Motor Response", Unit = "score",
|
||||
CriticalLow = null, WarningLow = null, WarningHigh = null, CriticalHigh = null,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
},
|
||||
new AlertThreshold
|
||||
{
|
||||
Id = Guid.NewGuid(), ObservationCode = "PAO2_MMHG",
|
||||
DisplayName = "Partial Pressure O2 (Arterial)", Unit = "mmHg",
|
||||
CriticalLow = 60m, WarningLow = 80m, WarningHigh = null, CriticalHigh = null,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
},
|
||||
new AlertThreshold
|
||||
{
|
||||
Id = Guid.NewGuid(), ObservationCode = "FIO2_PCT",
|
||||
DisplayName = "Fraction of Inspired O2", Unit = "%",
|
||||
CriticalLow = null, WarningLow = null, WarningHigh = null, CriticalHigh = null,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
},
|
||||
new AlertThreshold
|
||||
{
|
||||
Id = Guid.NewGuid(), ObservationCode = "PLATELET_K_UL",
|
||||
DisplayName = "Platelet Count", Unit = "k/µL",
|
||||
CriticalLow = 20m, WarningLow = 50m, WarningHigh = null, CriticalHigh = null,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
},
|
||||
new AlertThreshold
|
||||
{
|
||||
Id = Guid.NewGuid(), ObservationCode = "BILIRUBIN_MG_DL",
|
||||
DisplayName = "Total Bilirubin", Unit = "mg/dL",
|
||||
CriticalLow = null, WarningLow = null, WarningHigh = 2.0m, CriticalHigh = 6.0m,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
},
|
||||
new AlertThreshold
|
||||
{
|
||||
Id = Guid.NewGuid(), ObservationCode = "CREATININE_MG_DL",
|
||||
DisplayName = "Serum Creatinine", Unit = "mg/dL",
|
||||
CriticalLow = null, WarningLow = null, WarningHigh = 2.0m, CriticalHigh = 3.5m,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
},
|
||||
new AlertThreshold
|
||||
{
|
||||
Id = Guid.NewGuid(), ObservationCode = "URINE_OUTPUT_ML_H",
|
||||
DisplayName = "Urine Output", Unit = "mL/h",
|
||||
CriticalLow = null, WarningLow = null, WarningHigh = null, CriticalHigh = null,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
},
|
||||
};
|
||||
|
||||
private static async Task CacheThresholdsAsync(
|
||||
IConnectionMultiplexer redis, IEnumerable<AlertThreshold> thresholds)
|
||||
{
|
||||
var cache = redis.GetDatabase();
|
||||
foreach (var t in thresholds)
|
||||
{
|
||||
|
||||
@@ -9,7 +9,8 @@ public class SofaDetector
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
PropertyNameCaseInsensitive = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
|
||||
private readonly IConnectionMultiplexer _redis;
|
||||
|
||||
Reference in New Issue
Block a user