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 List <SourceConfiguration> ReadSourcesConfig(string confCsv)
        {
            var confs = new List <SourceConfiguration>();

            using (var reader = File.OpenText(confCsv))
            {
                var    hdrs = reader.ReadLine();
                string line;

                var colNames = hdrs.Split(';').ToList();

                while ((line = reader.ReadLine()) != null)
                {
                    if (line.StartsWith("#"))
                    {
                        continue;
                    }
                    var split = line.Split(";");

                    if (split.Length != colNames.Count)
                    {
                        throw new Exception($"'{confCsv}' ERROR: bad column count on line: '{line}'");
                    }
                    var conf = new SourceConfiguration();

                    conf.Alias = split[0];

                    var dict    = new Dictionary <string, bool>();
                    var maxDict = new Dictionary <string, double>();


                    for (int i = 1; i < split.Length; i++)
                    {
                        var sourceContrib = double.Parse(split[i], CultureInfo.InvariantCulture);

                        var used = false;
                        switch (split[i])
                        {
                        case "0":
                            used = false;
                            break;

                        case "1":
                            used = true;
                            break;

                        default:
                            throw new Exception($"Invalid value on line: '{line}'");
                        }

                        used = sourceContrib > 0;

                        if (sourceContrib > 1)
                        {
                            throw new InvalidOperationException("Source max contribution cannnot be higher than 1");
                        }
                        maxDict.Add(colNames[i], sourceContrib);
                        dict.Add(colNames[i], used);
                    }

                    conf.SourcesUsage          = dict;
                    conf.MaxSourceContribution = maxDict;

                    confs.Add(conf);
                }
            }
            return(confs);
        }
        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);
        }