feature: Simulator Scenarios + Clinical Validation: SOFA/GCS

This commit is contained in:
voltsrage
2026-06-21 04:55:39 +08:00
parent bf46e6554a
commit 3638dd0669
39 changed files with 1619 additions and 213 deletions
@@ -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)));
}
+157 -145
View File
@@ -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)
{
+2 -1
View File
@@ -9,7 +9,8 @@ public class SofaDetector
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNameCaseInsensitive = true
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
private readonly IConnectionMultiplexer _redis;