From 8b23133141c8ff90c4d1588ce2d3aa5792bbbf4a Mon Sep 17 00:00:00 2001 From: Joe Ranalli Date: Tue, 8 Oct 2024 21:30:01 -0400 Subject: [PATCH] Working demo including extracting the timeseries for positions within the field. No doublecheck on the realism yet. At a glance it looks like our spatial variance is very small, because we assume a 1 second spatiotemporal resolution. That means our crosswind distances for large cloud velocities are actually very large, which is to say that many along-wind spaced points get the exact same time series with only temporal shifting. That could be good or bad depending on your point of view. Maybe "it is what it is" for the Lave scaling case, but this could be a contribution that I make to the methodology. --- .../synthirrad/cloudfield.py | 129 ++++++++++++++---- 1 file changed, 101 insertions(+), 28 deletions(-) diff --git a/src/solarspatialtools/synthirrad/cloudfield.py b/src/solarspatialtools/synthirrad/cloudfield.py index a2d6ca7..18b7cd4 100644 --- a/src/solarspatialtools/synthirrad/cloudfield.py +++ b/src/solarspatialtools/synthirrad/cloudfield.py @@ -139,13 +139,14 @@ def _stack_random_field(weights, scales, size, normalize=False, plot=False): if len(weights) != len(scales): raise ValueError("Number of weights must match scales.") - # Calculate a field for each scale, and add them together in a weighted manner to form the field field = np.zeros(size, dtype=float) for scale, weight in zip(scales, weights): prop = 2.0**(-scale+1) # proportion for this scale - _, i_field = _random_at_scale((int(size[0]*prop), int(size[1]*prop)), size) + xsz = np.max([int(size[0]*prop),2]) # min of 2 so that we never go past the limit on interp + ysz = np.max([int(size[1]*prop),2]) + _, i_field = _random_at_scale((xsz, ysz), size) field += i_field * weight # Optionally Scale it zero to 1 @@ -284,8 +285,6 @@ def _scale_field_lave(field, clear_mask, edge_mask, ktmean, ktmax=1.4, kt1pct=0. # Calc the "max" and "min", excluding clear values field_max = np.quantile(field[clear_mask == 0], max_quant) - print(f"Field Max: {field_max}") - print(f"kt1pct: {kt1pct}") # Create a flipped version of the distribution that scales between slightly below kt1pct and bascially (1-field_min) # I think the intent here would be to make it vary between kt1pct and 1, but that's not quite what it does. @@ -321,8 +320,6 @@ def _scale_field_lave(field, clear_mask, edge_mask, ktmean, ktmax=1.4, kt1pct=0. # Edges then clear means that the clearsky overrides the edge enhancement clouds5[edge_mask > 0] = ce[edge_mask > 0] clouds5[clear_mask > 0] = 1 - print(f"Desired Mean: {ktmean}, actual global mean {np.mean(clouds5)}.") - if plot: plt.hist(ce.flatten(), bins=100) @@ -334,9 +331,9 @@ def _scale_field_lave(field, clear_mask, edge_mask, ktmean, ktmax=1.4, kt1pct=0. "Original Field Distribution"]) fig, axs = plt.subplots(1, 2, figsize=(10, 5)) - axs[0].imshow(field, extent=(0, ysiz, 0, xsiz)) + axs[0].imshow(field, extent=(0, field.shape[0], 0, field.shape[1])) axs[0].set_title('Original Field') - axs[1].imshow(clouds5, extent=(0, ysiz, 0, xsiz)) + axs[1].imshow(clouds5, extent=(0, field.shape[0], 0, field.shape[1])) axs[1].set_title('Shifted Field') plt.show() @@ -438,13 +435,38 @@ def space_to_time(pixres=1, cloud_speed=50): def cloudfield_timeseries(weights, scales, size, frac_clear, ktmean, ktmax, kt1pct): + """ + Generate a time series of cloud fields based on the properties of a time series of kt values. + + Parameters + ---------- + weights : np.ndarray + The wavelet weights at each scale + scales : list + The scales of the wavelets, should be integer values interpreted as 2**(scale-1) seconds + size : tuple + The size of the field to generate, x by y + frac_clear : float + The fraction of clear sky + ktmean : float + The mean of the kt values + ktmax : float + The maximum of the kt values + kt1pct : float + The 1st percentile of the kt values + + Returns + ------- + field_final : np.ndarray + The final field of simulated clouds + """ cfield = _stack_random_field(weights, scales, size) clear_mask = _stack_random_field(weights, scales, size) clear_mask = _calc_clear_mask(clear_mask, frac_clear) # 0 is cloudy, 1 is clear edges, smoothed = _find_edges(clear_mask, 3) - field_final = _scale_field_lave(cfield, clear_mask, smoothed, ktmean, ktmax, kt1pct) + field_final = _scale_field_lave(cfield, clear_mask, smoothed, ktmean, ktmax, kt1pct, plot=True) return field_final @@ -467,52 +489,103 @@ def cloudfield_timeseries(weights, scales, size, frac_clear, ktmean, ktmax, kt1p # plt.plot(data) # plt.show() + # Get the time series for a single sensor and convert it to a clear sky index. + # Record some statistics about it. pos = pd.read_hdf(datafn, mode="r", key="latlon") loc = pvlib.location.Location(np.mean(pos['lat']), np.mean(pos['lon'])) cs_ghi = loc.get_clearsky(data_i.index, model='simplified_solis')['ghi'] cs_ghi = 1000/max(cs_ghi) * cs_ghi # Rescale (possible scaling on kt = pvlib.irradiance.clearsky_index(data_i, cs_ghi, 2) - pos_utm = pd.read_hdf(datafn, mode="r", key="utm") - e_extent = np.abs(np.max(pos_utm['E'])-np.min(pos_utm['E'])) - n_extent = np.abs(np.max(pos_utm['N'])-np.min(pos_utm['N'])) - t_extent = (np.max(twin)-np.min(twin)).total_seconds() - dt = (twin[1] - twin[0]).total_seconds() + ktmean, kt1pct, ktmax, frac_clear, vs, weights, scales = get_timeseries_stats( + kt, plot=False) + # Get the Cloud Motion Vector for the Timeseries + pos_utm = pd.read_hdf(datafn, mode="r", key="utm") kt_all = irradiance.clearsky_index(data, cs_ghi, 2) cld_spd, cld_dir, _ = cmv.compute_cmv(kt_all, pos_utm, reference_id=None, method='jamaly') - cld_en = spatial.pol2rect(cld_spd, cld_dir) + cld_vec_rect = spatial.pol2rect(cld_spd, cld_dir) print(f"Cld Speed {cld_spd:8.2f}, Cld Dir {np.rad2deg(cld_dir):8.2f}") - spatial_time_x = n_extent/cld_spd - spatial_time_y = e_extent/cld_spd + # Rotate the sensor positions by -cld dir to position the incoming clouds + # toward the upwind side of the plant. Shift to zero out the minimum value. + rot = spatial.rotate_vector((pos_utm['E'], pos_utm['N']), theta=-cld_dir) + pos_utm_rot = pd.DataFrame({'X': rot[0] - np.min(rot[0]), + 'Y': rot[1] - np.min(rot[1])}, + index=pos_utm.index) + + # # plot the original field and the rotated field side by side in two subplots + # fig, axs = plt.subplots(1, 2, figsize=(10, 5)) + # axs[0].scatter(pos_utm['E'], pos_utm['N']) + # axs[0].set_title('Original Field') + # axs[0].quiver(pos_utm['E'][40], pos_utm['N'][40], 200 * cld_vec_rect[0], 200 * cld_vec_rect[1], scale=10, scale_units='xy') + # axs[0].set_xlabel('East') + # axs[0].set_ylabel('North') + # axs[1].scatter(pos_utm_rot['X'], pos_utm_rot['Y']) + # axs[1].quiver(pos_utm_rot['X'][40], pos_utm_rot['Y'][40], 200*cld_spd, 0, scale=10, scale_units='xy') + # axs[1].set_title('Rotated Field') + # axs[0].set_xlabel('CMV Direction') + # axs[0].set_ylabel('CMV Dir + 90 deg') + # axs[0].set_aspect('equal') + # axs[1].set_aspect('equal') + # plt.show() - # This now represents the time to space relationship in seconds, so each pixel represents a 1 second step. - # Our steps in X represent 1 second forward or backward in EITHER space or time - # Our steps in Y represent 1 "cloud second" left or right perpendicular to the motion axis - xt_size = int(np.ceil(spatial_time_x + t_extent)) - yt_size = int(np.ceil(spatial_time_y)) + # #### Generate the Simulated Cloud Field + # Calculate the size of the field + x_extent = np.abs(np.max(pos_utm_rot['X']) - np.min(pos_utm_rot['X'])) + y_extent = np.abs(np.max(pos_utm_rot['Y']) - np.min(pos_utm_rot['Y'])) + t_extent = (np.max(twin) - np.min(twin)).total_seconds() + dt = (twin[1] - twin[0]).total_seconds() - # #### Get Statistics + # Convert space to time + spatial_time_x = x_extent / cld_spd + spatial_time_y = y_extent / cld_spd - ktmean, kt1pct, ktmax, frac_clear, vs, weights, scales = get_timeseries_stats(kt, plot=False) + # This now represents the time to space relationship in seconds, so each pixel of the field represents a 1 second step. + # Our steps in X represent 1 second forward or backward in EITHER along-cloud space or time + # Our steps in Y represent 1 "cloud second" left or right perpendicular to the motion axis + # We actually have to oversize things a bit because if the field is too small, we can't + # halve its size a sufficient number of times. + # TODO rethink this one on the large scales side, are we interpolating for no reason? + xt_size = np.max([int(np.ceil(spatial_time_x + t_extent)), 2**len(scales)]) + # yt_size = np.max([int(np.ceil(spatial_time_y)), 2**len(scales)]) + # xt_size = int(np.ceil(spatial_time_x + t_extent)) + yt_size = int(np.ceil(spatial_time_y)) - # get Field - field_final = cloudfield_timeseries(weights, scales, (xsiz, ysiz), frac_clear, ktmean, ktmax, kt1pct) + # Calculate the randomized field + field_final = cloudfield_timeseries(weights, scales, (xt_size, yt_size), frac_clear, ktmean, ktmax, kt1pct) # Plot a timeseries plt.plot(field_final[1,:]) plt.show() + + # Convert space to time to extract time series + xpos = pos_utm_rot['X'] - np.min(pos_utm_rot['X']) + ypos = pos_utm_rot['Y'] - np.min(pos_utm_rot['Y']) + xpos_temporal = xpos / cld_spd + ypos_temporal = ypos / cld_spd + + sim_kt = pd.DataFrame(index=twin, columns=pos_utm_rot.index) + for sensor in pos_utm_rot.index: + x = int(xpos_temporal[sensor]) + y = int(ypos_temporal[sensor]) + sim_kt[sensor] = field_final[x:x+int(t_extent)+1, y] + + plt.plot(sim_kt[[40,42]]) + plt.show() + + + # Compare Hist of CS Index plt.hist(kt, bins=50) - plt.hist(field_final[1,:], bins=50, alpha=0.5) + plt.hist(field_final[:,1], bins=50, alpha=0.5) # Ramp Rate plt.figure() plt.hist(np.diff(kt), bins=50) - plt.hist(np.diff(field_final[1,:]), bins=200, alpha=0.5) + plt.hist(np.diff(field_final[:,1]), bins=200, alpha=0.5) plt.show()