pet-adjusted 0.1.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,115 @@
1
+ """PET with urban-adjusted air temperature and 1.2 m wind speed.
2
+
3
+ PET uses a GPU/array port of ``pythermalcomfort.models.pet_steady`` (Tartarini & Schiavon,
4
+ pythermalcomfort, SoftwareX 2020), which implements Höppe's MEMI with the corrections of
5
+ Walther & Goestchel (2018). See pet.py.
6
+ """
7
+
8
+ from datetime import date
9
+
10
+ import numpy as np
11
+
12
+ from .air_temperature import diurnal_factor, is_daytime, uhi_max, urban_air_temperature
13
+ from .backend import gpu_available
14
+ from .morphology import LC_BUILDING, classify, uhi_inputs, wind_inputs
15
+ from .pet import pet_steady
16
+ from .svf import sky_view_factor
17
+ from .tmrt import read_raster, solweig_tmrt, write_raster
18
+ from .wind import urban_wind_speed, wind_reduction
19
+
20
+ __all__ = [
21
+ "classify", "diurnal_factor", "gpu_available", "is_daytime", "pet_map", "pet_netherlands", "pet_steady", "read_raster",
22
+ "sky_view_factor", "solweig_tmrt", "uhi_inputs", "uhi_max", "urban_air_temperature", "urban_pet", "urban_pet_from_rasters",
23
+ "urban_wind_speed", "wind_inputs", "wind_reduction", "write_raster",
24
+ ]
25
+
26
+
27
+ def urban_pet(
28
+ t_ref, tr, rh, ff10, met, clo, *,
29
+ svf, fveg, s_down, tmax, tmin, u_mean, day: date, hour_utc,
30
+ lambda_b, lambda_t, h,
31
+ device="auto",
32
+ **pet_kwargs,
33
+ ):
34
+ """PET [°C] with Ta from the UHI model and v as the scaled 1.2 m urban wind.
35
+
36
+ t_ref, rh, ff10: reference station air temperature [°C], RH [%], 10-m wind [m/s].
37
+ tr: mean radiant temperature [°C]. met [met], clo [clo] as in pet_steady.
38
+ svf, fveg, s_down, tmax, tmin, u_mean, day, hour_utc: see urban_air_temperature.
39
+ lambda_b, lambda_t, h: see urban_wind_speed.
40
+ device: backend for all steps ("auto", "cpu", "gpu").
41
+ pet_kwargs: passed to pet_steady (p_atm, position, age, sex, weight, height, wme, chunk_size).
42
+ """
43
+ ta = urban_air_temperature(t_ref, svf, fveg, s_down, tmax, tmin, u_mean, day, hour_utc, device)
44
+ v = urban_wind_speed(ff10, lambda_b, lambda_t, h, device)
45
+ return pet_steady(ta, tr, v, rh, met, clo, device=device, **pet_kwargs)
46
+
47
+
48
+ def urban_pet_from_rasters(
49
+ dsm, dem, ndvi, cell_size, *,
50
+ tr, t_ref, rh, ff10, wind_dir, met, clo,
51
+ s_down, tmax, tmin, u_mean, day: date, hour_utc,
52
+ svf=None, svf_radius_m=50.0, landcover=None, device="auto",
53
+ **pet_kwargs,
54
+ ):
55
+ """PET grid [°C] from DSM, DEM and NDVI rasters plus reference-station data for one hour.
56
+
57
+ dsm, dem, ndvi: 2D arrays on the same north-up grid, square cells of ``cell_size`` m.
58
+ tr: mean radiant temperature [°C], scalar or grid. wind_dir: direction the 10-m wind
59
+ comes from [degrees]. svf: sky-view factor grid if already computed (else from the DSM).
60
+ landcover: optional UMEP land-cover grid (see morphology.classify). The rest as in urban_pet. Cells with DSM/DEM nodata (NaN) come out NaN.
61
+ """
62
+ if svf is None:
63
+ svf = sky_view_factor(dsm, cell_size, radius_m=svf_radius_m, device=device)
64
+ svf_avg, fveg = uhi_inputs(dsm, dem, ndvi, cell_size, wind_dir, ff10,
65
+ is_daytime(day, hour_utc), svf=svf, device=device, landcover=landcover)
66
+ lambda_b, lambda_t, h = wind_inputs(dsm, dem, ndvi, cell_size, wind_dir, ff10, device, landcover)
67
+ ta = urban_air_temperature(t_ref, svf_avg, fveg, s_down, tmax, tmin, u_mean, day, hour_utc, device)
68
+ v = urban_wind_speed(ff10, lambda_b, lambda_t, h, device)
69
+ nodata = ~(np.isfinite(dsm) & np.isfinite(dem))
70
+ return pet_steady(np.where(nodata, np.nan, ta), tr, v, rh, met, clo, device=device, **pet_kwargs)
71
+
72
+
73
+ def pet_map(dsm, dem, landcover, ndvi, cell_size, origin, epsg, day: date, weather, subject, *,
74
+ workdir, hours=None, svf_radius_m=50.0, device="auto"):
75
+ """PET and Tmrt maps [°C] for one day, shape (n_hours, rows, cols) each.
76
+
77
+ dsm, dem, landcover, ndvi: 2D arrays on the same north-up grid (NaN = nodata), square
78
+ cells of ``cell_size`` m, upper-left corner ``origin`` (x, y) in EPSG ``epsg``.
79
+ landcover in UMEP codes (1 paved, 2 building, 3 evergreen, 4 deciduous, 5 grass,
80
+ 6 bare soil, 7 water).
81
+ weather: mapping of hourly reference-station series for ``day`` (consecutive UTC hours,
82
+ ideally 0-23): hour_utc, t_ref [°C], rh [%], ff10 [m/s], wind_dir [°], kdown [W m-2] and
83
+ optionally p_atm [hPa]. Optional daily values for Eq. S.3: tmax, tmin [°C], s_down
84
+ [W m-2], u_mean [m/s]; by default they come from the series.
85
+ subject: person for pet_steady: met [met], clo [clo] and optionally age, sex, weight [kg],
86
+ height [m], position.
87
+ hours: UTC hours to return (default all hours in ``weather``).
88
+ Building cells (land cover 2) are NaN in both outputs: PET is for pedestrians.
89
+ Tmrt comes from SOLWEIG-GPU (see solweig_tmrt; inputs and outputs are kept in ``workdir``).
90
+ On Windows, call this under ``if __name__ == "__main__":``.
91
+ """
92
+ w = {k: np.asarray(v, dtype=float) for k, v in weather.items()}
93
+ hour_utc = w["hour_utc"].astype(int)
94
+ p_atm = np.broadcast_to(w.get("p_atm", 1013.25), hour_utc.shape)
95
+ tmrt = solweig_tmrt(dsm, dem, ndvi, cell_size, origin, epsg, day, hour_utc=hour_utc, t_ref=w["t_ref"],
96
+ rh=w["rh"], ff10=w["ff10"], kdown=w["kdown"], p_atm=p_atm, wind_dir=w["wind_dir"],
97
+ landcover=landcover, workdir=workdir)
98
+ # shortcut: daily values from this day's series; the paper uses 08 UTC to 07 UTC next day
99
+ tmax, tmin = w.get("tmax", w["t_ref"].max()), w.get("tmin", w["t_ref"].min())
100
+ s_down, u_mean = w.get("s_down", w["kdown"].mean()), w.get("u_mean", w["ff10"].mean())
101
+ svf = sky_view_factor(dsm, cell_size, radius_m=svf_radius_m, device=device)
102
+ idx = list(range(hour_utc.size)) if hours is None else [list(hour_utc).index(h) for h in hours]
103
+ pet = np.stack([
104
+ urban_pet_from_rasters(
105
+ dsm, dem, ndvi, cell_size, tr=tmrt[i], t_ref=w["t_ref"][i], rh=w["rh"][i], ff10=w["ff10"][i],
106
+ wind_dir=w["wind_dir"][i], s_down=s_down, tmax=tmax, tmin=tmin, u_mean=u_mean, day=day,
107
+ hour_utc=hour_utc[i], svf=svf, landcover=landcover, device=device, p_atm=p_atm[i], **subject,
108
+ )
109
+ for i in idx
110
+ ])
111
+ roof = np.asarray(landcover) == LC_BUILDING
112
+ return np.where(roof, np.nan, pet), np.where(roof, np.nan, tmrt[idx])
113
+
114
+
115
+ from .netherlands import pet_netherlands # noqa: E402 (uses pet_map above)
@@ -0,0 +1,75 @@
1
+ """Urban air temperature from a rural reference station (Koopmans et al. 2020, Building and Environment 181, 106984, supplement B2)."""
2
+
3
+ from datetime import date
4
+
5
+ import numpy as np
6
+
7
+ from .backend import get_xp, to_numpy
8
+
9
+ RHO_CP = 1.2 * 1005.0 # air density * heat capacity [J m-3 K-1]; W m-2 -> kinematic K m s-1
10
+
11
+ # Table S.3: start date (month, day) of each period -> sunrise/sunset column (UTC).
12
+ # The paper lists "1 April t/m 19 April" and "13 April t/m 20 April"; read as 1-12 April.
13
+ _PERIODS = [
14
+ ((4, 1), "5/18"), ((4, 13), "5/19"), ((4, 20), "4/19"), ((5, 20), "4/20"),
15
+ ((5, 26), "3/20"), ((7, 11), "4/20"), ((7, 31), "4/19"), ((8, 22), "5/19"),
16
+ ((8, 31), "5/18"), ((9, 25), "5/17"), ((9, 28), "6/17"),
17
+ ]
18
+
19
+ # Table S.4: UHI factor per UTC hour 0..23 for each sunrise/sunset column.
20
+ _UHI_FACTORS = {
21
+ "6/17": [0.748, 0.667, 0.602, 0.525, 0.449, 0.281, 0.127, 0.063, 0.019, -0.015, -0.020, 0.000,
22
+ 0.030, 0.065, 0.117, 0.205, 0.335, 0.532, 0.747, 0.906, 0.975, 1.000, 0.931, 0.849],
23
+ "5/17": [0.728, 0.640, 0.573, 0.490, 0.355, 0.150, 0.078, 0.025, -0.013, -0.020, -0.001, 0.025,
24
+ 0.056, 0.090, 0.165, 0.270, 0.413, 0.600, 0.803, 0.920, 0.978, 1.000, 0.925, 0.830],
25
+ "5/18": [0.807, 0.704, 0.617, 0.533, 0.435, 0.227, 0.095, 0.032, -0.009, -0.020, -0.003, 0.020,
26
+ 0.048, 0.080, 0.136, 0.215, 0.325, 0.485, 0.662, 0.849, 0.932, 0.979, 1.000, 0.918],
27
+ "5/19": [0.910, 0.780, 0.675, 0.590, 0.490, 0.320, 0.120, 0.040, -0.005, -0.020, -0.004, 0.016,
28
+ 0.042, 0.071, 0.111, 0.176, 0.270, 0.386, 0.546, 0.716, 0.877, 0.941, 0.981, 1.000],
29
+ "4/19": [0.900, 0.757, 0.710, 0.543, 0.413, 0.150, 0.057, 0.000, -0.020, -0.005, 0.013, 0.037,
30
+ 0.063, 0.090, 0.150, 0.222, 0.318, 0.450, 0.600, 0.762, 0.890, 0.950, 0.982, 1.000],
31
+ "4/20": [1.000, 0.888, 0.728, 0.609, 0.490, 0.256, 0.079, 0.007, -0.020, -0.006, 0.010, 0.033,
32
+ 0.056, 0.082, 0.128, 0.184, 0.270, 0.366, 0.506, 0.651, 0.803, 0.901, 0.958, 0.983],
33
+ "3/20": [1.000, 0.866, 0.690, 0.560, 0.380, 0.107, 0.015, -0.020, -0.007, 0.007, 0.029, 0.050,
34
+ 0.074, 0.108, 0.161, 0.228, 0.312, 0.424, 0.556, 0.695, 0.838, 0.911, 0.964, 0.984],
35
+ }
36
+
37
+
38
+ def _column(day: date):
39
+ if not 4 <= day.month <= 9:
40
+ raise ValueError("Table S.3 only covers 1 April - 30 September")
41
+ return [c for start, c in _PERIODS if (day.month, day.day) >= start][-1]
42
+
43
+
44
+ def diurnal_factor(day: date, hour_utc):
45
+ """UHI correction factor (Tables S.3/S.4) for a date and UTC hour(s) 0-23."""
46
+ return np.asarray(_UHI_FACTORS[_column(day)])[np.asarray(hour_utc)]
47
+
48
+
49
+ def is_daytime(day: date, hour_utc) -> bool:
50
+ """Between sunrise and sunset of Table S.3 (UTC)."""
51
+ sunrise, sunset = map(int, _column(day).split("/"))
52
+ return sunrise <= hour_utc < sunset
53
+
54
+
55
+ def _uhi_max(xp, svf, fveg, s_down, tmax, tmin, u):
56
+ svf, fveg, s_down, tmax, tmin, u = (xp.asarray(a, dtype=float) for a in (svf, fveg, s_down, tmax, tmin, u))
57
+ return (2 - svf - fveg) * (s_down / RHO_CP * (tmax - tmin) ** 3 / u) ** 0.25
58
+
59
+
60
+ def uhi_max(svf, fveg, s_down, tmax, tmin, u, device="auto"):
61
+ """Daily maximum UHI [K], Eq. S.3.
62
+
63
+ svf, fveg: source-area averaged sky-view factor and vegetation fraction [-].
64
+ s_down: mean daily downward shortwave radiation at the reference station [W m-2].
65
+ tmax, tmin: max/min reference temperature (08 UTC - 07 UTC next day) [°C or K].
66
+ u: mean reference wind speed over the same period [m s-1].
67
+ """
68
+ return to_numpy(_uhi_max(get_xp(device), svf, fveg, s_down, tmax, tmin, u))
69
+
70
+
71
+ def urban_air_temperature(t_ref, svf, fveg, s_down, tmax, tmin, u, day: date, hour_utc, device="auto"):
72
+ """Urban air temperature [°C], Eq. S.4: T_ref + UHImax * diurnal_cycle[h]."""
73
+ xp = get_xp(device)
74
+ factor = xp.asarray(diurnal_factor(day, hour_utc))
75
+ return to_numpy(xp.asarray(t_ref, dtype=float) + _uhi_max(xp, svf, fveg, s_down, tmax, tmin, u) * factor)
@@ -0,0 +1,29 @@
1
+ """Array backend: CuPy on a CUDA GPU when available, otherwise NumPy."""
2
+
3
+ import numpy as np
4
+
5
+ try:
6
+ import cupy as cp
7
+
8
+ _GPU = cp.cuda.runtime.getDeviceCount() > 0
9
+ except Exception: # CuPy not installed, or no CUDA driver/device
10
+ cp, _GPU = None, False
11
+
12
+
13
+ def gpu_available() -> bool:
14
+ return _GPU
15
+
16
+
17
+ def get_xp(device="auto"):
18
+ """Array module for ``device``: "auto" (GPU if available), "cpu" or "gpu"."""
19
+ if device not in ("auto", "cpu", "gpu"):
20
+ raise ValueError(f"device must be 'auto', 'cpu' or 'gpu', got {device!r}")
21
+ if device == "cpu" or (device == "auto" and not _GPU):
22
+ return np
23
+ if not _GPU:
24
+ raise RuntimeError("device='gpu' requested but CuPy/CUDA is not available")
25
+ return cp
26
+
27
+
28
+ def to_numpy(a):
29
+ return cp.asnumpy(a) if cp is not None and isinstance(a, cp.ndarray) else np.asarray(a)
@@ -0,0 +1,152 @@
1
+ """UHI and wind inputs derived from DSM, DEM and NDVI rasters.
2
+
3
+ All rasters are 2D arrays on the same grid with square cells of ``cell_size`` metres and
4
+ north up (row 0 = north). Source areas follow Fig. S.2 (air temperature) and Fig. S.3
5
+ (wind): rectangles oriented to the wind, or squares around the cell when ff10 < 1.5 m/s.
6
+ Source-area sums are FFT convolutions, so every cell gets its own source area (the paper
7
+ computes on a 35 m grid and interpolates, only to save time).
8
+ """
9
+
10
+ import numpy as np
11
+ from scipy import ndimage, signal
12
+
13
+ from .backend import get_xp, to_numpy
14
+ from .svf import sky_view_factor
15
+
16
+ NDVI_VEG = 0.16 # vegetation threshold (B1, B2)
17
+ MIN_OBSTACLE = 2.0 # buildings and trees lower than this are ignored [m]
18
+ LOW_WIND = 1.5 # [m/s] below this the source areas are squares
19
+ TILE_M = 25.0 # intermediate Svf/Fveg grid for the air temperature (B2)
20
+
21
+
22
+ # UMEP / SOLWEIG land-cover codes
23
+ LC_PAVED, LC_BUILDING, LC_EVERGREEN, LC_DECIDUOUS, LC_GRASS, LC_BARE, LC_WATER = range(1, 8)
24
+
25
+
26
+ def classify(dsm, dem, ndvi, landcover=None):
27
+ """Object height [m] and masks (building, tree, vegetation, water).
28
+
29
+ landcover: optional grid in UMEP codes (1 paved, 2 building, 3 evergreen, 4 deciduous,
30
+ 5 grass, 6 bare soil, 7 water). With it, buildings and water come from the land cover;
31
+ without it, buildings are tall non-green cells and water is NDVI < 0.
32
+ """
33
+ height = np.asarray(dsm, dtype=float) - np.asarray(dem, dtype=float)
34
+ ndvi = np.asarray(ndvi, dtype=float)
35
+ veg = ndvi > NDVI_VEG
36
+ tall = height > MIN_OBSTACLE
37
+ if landcover is None:
38
+ water = ndvi < 0
39
+ building = tall & ~veg
40
+ else:
41
+ landcover = np.asarray(landcover)
42
+ water = landcover == LC_WATER
43
+ building = tall & (landcover == LC_BUILDING)
44
+ tree = tall & veg & ~building
45
+ return height, building, tree, veg, water
46
+
47
+
48
+ def source_kernel(cell, wind_dir, upwind, downwind, half_width):
49
+ """1/0 mask of a source area around the centre cell.
50
+
51
+ wind_dir: direction the wind comes from [degrees from north]. The area reaches
52
+ ``upwind`` metres into the wind, ``downwind`` metres behind and ``half_width`` aside.
53
+ """
54
+ r = int(np.ceil(np.hypot(max(upwind, downwind), half_width) / cell))
55
+ rows, cols = np.mgrid[-r:r + 1, -r:r + 1] * cell
56
+ east, north = cols, -rows
57
+ th = np.radians(wind_dir)
58
+ along = east * np.sin(th) + north * np.cos(th) # > 0 towards where the wind comes from
59
+ across = east * np.cos(th) - north * np.sin(th)
60
+ return ((along <= upwind) & (along >= -downwind) & (np.abs(across) <= half_width)).astype(float)
61
+
62
+
63
+ def _area_sum(xp, field, kernel):
64
+ """Sum of ``field`` over the source area of every cell (zero outside the grid)."""
65
+ if xp is np:
66
+ conv = signal.fftconvolve
67
+ else:
68
+ import cupyx.scipy.signal
69
+
70
+ conv = cupyx.scipy.signal.fftconvolve
71
+ out = conv(xp.asarray(field, dtype=xp.float64), xp.asarray(kernel[::-1, ::-1]), mode="same")
72
+ return xp.maximum(out, 0.0) # FFT round-off can give tiny negatives
73
+
74
+
75
+ def uhi_inputs(dsm, dem, ndvi, cell_size, wind_dir, ff10, daytime, svf=None,
76
+ svf_radius_m=50.0, device="auto", landcover=None):
77
+ """Source-area averaged sky-view factor and vegetation fraction for Eq. S.3.
78
+
79
+ Svf excludes buildings, water and vegetation; water counts as vegetation by day and as
80
+ built-up at night. Per 25 m tile, Svf is nodata when more than 90% of it is excluded.
81
+ Over the source area: Svf = 1 when 10% or less of it has data, the plain mean above 20%,
82
+ and a linear blend in between. ``svf`` can be passed if already computed from the DSM.
83
+ landcover: optional UMEP land-cover grid (see classify).
84
+ Returns (svf, fveg) on the input grid.
85
+ """
86
+ height, building, tree, veg, water = classify(dsm, dem, ndvi, landcover)
87
+ if svf is None:
88
+ svf = sky_view_factor(dsm, cell_size, radius_m=svf_radius_m, device=device)
89
+ svf = np.where(building | water | veg, np.nan, svf)
90
+ green = (veg | water) if daytime else (veg & ~water)
91
+
92
+ # 25 m tiles
93
+ t = max(int(round(TILE_M / cell_size)), 1)
94
+ n_rows, n_cols = svf.shape
95
+ pr, pc = -n_rows % t, -n_cols % t
96
+ svf_t = np.pad(svf, ((0, pr), (0, pc)), constant_values=np.nan).reshape(
97
+ (n_rows + pr) // t, t, (n_cols + pc) // t, t)
98
+ inside = np.pad(np.ones_like(svf), ((0, pr), (0, pc))).reshape(svf_t.shape)
99
+ green_t = np.pad(green.astype(float), ((0, pr), (0, pc))).reshape(svf_t.shape)
100
+ n_valid = np.isfinite(svf_t).sum(axis=(1, 3))
101
+ n_inside = inside.sum(axis=(1, 3))
102
+ tile_svf = np.nansum(svf_t, axis=(1, 3)) / np.maximum(n_valid, 1)
103
+ tile_ok = n_valid > 0.1 * n_inside
104
+ tile_fveg = green_t.sum(axis=(1, 3)) / n_inside
105
+
106
+ tile = t * cell_size
107
+ if ff10 < LOW_WIND:
108
+ kernel = source_kernel(tile, 0.0, 350.0, 350.0, 350.0)
109
+ else:
110
+ kernel = source_kernel(tile, wind_dir, 1000.0, 100.0, 250.0)
111
+ # 25 m grids are small: NumPy is enough here
112
+ total = _area_sum(np, np.ones_like(tile_svf), kernel)
113
+ n_ok = _area_sum(np, tile_ok, kernel)
114
+ mean = _area_sum(np, np.where(tile_ok, tile_svf, 0.0), kernel) / np.maximum(n_ok, 1e-9)
115
+ frac = n_ok / total
116
+ svf_avg = np.where(frac <= 0.1, 1.0, np.where(frac > 0.2, mean, 1 + (mean - 1) * (frac - 0.1) / 0.1))
117
+ fveg_avg = _area_sum(np, tile_fveg, kernel) / total
118
+
119
+ def to_grid(a): # back to the input grid, bilinear
120
+ return ndimage.zoom(a, t, order=1)[:n_rows, :n_cols]
121
+
122
+ return to_grid(svf_avg), to_grid(fveg_avg)
123
+
124
+
125
+ def wind_inputs(dsm, dem, ndvi, cell_size, wind_dir, ff10, device="auto", landcover=None):
126
+ """Frontal area densities of buildings and trees and mean obstacle height (steps 3-8).
127
+
128
+ Frontal area of a cell = rise in obstacle height from its upwind neighbour x cell width.
129
+ landcover: optional UMEP land-cover grid (see classify).
130
+ Returns (lambda_b, lambda_t, h) on the input grid.
131
+ """
132
+ xp = get_xp(device)
133
+ height, building, tree, _, _ = classify(dsm, dem, ndvi, landcover)
134
+ th = np.radians(wind_dir)
135
+ upwind_shift = (np.cos(th), -np.sin(th)) # out[x] = in[x + one cell upwind]
136
+
137
+ def frontal(mask):
138
+ h = np.where(mask, height, 0.0)
139
+ # shortcut: frontal width is one cell for any direction; exact only for N/E/S/W winds
140
+ return np.maximum(h - ndimage.shift(h, upwind_shift, order=1, cval=0.0), 0.0) * cell_size
141
+
142
+ if ff10 < LOW_WIND:
143
+ kernel = source_kernel(cell_size, 0.0, 87.5, 87.5, 87.5)
144
+ else:
145
+ kernel = source_kernel(cell_size, wind_dir, 210.0, 70.0, 70.0)
146
+ area = _area_sum(xp, np.ones_like(height), kernel) * cell_size**2
147
+ obstacle = building | tree
148
+ lambda_b = _area_sum(xp, frontal(building), kernel) / area
149
+ lambda_t = _area_sum(xp, frontal(tree), kernel) / area
150
+ h_sum = _area_sum(xp, np.where(obstacle, height, 0.0), kernel)
151
+ h_mean = h_sum / xp.maximum(_area_sum(xp, obstacle, kernel), 1e-9)
152
+ return to_numpy(lambda_b), to_numpy(lambda_t), to_numpy(h_mean)