Ground-to-sky viewfactor#

This post shows two things:

  • How to calculate, in an infinite sheds model, the view factor to the sky from a point underneath the array

  • How to create, using matplotlib, an HTML animation that works with sphinx and nbviewer.org

import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from IPython.display import HTML
import numpy as np
from pvlib.tools import cosd, sind, tand
def calc_vf(x, rotation, collector_width, pitch, height, N=50):
    """
    Calculate the fraction of the sky dome visible from a point on the ground,
    accounting for partial obstruction by infinite sheds.

    Parameters
    ----------
    x : float
        Position between rows (along the horizontal axis perpendicular to the sheds) [m]
    rotation : float
        Rotation (positive or negative surface_tilt) of the sheds [degrees]
    collector_width : float
        Slant length of sheds [m]
    pitch : float
        On-center row spacing [m]
    height : float
        Height of center of modules [m]
    N : int, default 50
        Number of sheds to consider in the forwards and backwards directions.
        50 is way overkill for a reasonable array.

    Returns
    -------
    vf : float
        Fraction of sky dome visible from the specified ground point [unitless]
    wedge_angles : list of (float, float)
        Bounding angles of each wedge of visible sky [degrees]
    """
    all_k = np.arange(-N, N)
    phi_k_1 = np.degrees(np.arctan2(height - collector_width * sind(rotation) / 2, all_k*pitch + collector_width * cosd(rotation)/2 - x))
    phi_k_2 = np.degrees(np.arctan2(height + collector_width * sind(rotation) / 2, all_k*pitch - collector_width * cosd(rotation)/2 - x))
    phi_k_max = np.maximum(phi_k_1, phi_k_2)
    phi_k_min = np.minimum(phi_k_1, phi_k_2)
    wedge_vfs = 0.5*(cosd(phi_k_max[1:]) - cosd(phi_k_min[:-1]))
    keep = wedge_vfs > 0
    st = phi_k_max[1:][keep]
    ed = phi_k_min[:-1][keep]
    vf = np.sum(np.where(wedge_vfs>0, wedge_vfs, 0))
    return vf, list(zip(st, ed))
def calc_phi(x, xm, zm):
    return np.degrees(np.arctan2(zm, xm-x))

def plot_wedge(x, phi1, phi2, **kwargs):
    dx = 100
    sign1 = 1 if abs(phi1) < 90 else -1
    sign2 = 1 if abs(phi2) < 90 else -1
    t1 = np.array([x, 0]) + sign1 * np.array([dx, tand(phi1) * dx])
    t2 = np.array([x, 0]) + sign2 * np.array([dx, tand(phi2) * dx])
    plt.fill([x, t1[0], t2[0]], [0, t1[1], t2[1]], **kwargs)
    
def plot_scene(x, rotation, collector_width, pitch, height, fig):
    plt.gca().clear()
    # draw modules and sky wedges
    for k in k_range:
        # plot modules:
        delta = collector_width * np.array([cosd(rotation), -sind(rotation)])
        pos_center = np.array([k * pitch, height])
        pos_left = pos_center - delta/2
        pos_right = pos_left + delta
        plt.plot([pos_left[0], pos_right[0]], [pos_left[1], pos_right[1]], c='k', marker='o', markerfacecolor='r', ms=3)

        # plot shaded sky wedges:
        phi_left = calc_phi(x, pos_left[0], pos_left[1])
        phi_right = calc_phi(x, pos_right[0], pos_right[1])
        plot_wedge(x, phi_left, phi_right, c='grey', alpha=0.5)

        # plot module ID (k):
        plt.text(pos_left[0], -1, f'k={k}'.replace('-', '−'))

    # plot unshaded wedges:
    _, wedge_angles = calc_vf(x, rotation, collector_width, pitch, height)
    for phi1, phi2 in wedge_angles:
        plot_wedge(x, phi1, phi2, c='yellow', alpha=0.5, ls='')

    # plot ground:
    plt.plot([k_range[0]*pitch, k_range[-1]*pitch], [0, 0], c='k')
    plt.scatter([x], [0], marker='x', c='r')

    # display plot:
    plt.xlim((k_range[0]-1)*pitch, (k_range[-1]+1)*pitch)
    plt.ylim(-height/4, 1.2*height)
    plt.gca().set_aspect('equal', adjustable='datalim')
    plt.gca().set_title(f'Number of visible sky wedges: {len(wedge_angles)}')
pitch = 2.5
collector_width = 1.0
height = 1.5  # height of middle of module above ground
rotation = 40
k_range = list(range(-4, 5))

fig, _ = plt.subplots(figsize=(10, 3), dpi=200)
_ = plot_scene(1.9, rotation, collector_width, pitch, height, fig)
../_images/dbd358264b913bf19442ca1c96acc7ecfbce066a9ac29a27e2542c85c2260ca4.png
fig, _ = plt.subplots(figsize=(6, 2), dpi=150)

def animate(i):
    all_x = np.linspace(-pitch, 2*pitch, 200)
    this_x = all_x[i]
    _ = plot_scene(this_x, rotation, collector_width, pitch, height, fig)

ani = FuncAnimation(fig, animate, frames=200, interval=int(1000/20), repeat=True, blit=False)
HTML(ani.to_html5_video())
../_images/253930f99cb9178b73b333e1a81e35bd9b13e5d1dd19d78d61299d7aabd12344.png