Skip to content
This repository has been archived by the owner on Mar 6, 2024. It is now read-only.

PV Number of Panels #179

Merged
merged 2 commits into from
Dec 10, 2021
Merged
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
72 changes: 46 additions & 26 deletions docs/source/translation/generation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,55 +4,75 @@ Generation
Solar Electric
**************

HEScore allows for a single photovoltaic system to be included as of v2016.
In HPXML, multiple ``PVSystem`` elements can be specified to represent the PV systems on the house.
The translator combines multiple systems and generates the appropriate HEScore inputs as follows:
HEScore allows for a single photovoltaic system to be included as of v2016. In
HPXML, multiple ``PVSystem`` elements can be specified to represent the PV
systems on the house. The translator combines multiple systems and generates the
appropriate HEScore inputs as follows:

Capacity Known
==============

If each ``PVSystem`` has a ``MaxPowerOutput``, this is true.
If each ``PVSystem`` has a ``CollectorArea``, this is false.
Preference is given to known capacity if both are available.
Either a ``MaxPowerOutput`` must be specified for every ``PVSystem``
or ``CollectorArea`` must be specified for every ``PVSystem``.
If each ``PVSystem`` has a ``MaxPowerOutput``, this is true. If each
``PVSystem`` has a ``NumberOfPanels`` or if each has ``CollectorArea``, this is
false. Preference is given to known capacity if available. Either a
``MaxPowerOutput`` must be specified for every ``PVSystem`` or ``CollectorArea``
must be specified for every ``PVSystem``.

DC Capacity
===========

If each ``PVSystem`` has a ``MaxPowerOutput``, the system capacity is known.
The ``system_capacity`` in HEScore is calculated by summing all the ``MaxPowerOutput`` elements in HPXML.
If each ``PVSystem`` has a ``MaxPowerOutput``, the system capacity is known. The
``system_capacity`` in HEScore is calculated by summing all the
``MaxPowerOutput`` elements in HPXML.

Number of Panels
================

If ``MaxPowerOutput`` is missing from any ``PVSystem``,
``CollectorArea`` is required on every PVSystem and the system capacity is not known.
The number of panels is calculated by summing all the collector area, dividing by 17.6 sq.ft.,
and rounding to the nearest whole number.
If ``MaxPowerOutput`` is missing from any ``PVSystem``, the translator will
check to see if every system has ``NumberOfPanels`` and calculate the total
number of panels.

If ``NumberOfPanels`` isn't available on every system, the translator will look
for ``CollectorArea`` on every PVSystem. The number of panels is calculated by
summing all the collector area, dividing by 17.6 sq.ft., and rounding to the
nearest whole number.

Weighted Averages
=================

The below quantities are calculated using weighted averages. The weights used
are in priority order:

- ``MaxPowerOutput``
- ``NumberOfPanels``
- ``CollectorArea``

Which is the same data elements used to determine the PV sizing inputs above.

Year Installed
==============

For each ``PVSystem`` the ``YearInverterManufactured`` and ``YearModulesManufactured`` element values are retrieved,
and the greater of the two is assumed to be the year that system was installed.
When there are multiple ``PVSystem`` elements, a capacity or area-weighted average of the assumed year installed
is calculated and used.
For each ``PVSystem`` the ``YearInverterManufactured`` and
``YearModulesManufactured`` element values are retrieved, and the greater of the
two is assumed to be the year that system was installed. When there are multiple
``PVSystem`` elements, a weighted average is calculated and used.

Panel Orientation (Azimuth)
===========================

For each ``PVSystem`` the ``ArrayAzimuth`` (degrees clockwise from north) is retrieved.
If ``ArrayAzimuth`` is not available, ``ArrayOrientation`` (north, northwest, etc) is converted into an azimuth.
A capacity or area-weighted average azimuth is calculated and converted into the nearest cardinal direction
(north, northwest, etc) for submission into the ``array_azimuth`` HEScore input (which expects a direction,
not a numeric azimuth).
For each ``PVSystem`` the ``ArrayAzimuth`` (degrees clockwise from north) is
retrieved. If ``ArrayAzimuth`` is not available, ``ArrayOrientation`` (north,
northwest, etc) is converted into an azimuth. A weighted average azimuth is
calculated and converted into the nearest cardinal direction (north, northwest,
etc) for submission into the ``array_azimuth`` HEScore input (which expects a
direction, not a numeric azimuth).

Panel Tilt
==========
For each ``PVSystem`` the ``ArrayTilt`` (in degrees from horizontal) is retrieved.
A capacity or area-weighted average tilt is calculated and submitted to the ``array_tilt`` HEScore input
(which expects an enumeration, not a numeric tilt).

For each ``PVSystem`` the ``ArrayTilt`` (in degrees from horizontal) is
retrieved. A weighted average tilt is calculated and submitted to the
``array_tilt`` HEScore input (which expects an enumeration, not a numeric tilt).
The tilt is mapped to HEScore as follows:

.. table:: Tilt mapping
Expand Down
65 changes: 30 additions & 35 deletions hescorehpxml/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ def round_to_nearest(x, vals, tails_tolerance=None):
return nearest


def weighted_average(items, weights):
return sum(item * weight for item, weight in zip(items, weights)) / sum(weights)


class HPXMLtoHEScoreTranslatorBase(object):
SCHEMA_DIR = None

Expand Down Expand Up @@ -2235,31 +2239,28 @@ def get_generation(self, b):

capacities = []
collector_areas = []
n_panels_per_system = []
years = []
azimuths = []
tilts = []
for pvsystem in pvsystems:

max_power_output = self.xpath(pvsystem, 'h:MaxPowerOutput/text()')
if max_power_output:
capacities.append(float(max_power_output)) # W
collector_areas.append(None)
else:
capacities.append(None)
collector_area = self.xpath(pvsystem, 'h:CollectorArea/text()')
if collector_area:
collector_areas.append(float(collector_area))
else:
raise TranslationError('MaxPowerOutput or CollectorArea is required for every PVSystem.')
capacities.append(convert_to_type(float, self.xpath(pvsystem, 'h:MaxPowerOutput/text()')))
collector_areas.append(convert_to_type(float, self.xpath(pvsystem, 'h:CollectorArea/text()')))
n_panels_per_system.append(convert_to_type(int, self.xpath(pvsystem, 'h:NumberOfPanels/text()')))

if not (capacities[-1] or collector_areas[-1] or n_panels_per_system[-1]):
raise TranslationError(
'MaxPowerOutput, NumberOfPanels, or CollectorArea is required for every PVSystem.'
)
bpark1327 marked this conversation as resolved.
Show resolved Hide resolved

manufacture_years = list(map(
int,
self.xpath(
manufacture_years = [
int(x) for x in self.xpath(
pvsystem,
'h:YearInverterManufactured/text()|h:YearModulesManufactured/text()',
aslist=True)
)
)
aslist=True
)
]
if manufacture_years:
years.append(max(manufacture_years)) # Use the latest year of manufacture
else:
Expand All @@ -2283,31 +2284,25 @@ def get_generation(self, b):

if None not in capacities:
solar_electric['capacity_known'] = True
total_capacity = sum(capacities)
solar_electric['system_capacity'] = total_capacity / 1000.
solar_electric['year'] = int(
old_div(sum([year * capacity for year, capacity in zip(years, capacities)]), total_capacity))
wtavg_azimuth = old_div(sum(
[az * capacity for az, capacity in zip(azimuths, capacities)]), total_capacity)
wtavg_tilt = sum(t * capacity for t, capacity in zip(tilts, capacities)) / total_capacity
solar_electric['system_capacity'] = sum(capacities) / 1000.
weights = capacities
elif None not in n_panels_per_system:
solar_electric['capacity_known'] = False
solar_electric['num_panels'] = sum(n_panels_per_system)
weights = n_panels_per_system
elif None not in collector_areas:
solar_electric['capacity_known'] = False
total_area = sum(collector_areas)
solar_electric['num_panels'] = int(python2round(total_area / 17.6))
solar_electric['year'] = int(sum([year * area for year, area in zip(years, collector_areas)]) / total_area)
wtavg_azimuth = old_div(sum(
[az * area for az, area in zip(azimuths, collector_areas)]
), total_area)
wtavg_tilt = sum(t * area for t, area in zip(tilts, collector_areas)) / total_area
solar_electric['num_panels'] = int(round(sum(collector_areas) / 17.6))
weights = collector_areas
else:
raise TranslationError(
'Either a MaxPowerOutput must be specified for every PVSystem '
'or CollectorArea must be specified for every PVSystem.'
'Either a MaxPowerOutput or NumberOfPanels or CollectorArea must be specified for every PVSystem.'
)

nearest_azimuth = self.get_nearest_azimuth(azimuth=wtavg_azimuth)
solar_electric['year'] = round(weighted_average(years, weights))
nearest_azimuth = self.get_nearest_azimuth(azimuth=weighted_average(azimuths, weights))
solar_electric['array_azimuth'] = self.azimuth_to_hescore_orientation[nearest_azimuth]
solar_electric['array_tilt'] = self.get_nearest_tilt(wtavg_tilt)
solar_electric['array_tilt'] = self.get_nearest_tilt(weighted_average(tilts, weights))

return generation

Expand Down
24 changes: 20 additions & 4 deletions tests/test_translation.py
Original file line number Diff line number Diff line change
Expand Up @@ -1642,6 +1642,7 @@ def _add_pv(
capacity=5,
inverter_year=2015,
module_year=2013,
n_panels=None,
collector_area=None):
addns = self.translator.addns

Expand All @@ -1668,6 +1669,8 @@ def add_elem(parent, subname, text=None):
add_elem(pv_system, 'MaxPowerOutput', capacity * 1000)
if collector_area is not None:
add_elem(pv_system, 'CollectorArea', collector_area)
if n_panels is not None:
add_elem(pv_system, 'NumberOfPanels', n_panels)
if inverter_year is not None:
add_elem(pv_system, 'YearInverterManufactured', inverter_year)
if module_year is not None:
Expand All @@ -1690,17 +1693,30 @@ def test_capacity_missing(self):
self._add_pv(capacity=None)
self.assertRaisesRegex(
TranslationError,
r'MaxPowerOutput or CollectorArea is required',
r'MaxPowerOutput, NumberOfPanels, or CollectorArea is required',
tr.hpxml_to_hescore
)
)

def test_n_panels(self):
tr = self._load_xmlfile('hescore_min_v3')
self._add_pv(
capacity=None,
n_panels=12,
collector_area=1
)
hesd = tr.hpxml_to_hescore()
pv = hesd['building']['systems']['generation']['solar_electric']
self.assertFalse(pv['capacity_known'])
self.assertNotIn('system_capacity', list(pv.keys()))
self.assertEqual(pv['num_panels'], 12)

def test_collector_area(self):
tr = self._load_xmlfile('hescore_min')
self._add_pv(capacity=None, collector_area=176)
hesd = tr.hpxml_to_hescore()
pv = hesd['building']['systems']['generation']['solar_electric']
self.assertFalse(pv['capacity_known'])
self.assertNotIn('capacity', list(pv.keys()))
self.assertNotIn('system_capacity', list(pv.keys()))
self.assertEqual(pv['num_panels'], 10)

def test_orientation(self):
Expand Down Expand Up @@ -1761,7 +1777,7 @@ def test_two_sys_different_capacity_error(self):
module_year=2013)
self.assertRaisesRegex(
TranslationError,
r'Either a MaxPowerOutput must be specified for every PVSystem or CollectorArea',
r'Either a MaxPowerOutput or NumberOfPanels or CollectorArea must be specified',
tr.hpxml_to_hescore
)

Expand Down