Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions HEC.FDA.Model/metrics/ScenarioResults.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using HEC.FDA.Model.metrics.Extensions;
using HEC.FDA.Model.paireddata;
using HEC.MVVMFramework.Base.Events;
using HEC.MVVMFramework.Base.Implementations;
using HEC.MVVMFramework.Model.Messaging;
Expand Down Expand Up @@ -187,6 +188,50 @@ public Empirical GetConsequencesDistribution(int impactAreaID = utilities.Intege
return Empirical.StackEmpiricalDistributions(empiricalDistsToStack, Empirical.Sum);
}
}
/// <summary>
/// Accumulates life loss F-N curve data across all impact areas by summing
/// the DynamicHistogram distributions at each AEP ordinate.
/// Returns null if no life loss F-N data exists or if Xvals don't match across impact areas.
/// </summary>
public UncertainPairedData GetAccumulatedLifeLossFnCurveData()
{
List<UncertainPairedData> lifeLossUPDs = new();
foreach (ImpactAreaScenarioResults iaResult in ResultsList)
{
CategoriedUncertainPairedData curve = iaResult.UncertainConsequenceFrequencyCurves
.FirstOrDefault(c => c.ConsequenceType == ConsequenceType.LifeLoss);
if (curve != null && curve.YHistograms != null && curve.YHistograms.Count > 0)
{
lifeLossUPDs.Add(curve.GetUncertainPairedData());
}
}

if (lifeLossUPDs.Count <= 1) return null;

double[] referenceXvals = lifeLossUPDs[0].Xvals;
for (int i = 1; i < lifeLossUPDs.Count; i++)
{
if (!lifeLossUPDs[i].Xvals.SequenceEqual(referenceXvals))
{
return null;
}
}

Empirical[] stackedEmpiricals = new Empirical[referenceXvals.Length];
for (int i = 0; i < referenceXvals.Length; i++)
{
List<Empirical> empiricalsAtOrdinate = new();
foreach (UncertainPairedData upd in lifeLossUPDs)
{
DynamicHistogram histogram = (DynamicHistogram)upd.Yvals[i];
empiricalsAtOrdinate.Add(DynamicHistogram.ConvertToEmpiricalDistribution(histogram));
}
stackedEmpiricals[i] = Empirical.StackEmpiricalDistributions(empiricalsAtOrdinate, Empirical.Sum);
}

return new UncertainPairedData(referenceXvals, stackedEmpiricals, new CurveMetaData());
}

public void AddResults(ImpactAreaScenarioResults resultsToAdd)
{
ResultsList.Add(resultsToAdd);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -505,5 +505,220 @@ public void ImpactAreaScenarioResultsSerializesAndDeserializesCurvesCorrectly()
// Verify median values are reasonable (should be around 1.0 + (batchSize-1)/2 * 0.1 = ~5.95 for first position)
Assert.True(median.Yvals[0] > 0, "Median should have positive values");
}

[Fact]
public void AccumulateLifeLossFnCurveAcrossImpactAreas()
{
// Arrange: create two impact areas with known life loss F-N curves
int batchSize = 100;
ConvergenceCriteria cc = new(minIterations: batchSize, maxIterations: batchSize * 10);

// Impact area 1: life loss values ~[10, 20, 30, 40, 50] at each AEP
ImpactAreaScenarioResults ia1 = new(1);
CategoriedUncertainPairedData curve1 = ia1.GetOrCreateUncertainConsequenceFrequencyCurve(
TestXvals, "LifeLoss", "LifeLoss", ConsequenceType.LifeLoss, RiskType.Fail, cc);
for (int i = 0; i < batchSize; i++)
{
double[] yvals = { 10, 20, 30, 40, 50 };
curve1.AddCurveRealization(new PairedData(TestXvals, yvals), i);
}
ia1.PutUncertainFrequencyCurvesIntoHistograms();

// Impact area 2: life loss values ~[5, 10, 15, 20, 25] at each AEP
ImpactAreaScenarioResults ia2 = new(2);
CategoriedUncertainPairedData curve2 = ia2.GetOrCreateUncertainConsequenceFrequencyCurve(
TestXvals, "LifeLoss", "LifeLoss", ConsequenceType.LifeLoss, RiskType.Fail, cc);
for (int i = 0; i < batchSize; i++)
{
double[] yvals = { 5, 10, 15, 20, 25 };
curve2.AddCurveRealization(new PairedData(TestXvals, yvals), i);
}
ia2.PutUncertainFrequencyCurvesIntoHistograms();

ScenarioResults scenarioResults = new();
scenarioResults.AddResults(ia1);
scenarioResults.AddResults(ia2);

// Act
var accumulated = scenarioResults.GetAccumulatedLifeLossFnCurveData();

// Assert
Assert.NotNull(accumulated);
Assert.Equal(TestXvals.Length, accumulated.Xvals.Length);
Assert.True(accumulated.Xvals.SequenceEqual(TestXvals));

// The accumulated median should approximate the sum: [15, 30, 45, 60, 75]
PairedData median = accumulated.SamplePairedData(0.5);
Assert.Equal(TestXvals.Length, median.Yvals.Count);

double tolerance = 2.0;
Assert.InRange(median.Yvals[0], 15 - tolerance, 15 + tolerance);
Assert.InRange(median.Yvals[1], 30 - tolerance, 30 + tolerance);
Assert.InRange(median.Yvals[2], 45 - tolerance, 45 + tolerance);
Assert.InRange(median.Yvals[3], 60 - tolerance, 60 + tolerance);
Assert.InRange(median.Yvals[4], 75 - tolerance, 75 + tolerance);
}

[Fact]
public void ReturnNullWhenSingleImpactArea()
{
int batchSize = 100;
ConvergenceCriteria cc = new(minIterations: batchSize, maxIterations: batchSize * 10);

ImpactAreaScenarioResults ia1 = new(1);
CategoriedUncertainPairedData curve1 = ia1.GetOrCreateUncertainConsequenceFrequencyCurve(
TestXvals, "LifeLoss", "LifeLoss", ConsequenceType.LifeLoss, RiskType.Fail, cc);
for (int i = 0; i < batchSize; i++)
{
curve1.AddCurveRealization(new PairedData(TestXvals, new double[] { 10, 20, 30, 40, 50 }), i);
}
ia1.PutUncertainFrequencyCurvesIntoHistograms();

ScenarioResults scenarioResults = new();
scenarioResults.AddResults(ia1);

var accumulated = scenarioResults.GetAccumulatedLifeLossFnCurveData();
Assert.Null(accumulated);
}

[Fact]
public void ReturnNullWhenNoLifeLossData()
{
ImpactAreaScenarioResults ia1 = new(1);
ImpactAreaScenarioResults ia2 = new(2);

ScenarioResults scenarioResults = new();
scenarioResults.AddResults(ia1);
scenarioResults.AddResults(ia2);

var accumulated = scenarioResults.GetAccumulatedLifeLossFnCurveData();
Assert.Null(accumulated);
}

[Fact]
public void ReturnNullWhenXvalsMismatch()
{
int batchSize = 100;
ConvergenceCriteria cc = new(minIterations: batchSize, maxIterations: batchSize * 10);

double[] xvals1 = { 0.01, 0.1, 0.5, 0.9, 0.99 };
double[] xvals2 = { 0.02, 0.2, 0.5, 0.8, 0.98 };

ImpactAreaScenarioResults ia1 = new(1);
CategoriedUncertainPairedData curve1 = ia1.GetOrCreateUncertainConsequenceFrequencyCurve(
xvals1, "LifeLoss", "LifeLoss", ConsequenceType.LifeLoss, RiskType.Fail, cc);
for (int i = 0; i < batchSize; i++)
{
curve1.AddCurveRealization(new PairedData(xvals1, new double[] { 10, 20, 30, 40, 50 }), i);
}
ia1.PutUncertainFrequencyCurvesIntoHistograms();

ImpactAreaScenarioResults ia2 = new(2);
CategoriedUncertainPairedData curve2 = ia2.GetOrCreateUncertainConsequenceFrequencyCurve(
xvals2, "LifeLoss", "LifeLoss", ConsequenceType.LifeLoss, RiskType.Fail, cc);
for (int i = 0; i < batchSize; i++)
{
curve2.AddCurveRealization(new PairedData(xvals2, new double[] { 5, 10, 15, 20, 25 }), i);
}
ia2.PutUncertainFrequencyCurvesIntoHistograms();

ScenarioResults scenarioResults = new();
scenarioResults.AddResults(ia1);
scenarioResults.AddResults(ia2);

var accumulated = scenarioResults.GetAccumulatedLifeLossFnCurveData();
Assert.Null(accumulated);
}

[Fact]
public void AccumulateLifeLossFnCurveAcrossThreeImpactAreas()
{
int batchSize = 100;
ConvergenceCriteria cc = new(minIterations: batchSize, maxIterations: batchSize * 10);

ImpactAreaScenarioResults ia1 = new(1);
CategoriedUncertainPairedData curve1 = ia1.GetOrCreateUncertainConsequenceFrequencyCurve(
TestXvals, "LifeLoss", "LifeLoss", ConsequenceType.LifeLoss, RiskType.Fail, cc);

ImpactAreaScenarioResults ia2 = new(2);
CategoriedUncertainPairedData curve2 = ia2.GetOrCreateUncertainConsequenceFrequencyCurve(
TestXvals, "LifeLoss", "LifeLoss", ConsequenceType.LifeLoss, RiskType.Fail, cc);

ImpactAreaScenarioResults ia3 = new(3);
CategoriedUncertainPairedData curve3 = ia3.GetOrCreateUncertainConsequenceFrequencyCurve(
TestXvals, "LifeLoss", "LifeLoss", ConsequenceType.LifeLoss, RiskType.Fail, cc);

for (int i = 0; i < batchSize; i++)
{
curve1.AddCurveRealization(new PairedData(TestXvals, new double[] { 10, 20, 30, 40, 50 }), i);
curve2.AddCurveRealization(new PairedData(TestXvals, new double[] { 5, 10, 15, 20, 25 }), i);
curve3.AddCurveRealization(new PairedData(TestXvals, new double[] { 3, 6, 9, 12, 15 }), i);
}
ia1.PutUncertainFrequencyCurvesIntoHistograms();
ia2.PutUncertainFrequencyCurvesIntoHistograms();
ia3.PutUncertainFrequencyCurvesIntoHistograms();

ScenarioResults scenarioResults = new();
scenarioResults.AddResults(ia1);
scenarioResults.AddResults(ia2);
scenarioResults.AddResults(ia3);

var accumulated = scenarioResults.GetAccumulatedLifeLossFnCurveData();
Assert.NotNull(accumulated);

// Expected sums: [18, 36, 54, 72, 90]
PairedData median = accumulated.SamplePairedData(0.5);
double tolerance = 2.0;
Assert.InRange(median.Yvals[0], 18 - tolerance, 18 + tolerance);
Assert.InRange(median.Yvals[1], 36 - tolerance, 36 + tolerance);
Assert.InRange(median.Yvals[2], 54 - tolerance, 54 + tolerance);
Assert.InRange(median.Yvals[3], 72 - tolerance, 72 + tolerance);
Assert.InRange(median.Yvals[4], 90 - tolerance, 90 + tolerance);
}

[Fact]
public void AccumulateOnlyImpactAreasWithLifeLossData()
{
int batchSize = 100;
ConvergenceCriteria cc = new(minIterations: batchSize, maxIterations: batchSize * 10);

// IA 1 and IA 3 have life loss data; IA 2 does not
ImpactAreaScenarioResults ia1 = new(1);
CategoriedUncertainPairedData curve1 = ia1.GetOrCreateUncertainConsequenceFrequencyCurve(
TestXvals, "LifeLoss", "LifeLoss", ConsequenceType.LifeLoss, RiskType.Fail, cc);
for (int i = 0; i < batchSize; i++)
{
curve1.AddCurveRealization(new PairedData(TestXvals, new double[] { 10, 20, 30, 40, 50 }), i);
}
ia1.PutUncertainFrequencyCurvesIntoHistograms();

ImpactAreaScenarioResults ia2 = new(2); // no life loss data

ImpactAreaScenarioResults ia3 = new(3);
CategoriedUncertainPairedData curve3 = ia3.GetOrCreateUncertainConsequenceFrequencyCurve(
TestXvals, "LifeLoss", "LifeLoss", ConsequenceType.LifeLoss, RiskType.Fail, cc);
for (int i = 0; i < batchSize; i++)
{
curve3.AddCurveRealization(new PairedData(TestXvals, new double[] { 5, 10, 15, 20, 25 }), i);
}
ia3.PutUncertainFrequencyCurvesIntoHistograms();

ScenarioResults scenarioResults = new();
scenarioResults.AddResults(ia1);
scenarioResults.AddResults(ia2);
scenarioResults.AddResults(ia3);

var accumulated = scenarioResults.GetAccumulatedLifeLossFnCurveData();
Assert.NotNull(accumulated);

// Only IA1 + IA3 contribute: expected sums [15, 30, 45, 60, 75]
PairedData median = accumulated.SamplePairedData(0.5);
double tolerance = 2.0;
Assert.InRange(median.Yvals[0], 15 - tolerance, 15 + tolerance);
Assert.InRange(median.Yvals[1], 30 - tolerance, 30 + tolerance);
Assert.InRange(median.Yvals[2], 45 - tolerance, 45 + tolerance);
Assert.InRange(median.Yvals[3], 60 - tolerance, 60 + tolerance);
Assert.InRange(median.Yvals[4], 75 - tolerance, 75 + tolerance);
}
}
}
11 changes: 11 additions & 0 deletions HEC.FDA.ViewModel/ImpactAreaScenario/IASElement.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using HEC.FDA.Model.metrics;
using HEC.FDA.Model.paireddata;
using HEC.FDA.ViewModel.Compute;
using HEC.FDA.ViewModel.Editors;
using HEC.FDA.ViewModel.ImpactArea;
Expand Down Expand Up @@ -203,6 +204,16 @@ public List<SpecificIASResultVM> GetResults()
}
}
}

// Add accumulated "Total" F-N curve when multiple IAs have life loss data
if (results.Count > 1)
{
UncertainPairedData accumulatedFnData = Results.GetAccumulatedLifeLossFnCurveData();
if (accumulatedFnData != null)
{
results.Add(new SpecificIASResultVM(SpecificIASResultVM.TOTAL, accumulatedFnData));
}
}
return results;
}
public static Dictionary<int, string> GetImpactAreaNamesFromIDs()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using HEC.FDA.Model.metrics;
using HEC.FDA.Model.paireddata;
using HEC.FDA.ViewModel.Alternatives.Results;
using HEC.FDA.ViewModel.Editors;
using HEC.FDA.ViewModel.ImpactAreaScenario.Editor;
Expand All @@ -23,12 +24,16 @@ public class SpecificIASResultVM : NameValidatingVM
private const string ANNUAL_EXC_PROB = "Annual Exceedance Probability";
private const string LONG_TERM_EXCEEDANCE_PROBABILITY = "Long-Term Exceedance Probability";
private const string ASSURANCE_OF_THRESHOLD = "Assurance of Threshold";
public const string TOTAL = "Total";
#endregion

#region Property Backing Fields
private readonly List<string> _damageReports = new List<string>() { DAMAGE_WITH_UNCERTAINTY, DAMAGE_BY_DAMCAT };
private List<string> _lifeLossReports = new List<string>() { LIFE_LOSS_WITH_UNCERTAINTY, FN_CURVE };
private readonly List<string> _performanceReports = new List<string>() { ANNUAL_EXC_PROB, LONG_TERM_EXCEEDANCE_PROBABILITY, ASSURANCE_OF_THRESHOLD };
/// <summary>
/// Null when constructed via the "Total" accumulated F-N curve constructor.
/// </summary>
private readonly ImpactAreaScenarioResults _IASResult;
private string _selectedOutcome;
private string _selectedReport;
Expand Down Expand Up @@ -132,6 +137,27 @@ public SpecificIASResultVM(string iasName, int impactAreaID, ScenarioResults sce
SelectedOutcome = Outcomes.First();
}
}

/// <summary>
/// Constructor for the accumulated "Total" F-N curve result across all impact areas.
/// Only shows the Life Loss > F-N Curve report.
/// </summary>
public SpecificIASResultVM(string name, UncertainPairedData accumulatedFnData)
{
IASName = name;
_lifeLossReports = new List<string>() { FN_CURVE };

if (accumulatedFnData != null)
{
_lifeLossFnChartVM = new LifeLossFnChartVM(accumulatedFnData, FN_CURVE);
Outcomes.Add(LIFE_LOSS);
}

if (Outcomes.Count > 0)
{
SelectedOutcome = Outcomes.First();
}
}
#endregion

#region Methods
Expand Down
Loading