diff --git a/LadybugTools_Adapter/AdapterActions/Execute/CompareEPWKeyPlotCommand.cs b/LadybugTools_Adapter/AdapterActions/Execute/CompareEPWKeyPlotCommand.cs new file mode 100644 index 00000000..61081c23 --- /dev/null +++ b/LadybugTools_Adapter/AdapterActions/Execute/CompareEPWKeyPlotCommand.cs @@ -0,0 +1,124 @@ +/* + * This file is part of the Buildings and Habitats object Model (BHoM) + * Copyright (c) 2015 - 2026, the respective contributors. All rights reserved. + * + * Each contributor holds copyright over their respective contributions. + * The project versioning (Git) records all such contribution source information. + * + * + * The BHoM is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, or + * (at your option) any later version. + * + * The BHoM 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this code. If not, see . + */ + +using BH.Engine.Adapter; +using BH.Engine.Base; +using BH.oM.Adapter; +using BH.oM.Base; +using BH.oM.LadybugTools; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BH.Adapter.LadybugTools +{ + public partial class LadybugToolsAdapter : BHoMAdapter + { + private List RunCommand(CompareEPWKeyPlotCommand command, ActionConfig actionConfig) + { + bool ignoreEPWCheck = false; + if (actionConfig is LadybugConfig config) + ignoreEPWCheck = config.SkipEPWCheck; + + if (command.EPWFile == null) + { + BH.Engine.Base.Compute.RecordError($"{nameof(command.EPWFile)} input cannot be null."); + return null; + } + + if (!ignoreEPWCheck) + { + foreach (FileSettings epwFile1 in command.EPWCompareFiles) + { + if (!System.IO.File.Exists(epwFile1.GetFullFileName())) + { + BH.Engine.Base.Compute.RecordError($"File '{epwFile1.GetFullFileName()}' does not exist."); + return null; + } + } + + if (!System.IO.File.Exists(command.EPWFile.GetFullFileName())) + { + BH.Engine.Base.Compute.RecordError($"File '{command.EPWFile.GetFullFileName()}' does not exist."); + return null; + } + } + + if (command.EPWKey == EPWKey.Undefined) + { + BH.Engine.Base.Compute.RecordError("Please provide a valid EPW key."); + return null; + } + + string epwFile = System.IO.Path.GetFullPath(command.EPWFile.GetFullFileName()).Replace('\\', '/'); + List epwFileList = command.EPWCompareFiles.Select(e => e.GetFullFileName().Replace('\\', '/')).ToList(); + + // run the process + List args = new List + { + "--command", "plot/epw_comparison", + "-e", epwFile, + "-dtk", command.EPWKey.ToText(), + "-p", command.OutputLocation.Replace('\\', '/'), + "-el" //append compare epw file list here + }; + args.AddRange(epwFileList); + + if (command.PlotTimeseries) + args.Add("-l"); + + string result = ""; + bool success; + + if (m_httpClient != null) + { + Task<(string, bool)> task = Compute.SendHttp(m_httpClient, args); + task.Wait(); + (result, success) = task.Result; + } + else + { + //if the server was not running or some other error happened, try running the python directly. + string script = Path.Combine(Engine.LadybugTools.Query.PythonCodeDirectory(), "LadybugTools_Toolkit\\src\\ladybugtools_toolkit\\bhom", "run_wrapped.py"); + string cmdCommand = $"{m_environment.Executable} {script} {args.Select(x => x.Contains(' ') || string.IsNullOrEmpty(x) ? '"' + x + '"' : x).Aggregate((a, b) => a + " " + b)}"; + + result = Engine.Python.Compute.RunCommandStdout(command: cmdCommand, hideWindows: true).Split('\n').Last(); + } + + try + { + CustomObject obj = (CustomObject)BH.Engine.Serialiser.Convert.FromJson(result); + PlotInformation info = Convert.ToPlotInformation(obj, new NoData()); //this plot type doesn't have collection metadata yet... + m_executeSuccess = true; + return new List() { info }; + } + catch (Exception ex) + { + BH.Engine.Base.Compute.RecordError(ex, $"An error occurred when deserialising the output from the script.\n Python output: {result}"); + return new List(); + } + } + } +} diff --git a/LadybugTools_Adapter/Convert/MetaData/PlotInformation.cs b/LadybugTools_Adapter/Convert/MetaData/PlotInformation.cs index f0c58fe9..73b43795 100644 --- a/LadybugTools_Adapter/Convert/MetaData/PlotInformation.cs +++ b/LadybugTools_Adapter/Convert/MetaData/PlotInformation.cs @@ -43,7 +43,15 @@ public static PlotInformation ToPlotInformation(this CustomObject oldObject, ISi plotInformation.Image = oldObject.CustomData["figure"].ToString(); - plotInformation.OtherData = ToSimulationData((oldObject.CustomData["data"] as CustomObject).CustomData, toUpdate as dynamic); + Dictionary data = (oldObject.CustomData["data"] as CustomObject)?.CustomData; + + if (data == null) + { + plotInformation.OtherData = toUpdate; + return plotInformation; + } + + plotInformation.OtherData = ToSimulationData(data, toUpdate as dynamic); return plotInformation; @@ -380,6 +388,17 @@ private static SolarRadiationData ToSimulationData(this Dictionary oldObject, NoData toUpdate) + { + if (oldObject != null) + if (oldObject.TryGetValue("description", out object description)) + { + toUpdate.Description = description.ToString(); + } + + return toUpdate; + } + /**************************************************/ /**** Private Methods: Fallback ****/ /**************************************************/ diff --git a/LadybugTools_Engine/Python/pyproject.toml b/LadybugTools_Engine/Python/pyproject.toml index 0352fd41..790bf656 100644 --- a/LadybugTools_Engine/Python/pyproject.toml +++ b/LadybugTools_Engine/Python/pyproject.toml @@ -65,10 +65,10 @@ dependencies = [ "ladybug-radiance", "ladybug-rhino", "ladybug-vtk", - "lbt-dragonfly", - "lbt-honeybee", - "lbt-ladybug", - "lbt-recipes", + "lbt-dragonfly==0.12.99", + "lbt-honeybee==0.9.95", + "lbt-ladybug==0.27.150", + "lbt-recipes==0.26.23", "lockfile", "luigi", "matplotlib", @@ -82,8 +82,8 @@ dependencies = [ "py7zr", "pybcj", "pycryptodomex", - "pydantic", - "pydantic-openapi-helper", + "pydantic==1.10.22", + "pydantic-openapi-helper==0.2.11", "pyparsing", "pyppmd", "python-daemon", diff --git a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/bhom/run_wrapped.py b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/bhom/run_wrapped.py index a4fe4a5c..e0351591 100644 --- a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/bhom/run_wrapped.py +++ b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/bhom/run_wrapped.py @@ -14,6 +14,7 @@ #import methods and parsers from ladybugtools_toolkit.bhom.wrapped.plot.walkability_heatmap import PARSER as walkability_heatmap_parser, walkability_heatmap +from ladybugtools_toolkit.bhom.wrapped.plot.epw_comparison import PARSER as epw_comparison_parser, epw_comparison from ladybugtools_toolkit.bhom.wrapped.plot.windrose import PARSER as windrose_parser, windrose from ladybugtools_toolkit.bhom.wrapped.plot.directional_solar_radiation import PARSER as directional_solar_radiation_parser, directional_solar_radiation from ladybugtools_toolkit.bhom.wrapped.plot.diurnal import PARSER as diurnal_parser, diurnal @@ -37,6 +38,7 @@ #dictionary containing all the parsers for bhom/wrapped commands PARSERS = { "plot/walkability_heatmap": (walkability_heatmap_parser, walkability_heatmap), + "plot/epw_comparison": (epw_comparison_parser, epw_comparison), "plot/windrose": (windrose_parser, windrose), "plot/directional_solar_radiation": (directional_solar_radiation_parser, directional_solar_radiation), "plot/diurnal": (diurnal_parser, diurnal), diff --git a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/bhom/wrapped/plot/epw_comparison.py b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/bhom/wrapped/plot/epw_comparison.py new file mode 100644 index 00000000..1b085e08 --- /dev/null +++ b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/bhom/wrapped/plot/epw_comparison.py @@ -0,0 +1,100 @@ +"""Method to wrap for conversion of EPW to CSV file.""" +# pylint: disable=C0415,E0401,W0703 +import argparse +import os +import json +import sys +import traceback +import matplotlib +import matplotlib.figure +from ladybug.epw import EPW +from ladybugtools_toolkit.plot.compare import compare_epw_key_line, compare_epw_key_hist +from ladybugtools_toolkit.plot.utilities import figure_to_base64 +import matplotlib.pyplot as plt +from ...logger import CONSOLE_LOGGER +from typing import List + +PARSER = argparse.ArgumentParser( + description=( + "Given an EPW file path, and a list of epws to compare to, construct a line chart for a specific epw key." + ) +) +PARSER.add_argument( + "-e", + "--epw_file", + help="The EPW file to compare from", + type=str, + required=True, +) +PARSER.add_argument( + "-el", + "--epw_list", + help="List of EPW files to compare with the base", + type=str, + nargs='*', + action="extend", + required=True, +) +PARSER.add_argument( + "-dtk", + "--data_type_key", + help="Key to compare.", + type=str, + required=True, +) +PARSER.add_argument( + "-p", + "--save_path", + help="Path where to save the output image.", + type=str, + required=False, + ) +PARSER.add_argument( + "-l", + "--line", + help="Produce a line plot instead of a histogram", + action="store_true", + default=False + ) + +def epw_comparison(epw_file: str, epw_list: List[str], data_type_key: str, line:bool, save_path:str = None) -> str: + """Create a timeseries plot with a line for each epw file for the specified data key and return it in a format readable by the LadybugToolsAdapter.""" + try: + style = os.environ.get("BHOM_style_context", "python_toolkit.bhom") + + with plt.style.context(style): + fig, ax = plt.subplots() + + epws = [EPW(epw_file)] + epws.extend([EPW(f) for f in epw_list]) + + if line: + compare_epw_key_line(epws, key=data_type_key.lower().strip().replace(" ", "_"), style_context=style, ax=ax) + else: + compare_epw_key_hist(epws, key=data_type_key.lower().strip().replace(" ", "_"), style_context=style, ax=ax) + + return_dict = {} + + if save_path == None or save_path == "": + base64 = figure_to_base64(fig,html=False) + return_dict["figure"] = base64 + else: + fig.savefig(save_path, dpi=150, transparent=True) + return_dict["figure"] = save_path + + plt.close(fig) + + return_dict["data"] = None #Unsure of how to create representative collection metadata for a comparison plot type that doesn't simply list every epw file compared + + return json.dumps(return_dict, default=str) + + except Exception: + CONSOLE_LOGGER.error("Timeseries comparison could not be created.", exc_info=1) + return traceback.format_exc() + +if __name__ == "__main__": + + args = PARSER.parse_args() + matplotlib.use("Agg") + + epw_comparison(args.epw_file, args.epw_list, args.data_type_key, args.line, args.save_path) diff --git a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/ladybug_extension/epw.py b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/ladybug_extension/epw.py index 3a00206e..55516659 100644 --- a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/ladybug_extension/epw.py +++ b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/ladybug_extension/epw.py @@ -41,7 +41,38 @@ from .location import average_location, location_to_string # pylint: enable=E0401 - +EPW_PROPERTIES = [ + "aerosol_optical_depth", + "albedo", + "atmospheric_station_pressure", + "ceiling_height", + "days_since_last_snowfall", + "dew_point_temperature", + "diffuse_horizontal_illuminance", + "diffuse_horizontal_radiation", + "direct_normal_illuminance", + "direct_normal_radiation", + "dry_bulb_temperature", + "extraterrestrial_direct_normal_radiation", + "extraterrestrial_horizontal_radiation", + "global_horizontal_illuminance", + "global_horizontal_radiation", + "horizontal_infrared_radiation_intensity", + "liquid_precipitation_depth", + "liquid_precipitation_quantity", + "opaque_sky_cover", + "precipitable_water", + "present_weather_codes", + "present_weather_observation", + "relative_humidity", + "snow_depth", + "total_sky_cover", + "visibility", + "wind_direction", + "wind_speed", + "years", + "zenith_luminance", + ] @@ -66,41 +97,8 @@ def epw_to_dataframe( A Pandas DataFrame containing the source EPW data. """ - properties = [ - "aerosol_optical_depth", - "albedo", - "atmospheric_station_pressure", - "ceiling_height", - "days_since_last_snowfall", - "dew_point_temperature", - "diffuse_horizontal_illuminance", - "diffuse_horizontal_radiation", - "direct_normal_illuminance", - "direct_normal_radiation", - "dry_bulb_temperature", - "extraterrestrial_direct_normal_radiation", - "extraterrestrial_horizontal_radiation", - "global_horizontal_illuminance", - "global_horizontal_radiation", - "horizontal_infrared_radiation_intensity", - "liquid_precipitation_depth", - "liquid_precipitation_quantity", - "opaque_sky_cover", - "precipitable_water", - "present_weather_codes", - "present_weather_observation", - "relative_humidity", - "snow_depth", - "total_sky_cover", - "visibility", - "wind_direction", - "wind_speed", - "years", - "zenith_luminance", - ] - all_series = [] - for prop in properties: + for prop in EPW_PROPERTIES: try: s = collection_to_series(getattr(epw, prop)) # s.rename((Path(epw.file_path).stem, "EPW", s.name), inplace=True) diff --git a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/compare.py b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/compare.py new file mode 100644 index 00000000..be218ce4 --- /dev/null +++ b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/compare.py @@ -0,0 +1,64 @@ +from ladybug.epw import EPW, EPWFields +import matplotlib.pyplot as plt +import pandas as pd +import numpy as np +from pathlib import Path +from ..ladybug_extension.epw import collection_to_series, EPW_PROPERTIES +from python_toolkit.bhom.analytics import bhom_analytics +from python_toolkit.plot.timeseries import timeseries + +@bhom_analytics() +def compare_epw_key_hist( + epws: list[EPW], + key: str, + ax: plt.Axes = None, + bins: list[float] = None, + **kwargs + ) -> plt.Axes: + + if key not in EPW_PROPERTIES: + raise ValueError(f"The key: {key}, is not a valid epw key. Please select one from the list in: ladybugtools_toolkit.ladybug_extension.epw EPW_PROPERTIES") + + serieses = [collection_to_series(getattr(i, key)) for i in epws] + df = pd.concat(serieses, axis=1, keys=[Path(epw.file_path).stem for epw in epws]) + + if bins is None: + bins = np.linspace(df.values.min(), df.values.max(), 31) + elif len(bins) == 0: + bins = np.linspace(df.values.min(), df.values.max(), 31) + + style_context = kwargs.pop("style_context", "python_toolkit.bhom") + with plt.style.context(style_context): + if ax is None: + ax = plt.gca() + ax.hist(df.values, bins=bins, label = df.columns, density=False, **kwargs) + ax.legend() + ax.set_ylabel("Number of hours (/8760)") + ax.set_xlabel(serieses[0].name) + return ax + +@bhom_analytics() +def compare_epw_key_line( + epws: list[EPW], + key: str, + ax: plt.Axes = None, + **kwargs + ) -> plt.Axes: + + if key not in EPW_PROPERTIES: + raise ValueError(f"The key: {key}, is not a valid epw key. Please select one from the list in: ladybugtools_toolkit.ladybug_extension.epw EPW_PROPERTIES") + + serieses = [collection_to_series(getattr(epw, key)) for epw in epws] + + style_context = kwargs.get("style_context", "python_toolkit.bhom") + with plt.style.context(style_context): + if ax is None: + ax = plt.gca() + + for series, epw in zip(serieses, epws): + ax = timeseries(series, ax=ax, label=Path(epw.file_path).stem, **kwargs) + + ax.set_ylabel(serieses[0].name) + ax.legend() + + return ax \ No newline at end of file diff --git a/LadybugTools_Engine/Python/tests/test_plot.py b/LadybugTools_Engine/Python/tests/test_plot.py index a1de7509..0fecf605 100644 --- a/LadybugTools_Engine/Python/tests/test_plot.py +++ b/LadybugTools_Engine/Python/tests/test_plot.py @@ -5,6 +5,7 @@ from ladybug.analysisperiod import AnalysisPeriod from ladybug_comfort.collection.utci import UTCI from ladybugtools_toolkit.ladybug_extension.datacollection import collection_to_series +from ladybugtools_toolkit.ladybug_extension.epw import EPW_PROPERTIES from ladybugtools_toolkit.plot._degree_days import ( cooling_degree_days, degree_days, @@ -26,6 +27,7 @@ utci_shade_benefit, ) from ladybugtools_toolkit.plot.colormaps import colormap_sequential +from ladybugtools_toolkit.plot.compare import compare_epw_key_line, compare_epw_key_hist from ladybugtools_toolkit.plot.utilities import ( colormap_sequential, contrasting_color, @@ -365,3 +367,15 @@ def test_shade_benefit_plot() -> None: def test_seasonality_plot() -> None: """_""" assert isinstance(seasonality_comparison(epw=EPW_OBJ), plt.Axes) + +def test_epw_comparison() -> None: + """_""" + epw_objs = [EPW_OBJ, EPW_OBJ] #compare to self as the visual does not matter, only that it works + + assert EPW_PROPERTIES[10] == "dry_bulb_temperature" #handle regression of properties + + assert isinstance(compare_epw_key_line(epw_objs, EPW_PROPERTIES[10]), plt.Axes) + plt.close("all") + + assert isinstance(compare_epw_key_hist(epw_objs, EPW_PROPERTIES[10]), plt.Axes) + plt.close("all") \ No newline at end of file diff --git a/LadybugTools_oM/ExecuteCommands/CompareEPWKeyPlotCommand.cs b/LadybugTools_oM/ExecuteCommands/CompareEPWKeyPlotCommand.cs new file mode 100644 index 00000000..94f14c78 --- /dev/null +++ b/LadybugTools_oM/ExecuteCommands/CompareEPWKeyPlotCommand.cs @@ -0,0 +1,48 @@ +/* + * This file is part of the Buildings and Habitats object Model (BHoM) + * Copyright (c) 2015 - 2026, the respective contributors. All rights reserved. + * + * Each contributor holds copyright over their respective contributions. + * The project versioning (Git) records all such contribution source information. + * + * + * The BHoM is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, or + * (at your option) any later version. + * + * The BHoM 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this code. If not, see . + */ + +using BH.oM.Adapter; +using System; +using System.Collections.Generic; +using System.Text; +using System.ComponentModel; + +namespace BH.oM.LadybugTools +{ + public class CompareEPWKeyPlotCommand : ISimulationCommand + { + [Description("The EPW file that acts as the base for comparisons.")] + public virtual FileSettings EPWFile { get; set; } = new FileSettings(); + + [Description("Key (e.g. Dry Bulb Temperature) to compare.")] + public virtual EPWKey EPWKey { get; set; } = EPWKey.Undefined; + + [Description("The list of EPW files to be compared with the base file (or each other).")] + public virtual List EPWCompareFiles { get; set; } = new List(); + + [Description("Whether to plot a time series chart. If set to false, plots data as a histogram instead.")] + public virtual bool PlotTimeseries { get; set; } = true; + + [Description("The location to place the image file once complete.")] + public virtual string OutputLocation { get; set; } = ""; + } +} diff --git a/LadybugTools_oM/MetaData/NoData.cs b/LadybugTools_oM/MetaData/NoData.cs new file mode 100644 index 00000000..f460ae5c --- /dev/null +++ b/LadybugTools_oM/MetaData/NoData.cs @@ -0,0 +1,39 @@ +/* + * This file is part of the Buildings and Habitats object Model (BHoM) + * Copyright (c) 2015 - 2026, the respective contributors. All rights reserved. + * + * Each contributor holds copyright over their respective contributions. + * The project versioning (Git) records all such contribution source information. + * + * + * The BHoM is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, or + * (at your option) any later version. + * + * The BHoM 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this code. If not, see . + */ + +using BH.oM.Base; +using BH.oM.Base.Attributes; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Text; + +namespace BH.oM.LadybugTools +{ + [NoAutoConstructor] + public class NoData : ISimulationData + { + public virtual string Description { get; set; } = "There is not additional data for this object"; + } +} + +