Source code for chiller_sim.layout.grid

from __future__ import annotations

from dataclasses import dataclass

import numpy as np
from numpy.typing import NDArray


[docs] @dataclass(frozen=True) class ChillerLayout: """Immutable description of a chiller array layout and its ageing state.""" positions_m: NDArray[np.float64] # shape (n_chillers, 2) base_cop: float alpha: float ages_years: NDArray[np.float64] # shape (n_chillers,) max_cooling_kw: float # nameplate capacity of a brand-new chiller (same for all) @property def num_chillers(self) -> int: """Return the total number of chillers in the grid.""" return len(self.positions_m)
[docs] @classmethod def from_positions( cls, positions_m: NDArray[np.float64], ages_years: NDArray[np.float64], base_cop: float, max_cooling_kw: float, alpha: float = 0.7, ) -> ChillerLayout: """Build a layout from explicit (x, y) positions. Parameters ---------- positions_m : ndarray, shape (n, 2) Each row is an (x, y) coordinate in metres. ages_years : ndarray, shape (n,) Age of each chiller in years. base_cop : float Nameplate COP for a brand-new chiller. max_cooling_kw : float Nameplate cooling capacity in kW (must be > 0). alpha : float, optional Age-degradation exponent, by default 0.7. Returns ------- ChillerLayout """ positions_m = np.asarray(positions_m, dtype=np.float64) ages_years = np.asarray(ages_years, dtype=np.float64) if positions_m.ndim != 2 or positions_m.shape[1] != 2: raise ValueError(f"positions_m must have shape (n, 2), got {positions_m.shape}") if ages_years.shape[0] != positions_m.shape[0]: raise ValueError( f"ages_years length {len(ages_years)} does not match " f"{positions_m.shape[0]} positions" ) if max_cooling_kw <= 0: raise ValueError(f"max_cooling_kw must be > 0, got {max_cooling_kw}") return cls( positions_m=positions_m, base_cop=base_cop, alpha=alpha, ages_years=ages_years, max_cooling_kw=max_cooling_kw, )
[docs] @classmethod def create_grid( cls, rows: int, cols: int, spacing_m: float, base_cop: float, max_cooling_kw: float, alpha: float = 0.7, ages_years: NDArray[np.float64] | None = None, seed: int | None = None, ) -> ChillerLayout: """Build a regular rectangular chiller grid with optional random ages.""" xs = np.arange(cols) * spacing_m ys = np.arange(rows) * spacing_m xx, yy = np.meshgrid(xs, ys) positions = np.column_stack([xx.ravel(), yy.ravel()]) n = rows * cols if ages_years is None: rng = np.random.default_rng(seed) resolved_ages = rng.uniform(0.0, 20.0, size=n) else: resolved_ages = np.asarray(ages_years, dtype=np.float64) if len(resolved_ages) != n: raise ValueError( f"ages_years length {len(resolved_ages)} does not match " f"grid size {n} ({rows}×{cols})" ) return cls.from_positions( positions_m=positions, ages_years=resolved_ages, base_cop=base_cop, max_cooling_kw=max_cooling_kw, alpha=alpha, )