Source code for windkit.io.wasp.lib

"""Read and write WAsP .lib files.

The .lib format stores generalized wind climate data as Weibull
parameters (A, k) and wind direction frequencies for a single point
across multiple generalized roughness classes and heights.

Data contract: wdfreq values are stored as percentages (0-100) in the
file but returned/expected as normalized fractions (0-1) by these
functions. Conversion happens automatically. A and k are raw Weibull
parameters (no conversion).
"""

import re

import numpy as np

from windkit.io.wasp._common import detect_encoding


[docs] def read_lib(filepath, encoding="infer"): """Read a WAsP .lib file and return parsed data. Parameters ---------- filepath : str or Path Path to the ``.lib`` file. encoding : str, optional File encoding. If ``"infer"`` (default), the encoding is detected automatically. Returns ------- dict Dictionary with keys: - ``"A"`` : ndarray, shape (n_sectors, n_heights, n_roughnesses) - ``"k"`` : ndarray, same shape as *A* - ``"wdfreq"`` : ndarray, shape (n_sectors, n_roughnesses), normalized values (0-1) - ``"gen_roughness"`` : ndarray, shape (n_roughnesses,) - ``"gen_height"`` : ndarray, shape (n_heights,) - ``"coords"`` : dict with ``"west_east"``, ``"south_north"``, ``"height"`` (values are *None* when the file has no ``<coordinates>`` tag) - ``"metadata"`` : dict with ``"description"`` """ if encoding == "infer": encoding = detect_encoding(filepath) with open(filepath, encoding=encoding) as f: desc = f.readline().strip() n_roughnesses, n_heights, n_sectors = ( int(x) for x in f.readline().strip().split() ) gen_roughness = np.array(f.readline().strip().split(), dtype=np.float64) gen_height = np.array(f.readline().strip().split(), dtype=np.float64) wdfreq = np.zeros((n_sectors, n_roughnesses), dtype=np.float64) A = np.zeros((n_sectors, n_heights, n_roughnesses), dtype=np.float64) k = np.zeros((n_sectors, n_heights, n_roughnesses), dtype=np.float64) def _read_floats(f): return [float(x) for x in f.readline().strip().split()] for i in range(n_roughnesses): wdfreq[:, i] = _read_floats(f) for j in range(n_heights): A[:, j, i] = _read_floats(f) k[:, j, i] = _read_floats(f) coords = {"west_east": None, "south_north": None, "height": None} match = re.search(r"<coordinates>(.*?)</coordinates>", desc) if match: we, sn, h = (float(x) for x in match.group(1).split(",")) coords = {"west_east": we, "south_north": sn, "height": h} if gen_roughness[0] != 0.0: raise ValueError( f"The first generalized roughness must be 0.0, got {gen_roughness[0]}" ) # Normalize wdfreq from percent (0-100) to fraction (0-1) wdfreq_sum = wdfreq.sum(axis=0, keepdims=True) wdfreq_sum[wdfreq_sum == 0] = 1.0 # avoid division by zero wdfreq = wdfreq / wdfreq_sum return { "A": A, "k": k, "wdfreq": wdfreq, "gen_roughness": gen_roughness, "gen_height": gen_height, "coords": coords, "metadata": {"description": desc}, }
[docs] def format_lib(A, k, wdfreq, gen_roughness, gen_height, coords, metadata=None): """Format Weibull parameters as a .lib file content string. Parameters ---------- A : array_like, shape (n_sectors, n_heights, n_roughnesses) Weibull A parameters. k : array_like, shape (n_sectors, n_heights, n_roughnesses) Weibull k parameters. wdfreq : array_like, shape (n_sectors, n_roughnesses) Normalized wind direction frequencies (0-1). gen_roughness : array_like, shape (n_roughnesses,) Generalized roughness classes. gen_height : array_like, shape (n_heights,) Generalized heights. coords : dict Dictionary with ``"west_east"``, ``"south_north"``, ``"height"``. metadata : dict, optional Dictionary with ``"description"`` key. Returns ------- str File content string (with ``\\n`` line endings). """ A = np.asarray(A) k = np.asarray(k) wdfreq = np.asarray(wdfreq) * 100.0 # Convert from fraction (0-1) to percent gen_roughness = np.asarray(gen_roughness) gen_height = np.asarray(gen_height) if gen_roughness[0] != 0.0: raise ValueError( f"The first generalized roughness must be 0.0, got {gen_roughness[0]}" ) n_roughnesses = len(gen_roughness) n_heights = len(gen_height) n_sectors = A.shape[0] lines = [] # Description line with coordinate tag description = metadata.get("description", "") if metadata else "" description = re.sub(r"<coordinates>.*?</coordinates>", "", description) coord_tag = "" if coords and coords.get("west_east") is not None: coord_tag = ( f"<coordinates>" f"{coords['west_east']},{coords['south_north']},{coords['height']}" f"</coordinates>" ) lines.append(description + coord_tag) # Dimensions: n_roughnesses, n_heights, n_sectors lines.append(f"{n_roughnesses} {n_heights} {n_sectors}") # Roughness classes lines.append(" ".join(f"{v:9.3f}" for v in gen_roughness)) # Heights lines.append(" ".join(f"{v:9.1f}" for v in gen_height)) # Data arrays for i in range(n_roughnesses): lines.append("".join(f"{v:9.2f}" for v in wdfreq[:, i])) for j in range(n_heights): lines.append("".join(f"{v:9.2f}" for v in A[:, j, i])) lines.append("".join(f"{v:9.3f}" for v in k[:, j, i])) return "\n".join(lines) + "\n"
[docs] def write_lib(filepath, A, k, wdfreq, gen_roughness, gen_height, coords, metadata=None): """Write Weibull parameters to a WAsP .lib file. Parameters ---------- filepath : str or Path Output file path. A : array_like, shape (n_sectors, n_heights, n_roughnesses) Weibull A parameters. k : array_like, shape (n_sectors, n_heights, n_roughnesses) Weibull k parameters. wdfreq : array_like, shape (n_sectors, n_roughnesses) Normalized wind direction frequencies (0-1). gen_roughness : array_like, shape (n_roughnesses,) Generalized roughness classes. gen_height : array_like, shape (n_heights,) Generalized heights. coords : dict Dictionary with ``"west_east"``, ``"south_north"``, ``"height"``. metadata : dict, optional Dictionary with ``"description"`` key. """ content = format_lib(A, k, wdfreq, gen_roughness, gen_height, coords, metadata) with open(filepath, "w", newline="\r\n") as f: f.write(content)