Skip to main content

pyepwmorph

License: MIT Python 3.9+ PyPI

A Python package for morphing EnergyPlus Weather (EPW) files with climate model data. Supports CMIP6 projections from Google Cloud and custom CSV-based model data for both future and historical scenarios.

Overview

pyepwmorph enables building performance analysts and researchers to create morphed weather files by applying scientifically validated procedures (Belcher et al. 2005, Jentsch et al. 2013) to existing EPW files. The package can morph EPWs using:

  • CMIP6 data fetched automatically from Google Cloud (Pangeo)
  • Custom CSV data from any climate model, including historical reconstructions
  • CH2025 station scenarios for Switzerland, shipped with the package and indexed by global warming level

Installation

uv add pyepwmorph

Development setup

git clone https://github.com/justinfmccarty/pyepwmorph.git
cd pyepwmorph
uv sync --extra dev

With pip

pip install pyepwmorph

Quick start

CMIP6 workflow (future projections)

import pyepwmorph.tools.workflow as workflow

results = workflow.morphing_workflow(
    project_name="MyBuilding_Future",
    epw_file="weather.epw",
    user_variables=["Temperature", "Humidity", "Clouds and Radiation"],
    user_pathways=["Middle of the Road"],   # ssp245
    percentiles=[50],
    target_years=[2050],
    output_directory="output/",
)

Custom CSV workflow (historical or any model)

import pyepwmorph.tools.workflow as workflow

custom_data = {
    "reference": {
        "tas": "data/reference_tas.csv",
        "tasmax": "data/reference_tasmax.csv",
        "tasmin": "data/reference_tasmin.csv",
    },
    "target_1990s": {
        "tas": "data/target_tas.csv",
        "tasmax": "data/target_tasmax.csv",
        "tasmin": "data/target_tasmin.csv",
    },
}

results = workflow.morphing_workflow(
    project_name="Historical_Morph",
    epw_file="weather.epw",
    user_variables=["Temperature"],
    user_pathways=["target_1990s"],
    percentiles=[50],
    target_years=[1990],
    output_directory="output/",
    data_source="custom",
    custom_data=custom_data,
    reference_scenario="reference",
)

Custom CSVs should have a date column (parseable by pandas) and a column named after the CMIP6 variable (e.g. tas, tasmax). Rows should be monthly.

The reference and target scenarios must cover different years, the same way the CMIP6 historical and sspXXX experiments do. The two series are concatenated before the baseline and target periods are sliced out, so overlapping years get averaged together and weaken the climate signal. Keep baseline_range inside the years the reference scenario covers.

Switzerland (CH2025 warming levels)

For an EPW inside Switzerland, data_source="ch2025" morphs from MeteoSwiss CH2025 station scenarios. There is no target year: each pathway is a global warming level relative to 1991-2020, and the run is offline.

results = workflow.morphing_workflow(
    project_name="Zurich_GWL2",
    epw_file="zurich.epw",
    user_variables=["Temperature", "Humidity", "Wind", "Radiation", "Dew Point"],
    user_pathways=["GWL 2.0"],
    percentiles=[50],
    output_directory="output/",
    data_source="ch2025",
)
morphed = results["gwl2.0"]["50"]

The site is matched to the nearest station that has the requested variables (or, with ch2025_full_coverage=True, the nearest station that has every CH2025 variable), with an elevation penalty so a much higher station is not chosen just because it is close on the map. Supported variables are temperature, humidity, dew point, wind, and radiation (global, diffuse, and direct). Pressure and cloud cover are not in CH2025; requesting them raises an error. Sky cover is left at the EPW's baseline values when radiation is morphed. Horizontal infrared follows the morphed temperature and humidity (see "Variable dependencies"), but not cloud changes.

The signal is the change from the 1991-2020 reference climate to the chosen warming level. Each model chain's change is computed first, over the chains that reach that warming level, and the percentiles are taken of those changes. The EPW's period is read from the Period of Record in its header. If that is not exactly 1991-2020, a UserWarning recommends a TMY built from 1991-2020 data. If the header states no period, a second warning says the period could not be detected, and the mismatch warning still fires if the data rows' years fall outside 1991-2020. The signal is applied to the file as it stands either way. tools.io.write_period_of_record adds the statement to files built without one. The same caveats are listed in MorphConfig.ch2025_notes and the matched station in MorphConfig.ch2025_station.

CH2025 data: MeteoSwiss & ETH Zurich (2025), Climate CH2025 - Daily Datasets, CC-BY 4.0, https://doi.org/10.18751/climate/scenarios/ch2025/data/1.0/

Measured data (actual meteorological year)

Moved to weather-file-builder (2.1 and later), together with ISO 15927-4 typical years from station data: weather_file_builder.amy.station_table_to_epw and weather_file_builder.station_tmy.station_table_to_tmy_epw. pyepwmorph.tools.amy still works in 3.4 but warns on import and will be removed in 4.0.

Climate scenarios

Scenario SSP Description Expected warming
Best Case Scenario ssp126 Strong mitigation, renewable transition ~1.8 C by 2100
Middle of the Road ssp245 Moderate mitigation efforts ~2.7 C by 2100
Upper Middle Scenario ssp370 Regional rivalry, slow convergence ~3.6 C by 2100
Worst Case Scenario ssp585 Fossil-fueled development ~4.4 C by 2100

Morphing variables

  • Temperature -- dry bulb temperature (shift + stretch)
  • Humidity -- relative humidity, stretched in specific humidity space
  • Pressure -- atmospheric pressure (shift)
  • Wind -- wind speed (stretch)
  • Radiation -- global/diffuse/direct radiation, without changing sky cover (all sources; the only radiation option for CH2025)
  • Clouds and Radiation -- global/diffuse/direct radiation and sky cover (CMIP6 and custom data). Morphed sky cover keeps two decimals (EnergyPlus reads it as a real number), so small monthly changes are not lost to rounding
  • Dew Point -- recalculated from morphed temperature and humidity

Variable dependencies

Some variables cannot be morphed on their own:

  • Humidity requires Temperature and Pressure, except with CH2025, where relative humidity is stretched directly
  • Dew Point requires Temperature, Humidity, and Pressure (Temperature and Humidity only with CH2025)

Dependencies are added automatically and are written to the output file. Asking for Dew Point alone therefore returns an EPW with morphed pressure, temperature, relative humidity, and dew point, which keeps the file internally consistent. MorphConfig.resolved_variables shows exactly what will be written, in the order it is computed.

Some fields are recomputed from the morphed ones without climate data of their own (MorphConfig.recomputed_variables):

  • Dew point whenever Temperature or Humidity is morphed. After a temperature-only morph it comes from the morphed dry-bulb and the file's own relative humidity, so relative humidity is held as it was.
  • Horizontal infrared whenever Temperature, Humidity, Dew Point or Clouds and Radiation is morphed. The file's own value is scaled by the change in eps_sky * sigma * T^4 from the Clark and Allen sky model (EnergyPlus's default), computed from dry-bulb, dew point and opaque sky cover. Missing values (9999) stay missing. With CH2025, which has no cloud data, it follows temperature and humidity only.

Header blocks

A morphed file's header is brought in line with its new climate:

  • GROUND TEMPERATURES shift with the change in monthly mean dry-bulb. The annual-mean change passes through in full; the seasonal part is damped and lagged at each depth the way the source header's own ground temperatures are against the source air. A file without ground temperatures gets them at 0.5, 2 and 4 m from the morphed air (Kusuda and Achenbach).
  • DESIGN CONDITIONS are replaced by DESIGN CONDITIONS,0, because the source's ASHRAE values describe the baseline climate. Size equipment from the morphed hours, not from the header.

Both are noted in COMMENTS 2. CMIP6 morphs also name the grid cell each model's data came from and its distance from the site.

Caching

Climate model data is cached locally after the first download, and the cache is consulted before the remote catalogue is opened so a hit costs no network traffic.

import pyepwmorph.models.access as access

stats = access.get_cmip6_cache_stats()
access.clear_cmip6_cache()

Cache entries live in the per-user cache directory (~/Library/Caches/pyepwmorph on macOS, ~/.cache/pyepwmorph on Linux, %LOCALAPPDATA%\pyepwmorph on Windows) and are keyed by location, pathway, variable, model sources, and time slices. Two environment variables override the defaults:

Variable Purpose Default
PYEPWMORPH_CACHE_DIR Where cache files are written per-user cache directory
PYEPWMORPH_CACHE_MAX_MB Size cap before old files go 500

Available climate models

from pyepwmorph.tools.utilities import available_models

models = available_models()

Development

git clone https://github.com/justinfmccarty/pyepwmorph.git
cd pyepwmorph
uv sync --extra dev

# Run tests (fully offline)
uv run pytest

# Run with coverage
uv run pytest --cov=pyepwmorph

# Lint
uv run ruff check pyepwmorph tests gui

Releases

./release.sh [patch|minor|major]

The script refuses to run on a dirty tree, off main, with failing lint or tests, or without a matching CHANGELOG.md section. It bumps the version, tags, and pushes; creating the GitHub Release then triggers the PyPI publish workflow.

gh release create v3.0.0 \
  --title "v3.0.0" \
  --notes "See CHANGELOG.md."

Changes

See CHANGELOG.md for the full history. The most recent release corrects several morphing calculations, so morphed humidity, dew point, wind speed, cloud cover, and direct/diffuse radiation all differ from files produced by earlier versions.

Requirements

  • Python >= 3.9
  • pandas >= 2.2
  • Internet connection (for CMIP6 data download; the custom CSV and CH2025 workflows run offline)

License

MIT License. See LICENSE.

Citation

McCarty, J. (2026). pyepwmorph: A Python package for climate-informed
EPW file morphing. Version 3.0.0.
https://github.com/justinfmccarty/pyepwmorph

Metadata

Release files for pyepwmorph 3.6.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pyepwmorph 3.6.1
File Size Uploaded Provenance Actions
pyepwmorph-3.6.1.tar.gz 3.3 MB With provenance Details for pyepwmorph-3.6.1.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for pyepwmorph 3.6.1
File Interpreter ABI Platform Actions
pyepwmorph-3.6.1-py3-none-any.whl Python 3 none any Details for pyepwmorph-3.6.1-py3-none-any.whl

Total release size: 6.7 MB

Release files / pyepwmorph-3.6.1.tar.gz

Download URL pyepwmorph-3.6.1.tar.gz
Size 3.3 MB
Tags Source
SHA-256 checksum
How to use checksums
7a1ed88a0eeba825d279eff2056a9365067a71e2e2bbf9ee4a9343597a850c8c
BLAKE2b-256 checksum
How to use checksums
c827a745c30ce57915d84912102d31c39e7714e2f484f86add99357786245dd7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 2, 2026.

Transparency log

Release files / pyepwmorph-3.6.1-py3-none-any.whl

Download URL pyepwmorph-3.6.1-py3-none-any.whl
Size 3.3 MB
Tags Python 3
SHA-256 checksum
How to use checksums
71d15476e0677e04d2eb202332c8ddb7ce9e7de84c1cdf0032413c8158f73396
BLAKE2b-256 checksum
How to use checksums
fff46726cf69c085416c927471b5f7b2e42c253eefd15d361ad62cf6fa8a364a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 2, 2026.

Transparency log

Security for pyepwmorph 3.6.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for pyepwmorph-3.6.1.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 2, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

3.6.1 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page