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()