namespace ResultsShared { using System.Globalization; /// /// Useful utility classes relating to calculation results that have been saved to file /// after a calculation has been run. /// public static class Utils { public const string PerformanceStatisticsXmlFileName = "Performance Statistics.xml"; public const string LayoutStatisticsFileName = "Site Layout Statistics.xml"; public const string CalculationSettingsFileName = "Calculation Settings.xml"; public const string AlbedoFileName = "Albedo Settings.xml"; public const string SoilingFileName = "Soiling Settings.xml"; public const string YearlyResultsFileNameFormatString = "Annual Yield Results and Effects ({0}).xml"; public const string YearlyResultsFileSearchString = "Annual Yield Results and Effects (*).xml"; public const string LogFileSearchString = "SolarFarmer log *"; public const string MetaDataFileName = "Metadata.tsv"; private static readonly string RowPerResultsTsvFileName = "Results (row per result).tsv"; private static readonly string RowPerColumnTsvFileName = "Results (column per result).tsv"; /// /// Output the results list to TSV files. It goes through each sub-folder in the specified folder looking /// for the necessary statistics files (e.g. layout statistics, yearly results, etc.) and if it finds them, creates /// a object, then outputs two TSV files - listing the results so they are more /// easily compared. /// /// The full path of the folder containing all the folders containing results. public static void SummariseResultsToTsv(string resultsFolderPath) { if (string.IsNullOrWhiteSpace(resultsFolderPath)) { Toolbox.Log("The folder path is empty. Please supply a folder path", LogLevel.Error); return; } if (!Directory.Exists(resultsFolderPath)) { Toolbox.Log(string.Format("The folder path \"{0}\" does not exist. Please supply a folder path that exists.", resultsFolderPath), LogLevel.Error); return; } // Go through each sub-folder, looking for the necessary files (layout statistics, performance statistics, yearly results) // and if they exist, create a Results object - then push it through the same function as before. List resultsList = new List(); foreach (string subFolderPath in Directory.EnumerateDirectories(resultsFolderPath)) { Results results = ReadResultsFromFolder(subFolderPath); if (results != null) { resultsList.Add(results); } } OutputResultsListToTsv(resultsList, resultsFolderPath); } /// /// Creates a results folder (from the current time and date) in the base folder you provide. /// /// The base folder. /// The full folder path of the created results folder. public static string CreateResultsFolderFromCurrentDateAndTime(string baseFolderPath) { string currentDateTimeFolderName = DateTime.Now.ToString("yyyy-MM-dd HH-mm-ss"); string resultsFolderPath = Path.Combine(baseFolderPath, currentDateTimeFolderName); Directory.CreateDirectory(resultsFolderPath); return resultsFolderPath; } private static Results ReadResultsFromFolder(string folderPath) { if (string.IsNullOrWhiteSpace(folderPath)) { Toolbox.Log("The folder path is empty. Please supply a folder path", LogLevel.Error); return null; } if (!Directory.Exists(folderPath)) { Toolbox.Log(string.Format("The folder path \"{0}\" does not exist. Please supply a folder path that exists.", folderPath), LogLevel.Error); return null; } string folderName = Path.GetFileName(folderPath); string layoutStatsFilePath = Path.Combine(folderPath, LayoutStatisticsFileName); string perfStatsFilePath = Path.Combine(folderPath, PerformanceStatisticsXmlFileName); string calcSettingsFilePath = Path.Combine(folderPath, CalculationSettingsFileName); string albedoFilePath = Path.Combine(folderPath, AlbedoFileName); if (File.Exists(layoutStatsFilePath)) { // Now look for any yearly results files string[] yearlyResultsFilePaths = Directory.GetFiles(folderPath, YearlyResultsFileSearchString); if (yearlyResultsFilePaths.Any()) { // There is at least one. So good to go SiteLayoutStatistics siteLayoutStatistics = SiteLayoutStatistics.LoadFromFile(layoutStatsFilePath); PerformanceStatistics performanceStatistics = new PerformanceStatistics(); if (File.Exists(perfStatsFilePath)) { performanceStatistics = PerformanceStatistics.LoadFromFile(perfStatsFilePath); } CalculationSettings calculationSettings = CalculationSettings.LoadFromFile(calcSettingsFilePath); ScenarioAlbedoData albedoData = ScenarioAlbedoData.LoadFromFile(albedoFilePath); List metaData = LoadMetaData(Path.Combine(folderPath, MetaDataFileName)); List allAnnualResults = new List(); foreach (string yearlyResultsFilePath in yearlyResultsFilePaths) { AnnualEnergyYieldResults annualResults = AnnualEnergyYieldResults.LoadFromFile(yearlyResultsFilePath); allAnnualResults.Add(annualResults); } return new Results(folderName, siteLayoutStatistics, performanceStatistics, calculationSettings, albedoData, allAnnualResults, metaData); } } // Was not a valid folder - i.e. it didn't contain the appropriate files. return null; } /// /// Outputs the specified results list to two TSV files in the specified folder path. /// One TSV file has a result per row. /// The other TSV file has a results per column. /// /// The results list to use in the TSV files. /// The full folder path to save the TSV files. private static void OutputResultsListToTsv(List resultsList, string folderPathToSaveTsvFiles) { Results firstResults = resultsList.FirstOrDefault(); List columns = new List() { new HeaderAndFunc("Site Layout", r => r.SiteLayoutName), }; // Add any meta data columns. This currently assumes that all results will have the same meta data if (firstResults != null) { for (int i = 0; i < firstResults.MetaDataList.Count; i++) { int index = i; columns.Add(new HeaderAndFunc(firstResults.MetaDataList[index].PropertyName, r => r.MetaDataList[index].PropertyValue.ToString())); } } columns.Add(new HeaderAndFunc("Num inverters", r => r.SiteLayoutStatistics.ComponentStatistics.FirstOrDefault(c => c.ComponentType == "Inverter").Count.ToString(CultureInfo.CurrentCulture))); columns.Add(new HeaderAndFunc("Num racks", r => r.SiteLayoutStatistics.PvSystemStatistics.First().RackCount.ToString(CultureInfo.CurrentCulture))); columns.Add(new HeaderAndFunc("Num modules", r => r.SiteLayoutStatistics.PvSystemStatistics.First().ModuleCount.ToString(CultureInfo.CurrentCulture))); columns.Add(new HeaderAndFunc("Num strings", r => r.SiteLayoutStatistics.PvSystemStatistics.First().StringCount.ToString(CultureInfo.CurrentCulture))); columns.Add(new HeaderAndFunc("Calc type", r => r.CalculationSettings.EnergyYieldCalculationType.ToString())); columns.Add(new HeaderAndFunc("Annual albedo", r => r.AlbedoData.OverallAlbedoValue.ToString(CultureInfo.CurrentCulture))); columns.Add(new HeaderAndFunc("Energy Yield (kWh/kWp)", r => r.YearlyResultsList.First().EnergyYield.ToString(CultureInfo.CurrentCulture))); columns.Add(new HeaderAndFunc("Net energy (MWh/year)", r => r.YearlyResultsList.First().NetEnergy.ToString(CultureInfo.CurrentCulture))); columns.Add(new HeaderAndFunc("Performance Ratio (%)", r => r.YearlyResultsList.First().PerformanceRatio.ToString(CultureInfo.CurrentCulture))); columns.Add(new HeaderAndFunc("Near shading effect (%)", r => (r.YearlyResultsList.First().AnnualEffects.NearShadingIrradiance * 100.0).ToString(CultureInfo.CurrentCulture))); columns.Add(new HeaderAndFunc("Production DC (kWh)", r => r.YearlyResultsList.First().PDC.ToString(CultureInfo.CurrentCulture))); columns.Add(new HeaderAndFunc("Production AC (kWh)", r => r.YearlyResultsList.First().PAC.ToString(CultureInfo.CurrentCulture))); columns.Add(new HeaderAndFunc("GHI (kWh/m2)", r => r.YearlyResultsList.First().GHI.ToString(CultureInfo.CurrentCulture))); columns.Add(new HeaderAndFunc("GI (kWh/m2)", r => r.YearlyResultsList.First().GI.ToString(CultureInfo.CurrentCulture))); columns.Add(new HeaderAndFunc("Average Temp (C)", r => r.YearlyResultsList.First().AverageTemperature.ToString(CultureInfo.CurrentCulture))); columns.Add(new HeaderAndFunc("Gain on tilted plane (%)", r => r.YearlyResultsList.First().GainOnTiltedPlane.ToString(CultureInfo.CurrentCulture))); columns.Add(new HeaderAndFunc("Far Shading/Horizon effect (%)", r => (r.YearlyResultsList.First().AnnualEffects.Horizon * 100.0).ToString(CultureInfo.CurrentCulture))); columns.Add(new HeaderAndFunc("Angular effect (%)", r => (r.YearlyResultsList.First().AnnualEffects.Angular * 100.0).ToString(CultureInfo.CurrentCulture))); columns.Add(new HeaderAndFunc("Soiling effect (%)", r => (r.YearlyResultsList.First().AnnualEffects.Soiling * 100.0).ToString(CultureInfo.CurrentCulture))); columns.Add(new HeaderAndFunc("Modelling effect (%)", r => (r.YearlyResultsList.First().AnnualEffects.Modelling * 100.0).ToString(CultureInfo.CurrentCulture))); columns.Add(new HeaderAndFunc("Modelling correction effect (%)", r => (r.YearlyResultsList.First().AnnualEffects.ModellingCorrection * 100.0).ToString(CultureInfo.CurrentCulture))); columns.Add(new HeaderAndFunc("Temperature effect (%)", r => (r.YearlyResultsList.First().AnnualEffects.Temperature * 100.0).ToString(CultureInfo.CurrentCulture))); columns.Add(new HeaderAndFunc("Irradiance effect (%)", r => (r.YearlyResultsList.First().AnnualEffects.Irradiance * 100.0).ToString(CultureInfo.CurrentCulture))); columns.Add(new HeaderAndFunc("Module mismatch effect (%)", r => (r.YearlyResultsList.First().AnnualEffects.ModuleMismatch * 100.0).ToString(CultureInfo.CurrentCulture))); columns.Add(new HeaderAndFunc("Electrical mismatch effect (%)", r => (r.YearlyResultsList.First().AnnualEffects.ElectricalMismatch * 100.0).ToString(CultureInfo.CurrentCulture))); columns.Add(new HeaderAndFunc("Module quality effect (%)", r => (r.YearlyResultsList.First().AnnualEffects.ModuleQuality * 100.0).ToString(CultureInfo.CurrentCulture))); columns.Add(new HeaderAndFunc("Spectral effect (%)", r => (r.YearlyResultsList.First().AnnualEffects.Spectral * 100.0).ToString(CultureInfo.CurrentCulture))); columns.Add(new HeaderAndFunc("Back irradiance gain effect (%)", r => (r.YearlyResultsList.First().AnnualEffects.BackIrradianceGain * 100.0).ToString(CultureInfo.CurrentCulture))); columns.Add(new HeaderAndFunc("Power binning effect (%)", r => (r.YearlyResultsList.First().AnnualEffects.PowerBinning * 100.0).ToString(CultureInfo.CurrentCulture))); columns.Add(new HeaderAndFunc("LID effect (%)", r => (r.YearlyResultsList.First().AnnualEffects.LightInducedDegradation * 100.0).ToString(CultureInfo.CurrentCulture))); columns.Add(new HeaderAndFunc("Ohmic DC effect (%)", r => (r.YearlyResultsList.First().AnnualEffects.OhmicDc * 100.0).ToString(CultureInfo.CurrentCulture))); columns.Add(new HeaderAndFunc("Ohmic AC effect (%)", r => (r.YearlyResultsList.First().AnnualEffects.OhmicAc * 100.0).ToString(CultureInfo.CurrentCulture))); columns.Add(new HeaderAndFunc("Transformer effect (%)", r => (r.YearlyResultsList.First().AnnualEffects.Transformer * 100.0).ToString(CultureInfo.CurrentCulture))); columns.Add(new HeaderAndFunc("Availability effect (%)", r => (r.YearlyResultsList.First().AnnualEffects.Availability * 100.0).ToString(CultureInfo.CurrentCulture))); columns.Add(new HeaderAndFunc("Inverter Min DC effect (%)", r => (r.YearlyResultsList.First().AnnualEffects.InverterMinDcVoltage * 100.0).ToString(CultureInfo.CurrentCulture))); columns.Add(new HeaderAndFunc("Inverter Max AC Power effect (%)", r => (r.YearlyResultsList.First().AnnualEffects.InverterMaxAcPower * 100.0).ToString(CultureInfo.CurrentCulture))); columns.Add(new HeaderAndFunc("Inverter Max DC Current effect (%)", r => (r.YearlyResultsList.First().AnnualEffects.InverterMaxDcCurrent * 100.0).ToString(CultureInfo.CurrentCulture))); columns.Add(new HeaderAndFunc("Inverter Max DC Voltage effect (%)", r => (r.YearlyResultsList.First().AnnualEffects.InverterMaxDcVoltage * 100.0).ToString(CultureInfo.CurrentCulture))); columns.Add(new HeaderAndFunc("Inverter Min DC Power effect (%)", r => (r.YearlyResultsList.First().AnnualEffects.InverterMinDcPower * 100.0).ToString(CultureInfo.CurrentCulture))); columns.Add(new HeaderAndFunc("Inverter Efficiency effect (%)", r => (r.YearlyResultsList.First().AnnualEffects.InverterEfficiency * 100.0).ToString(CultureInfo.CurrentCulture))); columns.Add(new HeaderAndFunc("Inverter Overpower Shutdown effect (%)", r => (r.YearlyResultsList.First().AnnualEffects.InverterOverPowerShutdown * 100.0).ToString(CultureInfo.CurrentCulture))); columns.Add(new HeaderAndFunc("Inverter Tare effect (%)", r => (r.YearlyResultsList.First().AnnualEffects.InverterTare * 100.0).ToString(CultureInfo.CurrentCulture))); columns.Add(new HeaderAndFunc("AC output (kW)", r => r.SiteLayoutStatistics.PvSystemStatistics.First().AcOutput.ToString(CultureInfo.CurrentCulture))); columns.Add(new HeaderAndFunc("DC Power (kW)", r => r.SiteLayoutStatistics.PvSystemStatistics.First().DcPower.ToString(CultureInfo.CurrentCulture))); columns.Add(new HeaderAndFunc("DC/AC Ratio", r => r.SiteLayoutStatistics.PvSystemStatistics.First().DcAcRatio.ToString(CultureInfo.CurrentCulture))); Toolbox.Log(string.Format("Added {0} columns", columns.Count)); OutputResultsToTsvRowForEachResult(resultsList, columns, Path.Combine(folderPathToSaveTsvFiles, RowPerResultsTsvFileName)); OutputResultsToTsvColumnForEachResult(resultsList, columns, Path.Combine(folderPathToSaveTsvFiles, RowPerColumnTsvFileName)); } /// /// Loads metadata from the specified file. /// /// The file path. /// A list of meta data (or an empty list if there isn't a file). private static List LoadMetaData(string filePath) { List metaDataList = new List(); if ((string.IsNullOrWhiteSpace(filePath)) || (!File.Exists(filePath))) { return metaDataList; } string fileContents = File.ReadAllText(filePath); string[] lines = fileContents.Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); foreach (string line in lines.Where(l => !string.IsNullOrWhiteSpace(l))) { string[] content = line.Split('\t'); if (content.Length == 2) { string propertyName = content[0]; object propertyValue = content[1]; metaDataList.Add(new MetaData(propertyName, propertyValue)); } } return metaDataList; } private static void OutputResultsToTsvRowForEachResult(List resultsList, List columns, string outputFilePath) { StringBuilder sb = new StringBuilder(); // First, output the headers sb.AppendLine(string.Join("\t", columns.Select(c => c.Header))); foreach (Results results in resultsList) { if (!results.YearlyResultsList.Any()) { // Hmmm, no yearly results Toolbox.Log(string.Format("!!! There are no yearly results for site layout {0}", results.SiteLayoutName), LogLevel.Error); } else { List strings = new List(); foreach (HeaderAndFunc headerAndFunc in columns) { strings.Add(headerAndFunc.Func(results)); } sb.AppendLine(string.Join("\t", strings)); } } SaveStringBuilderToFile(sb, outputFilePath); } private static void SaveStringBuilderToFile(StringBuilder sb, string filePath) { try { System.IO.File.WriteAllText(filePath, sb.ToString()); Toolbox.Log(string.Format("Written file to \"{0}\"", filePath)); } catch (Exception ex) { Toolbox.Log(string.Format("Could not write file to \"{0}\". Error message: {1}", filePath, ex.Message), LogLevel.Error); } } private static void OutputResultsToTsvColumnForEachResult(List resultsList, List columns, string outputFilePath) { StringBuilder sb = new StringBuilder(); // Do a row for each result foreach (HeaderAndFunc headerAndFunc in columns) { List strings = new List(); strings.Add(headerAndFunc.Header); foreach (Results results in resultsList) { strings.Add(headerAndFunc.Func(results)); } sb.AppendLine(string.Join("\t", strings)); } SaveStringBuilderToFile(sb, outputFilePath); } /// /// Represents a header and a function for how to get the value from a Results object. /// private class HeaderAndFunc { /// /// Initializes a new instance of the class. /// /// The header. /// The function to get the value (string) from a Results object. public HeaderAndFunc(string header, Func func) { this.Header = header; this.Func = func; } /// /// Gets or sets the header. /// public string Header { get; set; } /// /// Gets or sets the function. /// public Func Func { get; set; } } } /// /// Class to hold any additional meta-data you would like output with the results. /// public class MetaData { /// /// Initializes a new instance of the class. /// /// The name of the property of the metadata. /// The value of the metadata. public MetaData(string propertyName, object propertyValue) { this.PropertyName = propertyName; this.PropertyValue = propertyValue; } /// /// The name of the property of the metadata. /// public string PropertyName { get; private set; } /// /// The value fo the metadata. /// public object PropertyValue { get; private set; } } /// /// Encapsulates the results from an energy yield calculation scenario /// public class Results { /// /// Initializes a new instance of the class. /// /// The site layout that the results were generated from. /// The calculation scenario produced from running the energy calculation. /// The performance statistics from running the energy calculation. /// The calculation settings used when running the energy calculation. /// Extra meta data - used to add extra columns to .tsv files. public Results( SiteLayout siteLayout, EnergyYieldCalculationScenario scenario, PerformanceStatistics performanceStatistics, CalculationSettings calculationSettings, IEnumerable extraMetaData) : this(extraMetaData) { this.SiteLayoutName = siteLayout.Name; this.SiteLayoutStatistics = siteLayout.Statistics; this.YearlyResultsList = new List(); this.YearlyResultsList.AddRange(scenario.YearlyResultsList); this.PerformanceStatistics = performanceStatistics; this.CalculationSettings = calculationSettings; } /// /// Initializes a new instance of the class. /// /// The name of the site layout. /// The site layout statistics of the site. /// The performance statistics from running the energy calculation. /// The calculation settings used when running the energy calculation. /// The albedo data. /// The yearly results from running the energy calculation. /// Extra meta data - used to add extra columns to .tsv files. public Results( string siteLayoutName, SiteLayoutStatistics siteLayoutStatistics, PerformanceStatistics performanceStatistics, CalculationSettings calculationSettings, ScenarioAlbedoData albedoData, IEnumerable yearlyResults, IEnumerable extraMetaData) : this(extraMetaData) { this.SiteLayoutName = siteLayoutName; this.SiteLayoutStatistics = siteLayoutStatistics; this.PerformanceStatistics = performanceStatistics; this.YearlyResultsList = new List(); this.YearlyResultsList.AddRange(yearlyResults); this.CalculationSettings = calculationSettings; this.AlbedoData = albedoData; } /// /// The name of the site layout. /// public string SiteLayoutName { get; set; } /// /// The site layout statistics. /// public SiteLayoutStatistics SiteLayoutStatistics { get; set; } /// /// The performance statistics. /// public PerformanceStatistics PerformanceStatistics { get; set; } /// /// The calculation statistics. /// public CalculationSettings CalculationSettings { get; set; } /// /// The list of yearly results. /// public List YearlyResultsList { get; set; } /// /// Gets or sets the albedo data. /// public ScenarioAlbedoData AlbedoData { get; set; } /// /// The list of optional metadata. /// public List MetaDataList { get; private set; } protected Results(IEnumerable extraMetaData) { this.MetaDataList = new List(); if (extraMetaData != null) { this.MetaDataList.AddRange(extraMetaData); } } } }