Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Bundle-ManifestVersion: 2
Bundle-Name: pvlib Datum Filter
Bundle-Description: Calculate solar characteristics like POA irradiance from GHI data.
Bundle-SymbolicName: net.solarnetwork.node.datum.filter.pvlib
Bundle-Version: 2.0.0
Bundle-Version: 2.1.0
Bundle-Vendor: SolarNetwork
Automatic-Module-Name: net.solarnetwork.node.datum.filter.pvlib
Bundle-RequiredExecutionEnvironment: JavaSE-17
Expand Down
203 changes: 118 additions & 85 deletions net.solarnetwork.node.datum.filter.pvlib/README.md

Large diffs are not rendered by default.

129 changes: 112 additions & 17 deletions net.solarnetwork.node.datum.filter.pvlib/def/ghi-to-poa.py
Original file line number Diff line number Diff line change
@@ -1,73 +1,134 @@
import getopt
import json
import math
import pandas as pd
import sys
import warnings

from datetime import datetime
from datetime import timezone
from pvlib import irradiance
from pvlib import tracking as pvtracking
from pvlib.location import Location

# suppress numpy/pvlib runtime warnings.
warnings.simplefilter('ignore')

def usage():
print("""Usage:

-a --altitude elevation above sea level, in meters
-A --max-angle optional maximum tracker rotation angle from horizontal, in degrees,
0 exclusive to 180 (default 90)
-b --backtrack optional 'true'/'false' to enable tracker backtracking (default false)
-d --date date, like YYYY-MM-DDTHH:mm:ss
-g --gcr optional ground coverage ratio, used for backtracking,
0 exclusive to 1 (default 0.2857)
-i --irradiance GHI irradiance, in W/m^2
-k --tracking 'true'/'false' to enable single-axis tracker mode; when true
--array-tilt and --array-azimuth are ignored
-l --latitude decimal latitude
-L --longitude decimal longitude
-m --min-cos-zenith optional minimum cos(zenith) value when calculating global clearness index
-M --max-zenith optional maximum zenith value in DNI calculation
-t --array-tilt solar array tilt angle from horizontal, in degrees
-T --transpose the transposition model to use, e.g. 'haydavies', 'perez-driesse'
-u --array-azimuth solar array angle clockwise from north
-x --axis-tilt tracker axis tilt angle from horizontal, in degrees, 0 to 90 (default 0)
-X --axis-azimuth tracker axis angle clockwise from north, in degrees, 0 to 360 (default 0)
-z --zone time zone, like Pacific/Auckland
""")

def invalid_value(message: str):
print(message, file=sys.stderr)
sys.exit(2)

def parse_bool(opt: str, s: str) -> bool:
v = s.strip().lower()
if v in ('true', '1', 'yes', 'y'):
return True
if v in ('false', '0', 'no', 'n'):
return False
invalid_value("%s: invalid boolean value '%s'" % (opt, s))

def parse_ranged_float(opt: str, s: str, lo: float, hi: float, lo_exclusive=False) -> float:
try:
v = float(s)
except ValueError:
v = math.nan
if not math.isfinite(v) or v > hi or (v <= lo if lo_exclusive else v < lo):
invalid_value("%s: value '%s' not a number between %s%s and %s"
% (opt, s, lo, ' (exclusive)' if lo_exclusive else '', hi))
return v

def ghi_get_irradiance(location: Location,
array_tilt: float,
array_azimuth: float,
ghi: float,
date: str,
min_cos_zenith=None,
max_zenith=None,
transposition_model='haydavies') -> dict:

transposition_model='haydavies',
tracking=False,
axis_tilt=0,
axis_azimuth=0,
max_angle=90,
backtrack=False,
gcr=2.0/7.0) -> dict:

times = pd.DatetimeIndex(data = [date], tz = location.tz)

solar_position = location.get_solarposition(times=times)

ghi_data = pd.Series([ghi], index=times)

min_cos_zenith = 0.065 if min_cos_zenith is None else min_cos_zenith
max_zenith = 87 if max_zenith is None else max_zenith

erbs = irradiance.erbs(
ghi = ghi_data,
zenith = solar_position['apparent_zenith'],
min_cos_zenith = min_cos_zenith,
max_zenith = max_zenith,
datetime_or_doy = times
)

dni_extra = irradiance.get_extra_radiation(times)


tracker = None
if tracking:
# single-axis tracker: derive the panel orientation from the sun
# position; sun below the horizon produces NaN, fall back to flat
tracker = pvtracking.singleaxis(
apparent_zenith = solar_position['apparent_zenith'],
apparent_azimuth = solar_position['azimuth'],
axis_tilt = axis_tilt,
axis_azimuth = axis_azimuth,
max_angle = max_angle,
backtrack = backtrack,
gcr = gcr).fillna(0)
surface_tilt = tracker['surface_tilt']
surface_azimuth = tracker['surface_azimuth']
else:
surface_tilt = array_tilt
surface_azimuth = array_azimuth

poa = irradiance.get_total_irradiance(
model = transposition_model,
surface_tilt = array_tilt,
surface_azimuth = array_azimuth,
surface_tilt = surface_tilt,
surface_azimuth = surface_azimuth,
dni = erbs['dni'],
dhi = erbs['dhi'],
dni_extra = dni_extra,
ghi = ghi_data,
solar_azimuth = solar_position['azimuth'],
solar_zenith = solar_position['apparent_zenith']
)

# transpose single row (timestamp) into into simple dictionary
result = {'date': date,
'zone': location.tz,
'ghi': ghi,
'ghi': ghi,
'dni': erbs['dni'].iloc[0],
'dhi': erbs['dhi'].iloc[0],
'zenith': solar_position['apparent_zenith'].iloc[0],
Expand All @@ -79,17 +140,26 @@ def ghi_get_irradiance(location: Location,
for r in poa[d]:
result.update({d: r})

if tracker is not None:
result.update({'tracker_theta': tracker['tracker_theta'].iloc[0],
'aoi': tracker['aoi'].iloc[0],
'surface_tilt': tracker['surface_tilt'].iloc[0],
'surface_azimuth': tracker['surface_azimuth'].iloc[0],
})

return result

try:
opts, args = getopt.getopt(
sys.argv[1:],
'a:d:i:l:L:m:M:t:T:u:z:',
'a:A:b:d:g:i:k:l:L:m:M:t:T:u:x:X:z:',
['altitude=', 'date=', 'irradiance=',
'latitude=', 'longitude=',
'min-cos-zenith=', 'max-zenith=',
'latitude=', 'longitude=',
'min-cos-zenith=', 'max-zenith=',
'array-tilt=', 'transpose=',
'array-azimuth=', 'zone='],
'array-azimuth=', 'zone=',
'tracking=', 'axis-tilt=', 'axis-azimuth=',
'max-angle=', 'backtrack=', 'gcr='],
)
except getopt.GetoptError as e:
print(e)
Expand All @@ -107,16 +177,31 @@ def ghi_get_irradiance(location: Location,
max_zenith = None
model = 'haydavies'

tracking = False
axis_tilt = 0
axis_azimuth = 0
max_angle = 90
backtrack = False
gcr = 2.0/7.0

ghi = 0
date = datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%S')

for opt, arg in opts:
if opt in ('-a', '--altitude'): # m
alt = float(arg)
elif opt in ('-A', '--max-angle'): # angle in degrees
max_angle = parse_ranged_float(opt, arg, 0, 180, lo_exclusive=True)
elif opt in ('-b', '--backtrack'):
backtrack = parse_bool(opt, arg)
elif opt in ('-d', '--date'):
date = arg
elif opt in ('-g', '--gcr'):
gcr = parse_ranged_float(opt, arg, 0, 1, lo_exclusive=True)
elif opt in ('-i', '--irradiance'): # W/m2
ghi = float(arg)
elif opt in ('-k', '--tracking'):
tracking = parse_bool(opt, arg)
elif opt in ('-l', '--latitude'):
lat = float(arg)
elif opt in ('-L', '--longitude'):
Expand All @@ -131,6 +216,10 @@ def ghi_get_irradiance(location: Location,
model = arg
elif opt in ('-u', '--array-azimuth'): # angle in degrees
array_azimuth = float(arg)
elif opt in ('-x', '--axis-tilt'): # angle in degrees
axis_tilt = parse_ranged_float(opt, arg, 0, 90)
elif opt in ('-X', '--axis-azimuth'): # angle in degrees
axis_azimuth = parse_ranged_float(opt, arg, 0, 360)
elif opt in ('-z', '--zone'):
zone = arg

Expand All @@ -144,7 +233,13 @@ def ghi_get_irradiance(location: Location,
max_zenith = max_zenith,
ghi = ghi,
date = date,
transposition_model = model
transposition_model = model,
tracking = tracking,
axis_tilt = axis_tilt,
axis_azimuth = axis_azimuth,
max_angle = max_angle,
backtrack = backtrack,
gcr = gcr
)

print(json.dumps(poa))
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
* Enumeration of command options with associated metadata keys.
*
* @author matt

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

You should add your own @author tag after this one.

* @version 1.1
* @version 1.2
*/
public enum CommandOptions {

Expand Down Expand Up @@ -69,6 +69,48 @@ public enum CommandOptions {
*/
TranspositionModel("--transpose", "transpositionModel"),

/**
* A single-axis tracker mode flag, as {@literal true} or {@literal false}.
*
* @since 1.2
*/
Tracking("--tracking", "tracking"),

/**
* A tracker axis tilt angle value, in degrees from horizontal.
*
* @since 1.2
*/
AxisTilt("--axis-tilt", "pvAxisTilt"),
Comment thread
eljpsm marked this conversation as resolved.

/**
* A tracker axis angle value, in degrees clockwise from north.
*
* @since 1.2
*/
AxisAzimuth("--axis-azimuth", "pvAxisAzimuth"),

/**
* A maximum tracker rotation angle value, in degrees from horizontal.
*
* @since 1.2
*/
MaxAngle("--max-angle", "maxAngle"),

/**
* A tracker backtracking flag, as {@literal true} or {@literal false}.
*
* @since 1.2
*/
Backtrack("--backtrack", "backtrack"),

/**
* A ground coverage ratio value, used for backtracking.
*
* @since 1.2
*/
Gcr("--gcr", "gcr"),

;

private final String option;
Expand Down
Loading