From 3ce4df2c134bfb141cc27daadb22349892144f38 Mon Sep 17 00:00:00 2001 From: Alexandre CUER Date: Tue, 10 Nov 2026 18:45:42 +0100 Subject: [PATCH 1/6] Add workflow to generate geometry IDF artifacts --- .github/workflows/generate-geometry-idf.yml | 172 ++++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 .github/workflows/generate-geometry-idf.yml diff --git a/.github/workflows/generate-geometry-idf.yml b/.github/workflows/generate-geometry-idf.yml new file mode 100644 index 0000000..854e68d --- /dev/null +++ b/.github/workflows/generate-geometry-idf.yml @@ -0,0 +1,172 @@ +name: Generate geometry IDF + +on: + workflow_dispatch: + inputs: + ladybug_version: + description: "ladybug_codes tag, branch or commit" + required: true + default: "0.0.3" + type: string + geometry_yaml: + description: "Geometry configuration (YAML)" + required: true + type: string + default: | + name: example + + blocks: + ss1_alt: &ss1_alt -4 + rdc_alt: &rdc_alt 0 + d0: 19 + d3: 4 + d4: 4 + d5: 3 + d8: 2.8 + d9: 3 + d10: 1.6 + d11: 3.8 + d911: d9+d10+d11 + ss1_floor: + - [d0, 0, *ss1_alt] + - [d0, d3+d4+d5, *ss1_alt] + - [0, d3+d4+d5, *ss1_alt] + - [0, 0, *ss1_alt] + ss1_roof: + - [d0, d3+d4, *rdc_alt] + - [d0, d3+d4+d5, *rdc_alt] + - [0, d3+d4+d5, *rdc_alt] + - [0, d3+d4, *rdc_alt] + rdc_cage: + - [d9+d10, 0, *rdc_alt] + - [d9+d10, d8, *rdc_alt, air_boundary] + - [d911, d8, *rdc_alt] + - [d911, 0, *rdc_alt] + caves: + - [0, 0, *rdc_alt] + - rdc_cage + - ["d0", 0, *rdc_alt] + - ["d0", "d3+d4", *rdc_alt] + - [0, "d3+d4", *rdc_alt] + + immeuble: + ss1: + height: 4 + use_blocks_vars: 1 + constructions: + default: townhouse_basement + exclude: + roofs: [2] + floors: [ss1_floor] + roofs: [ss1_roof, caves, rdc_cage] + walls: [ss1_floor] + rdc: + height: 3 + use_blocks_vars: 1 + constructions: + floors: townhouse_basement + walls: [caves] + apertures: + 5: [2, 1.4, 1.5, 1] + doors: + 6: [1, 2, 3, 0] + rdc_cage: + height: 3 + use_blocks_vars: 1 + walls: [rdc_cage] + +permissions: + contents: read + +jobs: + generate: + name: Generate geometry IDF + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - name: Checkout ladybug_codes + uses: actions/checkout@v6 + with: + repository: Open-Building-Management/ladybug_codes + ref: ${{ inputs.ladybug_version }} + path: ladybug_codes + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.11" + cache: pip + cache-dependency-path: ladybug_codes/pyproject.toml + + - name: Install ladybug_codes and dependencies + working-directory: ladybug_codes + run: python -m pip install --upgrade pip && python -m pip install -e . + + - name: Validate and write geometry YAML + id: validate + working-directory: ladybug_codes + env: + GEOMETRY_YAML: ${{ inputs.geometry_yaml }} + run: | + python - <<'PY' + import os + import re + import sys + from pathlib import Path + + import yaml + + raw_yaml = os.environ["GEOMETRY_YAML"] + if not raw_yaml.strip(): + sys.exit("The geometry YAML input is empty.") + if len(raw_yaml) > 60_000: + sys.exit("The geometry YAML input exceeds 60,000 characters.") + + try: + config = yaml.safe_load(raw_yaml) + except yaml.YAMLError as exc: + sys.exit(f"Invalid YAML: {exc}") + + if not isinstance(config, dict): + sys.exit("The YAML document must contain a mapping at its root.") + + name = config.get("name") + if not isinstance(name, str) or not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_-]{0,63}", name): + sys.exit( + "The top-level 'name' must contain 1 to 64 letters, " + "digits, underscores or hyphens, and must start with a letter or digit." + ) + + config_path = Path("conf_geometry/user_geometry.yml") + config_path.write_text(raw_yaml.rstrip() + "\\n", encoding="utf-8") + + idf_path = f"{name}.idf" + with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output: + output.write(f"idf_path={idf_path}\\n") + print(f"Validated geometry configuration; expected output: {idf_path}") + PY + + - name: Generate geometry IDF + working-directory: ladybug_codes + run: python generate_geometry_from_yaml.py --geoconf=conf_geometry/user_geometry.yml + + - name: Check generated IDF + working-directory: ladybug_codes + env: + IDF_PATH: ${{ steps.validate.outputs.idf_path }} + run: | + test -s "$IDF_PATH" || { + echo "::error::The geometry generator did not produce $IDF_PATH" + exit 1 + } + echo "Generated $(wc -c < "$IDF_PATH") bytes in $IDF_PATH" + + - name: Upload generated IDF + uses: actions/upload-artifact@v4 + with: + name: geometry-idf-${{ github.run_id }} + path: ladybug_codes/${{ steps.validate.outputs.idf_path }} + if-no-files-found: error + retention-days: 30 From 917ccec0502af784ce1fabd278af2ae78875e800 Mon Sep 17 00:00:00 2001 From: Alexandre CUER Date: Tue, 10 Nov 2026 18:45:54 +0100 Subject: [PATCH 2/6] Fix newline handling in geometry workflow --- .github/workflows/generate-geometry-idf.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/generate-geometry-idf.yml b/.github/workflows/generate-geometry-idf.yml index 854e68d..32a9c61 100644 --- a/.github/workflows/generate-geometry-idf.yml +++ b/.github/workflows/generate-geometry-idf.yml @@ -140,11 +140,11 @@ jobs: ) config_path = Path("conf_geometry/user_geometry.yml") - config_path.write_text(raw_yaml.rstrip() + "\\n", encoding="utf-8") + config_path.write_text(raw_yaml.rstrip() + "\n", encoding="utf-8") idf_path = f"{name}.idf" with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output: - output.write(f"idf_path={idf_path}\\n") + output.write(f"idf_path={idf_path}\n") print(f"Validated geometry configuration; expected output: {idf_path}") PY From c3cbd8a29c579be19fb4714946d89545cddf39e7 Mon Sep 17 00:00:00 2001 From: Alexandre CUER Date: Tue, 10 Nov 2026 18:46:17 +0100 Subject: [PATCH 3/6] Prepare EnergyPlus IDD for geometry generation --- .github/workflows/generate-geometry-idf.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/generate-geometry-idf.yml b/.github/workflows/generate-geometry-idf.yml index 32a9c61..e59d8d5 100644 --- a/.github/workflows/generate-geometry-idf.yml +++ b/.github/workflows/generate-geometry-idf.yml @@ -100,6 +100,22 @@ jobs: cache: pip cache-dependency-path: ladybug_codes/pyproject.toml + # The 0.0.3 generator imports common.py, which loads Energy+.idd + # from configuration.yml even though this geometry-only job does not + # run EnergyPlus itself. Supply the matching IDD and override its path. + - name: Prepare EnergyPlus 24.1 IDD + run: | + curl --fail --location --retry 3 \ + https://github.com/NatLabRockies/EnergyPlus/releases/download/v24.1.0/EnergyPlus-24.1.0-9d7789a3ac-Linux-Ubuntu22.04-x86_64.tar.gz \ + --output /tmp/energyplus-24.1.0.tar.gz + mkdir -p /tmp/energyplus + tar -xzf /tmp/energyplus-24.1.0.tar.gz \ + --wildcards '*/Energy+.idd' \ + --strip-components=1 \ + -C /tmp/energyplus + test -s /tmp/energyplus/Energy+.idd + sed -i 's|^os_ep_path:.*|os_ep_path: "/tmp/energyplus"|' ladybug_codes/configuration.yml + - name: Install ladybug_codes and dependencies working-directory: ladybug_codes run: python -m pip install --upgrade pip && python -m pip install -e . From 6e297f87e58d8ae33e988d8ca8ca2169aaed60d8 Mon Sep 17 00:00:00 2001 From: Alexandre CUER Date: Fri, 13 Nov 2026 17:26:06 +0100 Subject: [PATCH 4/6] Use the published GHCR image for geometry generation --- .github/workflows/generate-geometry-idf.yml | 130 +++++++------------- 1 file changed, 44 insertions(+), 86 deletions(-) diff --git a/.github/workflows/generate-geometry-idf.yml b/.github/workflows/generate-geometry-idf.yml index e59d8d5..8b23e8c 100644 --- a/.github/workflows/generate-geometry-idf.yml +++ b/.github/workflows/generate-geometry-idf.yml @@ -3,10 +3,10 @@ name: Generate geometry IDF on: workflow_dispatch: inputs: - ladybug_version: - description: "ladybug_codes tag, branch or commit" + image_tag: + description: "ladybug_codes GHCR image tag" required: true - default: "0.0.3" + default: "ep24.1.0-9d7789a3ac_idfhub0.0.4" type: string geometry_yaml: description: "Geometry configuration (YAML)" @@ -76,7 +76,7 @@ on: walls: [rdc_cage] permissions: - contents: read + packages: read jobs: generate: @@ -85,104 +85,62 @@ jobs: timeout-minutes: 15 steps: - - name: Checkout ladybug_codes - uses: actions/checkout@v6 - with: - repository: Open-Building-Management/ladybug_codes - ref: ${{ inputs.ladybug_version }} - path: ladybug_codes - persist-credentials: false - - - name: Set up Python - uses: actions/setup-python@v6 - with: - python-version: "3.11" - cache: pip - cache-dependency-path: ladybug_codes/pyproject.toml - - # The 0.0.3 generator imports common.py, which loads Energy+.idd - # from configuration.yml even though this geometry-only job does not - # run EnergyPlus itself. Supply the matching IDD and override its path. - - name: Prepare EnergyPlus 24.1 IDD - run: | - curl --fail --location --retry 3 \ - https://github.com/NatLabRockies/EnergyPlus/releases/download/v24.1.0/EnergyPlus-24.1.0-9d7789a3ac-Linux-Ubuntu22.04-x86_64.tar.gz \ - --output /tmp/energyplus-24.1.0.tar.gz - mkdir -p /tmp/energyplus - tar -xzf /tmp/energyplus-24.1.0.tar.gz \ - --wildcards '*/Energy+.idd' \ - --strip-components=1 \ - -C /tmp/energyplus - test -s /tmp/energyplus/Energy+.idd - sed -i 's|^os_ep_path:.*|os_ep_path: "/tmp/energyplus"|' ladybug_codes/configuration.yml - - - name: Install ladybug_codes and dependencies - working-directory: ladybug_codes - run: python -m pip install --upgrade pip && python -m pip install -e . - - name: Validate and write geometry YAML - id: validate - working-directory: ladybug_codes + id: geometry env: GEOMETRY_YAML: ${{ inputs.geometry_yaml }} run: | - python - <<'PY' - import os - import re - import sys - from pathlib import Path - - import yaml - - raw_yaml = os.environ["GEOMETRY_YAML"] - if not raw_yaml.strip(): - sys.exit("The geometry YAML input is empty.") - if len(raw_yaml) > 60_000: - sys.exit("The geometry YAML input exceeds 60,000 characters.") - - try: - config = yaml.safe_load(raw_yaml) - except yaml.YAMLError as exc: - sys.exit(f"Invalid YAML: {exc}") - - if not isinstance(config, dict): - sys.exit("The YAML document must contain a mapping at its root.") - - name = config.get("name") - if not isinstance(name, str) or not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_-]{0,63}", name): - sys.exit( - "The top-level 'name' must contain 1 to 64 letters, " - "digits, underscores or hyphens, and must start with a letter or digit." - ) + if [[ -z "${GEOMETRY_YAML//[[:space:]]/}" ]]; then + echo "::error::The geometry YAML input is empty." + exit 1 + fi + if (( ${#GEOMETRY_YAML} > 60000 )); then + echo "::error::The geometry YAML input exceeds 60,000 characters." + exit 1 + fi + printf '%s\n' "$GEOMETRY_YAML" > geometry.yml + echo "Geometry YAML written to $GITHUB_WORKSPACE/geometry.yml" - config_path = Path("conf_geometry/user_geometry.yml") - config_path.write_text(raw_yaml.rstrip() + "\n", encoding="utf-8") + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - idf_path = f"{name}.idf" - with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output: - output.write(f"idf_path={idf_path}\n") - print(f"Validated geometry configuration; expected output: {idf_path}") - PY + - name: Pull geometry generator image + env: + IMAGE_TAG: ${{ inputs.image_tag }} + run: | + image="ghcr.io/open-building-management/ladybug_codes:$IMAGE_TAG" + docker pull "$image" + echo "IMAGE=$image" >> "$GITHUB_ENV" - name: Generate geometry IDF - working-directory: ladybug_codes - run: python generate_geometry_from_yaml.py --geoconf=conf_geometry/user_geometry.yml + run: | + docker run --rm \ + --volume "$GITHUB_WORKSPACE:/workspace" \ + --workdir /workspace \ + "$IMAGE" \ + python3 -m generate_geometry_from_yaml \ + --conf=/opt/ladybug_codes/configuration.yml \ + --geoconf=/workspace/geometry.yml - name: Check generated IDF - working-directory: ladybug_codes - env: - IDF_PATH: ${{ steps.validate.outputs.idf_path }} + id: output run: | - test -s "$IDF_PATH" || { - echo "::error::The geometry generator did not produce $IDF_PATH" + idf_file="$(find "$GITHUB_WORKSPACE" -maxdepth 1 -type f -name '*.idf' -print -quit)" + if [[ -z "$idf_file" || ! -s "$idf_file" ]]; then + echo "::error::The geometry generator did not produce a non-empty IDF file." exit 1 - } - echo "Generated $(wc -c < "$IDF_PATH") bytes in $IDF_PATH" + fi + echo "idf_path=$idf_file" >> "$GITHUB_OUTPUT" + echo "Generated $(wc -c < "$idf_file") bytes in $(basename "$idf_file")" - name: Upload generated IDF uses: actions/upload-artifact@v4 with: name: geometry-idf-${{ github.run_id }} - path: ladybug_codes/${{ steps.validate.outputs.idf_path }} + path: ${{ steps.output.outputs.idf_path }} if-no-files-found: error retention-days: 30 From bbc6c723d66c066efaddb03ad7dd64988c6a28fd Mon Sep 17 00:00:00 2001 From: Alexandre CUER Date: Fri, 13 Nov 2026 17:44:33 +0100 Subject: [PATCH 5/6] Use bundled geometry example when YAML input is empty --- .github/workflows/generate-geometry-idf.yml | 101 ++++---------------- 1 file changed, 21 insertions(+), 80 deletions(-) diff --git a/.github/workflows/generate-geometry-idf.yml b/.github/workflows/generate-geometry-idf.yml index 8b23e8c..3462544 100644 --- a/.github/workflows/generate-geometry-idf.yml +++ b/.github/workflows/generate-geometry-idf.yml @@ -9,71 +9,10 @@ on: default: "ep24.1.0-9d7789a3ac_idfhub0.0.4" type: string geometry_yaml: - description: "Geometry configuration (YAML)" - required: true + description: "Optional geometry configuration (YAML). Leave empty to use the bundled example." + required: false + default: "" type: string - default: | - name: example - - blocks: - ss1_alt: &ss1_alt -4 - rdc_alt: &rdc_alt 0 - d0: 19 - d3: 4 - d4: 4 - d5: 3 - d8: 2.8 - d9: 3 - d10: 1.6 - d11: 3.8 - d911: d9+d10+d11 - ss1_floor: - - [d0, 0, *ss1_alt] - - [d0, d3+d4+d5, *ss1_alt] - - [0, d3+d4+d5, *ss1_alt] - - [0, 0, *ss1_alt] - ss1_roof: - - [d0, d3+d4, *rdc_alt] - - [d0, d3+d4+d5, *rdc_alt] - - [0, d3+d4+d5, *rdc_alt] - - [0, d3+d4, *rdc_alt] - rdc_cage: - - [d9+d10, 0, *rdc_alt] - - [d9+d10, d8, *rdc_alt, air_boundary] - - [d911, d8, *rdc_alt] - - [d911, 0, *rdc_alt] - caves: - - [0, 0, *rdc_alt] - - rdc_cage - - ["d0", 0, *rdc_alt] - - ["d0", "d3+d4", *rdc_alt] - - [0, "d3+d4", *rdc_alt] - - immeuble: - ss1: - height: 4 - use_blocks_vars: 1 - constructions: - default: townhouse_basement - exclude: - roofs: [2] - floors: [ss1_floor] - roofs: [ss1_roof, caves, rdc_cage] - walls: [ss1_floor] - rdc: - height: 3 - use_blocks_vars: 1 - constructions: - floors: townhouse_basement - walls: [caves] - apertures: - 5: [2, 1.4, 1.5, 1] - doors: - 6: [1, 2, 3, 0] - rdc_cage: - height: 3 - use_blocks_vars: 1 - walls: [rdc_cage] permissions: packages: read @@ -85,21 +24,23 @@ jobs: timeout-minutes: 15 steps: - - name: Validate and write geometry YAML + - name: Prepare geometry configuration id: geometry env: GEOMETRY_YAML: ${{ inputs.geometry_yaml }} run: | if [[ -z "${GEOMETRY_YAML//[[:space:]]/}" ]]; then - echo "::error::The geometry YAML input is empty." - exit 1 - fi - if (( ${#GEOMETRY_YAML} > 60000 )); then - echo "::error::The geometry YAML input exceeds 60,000 characters." - exit 1 + echo "No geometry YAML provided; using the bundled example." + echo "config_path=/opt/ladybug_codes/conf_geometry/example.yml" >> "$GITHUB_OUTPUT" + else + if (( ${#GEOMETRY_YAML} > 60000 )); then + echo "::error::The geometry YAML input exceeds 60,000 characters." + exit 1 + fi + printf '%s\\n' "$GEOMETRY_YAML" > geometry.yml + echo "config_path=/workspace/geometry.yml" >> "$GITHUB_OUTPUT" + echo "Custom geometry YAML written to $GITHUB_WORKSPACE/geometry.yml" fi - printf '%s\n' "$GEOMETRY_YAML" > geometry.yml - echo "Geometry YAML written to $GITHUB_WORKSPACE/geometry.yml" - name: Login to GHCR uses: docker/login-action@v3 @@ -118,13 +59,13 @@ jobs: - name: Generate geometry IDF run: | - docker run --rm \ - --volume "$GITHUB_WORKSPACE:/workspace" \ - --workdir /workspace \ - "$IMAGE" \ - python3 -m generate_geometry_from_yaml \ - --conf=/opt/ladybug_codes/configuration.yml \ - --geoconf=/workspace/geometry.yml + docker run --rm \\ + --volume "$GITHUB_WORKSPACE:/workspace" \\ + --workdir /workspace \\ + "$IMAGE" \\ + python3 -m generate_geometry_from_yaml \\ + --conf=/opt/ladybug_codes/configuration.yml \\ + --geoconf="${{ steps.geometry.outputs.config_path }}" - name: Check generated IDF id: output From 66bbd0a3071965939d3ed9a0d679783bd9808a16 Mon Sep 17 00:00:00 2001 From: Alexandre CUER Date: Fri, 13 Nov 2026 17:44:48 +0100 Subject: [PATCH 6/6] Fix shell escaping in geometry workflow --- .github/workflows/generate-geometry-idf.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/generate-geometry-idf.yml b/.github/workflows/generate-geometry-idf.yml index 3462544..4c68347 100644 --- a/.github/workflows/generate-geometry-idf.yml +++ b/.github/workflows/generate-geometry-idf.yml @@ -37,7 +37,7 @@ jobs: echo "::error::The geometry YAML input exceeds 60,000 characters." exit 1 fi - printf '%s\\n' "$GEOMETRY_YAML" > geometry.yml + printf '%s\n' "$GEOMETRY_YAML" > geometry.yml echo "config_path=/workspace/geometry.yml" >> "$GITHUB_OUTPUT" echo "Custom geometry YAML written to $GITHUB_WORKSPACE/geometry.yml" fi @@ -59,12 +59,12 @@ jobs: - name: Generate geometry IDF run: | - docker run --rm \\ - --volume "$GITHUB_WORKSPACE:/workspace" \\ - --workdir /workspace \\ - "$IMAGE" \\ - python3 -m generate_geometry_from_yaml \\ - --conf=/opt/ladybug_codes/configuration.yml \\ + docker run --rm \ + --volume "$GITHUB_WORKSPACE:/workspace" \ + --workdir /workspace \ + "$IMAGE" \ + python3 -m generate_geometry_from_yaml \ + --conf=/opt/ladybug_codes/configuration.yml \ --geoconf="${{ steps.geometry.outputs.config_path }}" - name: Check generated IDF