diff --git a/.github/workflows/spdx-check.yml b/.github/workflows/spdx-check-sam-ui.yml
similarity index 63%
rename from .github/workflows/spdx-check.yml
rename to .github/workflows/spdx-check-sam-ui.yml
index a3925fae..de33f8eb 100644
--- a/.github/workflows/spdx-check.yml
+++ b/.github/workflows/spdx-check-sam-ui.yml
@@ -1,7 +1,8 @@
-name: SPDX + Copyright header check
+name: SPDX + Copyright header check (SAM_UI special case)
on:
pull_request:
+ workflow_dispatch:
jobs:
spdx:
@@ -9,9 +10,11 @@ jobs:
permissions:
contents: read
pull-requests: read
+
steps:
- name: Check SPDX + copyright header in changed .cs files
env:
+ EVENT_NAME: ${{ github.event_name }}
GH_TOKEN: ${{ github.token }}
PR: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
@@ -20,14 +23,20 @@ jobs:
run: |
set -euo pipefail
+ if [ "$EVENT_NAME" != "pull_request" ]; then
+ echo "Manual run detected."
+ echo "This workflow checks files changed in a pull request, so there is nothing to do without PR context."
+ exit 0
+ fi
+
echo "Repo: $REPO"
echo "PR: $PR"
echo "Head SHA: $HEAD_SHA"
- echo ""
+ echo
mapfile -t files < <(
gh api "repos/$REPO/pulls/$PR/files" --paginate \
- --jq '.[] | .filename | select(endswith(".cs"))'
+ --jq '.[] | select(.status != "removed") | .filename | select(endswith(".cs"))'
)
if [ "${#files[@]}" -eq 0 ]; then
@@ -37,7 +46,7 @@ jobs:
echo "C# files changed in PR:"
printf ' - %s\n' "${files[@]}"
- echo ""
+ echo
spdx_re='SPDX-License-Identifier:[[:space:]]*LGPL-3\.0-or-later'
cr_re='Copyright[[:space:]]*\(c\)[[:space:]]*2020[[:space:]]*[-–—][[:space:]]*2026[[:space:]]*Michal[[:space:]]+Dengusiak[[:space:]]*&[[:space:]]*Jakub[[:space:]]+Ziolkowski[[:space:]]+and[[:space:]]+contributors'
@@ -47,50 +56,49 @@ jobs:
for f in "${files[@]}"; do
echo "Checking: $f"
- # Skip entire Properties folders (typically generated / designer-managed code)
case "$f" in
- */Properties/*)
- echo " ⏭️ Skipping Properties/*"
+ */Properties/*|*.xaml.cs)
+ echo " Skipping Properties/* and *.xaml.cs"
continue
;;
esac
- content_b64=$(gh api "repos/$REPO/contents/$f?ref=$HEAD_SHA" --jq '.content' 2>/dev/null || true)
- if [ -z "$content_b64" ]; then
- echo " ❌ Could not fetch file content (deleted? submodule? path issue)"
+ content=$(gh api \
+ -H "Accept: application/vnd.github.raw" \
+ "repos/$REPO/contents/$f?ref=$HEAD_SHA" 2>/dev/null || true)
+
+ if [ -z "$content" ]; then
+ echo " Could not fetch file content"
missing+=("$f (unreadable)")
continue
fi
- # Decode fully first (avoids SIGPIPE/broken pipe with pipefail),
- # normalize CRLF, then take first 80 lines.
- decoded=$(printf '%s' "$content_b64" | base64 -d)
- headblock=$(printf '%s' "$decoded" | tr -d '\r' | sed -n '1,80p')
+ headblock=$(printf '%s' "$content" | sed '1s/^\xEF\xBB\xBF//' | tr -d '\r' | sed -n '1,80p')
if ! printf '%s' "$headblock" | grep -Eiq "$spdx_re"; then
- echo " ❌ Missing SPDX line in first 80 lines"
+ echo " Missing SPDX line in first 80 lines"
missing+=("$f (SPDX)")
continue
fi
if ! printf '%s' "$headblock" | grep -Eiq "$cr_re"; then
- echo " ❌ Missing copyright line in first 80 lines"
+ echo " Missing copyright line in first 80 lines"
missing+=("$f (Copyright)")
continue
fi
- echo " ✅ OK"
+ echo " OK"
done
- echo ""
+ echo
if [ "${#missing[@]}" -ne 0 ]; then
- echo "❌ Missing required header in:"
+ echo "Missing required header in:"
printf ' - %s\n' "${missing[@]}"
- echo ""
+ echo
echo "Expected somewhere in first 80 lines:"
echo "// SPDX-License-Identifier: LGPL-3.0-or-later"
echo "// Copyright (c) 2020-2026 Michal Dengusiak & Jakub Ziolkowski and contributors"
exit 1
fi
- echo "✅ SPDX + copyright headers OK."
+ echo "SPDX + copyright headers OK."
\ No newline at end of file
diff --git a/Application/SAM Analytical/SAM Analytical.csproj b/Application/SAM Analytical/SAM Analytical.csproj
index c01f1bcc..66249e6b 100644
--- a/Application/SAM Analytical/SAM Analytical.csproj
+++ b/Application/SAM Analytical/SAM Analytical.csproj
@@ -156,6 +156,7 @@
2.27.0
+
5.5.13.4
diff --git a/Grasshopper/SAM.Analytical.UI.WPF.Grasshopper/Component/SAMAnalyticalMultitaskerWorkflow.cs b/Grasshopper/SAM.Analytical.UI.WPF.Grasshopper/Component/SAMAnalyticalMultitaskerWorkflow.cs
new file mode 100644
index 00000000..b45e7b56
--- /dev/null
+++ b/Grasshopper/SAM.Analytical.UI.WPF.Grasshopper/Component/SAMAnalyticalMultitaskerWorkflow.cs
@@ -0,0 +1,428 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+// Copyright (c) 2020-2026 Michal Dengusiak & Jakub Ziolkowski and contributors
+
+using Grasshopper.Kernel;
+using Grasshopper.Kernel.Types;
+using SAM.Analytical.Grasshopper;
+using SAM.Analytical.Tas;
+using SAM.Core;
+using SAM.Core.Grasshopper;
+using SAM.Core.Tas;
+using SAM.Weather;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Windows.Forms;
+
+namespace SAM.Analytical.UI.WPF.Grasshopper
+{
+ public class SAMAnalyticalMultitaskerWorkflow : GH_SAMVariableOutputParameterComponent
+ {
+ ///
+ /// Gets the unique ID for this component. Do not change this ID after release.
+ ///
+ public override Guid ComponentGuid => new Guid("f862c3cd-fdfb-469c-828c-b3e5b3e5d174");
+
+ ///
+ /// The latest version of this component
+ ///
+ public override string LatestComponentVersion => "1.0.4";
+
+ ///
+ /// Provides an Icon for the component.
+ ///
+ protected override System.Drawing.Bitmap Icon => Properties.Resources.SAM_Small3;
+
+
+ public override GH_Exposure Exposure => GH_Exposure.quarternary;
+
+ ///
+ /// Initializes a new instance of the SAM_point3D class.
+ ///
+ public SAMAnalyticalMultitaskerWorkflow()
+ : base("SAMAnalytical.MultitaskerWorkflow", "SAMAnalytical.MultitaskerWorkflow",
+ "MultitaskerWorkflow",
+ "SAM WIP", "Tas")
+ {
+ }
+
+ ///
+ /// Registers all the input parameters for this component.
+ ///
+ protected override GH_SAMParam[] Inputs
+ {
+ get
+ {
+ List result = new List();
+ result.Add(new GH_SAMParam(new GooAnalyticalModelParam { Name = "_analyticalModels", NickName = "_analyticalModels", Description = "AnalyticalModels", Access = GH_ParamAccess.list }, ParamVisibility.Binding));
+ result.Add(new GH_SAMParam(new global::Grasshopper.Kernel.Parameters.Param_String() { Name = "_directory", NickName = "_directory", Description = "Directory", Access = GH_ParamAccess.item }, ParamVisibility.Binding));
+
+ result.Add(new GH_SAMParam(new Weather.Grasshopper.GooWeatherDataParam() { Name = "weatherData_", NickName = "weatherData_", Description = "SAM WeatherData", Access = GH_ParamAccess.item, Optional = true }, ParamVisibility.Binding));
+ result.Add(new GH_SAMParam(new GooAnalyticalObjectParam() { Name = "coolingDesignDays_", NickName = "coolingDesignDays_", Description = "The SAM Analytical Design Days for Cooling", Access = GH_ParamAccess.list, Optional = true }, ParamVisibility.Voluntary));
+ result.Add(new GH_SAMParam(new GooAnalyticalObjectParam() { Name = "heatingDesignDays_", NickName = "heatingDesignDays_", Description = "The SAM Analytical Design Days for Heating", Access = GH_ParamAccess.list, Optional = true }, ParamVisibility.Voluntary));
+
+ global::Grasshopper.Kernel.Parameters.Param_Boolean @boolean = null;
+
+ boolean = new global::Grasshopper.Kernel.Parameters.Param_Boolean() { Name = "_addIZAMs_", NickName = "_addIZAMs_", Description = "Add IZAMs", Access = GH_ParamAccess.item };
+ @boolean.SetPersistentData(true);
+ result.Add(new GH_SAMParam(boolean, ParamVisibility.Voluntary));
+
+ boolean = new global::Grasshopper.Kernel.Parameters.Param_Boolean() { Name = "_sizing_", NickName = "_sizing_", Description = "Sizing", Access = GH_ParamAccess.item };
+ @boolean.SetPersistentData(true);
+ result.Add(new GH_SAMParam(boolean, ParamVisibility.Binding));
+
+ boolean = new global::Grasshopper.Kernel.Parameters.Param_Boolean() { Name = "_simulate_", NickName = "_simulate_", Description = "Simulates the model from 1 to 365 day.", Access = GH_ParamAccess.item };
+ @boolean.SetPersistentData(false);
+ result.Add(new GH_SAMParam(boolean, ParamVisibility.Binding));
+
+ boolean = new global::Grasshopper.Kernel.Parameters.Param_Boolean() { Name = "_useBEthickness_", NickName = "_useBEthickness_", Description = "If True Building Element thickness will be applied in T3D. Default False.", Access = GH_ParamAccess.item };
+ @boolean.SetPersistentData(false);
+ result.Add(new GH_SAMParam(boolean, ParamVisibility.Voluntary));
+
+ global::Grasshopper.Kernel.Parameters.Param_GenericObject genericObject = new global::Grasshopper.Kernel.Parameters.Param_GenericObject() { Name = "surfaceOutputSpec_", NickName = "surfaceOutputSpec_", Description = "Surface Output Spec", Access = GH_ParamAccess.list, Optional = true };
+ result.Add(new GH_SAMParam(genericObject, ParamVisibility.Voluntary));
+
+ global::Grasshopper.Kernel.Parameters.Param_Number number = null;
+
+ number = new global::Grasshopper.Kernel.Parameters.Param_Number() { Name = "_tolerance_", NickName = "_tolerance_", Description = "Tolerance", Access = GH_ParamAccess.item };
+ number.SetPersistentData(Core.Tolerance.Distance);
+ result.Add(new GH_SAMParam(number, ParamVisibility.Voluntary));
+
+ boolean = new global::Grasshopper.Kernel.Parameters.Param_Boolean() { Name = "_runUnmetHours_", NickName = "_runUnmetHours_", Description = "Calculates the amount of hours that the Zone/Space will be outside of the thermostat setpoint (unmet hours).", Access = GH_ParamAccess.item };
+ @boolean.SetPersistentData(false);
+ result.Add(new GH_SAMParam(boolean, ParamVisibility.Voluntary));
+
+ @boolean = new global::Grasshopper.Kernel.Parameters.Param_Boolean() { Name = "_removeTBD_", NickName = "_removeTBD_", Description = "If True existing TBD file will be deleted before simulation", Access = GH_ParamAccess.item };
+ @boolean.SetPersistentData(false);
+ result.Add(new GH_SAMParam(@boolean, ParamVisibility.Voluntary));
+
+ @boolean = new global::Grasshopper.Kernel.Parameters.Param_Boolean() { Name = "_parallel_", NickName = "_parallel_", Description = "Parallel.", Optional = true, Access = GH_ParamAccess.item };
+ @boolean.SetPersistentData(true);
+ result.Add(new GH_SAMParam(@boolean, ParamVisibility.Voluntary));
+
+ global::Grasshopper.Kernel.Parameters.Param_Integer integer = null;
+
+ integer = new global::Grasshopper.Kernel.Parameters.Param_Integer() { Name = "_cPUs_", NickName = "_cPUs_", Description = "Number of logical processors (as shown in Task Manager) used for the calculation.\r\nIf not specified, defaults to maximum available − 1 (leaving one processor free). If only one logical processor is available, it uses 1.", Optional = true, Access = GH_ParamAccess.item };
+ result.Add(new GH_SAMParam(integer, ParamVisibility.Voluntary));
+
+ @boolean = new global::Grasshopper.Kernel.Parameters.Param_Boolean() { Name = "_run", NickName = "_run", Description = "Connect a boolean toggle to run.", Access = GH_ParamAccess.item };
+ @boolean.SetPersistentData(false);
+ result.Add(new GH_SAMParam(@boolean, ParamVisibility.Binding));
+
+ return result.ToArray();
+ }
+ }
+
+ ///
+ /// Registers all the output parameters for this component.
+ ///
+ protected override GH_SAMParam[] Outputs
+ {
+ get
+ {
+ List result = [];
+ result.Add(new GH_SAMParam(new global::Grasshopper.Kernel.Parameters.Param_String() { Name = "CaseDescriptions", NickName = "CaseDescriptions", Description = "CaseDescriptions", Access = GH_ParamAccess.list }, ParamVisibility.Binding));
+ result.Add(new GH_SAMParam(new global::Grasshopper.Kernel.Parameters.Param_String() { Name = "Directories", NickName = "Directories", Description = "Directories", Access = GH_ParamAccess.list }, ParamVisibility.Binding));
+ result.Add(new GH_SAMParam(new global::Grasshopper.Kernel.Parameters.Param_Boolean() { Name = "successful", NickName = "successful", Description = "successful", Access = GH_ParamAccess.item }, ParamVisibility.Binding));
+ return [.. result];
+ }
+ }
+
+ ///
+ /// This is the method that actually does the work.
+ ///
+ /// The DA object is used to retrieve from inputs and store in outputs.
+ protected override void SolveInstance(IGH_DataAccess dataAccess)
+ {
+ int index_successful = Params.IndexOfOutputParam("successful");
+ if (index_successful != -1)
+ {
+ dataAccess.SetData(index_successful, false);
+ }
+
+ int index;
+
+ bool run = false;
+ index = Params.IndexOfInputParam("_run");
+ if (index == -1 || !dataAccess.GetData(index, ref run))
+ run = false;
+
+ if (!run)
+ return;
+
+ string directory = null;
+ index = Params.IndexOfInputParam("_directory");
+ if (index == -1 || !dataAccess.GetData(index, ref directory) || string.IsNullOrWhiteSpace(directory))
+ {
+ AddRuntimeMessage(GH_RuntimeMessageLevel.Error, "Invalid data");
+ return;
+ }
+
+ WeatherData weatherData = null;
+ index = Params.IndexOfInputParam("weatherData_");
+ if (index != -1)
+ {
+ if (!dataAccess.GetData(index, ref weatherData))
+ {
+ weatherData = null;
+ }
+ }
+
+ if (weatherData != null)
+ {
+ weatherData = new WeatherData(weatherData);
+ }
+
+ List analyticalModels = [];
+ index = Params.IndexOfInputParam("_analyticalModels");
+ if (index == -1 || !dataAccess.GetDataList(index, analyticalModels) || analyticalModels == null)
+ {
+ AddRuntimeMessage(GH_RuntimeMessageLevel.Error, "Invalid data");
+ return;
+ }
+
+ List heatingDesignDays = [];
+ index = Params.IndexOfInputParam("heatingDesignDays_");
+ if (index == -1 || !dataAccess.GetDataList(index, heatingDesignDays) || heatingDesignDays == null || heatingDesignDays.Count == 0)
+ {
+ heatingDesignDays = null;
+ }
+
+ if (heatingDesignDays != null)
+ {
+ heatingDesignDays = heatingDesignDays.ConvertAll(x => x.Clone());
+ }
+
+ List coolingDesignDays = [];
+ index = Params.IndexOfInputParam("coolingDesignDays_");
+ if (index == -1 || !dataAccess.GetDataList(index, coolingDesignDays) || coolingDesignDays == null || coolingDesignDays.Count == 0)
+ {
+ coolingDesignDays = null;
+ }
+
+ if (coolingDesignDays != null)
+ {
+ coolingDesignDays = coolingDesignDays.ConvertAll(x => x.Clone());
+ }
+
+ List surfaceOutputSpecs = null;
+
+ List objectWrappers = [];
+ index = Params.IndexOfInputParam("surfaceOutputSpec_");
+ if (index != -1 && dataAccess.GetDataList(index, objectWrappers) && objectWrappers != null && objectWrappers.Count != 0)
+ {
+ surfaceOutputSpecs = [];
+ foreach (GH_ObjectWrapper objectWrapper in objectWrappers)
+ {
+ object value = objectWrapper.Value;
+ if (value is IGH_Goo)
+ {
+ value = (value as dynamic)?.Value;
+ }
+
+ if (value is bool && ((bool)value))
+ {
+ SurfaceOutputSpec surfaceOutputSpec = new("Tas.Simulate");
+ surfaceOutputSpec.SolarGain = true;
+ surfaceOutputSpec.Conduction = true;
+ surfaceOutputSpec.ApertureData = false;
+ surfaceOutputSpec.Condensation = false;
+ surfaceOutputSpec.Convection = false;
+ surfaceOutputSpec.LongWave = false;
+ surfaceOutputSpec.Temperature = false;
+
+ surfaceOutputSpecs.Add(surfaceOutputSpec);
+ }
+ else if (Core.Query.IsNumeric(value) && Core.Query.TryConvert(value, out double @double) && @double == 2.0)
+ {
+ surfaceOutputSpecs = [new("Tas.Simulate")];
+ surfaceOutputSpecs[0].SolarGain = true;
+ surfaceOutputSpecs[0].Conduction = true;
+ surfaceOutputSpecs[0].ApertureData = true;
+ surfaceOutputSpecs[0].Condensation = true;
+ surfaceOutputSpecs[0].Convection = true;
+ surfaceOutputSpecs[0].LongWave = true;
+ surfaceOutputSpecs[0].Temperature = true;
+ }
+ else if (value is SurfaceOutputSpec)
+ {
+ surfaceOutputSpecs.Add((SurfaceOutputSpec)value);
+ }
+ }
+ }
+
+ bool simulate = false;
+ index = Params.IndexOfInputParam("_simulate_");
+ if (index != -1)
+ {
+ if (!dataAccess.GetData(index, ref simulate))
+ {
+ simulate = false;
+ }
+ }
+
+ bool useBEWidths = false;
+ index = Params.IndexOfInputParam("_useBEthickness_");
+ if (index != -1)
+ {
+ if (!dataAccess.GetData(index, ref useBEWidths))
+ {
+ useBEWidths = false;
+ }
+ }
+
+ bool sizing = true;
+ index = Params.IndexOfInputParam("_sizing_");
+ if (index != -1)
+ {
+ if (!dataAccess.GetData(index, ref sizing))
+ {
+ sizing = true;
+ }
+ }
+
+ bool unmetHours = false;
+ index = Params.IndexOfInputParam("_runUnmetHours_");
+ if (index != -1)
+ if (!dataAccess.GetData(index, ref unmetHours))
+ unmetHours = true;
+
+ bool addIZAMs = true;
+ index = Params.IndexOfInputParam("_addIZAMs_");
+ if (index != -1)
+ {
+ if (!dataAccess.GetData(index, ref addIZAMs))
+ {
+ addIZAMs = true;
+ }
+ }
+
+ bool removeExistingTBD = false;
+ index = Params.IndexOfInputParam("_removeTBD_");
+ if (index != -1)
+ {
+ if (!dataAccess.GetData(index, ref removeExistingTBD))
+ {
+ removeExistingTBD = false;
+ }
+ }
+
+ WorkflowSettings workflowSettings = new()
+ {
+ Path_TBD = null,
+ Path_gbXML = null,
+ WeatherData = weatherData,
+ DesignDays_Heating = heatingDesignDays,
+ DesignDays_Cooling = coolingDesignDays,
+ SurfaceOutputSpecs = surfaceOutputSpecs,
+ UnmetHours = unmetHours,
+ Simulate = simulate,
+ Sizing = sizing,
+ UpdateZones = true,
+ UseWidths = useBEWidths,
+ AddIZAMs = addIZAMs,
+ SimulateFrom = 1,
+ SimulateTo = 365,
+ RemoveExistingTBD = removeExistingTBD,
+ };
+
+ bool parallel = true;
+ index = Params.IndexOfInputParam("_parallel_");
+ if (index != -1)
+ {
+ if (!dataAccess.GetData(index, ref parallel))
+ {
+ parallel = true;
+ }
+ }
+
+ int? maxDegreeOfParallelism = null;
+ index = Params.IndexOfInputParam("_cPUs_");
+ if (index != -1)
+ {
+ int maxDegreeOfParallelism_Temp = -1;
+ if (dataAccess.GetData(index, ref maxDegreeOfParallelism_Temp) && maxDegreeOfParallelism_Temp > 0)
+ {
+ maxDegreeOfParallelism = maxDegreeOfParallelism_Temp;
+ }
+ }
+
+ Dictionary dictionary = Modify.RunWorkflow(analyticalModels, workflowSettings, directory, parallel, maxDegreeOfParallelism);
+
+ if (analyticalModels.Count != dictionary.Count)
+ {
+ AddRuntimeMessage(GH_RuntimeMessageLevel.Warning, "Some of the models could not be calculated.");
+ }
+
+ index = Params.IndexOfOutputParam("CaseDescriptions");
+ if (index != -1)
+ {
+ dataAccess.SetDataList(index, dictionary?.Keys);
+ }
+
+ index = Params.IndexOfOutputParam("Directories");
+ if (index != -1)
+ {
+ dataAccess.SetDataList(index, dictionary?.Keys);
+ }
+
+ if (index_successful != -1)
+ {
+ dataAccess.SetData(index_successful, true);
+ }
+ }
+
+ public override void AppendAdditionalMenuItems(ToolStripDropDown menu)
+ {
+ base.AppendAdditionalMenuItems(menu);
+
+ Menu_AppendSeparator(menu);
+ Menu_AppendItem(menu, "Open Directory", Menu_OpenTBD, Properties.Resources.SAM_Small, true, false);
+ }
+
+ private void Menu_OpenTBD(object sender, EventArgs e)
+ {
+ int index_Path = Params.IndexOfInputParam("_directory");
+ if (index_Path == -1)
+ {
+ return;
+ }
+
+ string directory = null;
+
+ object @object = null;
+
+ @object = Params.Input[index_Path].VolatileData.AllData(true)?.OfType