10000 Calculating horizon profiles and associated shading losses by JPalakapillyKWH · Pull Request #758 · pvlib/pvlib-python · GitHub
[go: up one dir, main page]

Skip to content

Calculating horizon profiles and associated shading losses #758

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 29 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
691d353
first pass at horizon code in pvlib
JPalakapillyKWH Jul 19, 2019
99426ba
added most of horizon shading code
JPalakapillyKWH Jul 23, 2019
39302d6
wrapped up most code and added docstrings to irradiance
JPalakapillyKWH Jul 25, 2019
2daa5c6
added more documentation
JPalakapillyKWH Jul 29, 2019
6cb7399
reverted setup
JPalakapillyKWH Jul 29, 2019
df724e5
linted
JPalakapillyKWH Jul 29, 2019
808af03
more lints + moved import of gmaps
JPalakapillyKWH Jul 29, 2019
7573763
added scipy to setup.py and fixed bug in modelchain
JPalakapillyKWH Jul 31, 2019
3421899
moved some horizon functions to tools and fixed naming. Also wrote 2 …
JPalakapillyKWH Aug 1, 2019
b600e16
added test_horizon.py and code restructuring
JPalakapillyKWH Aug 2, 2019
b0c9193
moved horizon adjustmen to isotropic. Fixed some tests
JPalakapillyKWH Aug 2, 2019
e6beb32
removed gmaps from horizon. Deleted remnants of horizon adjusment model
JPalakapillyKWH Aug 2, 2019
10baccd
major code restructuring. much more numpy friendly now. still need to…
JPalakapillyKWH Aug 6, 2019
345eaa0
updated docstrings
JPalakapillyKWH Aug 6, 2019
5405d75
docstring changes
JPalakapillyKWH Aug 7, 2019
cc481c7
made some changes to modelchain and location due to restructuring of …
JPalakapillyKWH Aug 7, 2019
15e59eb
added one more test to modelchain
JPalakapillyKWH Aug 7, 2019
33c0bb8
threw code and some docs into horizon.rst
JPalakapillyKWH Aug 7, 2019
18ccd1a
reverted irradiance, location and modelchain (and tests) to master
JPalakapillyKWH Aug 7, 2019
d1119ab
changed dip angles to elevation angles
JPalakapillyKWH Aug 7, 2019
57c2035
removed horizon.rst for now
JPalakapillyKWH Aug 7, 2019
00017e2
added DNI correction to horizon.py
JPalakapillyKWH Aug 8, 2019
a87b797
added a test case to get 100% of diff hit
JPalakapillyKWH Aug 8, 2019
aeb5f95
minor test fix
JPalakapillyKWH Aug 8, 2019
b022f9e
docstring changes and improvement to filter_points
JPalakapillyKWH Aug 9, 2019
d07e66f
added tests for functions added in tools.py. Some docstring changes a…
JPalakapillyKWH Aug 12, 2019
2411520
minor improvements and docstring changes
JPalakapillyKWH Aug 13, 2019
210fd1c
docstring changes
JPalakapillyKWH Aug 13, 2019
a630acc
reference update
JPalakapillyKWH Aug 13, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
changed dip angles to elevation angles
  • Loading branch information
JPalakapillyKWH committed Aug 7, 2019
commit d1119abbcfd073a981abd9179269af9614ebb08f
118 changes: 61 additions & 57 deletions pvlib/horizon.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,33 +52,36 @@ def grid_lat_lon(lat, lon, grid_radius=200, grid_step=.001):
return lat_grid, lon_grid


def dip_calc(pt1, pt2):
def elevation_angle_calc(pt1, pt2):
'''
Calculates the dip angle from pt1 to pt2 where dip angle is defined as
the angle that the line connecting pt1 to pt2 makes with the plane normal
to the Earth's surface at pt2. Also computes the azimuth defined as degrees
East of North the bearing of pt2 from pt1. This uses the Haversine formula.
Calculates the elevation angle from pt1 to pt2 where elevation angle is
defined as the angle between the line connecting pt1 to pt2 and the plane
normal to the Earth's surface at pt1. A point that appears above the
horizontal has a positive elevation angle. Also computes the azimuth
defined as degrees East of North the bearing of pt2 from pt1.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"of the bearing from pt1 to pt2"

This uses the Haversine formula.

Parameters
----------
pt1 : ndarray
Nx3 array that contains lat, lon, and elev values that correspond
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add units, spell out latitude, longitude and elevation. Describe the sign convention for longitude.

to the origin points from which the dip angles are to be calculated.
The observer points. (will also take a 1darray with 3 elements)
to the origin points from which the elevation angles are to be
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"to the origin (observer) points" and drop the last sentence.

calculated. The observer points.

pt2 : ndarray
Nx3 array that contains lat, lon, and elev values that correspond
to the target points to which the dip angles are to be calculated.
The observee points. (will also take a 1darray with 3 elements)
to the target points to which the elevation angles are to be
calculated. The observee points.

Returns
-------
bearing_deg: Nx1 ndarray
bearing_deg: numeric
The bearings from pt1 to pt2 in degrees East of North.

dip_angle: Nx1 ndarray
The dip angles that the points in pt2 make with the horizontal
as observed from the points in pt1.
elevation_angle_deg: numeric
The elevation angles that the points in pt2 make with the horizontal
as observed from the points in pt1. Given in degrees above the
horizontal.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Brief Examples section would be helpful to avoid confusion about array formats.

'''
a = 6378137.0
Expand Down Expand Up @@ -111,9 +114,9 @@ def dip_calc(pt1, pt2):
dot = np.sum(np.multiply(delta, normal), axis=1)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would np.dot() do the trick?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay so I'm taking two Nx3 arrays and trying to do an "element-wise" dot product to get back an Nx1 array. I couldn't get np.dot() to do this but let me know if you know how.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think np.dot should work if the inputs are properly aligned, but let's address the potentially unnecessary array operations above before figuring out this one.

beta = np.arccos(dot / np.linalg.norm(delta, axis=1)
/ np.linalg.norm(normal, axis=1))
dip_angle = beta - np.pi/2
elevation_angle = beta - np.pi/2

dip_angle_deg = np.degrees(dip_angle)
elevation_angle_deg = np.degrees(elevation_angle)

bearing = np.arctan2(np.sin(theta2-theta1)*np.cos(phi2),
(np.cos(phi1) * np.sin(phi2)
Expand All @@ -123,7 +126,7 @@ def dip_calc(pt1, pt2):
mask = (bearing_deg < 0)
bearing_deg[mask] += 360

return bearing_deg, dip_angle_deg
return bearing_deg, elevation_angle_deg


def calculate_horizon_points(lat_grid, lon_grid, elev_grid,
Expand Down Expand Up @@ -161,9 +164,10 @@ def calculate_horizon_points(lat_grid, lon_grid, elev_grid,
The bearings from the "site" to sampled points in degrees
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bearings from the grid center to horizon points

East of North.

dip_angle_dip: Nx1 ndarray
The dip angles that the sampled points make with the horizontal
as observed from the the "site".
elevation_angle_deg: numeric
The angles that the sampled points make with the horizontal
as observed from the "site". Given in degrees above the
horizontal.

"""
assert(lat_grid.shape == lon_grid.shape == elev_grid.shape)
Expand All @@ -185,14 +189,14 @@ def calculate_horizon_points(lat_grid, lon_grid, elev_grid,
samples = sample_using_interpolator(lat_grid, lon_grid, elev_grid,
sampling_param)

bearing_deg, dip_angle_deg = dip_calc(site, samples)
bearing_deg, elevation_angle_deg = elevation_angle_calc(site, samples)

return bearing_deg, dip_angle_deg
return bearing_deg, elevation_angle_deg


def sample_using_grid(lat_grid, lon_grid, elev_grid):
"""
Calculates the dip angle from the site (center of the grid)
Calculates the Elevation angle from the site (center of the grid)
to every point on the grid.

Parameters
Expand Down Expand Up @@ -231,7 +235,7 @@ def sample_using_triangles(lat_grid, lon_grid, elev_grid,
samples_per_triangle=10):
"""
Creates triangles using nearest neighbors for every grid point and randomly
samples each of these triangles to find dip angles for the horizon profile.
samples each of these triangles to find Elevation angles for the horizon profile.

Parameters
----------
Expand Down Expand Up @@ -326,7 +330,7 @@ def sample_using_interpolator(lat_grid, lon_grid, elev_grid, num_samples):
"""
Creates a "grid" using polar coordinates and uses the scipy's grid
interpolator to estimate elevation values at each point on the polar grid
from the input (rectangular) grid that has true elevation values. Dip
from the input (rectangular) grid that has true elevation values. Elevation
calculations are done at each point on the polar grid and the results
are returned.

Expand Down Expand Up @@ -439,12 +443,11 @@ def filter_points(horizon_azimuths, horizon_angles, bin_size=1):
Azimuth values for points that define the horizon profile. The ith
element in this array corresponds to the ith element in horizon_angles.

horizon_angle: numeric
Dip angle values for points that define the horizon profile. The dip
angle of the horizon is the angle that the horizon makes with the
horizontal. It is given in degrees. If the horizon appears above
the horizontal, then the dip angle is positive. The ith element in
this array corresponds to the ith element in
horizon_angles: numeric
Elevation angle values for points that define the horizon profile. The
elevation angle of the horizon is the angle that the horizon makes with
the horizontal. It is given in degrees above the horizontal. The ith
element in this array corresponds to the ith element in
horizon_azimuths.

bin_size : int
Expand All @@ -458,23 +461,23 @@ def filter_points(horizon_azimuths, horizon_angles, bin_size=1):
filtered_angles.

filtered_angles: numeric
Dip angle values for points that define the horizon profile. The ith
element in this array corresponds to the ith element in
filtered_azimuths.
elevation angle values for points that define the horizon profile given
in degrees above the horizontal. The ith element in this array
corresponds to the ith element in filtered_azimuths.

"""
assert(horizon_azimuths.shape[0] == horizon_angles.shape[0])

wedges = {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll guess this loop may be a bit slow. What about instead:

rounded_azimuths = tools.round_to_nearest(horizon_azimuths, bin_size)
bins = np.unique(rounded_azimuths)

filtered = np.column_stack((bins, np.nan * bins))

for i in range(filtered.shape[0]):
    idx = np.nonzero(rounded_azimuths == filtered[i, 0])
    filtered[i, 1] = np.max(horizon_angles[idx])

return filtered[:, 0], filtered[:, 1]

Now that I've thought that through, there will always be a bin at 0 degrees, so the user ought to be informed of this point. I'd add to the note about bin_size.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple of questions on this.
Is np.nonzero necessary? I think you can just index with the boolean array.
Why will there always be a bin at 0 degrees?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple of questions on this.
Is np.nonzero necessary? I think you can just index with the boolean array.

You're probably right. I started with argwhere, decided it wasn't the right approach, stopped at nonzero without thinking further.

Why will there always be a bin at 0 degrees?

You are right again, there won't always be a bin at 0. But the bins will be a subset of 0, +/-bin_size, +/-2*bin_size, etc. That's what I meant to describe and what I think we shoudl tell the user.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs tests for the functions added to tools.py then I'm OK with the review.

for i in range(horizon_angles.shape[0]):
azimuth = horizon_azimuths[i]
dip = horizon_angles[i]
elevation = horizon_angles[i]
azimuth_wedge = tools.round_to_nearest(azimuth, bin_size)

if azimuth_wedge in wedges:
wedges[azimuth_wedge] = max(dip, wedges[azimuth_wedge])
wedges[azimuth_wedge] = max(elevation, wedges[azimuth_wedge])
else:
wedges[azimuth_wedge] = dip
wedges[azimuth_wedge] = elevation

filtered_azimuths = []
filtered_angles = []
Expand All @@ -487,10 +490,10 @@ def filter_points(horizon_azimuths, horizon_angles, bin_size=1):
return filtered_azimuths, filtered_angles


def collection_plane_dip_angle(surface_tilt, surface_azimuth, direction):
def collection_plane_elev_angle(surface_tilt, surface_azimuth, direction):
"""
Determine the dip angle created by the surface of a tilted plane
in a given direction. The dip angle is limited to be non-negative.
Determine the elevation angle created by the surface of a tilted plane
in a given direction. The angle is limited to be non-negative.

Parameters
----------
Expand All @@ -505,26 +508,28 @@ def collection_plane_dip_angle(surface_tilt, surface_azimuth, direction):
east of north (e.g. North = 0, South=180 East = 90, West = 270).

direction : numeric
The direction along which the dip angle is to be calculated in
The direction along which the elevation angle is to be calculated in
decimal degrees. The convention is defined as degrees
east of north (e.g. North = 0, South=180 East = 90, West = 270).

Returns
--------

dip_angle : numeric
The dip angle in direction created by the tilted plane.
elevation_angle : numeric
The angle between the surface of the tilted plane and the horizontal
when looking in the specified direction. Given in degrees above the
horizontal and limited to be non-negative.

"""
tilt = np.radians(surface_tilt)
bearing = np.radians(direction - surface_azimuth - 180.0)

declination = np.degrees(np.arctan(1.0/np.tan(tilt)/np.cos(bearing)))
mask = (declination <= 0)
dip = 90.0 - declination
dip[mask] = 0.0
elevation_angle = 90.0 - declination
elevation_angle[mask] = 0.0

return dip
return elevation_angle


def calculate_dtf(horizon_azimuths, horizon_angles,
Expand All @@ -540,11 +545,10 @@ def calculate_dtf(horizon_azimuths, horizon_angles,
element in this array corresponds to the ith element in horizon_angles.

horizon_angles: numeric
Dip angle values for points that define the horizon profile. The dip
angle of the horizon is the angle that the horizon makes with the
horizontal. It is given in degrees. If the horizon appears above
the horizontal, then the dip angle is positive. The ith element in
this array corresponds to the ith element in
Elevation angle values for points that define the horizon profile. The
elevation angle of the horizon is the angle that the horizon makes with
the horizontal. It is given in degrees above the horizontal. The ith
element in this array corresponds to the ith element in
horizon_azimuths.

surface_tilt : numeric
Expand Down Expand Up @@ -578,14 +582,14 @@ def calculate_dtf(horizon_azimuths, horizon_angles,
num_points = horizon_azimuths.shape[0]
for i in range(horizon_azimuths.shape[0]):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this for loop necessary? It looks like all of the operations would properly broadcast, then you'd replace the dtf += line with a sum call.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The broadcasting issue is in collection_plane_elev_angle. I'll see if I can make it work.

az = np.radians(horizon_azimuths[i])
horizon_dip = np.radians(horizon_angles[i])
temp = np.radians(collection_plane_dip_angle(surface_tilt,
surface_azimuth,
horizon_azimuths[i]))
dip = np.maximum(horizon_dip, temp)
horizon_elev = np.radians(horizon_angles[i])
temp = np.radians(collection_plane_elev_angle(surface_tilt,
surface_azimuth,
horizon_azimuths[i]))
elev = np.maximum(horizon_elev, temp)

first_term = .5 * (a*np.cos(az) + b*np.sin(az)) * \
(np.pi/2 - dip - np.sin(dip) * np.cos(dip))
second_term = .5 * c * np.cos(dip)**2
(np.pi/2 - elev - np.sin(elev) * np.cos(elev))
second_term = .5 * c * np.cos(elev)**2
dtf += 2 * (first_term + second_term) / num_points
return dtf
63 changes: 32 additions & 31 deletions pvlib/test/test_horizon.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def test_grid_lat_lon():
assert_allclose(lat_grid[30][72], lat + 22*grid_step)


def test_dip_calc():
def test_elev_calc():
pt1 = np.array((71.23, -34.70, 1234))
pt2 = np.array((71.12, -34.16, 124))
pt3 = np.array((71.29, -35.23, 30044))
Expand All @@ -28,15 +28,16 @@ def test_dip_calc():
reverse_test_pts = np.vstack([pt2, pt1])

expected_bearings = np.array([121.9895, 302.5061])
expected_dips = np.array([-2.8654, 2.6593])
expected_elevs = np.array([-2.8654, 2.6593])

expected13 = np.array([[289.8132], [54.8663]])

act_bearings, act_dips = horizon.dip_calc(test_pts, reverse_test_pts)
act_bearings, act_elevs = horizon.elevation_angle_calc(test_pts,
reverse_test_pts)
assert_allclose(act_bearings, expected_bearings, rtol=1e-3)
assert_allclose(act_dips, expected_dips, rtol=1e-3)
assert_allclose(act_elevs, expected_elevs, rtol=1e-3)

actual13 = horizon.dip_calc(pt1, pt3)
actual13 = horizon.elevation_angle_calc(pt1, pt3)
assert_allclose(expected13, actual13, rtol=1e-3)


Expand All @@ -53,29 +54,29 @@ def test_calculate_horizon_points():
[212, 135, 1],
[36, 145, 5]])

bearings, dips = horizon.calculate_horizon_points(test_lat_grid,
test_lon_grid,
test_elev_grid,
sampling_method="grid")
dirs, elevs = horizon.calculate_horizon_points(test_lat_grid,
test_lon_grid,
test_elev_grid,
sampling_method="grid")

expected_bearings = np.array([0, 90, 45, 270, 135])
rounded_bearings = np.round(bearings).astype(int)
assert(bearings.shape == dips.shape)
assert(np.all(np.in1d(expected_bearings, rounded_bearings)))
expected_dirs = np.array([0, 90, 45, 270, 135])
rounded_dirs = np.round(dirs).astype(int)
assert(dirs.shape == elevs.shape)
assert(np.all(np.in1d(expected_dirs, rounded_dirs)))

bears, dips = horizon.calculate_horizon_points(test_lat_grid,
dirs, elevs = horizon.calculate_horizon_points(test_lat_grid,
test_lon_grid,
test_elev_grid,
sampling_method="triangles",
sampling_param=5)
assert(bears.shape == dips.shape)
assert(dirs.shape == elevs.shape)

bears, _ = horizon.calculate_horizon_points(test_lat_grid,
test_lon_grid,
test_elev_grid,
sampling_method="interpolator",
sampling_param=(10, 10))
assert(bears.shape[0] == 100)
dirs, _ = horizon.calculate_horizon_points(test_lat_grid,
test_lon_grid,
test_elev_grid,
sampling_method="interpolator",
sampling_param=(10, 10))
assert(dirs.shape[0] == 100)


def test_sample_using_grid():
Expand Down Expand Up @@ -173,7 +174,7 @@ def test_filter_points():
assert(filtered_angles[1] == 10)


def test_collection_plane_dip_angle():
def test_collection_plane_elev_angle():
surface_tilts = np.array([0, 5, 20, 38, 89])
surface_azimuths = np.array([0, 90, 180, 235, 355])
directions_easy = np.array([78, 270, 0, 145, 355])
E 7BAA xpand All @@ -182,15 +183,15 @@ def test_collection_plane_dip_angle():
expected_easy = np.array([0, 5, 20, 0, 0])
expected_hard = np.array([0, 3.21873120519, 10.3141048156,
21.3377447931, 0])
dips_easy = horizon.collection_plane_dip_angle(surface_tilts,
surface_azimuths,
directions_easy)
assert_allclose(dips_easy, expected_easy)

dips_hard = horizon.collection_plane_dip_angle(surface_tilts,
surface_azimuths,
directions_hard)
assert_allclose(dips_hard, expected_hard)
elevs_easy = horizon.collection_plane_elev_angle(surface_tilts,
surface_azimuths,
directions_easy)
assert_allclose(elevs_easy, expected_easy)

elevs_hard = horizon.collection_plane_elev_angle(surface_tilts,
surface_azimuths,
directions_hard)
assert_allclose(elevs_hard, expected_hard)


def test_calculate_dtf():
Expand Down
0