#region License Information /* HeuristicLab * Copyright (C) 2002-2015 Heuristic and Evolutionary Algorithms Laboratory (HEAL) * * This file is part of HeuristicLab. * * HeuristicLab is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * HeuristicLab is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with HeuristicLab. If not, see . */ #endregion using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Text; using HeuristicLab.BioBoost.Evaluators; using HeuristicLab.BioBoost.Operators.Mutation; using HeuristicLab.BioBoost.ProblemDescription; using HeuristicLab.BioBoost.Utils; using HeuristicLab.Common; using HeuristicLab.Core; using HeuristicLab.Data; using HeuristicLab.Parameters; using HeuristicLab.Persistence.Default.CompositeSerializers.Storable; using NetTopologySuite.GeometriesGraph; using SharpMap; using SharpMap.Data.Providers; using SharpMap.Layers; using SharpMap.Utilities.Wfs; // using YamlDotNet.RepresentationModel.Serialization.NodeTypeResolvers; namespace HeuristicLab.BioBoost.Representation { [StorableClass] public sealed class BioBoostCompoundSolution : ParameterizedNamedItem, IStorableContent { public class TransportInfo { public string Product; public int SrcRegionIdx; public int DestRegionIdx; public string SrcRegion; public string DestRegion; public string Mode; public double Distance; public double Amount; } [Storable] public Dictionary IntValues; [Storable] public Dictionary DoubleValues; [Storable] public Dictionary StringValues; [Storable] public Dictionary>> TransportTargets; [Storable] private BioBoostProblemData problemDataRef; public BioBoostProblemData ProblemDataReference { get { return problemDataRef; } set { if (problemDataRef != value) { problemDataRef = value; EvaluateAndUpdateAllResults(); // also raises OnSolutionChanged (re-evaluation is only done when the problem is set by an external entity) } } } #region Parameters public ValueParameter LocationNamesParameter { get { return (ValueParameter)Parameters["LocationNames"]; } } public FixedValueParameter QualityParameter { get { return (FixedValueParameter)Parameters["Quality"]; } } public ValueParameter QualitiesParameter { get { return (ValueParameter)Parameters["Qualities"]; } } #endregion #region Parameter Values public StringArray LocationNames { get { return LocationNamesParameter.Value; } private set { LocationNamesParameter.Value = value; } } public double Quality { get { return QualityParameter.Value.Value; } private set { QualityParameter.Value.Value = value; } } public DoubleArray Qualities { get { return QualitiesParameter.Value; } private set { QualitiesParameter.Value = value; } } #endregion private readonly Closure geometry = new Closure(Closure.Encapsulation.Referenced); [Storable] public GeometryFeatureProvider Geometry { get { return geometry.Value; } private set { geometry.Value = value; } } public event EventHandler SolutionChanged; private void OnSolutionChanged() { var handler = SolutionChanged; if (handler != null) handler(this, EventArgs.Empty); } private static bool IsSlimKey(string key) { if (LayerDescriptor.Utilizations.IsSuffixOf(key)) return true; if (LayerDescriptor.ConverterCapacities.IsSuffixOf(key)) return true; if (LayerDescriptor.RelativeCostAtSource.IsSuffixOf(key)) return true; return false; } #region Construction & Cloning [StorableConstructor] private BioBoostCompoundSolution(bool isDeserializing) : base(isDeserializing) { } private BioBoostCompoundSolution(BioBoostCompoundSolution orig, Cloner cloner, bool full = true) : base(orig, cloner) { problemDataRef = orig.problemDataRef; // doesn't re-evaluate the solution Geometry = orig.Geometry; Quality = orig.Quality; Qualities = (DoubleArray) orig.Qualities.Clone(); IntValues = orig.IntValues.ToDictionary(kvp => kvp.Key, kvp => (IntArray) kvp.Value.Clone()); if (full) { DoubleValues = orig.DoubleValues.ToDictionary(kvp => kvp.Key, kvp => (DoubleArray) kvp.Value.Clone()); StringValues = orig.StringValues.ToDictionary(kvp => kvp.Key, kvp => (StringArray) kvp.Value.Clone()); if (orig.TransportTargets != null) TransportTargets = orig.TransportTargets.ToDictionary(kvp => kvp.Key, kvp => (ItemArray>)kvp.Value.Clone()); } else { DoubleValues = orig.DoubleValues.Where(kvp => IsSlimKey(kvp.Key)).ToDictionary(kvp => kvp.Key, kvp => (DoubleArray) kvp.Value.Clone()); StringValues = new Dictionary(); if (orig.TransportTargets != null) TransportTargets = new Dictionary>>(); } } private BioBoostCompoundSolution() { Parameters.Add(new ValueParameter("LocationNames", "The names of the different locations.")); Parameters.Add(new FixedValueParameter("Quality", "The quality of the solution as produced by the evaluator.", new DoubleValue())); Parameters.Add(new ValueParameter("Qualities", "The quality criteria of the solution as produced by the evaluator.", new DoubleArray())); IntValues = new Dictionary(); DoubleValues = new Dictionary(); StringValues = new Dictionary(); TransportTargets = new Dictionary>>(); } private bool TryAddValue(IScope scope, string valueName) { IVariable var; if (!scope.Variables.TryGetValue(valueName, out var)) return false; var doubleArray = var.Value as DoubleArray; if (doubleArray != null && !DoubleValues.ContainsKey(valueName)) { DoubleValues.Add(valueName, (DoubleArray) doubleArray.Clone()); return true; } var intArray = var.Value as IntArray; if (intArray != null && !IntValues.ContainsKey(valueName)) { IntValues.Add(valueName, (IntArray) intArray.Clone()); return true; } var stringArray = var.Value as StringArray; if (stringArray != null && !StringValues.ContainsKey(valueName)) { StringValues.Add(valueName, (StringArray) stringArray.Clone()); return true; } var itemArray = var.Value as ItemArray>; if (itemArray != null && !TransportTargets.ContainsKey(valueName)) { TransportTargets.Add(valueName, (ItemArray>) itemArray.Clone()); } return false; } private bool TryAddValue(BioBoostProblemData problemData, string valueName) { IParameter param; if (problemData.FeedstockPotentials.TryGetValue(valueName, out param)) { var valueParam = param as IValueParameter; if (valueParam != null) { var doubleArray = valueParam.Value as DoubleArray; if (doubleArray != null && !DoubleValues.ContainsKey(valueName)) { DoubleValues.Add(valueName, (DoubleArray) doubleArray.Clone()); return true; } var intArray = valueParam.Value as IntArray; if (intArray != null && !IntValues.ContainsKey(valueName)) { IntValues.Add(valueName, (IntArray) intArray.Clone()); return true; } var stringArray = valueParam.Value as StringArray; if (stringArray != null && !StringValues.ContainsKey(valueName)) { StringValues.Add(valueName, (StringArray) stringArray.Clone()); return true; } var itemArray = valueParam.Value as ItemArray>; if (itemArray != null && !TransportTargets.ContainsKey(valueName)) { TransportTargets.Add(valueName, (ItemArray>) itemArray.Clone()); return true; } } } return false; } public BioBoostCompoundSolution(IScope scope, BioBoostProblemData problemData) : this() { this.problemDataRef = problemData; // not cloned on purpose Geometry = problemData.Geometry; // TODO: maybe clone geometry once? LocationNames = (StringArray)problemData.LocationNames.Clone(); IVariable qualityVariable; // TotalCost is the quality produced by the aggregate evaluator if (scope.Variables.TryGetValue("TotalCost", out qualityVariable)) { Quality = ((DoubleValue) qualityVariable.Value).Value; // TODO: should be read only } else if (scope.Variables.TryGetValue("Quality", out qualityVariable)) { Quality = ((DoubleValue) qualityVariable.Value).Value; // TODO: should be read only } if (scope.Variables.TryGetValue("QualityCriteria", out qualityVariable)) { Qualities = (DoubleArray) qualityVariable.Value.Clone(); // TODO: should be read only } var isUpgradedSolution = scope.Variables.Any(v => v.Name.Contains(LayerDescriptor.AmountsAtSource.FullName)); foreach (var p in problemData.Products) { if (!isUpgradedSolution) { if (TryAddValue(scope, LayerDescriptor.TransportTargets.NameWithPrefix(p.Label))) TryAddValue(scope, LayerDescriptor.Utilizations.NameWithPrefix(p.Label)); } else { TryAddValue(problemData, LayerDescriptor.PotentialsFromProblemData.NameWithPrefix(p.Label)); TryAddValue(scope, p.Label); // emissions foreach (var layer in LayerDescriptor.Layers) { TryAddValue(scope, layer.NameWithPrefix(p.Label)); } } } if (isUpgradedSolution) foreach (var layer in LayerDescriptor.Layers) { TryAddValue(scope, layer.Name); } } public override IDeepCloneable Clone(Cloner cloner) { return new BioBoostCompoundSolution(this, cloner); } public BioBoostCompoundSolution SlimClone() { return new BioBoostCompoundSolution(this, new Cloner(), false); } [StorableHook(HookType.AfterDeserialization)] private void AfterDeserialization() { if (StringValues == null) StringValues = new Dictionary(); // backwards compatibility if (!Parameters.ContainsKey("Quality")) Parameters.Add(new FixedValueParameter("Quality", new DoubleValue(double.NaN))); // TODO: use different quality value to indicate that the value is not known? if(!Parameters.ContainsKey("Qualities")) Parameters.Add(new ValueParameter("Qualities", "The quality criteria of the solution as produced by the evaluator.", new DoubleArray())); } #endregion public void UpdateTo(BioBoostCompoundSolution bestSolution) { if (LocationNames.SequenceEqual(bestSolution.LocationNames)) { // TODO: replace with comparison of problem reference Quality = bestSolution.Quality; Qualities = (DoubleArray) bestSolution.Qualities.Clone(); IntValues = bestSolution.IntValues.ToDictionary(kvp => kvp.Key, kvp => (IntArray)kvp.Value.Clone()); DoubleValues = bestSolution.DoubleValues.ToDictionary(kvp => kvp.Key, kvp => (DoubleArray)kvp.Value.Clone()); StringValues = bestSolution.StringValues.ToDictionary(kvp => kvp.Key, kvp => (StringArray)kvp.Value.Clone()); TransportTargets = bestSolution.TransportTargets.ToDictionary(kvp => kvp.Key, kvp => (ItemArray>)kvp.Value.Clone()); OnSolutionChanged(); } else { throw new ArgumentException("Invalid solution update, proposed solution has different problem instance."); } } public static DoubleArray FindAmountsTransportedFromSource(IEnumerable> dict, string productName) { if (LayerDescriptor.TransportTargets.IsSuffixOf(productName)) productName = LayerDescriptor.TransportTargets.RemoveSuffixFrom(productName); foreach (var kvp in dict) { if (LayerDescriptor.AmountsTransportedFromSource.IsSuffixOf(kvp.Key) && LayerDescriptor.AmountsTransportedFromSource.RemoveSuffixFrom(kvp.Key) == productName) return kvp.Value; } return null; } #region convenience methods for retrieving values public double TotalFeedstockCosts { get { // first get the list of feedstock (all products which are utilized) var feedStockLabels = DoubleValues.Keys .Where(k => LayerDescriptor.Utilizations.IsSuffixOf(k)) .Select(k => LayerDescriptor.Utilizations.RemoveSuffixFrom(k)); return feedStockLabels .Select(feedstock => DoubleValues[LayerDescriptor.TotalCostsAtSource.NameWithPrefix(feedstock)].Sum()) // sum over regions .Sum(); // sum over feedstock } } public double TotalTransportCosts { get { // here we add transport and handling costs // keys for all transport costs end with BioBoostProblem.TransportCostsName // keys for all handling costs end with BioBoostProblem.HandlingCostsName return DoubleValues .Where(kvp => LayerDescriptor.TransportCosts.IsSuffixOf(kvp.Key) || LayerDescriptor.HandlingCosts.IsSuffixOf(kvp.Key)) .Select(kvp => kvp.Value.Sum()) // sum over all regions .Sum(); // sum over all products } } public double TotalCosts { get { // we add all costs (including all revenues from selling products) // assumes that revenues are also labeled as costs! return DoubleValues .Where(kvp => kvp.Key.EndsWith("Cost") || kvp.Key.EndsWith("Costs")) .Sum(kvp => kvp.Value.Where(v => v > 0).Sum()); // sum over products and regions } } public double TotalTransportFuelAmount { get { // assumes that the values are stored as "TransportFuel" + BioBoostProblem.AmountsAtSourceName return DoubleValues .Where(kvp => kvp.Key == LayerDescriptor.AmountsAtSource.NameWithPrefix("TransportFuel")) .Sum(kvp => kvp.Value.Sum()); } } public IEnumerable Transports { get { foreach (var pair in TransportVectors) { var prod = pair.Item1; var modes = StringValues[LayerDescriptor.TransportModes.NameWithPrefix(prod)]; var distances = DoubleValues[LayerDescriptor.TransportDistance.NameWithPrefix(prod)]; var amounts = DoubleValues[LayerDescriptor.AmountsTransportedFromSource.NameWithPrefix(prod)]; for (int srcIdx = 0; srcIdx < pair.Item2.Length; srcIdx++) { var destIdx = pair.Item2[srcIdx]; if (destIdx == -1) continue; var info = new TransportInfo { Product = prod, SrcRegionIdx = srcIdx, DestRegionIdx = destIdx, SrcRegion = LocationNames[srcIdx], DestRegion = LocationNames[destIdx], Mode = modes[srcIdx], Distance = distances[srcIdx], Amount = amounts[srcIdx] }; yield return info; } } } } public IEnumerable> TransportVectors { get { // here we yield one tuple for each product containing the product name and transport target map // we know transport targets are int-arrays and the key ends with 'BioBoostProblem.TransportTargetsName' foreach (var p in IntValues.Where(p => LayerDescriptor.TransportTargets.IsSuffixOf(p.Key))) { var productLabel = LayerDescriptor.TransportTargets.RemoveSuffixFrom(p.Key); yield return Tuple.Create(productLabel, p.Value.ToArray()); } } } public IEnumerable> UtilizationVectors { get { // here we yield one tuple for each feedstock containing the feedstock name and the utilization factor // we know feedstock utilizations are double-arrays and the key ends with 'BioBoostProblem.UtilizationsName' foreach (var p in DoubleValues.Where(p => LayerDescriptor.Utilizations.IsSuffixOf(p.Key))) { var productLabel = LayerDescriptor.Utilizations.RemoveSuffixFrom(p.Key); yield return Tuple.Create(productLabel, p.Value.ToArray()); } } } public double GetUtilization(string feedstock, string currentRegion) { return DoubleValues[LayerDescriptor.Utilizations.NameWithPrefix(feedstock)][IndexOfLoc(currentRegion)]; } public void SetUtilization(string feedstock, string currentRegion, double newUtil) { SetUtilization(feedstock, IndexOfLoc(currentRegion), newUtil); } public void SetUtilization(string feedstock, int regionIdx, double newUtil) { DoubleValues[LayerDescriptor.Utilizations.NameWithPrefix(feedstock)][regionIdx] = newUtil; EvaluateAndUpdateAllResults(); } public void SetTransport(string product, int srcIdx, int destIdx) { IntValues[LayerDescriptor.TransportTargets.NameWithPrefix(product)][srcIdx] = destIdx; EvaluateAndUpdateAllResults(); } public void SetTransport(string product, string srcNuts, string destNuts) { SetTransport(product, IndexOfLoc(srcNuts), IndexOfLoc(destNuts)); } public void EvaluateAndUpdateAllResults() { if (problemDataRef == null) { // for old persisted problems throw new NotSupportedException("The problem data has not been set for this solution. Editing and re-evaluation is not possible"); } var evalScope = AggregateEvaluator.Evaluate( problemDataRef, DoubleValues.Where(kvp => LayerDescriptor.Utilizations.IsSuffixOf(kvp.Key)), IntValues.Where(kvp => LayerDescriptor.TransportTargets.IsSuffixOf(kvp.Key)) ); var newSolution = new BioBoostCompoundSolution(evalScope, problemDataRef); this.UpdateTo(newSolution); } private int IndexOfLoc(string locationName) { for (int i = 0; i < LocationNames.Length; i++) { if (LocationNames[i] == locationName) return i; } return -1; } #endregion public override void CollectParameterValues(IDictionary values) { base.CollectParameterValues(values); // also produce additional values (virtual parameters) for most important results foreach (var kvp in DoubleValues) { var vFiltered = kvp.Value.Where(x => x > 0); if (!vFiltered.Any()) continue; if (kvp.Key.Contains("Relative") || kvp.Key.Contains("Capacities")) { values.Add(kvp.Key + " (min)", new DoubleValue(vFiltered.Min())); values.Add(kvp.Key + " (max)", new DoubleValue(vFiltered.Max())); values.Add(kvp.Key + " (avg)", new DoubleValue(vFiltered.Average())); } else if (kvp.Key.Contains("Utilization")) { values.Add(kvp.Key + " (count)", new DoubleValue(vFiltered.Count())); // |{u | u > 0}| values.Add(kvp.Key + " (avg)", new DoubleValue(vFiltered.Average())); } else if (!kvp.Key.Contains("Potentials")) { values.Add(kvp.Key + " (sum)", new DoubleValue(kvp.Value.Sum())); values.Add(kvp.Key + " (count)", new DoubleValue(vFiltered.Count())); values.Add(kvp.Key + " (min)", new DoubleValue(vFiltered.Min())); values.Add(kvp.Key + " (max)", new DoubleValue(vFiltered.Max())); values.Add(kvp.Key + " (avg)", new DoubleValue(vFiltered.Average())); } } var finalProductNames = DoubleValues.Keys.Where(k => LayerDescriptor.AmountsAtSource.IsSuffixOf(k)) .Select(k => LayerDescriptor.AmountsAtSource.RemoveSuffixFrom(k)); finalProductNames = finalProductNames .Where(k => DoubleValues.ContainsKey(LayerDescriptor.DischargeCosts.NameWithPrefix(k)) && DoubleValues[LayerDescriptor.DischargeCosts.NameWithPrefix(k)].Sum() < 0); foreach (var finalProductName in finalProductNames) { var costs = DoubleValues .Where(kvp => !kvp.Key.StartsWith(finalProductName)) .Where(kvp => kvp.Key.EndsWith("Costs") || kvp.Key.EndsWith("Cost")) .Select(kvp => kvp.Value.Sum()) .Sum(); var amount = DoubleValues[LayerDescriptor.AmountsAtSource.NameWithPrefix(finalProductName)].Sum(); values.Add(finalProductName + " net cost [€/a]", new DoubleValue(costs)); values.Add(finalProductName + " net cost [€/t]", new DoubleValue(costs / amount)); } } public double PerformPostProcessing(TextWriter logger = null) { // DeletePenalizedFeedstockTransportsUtilizations(); // DeletePlantsWithScalingFactorBelow(0.8, feedstockName); // DeletePlantsWithScalingFactorBelow(0.8, intermediateName); // DeleteEmptyPlants(intermediateName); // maybe this is covered by previous functions? string feedstockName = null, intermediateName = null; IDictionary decentralPlantSources = null, centralPlantSources = null; foreach (var transportTargets in IntValues.Where(kvp => LayerDescriptor.TransportTargets.IsSuffixOf(kvp.Key))) { var name = LayerDescriptor.TransportTargets.RemoveSuffixFrom(transportTargets.Key); var utilizationLayerName = LayerDescriptor.Utilizations.NameWithPrefix(name); var utilizationsKvp = DoubleValues.FirstOrDefault(kvp => kvp.Key == utilizationLayerName); if (utilizationsKvp.Key != null) { feedstockName = name; decentralPlantSources = transportTargets.Value.SourceIndices(); } else { intermediateName = name; centralPlantSources = transportTargets.Value.SourceIndices(); } } if (feedstockName == null || intermediateName == null || decentralPlantSources == null || centralPlantSources == null) throw new Exception("Post Processing Failed"); var utilizations = DoubleValues.First(kvp => kvp.Key == LayerDescriptor.Utilizations.NameWithPrefix(feedstockName)).Value; var feedstockUsageBefore = utilizations.Zip( (DoubleArray) ((IValueParameter) problemDataRef.FeedstockPotentials[feedstockName + "Potentials"]).Value, (u, p) => u*p).Sum(); // remove penalized transports var feedstockTransportPenaltyLayer = DoubleValues.First( kvp => kvp.Key == LayerDescriptor.ExceedingTransportDistancePenalty.NameWithPrefix(feedstockName)).Value; var removableSuppliers = feedstockTransportPenaltyLayer .Select((penalty, idx) => new {penalty, idx}) .Where(pi => pi.penalty > 0) .Select(pi => new List {pi.idx}).ToList(); // remove undersize central plants var centralConversion = ProblemDataReference.Conversions[feedstockName].First(); var minCentralScalingFactor = centralConversion.MinCapacity/centralConversion.DesignCapacity; var centralScalingFactors = DoubleValues.First(kvp => kvp.Key == LayerDescriptor.ScalingFactors.NameWithPrefix(intermediateName)).Value; removableSuppliers.AddRange(centralScalingFactors .Select((factor, idx) => new {factor, idx}) .Where(fi => fi.factor < minCentralScalingFactor) .Select(fi => GetSuppliers(new[] {centralPlantSources, decentralPlantSources}, fi.idx).ToList()) .Where(l => l.Count > 0)); // remove undersize decentral plants var decentralConversion = ProblemDataReference.Conversions[intermediateName].First(); var minDeCentralScalingFactor = decentralConversion.MinCapacity/decentralConversion.DesignCapacity; var decentralScalingFactors = DoubleValues.First(kvp => kvp.Key == LayerDescriptor.ScalingFactors.NameWithPrefix(feedstockName)).Value; removableSuppliers.AddRange(decentralScalingFactors .Select((factor, idx) => new {factor, idx}) .Where(fi => fi.factor < minDeCentralScalingFactor) .Select(fi => GetSuppliers(new[] {decentralPlantSources}, fi.idx).ToList()) .Where(l => l.Count > 0)); EvaluateAndUpdateAllResults(); if (logger != null) logger.Write("Pruning plants (q={0})...", Quality); int successes = 0; int k = 0, lastK = 0; foreach (var variant in removableSuppliers) { var oldUtilizations = (DoubleArray)utilizations.Clone(); var oldQuality = Quality; foreach (int j in variant) utilizations[j] = 0; DoubleValues.Remove(LayerDescriptor.Utilizations.NameWithPrefix(feedstockName)); DoubleValues.Add(LayerDescriptor.Utilizations.NameWithPrefix(feedstockName), utilizations); EvaluateAndUpdateAllResults(); if (Quality < oldQuality) { for (int i = 0; i lastK + removableSuppliers.Count/20 && logger != null) { logger.Write("{0}%...", k*100/removableSuppliers.Count); lastK = k; } } EvaluateAndUpdateAllResults(); if (logger != null) logger.WriteLine("... pruned {0} of {1} variants (q={2})", successes, removableSuppliers.Count, Quality); var feedstockUsageAfter = utilizations.Zip((DoubleArray) ((IValueParameter) problemDataRef.FeedstockPotentials[feedstockName + "Potentials"]).Value, (u, p) => u*p).Sum(); var utilizationFactor = feedstockUsageAfter/feedstockUsageBefore; return utilizationFactor; } public static IEnumerable GetSuppliers(IEnumerable> suppliers, int i) { throw new NotImplementedException(); // waiting for source for DictionaryExtensions // var lists = suppliers.ToList(); // switch (lists.Count) { // case 0: { // yield break; // } // case 1: { // foreach (var j in lists[0].ElementAtOrDefault(i, new int[0])) // yield return j; // break; // } // default: { // foreach ( // var x in lists[0].ApplyAt(i, // v => v.Select(j => GetSuppliers(lists.Skip(1), j)) // .Aggregate(Enumerable.Empty(), (l1, l2) => l1.Concat(l2)), // () => new int[0])) // yield return x; // break; // } // } } #region unused public void CreateJsonSolution() { var builder = new StringBuilder(); builder.Append("{ \"type\": \"FeatureCollection\", \n\"features\": ["); for (int i = 0; i < LocationNames.Length; i++) { if (i > 0) builder.Append(","); builder.Append("\n{ \"type\": \"Feature\", \"properties\": { \"NUTS_ID\": \""); builder.Append(LocationNames[i]); builder.Append("\""); foreach (var kvp in IntValues) { var target = LocationNames[i]; if (kvp.Value[i] >= 0) { target = LocationNames[kvp.Value[i]]; } builder.Append(",\n \""); builder.Append(kvp.Key); builder.Append("\": \""); builder.Append(target); builder.Append("\""); } foreach (var kvp in DoubleValues) { builder.Append(",\n \""); builder.Append(kvp.Key); builder.Append("\": "); builder.Append(kvp.Value[i].ToString("R", CultureInfo.InvariantCulture)); } builder.Append("} }"); } builder.Append("\n]\n}"); File.WriteAllText(@"c:\temp\test.json", builder.ToString()); } #endregion public string Filename { get; set; } } }