/// <summary> /// Adds policy recommendation for the state /// </summary> /// <param name="year">Age in the state (year)</param> /// <param name="actionNumber">Number of the recommended action: 1, 2, etc.</param> /// <param name="benefit">Unit benefit of the recommended action</param> /// <returns>Created recommendation object</returns> public WeibullMarkovStatePolicyRecommendation AddRecommendation(Int32 year, Int32 actionNumber, Double benefit) { WeibullMarkovStatePolicyRecommendation rec = new WeibullMarkovStatePolicyRecommendation(year, actionNumber, benefit); Recommendations.Add(rec); return(rec); }
/// <summary> /// Constructs an object of the class from the given XML element /// </summary> /// <param name="xml">XML element</param> /// <param name="errorMessage">out Error message</param> /// <returns>Constructed object, null on failure</returns> public static WeibullMarkovStatePolicyRecommendation FromXmlElement(XmlElement xml, out String errorMessage) { WeibullMarkovStatePolicyRecommendation rec = null; errorMessage = null; try { if (xml.Name != RECOMMENDATION) { throw new Exception("Expected a <" + RECOMMENDATION + " ...> XML element"); } rec = new WeibullMarkovStatePolicyRecommendation(0, 0, 0.0); if (xml.HasAttributes) { foreach (XmlAttribute attr in xml.Attributes) { if (attr.Name == _AGE_YEAR) { rec.Year = Int32.Parse(attr.Value.Trim()); } else if (attr.Name == _ACTION) { rec.ActionNumber = Int32.Parse(attr.Value.Trim()); } else if (attr.Name == _UNIT_BENEFIT) { rec.Benefit = Double.Parse(attr.Value.Trim()); } } } } catch (Exception ex) { errorMessage = ex.Message; rec = null; } return(rec); }
/// <summary> /// Solves Markov Decision Model (MDM) /// </summary> /// <param name="numYears">Planning horizon for the optimal policy (number of years to look ahead for)</param> /// <param name="numIter">out Number of iterations run</param> /// <param name="errorMessage">out Error message</param> /// <param name="failCostOverridden">out True if failure cost was overridden</param> /// <param name="failureCost">out Estimated or overriding value of the failure cost</param> /// <returns>True on success, False on failure</returns> public Boolean Solve(Int32 numYears, out Int32 numIter, out Boolean failCostOverridden, out Double failureCost, out String errorMessage) { Boolean ok = true; numIter = 0; errorMessage = null; failCostOverridden = false; failureCost = FailureCost; Log.InfoFormat("Solving Weibull-Markov Decision model - started. Planning horizon: {0}", numYears); try { Log.Info("Resetting."); _discFactor = 1.0 / (1.0 + DiscRate / 100.0); Log.InfoFormat("Discounting factor: {0}", _discFactor); FailureCostUsedInPolicy = null; ResetSolution(); Int32 NS = States.Count; Boolean noChange = true; Log.InfoFormat("Checking inputs"); // Checking inputs for (Int32 i = NS - 1; i >= 0; i--) { WeibullMarkovConditionState state = States[i]; if (state == null) { String err = String.Format("Slot number {0} (zero-based) in the condition state list is null or empty.", i); throw new Exception(err); } if (i == NS - 1 && (state.Actions == null || state.Actions.Count < 1)) { String err = String.Format("Last condition state (state number {0}) has no actions specified for it other than Do Nothing.", state.Number); throw new Exception(err); } if (state.Beta == 1.0 || state.ff == 0.0) { Double f = Weibull.OneYearFailure(state.Eta, state.Beta, 1.0); if (f <= 0.0) { String err = String.Format("No optimal solution can be found for condition state {0} because its failure probability is constant over time and equals zero or remains negligibly small.", state.Number); throw new Exception(err); } } } Log.InfoFormat("Main iteration cycle - started"); Log.InfoFormat("Nummber of condition states: {0}", NS); do { noChange = true; numIter++; Log.InfoFormat("Iteration: {0}", numIter); for (Int32 i = NS - 1; i >= 0; i--) { WeibullMarkovConditionState state = States[i]; Log.InfoFormat("Iter: {0}\tCondition state: {1}\t{2}\tEta={3}\tBeta={4}\tdfdt({5})={6}", numIter, state.Number, (i == NS - 1) ? "(last state)" : String.Empty, state.Eta, state.Beta, state.T, state.ff); Log.InfoFormat("Iter: {0}\tCondition state: {1}\tNumber of actions: {2}", numIter, state.Number, state.Actions == null ? 0 : state.Actions.Count); Int32 minCostActionNumber = -1; Double minActionShadowCost = -1.0; // Compute shadow costs for the actions and find the action with the minimum shadow cost if (state.Actions != null && state.Actions.Count > 0) { foreach (WeibullMarkovAction action in state.Actions) { if (action.IsApplicable) { Double shCost = action.Cost; for (Int32 j = 0; j < action.TranProb.Length; j++) { Double frac = action.TranProb[j]; shCost += _discFactor * frac * States[j].ShadowCost; } action.AssignShadowCost(shCost); Log.InfoFormat("Iter: {0}\tCondition state: {1}\tAction: {2}\tAgency cost: {3:f2}\tShadow cost: {4:f2}", numIter, state.Number, action.Number, action.Cost, action.ShadowCost); if (minCostActionNumber < 0 || Math.Round(minActionShadowCost, 2) > Math.Round(shCost, 2)) { minCostActionNumber = action.Number; minActionShadowCost = shCost; } } } } Log.InfoFormat("Iter: {0}\tCondition state: {1}\tMinShadowCostAction: {2}\tMinActionShadowCost: {3:f2}\tDo-Nothing direct (agency) cost: {4:f2}", numIter, state.Number, minCostActionNumber, minActionShadowCost, state.DoNothingCost); if (i == NS - 1) // If it is the last condition state { Log.InfoFormat("Iter: {0}\tLast state processing", numIter); if (!this.FailCostEstimate) { Double d = Weibull.ShadowCostAtAgeZero(state.Eta, state.Beta, FailureCost, _discFactor, out state.ShadowCostStreamLength, ref state.ShadowCostStream, out errorMessage); Log.InfoFormat("Iter: {0}\tLast state processing\tDN-ShadowCost-in-Year-One: {1} - computed", numIter, d); if (d < 0.0) { throw new Exception(errorMessage); } d += state.DoNothingCost; if (d < minActionShadowCost && 0 < minCostActionNumber && FailCostOverride) { d = minActionShadowCost + 0.01; failCostOverridden = true; Log.InfoFormat("Iter: {0}\tLast state processing\tDN-ShadowCost-in-Year-One: {1} - as to override", numIter, d); } else { Log.InfoFormat("Iter: {0}\tLast state processing\tDN-ShadowCost-in-Year-One: {1} - as computed", numIter, d); } state.AssignDoNothingShadowCostAgeZero(d); } else { Double d = minActionShadowCost + 0.01; state.AssignDoNothingShadowCostAgeZero(d); Log.InfoFormat("Iter: {0}\tLast state processing\tDN-ShadowCost-in-Year-One: {1} - estimated", numIter, d); } } else // ..., i.e. if it is NOT the last state { Double d = Weibull.ShadowCostAtAgeZero(state.Eta, state.Beta, States[i + 1].ShadowCost, _discFactor, out state.ShadowCostStreamLength, ref state.ShadowCostStream, out errorMessage); d += state.DoNothingCost; state.AssignDoNothingShadowCostAgeZero(d); Log.InfoFormat("Iter: {0}\tCondition state: {1}\tDN-ShadowCost-in-Year-One: {2} - as computed", numIter, state.Number, d); } if (minCostActionNumber > 0) { state.AssignMinShadowCostAction(minCostActionNumber); } // Assign action that has the minimum shadow cost (including do nothing) if (minCostActionNumber < 0 || state.DoNothingShadowCostAgeZero < minActionShadowCost) { state.AssignAction(0); } else { state.AssignAction(minCostActionNumber); } Log.InfoFormat("Iter: {0}\tCondition state: {1}\tAssigned action: {2}\tShadow cost: {3}", numIter, state.Number, state.AssignedAction, state.ShadowCost); noChange = noChange && state.NoChange; Log.InfoFormat("Iter: {0}\tCondition state: {1}\tConvergence achieved: {2}", numIter, state.Number, noChange); } } while (!noChange && numIter <= _maxIter); Log.InfoFormat("Main iteration cycle ended. Converted: {0}. Iterations run: {1}", noChange, numIter); if (!noChange) { String err = String.Format("Conversion of the main iteration cycle has not been achieved after {0} iterations.", numIter); throw new Exception(err); } if (FailCostEstimate || failCostOverridden) { Log.InfoFormat("Calculating the failure cost."); WeibullMarkovConditionState lastS = States[NS - 1]; if (lastS.ff == 0.0 || lastS.Beta == 1.0) { // f cannot be zero because it has been checked against it in the beginning Double f = Weibull.OneYearFailure(lastS.Eta, lastS.Beta, 1.0); Double d = lastS.DoNothingShadowCostAgeZero - lastS.DoNothingCost; failureCost = d * (1.0 - _discFactor * (1.0 - f)) / f; // we are done because f does not change over time Log.InfoFormat("For constant deterioration rate failure cost is: {0:f2}", failureCost); } else { if (!FailCostEstimate) { failureCost = FailureCost; // Use user's input as the first approximation } else //..., i.e. if it has to be calculated from scratch (FailCostEstimate==True) { Double f = Weibull.OneYearFailure(lastS.Eta, lastS.Beta, 1.0); // To calculate the first approximation we use 1% of failure if actual fialure probability is // less than 0.01 at age zero (in year 1) f = Math.Max(f, 0.01); Double d = lastS.DoNothingShadowCostAgeZero - lastS.DoNothingCost; failureCost = d * (1.0 - _discFactor * (1.0 - f)) / f; } Log.InfoFormat("Failure cost approximation: {0:f2}", failureCost); // Make sure that the crude estimate of failure cost delivers tangible (i.e. >=0.01) cost at age zero (year 1). Double ageZeroCost = 0.0; Int32 n = 0; while (n < _maxIter) { ageZeroCost = Weibull.ShadowCostAtAgeZero(lastS.Eta, lastS.Beta, failureCost, _discFactor, out lastS.ShadowCostStreamLength, ref lastS.ShadowCostStream, out errorMessage); if (ageZeroCost < 0.0) { throw new Exception(errorMessage); } if (Math.Round(ageZeroCost, 3) <= 0.005) // If we are loosing precision multiply failure cost by 10 and try again { failureCost *= 10.0; } else { break; } n++; Log.InfoFormat("Failure cost approximation iteration {0}. Shadow cost at age zero if approximated failure cost used: {0}", n, ageZeroCost); } if (n >= _maxIter) { String err = String.Format("Failure cost could not be estimated after {0} iterations.", n); throw new Exception(err); } // From this moment on we do not pay attention to the direct do-nothing cost and compare only their // shadow-cost (i.e. failure-cost related components). // Adjsut it making use of linearity Log.InfoFormat("Shadow cost at age-zero - used by the policy: {0:f2}", lastS.DoNothingShadowCostAgeZero); Log.InfoFormat("Failure-related shadow cost at age-zero - used by the policy: {0:f2}", lastS.DoNothingShadowCostAgeZero - lastS.DoNothingCost); failureCost *= (lastS.DoNothingShadowCostAgeZero - lastS.DoNothingCost); failureCost /= ageZeroCost; Log.InfoFormat("Adjusted failure cost: {0:f2}", failureCost); ageZeroCost = Weibull.ShadowCostAtAgeZero(lastS.Eta, lastS.Beta, failureCost, _discFactor, out lastS.ShadowCostStreamLength, ref lastS.ShadowCostStream, out errorMessage); Log.InfoFormat("Failure-related shadow cost at age zero - adjusted: {0:f2}", ageZeroCost); if (ageZeroCost < 0.0) { throw new Exception(errorMessage); } if (Math.Round(ageZeroCost, 2) != Math.Round(lastS.DoNothingShadowCostAgeZero, 2)) { Log.WarnFormat("Computed failure cost ({0:f2}) does not deliver the required shadow cost in year one ({1:f2}). The value it delivers is {2:f2}", failureCost, lastS.DoNothingShadowCostAgeZero, ageZeroCost); } } } Log.InfoFormat("Generating policy - started."); FailureCostUsedInPolicy = failureCost; Log.InfoFormat("Failure cost used: {0}", FailureCostUsedInPolicy); Log.InfoFormat("Alpha-adjusted failure cost used: {0}", FailureCostUsedInPolicy * _discFactor); for (Int32 j = 0; j < NS; j++) { WeibullMarkovConditionState state = States[j]; state.Recommendations.Clear(); Double dTarget = (j == NS - 1) ? FailureCostUsedInPolicy.Value : States[j + 1].DoNothingShadowCostAgeZero; dTarget *= _discFactor; Log.InfoFormat("Condition state: {0}\tTargetCost: {1}", state.Number, state.ff >= 0.0 ? dTarget : 0.0); Int32 yMax = Math.Min(numYears, state.ShadowCostStreamLength); // The first pass is to establish yMax, the second one is to add recommendations for (Int32 iPass = 0; iPass < 2; iPass++) { for (Int32 y = 0; y < yMax; y++) { Double d = state.ShadowCostStream[y]; Int32 A = 0; Double B = 0.0; if (0 < state.MinShadowCostAction && Math.Round(d + state.DoNothingCost - state.MinActionShadowCost, 2) >= 0.01) { A = state.MinShadowCostAction; B = Math.Round(d - state.MinActionShadowCost, 2); } if (iPass > 0) { WeibullMarkovStatePolicyRecommendation R = new WeibullMarkovStatePolicyRecommendation(y + 1, A, B); state.Recommendations.Add(R); } Log.InfoFormat("Pass: {7}\tState: {0}\tYear={1,3:d}\tShC={2,9:f2}\tMinAct={3,3}\tMinActShC={4,9:f2}\tActRec={5}\tUnitBenf={6,9:f2}", state.Number, y + 1, d, state.MinShadowCostAction, state.MinActionShadowCost, A, B, iPass); if (state.ff == 0.0 || state.Beta == 1.0) { yMax = 1; break; } if (iPass == 0 && A == 0 && state.ff >= 0.0 && y == yMax - 1) { // If f-probability is growing but it is still do nothing in year yMax then cut it at the first year; yMax = 1; break; } // Saturation conditions if (iPass == 0 && y > 0 && state.ff >= 0.0 && Math.Round(d, 2) >= Math.Round(dTarget, 2)) { yMax = A == 0 ? 1 : y + 1; break; } if (iPass == 0 && y > 0 && state.ff < 0.0 && Math.Round(d, 2) <= 0.0) { yMax = y + 1; break; } if (iPass == 0 && A == 0 && state.ff <= 0.0) { yMax = y + 1; break; // If f-probability is decreasing and it is do-nothing in year Y then it can only be do-nothing later } } } } Log.InfoFormat("Generating policy - ended."); } catch (Exception ex) { errorMessage = ex.Message; ok = false; Log.Error(errorMessage); } Log.InfoFormat("Solving Weibull-Markov Decision model - ended. OK={0}", ok); return(ok); }
/// <summary> /// Constructs an object from the XML element /// </summary> /// <param name="xml">XML element</param> /// <param name="errorMessage">out Error message</param> /// <returns>Constructed object, null on failure</returns> public static WeibullMarkovConditionState FromXmlElement(XmlElement xml, out String errorMessage) { WeibullMarkovConditionState state = null; errorMessage = null; try { if (xml.Name != CONDITION_STATE) { throw new Exception("A <" + CONDITION_STATE + " ...> XML element expected."); } state = new WeibullMarkovConditionState(0.0, 0.0); String number = xml.GetAttribute(_NUMBER); if (number == null) { throw new Exception("The '" + _NUMBER + "' attribute is missing in the State XML element"); } state.Number = Int32.Parse(number); String eta = xml.GetAttribute(_ETA); if (eta == null) { throw new Exception("The '" + _ETA + "' attribute is missing in the State XML element"); } state.Eta = Double.Parse(eta); String beta = xml.GetAttribute(_BETA); if (beta == null) { throw new Exception("The '" + _BETA + "' attribute is missing in the State XML element"); } state.Beta = Double.Parse(beta); String doNothCost = xml.GetAttribute(_DO_NOTHING_COST); if (!String.IsNullOrEmpty(doNothCost)) { state.DoNothingCost = Double.Parse(doNothCost); } String tTabulated = xml.GetAttribute(_T_TABULATED); if (!String.IsNullOrEmpty(tTabulated)) { state.T = Double.Parse(tTabulated); } String f = xml.GetAttribute(_F_PROB_AT_T); if (!String.IsNullOrEmpty(f)) { state.f = Double.Parse(f); } String ff = xml.GetAttribute(_DFDT_AT_T); if (!String.IsNullOrEmpty(ff)) { state.ff = Double.Parse(ff); } String at50 = xml.GetAttribute(_T50); if (!String.IsNullOrEmpty(at50)) { state.t50 = Double.Parse(at50); String at9x = xml.GetAttribute(_T9X); String ax = xml.GetAttribute(_X); if (!String.IsNullOrEmpty(at9x) && !String.IsNullOrEmpty(ax)) { state.t9X = Double.Parse(at9x); state.x = ax; } } if (xml.HasChildNodes) { foreach (XmlNode n in xml.ChildNodes) { if (n.Name == _ACTIONS) { if (n.HasChildNodes) { foreach (XmlNode n1 in n.ChildNodes) { if (n1.Name == WeibullMarkovAction.ACTION) { WeibullMarkovAction action = WeibullMarkovAction.FromXmlElement(n1 as XmlElement, out errorMessage); if (action == null) { throw new Exception(errorMessage); } state.Actions.Add(action); } } } } if (n.Name == _POLICY) { foreach (XmlNode n2 in n.ChildNodes) { if (n2.Name == WeibullMarkovStatePolicyRecommendation.RECOMMENDATION) { WeibullMarkovStatePolicyRecommendation rec = WeibullMarkovStatePolicyRecommendation.FromXmlElement(n2 as XmlElement, out errorMessage); if (rec == null) { throw new Exception(errorMessage); } state.Recommendations.Add(rec); } } } } } } catch (Exception ex) { errorMessage = ex.Message; state = null; } return(state); }