Working with PWW Files
.pww is the PowerWorld Timestep Simulation Weather format. You can open the
downloaded files directly in PowerWorld, or read them into numpy with the
pww_io module bundled with this package.
Reading a file
from TeamOverbyeWeather import pww_io
header, stations, arr = pww_io.read_pww(open(path, "rb").read())
For large files, read from disk instead — it memory-maps rather than loading everything at once:
header, stations, arr = pww_io.read_pww_file(path)
The array
arr is a 4-dimensional uint8 array:
arr.shape # (385, 8, 44, 53)
# time, variable, latitude, longitude
Axis |
Meaning |
|---|---|
0 |
time step — |
1 |
weather variable — see |
2 |
latitude, ascending (south to north) |
3 |
longitude, descending (east to west) |
Warning
The longitude axis runs east to west, so index 0 is lon_max, not
lon_min. This trips people up when indexing by hand — the latitude axis runs
the other way.
255 is the missing-data sentinel, not a real measurement. Mask it before doing arithmetic:
import numpy as np
data = arr.astype(float)
data[arr == 255] = np.nan
The header
header
{'key1': 2001, 'key2': 8066, 'version': 2,
'date_min': 46225.5, 'date_max': 46241.5,
'lat_min': 25.75, 'lat_max': 36.5,
'lon_min': -106.5, 'lon_max': -93.5,
'meta_strings': ['PowerWorld Timestep Simulation Weather'],
'count': 385, 'sample_sec': 3600,
'loc': 2332, 'loc_fc': 0,
'varcount': 8, 'var_codes': [102, 104, 106, 107, 119, 110, 120, 121]}
Field |
Meaning |
|---|---|
|
1 or 2 — both are readable |
|
first and last time step, in OLE Automation days |
|
grid extent in degrees |
|
number of time steps |
|
seconds between steps (900 = 15 min, 3600 = hourly) |
|
number of station records |
|
how many weather variables, and their codes |
var_codes are character codes; the mapping to physical quantities is defined by
the PowerWorld weather format. Read them as letters:
print([chr(c) for c in header["var_codes"]])
['f', 'h', 'j', 'k', 'w', 'n', 'x', 'y']
Dates
PWW stores time as OLE Automation days — days since 30 December 1899 — not Unix time. Convert:
from datetime import datetime, timezone
OLE_EPOCH = 25569.0 # OLE days between 1899-12-30 and 1970-01-01
def ole_to_utc(ole):
return datetime.fromtimestamp((ole - OLE_EPOCH) * 86400, tz=timezone.utc)
print(ole_to_utc(header["date_min"])) # 2026-07-22 12:00:00+00:00
Build a timestamp for every step:
import numpy as np
start = ole_to_utc(header["date_min"])
times = [start + np.timedelta64(i * header["sample_sec"], "s").item()
for i in range(arr.shape[0])]
Coordinates
Grid spacing is 0.25° throughout. Reconstruct the axes:
import numpy as np
lats = np.arange(header["lat_min"], header["lat_max"] + 0.001, 0.25)
lons = np.arange(header["lon_max"], header["lon_min"] - 0.001, -0.25) # descending
assert len(lats) == arr.shape[2]
assert len(lons) == arr.shape[3]
Station records
VERSION 2 files carry one record per grid point:
stations[0]
{'lat': 25.75, 'lon': -93.5, 'elev': 0,
'who': '+25.75-093.50/', 'country': '', 'region': 'NorthAmerica'}
VERSION 1 files (older ERA5) have a station block holding grid metadata rather
than real coordinates, so read_pww returns stations == [] for them. Use the
header extent to build coordinates instead — the code above works for both.
Cropping locally
If you already have a file on disk and want a smaller piece, crop without re-downloading:
from TeamOverbyeWeather import localcrop
localcrop.crop_file(
"data/big_file.zip",
"data/small.pww",
bbox=(36.5, -106.6, 25.8, -93.5),
t_start=None, # Unix epoch seconds, or None
t_end=None,
)
This handles every on-disk shape — a bare .pww, a zip with one .pww, a zip
with four quarter .pww files, or a zip of daily zips — stitching multi-part
files along the time axis. Given the same inputs it produces the same output as
the server.
The lower-level pieces are available too:
header, stations, arr = pww_io.crop_to_bbox(header, stations, arr, bbox)
header, arr = pww_io.crop_to_timerange(header, arr, t_start, t_end)
data = pww_io.write_pww(header, stations, arr)
crop_to_timerange takes Unix epoch seconds, and either bound may be None.
Writing a file
open("out.pww", "wb").write(pww_io.write_pww(header, stations, arr))
The original version and magic numbers are preserved, and VERSION 2 valid-counts are recomputed from the array — so a file written this way opens in PowerWorld like any other.
Tip
Always .copy() after slicing an array before writing it. Numpy slices are
views, and write_pww expects a contiguous buffer. The crop_* helpers already
do this for you.