HydroSourceValues CreateModel(List <Source> sources, List <MarkerInfo> markerInfos, HydroMixModelConfig config, Target t, SourceConfiguration sourcesConfiguration) { var ret = new HydroSourceValues(sources, markerInfos, config.NaValuesHandling, t); ret.StandardizeValues(); foreach (var configMarkerWeigth in config.MarkerWeigths) { var mi = markerInfos.Single(x => x.MarkerName == configMarkerWeigth.Key); if (configMarkerWeigth.Value < 0) { throw new InvalidOperationException("Marker weight cannot be negative!"); } mi.Weight = configMarkerWeigth.Value; } foreach (var src in sources) { var val = sourcesConfiguration.MaxSourceContribution[src.Code]; if (val > 1 || val < 0) { throw new InvalidOperationException("Source max contribution must be between 0 and 1!"); } src.MaxSourceContribution = val; } return(ret); }
public HydroMixProblem(HydroSourceValues values, HydroMixModelConfig config, Target target, SourceConfiguration sourceConfiguration) { Model = new Model(); //var sources = values.GetSources(); var sources = values.Sources().Where(s => sourceConfiguration.SourcesUsage[s.Code]).ToList(); Sources = sources; SourceContribution = new VariableCollection <Source>( Model, sources, "SourceContributions", s => $"Source {s.Code} contribution", s => 0, s => s.MaxSourceContribution, // weights should be >= 0 <= 1 s => OPTANO.Modeling.Optimization.Enums.VariableType.Continuous); SourceUsed = new VariableCollection <Source>( Model, sources, "SourceIsUsed", s => $"Indicator whether source {s.Code} is used.", s => 0, s => 1, s => OPTANO.Modeling.Optimization.Enums.VariableType.Binary); PositiveErrors = new VariableCollection <MarkerInfo>(Model, values.MarkerInfos(), "Epsilon (+) error for each marker", mi => $"Error for marker: {mi.MarkerName}.", mi => 0, mi => double.MaxValue, mi => VariableType.Continuous ); NegativeErrors = new VariableCollection <MarkerInfo>(Model, values.MarkerInfos(), "Epsilon (-) error for each marker", mi => $"Error for marker: {mi.MarkerName}.", mi => 0, mi => double.MaxValue, mi => VariableType.Continuous ); // https://math.stackexchange.com/questions/2571788/indicator-variable-if-x-is-in-specific-range // constraints foreach (var source in sources) { // force not-used when contribution = 0 var indicator = SourceContribution[source] + SourceUsed[source] <= 1000 * SourceUsed[source]; Model.AddConstraint(indicator, $"Indicator for {source.Code}"); // force used when contribution > 0 var indicator2 = SourceUsed[source] - SourceContribution[source] >= 0; Model.AddConstraint(indicator2, $"Indicator for {source.Code}"); if (config.MinimalSourceContribution > 0) { Model.AddConstraint(SourceContribution[source] >= config.MinimalSourceContribution * SourceUsed[source], $"Minimal source {source.Code} contribution."); } } // weights sum should be 1 var contributionsSum = Expression.Sum(sources.Select(s => SourceContribution[s])); Model.AddConstraint(contributionsSum == 1, "Contributions sum equals 1"); // max sources contributing var usedSourcesCount = Expression.Sum(sources.Select(s => SourceUsed[s])); if (config.MaxSourcesUsed.HasValue) { Model.AddConstraint(usedSourcesCount <= config.MaxSourcesUsed.Value); } // min sources contributing if (config.MinSourcesUsed.HasValue) { Model.AddConstraint(usedSourcesCount >= config.MinSourcesUsed.Value); } // equation for each marker foreach (var markerInfo in values.MarkerInfos() //.Where(mi => mi.Weight > 0) ) { var positiveEpsilon = PositiveErrors[markerInfo]; var negativeEpsilon = NegativeErrors[markerInfo]; // get all values for current marker var markerValues = sources.Where(x => x.MaxSourceContribution > 0).Select(s => s.MarkerValues.Single(mv => mv.MarkerInfo == markerInfo)); var sourcesContributedToMarker = Expression.Sum( markerValues.Select(x => SourceContribution[x.Source] * x.Value)); Model.AddConstraint(sourcesContributedToMarker == positiveEpsilon + target[markerInfo] - negativeEpsilon); } // min: diff between target and resulting mix Model.AddObjective(new Objective(Expression.Sum( values.MarkerInfos().Where(mi => mi.Weight > 0).Select(mi => PositiveErrors[mi] * mi.Weight) .Concat(values.MarkerInfos().Where(mi => mi.Weight > 0).Select(mi => NegativeErrors[mi] * mi.Weight)) ), "Difference between mix and target.", ObjectiveSense.Minimize)); }
GeoHydroSolutionOutput SolveTheModel(HydroSourceValues inputValues, HydroMixModelConfig config, SourceConfiguration sourceConfiguration) { var text = new StringBuilder( $"\n" + $" ===================================\n" + $"Result for target: '{inputValues.Target.Source.Name}' and configuration: '{config.ConfigAlias}' and source conf: '{sourceConfiguration.Alias}' :" + $"\n\n"); var optanoConfig = new Configuration(); optanoConfig.NameHandling = NameHandlingStyle.UniqueLongNames; optanoConfig.ComputeRemovedVariables = true; GeoHydroSolutionOutput geoHydroSolutionOutput; using (var scope = new ModelScope(optanoConfig)) { var problem = new HydroMixProblem(inputValues, config, inputValues.Target, sourceConfiguration); using (var solver = new GLPKSolver()) { // solve the model var solution = solver.Solve(problem.Model); // import the results back into the model foreach (var vc in problem.Model.VariableCollections) { vc.SetVariableValues(solution.VariableValues); } var TOLERANCE = 0.001; var sourcesContributions = problem.Sources.Select(s => (Source: s, Contribution: problem.SourceContribution[s].Value)).ToList(); var resultingMix = inputValues.MarkerInfos().Select(x => x.MarkerName).ToDictionary(x => x, x => 0.0); foreach (var source in sourcesContributions.Where(x => x.Contribution > 0.01).OrderByDescending(x => x.Contribution)) { var array = source.Source.Name.Take(25).ToArray(); var sourceContribution = problem.SourceUsed[source.Source].Value; var formattableString = $"Source({sourceContribution:F0}): {new string(array),25} | Contribution: {source.Contribution * 100:F1} "; text.AppendLine(formattableString); foreach (var markerVal in source.Source.MarkerValues) { resultingMix[markerVal.MarkerInfo.MarkerName] += markerVal.Value * markerVal.MarkerInfo.NormalizationCoefficient * source.Contribution; } } text.AppendLine(); var totalError = 0.0; var optimizedError = 0.0; foreach (var markerInfo in inputValues.MarkerInfos() //.Where(x => x.Weight > 0) ) { var epsilonMarkerErrorPos = problem.PositiveErrors[markerInfo].Value; var epsilonMarkerErrorNeg = problem.NegativeErrors[markerInfo].Value; totalError += Math.Abs(epsilonMarkerErrorNeg) + Math.Abs(epsilonMarkerErrorPos); if (markerInfo.Weight > 0) { optimizedError += Math.Abs(epsilonMarkerErrorNeg) + Math.Abs(epsilonMarkerErrorPos); } var originalTargetValue = inputValues.Target.Source[markerInfo].OriginalValue.Value; var computedValue = resultingMix[markerInfo.MarkerName] - (epsilonMarkerErrorPos * markerInfo.NormalizationCoefficient) + (epsilonMarkerErrorNeg * markerInfo.NormalizationCoefficient); string diffInfo = null; if (Math.Abs(computedValue - originalTargetValue) > TOLERANCE) { diffInfo = $"| diffComputed/Target: ({computedValue,6:F3}/{originalTargetValue,6:F3})"; } var realDiff = resultingMix[markerInfo.MarkerName] - originalTargetValue; var formattableString = $"Marker({markerInfo.Weight:F0}) {markerInfo.MarkerName,10} | targetVal: {originalTargetValue,6:F2} | diff: ({realDiff,6:F2}) | mixValue: {resultingMix[markerInfo.MarkerName],6:F2} {diffInfo}"; text.AppendLine(formattableString); } geoHydroSolutionOutput = new GeoHydroSolutionOutput() { TextOutput = text + $" ===================================\n" + '\n', ConfigALias = config.ConfigAlias, Target = inputValues.Target, SourcesConfigAlias = sourceConfiguration.Alias, UsedSources = sourcesContributions.Where(x => x.Contribution > 0.01).OrderByDescending(x => x.Contribution), NormalizedError = totalError, ResultingMix = resultingMix, OptimalizedError = optimizedError }; } } return(geoHydroSolutionOutput); }