diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 000000000..efb824584 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,18 @@ +{ + "permissions": { + "allow": [ + "Bash(dotnet test:*)", + "Bash(cat:*)", + "Bash(dotnet sln:*)", + "Bash(dotnet build:*)", + "Bash(dir \"C:\\\\Users\\\\HEC\\\\Data\\\\FDA\\\\Canon\" /b)", + "Bash(nuget sources:*)", + "Bash(dotnet restore:*)", + "Bash(./HEC.FDA.TestingUtility.exe:*)", + "Bash(./HEC.FDA.TestingUtility/bin/Debug/net9.0-windows/HEC.FDA.TestingUtility.exe:*)", + "Bash(dir /s \"C:\\\\Programs\\\\Source\\\\HEC-FDA\\\\HEC.FDA.TestingUtility\\\\*.cs\")", + "Bash(findstr:*)" + ], + "deny": [] + } +} diff --git a/Fda.sln b/Fda.sln index cd22101d3..418a49aa2 100644 --- a/Fda.sln +++ b/Fda.sln @@ -62,6 +62,7 @@ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VisualScratchSpace", "VisualScratchSpace\VisualScratchSpace.csproj", "{90861E11-37B0-49F1-AB98-67D42F39ED1B}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HEC.FDA.Benchmarks", "HEC.FDA.Benchmarks\HEC.FDA.Benchmarks.csproj", "{CFD13894-A4AA-4FBC-8A55-9D62CEAD1893}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HEC.FDA.TestingUtility", "HEC.FDA.TestingUtility\HEC.FDA.TestingUtility.csproj", "{38BE2FDF-9E55-4C96-8759-1A98A67B1789}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -313,6 +314,18 @@ Global {CFD13894-A4AA-4FBC-8A55-9D62CEAD1893}.Release|x64.Build.0 = Release|Any CPU {CFD13894-A4AA-4FBC-8A55-9D62CEAD1893}.Release|x86.ActiveCfg = Release|Any CPU {CFD13894-A4AA-4FBC-8A55-9D62CEAD1893}.Release|x86.Build.0 = Release|Any CPU + {38BE2FDF-9E55-4C96-8759-1A98A67B1789}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {38BE2FDF-9E55-4C96-8759-1A98A67B1789}.Debug|Any CPU.Build.0 = Debug|Any CPU + {38BE2FDF-9E55-4C96-8759-1A98A67B1789}.Debug|x64.ActiveCfg = Debug|Any CPU + {38BE2FDF-9E55-4C96-8759-1A98A67B1789}.Debug|x64.Build.0 = Debug|Any CPU + {38BE2FDF-9E55-4C96-8759-1A98A67B1789}.Debug|x86.ActiveCfg = Debug|Any CPU + {38BE2FDF-9E55-4C96-8759-1A98A67B1789}.Debug|x86.Build.0 = Debug|Any CPU + {38BE2FDF-9E55-4C96-8759-1A98A67B1789}.Release|Any CPU.ActiveCfg = Release|Any CPU + {38BE2FDF-9E55-4C96-8759-1A98A67B1789}.Release|Any CPU.Build.0 = Release|Any CPU + {38BE2FDF-9E55-4C96-8759-1A98A67B1789}.Release|x64.ActiveCfg = Release|Any CPU + {38BE2FDF-9E55-4C96-8759-1A98A67B1789}.Release|x64.Build.0 = Release|Any CPU + {38BE2FDF-9E55-4C96-8759-1A98A67B1789}.Release|x86.ActiveCfg = Release|Any CPU + {38BE2FDF-9E55-4C96-8759-1A98A67B1789}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/HEC.FDA.Model/HEC.FDA.Model.csproj b/HEC.FDA.Model/HEC.FDA.Model.csproj index 6cead47a5..5f8e01863 100644 --- a/HEC.FDA.Model/HEC.FDA.Model.csproj +++ b/HEC.FDA.Model/HEC.FDA.Model.csproj @@ -2,6 +2,10 @@ + + + + diff --git a/HEC.FDA.TestingUtility/ComputeRunner.cs b/HEC.FDA.TestingUtility/ComputeRunner.cs new file mode 100644 index 000000000..45c033f30 --- /dev/null +++ b/HEC.FDA.TestingUtility/ComputeRunner.cs @@ -0,0 +1,326 @@ +using System.Diagnostics; +using HEC.FDA.Model.metrics; +using HEC.FDA.Model.paireddata; +using HEC.FDA.TestingUtility.Configuration; +using HEC.FDA.TestingUtility.Reporting; +using HEC.FDA.TestingUtility.Services; +using HEC.FDA.ViewModel; +using HEC.FDA.ViewModel.AggregatedStageDamage; +using HEC.FDA.ViewModel.Alternatives; +using HEC.FDA.ViewModel.AlternativeComparisonReport; +using HEC.FDA.ViewModel.ImpactAreaScenario; +using HEC.FDA.ViewModel.ImpactArea; +using HEC.FDA.ViewModel.Saving; +using HEC.FDA.ViewModel.TableWithPlot; +using HEC.FDA.ViewModel.Utilities; + +namespace HEC.FDA.TestingUtility; + +/// +/// Runs FDA computations and generates CSV result reports. +/// +public class ComputeRunner +{ + private readonly TestConfiguration _config; + private readonly string _outputDir; + private readonly string[]? _studyFilter; + private readonly CancellationTokenSource _cts; + private readonly CsvReportFactory _csvReportFactory = new(); + + public ComputeRunner(TestConfiguration config, string outputDir, string[]? studyFilter) + { + _config = config; + _outputDir = outputDir; + _studyFilter = studyFilter; + _cts = new CancellationTokenSource(); + + if (_config.GlobalSettings.TimeoutMinutes > 0) + { + _cts.CancelAfter(TimeSpan.FromMinutes(_config.GlobalSettings.TimeoutMinutes)); + } + } + + public async Task RunAsync() + { + int errors = 0; + int completed = 0; + Stopwatch totalStopwatch = Stopwatch.StartNew(); + List<(string StudyId, TimeSpan Duration, int ComputeCount, int ErrorCount)> studyTimings = new(); + + Console.WriteLine($"Configuration: {_config.TestSuiteId}"); + Console.WriteLine($"Output directory: {_outputDir}"); + Console.WriteLine(); + + List studiesToRun = _config.Studies; + if (_studyFilter != null && _studyFilter.Length > 0) + { + studiesToRun = studiesToRun + .Where(s => _studyFilter.Contains(s.StudyId, StringComparer.OrdinalIgnoreCase)) + .ToList(); + + if (studiesToRun.Count == 0) + { + Console.WriteLine($"No studies match the filter: {string.Join(", ", _studyFilter)}"); + return 1; + } + } + + foreach (StudyConfiguration study in studiesToRun) + { + Console.WriteLine($"=== Computing: {study.StudyName} ({study.StudyId}) ==="); + Stopwatch studyStopwatch = Stopwatch.StartNew(); + int studyErrors = 0; + int studyCompleted = 0; + + try + { + using StudyLoader loader = new(); + loader.LoadStudy(study.NetworkSourcePath, _config.GlobalSettings.LocalTempDirectory); + + List computations = BuildComputationList(study); + Console.WriteLine($" Found {computations.Count} computations to run."); + + foreach (ComputeConfiguration compute in computations) + { + _cts.Token.ThrowIfCancellationRequested(); + Stopwatch computeStopwatch = Stopwatch.StartNew(); + + try + { + switch (compute.Type.ToLowerInvariant()) + { + case "stagedamage": + List sdCurves = StageDamageRunner.RunStageDamage(compute.ElementName); + SaveStageDamageResults(compute.ElementName, sdCurves); + _csvReportFactory.AddStageDamageSummary(study.StudyId, compute.ElementName, sdCurves); + break; + + case "scenario": + ScenarioResults scenarioResults = ScenarioRunner.RunScenario(compute.ElementName, _cts.Token); + IASElement scenarioElement = SaveScenarioResults(compute.ElementName, scenarioResults); + _csvReportFactory.AddScenarioResults(study.StudyId, scenarioElement); + break; + + case "alternative": + AlternativeResults altResults = AlternativeRunner.RunAlternative(compute.ElementName, _cts.Token); + AlternativeElement altElement = SaveAlternativeResults(compute.ElementName, altResults); + _csvReportFactory.AddAlternativeResults(study.StudyId, altElement); + break; + + case "alternativecomparison": + (AlternativeComparisonReportResults compResults, List<(int altId, string altName)> withProjAlts) = RunAlternativeComparisonWithMetadata(compute.ElementName, _cts.Token); + _csvReportFactory.AddAlternativeComparisonResults(study.StudyId, compute.ElementName, compResults, withProjAlts); + break; + + default: + Console.WriteLine($" SKIP: Unknown compute type '{compute.Type}'"); + continue; + } + + computeStopwatch.Stop(); + studyCompleted++; + Console.WriteLine($" OK: {compute.Type} '{compute.ElementName}' [{FormatDuration(computeStopwatch.Elapsed)}]"); + } + catch (Exception ex) + { + computeStopwatch.Stop(); + studyErrors++; + Console.WriteLine($" ERROR: {compute.Type} '{compute.ElementName}' [{FormatDuration(computeStopwatch.Elapsed)}]"); + Console.WriteLine($" {ex.Message}"); + Console.WriteLine($" {ex.StackTrace}"); + } + } + } + catch (OperationCanceledException) + { + Console.WriteLine(" TIMEOUT: Computation exceeded time limit."); + studyErrors++; + break; + } + catch (Exception ex) + { + Console.WriteLine($" ERROR loading study: {ex.Message}"); + Console.WriteLine($" {ex.StackTrace}"); + studyErrors++; + } + + studyStopwatch.Stop(); + studyTimings.Add((study.StudyId, studyStopwatch.Elapsed, studyCompleted, studyErrors)); + completed += studyCompleted; + errors += studyErrors; + Console.WriteLine($" Completed in {FormatDuration(studyStopwatch.Elapsed)} ({studyCompleted} succeeded, {studyErrors} failed)"); + Console.WriteLine(); + } + + totalStopwatch.Stop(); + + // Summary + Console.WriteLine("=== Summary ==="); + Console.WriteLine($"Completed: {completed}"); + Console.WriteLine($"Errors: {errors}"); + Console.WriteLine($"Duration: {FormatDuration(totalStopwatch.Elapsed)}"); + Console.WriteLine(); + + // Save CSV report + string csvPath = Path.Combine(_outputDir, "results_report.csv"); + _csvReportFactory.SaveReport(csvPath); + + return errors > 0 ? 1 : 0; + } + + private static string FormatDuration(TimeSpan duration) + { + if (duration.TotalHours >= 1) + { + return $"{duration.Hours}h {duration.Minutes}m {duration.Seconds}s"; + } + else if (duration.TotalMinutes >= 1) + { + return $"{duration.Minutes}m {duration.Seconds}.{duration.Milliseconds / 100}s"; + } + else + { + return $"{duration.Seconds}.{duration.Milliseconds:D3}s"; + } + } + + private static List BuildComputationList(StudyConfiguration study) + { + List computations = new(study.Computations); + + if (study.RunAllStageDamage) + { + List stageDamages = BaseViewModel.StudyCache.GetChildElementsOfType(); + foreach (AggregatedStageDamageElement sd in stageDamages) + { + if (!computations.Any(c => c.Type.Equals("stagedamage", StringComparison.OrdinalIgnoreCase) + && c.ElementName.Equals(sd.Name, StringComparison.OrdinalIgnoreCase))) + { + computations.Add(new ComputeConfiguration { Type = "stagedamage", ElementName = sd.Name }); + Console.WriteLine($" Auto-discovered stage damage: {sd.Name}"); + } + } + } + + if (study.RunAllScenarios) + { + List scenarios = BaseViewModel.StudyCache.GetChildElementsOfType(); + foreach (IASElement scenario in scenarios) + { + if (!computations.Any(c => c.Type.Equals("scenario", StringComparison.OrdinalIgnoreCase) + && c.ElementName.Equals(scenario.Name, StringComparison.OrdinalIgnoreCase))) + { + computations.Add(new ComputeConfiguration { Type = "scenario", ElementName = scenario.Name }); + Console.WriteLine($" Auto-discovered scenario: {scenario.Name}"); + } + } + } + + if (study.RunAllAlternatives) + { + List alternatives = BaseViewModel.StudyCache.GetChildElementsOfType(); + foreach (AlternativeElement alt in alternatives) + { + if (!computations.Any(c => c.Type.Equals("alternative", StringComparison.OrdinalIgnoreCase) + && c.ElementName.Equals(alt.Name, StringComparison.OrdinalIgnoreCase))) + { + computations.Add(new ComputeConfiguration { Type = "alternative", ElementName = alt.Name }); + Console.WriteLine($" Auto-discovered alternative: {alt.Name}"); + } + } + } + + if (study.RunAllAlternativeComparisons) + { + List altCompReports = BaseViewModel.StudyCache.GetChildElementsOfType(); + foreach (AlternativeComparisonReportElement report in altCompReports) + { + if (!computations.Any(c => c.Type.Equals("alternativecomparison", StringComparison.OrdinalIgnoreCase) + && c.ElementName.Equals(report.Name, StringComparison.OrdinalIgnoreCase))) + { + computations.Add(new ComputeConfiguration { Type = "alternativecomparison", ElementName = report.Name }); + Console.WriteLine($" Auto-discovered alternative comparison: {report.Name}"); + } + } + } + + return SortByDependencyOrder(computations); + } + + private static List SortByDependencyOrder(List computations) + { + int GetOrder(string type) => type.ToLowerInvariant() switch + { + "stagedamage" => 0, + "scenario" => 1, + "alternative" => 2, + "alternativecomparison" => 3, + _ => 99 + }; + + return computations.OrderBy(c => GetOrder(c.Type)).ToList(); + } + + private static (AlternativeComparisonReportResults results, List<(int altId, string altName)> withProjectAlternatives) RunAlternativeComparisonWithMetadata(string elementName, CancellationToken cancellationToken) + { + AlternativeComparisonReportElement element = ScenarioRunner.FindElement(elementName); + + List<(int altId, string altName)> withProjectAlternatives = new(); + List allAlternatives = BaseViewModel.StudyCache.GetChildElementsOfType(); + + foreach (int altId in element.WithProjAltIDs) + { + AlternativeElement? alt = allAlternatives.FirstOrDefault(a => a.ID == altId); + string altName = alt?.Name ?? $"Alternative_{altId}"; + withProjectAlternatives.Add((altId, altName)); + } + + AlternativeComparisonReportResults results = AlternativeComparisonRunner.RunAlternativeComparison(elementName, cancellationToken); + return (results, withProjectAlternatives); + } + + private static IASElement SaveScenarioResults(string elementName, ScenarioResults results) + { + IASElement element = ScenarioRunner.FindElement(elementName); + element.Results = results; + PersistenceFactory.GetIASManager().SaveExisting(element); + Console.WriteLine($" Saved to temp database."); + return element; + } + + private static AlternativeElement SaveAlternativeResults(string elementName, AlternativeResults results) + { + AlternativeElement element = ScenarioRunner.FindElement(elementName); + element.Results = results; + PersistenceFactory.GetElementManager().SaveExisting(element); + Console.WriteLine($" Saved to temp database."); + return element; + } + + private static void SaveStageDamageResults(string elementName, List curves) + { + AggregatedStageDamageElement element = ScenarioRunner.FindElement(elementName); + + List impactAreaElements = BaseViewModel.StudyCache.GetChildElementsOfType(); + ImpactAreaElement? impactAreaElement = impactAreaElements.Count > 0 ? impactAreaElements[0] : null; + + List stageDamageCurves = new(); + foreach (UncertainPairedData upd in curves) + { + CurveComponentVM curveComponent = new(StringConstants.STAGE_DAMAGE, StringConstants.STAGE, StringConstants.DAMAGE, DistributionOptions.HISTOGRAM_ONLY); + curveComponent.SetPairedData(upd); + + ImpactAreaRowItem impactAreaRowItem = impactAreaElement?.GetImpactAreaRow(upd.ImpactAreaID) + ?? new ImpactAreaRowItem(upd.ImpactAreaID, ""); + + StageDamageCurve sdCurve = new(impactAreaRowItem, upd.DamageCategory, curveComponent, upd.AssetCategory, StageDamageConstructionType.COMPUTED); + stageDamageCurves.Add(sdCurve); + } + + element.Curves.Clear(); + element.Curves.AddRange(stageDamageCurves); + + PersistenceFactory.GetElementManager().SaveExisting(element); + Console.WriteLine($" Saved {curves.Count} curves to temp database."); + } +} diff --git a/HEC.FDA.TestingUtility/Configuration/TestConfiguration.cs b/HEC.FDA.TestingUtility/Configuration/TestConfiguration.cs new file mode 100644 index 000000000..569a1a9a5 --- /dev/null +++ b/HEC.FDA.TestingUtility/Configuration/TestConfiguration.cs @@ -0,0 +1,74 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace HEC.FDA.TestingUtility.Configuration; + +public class TestConfiguration +{ + [JsonPropertyName("testSuiteId")] + public string TestSuiteId { get; set; } = string.Empty; + + [JsonPropertyName("globalSettings")] + public GlobalSettings GlobalSettings { get; set; } = new(); + + [JsonPropertyName("studies")] + public List Studies { get; set; } = new(); + + public static TestConfiguration LoadFromFile(string path) + { + string json = File.ReadAllText(path); + return JsonSerializer.Deserialize(json) + ?? throw new InvalidOperationException($"Failed to deserialize configuration from {path}"); + } +} + +public class GlobalSettings +{ + [JsonPropertyName("localTempDirectory")] + public string LocalTempDirectory { get; set; } = Path.Combine(Path.GetTempPath(), "FDATests"); + + [JsonPropertyName("timeoutMinutes")] + public int TimeoutMinutes { get; set; } = 120; +} + +public class StudyConfiguration +{ + [JsonPropertyName("studyId")] + public string StudyId { get; set; } = string.Empty; + + [JsonPropertyName("studyName")] + public string StudyName { get; set; } = string.Empty; + + [JsonPropertyName("networkSourcePath")] + public string NetworkSourcePath { get; set; } = string.Empty; + + [JsonPropertyName("baselineDirectory")] + public string BaselineDirectory { get; set; } = string.Empty; + + [JsonPropertyName("runAllScenarios")] + public bool RunAllScenarios { get; set; } = false; + + [JsonPropertyName("runAllAlternatives")] + public bool RunAllAlternatives { get; set; } = false; + + [JsonPropertyName("runAllStageDamage")] + public bool RunAllStageDamage { get; set; } = false; + + [JsonPropertyName("runAllAlternativeComparisons")] + public bool RunAllAlternativeComparisons { get; set; } = false; + + [JsonPropertyName("computations")] + public List Computations { get; set; } = new(); +} + +public class ComputeConfiguration +{ + [JsonPropertyName("type")] + public string Type { get; set; } = string.Empty; + + [JsonPropertyName("elementName")] + public string ElementName { get; set; } = string.Empty; + + [JsonPropertyName("elementId")] + public int? ElementId { get; set; } +} diff --git a/HEC.FDA.TestingUtility/HEC.FDA.TestingUtility.csproj b/HEC.FDA.TestingUtility/HEC.FDA.TestingUtility.csproj new file mode 100644 index 000000000..c18ef01cd --- /dev/null +++ b/HEC.FDA.TestingUtility/HEC.FDA.TestingUtility.csproj @@ -0,0 +1,22 @@ + + + Exe + net9.0-windows + true + enable + enable + + + + + + + + + + + + + + + diff --git a/HEC.FDA.TestingUtility/Program.cs b/HEC.FDA.TestingUtility/Program.cs new file mode 100644 index 000000000..d90653df9 --- /dev/null +++ b/HEC.FDA.TestingUtility/Program.cs @@ -0,0 +1,84 @@ +using System.CommandLine; +using Geospatial.GDALAssist; +using HEC.FDA.TestingUtility.Configuration; + +namespace HEC.FDA.TestingUtility; + +public class Program +{ + public static async Task Main(string[] args) + { + // Initialize GDAL early + GDALSetup.InitializeMultiplatform(); + + // Create root command + RootCommand rootCommand = new("FDA Testing Utility - Regression Testing Tool for FDA Studies"); + + // ============ COMPUTE COMMAND ============ + Command computeCommand = new("compute", "Run computations on FDA studies and generate CSV result reports"); + + Option computeConfigOption = new( + name: "--config", + description: "Path to JSON configuration file") + { IsRequired = true }; + computeConfigOption.AddAlias("-c"); + + Option computeOutputOption = new( + name: "--output", + description: "Output directory for generated files", + getDefaultValue: () => new DirectoryInfo(Environment.CurrentDirectory)); + computeOutputOption.AddAlias("-o"); + + Option computeStudyOption = new( + name: "--study", + description: "Filter to specific study IDs (can specify multiple)") + { AllowMultipleArgumentsPerToken = true }; + computeStudyOption.AddAlias("-s"); + + computeCommand.AddOption(computeConfigOption); + computeCommand.AddOption(computeOutputOption); + computeCommand.AddOption(computeStudyOption); + + computeCommand.SetHandler(async (configFile, outputDir, studyFilter) => + { + try + { + Console.WriteLine("FDA Testing Utility - Compute"); + Console.WriteLine("============================="); + Console.WriteLine(); + + if (!configFile.Exists) + { + Console.WriteLine($"Error: Configuration file not found: {configFile.FullName}"); + return; + } + + Console.WriteLine($"Loading configuration: {configFile.FullName}"); + TestConfiguration config = TestConfiguration.LoadFromFile(configFile.FullName); + + if (!outputDir.Exists) + { + outputDir.Create(); + } + + ComputeRunner runner = new( + config, + outputDir.FullName, + studyFilter?.Length > 0 ? studyFilter : null); + + await runner.RunAsync(); + } + catch (Exception ex) + { + Console.WriteLine($"Fatal error: {ex.Message}"); + Console.WriteLine(ex.StackTrace); + } + }, computeConfigOption, computeOutputOption, computeStudyOption); + + // Add subcommands to root + rootCommand.AddCommand(computeCommand); + + // Run + return await rootCommand.InvokeAsync(args); + } +} \ No newline at end of file diff --git a/HEC.FDA.TestingUtility/README.md b/HEC.FDA.TestingUtility/README.md new file mode 100644 index 000000000..64a81bbfb --- /dev/null +++ b/HEC.FDA.TestingUtility/README.md @@ -0,0 +1,262 @@ +# HEC.FDA.TestingUtility + +A command-line regression testing tool for HEC-FDA studies. It loads FDA study databases, runs computations (stage damage, scenarios, alternatives, and alternative comparisons), and produces a comprehensive CSV report of the results. + +## Purpose + +This utility runs FDA computations outside the GUI so that results can be validated programmatically. Typical use cases include: + +- **Regression testing** - Run computations on known studies and compare the CSV output against baseline results to detect unintended changes. +- **Batch computation** - Execute all computations in one or more studies without manual interaction. +- **CI/CD integration** - Automate computation verification as part of a build pipeline. + +## Quick Start + +### 1. Build the project + +```bash +dotnet build HEC.FDA.TestingUtility/HEC.FDA.TestingUtility.csproj +``` + +### 2. Create a configuration file + +Copy `example-config.json` from this project directory and update `networkSourcePath` to point to your FDA study folder (the folder containing the `.sqlite` or `.db` file): + +```json +{ + "testSuiteId": "quick-start", + "globalSettings": { + "localTempDirectory": "C:/temp/FDATests", + "timeoutMinutes": 60 + }, + "studies": [ + { + "studyId": "my-study", + "studyName": "My Study", + "networkSourcePath": "C:/path/to/my/study/folder", + "runAllStageDamage": true, + "runAllScenarios": true, + "runAllAlternatives": true, + "runAllAlternativeComparisons": true, + "computations": [] + } + ] +} +``` + +Setting the `runAll*` flags to `true` auto-discovers every element of that type in the study, so you don't need to list them individually. If you only want to run specific elements, set the flags to `false` and use `computations` instead: + +```json +{ + "testSuiteId": "targeted-run", + "globalSettings": {}, + "studies": [ + { + "studyId": "my-study", + "studyName": "My Study", + "networkSourcePath": "C:/path/to/my/study/folder", + "runAllStageDamage": false, + "runAllScenarios": false, + "runAllAlternatives": false, + "runAllAlternativeComparisons": false, + "computations": [ + { "type": "stagedamage", "elementName": "My Stage Damage" }, + { "type": "scenario", "elementName": "Existing Conditions" }, + { "type": "alternative", "elementName": "Proposed Levee" } + ] + } + ] +} +``` + +You can list computations in any order -- the utility automatically sorts them by dependency (stage damage first, then scenarios, then alternatives, then alternative comparisons). + +### 3. Run the utility + +```bash +# Run all studies in the config, write results to a "results" folder +dotnet run --project HEC.FDA.TestingUtility -- compute -c my-test.json -o results + +# Run only a single study by its studyId +dotnet run --project HEC.FDA.TestingUtility -- compute -c my-test.json -o results -s "my-study" + +# Filter to multiple studies +dotnet run --project HEC.FDA.TestingUtility -- compute -c my-test.json -s "study-a" -s "study-b" +``` + +### 4. Check the output + +After the run completes, look for `results_report.csv` in the output directory. It contains separate sections for scenario EAD, damage by category, performance metrics, alternative EqAD, stage damage summaries, and alternative comparison results. + +The console output will also show a summary: + +``` +=== Summary === +Completed: 5 +Errors: 0 +Duration: 2m 34.1s + +CSV report saved to: results/results_report.csv +``` + +An exit code of `0` means all computations passed. An exit code of `1` means at least one failed -- scroll up in the console output to find lines marked `ERROR`. + +## Prerequisites + +- .NET 9.0 SDK (Windows, targets `net9.0-windows`) +- Access to FDA study databases (`.sqlite` or `.db` files) located on a network share or local directory +- NuGet sources configured as described in the root `CLAUDE.md` + +## Building + +```bash +dotnet build HEC.FDA.TestingUtility/HEC.FDA.TestingUtility.csproj +``` + +## Usage + +The utility uses the `System.CommandLine` library and exposes a `compute` subcommand: + +```bash +dotnet run --project HEC.FDA.TestingUtility -- compute --config [options] +``` + +### Options + +| Option | Alias | Required | Description | +|---|---|---|---| +| `--config` | `-c` | Yes | Path to a JSON configuration file defining the studies and computations to run. | +| `--output` | `-o` | No | Output directory for generated files. Defaults to the current working directory. | +| `--study` | `-s` | No | Filter to one or more specific study IDs. Can be specified multiple times. | + +### Examples + +Run all studies defined in a config file: + +```bash +dotnet run --project HEC.FDA.TestingUtility -- compute -c tests/regression.json -o results/ +``` + +Run only a specific study: + +```bash +dotnet run --project HEC.FDA.TestingUtility -- compute -c tests/regression.json -s "muncie" +``` + +## Configuration File + +The configuration is a JSON file with the following structure: + +```json +{ + "testSuiteId": "regression-v1", + "globalSettings": { + "localTempDirectory": "C:/temp/FDATests", + "timeoutMinutes": 30 + }, + "studies": [ + { + "studyId": "muncie", + "studyName": "Muncie Indiana", + "networkSourcePath": "\\\\server\\share\\studies\\Muncie", + "runAllStageDamage": true, + "runAllScenarios": true, + "runAllAlternatives": true, + "runAllAlternativeComparisons": true, + "computations": [] + } + ] +} +``` + +### Global Settings + +| Field | Default | Description | +|---|---|---| +| `localTempDirectory` | System temp + `FDATests` | Directory where studies are copied locally before computation. | +| `timeoutMinutes` | `30` | Maximum wall-clock time for all computations before cancellation. | + +### Study Configuration + +| Field | Description | +|---|---| +| `studyId` | Short identifier used in the CSV report and for the `--study` filter. | +| `studyName` | Human-readable name printed during execution. | +| `networkSourcePath` | Path to the study folder containing the `.sqlite` or `.db` database file. | +| `runAllStageDamage` | Auto-discover and run all stage damage elements in the study. | +| `runAllScenarios` | Auto-discover and run all scenario elements. | +| `runAllAlternatives` | Auto-discover and run all alternative elements. | +| `runAllAlternativeComparisons` | Auto-discover and run all alternative comparison report elements. | +| `computations` | Explicit list of computations (see below). Combined with auto-discovered elements. | + +### Computation Entry + +Each entry in the `computations` array targets a specific element: + +```json +{ + "type": "scenario", + "elementName": "Existing Conditions" +} +``` + +Valid `type` values (case-insensitive): + +| Type | Description | +|---|---| +| `stagedamage` | Computes stage-damage curves from hydraulics and inventory data. | +| `scenario` | Runs a Monte Carlo scenario simulation using convergence criteria from study properties. | +| `alternative` | Computes annualized damages for an alternative using its base and future scenario results. | +| `alternativecomparison` | Compares with-project alternatives against a without-project alternative. | + +Computations are automatically sorted in dependency order: stage damage -> scenario -> alternative -> alternative comparison. This means you can list them in any order and the utility will execute them correctly. + +## How It Works + +1. **Study Loading** - The study folder is copied from `networkSourcePath` to a local temp directory to avoid locking network files. The SQLite database is opened and all element types are loaded into the `StudyCache` in dependency order (terrains, impact areas, hydraulics, frequencies, inventories, stage damage, scenarios, alternatives, etc.). + +2. **Computation** - Each computation is dispatched to the appropriate runner: + - `StageDamageRunner` - Builds stage-damage configuration from hydraulics and inventory, then calls `ScenarioStageDamage.Compute()`. + - `ScenarioRunner` - Creates `ImpactAreaScenarioSimulation` objects and runs `Scenario.Compute()` with the study's convergence criteria. + - `AlternativeRunner` - Calls `Alternative.AnnualizationCompute()` using the base/future scenario results and study discount rate / period of analysis. + - `AlternativeComparisonRunner` - Computes alternatives and then runs `AlternativeComparisonReport.ComputeAlternativeComparisonReport()`. + +3. **Result Saving** - Computed results are saved back to the temp study database so that downstream computations (e.g., alternatives depending on scenario results) can access them. + +4. **CSV Report** - A single `results_report.csv` file is written to the output directory containing sections for: + - Scenario results (mean and percentile EAD by impact area) + - Scenario damage by category + - Scenario performance (long-term risk, AEP, assurance) + - Scenario assurance of AEP + - Alternative results (mean and percentile EqAD) + - Alternative damage by category + - Stage damage summary (point counts, stage ranges, median integrals) + - Alternative comparison summary (EqAD reduced, base/future EAD reduced) + - Alternative comparison by damage category + +5. **Cleanup** - When a study finishes, the temp copy is deleted automatically. + +## Project Structure + +``` +HEC.FDA.TestingUtility/ + Program.cs # CLI entry point (System.CommandLine) + ComputeRunner.cs # Orchestrates study loading, computation, and reporting + Configuration/ + TestConfiguration.cs # JSON config deserialization models + Services/ + StudyLoader.cs # Copies study to temp, opens DB, loads all elements + StageDamageRunner.cs # Stage damage computation + ScenarioRunner.cs # Scenario computation + element lookup helpers + AlternativeRunner.cs # Alternative annualization computation + AlternativeComparisonRunner.cs # Alternative comparison report computation + Reporting/ + CsvReportFactory.cs # Builds the multi-section CSV report +``` + +## Exit Codes + +| Code | Meaning | +|---|---| +| `0` | All computations completed successfully. | +| `1` | One or more computations failed, or the configuration was invalid. | diff --git a/HEC.FDA.TestingUtility/Reporting/CsvReportFactory.cs b/HEC.FDA.TestingUtility/Reporting/CsvReportFactory.cs new file mode 100644 index 000000000..167dd832d --- /dev/null +++ b/HEC.FDA.TestingUtility/Reporting/CsvReportFactory.cs @@ -0,0 +1,382 @@ +using System.Text; +using HEC.FDA.Model.metrics; +using HEC.FDA.Model.paireddata; +using HEC.FDA.ViewModel.Alternatives; +using HEC.FDA.ViewModel.Alternatives.Results.BatchCompute; +using HEC.FDA.ViewModel.ImpactAreaScenario; +using HEC.FDA.ViewModel.Results; + +namespace HEC.FDA.TestingUtility.Reporting; + +/// +/// Factory class that produces a comprehensive CSV report containing all tabularly +/// visualized results from FDA computations across all studies. +/// Uses ViewModel row item classes for scenario and alternative results. +/// +public class CsvReportFactory +{ + private readonly List<(string StudyId, IASElement Element)> _scenarioElements = []; + private readonly List<(string StudyId, AlternativeElement Element)> _alternativeElements = []; + private readonly StringBuilder _stageDamageSummary = new(); + private readonly StringBuilder _altComparisonSummary = new(); + private readonly StringBuilder _altComparisonByCategory = new(); + + public CsvReportFactory() + { + WriteStageDamageHeader(); + WriteAltComparisonHeaders(); + } + + private void WriteStageDamageHeader() + { + _stageDamageSummary.AppendLine("Study ID,Element Name,Impact Area ID,Impact Area Name,Damage Category,Asset Category,Point Count,Min Stage,Max Stage, Median Sample Integral"); + } + + private void WriteAltComparisonHeaders() + { + _altComparisonSummary.AppendLine("Study ID,Report Name,With Project Alt,Impact Area ID,Without Proj EqAD,With Proj EqAD,EqAD Reduced,EqAD Reduced 25th Pct,EqAD Reduced 50th Pct,EqAD Reduced 75th Pct,Without Proj Base EAD,With Proj Base EAD,Base EAD Reduced,Without Proj Future EAD,With Proj Future EAD,Future EAD Reduced"); + _altComparisonByCategory.AppendLine("Study ID,Report Name,With Project Alt,Impact Area ID,Damage Category,Asset Category,EqAD Reduced,Base Year EAD Reduced,Future Year EAD Reduced"); + } + + /// + /// Adds a scenario element to be included in the report. + /// Uses ScenarioDamageRowItem, ScenarioDamCatRowItem, and ScenarioPerformanceRowItem for data extraction. + /// + public void AddScenarioResults(string studyId, IASElement element) + { + if (element?.Results == null) return; + _scenarioElements.Add((studyId, element)); + } + + /// + /// Adds an alternative element to be included in the report. + /// Uses AlternativeDamageRowItem and AlternativeDamCatRowItem for data extraction. + /// + public void AddAlternativeResults(string studyId, AlternativeElement element) + { + if (element?.Results == null || element.Results.IsNull) return; + _alternativeElements.Add((studyId, element)); + } + + /// + /// Adds stage damage summary from computed curves to the report. + /// + public void AddStageDamageSummary(string studyId, string elementName, List curves) + { + if (curves == null) return; + + try + { + foreach (var curve in curves) + { + int impactAreaId = curve.ImpactAreaID; + string damCat = curve.DamageCategory ?? ""; + string assetCat = curve.AssetCategory ?? ""; + + int pointCount = curve.Xvals?.Length ?? 0; + double minStage = pointCount > 0 ? curve.Xvals!.Min() : 0; + double maxStage = pointCount > 0 ? curve.Xvals!.Max() : 0; + + PairedData medianCurve = curve.SamplePairedData(.5); + double integral = medianCurve.Integrate(false); + + _stageDamageSummary.AppendLine($"{EscapeCsv(studyId)},{EscapeCsv(elementName)},{impactAreaId},{EscapeCsv(damCat)},{EscapeCsv(assetCat)},{pointCount},{minStage:F2},{maxStage:F2},{integral:F2}"); + } + } + catch (Exception ex) + { + Console.WriteLine($" Warning: Error extracting stage damage for CSV: {ex.Message}"); + } + } + + /// + /// Adds alternative comparison report results to the report. + /// + public void AddAlternativeComparisonResults(string studyId, string reportName, AlternativeComparisonReportResults results, List<(int altId, string altName)> withProjectAlternatives) + { + if (results == null) return; + + try + { + var impactAreaIds = results.GetImpactAreaIDs(); + var damageCategories = results.GetDamageCategories(); + var assetCategories = results.GetAssetCategories(); + + foreach (var (altId, altName) in withProjectAlternatives) + { + foreach (int impactAreaId in impactAreaIds) + { + double withoutProjEqad = results.SampleMeanWithoutProjectEqad(impactAreaId); + double withProjEqad = results.SampleMeanWithProjectEqad(altId, impactAreaId); + double eqadReduced = results.SampleMeanEqadReduced(altId, impactAreaId); + double eqadReduced25 = results.EqadReducedExceededWithProbabilityQ(0.75, altId, impactAreaId); + double eqadReduced50 = results.EqadReducedExceededWithProbabilityQ(0.50, altId, impactAreaId); + double eqadReduced75 = results.EqadReducedExceededWithProbabilityQ(0.25, altId, impactAreaId); + + double withoutProjBaseEad = results.SampleMeanWithoutProjectBaseYearEAD(impactAreaId); + double withProjBaseEad = results.SampleMeanWithProjectBaseYearEAD(altId, impactAreaId); + double baseEadReduced = results.SampleMeanBaseYearEADReduced(altId, impactAreaId); + + double withoutProjFutureEad = results.SampleMeanWithoutProjectFutureYearEAD(impactAreaId); + double withProjFutureEad = results.SampleMeanWithProjectFutureYearEAD(altId, impactAreaId); + double futureEadReduced = results.SampleMeanFutureYearEADReduced(altId, impactAreaId); + + _altComparisonSummary.AppendLine($"{EscapeCsv(studyId)},{EscapeCsv(reportName)},{EscapeCsv(altName)},{impactAreaId},{withoutProjEqad:F2},{withProjEqad:F2},{eqadReduced:F2},{eqadReduced25:F2},{eqadReduced50:F2},{eqadReduced75:F2},{withoutProjBaseEad:F2},{withProjBaseEad:F2},{baseEadReduced:F2},{withoutProjFutureEad:F2},{withProjFutureEad:F2},{futureEadReduced:F2}"); + } + + foreach (int impactAreaId in impactAreaIds) + { + foreach (string damCat in damageCategories) + { + foreach (string assetCat in assetCategories) + { + double eqadReduced = results.SampleMeanEqadReduced(altId, impactAreaId, damCat, assetCat); + double baseEadReduced = results.SampleMeanBaseYearEADReduced(altId, impactAreaId, damCat, assetCat); + double futureEadReduced = results.SampleMeanFutureYearEADReduced(altId, impactAreaId, damCat, assetCat); + + if (eqadReduced != 0 || baseEadReduced != 0 || futureEadReduced != 0) + { + _altComparisonByCategory.AppendLine($"{EscapeCsv(studyId)},{EscapeCsv(reportName)},{EscapeCsv(altName)},{impactAreaId},{EscapeCsv(damCat)},{EscapeCsv(assetCat)},{eqadReduced:F2},{baseEadReduced:F2},{futureEadReduced:F2}"); + } + } + } + } + } + } + catch (Exception ex) + { + Console.WriteLine($" Warning: Error extracting alternative comparison results for CSV: {ex.Message}"); + } + } + + /// + /// Saves the comprehensive report to a CSV file. + /// + public void SaveReport(string outputPath) + { + StringBuilder report = new(); + + report.AppendLine("=== FDA COMPUTATION RESULTS REPORT ==="); + report.AppendLine($"Generated: {DateTime.Now:yyyy-MM-dd HH:mm:ss}"); + report.AppendLine(); + + report.AppendLine("=== SCENARIO RESULTS ==="); + report.Append(BuildScenarioResultsSection()); + report.AppendLine(); + + report.AppendLine("=== SCENARIO DAMAGE BY CATEGORY ==="); + report.Append(BuildScenarioDamCatSection()); + report.AppendLine(); + + report.AppendLine("=== SCENARIO PERFORMANCE ==="); + report.Append(BuildScenarioPerformanceSection()); + report.AppendLine(); + + report.AppendLine("=== SCENARIO ASSURANCE OF AEP ==="); + report.Append(BuildScenarioAssuranceSection()); + report.AppendLine(); + + report.AppendLine("=== ALTERNATIVE RESULTS ==="); + report.Append(BuildAlternativeResultsSection()); + report.AppendLine(); + + report.AppendLine("=== ALTERNATIVE DAMAGE BY CATEGORY ==="); + report.Append(BuildAlternativeDamCatSection()); + report.AppendLine(); + + report.AppendLine("=== STAGE DAMAGE SUMMARY ==="); + report.Append(_stageDamageSummary); + report.AppendLine(); + + report.AppendLine("=== ALTERNATIVE COMPARISON SUMMARY ==="); + report.Append(_altComparisonSummary); + report.AppendLine(); + + report.AppendLine("=== ALTERNATIVE COMPARISON BY CATEGORY ==="); + report.Append(_altComparisonByCategory); + + File.WriteAllText(outputPath, report.ToString()); + Console.WriteLine($"CSV report saved to: {outputPath}"); + } + + #region Scenario Section Builders + + private StringBuilder BuildScenarioResultsSection() + { + StringBuilder sb = new(); + sb.AppendLine("Study ID,Name,Analysis Year,Impact Area,Mean EAD,25th Percentile EAD,50th Percentile EAD,75th Percentile EAD"); + + foreach (var (studyId, element) in _scenarioElements) + { + try + { + var rows = ScenarioDamageRowItem.CreateScenarioDamageRowItems(element); + foreach (var row in rows) + { + sb.AppendLine($"{EscapeCsv(studyId)},{EscapeCsv(row.Name)},{EscapeCsv(row.AnalysisYear)},{EscapeCsv(row.ImpactArea)},{row.Mean:F2},{row.Point25:F2},{row.Point5:F2},{row.Point75:F2}"); + } + } + catch (Exception ex) + { + Console.WriteLine($" Warning: Error extracting scenario results for {element.Name}: {ex.Message}"); + } + } + + return sb; + } + + private StringBuilder BuildScenarioDamCatSection() + { + StringBuilder sb = new(); + sb.AppendLine("Study ID,Scenario Name,Analysis Year,Impact Area,Damage Category,Asset Category,Mean EAD"); + + foreach (var (studyId, element) in _scenarioElements) + { + try + { + var rows = ScenarioDamCatRowItem.CreateScenarioDamCatRowItems(element); + foreach (var row in rows) + { + sb.AppendLine($"{EscapeCsv(studyId)},{EscapeCsv(row.Name)},{EscapeCsv(row.AnalysisYear)},{EscapeCsv(row.ImpactAreaName)},{EscapeCsv(row.DamCat)},{EscapeCsv(row.AssetCat)},{row.MeanDamage:F2}"); + } + } + catch (Exception ex) + { + Console.WriteLine($" Warning: Error extracting scenario damage categories for {element.Name}: {ex.Message}"); + } + } + + return sb; + } + + private StringBuilder BuildScenarioPerformanceSection() + { + StringBuilder sb = new(); + sb.AppendLine("Study ID,Name,Analysis Year,Impact Area,Threshold Type,Threshold Value,LT Risk 10yr,LT Risk 30yr,LT Risk 50yr,Mean AEP,Median AEP,Assurance 0.10,Assurance 0.04,Assurance 0.02,Assurance 0.01,Assurance 0.004,Assurance 0.002"); + + foreach (var (studyId, element) in _scenarioElements) + { + try + { + foreach (var iaResult in element.Results.ResultsList) + { + int iasID = iaResult.ImpactAreaID; + SpecificIAS? ias = element.SpecificIASElements.FirstOrDefault(s => s.ImpactAreaID == iasID); + if (ias == null) continue; + + foreach (var threshold in iaResult.PerformanceByThresholds.ListOfThresholds) + { + var row = new ScenarioPerformanceRowItem(element, ias, threshold); + sb.AppendLine($"{EscapeCsv(studyId)},{EscapeCsv(row.Name)},{EscapeCsv(row.AnalysisYear)},{EscapeCsv(row.ImpactArea)},{EscapeCsv(row.ThresholdType)},{row.ThresholdValue:F2},{row.LongTerm10:F4},{row.LongTerm30:F4},{row.LongTerm50:F4},{row.Mean:F6},{row.Median:F6},{row.Threshold1:F4},{row.Threshold04:F4},{row.Threshold02:F4},{row.Threshold01:F4},{row.Threshold004:F4},{row.Threshold002:F4}"); + } + } + } + catch (Exception ex) + { + Console.WriteLine($" Warning: Error extracting scenario performance for {element.Name}: {ex.Message}"); + } + } + + return sb; + } + + private StringBuilder BuildScenarioAssuranceSection() + { + StringBuilder sb = new(); + sb.AppendLine("Study ID,Name,Analysis Year,Impact Area,Threshold Type,Threshold Value,Mean AEP,Median AEP,90% Assurance AEP,Assurance of 0.10 AEP,Assurance of 0.04 AEP,Assurance of 0.02 AEP,Assurance of 0.01 AEP,Assurance of 0.004 AEP,Assurance of 0.002 AEP"); + + foreach (var (studyId, element) in _scenarioElements) + { + try + { + foreach (var iaResult in element.Results.ResultsList) + { + int iasID = iaResult.ImpactAreaID; + SpecificIAS? ias = element.SpecificIASElements.FirstOrDefault(s => s.ImpactAreaID == iasID); + if (ias == null) continue; + + foreach (var threshold in iaResult.PerformanceByThresholds.ListOfThresholds) + { + var row = new AssuranceOfAEPRowItem(element, ias, threshold); + sb.AppendLine($"{EscapeCsv(studyId)},{EscapeCsv(row.Name)},{EscapeCsv(row.AnalysisYear)},{EscapeCsv(row.ImpactArea)},{EscapeCsv(row.ThresholdType)},{row.ThresholdValue:F2},{row.Mean:F6},{row.Median:F6},{row.NinetyPercentAssurance:F6},{row.AEP1:F4},{row.AEP04:F4},{row.AEP02:F4},{row.AEP01:F4},{row.AEP004:F4},{row.AEP002:F4}"); + } + } + } + catch (Exception ex) + { + Console.WriteLine($" Warning: Error extracting scenario assurance for {element.Name}: {ex.Message}"); + } + } + + return sb; + } + + #endregion + + #region Alternative Section Builders + + private StringBuilder BuildAlternativeResultsSection() + { + StringBuilder sb = new(); + sb.AppendLine("Study ID,Name,Impact Area,Base Year,Future Year,Discount Rate,Period of Analysis,Mean EqAD,25th Percentile EqAD,50th Percentile EqAD,75th Percentile EqAD"); + + foreach (var (studyId, element) in _alternativeElements) + { + try + { + var rows = AlternativeDamageRowItem.CreateAlternativeDamageRowItems(element); + foreach (var row in rows) + { + sb.AppendLine($"{EscapeCsv(studyId)},{EscapeCsv(row.Name)},{EscapeCsv(row.ImpactArea)},{row.BaseYear},{row.FutureYear},{row.DiscountRate:F4},{row.PeriodOfAnalysis},{row.Mean:F2},{row.Point75:F2},{row.Point5:F2},{row.Point25:F2}"); + } + } + catch (Exception ex) + { + Console.WriteLine($" Warning: Error extracting alternative results for {element.Name}: {ex.Message}"); + } + } + + return sb; + } + + private StringBuilder BuildAlternativeDamCatSection() + { + StringBuilder sb = new(); + sb.AppendLine("Study ID,Scenario Name,Impact Area,Damage Category,Asset Category,Mean EqAD"); + + foreach (var (studyId, element) in _alternativeElements) + { + try + { + var rows = AlternativeDamCatRowItem.CreateAlternativeDamCatRowItems(element); + foreach (var row in rows) + { + sb.AppendLine($"{EscapeCsv(studyId)},{EscapeCsv(row.Name)},{EscapeCsv(row.ImpactAreaName)},{EscapeCsv(row.DamCat)},{EscapeCsv(row.AssetCat)},{row.MeanDamage:F2}"); + } + } + catch (Exception ex) + { + Console.WriteLine($" Warning: Error extracting alternative damage categories for {element.Name}: {ex.Message}"); + } + } + + return sb; + } + + #endregion + + /// + /// Escapes a value for CSV output (handles commas and quotes). + /// + private static string EscapeCsv(string value) + { + if (string.IsNullOrEmpty(value)) return ""; + + if (value.Contains(',') || value.Contains('"') || value.Contains('\n')) + { + return $"\"{value.Replace("\"", "\"\"")}\""; + } + + return value; + } +} diff --git a/HEC.FDA.TestingUtility/Services/AlternativeComparisonRunner.cs b/HEC.FDA.TestingUtility/Services/AlternativeComparisonRunner.cs new file mode 100644 index 000000000..3d1cb6be8 --- /dev/null +++ b/HEC.FDA.TestingUtility/Services/AlternativeComparisonRunner.cs @@ -0,0 +1,127 @@ +using HEC.FDA.Model.alternatives; +using HEC.FDA.Model.alternativeComparisonReport; +using HEC.FDA.Model.metrics; +using HEC.FDA.ViewModel; +using HEC.FDA.ViewModel.Alternatives; +using HEC.FDA.ViewModel.AlternativeComparisonReport; +using HEC.FDA.ViewModel.Study; +using HEC.FDA.ViewModel.Utilities; + +namespace HEC.FDA.TestingUtility.Services; + +public static class AlternativeComparisonRunner +{ + public static AlternativeComparisonReportResults RunAlternativeComparison(string elementName, CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(elementName)) + { + throw new ArgumentException("Alternative comparison report element name cannot be empty.", nameof(elementName)); + } + + Console.WriteLine($" Running alternative comparison report '{elementName}'..."); + + var element = ScenarioRunner.FindElement(elementName); + + // Get study properties + StudyPropertiesElement? props = BaseViewModel.StudyCache.GetStudyPropertiesElement(); + if (props == null) + { + throw new InvalidOperationException("Study properties not found."); + } + + // Get the without-project alternative + AlternativeElement? withoutProjAlt = GetAlternativeById(element.WithoutProjAltID); + if (withoutProjAlt == null) + { + throw new InvalidOperationException($"Without-project alternative (ID={element.WithoutProjAltID}) not found."); + } + + // Get all with-project alternatives + List withProjAlts = new(); + foreach (int altId in element.WithProjAltIDs) + { + AlternativeElement? alt = GetAlternativeById(altId); + if (alt == null) + { + throw new InvalidOperationException($"With-project alternative (ID={altId}) not found."); + } + withProjAlts.Add(alt); + } + + if (withProjAlts.Count == 0) + { + throw new InvalidOperationException("No with-project alternatives found for comparison."); + } + + Console.WriteLine($" Without-project alternative: {withoutProjAlt.Name}"); + Console.WriteLine($" With-project alternatives: {string.Join(", ", withProjAlts.Select(a => a.Name))}"); + + // Compute without-project alternative results + Console.WriteLine($" Computing without-project alternative '{withoutProjAlt.Name}'..."); + AlternativeResults withoutProjResults = ComputeAlternativeResults(withoutProjAlt, props, cancellationToken); + + // Compute each with-project alternative results + List withProjResults = new(); + foreach (AlternativeElement withProjAlt in withProjAlts) + { + Console.WriteLine($" Computing with-project alternative '{withProjAlt.Name}'..."); + AlternativeResults results = ComputeAlternativeResults(withProjAlt, props, cancellationToken); + withProjResults.Add(results); + } + + // Compute the comparison report + Console.WriteLine($" Computing alternative comparison report..."); + AlternativeComparisonReportResults? comparisonResults = AlternativeComparisonReport.ComputeAlternativeComparisonReport( + withoutProjResults, + withProjResults); + + if (comparisonResults == null) + { + throw new InvalidOperationException("Alternative comparison report computation failed."); + } + + Console.WriteLine($" Alternative comparison report complete."); + + return comparisonResults; + } + + private static AlternativeElement? GetAlternativeById(int id) + { + var alternatives = BaseViewModel.StudyCache.GetChildElementsOfType(); + return alternatives.FirstOrDefault(a => a.ID == id); + } + + private static AlternativeResults ComputeAlternativeResults(AlternativeElement element, StudyPropertiesElement props, CancellationToken cancellationToken) + { + FdaValidationResult validation = element.RunPreComputeValidation(); + if (!validation.IsValid) + { + throw new InvalidOperationException($"Alternative '{element.Name}' cannot compute: {validation.ErrorMessage}"); + } + + var baseScenarioElement = element.BaseScenario?.GetElement(); + var futureScenarioElement = element.FutureScenario?.GetElement(); + + if (baseScenarioElement?.Results == null) + { + throw new InvalidOperationException($"Base scenario for alternative '{element.Name}' has no computed results."); + } + + if (futureScenarioElement?.Results == null) + { + throw new InvalidOperationException($"Future scenario for alternative '{element.Name}' has no computed results."); + } + + int baseYear = element.BaseScenario?.Year ?? 0; + int futureYear = element.FutureScenario?.Year ?? 0; + + return Alternative.AnnualizationCompute( + props.DiscountRate, + props.PeriodOfAnalysis, + element.ID, + baseScenarioElement.Results, + futureScenarioElement.Results, + baseYear, + futureYear); + } +} diff --git a/HEC.FDA.TestingUtility/Services/AlternativeRunner.cs b/HEC.FDA.TestingUtility/Services/AlternativeRunner.cs new file mode 100644 index 000000000..d236bf380 --- /dev/null +++ b/HEC.FDA.TestingUtility/Services/AlternativeRunner.cs @@ -0,0 +1,82 @@ +using HEC.FDA.Model.alternatives; +using HEC.FDA.Model.metrics; +using HEC.FDA.ViewModel; +using HEC.FDA.ViewModel.Alternatives; +using HEC.FDA.ViewModel.ImpactAreaScenario; +using HEC.FDA.ViewModel.Study; +using HEC.FDA.ViewModel.Utilities; + +namespace HEC.FDA.TestingUtility.Services; + +public static class AlternativeRunner +{ + public static AlternativeResults RunAlternative(string elementName, CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(elementName)) + { + throw new ArgumentException("Alternative element name cannot be empty.", nameof(elementName)); + } + + AlternativeElement element = ScenarioRunner.FindElement(elementName); + + Console.WriteLine($" Running alternative '{elementName}'..."); + + FdaValidationResult validation = element.RunPreComputeValidation(); + if (!validation.IsValid) + { + throw new InvalidOperationException($"Alternative cannot compute: {validation.ErrorMessage}"); + } + + StudyPropertiesElement? props = BaseViewModel.StudyCache.GetStudyPropertiesElement(); + if (props == null) + { + throw new InvalidOperationException("Study properties not found."); + } + + IASElement? baseScenarioElement = element.BaseScenario?.GetElement(); + IASElement? futureScenarioElement = element.FutureScenario?.GetElement(); + + if (baseScenarioElement == null) + { + throw new InvalidOperationException("Base scenario element not found."); + } + + if (futureScenarioElement == null) + { + throw new InvalidOperationException("Future scenario element not found."); + } + + if (baseScenarioElement.Results == null) + { + throw new InvalidOperationException($"Base scenario '{baseScenarioElement.Name}' has no computed results. Run the scenario first."); + } + + if (futureScenarioElement.Results == null) + { + throw new InvalidOperationException($"Future scenario '{futureScenarioElement.Name}' has no computed results. Run the scenario first."); + } + + ScenarioResults baseResults = baseScenarioElement.Results; + ScenarioResults futureResults = futureScenarioElement.Results; + + int baseYear = element.BaseScenario?.Year ?? 0; + int futureYear = element.FutureScenario?.Year ?? 0; + + Console.WriteLine($" Using base scenario: {baseScenarioElement.Name} (Year: {baseYear})"); + Console.WriteLine($" Using future scenario: {futureScenarioElement.Name} (Year: {futureYear})"); + Console.WriteLine($" Discount rate: {props.DiscountRate}, Period of analysis: {props.PeriodOfAnalysis}"); + + AlternativeResults results = Alternative.AnnualizationCompute( + props.DiscountRate, + props.PeriodOfAnalysis, + element.ID, + baseResults, + futureResults, + baseYear, + futureYear); + + Console.WriteLine($" Alternative computation complete."); + + return results; + } +} diff --git a/HEC.FDA.TestingUtility/Services/ScenarioRunner.cs b/HEC.FDA.TestingUtility/Services/ScenarioRunner.cs new file mode 100644 index 000000000..5db4a3e0f --- /dev/null +++ b/HEC.FDA.TestingUtility/Services/ScenarioRunner.cs @@ -0,0 +1,88 @@ +using HEC.FDA.Model.compute; +using HEC.FDA.Model.metrics; +using HEC.FDA.Model.scenarios; +using HEC.FDA.ViewModel; +using HEC.FDA.ViewModel.Compute; +using HEC.FDA.ViewModel.ImpactAreaScenario; +using HEC.FDA.ViewModel.Utilities; +using Statistics; + +namespace HEC.FDA.TestingUtility.Services; + +public static class ScenarioRunner +{ + public static ScenarioResults RunScenario(string elementName, CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(elementName)) + { + throw new ArgumentException("Scenario element name cannot be empty.", nameof(elementName)); + } + + IASElement element = FindElement(elementName); + + Console.WriteLine($" Running scenario '{elementName}'..."); + + FdaValidationResult validation = element.CanCompute(); + if (!validation.IsValid) + { + throw new InvalidOperationException($"Scenario cannot compute: {validation.ErrorMessage}"); + } + + List sims = ComputeScenarioVM.CreateSimulations(element.SpecificIASElements); + if (sims.Count == 0) + { + throw new InvalidOperationException("No simulations could be created for this scenario."); + } + + Scenario scenario = new(sims); + + var studyProps = BaseViewModel.StudyCache.GetStudyPropertiesElement(); + if (studyProps == null) + { + throw new InvalidOperationException("Study properties not found. Cannot retrieve convergence criteria."); + } + + ConvergenceCriteria cc = studyProps.GetStudyConvergenceCriteria(); + Console.WriteLine($" Computing with convergence criteria: min={cc.MinIterations}, max={cc.MaxIterations}"); + + ScenarioResults results = scenario.Compute(cc, cancellationToken, computeIsDeterministic: false); + Console.WriteLine($" Scenario computation complete."); + + return results; + } + + internal static T FindElement(string elementName) where T : ChildElement + { + var elements = BaseViewModel.StudyCache.GetChildElementsOfType(); + var match = elements.FirstOrDefault(e => + e.Name.Equals(elementName, StringComparison.OrdinalIgnoreCase)); + + if (match == null) + { + string availableNames = elements.Count > 0 + ? string.Join(", ", elements.Select(e => e.Name)) + : "(none)"; + throw new InvalidOperationException( + $"Element '{elementName}' of type {typeof(T).Name} not found. Available: {availableNames}"); + } + + return match; + } + + internal static T FindElementById(int id) where T : ChildElement + { + var elements = BaseViewModel.StudyCache.GetChildElementsOfType(); + var match = elements.FirstOrDefault(e => e.ID == id); + + if (match == null) + { + string availableIds = elements.Count > 0 + ? string.Join(", ", elements.Select(e => $"{e.Name}(ID={e.ID})")) + : "(none)"; + throw new InvalidOperationException( + $"Element of type {typeof(T).Name} with ID {id} not found. Available: {availableIds}"); + } + + return match; + } +} diff --git a/HEC.FDA.TestingUtility/Services/StageDamageRunner.cs b/HEC.FDA.TestingUtility/Services/StageDamageRunner.cs new file mode 100644 index 000000000..c7e1c5232 --- /dev/null +++ b/HEC.FDA.TestingUtility/Services/StageDamageRunner.cs @@ -0,0 +1,95 @@ +using HEC.FDA.Model.paireddata; +using HEC.FDA.Model.stageDamage; +using HEC.FDA.ViewModel; +using HEC.FDA.ViewModel.AggregatedStageDamage; +using HEC.FDA.ViewModel.Hydraulics; +using HEC.FDA.ViewModel.Hydraulics.GriddedData; +using HEC.FDA.ViewModel.ImpactArea; +using HEC.FDA.ViewModel.Inventory; +using HEC.FDA.ViewModel.Utilities; + +namespace HEC.FDA.TestingUtility.Services; + +public static class StageDamageRunner +{ + public static List RunStageDamage(string elementName) + { + if (string.IsNullOrWhiteSpace(elementName)) + { + throw new ArgumentException("Stage damage element name cannot be empty.", nameof(elementName)); + } + + Console.WriteLine($" Running stage damage compute for '{elementName}'..."); + + AggregatedStageDamageElement element = ScenarioRunner.FindElement(elementName); + + if (element.IsManual) + { + Console.WriteLine($" Stage damage '{elementName}' is manual - returning existing curves."); + return ConvertCurvesToUPD(element.Curves); + } + + var impactAreaElements = BaseViewModel.StudyCache.GetChildElementsOfType(); + if (impactAreaElements.Count == 0) + { + throw new InvalidOperationException("No impact area element found in study."); + } + ImpactAreaElement impactAreaElement = impactAreaElements[0]; + + HydraulicElement hydraulicElement = ScenarioRunner.FindElementById(element.SelectedWSE); + InventoryElement inventoryElement = ScenarioRunner.FindElementById(element.SelectedStructures); + + Console.WriteLine($" Using hydraulics: {hydraulicElement.Name}"); + Console.WriteLine($" Using inventory: {inventoryElement.Name}"); + Console.WriteLine($" Analysis year: {element.AnalysisYear}"); + + StageDamageConfiguration config = new( + impactAreaElement, + hydraulicElement, + inventoryElement, + element.ImpactAreaFrequencyRows, + element.AnalysisYear); + + FdaValidationResult validation = config.ValidateConfiguration(); + if (!validation.IsValid) + { + throw new InvalidOperationException($"Stage damage configuration is invalid: {validation.ErrorMessage}"); + } + + List impactAreaStageDamages = config.CreateStageDamages(); + if (impactAreaStageDamages.Count == 0) + { + throw new InvalidOperationException("No impact area stage damages could be created."); + } + + ScenarioStageDamage scenarioStageDamage = new(impactAreaStageDamages); + + int totalStructureCount = impactAreaStageDamages.Sum(sd => sd.Inventory?.Structures?.Count ?? 0); + if (totalStructureCount == 0) + { + throw new InvalidOperationException("No structures found in any impact area for stage damage compute."); + } + + Console.WriteLine($" Computing stage damage with {totalStructureCount} structures..."); + + (List stageDamageFunctions, _) = scenarioStageDamage.Compute(); + + Console.WriteLine($" Stage damage computation complete. Generated {stageDamageFunctions.Count} curves."); + + return stageDamageFunctions; + } + + private static List ConvertCurvesToUPD(List curves) + { + List result = new(); + if (curves == null) return result; + + foreach (var curve in curves) + { + if (curve?.ComputeComponent == null) continue; + UncertainPairedData upd = curve.ComputeComponent.SelectedItemToPairedData(); + result.Add(upd); + } + return result; + } +} diff --git a/HEC.FDA.TestingUtility/Services/StudyLoader.cs b/HEC.FDA.TestingUtility/Services/StudyLoader.cs new file mode 100644 index 000000000..e8b5300b4 --- /dev/null +++ b/HEC.FDA.TestingUtility/Services/StudyLoader.cs @@ -0,0 +1,169 @@ +using System.Data.SQLite; +using HEC.FDA.TestingUtility.Configuration; +using HEC.FDA.ViewModel; +using HEC.FDA.ViewModel.AggregatedStageDamage; +using HEC.FDA.ViewModel.Alternatives; +using HEC.FDA.ViewModel.FlowTransforms; +using HEC.FDA.ViewModel.FrequencyRelationships; +using HEC.FDA.ViewModel.GeoTech; +using HEC.FDA.ViewModel.Hydraulics; +using HEC.FDA.ViewModel.Hydraulics.GriddedData; +using HEC.FDA.ViewModel.ImpactArea; +using HEC.FDA.ViewModel.ImpactAreaScenario; +using HEC.FDA.ViewModel.Inventory; +using HEC.FDA.ViewModel.Inventory.OccupancyTypes; +using HEC.FDA.ViewModel.Saving; +using HEC.FDA.ViewModel.StageTransforms; +using HEC.FDA.ViewModel.Storage; +using HEC.FDA.ViewModel.Study; +using HEC.FDA.ViewModel.Utilities; +using HEC.FDA.ViewModel.Watershed; + +namespace HEC.FDA.TestingUtility.Services; + +public class StudyLoader : IDisposable +{ + private string? _localStudyPath; + private bool _disposed; + + public void LoadStudy(string networkSourcePath, string localTempDirectory) + { + // 1. Copy study folder from network to local temp directory + _localStudyPath = CopyStudyToLocal(networkSourcePath, localTempDirectory); + + // 2. Find the database file + string dbPath = FindDatabaseFile(_localStudyPath); + + // 3. Set up connection (triggers folder structure creation) + Connection.Instance.ProjectFile = dbPath; + + // 4. Create and assign cache + FDACache cache = new(); + BaseViewModel.StudyCache = cache; + PersistenceFactory.StudyCacheForSaving = cache; + + // 5. Load elements in dependency order + LoadAllElements(); + } + + private static string CopyStudyToLocal(string networkPath, string localTempDir) + { + string studyFolderName = Path.GetFileName(networkPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)); + string localPath = Path.Combine(localTempDir, studyFolderName + "_" + Guid.NewGuid().ToString("N")[..8]); + + Console.WriteLine($" Copying study from {networkPath} to {localPath}..."); + + CopyDirectory(networkPath, localPath); + + Console.WriteLine($" Study copied successfully."); + return localPath; + } + + private static void CopyDirectory(string sourceDir, string destDir) + { + Directory.CreateDirectory(destDir); + + foreach (string file in Directory.GetFiles(sourceDir)) + { + string destFile = Path.Combine(destDir, Path.GetFileName(file)); + File.Copy(file, destFile, overwrite: true); + } + + foreach (string subDir in Directory.GetDirectories(sourceDir)) + { + string destSubDir = Path.Combine(destDir, Path.GetFileName(subDir)); + CopyDirectory(subDir, destSubDir); + } + } + + private static string FindDatabaseFile(string studyPath) + { + // Look for .sqlite or .db files + string[] dbExtensions = { "*.sqlite", "*.db" }; + + foreach (string pattern in dbExtensions) + { + string[] files = Directory.GetFiles(studyPath, pattern); + if (files.Length > 0) + { + return files[0]; + } + } + + throw new FileNotFoundException($"No database file (.sqlite or .db) found in study folder: {studyPath}"); + } + + private static void LoadAllElements() + { + // Order matters - dependencies first + Console.WriteLine(" Loading study elements..."); + + LoadElementType("Terrains"); + LoadElementType("Impact Areas"); + LoadElementType("Hydraulics"); + LoadElementType("Frequency Relationships"); + LoadElementType("Inflow-Outflow"); + LoadElementType("Stage-Discharge"); + LoadElementType("Exterior-Interior"); + LoadElementType("Lateral Structures"); + LoadElementType("Occupancy Types"); + LoadElementType("Inventories"); + LoadElementType("Stage Damage"); + LoadElementType("Scenarios"); + LoadElementType("Alternatives"); + LoadElementType("Study Properties"); + + Console.WriteLine(" All elements loaded."); + } + + private static void LoadElementType(string displayName) where T : ChildElement + { + try + { + PersistenceFactory.GetElementManager().Load(); + } + catch (Exception ex) + { + Console.WriteLine($" Warning: Failed to load {displayName}: {ex.Message}"); + } + } + + public void Cleanup() + { + if (_localStudyPath != null && Directory.Exists(_localStudyPath)) + { + try + { + // Close the connection first + if (!Connection.Instance.IsConnectionNull && Connection.Instance.IsOpen) + { + Connection.Instance.Close(); + } + + // Clear SQLite connection pool to release file handles + SQLiteConnection.ClearAllPools(); + + // Force garbage collection to release any remaining handles + GC.Collect(); + GC.WaitForPendingFinalizers(); + + Directory.Delete(_localStudyPath, recursive: true); + Console.WriteLine($" Cleaned up temp study folder: {_localStudyPath}"); + } + catch (Exception ex) + { + Console.WriteLine($" Warning: Failed to cleanup temp folder: {ex.Message}"); + } + } + } + + public void Dispose() + { + if (!_disposed) + { + Cleanup(); + _disposed = true; + } + GC.SuppressFinalize(this); + } +} diff --git a/HEC.FDA.TestingUtility/example-config.json b/HEC.FDA.TestingUtility/example-config.json new file mode 100644 index 000000000..88e461a27 --- /dev/null +++ b/HEC.FDA.TestingUtility/example-config.json @@ -0,0 +1,19 @@ +{ + "testSuiteId": "quick-start", + "globalSettings": { + "localTempDirectory": "C:/temp/FDATests", + "timeoutMinutes": 60 + }, + "studies": [ + { + "studyId": "my-study", + "studyName": "My Study", + "networkSourcePath": "C:/path/to/my/study/folder", + "runAllStageDamage": true, + "runAllScenarios": true, + "runAllAlternatives": true, + "runAllAlternativeComparisons": true, + "computations": [] + } + ] +} diff --git a/HEC.FDA.ViewModel/HEC.FDA.ViewModel.csproj b/HEC.FDA.ViewModel/HEC.FDA.ViewModel.csproj index e3c9b6b5c..ab8d8b21f 100644 --- a/HEC.FDA.ViewModel/HEC.FDA.ViewModel.csproj +++ b/HEC.FDA.ViewModel/HEC.FDA.ViewModel.csproj @@ -5,6 +5,10 @@ true HEC.FDA.ViewModel + + + + diff --git a/HEC.FDA.ViewModel/Results/AssuranceOfAEPRowItem.cs b/HEC.FDA.ViewModel/Results/AssuranceOfAEPRowItem.cs index def0e2c99..68da345db 100644 --- a/HEC.FDA.ViewModel/Results/AssuranceOfAEPRowItem.cs +++ b/HEC.FDA.ViewModel/Results/AssuranceOfAEPRowItem.cs @@ -1,25 +1,40 @@ using HEC.FDA.Model.metrics; using HEC.FDA.ViewModel.ImpactAreaScenario; +using HEC.FDA.ViewModel.TableWithPlot.Rows.Attributes; namespace HEC.FDA.ViewModel.Results { public class AssuranceOfAEPRowItem { + [DisplayAsColumn("Name")] public string Name { get; set; } + [DisplayAsColumn("Analysis Year")] public string AnalysisYear { get; set; } + [DisplayAsColumn("Impact Area")] public string ImpactArea { get; set; } + [DisplayAsColumn("Threshold Type")] public string ThresholdType { get; set; } + [DisplayAsColumn("Threshold Value")] public double ThresholdValue { get; set; } + [DisplayAsColumn("Mean AEP")] public double Mean { get; set; } + [DisplayAsColumn("Median AEP")] public double Median { get; set; } + [DisplayAsColumn("90% Assurance AEP")] public double NinetyPercentAssurance { get; set; } + [DisplayAsColumn("Assurance of 0.10 AEP")] public double AEP1 { get; set; } + [DisplayAsColumn("Assurance of 0.04 AEP")] public double AEP04 { get; set; } + [DisplayAsColumn("Assurance of 0.02 AEP")] public double AEP02 { get; set; } + [DisplayAsColumn("Assurance of 0.01 AEP")] public double AEP01 { get; set; } + [DisplayAsColumn("Assurance of 0.004 AEP")] public double AEP004 { get; set; } + [DisplayAsColumn("Assurance of 0.002 AEP")] public double AEP002 { get; set; } public AssuranceOfAEPRowItem(IASElement scenario, SpecificIAS ias, Threshold threshold) diff --git a/HEC.FDA.ViewModel/Results/ScenarioPerformanceRowItem.cs b/HEC.FDA.ViewModel/Results/ScenarioPerformanceRowItem.cs index 3f8a389ec..cb10dbcba 100644 --- a/HEC.FDA.ViewModel/Results/ScenarioPerformanceRowItem.cs +++ b/HEC.FDA.ViewModel/Results/ScenarioPerformanceRowItem.cs @@ -1,29 +1,46 @@ using HEC.FDA.Model.metrics; using HEC.FDA.ViewModel.ImpactAreaScenario; +using HEC.FDA.ViewModel.TableWithPlot.Rows.Attributes; namespace HEC.FDA.ViewModel.Results { public class ScenarioPerformanceRowItem { + [DisplayAsColumn("Name")] public string Name { get; set; } + [DisplayAsColumn("Analysis Year")] public string AnalysisYear { get; set; } + [DisplayAsColumn("Impact Area")] public string ImpactArea { get; set; } + [DisplayAsColumn("Threshold Type")] public string ThresholdType { get; set; } + [DisplayAsColumn("Threshold Value")] public double ThresholdValue { get; set; } + [DisplayAsColumn("LT Risk 10yr")] public double LongTerm10 { get; set; } + [DisplayAsColumn("LT Risk 30yr")] public double LongTerm30 { get; set; } + [DisplayAsColumn("LT Risk 50yr")] public double LongTerm50 { get; set; } + [DisplayAsColumn("Mean AEP")] public double Mean { get; set; } + [DisplayAsColumn("Median AEP")] public double Median { get; set; } + [DisplayAsColumn("Assurance 0.10")] public double Threshold1 { get; set; } + [DisplayAsColumn("Assurance 0.04")] public double Threshold04 { get; set; } + [DisplayAsColumn("Assurance 0.02")] public double Threshold02 { get; set; } + [DisplayAsColumn("Assurance 0.01")] public double Threshold01 { get; set; } + [DisplayAsColumn("Assurance 0.004")] public double Threshold004 { get; set; } + [DisplayAsColumn("Assurance 0.002")] public double Threshold002 { get; set; } public ScenarioPerformanceRowItem(IASElement scenario, SpecificIAS ias, Threshold threshold) diff --git a/ScratchSpace/Beam/ValidationAndVerification.cs b/ScratchSpace/Beam/ValidationAndVerification.cs new file mode 100644 index 000000000..8959fb9eb --- /dev/null +++ b/ScratchSpace/Beam/ValidationAndVerification.cs @@ -0,0 +1,20 @@ +using System; +using System.IO; +using System.Threading.Tasks; + +namespace ScratchSpace.Beam; + +public static class ValidationAndVerification +{ + private static readonly string ConfigPath = Path.GetFullPath( + Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "Beam", "west-sac-config.json")); + + private static readonly string OutputPath = Path.GetFullPath( + Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "Beam", "output")); + + public static async Task RunValidationAndVerificationReport() + { + string[] args = ["compute", "-c", ConfigPath, "-o", OutputPath]; + return await HEC.FDA.TestingUtility.Program.Main(args); + } +} diff --git a/ScratchSpace/Beam/output/results_report.csv b/ScratchSpace/Beam/output/results_report.csv new file mode 100644 index 000000000..01b7340c6 --- /dev/null +++ b/ScratchSpace/Beam/output/results_report.csv @@ -0,0 +1,64 @@ +=== FDA COMPUTATION RESULTS REPORT === +Generated: 2026-03-23 11:45:55 + +=== SCENARIO RESULTS === +Study ID,Name,Analysis Year,Impact Area,Mean EAD,25th Percentile EAD,50th Percentile EAD,75th Percentile EAD +west-sac-mixed,WOP,2025,Sole,1905043.32,1332887.55,1804764.20,2373226.31 + +=== SCENARIO DAMAGE BY CATEGORY === +Study ID,Scenario Name,Analysis Year,Impact Area,Damage Category,Asset Category,Mean EAD +west-sac-mixed,WOP,2025,Sole,RES,Structure,171487.20 +west-sac-mixed,WOP,2025,Sole,RES,Content,110842.82 +west-sac-mixed,WOP,2025,Sole,RES,Other,0.00 +west-sac-mixed,WOP,2025,Sole,RES,Vehicle,0.00 +west-sac-mixed,WOP,2025,Sole,COM,Structure,351976.86 +west-sac-mixed,WOP,2025,Sole,COM,Content,767616.45 +west-sac-mixed,WOP,2025,Sole,COM,Other,0.00 +west-sac-mixed,WOP,2025,Sole,COM,Vehicle,0.00 +west-sac-mixed,WOP,2025,Sole,IND,Structure,93064.37 +west-sac-mixed,WOP,2025,Sole,IND,Content,383460.04 +west-sac-mixed,WOP,2025,Sole,IND,Other,0.00 +west-sac-mixed,WOP,2025,Sole,IND,Vehicle,0.00 +west-sac-mixed,WOP,2025,Sole,PUB,Structure,14586.62 +west-sac-mixed,WOP,2025,Sole,PUB,Content,12008.96 +west-sac-mixed,WOP,2025,Sole,PUB,Other,0.00 +west-sac-mixed,WOP,2025,Sole,PUB,Vehicle,0.00 + +=== SCENARIO PERFORMANCE === +Study ID,Name,Analysis Year,Impact Area,Threshold Type,Threshold Value,LT Risk 10yr,LT Risk 30yr,LT Risk 50yr,Mean AEP,Median AEP,Assurance 0.10,Assurance 0.04,Assurance 0.02,Assurance 0.01,Assurance 0.004,Assurance 0.002 +west-sac-mixed,WOP,2025,Sole,Default Exterior Stage,12.50,1.0000,1.0000,1.0000,0.998995,0.999097,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000 + +=== SCENARIO ASSURANCE OF AEP === +Study ID,Name,Analysis Year,Impact Area,Threshold Type,Threshold Value,Mean AEP,Median AEP,90% Assurance AEP,Assurance of 0.10 AEP,Assurance of 0.04 AEP,Assurance of 0.02 AEP,Assurance of 0.01 AEP,Assurance of 0.004 AEP,Assurance of 0.002 AEP +west-sac-mixed,WOP,2025,Sole,Default Exterior Stage,12.50,0.998995,0.999097,0.999179,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000 + +=== ALTERNATIVE RESULTS === +Study ID,Name,Impact Area,Base Year,Future Year,Discount Rate,Period of Analysis,Mean EqAD,25th Percentile EqAD,50th Percentile EqAD,75th Percentile EqAD + +=== ALTERNATIVE DAMAGE BY CATEGORY === +Study ID,Scenario Name,Impact Area,Damage Category,Asset Category,Mean EqAD + +=== STAGE DAMAGE SUMMARY === +Study ID,Element Name,Impact Area ID,Impact Area Name,Damage Category,Asset Category,Point Count,Min Stage,Max Stage, Median Sample Integral +west-sac-mixed,WOP,0,RES,Structure,124,11.33,41.57,19532836.97 +west-sac-mixed,WOP,0,RES,Content,124,11.33,41.57,11072203.33 +west-sac-mixed,WOP,0,RES,Other,124,11.33,41.57,0.00 +west-sac-mixed,WOP,0,RES,Vehicle,124,11.33,41.57,0.00 +west-sac-mixed,WOP,0,COM,Structure,124,11.33,41.57,19349898.65 +west-sac-mixed,WOP,0,COM,Content,124,11.33,41.57,35406768.31 +west-sac-mixed,WOP,0,COM,Other,124,11.33,41.57,0.00 +west-sac-mixed,WOP,0,COM,Vehicle,124,11.33,41.57,0.00 +west-sac-mixed,WOP,0,IND,Structure,124,11.33,41.57,4336490.28 +west-sac-mixed,WOP,0,IND,Content,124,11.33,41.57,16141781.26 +west-sac-mixed,WOP,0,IND,Other,124,11.33,41.57,0.00 +west-sac-mixed,WOP,0,IND,Vehicle,124,11.33,41.57,0.00 +west-sac-mixed,WOP,0,PUB,Structure,124,11.33,41.57,943265.66 +west-sac-mixed,WOP,0,PUB,Content,124,11.33,41.57,682918.17 +west-sac-mixed,WOP,0,PUB,Other,124,11.33,41.57,0.00 +west-sac-mixed,WOP,0,PUB,Vehicle,124,11.33,41.57,0.00 + +=== ALTERNATIVE COMPARISON SUMMARY === +Study ID,Report Name,With Project Alt,Impact Area ID,Without Proj EqAD,With Proj EqAD,EqAD Reduced,EqAD Reduced 25th Pct,EqAD Reduced 50th Pct,EqAD Reduced 75th Pct,Without Proj Base EAD,With Proj Base EAD,Base EAD Reduced,Without Proj Future EAD,With Proj Future EAD,Future EAD Reduced + +=== ALTERNATIVE COMPARISON BY CATEGORY === +Study ID,Report Name,With Project Alt,Impact Area ID,Damage Category,Asset Category,EqAD Reduced,Base Year EAD Reduced,Future Year EAD Reduced diff --git a/ScratchSpace/Beam/west-sac-config.json b/ScratchSpace/Beam/west-sac-config.json new file mode 100644 index 000000000..b9f9a8fc4 --- /dev/null +++ b/ScratchSpace/Beam/west-sac-config.json @@ -0,0 +1,19 @@ +{ + "testSuiteId": "west-sac-mixed", + "globalSettings": { + "localTempDirectory": "C:/temp/FDATests", + "timeoutMinutes": 60 + }, + "studies": [ + { + "studyId": "west-sac-mixed", + "studyName": "West Sac Mixed", + "networkSourcePath": "C:/nexus_share/_HEYDUMMY/2.0.2", + "runAllStageDamage": true, + "runAllScenarios": true, + "runAllAlternatives": true, + "runAllAlternativeComparisons": true, + "computations": [] + } + ] +} diff --git a/ScratchSpace/EntryPoints/Beam.cs b/ScratchSpace/EntryPoints/Beam.cs index 6a5c61733..acbb44317 100644 --- a/ScratchSpace/EntryPoints/Beam.cs +++ b/ScratchSpace/EntryPoints/Beam.cs @@ -33,6 +33,7 @@ using Statistics.Histograms; using ScottPlot; using HEC.FDA.Model.utilities; +using ScratchSpace.Beam; namespace ScratchSpace.EntryPoints; @@ -44,9 +45,9 @@ public static class Beam private static readonly string STUDY_PATH = @"C:\Users\HEC\Projects\AlaiWai2\AlaiWai2\AlaiWai2.sqlite"; private static readonly string SCENARIO_NAME = "FWOP"; // Leave empty to compute all scenarios, or set a specific name - public static void EntryPoint() + public static async void EntryPoint() { - RunScenarioCompute(); + int success = await ValidationAndVerification.RunValidationAndVerificationReport(); } /// diff --git a/ScratchSpace/ScratchSpace.csproj b/ScratchSpace/ScratchSpace.csproj index 6d8d008c4..a1e196057 100644 --- a/ScratchSpace/ScratchSpace.csproj +++ b/ScratchSpace/ScratchSpace.csproj @@ -18,6 +18,7 @@ +