feature: MIMIC-IV Replay Scenario Generator
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
using System.CommandLine;
|
||||
using System.Text.Json;
|
||||
using Spectre.Console;
|
||||
|
||||
public static class MimicGenerateCommand
|
||||
{
|
||||
public static Command Create()
|
||||
{
|
||||
var dataDirArg = new Argument<DirectoryInfo>("mimic-dir",
|
||||
"Path to directory containing MIMIC-IV CSV files");
|
||||
var stayIdOpt = new Option<int>("--stay-id", "ICU stay ID to generate scenario for")
|
||||
{ IsRequired = true };
|
||||
var maxHoursOpt = new Option<int?>("--max-hours", "Limit scenario duration in hours");
|
||||
var noMedsOpt = new Option<bool>("--no-medications", () => false,
|
||||
"Exclude medication events");
|
||||
var noLabsOpt = new Option<bool>("--no-labs", () => false,
|
||||
"Exclude lab observations");
|
||||
var outputOpt = new Option<string?>("--output", "Output file path (default: auto-named)");
|
||||
var validateOpt = new Option<bool>("--validate", () => false,
|
||||
"Run scenario validation after generation");
|
||||
|
||||
var command = new Command("mimic-generate",
|
||||
"Generate a VigilCare scenario JSON from a MIMIC-IV ICU stay")
|
||||
{
|
||||
dataDirArg, stayIdOpt, maxHoursOpt, noMedsOpt, noLabsOpt, outputOpt, validateOpt
|
||||
};
|
||||
|
||||
command.SetHandler(context =>
|
||||
{
|
||||
var dataDir = context.ParseResult.GetValueForArgument(dataDirArg);
|
||||
var stayId = context.ParseResult.GetValueForOption(stayIdOpt);
|
||||
var maxHours = context.ParseResult.GetValueForOption(maxHoursOpt);
|
||||
var noMeds = context.ParseResult.GetValueForOption(noMedsOpt);
|
||||
var noLabs = context.ParseResult.GetValueForOption(noLabsOpt);
|
||||
var outputPath = context.ParseResult.GetValueForOption(outputOpt);
|
||||
var validate = context.ParseResult.GetValueForOption(validateOpt);
|
||||
|
||||
var loader = new MimicDataLoader(dataDir.FullName);
|
||||
|
||||
AnsiConsole.MarkupLine($"[bold]Loading MIMIC-IV data for stay {stayId}...[/]");
|
||||
|
||||
var stays = loader.LoadIcuStays();
|
||||
var stay = stays.FirstOrDefault(s => s.StayId == stayId);
|
||||
if (stay is null)
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[red]ICU stay {stayId} not found.[/]");
|
||||
var available = stays.Select(s => s.StayId).OrderBy(x => x).ToList();
|
||||
AnsiConsole.MarkupLine($"Available stay IDs: {string.Join(", ", available.Take(20))}...");
|
||||
return;
|
||||
}
|
||||
|
||||
var admissions = loader.LoadAdmissions();
|
||||
var admission = admissions.FirstOrDefault(a => a.HadmId == stay.HadmId);
|
||||
if (admission is null)
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[red]Admission {stay.HadmId} not found.[/]");
|
||||
return;
|
||||
}
|
||||
|
||||
var patients = loader.LoadPatients();
|
||||
var patient = patients.FirstOrDefault(p => p.SubjectId == stay.SubjectId);
|
||||
if (patient is null)
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[red]Patient {stay.SubjectId} not found.[/]");
|
||||
return;
|
||||
}
|
||||
|
||||
AnsiConsole.MarkupLine(
|
||||
$" Patient: [cyan]{patient.SubjectId}[/] ({patient.Gender}, ~{patient.AnchorAge}y)");
|
||||
AnsiConsole.MarkupLine(
|
||||
$" Stay: [cyan]{stay.StayId}[/] in {stay.FirstCareUnit}");
|
||||
AnsiConsole.MarkupLine(
|
||||
$" LOS: {stay.Los:F1} days ({stay.InTime:g} → {stay.OutTime:g})");
|
||||
AnsiConsole.MarkupLine(
|
||||
$" Outcome: {(admission.HospitalExpireFlag == 1 ? "[red]Expired[/]" : "Survived")}");
|
||||
|
||||
var options = new MimicGenerateOptions(
|
||||
MaxHours: maxHours,
|
||||
IncludeMedications: !noMeds,
|
||||
IncludeLabs: !noLabs);
|
||||
|
||||
AnsiConsole.MarkupLine("\n[bold]Generating scenario...[/]");
|
||||
|
||||
var builder = new MimicScenarioBuilder(loader);
|
||||
var (scenario, warnings) = builder.Build(stay, admission, patient, options);
|
||||
|
||||
foreach (var warning in warnings)
|
||||
AnsiConsole.MarkupLine($" [yellow]WARNING:[/] {Markup.Escape(warning)}");
|
||||
|
||||
var obsCount = scenario.Events.Count(e => e.Type == "observation");
|
||||
var medCount = scenario.Events.Count(e => e.Type == "medication");
|
||||
AnsiConsole.MarkupLine(
|
||||
$" Events: [green]{obsCount}[/] observations, [green]{medCount}[/] medications");
|
||||
AnsiConsole.MarkupLine(
|
||||
$" Duration: {scenario.Scenario.DurationMinutes} minutes " +
|
||||
$"({scenario.Scenario.DurationMinutes / 60.0:F1} hours)");
|
||||
|
||||
if (validate)
|
||||
{
|
||||
var errors = ScenarioValidator.Validate(scenario);
|
||||
if (errors.Count == 0)
|
||||
{
|
||||
AnsiConsole.MarkupLine(" [green]Validation: PASSED[/]");
|
||||
}
|
||||
else
|
||||
{
|
||||
AnsiConsole.MarkupLine($" [red]Validation: {errors.Count} error(s)[/]");
|
||||
foreach (var err in errors)
|
||||
AnsiConsole.MarkupLine($" [red]• {Markup.Escape(err)}[/]");
|
||||
}
|
||||
}
|
||||
|
||||
var jsonOptions = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
WriteIndented = true,
|
||||
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(scenario, jsonOptions);
|
||||
|
||||
var filePath = outputPath
|
||||
?? Path.Combine("Scenarios", "List", $"mimic-s{stayId}.json");
|
||||
|
||||
var dir = Path.GetDirectoryName(filePath);
|
||||
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
|
||||
Directory.CreateDirectory(dir);
|
||||
|
||||
File.WriteAllText(filePath, json);
|
||||
AnsiConsole.MarkupLine($"\n[bold green]Scenario written to:[/] {filePath}");
|
||||
AnsiConsole.MarkupLine($" Replay with: [dim]dotnet run -- replay {filePath}[/]");
|
||||
});
|
||||
|
||||
return command;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user