Skip to content

Commit

Permalink
Working demo including extracting the timeseries for positions within…
Browse files Browse the repository at this point in the history
… 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.
  • Loading branch information
jranalli committed Oct 9, 2024
1 parent 1c4d7bf commit 8b23133
Showing 1 changed file with 101 additions and 28 deletions.
129 changes: 101 additions & 28 deletions src/solarspatialtools/synthirrad/cloudfield.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand All @@ -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()

Expand Down Expand Up @@ -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


Expand All @@ -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()

0 comments on commit 8b23133

Please sign in to comment.