# """Honeybee PointGroup and TestPointGroup."""
from __future__ import division
from ..vectormath.euclid import Point3, Vector3
from ..schedule import Schedule
from collections import defaultdict, OrderedDict
try:
from itertools import izip as zip
except ImportError:
# python 3
pass
import types
import copy
import ladybug.dt as dt
[docs]class AnalysisPoint(object):
"""A radiance analysis point.
Attributes:
location: Location of analysis points as (x, y, z).
direction: Direction of analysis point as (x, y, z).
This class is developed to enable honeybee for running daylight control
studies with dynamic shadings without going back to several files.
Each AnalysisPoint can load annual total and direct results for every state of
each source assigned to it. As a result once can end up with a lot of data for
a single point (8760 * sources * states for each source). The data are sorted as
integers and in different lists for each source. There are several methods to
set or get the data but if you're interested in more details read the comments
under __init__ to know how the data is stored.
In this class:
- Id stands for 'the id of a blind state'. Each state has a name and an ID will
be assigned to it based on the order of loading.
- coupledValue stands for a tuple of (total, direct) values. If one the values is
not available it will be set to None.
"""
__slots__ = ('_loc', '_dir', '_sources', '_values', '_is_directLoaded', 'logic')
def __init__(self, location, direction):
"""Create an analysis point."""
self.location = location
self.direction = direction
# name of sources and their state. It's only meaningful in multi-phase daylight
# analysis. In analysis for a single time it will be {None: [None]}
# It is set inside _create_data_structure method on setting values.
self._sources = OrderedDict()
# an empty list for values
# for each source there will be a new list
# inside each source list there will be a dictionary for each state
# in each dictionary the key is the hoy and the values are a list which
# is [total, direct]. If the value is not available it will be None
self._values = []
self._is_directLoaded = False
self.logic = self._logic
# TODO(mostapha): Restructure analysis points and write a class to keep track of
# results.
# Note to self! This is a hack!
# assume it's only a single source
[docs] @classmethod
def from_json(cls, ap_json):
"""Create an analysis point from json object.
{"location": [x, y, z], "direction": [x, y, z]}
"""
_cls = cls(ap_json['location'], ap_json['direction'])
if 'values' in ap_json:
sid, stateid = _cls._create_data_structure(None, None)
values = []
hoys = []
try:
state_res = ap_json['values'][0]
except IndexError:
state_res = []
for item in state_res:
for k, v in item.items():
values.append(v)
hoys.append(float(k))
# set the values
_cls.set_coupled_values(values, hoys, source=None, state=None)
return _cls
[docs] @classmethod
def from_raw_values(cls, x, y, z, x1, y1, z1):
"""Create an analysis point from 6 values.
x, y, z are the location of the point and x1, y1 and z1 is the direction.
"""
return cls((x, y, z), (x1, y1, z1))
@property
def location(self):
"""Location of analysis points as Point3."""
return self._loc
@location.setter
def location(self, location):
try:
self._loc = Point3(*(float(l) for l in location))
except TypeError:
try:
# Dynamo Points!
self._loc = Point3(location.X, location.Y, location.Z)
except Exception as e:
raise TypeError(
'Failed to convert {} to location.\n'
'location should be a list or a tuple with 3 values.\n{}'
.format(location, e))
@property
def direction(self):
"""Direction of analysis points as Point3."""
return self._dir
@direction.setter
def direction(self, direction):
try:
self._dir = Vector3(*(float(d) for d in direction))
except TypeError:
try:
# Dynamo Points!
self._dir = Vector3(direction.X, direction.Y, direction.Z)
except Exception as e:
raise TypeError(
'Failed to convert {} to direction.\n'
'location should be a list or a tuple with 3 values.\n{}'
.format(direction, e))
@property
def sources(self):
"""Get sorted list of light sources.
In most of the cases light sources are window groups.
"""
srcs = list(range(len(self._sources)))
for name, d in self._sources.items():
srcs[d['id']] = name
return srcs
@property
def details(self):
"""Human readable details."""
header = 'Location: {}\nDirection: {}\n#hours: {}\n#window groups: {}\n'.format(
', '.join(str(c) for c in self.location),
', '.join(str(c) for c in self.direction),
len(self.hoys), len(self._sources)
)
sep = '-' * 15
wg = '\nWindow Group {}: {}\n'
st = '....State {}: {}\n'
# sort sources based on ids
sources = list(range(len(self._sources)))
for s, d in self._sources.items():
sources[d['id']] = (s, d)
# create the string for eacj window groups
notes = [header, sep]
for count, s in enumerate(sources):
name, states = s
notes.append(wg.format(count, name))
for count, name in enumerate(states['state']):
notes.append(st.format(count, name))
return ''.join(notes)
@property
def has_values(self):
"""Check if this point has results values."""
return len(self._values) != 0
@property
def has_direct_values(self):
"""Check if direct values are loaded for this point.
In some cases and based on the recipe only total values are available.
"""
return self._is_directLoaded
@property
def hoys(self):
"""Return hours of the year for results if any."""
if not self.has_values:
return []
else:
return sorted(key / 60.0 for key in self._values[0][0].keys())
@property
def moys(self):
"""Return minutes of the year for results if any."""
if not self.has_values:
return []
else:
return sorted(self._values[0][0].keys())
@staticmethod
def _logic(*args, **kwargs):
"""Dynamic blinds state logic.
If the logic is not met the blind will be moved to the next state.
Overwrite this method for optional blind control.
"""
return args[0] > 3000
[docs] def source_id(self, source):
"""Get source id from source name."""
# find the id for source and state
try:
return self._sources[source]['id']
except KeyError:
raise ValueError('Invalid source input: {}'.format(source))
[docs] def blind_state_id(self, source, state):
"""Get state id if available."""
try:
return int(state)
except ValueError:
pass
try:
return self._sources[source]['state'].index(state)
except ValueError:
raise ValueError('Invalid state input: {}'.format(state))
@property
def states(self):
"""Get list of states names for each source."""
return tuple(s[1]['state'] for s in self._sources.items())
@property
def longest_state_ids(self):
"""Get longest combination between blind states as blinds_state_ids."""
states = tuple(len(s[1]['state']) - 1 for s in self._sources.items())
if not states:
raise ValueError('This sensor is associated with no dynamic blinds.')
return tuple(tuple(min(s, i) for s in states)
for i in range(max(states) + 1))
def _create_data_structure(self, source, state):
"""Create place holders for sources and states if needed.
Returns:
source id and state id as a tuple.
"""
def double():
return [None, None]
current_sources = self._sources.keys()
if source not in current_sources:
self._sources[source] = {
'id': len(current_sources),
'state': []
}
# append a new list to values for the new source
self._values.append([])
# find the id for source and state
sid = self._sources[source]['id']
if state not in self._sources[source]['state']:
# add sources
self._sources[source]['state'].append(state)
# append a new dictionary for this state
self._values[sid].append(defaultdict(double))
# find the state id
stateid = self._sources[source]['state'].index(state)
return sid, stateid
[docs] def set_value(self, value, hoy, source=None, state=None, is_direct=False):
"""Set value for a specific hour of the year.
Args:
value: Value as a number.
hoy: The hour of the year that corresponds to this value.
source: Name of the source of light. Only needed in case of multiple
sources / window groups (default: None).
state: State of the source if any (default: None).
is_direct: Set to True if the value is direct contribution of sunlight.
"""
if hoy is None:
return
sid, stateid = self._create_data_structure(source, state)
if is_direct:
self._is_directLoaded = True
ind = 1 if is_direct else 0
self._values[sid][stateid][int(hoy * 60)][ind] = value
[docs] def set_values(self, values, hoys, source=None, state=None, is_direct=False):
"""Set values for several hours of the year.
Args:
values: List of values as numbers.
hoys: List of hours of the year that corresponds to input values.
source: Name of the source of light. Only needed in case of multiple
sources / window groups (default: None).
state: State of the source if any (default: None).
is_direct: Set to True if the value is direct contribution of sunlight.
"""
if not (isinstance(values, types.GeneratorType) or
isinstance(hoys, types.GeneratorType)):
assert len(values) == len(hoys), \
ValueError(
'Length of values [%d] is not equal to length of hoys [%d].'
% (len(values), len(hoys)))
sid, stateid = self._create_data_structure(source, state)
if is_direct:
self._is_directLoaded = True
ind = 1 if is_direct else 0
for hoy, value in zip(hoys, values):
if hoy is None:
continue
try:
self._values[sid][stateid][int(hoy * 60)][ind] = value
except Exception as e:
raise ValueError(
'Failed to load {} results for window_group [{}], state[{}]'
' for hour {}.\n{}'.format('direct' if is_direct else 'total',
sid, stateid, hoy, e)
)
[docs] def set_coupled_value(self, value, hoy, source=None, state=None):
"""Set both total and direct values for a specific hour of the year.
Args:
value: Value as as tuples (total, direct).
hoy: The hour of the year that corresponds to this value.
source: Name of the source of light. Only needed in case of multiple
sources / window groups (default: None).
state: State of the source if any (default: None).
"""
sid, stateid = self._create_data_structure(source, state)
if hoy is None:
return
try:
self._values[sid][stateid][int(hoy * 60)] = value[0], value[1]
except TypeError:
raise ValueError(
"Wrong input: {}. Input values must be of length of 2.".format(value)
)
except IndexError:
raise ValueError(
"Wrong input: {}. Input values must be of length of 2.".format(value)
)
else:
self._is_directLoaded = True
[docs] def set_coupled_values(self, values, hoys, source=None, state=None):
"""Set total and direct values for several hours of the year.
Args:
values: List of values as tuples (total, direct).
hoys: List of hours of the year that corresponds to input values.
source: Name of the source of light. Only needed in case of multiple
sources / window groups (default: None).
state: State of the source if any (default: None).
"""
if not (isinstance(values, types.GeneratorType) or
isinstance(hoys, types.GeneratorType)):
assert len(values) == len(hoys), \
ValueError(
'Length of values [%d] is not equal to length of hoys [%d].'
% (len(values), len(hoys)))
sid, stateid = self._create_data_structure(source, state)
for hoy, value in zip(hoys, values):
if hoy is None:
continue
try:
self._values[sid][stateid][int(hoy * 60)] = value[0], value[1]
except TypeError:
raise ValueError(
"Wrong input: {}. Input values must be of length of 2.".format(value)
)
except IndexError:
raise ValueError(
"Wrong input: {}. Input values must be of length of 2.".format(value)
)
self._is_directLoaded = True
[docs] def value(self, hoy, source=None, state=None):
"""Get total value for an hour of the year."""
# find the id for source and state
sid = self.source_id(source)
# find the state id
stateid = self.blind_state_id(source, state)
if int(hoy * 60) not in self._values[sid][stateid]:
raise ValueError('Hourly values are not available for {}.'
.format(dt.DateTime.from_hoy(hoy)))
return self._values[sid][stateid][int(hoy * 60)][0]
[docs] def direct_value(self, hoy, source=None, state=None):
"""Get direct value for an hour of the year."""
# find the id for source and state
sid = self.source_id(source)
# find the state id
stateid = self.blind_state_id(source, state)
if int(hoy * 60) not in self._values[sid][stateid]:
raise ValueError('Hourly values are not available for {}.'
.format(dt.DateTime.from_hoy(hoy)))
return self._values[sid][stateid][int(hoy * 60)][1]
[docs] def values(self, hoys=None, source=None, state=None):
"""Get values for several hours of the year."""
# find the id for source and state
sid = self.source_id(source)
# find the state id
stateid = self.blind_state_id(source, state)
hoys = hoys or self.hoys
for hoy in hoys:
if int(hoy * 60) not in self._values[sid][stateid]:
raise ValueError('Hourly values are not available for {}.'
.format(dt.DateTime.from_hoy(hoy)))
return tuple(self._values[sid][stateid][int(hoy * 60)][0] for hoy in hoys)
[docs] def direct_values(self, hoys=None, source=None, state=None):
"""Get direct values for several hours of the year."""
# find the id for source and state
sid = self.source_id(source)
# find the state id
stateid = self.blind_state_id(source, state)
hoys = hoys or self.hoys
for hoy in hoys:
if int(hoy * 60) not in self._values[sid][stateid]:
raise ValueError('Hourly values are not available for {}.'
.format(dt.DateTime.from_hoy(hoy)))
return tuple(self._values[sid][stateid][int(hoy * 60)][1] for hoy in hoys)
[docs] def coupled_value(self, hoy, source=None, state=None):
"""Get total and direct values for an hoy."""
# find the id for source and state
sid = self.source_id(source)
# find the state id
stateid = self.blind_state_id(source, state)
if int(hoy * 60) not in self._values[sid][stateid]:
raise ValueError('Hourly values are not available for {}.'
.format(dt.DateTime.from_hoy(hoy)))
return self._values[sid][stateid][int(hoy * 60)]
[docs] def coupled_values(self, hoys=None, source=None, state=None):
"""Get total and direct values for several hours of year."""
# find the id for source and state
sid = self.source_id(source)
# find the state id
stateid = self.blind_state_id(source, state)
hoys = hoys or self.hoys
for hoy in hoys:
if int(hoy * 60) not in self._values[sid][stateid]:
raise ValueError('Hourly values are not available for {}.'
.format(dt.DateTime.from_hoy(hoy)))
return tuple(self._values[sid][stateid][int(hoy * 60)] for hoy in hoys)
[docs] def coupled_value_by_id(self, hoy, source_id=None, state_id=None):
"""Get total and direct values for an hoy."""
# find the id for source and state
sid = source_id or 0
# find the state id
stateid = state_id or 0
if int(hoy * 60) not in self._values[sid][stateid]:
raise ValueError('Hourly values are not available for {}.'
.format(dt.DateTime.from_hoy(hoy)))
return self._values[sid][stateid][int(hoy * 60)]
[docs] def coupled_values_by_id(self, hoys=None, source_id=None, state_id=None):
"""Get total and direct values for several hours of year by source id.
Use this method to load the values if you have the ids for source and state.
Args:
hoys: A collection of hoys.
source_id: Id of source as an integer (default: 0).
state_id: Id of state as an integer (default: 0).
"""
sid = source_id or 0
stateid = state_id or 0
hoys = hoys or self.hoys
for hoy in hoys:
if int(hoy * 60) not in self._values[sid][stateid]:
raise ValueError('Hourly values are not available for {}.'
.format(dt.DateTime.from_hoy(hoy)))
return tuple(self._values[sid][stateid][int(hoy * 60)] for hoy in hoys)
[docs] def combined_value_by_id(self, hoy, blinds_state_ids=None):
"""Get combined value from all sources based on state_id.
Args:
hoy: hour of the year.
blinds_state_ids: List of state ids for all the sources for an hour. If you
want a source to be removed set the state to -1.
Returns:
total, direct values.
"""
total = 0
direct = 0 if self._is_directLoaded else None
if not blinds_state_ids:
blinds_state_ids = [0] * len(self._sources)
assert len(self._sources) == len(blinds_state_ids), \
'There should be a state for each source. #sources[{}] != #states[{}]' \
.format(len(self._sources), len(blinds_state_ids))
for sid, stateid in enumerate(blinds_state_ids):
if stateid == -1:
t = 0
d = 0
else:
if int(hoy * 60) not in self._values[sid][stateid]:
raise ValueError('Hourly values are not available for {}.'
.format(dt.DateTime.from_hoy(hoy)))
t, d = self._values[sid][stateid][int(hoy * 60)]
try:
total += t
direct += d
except TypeError:
# direct value is None
pass
return total, direct
[docs] def combined_values_by_id(self, hoys=None, blinds_state_ids=None):
"""Get combined value from all sources based on state_id.
Args:
hoys: A collection of hours of the year.
blinds_state_ids: List of state ids for all the sources for input hoys. If
you want a source to be removed set the state to -1.
Returns:
Return a generator for (total, direct) values.
"""
hoys = hoys or self.hoys
if not blinds_state_ids:
try:
hours_count = len(hoys)
except TypeError:
raise TypeError('hoys must be an iterable object: {}'.format(hoys))
blinds_state_ids = [[0] * len(self._sources)] * hours_count
assert len(hoys) == len(blinds_state_ids), \
'There should be a list of states for each hour. #states[{}] != #hours[{}]' \
.format(len(blinds_state_ids), len(hoys))
dir_value = 0 if self._is_directLoaded else None
for count, hoy in enumerate(hoys):
total = 0
direct = dir_value
for sid, stateid in enumerate(blinds_state_ids[count]):
if stateid == -1:
t = 0
d = 0
else:
if int(hoy * 60) not in self._values[sid][stateid]:
raise ValueError('Hourly values are not available for {}.'
.format(dt.DateTime.from_hoy(hoy)))
t, d = self._values[sid][stateid][int(hoy * 60)]
try:
total += t
direct += d
except TypeError:
# direct value is None
pass
yield total, direct
[docs] def sum_values_by_id(self, hoys=None, blinds_state_ids=None):
"""Get sum of value for all the hours.
This method is mostly useful for radiation and solar access analysis.
Args:
hoys: A collection of hours of the year.
blinds_state_ids: List of state ids for all the sources for input hoys. If
you want a source to be removed set the state to -1.
Returns:
Return a tuple for sum of (total, direct) values.
"""
values = tuple(self.combined_values_by_id(hoys, blinds_state_ids))
total = sum(v[0] for v in values)
try:
direct = sum(v[1] for v in values)
except TypeError as e:
if "'long' and 'NoneType'" in str(e):
# direct value is not loaded
direct = 0
else:
raise TypeError(e)
return total, direct
[docs] def max_values_by_id(self, hoys=None, blinds_state_ids=None):
"""Get maximum value for all the hours.
Args:
hoys: A collection of hours of the year.
blinds_state_ids: List of state ids for all the sources for input hoys. If
you want a source to be removed set the state to -1.
Returns:
Return a tuple for sum of (total, direct) values.
"""
values = tuple(self.combined_values_by_id(hoys, blinds_state_ids))
total = max(v[0] for v in values)
direct = max(v[1] for v in values)
return total, direct
[docs] def blinds_state(self, hoys=None, blinds_state_ids=None, *args, **kwargs):
"""Calculte blinds state based on a control logic.
Overwrite self.logic to overwrite the logic for this point.
Args:
hoys: List of hours of year. If None default is self.hoys.
blinds_state_ids: List of state ids for all the sources for an hour. If you
want a source to be removed set the state to -1. If not provided
a longest combination of states from sources (window groups) will
be used. Length of each item in states should be equal to number
of sources.
args: Additional inputs for self.logic. args will be passed to self.logic
kwargs: Additional inputs for self.logic. kwargs will be passed to self.logic
"""
hoys = hoys or self.hoys
if blinds_state_ids:
# recreate the states in case the inputs are the names of the states
# and not the numbers.
sources = self.sources
comb_ids = copy.deepcopy(blinds_state_ids)
# find state ids for each state if inputs are state names
try:
for c, comb in enumerate(comb_ids):
for count, source in enumerate(sources):
comb_ids[c][count] = self.blind_state_id(source, comb[count])
except IndexError:
raise ValueError(
'Length of each state should be equal to number of sources: {}'
.format(len(sources))
)
else:
comb_ids = self.longest_state_ids
print("Blinds combinations:\n{}".format(
'\n'.join(str(ids) for ids in comb_ids)))
# collect the results for each combination
results = list(range(len(comb_ids)))
for count, state in enumerate(comb_ids):
results[count] = tuple(self.combined_values_by_id(hoys, [state] * len(hoys)))
# assume the last state happens for all
hours_count = len(hoys)
blinds_index = [len(comb_ids) - 1] * hours_count
ill_values = [None] * hours_count
dir_values = [None] * hours_count
success = [0] * hours_count
for count, h in enumerate(hoys):
for state in range(len(comb_ids)):
ill, ill_dir = results[state][count]
if not self.logic(ill, ill_dir, h, args, kwargs):
blinds_index[count] = state
ill_values[count] = ill
dir_values[count] = ill_dir
if state > 0:
success[count] = 1
break
else:
success[count] = -1
ill_values[count] = ill
dir_values[count] = ill_dir
blinds_state = tuple(comb_ids[ids] for ids in blinds_index)
return blinds_state, blinds_index, ill_values, dir_values, success
[docs] def annual_metrics(self, da_threshhold=None, udi_min_max=None, blinds_state_ids=None,
occ_schedule=None):
"""Calculate annual metrics.
Daylight autonomy, continious daylight autonomy and useful daylight illuminance.
Args:
da_threshhold: Threshhold for daylight autonomy in lux (default: 300).
udi_min_max: A tuple of min, max value for useful daylight illuminance
(default: (100, 2000)).
blinds_state_ids: List of state ids for all the sources for input hoys. If
you want a source to be removed set the state to -1.
occ_schedule: An annual occupancy schedule (default: Office Schedule).
Returns:
Daylight autonomy, Continuous daylight autonomy, Useful daylight illuminance,
Less than UDI, More than UDI
"""
hours = self.hoys
values = tuple(v[0] for v in self.combined_values_by_id(hours, blinds_state_ids))
return self._calculate_annual_metrics(
values, hours, da_threshhold, udi_min_max, blinds_state_ids, occ_schedule)
[docs] def useful_daylight_illuminance(self, udi_min_max=None, blinds_state_ids=None,
occ_schedule=None):
"""Calculate useful daylight illuminance.
Args:
udi_min_max: A tuple of min, max value for useful daylight illuminance
(default: (100, 2000)).
blinds_state_ids: List of state ids for all the sources for input hoys. If
you want a source to be removed set the state to -1.
occ_schedule: An annual occupancy schedule.
Returns:
Useful daylight illuminance, Less than UDI, More than UDI
"""
udi_min_max = udi_min_max or (100, 2000)
udiMin, udiMax = udi_min_max
hours = self.hoys
schedule = occ_schedule or Schedule.eight_am_to_six_pm()
udi = 0
udi_l = 0
udi_m = 0
total_hour_count = len(hours)
values = tuple(v[0] for v in self.combined_values_by_id(hours, blinds_state_ids))
for h, v in zip(hours, values):
if h not in schedule:
total_hour_count -= 1
continue
if v < udiMin:
udi_l += 1
elif v > udiMax:
udi_m += 1
else:
udi += 1
if total_hour_count == 0:
raise ValueError('There is 0 hours available in the schedule.')
return 100 * udi / total_hour_count, 100 * udi_l / total_hour_count, \
100 * udi_m / total_hour_count
[docs] def daylight_autonomy(self, da_threshhold=None, blinds_state_ids=None,
occ_schedule=None):
"""Calculate daylight autonomy and continuous daylight autonomy.
Args:
da_threshhold: Threshhold for daylight autonomy in lux (default: 300).
blinds_state_ids: List of state ids for all the sources for input hoys. If
you want a source to be removed set the state to -1.
occ_schedule: An annual occupancy schedule.
Returns:
Daylight autonomy, Continuos daylight autonomy
"""
da_threshhold = da_threshhold or 300
hours = self.hoys
schedule = occ_schedule or Schedule.eight_am_to_six_pm()
DA = 0
cda = 0
total_hour_count = len(hours)
values = tuple(v[0] for v in self.combined_values_by_id(hours, blinds_state_ids))
for h, v in zip(hours, values):
if h not in schedule:
total_hour_count -= 1
continue
if v >= da_threshhold:
DA += 1
cda += 1
else:
cda += v / da_threshhold
if total_hour_count == 0:
raise ValueError('There is 0 hours available in the schedule.')
return 100 * DA / total_hour_count, 100 * cda / total_hour_count
[docs] def annual_sunlight_exposure(self, threshhold=None, blinds_state_ids=None,
occ_schedule=None, target_hours=None):
"""Annual Solar Exposure (ASE).
Calculate number of hours that this point is exposed to more than 1000lux
of direct sunlight. The point meets the target in the number of hours is
less than 250 hours per year.
Args:
threshhold: Threshhold for daylight autonomy in lux (default: 1000).
blinds_state_ids: List of state ids for all the sources for input hoys.
If you want a source to be removed set the state to -1. ase must
be calculated without dynamic blinds but you can use this option
to study the effect of different blind states.
occ_schedule: An annual occupancy schedule.
target_hours: Target minimum hours (default: 250).
Returns:
Success as a Boolean, Number of hours, Problematic hours
"""
if not self.has_direct_values:
raise ValueError(
'Direct values are not loaded. Data is not available to calculate ASE.')
hoys = self.hoys
values = tuple(v[1] for v in self.combined_values_by_id(hoys, blinds_state_ids))
return self._calculate_annual_sunlight_exposure(
values, hoys, threshhold, blinds_state_ids, occ_schedule, target_hours)
@staticmethod
def _calculate_annual_sunlight_exposure(
values, hoys, threshhold=None, blinds_state_ids=None, occ_schedule=None,
target_hours=None):
threshhold = threshhold or 1000
target_hours = target_hours or 250
schedule = occ_schedule or Schedule.eight_am_to_six_pm()
ase = 0
problematic_hours = []
for h, v in zip(hoys, values):
if h not in schedule:
continue
if v > threshhold:
ase += 1
problematic_hours.append(h)
return ase < target_hours, ase, problematic_hours
@staticmethod
def _calculate_annual_metrics(
values, hours, da_threshhold=None, udi_min_max=None, blinds_state_ids=None,
occ_schedule=None):
total_hour_count = len(hours)
udiMin, udiMax = udi_min_max
udi_min_max = udi_min_max or (100, 2000)
da_threshhold = da_threshhold or 300.0
schedule = occ_schedule or Schedule.eight_am_to_six_pm()
DA = 0
cda = 0
udi = 0
udi_l = 0
udi_m = 0
for h, v in zip(hours, values):
if h not in schedule:
total_hour_count -= 1
continue
if v >= da_threshhold:
DA += 1
cda += 1
else:
cda += v / da_threshhold
if v < udiMin:
udi_l += 1
elif v > udiMax:
udi_m += 1
else:
udi += 1
if total_hour_count == 0:
raise ValueError('There is 0 hours available in the schedule.')
return 100 * DA / total_hour_count, 100 * cda / total_hour_count, \
100 * udi / total_hour_count, 100 * udi_l / total_hour_count, \
100 * udi_m / total_hour_count
@staticmethod
def _calculate_daylight_autonomy(
values, hoys, da_threshhold=None, blinds_state_ids=None, occ_schedule=None):
"""Calculate daylight autonomy and continuous daylight autonomy.
Args:
da_threshhold: Threshhold for daylight autonomy in lux (default: 300).
blinds_state_ids: List of state ids for all the sources for input hoys. If
you want a source to be removed set the state to -1.
occ_schedule: An annual occupancy schedule.
Returns:
Daylight autonomy, Continuous daylight autonomy
"""
da_threshhold = da_threshhold or 300
hours = hoys
schedule = occ_schedule or Schedule.eight_am_to_six_pm()
DA = 0
cda = 0
total_hour_count = len(hours)
for h, v in zip(hours, values):
if h not in schedule:
total_hour_count -= 1
continue
if v >= da_threshhold:
DA += 1
cda += 1
else:
cda += v / da_threshhold
if total_hour_count == 0:
raise ValueError('There is 0 hours available in the schedule.')
return 100 * DA / total_hour_count, 100 * cda / total_hour_count
[docs] @staticmethod
def parse_blind_states(blinds_state_ids):
"""Parse input blind states.
The method tries to convert each state to a tuple of a list. Use this method
to parse the input from plugins.
Args:
blinds_state_ids: List of state ids for all the sources for an hour. If you
want a source to be removed set the state to -1. If not provided
a longest combination of states from sources (window groups) will
be used. Length of each item in states should be equal to number
of sources.
"""
try:
combs = [list(eval(cc)) for cc in blinds_state_ids]
except Exception as e:
ValueError('Failed to convert input blind states:\n{}'.format(e))
return combs
[docs] def unload(self):
"""Unload values and sources."""
self._values = []
self._sources = OrderedDict()
[docs] def duplicate(self):
"""Duplicate the analysis point."""
ap = AnalysisPoint(self._loc, self._dir)
# This should be good enough as most of the time an analysis point will be
# copied with no values assigned.
ap._values = copy.copy(self._values)
if len(ap._values) == len(self._sources):
ap._sources = self._sources
ap._is_directLoaded = bool(self._is_directLoaded)
ap.logic = copy.copy(self.logic)
return ap
[docs] def ToString(self):
"""Overwrite .NET ToString."""
return self.__repr__()
[docs] def to_rad_string(self):
"""Return Radiance string for a test point."""
return "%s %s" % (self.location, self.direction)
[docs] def to_json(self):
"""Create an analysis point from json object.
{"location": [x, y, z], "direction": [x, y, z]}
"""
return {"location": tuple(self.location),
"direction": tuple(self.direction),
"values": self._values}
def __repr__(self):
"""Print an analysis point."""
return 'AnalysisPoint::(%s)::(%s)' % (self.location, self.direction)