Skip to content

Conversation

justinfmccarty
Copy link
Contributor

@justinfmccarty justinfmccarty commented Sep 24, 2025

Initial functionality is ready to go! I tested all variables for SSP585 and there were no issues. Most of the integration had to do with renaming and making sure things are in lists instead of scalar. My package uses a lot of lists because it is meant to be iterated, but we turned that functionality off for simplicity in CEA

Summary by CodeRabbit

  • New Features

    • EPW weather morphing: selectable target year, climate pathway, percentile, and variables; produces a morphed weather.epw with progress/completion messages.
    • EPW location utility: extracts latitude, longitude, UTC, and elevation from an EPW file.
  • Behavior Changes

    • If morphing is enabled, the morph workflow runs instead of the previous fetch/validate flow.
    • Non-morph path now errors clearly if the specified weather file is missing.
  • Chores

    • Added runtime dependency for weather morphing.

reyery and others added 3 commits July 23, 2025 16:31
Introduced new weather morphing parameters in default.config, including year, climate pathway, percentile, and variables. Also added pyepwmorph import in weather_helper.py to support weather file morphing functionality.
Integrating pyepwmorph into CEA as a part of the Weather Helper. Have introduced general functionality. Need to make a new pyepwmorph version and then another scenario will become available.
Copy link
Contributor

coderabbitai bot commented Sep 24, 2025

Walkthrough

Adds EPW climate morphing: new epwmorpher module using pyepwmorph, a morph toggle in weather_helper, morph-related config options, a utility to read EPW location metadata, and a pyepwmorph runtime dependency. The morph workflow validates, backs up, compiles model data, performs morphing, and writes a morphed EPW.

Changes

Cohort / File(s) Summary
Weather morphing module
cea/datamanagement/weather_helper/epwmorpher.py
New module implementing scenario-name conversion, morphing_workflow orchestration of pyepwmorph steps, and main entrypoint with validations, I/O, and progress messages.
Weather helper integration
cea/datamanagement/weather_helper/weather_helper.py
Adds morph branch toggled by config.weather_helper.morph that calls epwmorpher.main(config); non-morph path now validates weather file presence before fetch/copy and retains online fetch/copy logic.
Config updates (morph settings)
cea/default.config
Adds weather-helper parameters: morph (bool), year (int), climate-pathway (choice), percentile (choice), variables (multi-choice) with help text under “Morph Settings”.
EPW utilities
cea/utilities/epwreader.py
Adds epw_location(weather_path) to parse latitude, longitude, UTC offset, and elevation from the EPW header line.
Project dependencies
pyproject.toml
Adds runtime dependency: pyepwmorph>=0.3.1,<0.4.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Config as Config
  participant WH as weather_helper
  participant EPM as epwmorpher
  participant PIO as pyepwmorph_io.Epw
  participant PWF as pyepwmorph_workflow
  participant FS as Filesystem

  User->>WH: Run weather helper
  WH->>Config: Read weather-helper.morph
  alt morph == true
    WH->>EPM: main(config)
    EPM->>FS: Copy original -> before_morph_weather.epw
    EPM->>PIO: Load and validate EPW
    EPM->>PIO: Detect baseline range (or warn/fallback)
    EPM->>Config: Read year, pathway, percentile, variables
    EPM->>PWF: iterate_compile_model_data(...)
    EPM->>PWF: morph_epw(...)
    PWF-->>EPM: Morphed EPW
    EPM->>FS: Write output `weather.epw`
  else morph == false
    WH->>FS: Validate weather file presence
    alt Source is online
      WH->>External: Fetch from Climate.OneBuilding.org
      External-->>WH: EPW file
      WH->>FS: Save EPW
    else Local file
      WH->>FS: Copy provided EPW
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested labels

feature-new

Suggested reviewers

  • reyery

Poem

A breeze in my whiskers, I hop through the code,
Morphing skies and seasons down a gentle road.
Years and pathways twirl, variables sway,
EPWs reborn for some future day.
I nibble the bytes and hop away—hooray! 🐇✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title “Weather morpher” concisely and accurately summarizes the primary change of introducing weather morphing functionality, making it clear to reviewers what the PR delivers without unnecessary detail.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch weather-morpher

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
cea/datamanagement/weather_helper/epwmorpher.py (1)

93-96: Consider making model sources configurable.

The hard-coded model sources list might benefit from being configurable, especially as model availability changes over time.

Consider extracting the model sources to a configuration parameter or constant:

# At module level
DEFAULT_MODEL_SOURCES = [
    'KACE-1-0-G', 'MRI-ESM2-0', 'GFDL-ESM4', 'INM-CM4-8', 'IPSL-CM6A-LR',
    'INM-CM5-0', 'ACCESS-CM2', 'MIROC6', 'EC-Earth3-Veg-LR', 'BCC-CSM2-MR'
]

# In the function
model_sources = getattr(config.weather_helper, 'model_sources', DEFAULT_MODEL_SOURCES)
pyproject.toml (1)

68-68: Minor: pyepwmorph dependency — package present, no advisories; consider loosening the upper bound.

PyPI reports pyepwmorph latest 0.4.12 and the GitHub security query returned no vulnerabilities; current constraint (>=0.3.1,<0.4) excludes 0.4.x — widen or remove the upper bound if 0.4.x is compatible so you can receive fixes/patches.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 85c963c and 0fdfa38.

⛔ Files ignored due to path filters (1)
  • pixi.lock is excluded by !**/*.lock
📒 Files selected for processing (5)
  • cea/datamanagement/weather_helper/epwmorpher.py (1 hunks)
  • cea/datamanagement/weather_helper/weather_helper.py (2 hunks)
  • cea/default.config (1 hunks)
  • cea/utilities/epwreader.py (1 hunks)
  • pyproject.toml (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2024-12-10T14:11:58.324Z
Learnt from: reyery
PR: architecture-building-systems/CityEnergyAnalyst#3713
File: cea/datamanagement/weather_helper/weather_helper.py:94-94
Timestamp: 2024-12-10T14:11:58.324Z
Learning: When converting .dat weather files in `parse_dat_weather_file`, extract location information from the first 5 lines of the `.dat` file header instead of using hardcoded values.

Applied to files:

  • cea/utilities/epwreader.py
🧬 Code graph analysis (2)
cea/datamanagement/weather_helper/epwmorpher.py (3)
cea/datamanagement/weather_helper/weather_helper.py (1)
  • main (80-105)
cea/inputlocator.py (1)
  • InputLocator (20-1505)
cea/config.py (1)
  • Configuration (35-277)
cea/datamanagement/weather_helper/weather_helper.py (2)
cea/datamanagement/weather_helper/epwmorpher.py (1)
  • main (139-147)
cea/inputlocator.py (2)
  • ensure_parent_folder_exists (104-106)
  • get_zone_geometry (883-887)
🪛 GitHub Actions: Ruff
cea/datamanagement/weather_helper/epwmorpher.py

[error] 11-11: F401: 'datetime' imported but unused. Remove unused import.


[error] 74-74: E722: Do not use bare 'except'.


[error] 1-1: Ruff check failed: 2 errors found; run 'ruff --fix' to address issues.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: tests (macos-latest)
  • GitHub Check: tests (ubuntu-latest)
  • GitHub Check: tests (windows-latest)
🔇 Additional comments (14)
cea/utilities/epwreader.py (1)

75-90: LGTM! Simple and effective EPW location parser.

The function correctly extracts location metadata from the EPW header line using standard CSV parsing. The implementation is straightforward and handles the required fields properly.

cea/datamanagement/weather_helper/weather_helper.py (4)

13-13: LGTM! Clean integration of the morphing module.

The import statement properly adds the epwmorpher module for the new morphing functionality.


90-93: Good integration of morphing workflow with clear user feedback.

The conditional logic cleanly separates morphing from regular weather handling, and the informative message helps users understand what happened during morphing.


95-99: Improved error handling with clear guidance.

The enhanced error message provides users with actionable guidance when no weather file is provided.


100-105: LGTM! Proper file handling and control flow.

The code ensures the parent directory exists and maintains the existing logic for both online fetching and file copying scenarios.

cea/default.config (4)

128-131: LGTM! Well-designed morphing toggle parameter.

The boolean parameter with clear help text provides an intuitive way to enable/disable weather morphing.


132-135: LGTM! Appropriate default year for climate projections.

Setting 2050 as the default year aligns well with typical climate planning horizons.


143-147: LGTM! Good choice of median percentile as default.

Using the 50th percentile (median) as the default provides a balanced middle-ground approach for climate projections.


149-152: LGTM! Temperature as default variable makes sense.

Starting with temperature as the default morphing variable is logical since it's often the primary climate concern.

cea/datamanagement/weather_helper/epwmorpher.py (5)

25-43: LGTM! Clear scenario name mapping with good error handling.

The function provides explicit mappings between CEA and pyepwmorph scenario names and includes a helpful error message for unsupported values.


45-63: LGTM! Good file handling and validation.

The function properly constructs the project name, handles file copying for backup, and includes appropriate file existence checks.


99-107: LGTM! Well-structured configuration object creation.

The MorphConfig initialization uses clear parameter mapping and proper conversion of scenario names.


113-118: LGTM! Proper workflow integration with pyepwmorph.

The climate model data retrieval correctly uses the configuration parameters and follows the pyepwmorph workflow.


123-135: LGTM! Clean morphing and output handling.

The morphing process is well-structured, properly updates the year field, and writes the output to the expected location.

Comment on lines 72 to 76
try:
baseline_range = user_epw_object.detect_baseline_range()
except:
print("Could not detect the baseline range from the EPW file, using default of 1985-2014")
baseline_range = (1985, 2014) # default if the EPW file does not have the years in it
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Replace bare except clause with specific exception handling.

Using a bare except: clause can mask unexpected errors and make debugging difficult.

Apply this diff to use more specific exception handling:

 try: 
     baseline_range = user_epw_object.detect_baseline_range()
-except:
+except Exception as e:
     print("Could not detect the baseline range from the EPW file, using default of 1985-2014")
+    print(f"Error details: {e}")
     baseline_range = (1985, 2014)  # default if the EPW file does not have the years in it
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try:
baseline_range = user_epw_object.detect_baseline_range()
except:
print("Could not detect the baseline range from the EPW file, using default of 1985-2014")
baseline_range = (1985, 2014) # default if the EPW file does not have the years in it
try:
baseline_range = user_epw_object.detect_baseline_range()
except Exception as e:
print("Could not detect the baseline range from the EPW file, using default of 1985-2014")
print(f"Error details: {e}")
baseline_range = (1985, 2014) # default if the EPW file does not have the years in it
🧰 Tools
🪛 GitHub Actions: Ruff

[error] 74-74: E722: Do not use bare 'except'.

🤖 Prompt for AI Agents
In cea/datamanagement/weather_helper/epwmorpher.py around lines 72 to 76, the
try/except uses a bare except which can hide unexpected errors; replace it with
specific exception handling (for example catch AttributeError and ValueError
that detect_baseline_range might raise), and if you must keep a generic fallback
catch use "except Exception as e" and log the exception (via logging or the
module's logger) before assigning the default baseline_range = (1985, 2014).

Comment on lines 139 to 147
def main(config):
"""
This script gets a polygon and calculates the zone.shp and the occupancy.dbf and age.dbf inputs files for CEA
"""
assert os.path.exists(
config.scenario), 'Scenario not found: %s' % config.scenario
locator = cea.inputlocator.InputLocator(config.scenario)
print(f"{'=' * 10} Starting the climate morphing workflow for scenario {config.general.scenario} {'=' * 10}")
morphing_workflow(locator, config)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Update docstring to match actual functionality.

The docstring appears to be copied from another function and doesn't describe what this function actually does.

Apply this diff to fix the docstring:

 def main(config):
     """
-    This script gets a polygon and calculates the zone.shp and the occupancy.dbf and age.dbf inputs files for CEA
+    Main entry point for EPW weather file morphing using pyepwmorph.
+    
+    :param cea.config.Configuration config: Configuration object containing morphing parameters
     """
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def main(config):
"""
This script gets a polygon and calculates the zone.shp and the occupancy.dbf and age.dbf inputs files for CEA
"""
assert os.path.exists(
config.scenario), 'Scenario not found: %s' % config.scenario
locator = cea.inputlocator.InputLocator(config.scenario)
print(f"{'=' * 10} Starting the climate morphing workflow for scenario {config.general.scenario} {'=' * 10}")
morphing_workflow(locator, config)
def main(config):
"""
Main entry point for EPW weather file morphing using pyepwmorph.
:param cea.config.Configuration config: Configuration object containing morphing parameters
"""
assert os.path.exists(
config.scenario), 'Scenario not found: %s' % config.scenario
locator = cea.inputlocator.InputLocator(config.scenario)
print(f"{'=' * 10} Starting the climate morphing workflow for scenario {config.general.scenario} {'=' * 10}")
morphing_workflow(locator, config)
🤖 Prompt for AI Agents
In cea/datamanagement/weather_helper/epwmorpher.py around lines 139 to 147, the
function main has an incorrect copied docstring; replace it with a short
accurate docstring stating that main runs the climate morphing workflow for a
given CEA config: it validates the scenario path, creates an InputLocator,
prints a start message and calls morphing_workflow(locator, config). Keep it
concise and reflect the actual parameters and behavior.

Comment on lines 137 to 141
climate-pathway = moderate_case
climate-pathway.type = ChoiceParameter
climate-pathway.choices = Best Case, Moderate, Worst Case
climate-pathway.help = Climate pathway to morph the weather file to. The "Best Case" pathway corresponds to a 1.8°C increase in global temperature, the "Moderate" pathway corresponds to a 2.7°C increase, and the "Worst Case" pathway corresponds to a 4.4°C increase. More information about these pathways can be read here https://www.carbonbrief.org/explainer-how-shared-socioeconomic-pathways-explore-future-climate-change/.
climate-pathway.category = Morph Settings
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Consider correcting the default value format.

The default value should match one of the choices. The choices use spaces, but the default uses underscores.

Apply this diff to fix the default value:

-climate-pathway = moderate_case
+climate-pathway = Moderate
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
climate-pathway = moderate_case
climate-pathway.type = ChoiceParameter
climate-pathway.choices = Best Case, Moderate, Worst Case
climate-pathway.help = Climate pathway to morph the weather file to. The "Best Case" pathway corresponds to a 1.8°C increase in global temperature, the "Moderate" pathway corresponds to a 2.7°C increase, and the "Worst Case" pathway corresponds to a 4.4°C increase. More information about these pathways can be read here https://www.carbonbrief.org/explainer-how-shared-socioeconomic-pathways-explore-future-climate-change/.
climate-pathway.category = Morph Settings
climate-pathway = Moderate
climate-pathway.type = ChoiceParameter
climate-pathway.choices = Best Case, Moderate, Worst Case
climate-pathway.help = Climate pathway to morph the weather file to. The "Best Case" pathway corresponds to a 1.8°C increase in global temperature, the "Moderate" pathway corresponds to a 2.7°C increase, and the "Worst Case" pathway corresponds to a 4.4°C increase. More information about these pathways can be read here https://www.carbonbrief.org/explainer-how-shared-socioeconomic-pathways-explore-future-climate-change/.
climate-pathway.category = Morph Settings
🤖 Prompt for AI Agents
In cea/default.config around lines 137 to 141, the default value for
climate-pathway ("moderate_case") does not match any of the declared choices
(Best Case, Moderate, Worst Case); update the default to exactly one of the
choices — replace climate-pathway = moderate_case with climate-pathway =
Moderate so the default matches the choices list and preserves casing and
spacing.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
cea/datamanagement/weather_helper/epwmorpher.py (1)

146-149: Clean up commented code.

Remove the commented-out code block to maintain code cleanliness.

-    # morphed_data.write_to_file(os.path.join(morph_config.output_directory,
-    #                                         f"{str(user_future_year)}_{user_climate_pathway}_{percentile_key}.epw"))
-
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0fdfa38 and 1793a95.

📒 Files selected for processing (1)
  • cea/datamanagement/weather_helper/epwmorpher.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
cea/datamanagement/weather_helper/epwmorpher.py (2)
cea/datamanagement/weather_helper/weather_helper.py (1)
  • main (80-105)
cea/inputlocator.py (1)
  • InputLocator (20-1505)
🪛 GitHub Actions: Ruff
cea/datamanagement/weather_helper/epwmorpher.py

[error] 10-10: F401: 'datetime' imported but unused. Remove unused import. (ruff)


[error] 88-88: E722: Do not use bare except. Use a specific exception type. (ruff)

🔇 Additional comments (8)
cea/datamanagement/weather_helper/epwmorpher.py (8)

10-10: Remove unused datetime import.

The datetime module is imported but never used in this file, as confirmed by the pipeline failure.


32-50: LGTM! Clear scenario name mapping with proper error handling.

The function correctly maps CEA scenario names to pyepwmorph equivalents and raises a descriptive ValueError for unsupported inputs.


61-77: LGTM! Proper project setup and file validation.

The code correctly constructs the project name, creates a backup of the original EPW file, ensures the output directory exists, and validates the EPW file existence with appropriate error handling.


86-90: Replace bare except with specific exception handling.

Using a bare except: clause can mask unexpected errors and make debugging difficult, as flagged by the pipeline failure.


107-110: Verify model sources are current and complete.

The hardcoded list of climate model sources includes a comment indicating they're from 2025.09.23. Ensure these models are still valid and complete for the intended use cases.
Based on the search results, I can see that CMIP6 models are actively used and documented, but I need more specific information about the exact model names listed in the code to verify their validity.Perfect! The search results confirm that the model names listed in the code are valid CMIP6 models. I can see multiple references to these models:

  • KACE-1-0-G is confirmed as a valid CMIP6 model
  • MRI-ESM2-0 is confirmed as a valid CMIP6 model
  • GFDL-ESM4 is confirmed as a valid CMIP6 model
  • INM-CM4-8 and INM-CM5-0 are confirmed as valid CMIP6 models
  • IPSL-CM6A-LR, ACCESS-CM2, MIROC6, EC-Earth3-Veg-LR, BCC-CSM2-MR are all confirmed as valid CMIP6 models

All the models listed in the code are legitimate CMIP6 models used in climate research and downscaling applications.


113-121: LGTM! Well-structured morph configuration.

The MorphConfig object is properly instantiated with all necessary parameters, including proper list wrapping for scenario names and percentiles as expected by the pyepwmorph library.


127-143: LGTM! Proper climate data compilation and morphing workflow.

The code correctly:

  1. Compiles climate model data using geographic coordinates from the EPW file
  2. Applies morphing with appropriate parameters
  3. Updates the year in the morphed data to match the target year

153-164: Update docstring to reflect actual functionality.

The docstring appears to be copied from another function and doesn't accurately describe this function's purpose.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
cea/datamanagement/weather_helper/epwmorpher.py (1)

81-82: Normalize variables to list to avoid downstream type errors.

If a single variable is provided as a string in config, pyepwmorph may expect a list. Safe-cast here.

Apply this diff:

-    user_variables = config.weather_helper.variables
+    user_variables = config.weather_helper.variables
+    if isinstance(user_variables, str):
+        user_variables = [user_variables]
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1793a95 and 775c6ce.

📒 Files selected for processing (1)
  • cea/datamanagement/weather_helper/epwmorpher.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
cea/datamanagement/weather_helper/epwmorpher.py (2)
cea/datamanagement/weather_helper/weather_helper.py (1)
  • main (80-105)
cea/inputlocator.py (1)
  • InputLocator (20-1505)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: tests (ubuntu-latest)
  • GitHub Check: tests (macos-latest)
  • GitHub Check: tests (windows-latest)
🔇 Additional comments (3)
cea/datamanagement/weather_helper/epwmorpher.py (3)

85-90: Capture exception details (and prefer logging over print).

You switched away from a bare except (good), but you’re discarding the exception details. At minimum, include the exception in the message; ideally, use a logger.

Minimal change:

-    except Exception:
-        print("Could not detect the baseline range from the EPW file, using default of 1985-2014")
+    except Exception as e:
+        print(f"Could not detect the baseline range from the EPW file, using default of 1985-2014. Details: {e}")

Recommended (logging):

+import logging
+logger = logging.getLogger(__name__)
@@
-    except Exception as e:
-        print(f"Could not detect the baseline range from the EPW file, using default of 1985-2014. Details: {e}")
+    except Exception as e:
+        logger.warning(
+            "Could not detect the baseline range from the EPW file, defaulting to 1985–2014. Details: %s", e
+        )

152-158: Docstring is accurate and clear.

Entry-point docs now reflect the actual morphing workflow and parameters.


62-64: config.general.scenario is defined
Tests in cea/tests/test_config.py confirm config.general.scenario always exists and equals config.scenario; no AttributeError risk and no change needed.

Likely an incorrect or invalid review comment.

Comment on lines 16 to 21
# pyepwmorph import
from pyepwmorph.tools import workflow as pyepwmorph_workflow
from pyepwmorph.tools import utilities as pyepwmorph_utilities
from pyepwmorph.tools import io as pyepwmorph_io
from pyepwmorph.tools import configuration as pyepwmorph_config

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

🧩 Analysis chain

Confirm pyepwmorph version compatibility and pin appropriately.

pyepwmorph ≥0.4.12 contains relevant bug fixes (e.g., DNI > 1000 handling) but requires Python ≥3.10. If CEA’s runtime meets this, consider pinning to 0.4.12 for stability; otherwise ensure current pin documents rationale.


🌐 Web query:

What is the latest version of pyepwmorph and its Python version requirement?

💡 Result:

Latest pyepwmorph version: 0.4.12 (released Oct 24, 2024). Python requirement: Python >= 3.10. [1][2]

Sources:
[1] PyPI — pyepwmorph 0.4.12 (project page).
[2] Wheelodex / package metadata for pyepwmorph 0.4.12.


🏁 Script executed:

#!/bin/bash
set -e

echo "=== python_requires / requires-python checks ==="
rg -n 'python_requires' -g '*.{py,toml,cfg,ini}' || echo "No python_requires found"
rg -n 'requires-python' -g 'pyproject.toml' || echo "No requires-python in pyproject.toml"

echo
echo "=== GitHub Actions Python versions ==="
rg -n 'python-version' -g '.github/workflows/*.yml' || echo "No python-version entries in workflows"

echo
echo "=== Dependency pin checks for pyepwmorph ==="
rg -n 'pyepwmorph' -g 'requirements*' -C3 || echo "No explicit pyepwmorph pin in requirements files"
rg -n 'pyepwmorph' -g 'Pipfile' -C3 || echo "No explicit pyepwmorph pin in Pipfile"

Length of output: 834


Pin pyepwmorph to ≥0.4.12
Add pyepwmorph>=0.4.12,<0.5.0 to your dependency spec (pyproject.toml or requirements.txt); project already targets Python ≥3.10.

🤖 Prompt for AI Agents
In cea/datamanagement/weather_helper/epwmorpher.py around lines 16 to 21, the
project currently imports pyepwmorph but does not enforce a minimum compatible
version; update your dependency spec to pin pyepwmorph to a safe range by adding
"pyepwmorph>=0.4.12,<0.5.0" to your dependency file (pyproject.toml or
requirements.txt), then run dependency install/update (poetry install or pip
install -r requirements.txt) and update any lockfile so the project uses the
specified version range.

Comment on lines +48 to +49
raise ValueError(f"Could not interpret the climate pathway: {name}. "
f"Please choose one of the following: best_case, moderate_case, semi_moderate_case, worst_case")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Align error message with actual accepted inputs.

The message lists snake_case names and “semi_moderate_case”, which don’t match the accepted inputs here.

Apply this diff:

-        raise ValueError(f"Could not interpret the climate pathway: {name}. "
-                         f"Please choose one of the following: best_case, moderate_case, semi_moderate_case, worst_case")
+        raise ValueError(
+            f"Unsupported climate pathway: {name!r}. "
+            "Please choose one of: 'Best Case', 'Moderate', 'Upper Middle', or 'Worst Case'."
+        )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
raise ValueError(f"Could not interpret the climate pathway: {name}. "
f"Please choose one of the following: best_case, moderate_case, semi_moderate_case, worst_case")
raise ValueError(
f"Unsupported climate pathway: {name!r}. "
"Please choose one of: 'Best Case', 'Moderate', 'Upper Middle', or 'Worst Case'."
)
🤖 Prompt for AI Agents
In cea/datamanagement/weather_helper/epwmorpher.py around lines 48-49, the
ValueError message lists snake_case names and "semi_moderate_case" which do not
match the actual accepted pathway strings used by the function; update the
exception text so it lists the exact accepted input values (use the exact
strings from the pathway parsing/mapping in this module) instead of the current
incorrect snake_case list, ensuring the message matches the code’s accepted
inputs verbatim.

Comment on lines +65 to +76
output_directory = os.path.dirname(locator.get_weather_file())
shutil.copy(locator.get_weather_file(), os.path.join(output_directory, "before_morph_weather.epw"))

if not os.path.exists(output_directory):
os.makedirs(output_directory)


# 1.2. user_epw_file is specified in the config but defaults to the scenario file
user_epw_file = locator.get_weather_file()
if not os.path.exists(user_epw_file):
raise FileNotFoundError(f"Could not find the specified EPW file: {user_epw_file}")

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix backup ordering and avoid clobbering baseline on re-runs.

You copy weather.epw before ensuring the directory exists and before verifying the file exists, which can raise FileNotFoundError. Also, repeated runs can overwrite the original baseline backup. Ensure the folder exists first, verify the EPW exists, and only create the backup if it’s missing.

Apply this diff:

-    output_directory = os.path.dirname(locator.get_weather_file())
-    shutil.copy(locator.get_weather_file(), os.path.join(output_directory, "before_morph_weather.epw"))
-    
-    if not os.path.exists(output_directory):
-        os.makedirs(output_directory)
-    
-    
-    # 1.2. user_epw_file is specified in the config but defaults to the scenario file
-    user_epw_file = locator.get_weather_file()
-    if not os.path.exists(user_epw_file):
-        raise FileNotFoundError(f"Could not find the specified EPW file: {user_epw_file}")
+    output_directory = os.path.dirname(locator.get_weather_file())
+    os.makedirs(output_directory, exist_ok=True)
+
+    # 1.2. user_epw_file is specified in the config but defaults to the scenario file
+    user_epw_file = locator.get_weather_file()
+    if not os.path.exists(user_epw_file):
+        raise FileNotFoundError(f"Could not find the specified EPW file: {user_epw_file}")
+
+    # Backup the original weather file only once to preserve the baseline across re-runs
+    backup_epw = os.path.join(output_directory, "before_morph_weather.epw")
+    if not os.path.exists(backup_epw):
+        shutil.copy2(user_epw_file, backup_epw)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
output_directory = os.path.dirname(locator.get_weather_file())
shutil.copy(locator.get_weather_file(), os.path.join(output_directory, "before_morph_weather.epw"))
if not os.path.exists(output_directory):
os.makedirs(output_directory)
# 1.2. user_epw_file is specified in the config but defaults to the scenario file
user_epw_file = locator.get_weather_file()
if not os.path.exists(user_epw_file):
raise FileNotFoundError(f"Could not find the specified EPW file: {user_epw_file}")
output_directory = os.path.dirname(locator.get_weather_file())
os.makedirs(output_directory, exist_ok=True)
# 1.2. user_epw_file is specified in the config but defaults to the scenario file
user_epw_file = locator.get_weather_file()
if not os.path.exists(user_epw_file):
raise FileNotFoundError(f"Could not find the specified EPW file: {user_epw_file}")
# Backup the original weather file only once to preserve the baseline across re-runs
backup_epw = os.path.join(output_directory, "before_morph_weather.epw")
if not os.path.exists(backup_epw):
shutil.copy2(user_epw_file, backup_epw)
🤖 Prompt for AI Agents
In cea/datamanagement/weather_helper/epwmorpher.py around lines 65 to 76, the
code copies the EPW before ensuring the output directory exists and before
checking the file exists, and it always overwrites the baseline backup on
re-runs; fix by (1) compute the weather file path once, (2) ensure
output_directory exists (create it if missing) before any file operations, (3)
check that the weather file exists and raise FileNotFoundError if not, and (4)
create the backup copy only if a backup file (e.g., before_morph_weather.epw)
does not already exist to avoid clobbering the original on repeated runs.

Comment on lines +136 to +143
morphed_data = pyepwmorph_workflow.morph_epw(morph_config.epw,
morph_config.user_variables,
morph_config.baseline_range,
user_future_range,
year_model_dict,
[p for p in morph_config.model_pathways if p!="historical"][0],
user_percentile)
morphed_data.dataframe['year'] = int(user_future_year)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Guard against empty non-historical pathway selection.

Selecting [p for p in morph_config.model_pathways if p != "historical"][0] can raise IndexError if only “historical” is present. Add a safe selection with a clear error.

Apply this diff:

-    morphed_data = pyepwmorph_workflow.morph_epw(morph_config.epw,
-                                                morph_config.user_variables,
-                                                morph_config.baseline_range,
-                                                user_future_range,
-                                                year_model_dict,
-                                                [p for p in morph_config.model_pathways if p!="historical"][0],
-                                                user_percentile)
+    pathways = [p for p in morph_config.model_pathways if p != "historical"]
+    if not pathways:
+        raise ValueError("No non-historical pathway available in morph_config.model_pathways")
+    selected_pathway = pathways[0]
+    morphed_data = pyepwmorph_workflow.morph_epw(
+        morph_config.epw,
+        morph_config.user_variables,
+        morph_config.baseline_range,
+        user_future_range,
+        year_model_dict,
+        selected_pathway,
+        user_percentile,
+    )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
morphed_data = pyepwmorph_workflow.morph_epw(morph_config.epw,
morph_config.user_variables,
morph_config.baseline_range,
user_future_range,
year_model_dict,
[p for p in morph_config.model_pathways if p!="historical"][0],
user_percentile)
morphed_data.dataframe['year'] = int(user_future_year)
pathways = [p for p in morph_config.model_pathways if p != "historical"]
if not pathways:
raise ValueError("No non-historical pathway available in morph_config.model_pathways")
selected_pathway = pathways[0]
morphed_data = pyepwmorph_workflow.morph_epw(
morph_config.epw,
morph_config.user_variables,
morph_config.baseline_range,
user_future_range,
year_model_dict,
selected_pathway,
user_percentile,
)
morphed_data.dataframe['year'] = int(user_future_year)
🤖 Prompt for AI Agents
In cea/datamanagement/weather_helper/epwmorpher.py around lines 136 to 143, the
code selects the first non-"historical" pathway via [p for p in
morph_config.model_pathways if p!="historical"][0], which will raise IndexError
when only "historical" is present; change this to safely find the first
non-historical pathway (e.g., use next((p for p in morph_config.model_pathways
if p != "historical"), None)) and if none found raise a clear ValueError (or
similar) describing that no non-historical pathway is available and include
morph_config.model_pathways in the message so callers know what was provided;
then use that validated pathway when calling morph_epw.

ShiZhongming and others added 3 commits September 26, 2025 17:10
I updated pyepwmorph to include caching and it is now more clear about adding variables where they are needed. So the version also changed. Now we are on v1.0.3. I reinstalled it an tested it with a few configurations. Check it out.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
cea/datamanagement/weather_helper/epwmorpher.py (2)

80-83: Ensure variables are lists (not scalars) before passing to pyepwmorph

Coerce strings to a single-item list to avoid API mismatches.

Apply this diff:

-    user_variables = config.weather_helper.variables
+    user_variables = config.weather_helper.variables
+    if isinstance(user_variables, str):
+        user_variables = [user_variables]

98-99: Prefer structured logging over print statements

Use a module logger (logging.getLogger(name)) and log at info/warning levels. Improves observability and integrates with CEA’s logging.

Also applies to: 112-113, 125-126, 138-139, 165-166

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b2d0cc3 and 3fa9dfc.

📒 Files selected for processing (2)
  • cea/datamanagement/weather_helper/epwmorpher.py (1 hunks)
  • cea/default.config (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • cea/default.config
🧰 Additional context used
🧬 Code graph analysis (1)
cea/datamanagement/weather_helper/epwmorpher.py (2)
cea/datamanagement/weather_helper/weather_helper.py (1)
  • main (80-105)
cea/inputlocator.py (1)
  • InputLocator (20-1505)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: tests (windows-latest)
  • GitHub Check: tests (macos-latest)
  • GitHub Check: tests (ubuntu-latest)
🔇 Additional comments (6)
cea/datamanagement/weather_helper/epwmorpher.py (6)

32-51: Harden scenario parsing and fix misleading error text

Make mapping case-insensitive, accept common SSP aliases, and correct the error message to list the actual accepted names.

Apply this diff:

-def convert_scenario_names(name):
-    """
-    Convert the scenario names from CEA to the names used in pyepwmorph
-
-    :param string name: name of the scenario in CEA
-    :return: name of the scenario in pyepwmorph
-    :rtype: string
-    """
-    if name == "Best Case":
-        return "Best Case Scenario"
-    elif name == "Moderate":
-        return "Middle of the Road"
-    elif name == "Upper Middle": 
-        return "Upper Middle Scenario"
-    elif name == "Worst Case":
-        return "Worst Case Scenario"
-    else:
-        raise ValueError(f"Could not interpret the climate pathway: {name}. "
-                         f"Please choose one of the following: best_case, moderate_case, semi_moderate_case, worst_case")
+def convert_scenario_names(name: str) -> str:
+    """
+    Convert a CEA pathway string (or SSP alias) to the pyepwmorph label.
+    """
+    mapping = {
+        # Best Case (SSP1-2.6)
+        "best case": "Best Case Scenario",
+        "best_case": "Best Case Scenario",
+        "ssp126": "Best Case Scenario",
+        "ssp1-2.6": "Best Case Scenario",
+        # Moderate (SSP2-4.5)
+        "moderate": "Middle of the Road",
+        "middle of the road": "Middle of the Road",
+        "middle_of_the_road": "Middle of the Road",
+        "ssp245": "Middle of the Road",
+        "ssp2-4.5": "Middle of the Road",
+        # Upper Middle (SSP3-7.0)
+        "upper middle": "Upper Middle Scenario",
+        "upper_middle": "Upper Middle Scenario",
+        "ssp370": "Upper Middle Scenario",
+        "ssp3-7.0": "Upper Middle Scenario",
+        # Worst Case (SSP5-8.5)
+        "worst case": "Worst Case Scenario",
+        "worst_case": "Worst Case Scenario",
+        "ssp585": "Worst Case Scenario",
+        "ssp5-8.5": "Worst Case Scenario",
+    }
+    key = (name or "").strip().lower()
+    if key in mapping:
+        return mapping[key]
+    raise ValueError(
+        f"Unsupported climate pathway: {name!r}. Choose one of: 'Best Case', 'Moderate', 'Upper Middle', or 'Worst Case'."
+    )

86-91: Avoid bare broad except; include error details

Capture the exception for troubleshooting; prefer logging in follow-up.

Apply this diff:

-    try:
-        baseline_range = user_epw_object.detect_baseline_range()
-    except Exception:
-        print("Could not detect the baseline range from the EPW file, using default of 1985-2014")
-        baseline_range = (1985, 2014)  # default if the EPW file does not have the years in it
+    try:
+        baseline_range = user_epw_object.detect_baseline_range()
+    except Exception as e:
+        print("Could not detect the baseline range from the EPW file, using default of 1985-2014")
+        print(f"Error details: {e}")
+        baseline_range = (1985, 2014)  # default if the EPW file does not have the years in it

16-22: Pin pyepwmorph to ≥0.4.12 and verify Python ≥3.10

Recent bugfixes (e.g., DNI handling) landed in 0.4.12. Recommend pinning pyepwmorph>=0.4.12,<0.5.0 and ensuring the project runs on Python ≥3.10. Update your dependency files accordingly.

Based on learnings

#!/bin/bash
# Check dependency pins and Python version constraints in the repo
set -e
echo "=== Search dependency specs for pyepwmorph ==="
rg -nP 'pyepwmorph\s*[<>=]' -C2 || true

echo
echo "=== Check Python versions in workflows/packaging ==="
rg -n 'python-version' -g '.github/workflows/*.yml' || true
rg -nP '(requires-python|python_requires)' -g '*.{toml,cfg,ini}' || true

66-76: Fix backup ordering and avoid clobbering the baseline on re-runs

Copying before ensuring the folder exists can raise FileNotFoundError, and always overwriting the backup loses the baseline. Ensure the folder exists first and only create the backup if it’s missing.

Apply this diff:

-    output_directory = os.path.dirname(locator.get_weather_file())
-    shutil.copy(locator.get_weather_file(), os.path.join(output_directory, "before_morph_weather.epw"))
-    
-    if not os.path.exists(output_directory):
-        os.makedirs(output_directory)
-    
-    
-    # 1.2. user_epw_file is specified in the config but defaults to the scenario file
-    user_epw_file = locator.get_weather_file()
-    if not os.path.exists(user_epw_file):
-        raise FileNotFoundError(f"Could not find the specified EPW file: {user_epw_file}")
+    output_directory = os.path.dirname(locator.get_weather_file())
+    os.makedirs(output_directory, exist_ok=True)
+
+    # 1.2. user_epw_file is specified in the config but defaults to the scenario file
+    user_epw_file = locator.get_weather_file()
+    if not os.path.exists(user_epw_file):
+        raise FileNotFoundError(f"Could not find the specified EPW file: {user_epw_file}")
+
+    # Backup the original weather file once to preserve the baseline across re-runs
+    backup_epw = os.path.join(output_directory, "before_morph_weather.epw")
+    if not os.path.exists(backup_epw):
+        shutil.copy2(user_epw_file, backup_epw)

139-145: Guard pathway selection against empty result

Indexing [0] after filtering can raise IndexError if only “historical” is present. Select safely and fail with a clear message.

Apply this diff:

-    morphed_data = pyepwmorph_workflow.morph_epw(morph_config.epw,
-                                                morph_config.user_variables,
-                                                morph_config.baseline_range,
-                                                user_future_range,
-                                                year_model_dict,
-                                                [p for p in morph_config.model_pathways if p!="historical"][0],
-                                                user_percentile)
+    pathways = [p for p in morph_config.model_pathways if p != "historical"]
+    if not pathways:
+        raise ValueError(f"No non-historical pathway available in morph_config.model_pathways: {morph_config.model_pathways}")
+    selected_pathway = pathways[0]
+    morphed_data = pyepwmorph_workflow.morph_epw(
+        morph_config.epw,
+        morph_config.user_variables,
+        morph_config.baseline_range,
+        user_future_range,
+        year_model_dict,
+        selected_pathway,
+        user_percentile,
+    )

93-95: Drop redundant int(...) cast config.weather_helper.year is already parsed as an int by IntegerParameter decoding.

Likely an incorrect or invalid review comment.

@reyery reyery linked an issue Sep 29, 2025 that may be closed by this pull request
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Enhancing CEA's Weather Helper
3 participants