# coding=utf-8
"""honeybee-doe2 schedule translators."""
from __future__ import division
import os
import re
from ladybug.dt import Date, MONTHNAMES
from ladybug.analysisperiod import AnalysisPeriod
from honeybee.typing import clean_doe2_string, clean_ep_string
from honeybee_energy.schedule.day import ScheduleDay
from honeybee_energy.schedule.rule import ScheduleRule
from honeybee_energy.schedule.ruleset import ScheduleRuleset
from honeybee_energy.lib.scheduletypelimits import fractional, on_off, temperature
from .config import RES_CHARS
from .util import generate_inp_string, generate_inp_string_list_format, \
clean_inp_file_contents, parse_inp_string, _get_lib_content, \
parse_lib_string
"""____________TRANSLATORS FROM HONEYBEE TO INP____________"""
[docs]
def schedule_type_limit_to_inp(type_limit):
"""Get the DOE-2 type for a honeybee-energy ScheduleTypeLimit."""
if type_limit is None:
return 'FRACTION'
elif type_limit.display_name == 'Multiplier':
return "MULTIPLIER"
elif type_limit.unit_type == 'Temperature':
return 'TEMPERATURE'
else:
if type_limit.numeric_type == 'Discrete':
return 'ON/OFF'
else:
return 'FRACTION'
[docs]
def schedule_day_to_inp(day_schedule, type_limit=None):
"""Convert a ScheduleDay into a DAY-SCHEDULE INP string."""
# setup the DOE-2 identifier and lists for keywords and values
doe2_id = clean_doe2_string(day_schedule.identifier, RES_CHARS)
type_text = schedule_type_limit_to_inp(type_limit)
keywords, values = ['TYPE'], [type_text]
hour_values = day_schedule.values_at_timestep(1)
# convert temperature to fahrenheit if the type if temperature
if type_text == 'TEMPERATURE':
hour_values = [round(v * (9. / 5.) + 32., 2) for v in hour_values]
# setup a function to format list of values correctly
def _format_day_values(values_to_format):
if len(values_to_format) == 1:
return '({})'.format(round(values_to_format[0], 3))
elif len(values_to_format) < 5:
return str(tuple(round(v, 3) for v in values_to_format))
else: # we have to format it with multiple lines
spc = ' ' * 31
full_str = '('
for i, v in enumerate(values_to_format):
if i == len(values_to_format) - 1:
full_str += str(round(v, 3)) + ')'
elif (i + 1) % 5 == 0:
full_str += str(round(v, 3)) + ',\n' + spc
else:
full_str += str(round(v, 3)) + ', '
return full_str
# loop through the hourly values and write them in the format DOE-2 likes
prev_count, prev_hour, prev_values = 0, 1, [hour_values[0]]
for i, val in enumerate(hour_values):
if i == 0:
continue # ignore the first value already in the list
if val == prev_values[-1]:
prev_count += 1
if len(prev_values) > 1:
keywords.append('HOURS')
if prev_hour != i - 1:
values.append('({}, {})'.format(prev_hour, i - 1))
keywords.append('VALUES')
values.append(_format_day_values(prev_values[:-1]))
prev_values = [prev_values[-1]]
prev_hour = i
else:
values.append('({}, {})'.format(prev_hour, i))
keywords.append('VALUES')
values.append(_format_day_values(prev_values))
prev_values = [prev_values[-1]]
prev_hour = i + 1
continue # wait for the value to change
if prev_count == 0: # just keep assembling the list of values
prev_values.append(val)
else:
keywords.append('HOURS')
values.append('({}, {})'.format(prev_hour, i))
keywords.append('VALUES')
values.append(_format_day_values(prev_values))
prev_values = [val]
prev_hour = i + 1
prev_count = 0
keywords.append('HOURS')
values.append('({}, {})'.format(prev_hour, 24))
keywords.append('VALUES')
values.append(_format_day_values(prev_values))
# return the INP string
return generate_inp_string(doe2_id, 'DAY-SCHEDULE', keywords, values)
[docs]
def schedule_ruleset_to_inp(schedule):
"""Convert a ScheduleRuleset into a WEEK-SCHEDULE-PD and SCHEDULE INP strings.
Note that this method only outputs SCHEDULE and WEEK-SCHEDULE objects
However, to write the full schedule into an INP, the schedules's
day_schedules must also be written.
Returns:
A tuple with two elements
- year_schedule: Text string representation of the SCHEDULE
describing this schedule.
- week_schedules: A list of WEEK-SCHEDULE-PD text strings that are
referenced in the year_schedule.
"""
# setup the DOE-2 identifier and lists for keywords and values
doe2_id = clean_doe2_string(schedule.identifier, RES_CHARS)
type_text = schedule_type_limit_to_inp(schedule.schedule_type_limit)
day_types = [
'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday',
'Sunday', 'Holiday', 'Winter Design Day', 'Summer Design Day'
]
def _get_week_list(schedule, rule_indices):
"""Get a list of the ScheduleDay identifiers applied on each day of the week."""
week_list = []
for dow in range(7):
for i in rule_indices:
if schedule._schedule_rules[i].week_apply_tuple[dow]:
day_sch_id = schedule._schedule_rules[i].schedule_day.identifier
week_list.append(clean_doe2_string(day_sch_id, RES_CHARS))
break
else: # no rule applies; use default_day_schedule.
day_sch_id = schedule.default_day_schedule.identifier
week_list.append(clean_doe2_string(day_sch_id, RES_CHARS))
week_list.append(week_list.pop(0)) # DOE-2 starts week on Monday; not Sunday
return week_list
def _get_extra_week_fields(schedule):
"""Get schedule identifiers of extra days in Schedule:Week."""
# add summer and winter design days
week_fields = []
if schedule._holiday_schedule is not None:
day_sch_id = schedule._holiday_schedule.identifier
week_fields.append(clean_doe2_string(day_sch_id, RES_CHARS))
else:
day_sch_id = schedule._default_day_schedule.identifier
week_fields.append(clean_doe2_string(day_sch_id, RES_CHARS))
if schedule._winter_designday_schedule is not None:
day_sch_id = schedule._winter_designday_schedule.identifier
week_fields.append(clean_doe2_string(day_sch_id, RES_CHARS))
else:
day_sch_id = schedule._default_day_schedule.identifier
week_fields.append(clean_doe2_string(day_sch_id, RES_CHARS))
if schedule._summer_designday_schedule is not None:
day_sch_id = schedule._summer_designday_schedule.identifier
week_fields.append(clean_doe2_string(day_sch_id, RES_CHARS))
else:
day_sch_id = schedule._default_day_schedule.identifier
week_fields.append(clean_doe2_string(day_sch_id, RES_CHARS))
return week_fields
def _inp_week_schedule_from_rule_indices(schedule, rule_indices, week_index):
"""Create an INP string of a week schedule from a list of rules indices."""
week_sch_id = '{}_Week {}'.format(schedule.identifier, week_index)
week_sch_id = clean_doe2_string(week_sch_id, RES_CHARS)
week_fields = []
# check rules that apply for the days of the week
week_fields.extend(_get_week_list(schedule, rule_indices))
# add extra days (including summer and winter design days)
week_fields.extend(_get_extra_week_fields(schedule))
week_keywords, week_values = ['TYPE'], [type_text]
day_list = []
for day_type, day_sch in zip(day_types, week_fields):
day_list.append('"{}", $ {}'.format(day_sch, day_type))
week_keywords.append('DAY-SCHEDULES')
week_values.append(day_list)
week_schedule = generate_inp_string_list_format(
week_sch_id, 'WEEK-SCHEDULE-PD', week_keywords, week_values)
return week_schedule, week_sch_id
def _inp_week_schedule_from_week_list(schedule, week_list, week_index):
"""Create an INP string of a week schedule from a list ScheduleDay identifiers.
"""
week_sch_id = '{}_Week {}'.format(schedule.identifier, week_index)
week_sch_id = clean_doe2_string(week_sch_id, RES_CHARS)
week_fields = list(week_list)
week_fields.append(week_fields.pop(0)) # DOE-2 starts week on Monday; not Sunday
week_fields.extend(_get_extra_week_fields(schedule))
week_keywords, week_values = ['TYPE'], [type_text]
day_list = []
for day_type, day_sch in zip(day_types, week_fields):
day_list.append('"{}", $ {}'.format(day_sch, day_type))
week_keywords.append('DAY-SCHEDULES')
week_values.append(day_list)
week_schedule = generate_inp_string_list_format(
week_sch_id, 'WEEK-SCHEDULE-PD', week_keywords, week_values)
return week_schedule, week_sch_id
# prepare to create a full Schedule:Year
week_schedules = []
if schedule.is_single_week: # create the only one week schedule
wk_sch, wk_sch_id = \
_inp_week_schedule_from_rule_indices(schedule, range(len(schedule)), 1)
week_schedules.append(wk_sch)
yr_wk_s_ids = [wk_sch_id]
yr_wk_dt_range = [[Date(1, 1), Date(12, 31)]]
else: # create a set of week schedules throughout the year
# loop through 365 days of the year to find unique combinations of rules
rules_each_day = []
for doy in range(1, 366):
rules_on_doy = tuple(i for i, rule in enumerate(schedule._schedule_rules)
if rule.does_rule_apply_doy(doy))
rules_each_day.append(rules_on_doy)
unique_rule_sets = set(rules_each_day)
# check if any combination yield the same week schedule and remove duplicates
week_tuples = [tuple(_get_week_list(schedule, rule_set))
for rule_set in unique_rule_sets]
unique_week_tuples = list(set(week_tuples))
# create the unique week schedules from the combinations of rules
week_sched_ids = []
for i, week_list in enumerate(unique_week_tuples):
wk_schedule, wk_sch_id = \
_inp_week_schedule_from_week_list(schedule, week_list, i + 1)
week_schedules.append(wk_schedule)
week_sched_ids.append(wk_sch_id)
# create a dictionary mapping unique rule index lists to week schedule ids
rule_set_map = {}
for rule_i, week_list in zip(unique_rule_sets, week_tuples):
unique_week_i = unique_week_tuples.index(week_list)
rule_set_map[rule_i] = week_sched_ids[unique_week_i]
# loop through all 365 days of the year to find when rules change
yr_wk_s_ids = []
yr_wk_dt_range = []
prev_week_sched = None
for doy in range(1, 366):
week_sched = rule_set_map[rules_each_day[doy - 1]]
if week_sched != prev_week_sched: # change to a new rule set
yr_wk_s_ids.append(week_sched)
if doy != 1:
yr_wk_dt_range[-1].append(Date.from_doy(doy - 1))
yr_wk_dt_range.append([Date.from_doy(doy)])
else:
yr_wk_dt_range.append([Date(1, 1)])
prev_week_sched = week_sched
yr_wk_dt_range[-1].append(Date(12, 31))
# create the year fields
keywords, values = ['TYPE'], [type_text]
for i, (wk_sch_id, dt_range) in enumerate(zip(yr_wk_s_ids, yr_wk_dt_range)):
end_date = dt_range[1]
thru = 'THRU {} {}'.format(MONTHNAMES[end_date.month - 1].upper(), end_date.day)
keywords.append(thru)
values.append('"{}"'.format(wk_sch_id))
year_schedule = generate_inp_string(doe2_id, 'SCHEDULE', keywords, values)
return year_schedule, week_schedules
[docs]
def schedule_fixed_interval_to_inp(schedule):
"""Convert a ScheduleFixedInterval to INP strings.
Note that true Fixed Interval schedules are not supported by DOE-2 and there
is no way to faithfully translate them given that DOE-2 SCHEDULE objects have
a hard limit of 12 THRU statements. This method tries to write as best of
an approximation for the schedule as possible by averaging the hourly values
from each day of the fixed interval schedule. A separate day schedule will
be used for each month in an attempt to account for changes in the fixed
interval schedule over the year.
All of this will allow the translation to succeed and gives roughly matching
behavior of the DOE-2 simulation to the EnergyPlus simulation. However, it is
recommended that users replace ScheduleFixedIntervals with ScheduleRulesets
that they know best represents the schedule. Or EnergyPlus should be used for
the simulation instead of DOE-2.
Returns:
A tuple with three elements
- year_schedule: Text string representation of the SCHEDULE
describing this schedule.
- week_schedules: A list of WEEK-SCHEDULE text strings that are
referenced in the year_schedule.
- day_schedules: A list of DAY-SCHEDULE-PD text strings that are
referenced in the week_schedules.
"""
# setup the DOE-2 identifier and lists for keywords and values
doe2_id = clean_doe2_string(schedule.identifier, RES_CHARS)
base_id = clean_doe2_string(schedule.identifier, RES_CHARS - 6)
type_text = schedule_type_limit_to_inp(schedule.schedule_type_limit)
# loop through the months of the year and create appropriate schedules
day_schedules, week_schedules = [], []
year_keywords, year_values = ['TYPE'], [type_text]
sch_data = schedule.data_collection
if sch_data.header.analysis_period.timestep != 1:
sch_data = sch_data.cull_to_timestep(1)
for month_i in range(1, 13):
# create the day schedules
month_name = AnalysisPeriod.MONTHNAMES[month_i]
month_days = AnalysisPeriod.NUMOFDAYSEACHMONTH[month_i - 1]
week_id = '{}{}'.format(base_id, month_name)
day_id = '{}{}'.format(week_id, 'Day')
period = AnalysisPeriod(st_month=month_i, end_month=month_i, end_day=month_days)
month_data = sch_data.filter_by_analysis_period(period)
mon_per_hr = month_data.average_monthly_per_hour()
hour_values = [round(v, 3) for v in mon_per_hr.values]
if type_text == 'TEMPERATURE':
hour_values = [round(v * (9. / 5.) + 32., 2) for v in hour_values]
day_keywords, day_values = ['TYPE', 'VALUES'], [type_text, hour_values]
day_inp_str = generate_inp_string_list_format(
day_id, 'DAY-SCHEDULE-PD', day_keywords, day_values)
day_schedules.append(day_inp_str)
# create week schedule
week_keywords = ['TYPE', 'DAYS', 'DAY-SCHEDULES']
week_values = [type_text, '(ALL)', '("{}")'.format(day_id)]
week_sch = generate_inp_string(
week_id, 'WEEK-SCHEDULE', week_keywords, week_values)
week_schedules.append(week_sch)
# add values to the year schedules
thru = 'THRU {} {}'.format(month_name.upper(), period.end_day)
year_keywords.append(thru)
year_values.append('"{}"'.format(week_id))
# return all of the strings
year_schedule = generate_inp_string(doe2_id, 'SCHEDULE', year_keywords, year_values)
return year_schedule, week_schedules, day_schedules
"""____________TRANSLATORS FROM INP TO HONEYBEE____________"""
[docs]
def schedule_type_limit_from_inp(inp_type_string):
"""Get a honeybee-energy ScheduleTypeLimit for a given DOE-2 schedule type."""
clean_type = inp_type_string.strip().upper()
if clean_type == 'ON/OFF':
return on_off
elif clean_type == 'TEMPERATURE':
return temperature
else:
return fractional
[docs]
def schedule_day_from_inp(inp_string):
"""Create a Honeybee ScheduleDay from a DOE-2 INP text string.
Note that this method can accept both types of DOE-2 Day Schedules
(DAY-SCHEDULE, DAY-SCHEDULE-PD).
Args:
inp_string: A text string fully describing a DOE-2 DAY-SCHEDULE.
"""
# parse the string into properties
u_name, command, keywords, values = parse_inp_string(inp_string)
# extract the hourly values of the schedule
hour_vals, sch_type = [], 'FRACTIONAL'
if command.upper() == 'DAY-SCHEDULE-PD':
field_dict = {k: v for k, v in zip(keywords, values)}
sch_type = field_dict['TYPE'].upper()
values_str = field_dict['VALUES'].replace('&D', '"&D"')
# Add commas between numbers that are seperated by a space
# In DOE it is still valid to have just a space no comma
values_str = re.sub(r'(\d)\s+(\d)', r'\1, \2', values_str)
hour_vals_init = eval(values_str, {})
if isinstance(hour_vals_init, tuple):
for val in hour_vals_init:
if val == '&D':
hour_vals.append(hour_vals[-1])
else:
hour_vals.append(float(val))
if len(hour_vals) < 24:
for _ in range(24 - len(hour_vals)):
hour_vals.append(hour_vals[-1])
else: # a constant schedule
hour_vals = [hour_vals_init] * 24
elif command.upper() == 'DAY-SCHEDULE':
prev_count = 0
for key, val in zip(keywords, values):
if key == 'HOURS':
hr_range = eval(val, {})
prev_count = hr_range[-1] - hr_range[0] + 1
elif key == 'VALUES':
hr_vals = eval(val, {})
if isinstance(hr_vals, tuple):
hour_vals.extend(hr_vals)
else:
hour_vals.extend([hr_vals] * prev_count)
elif key == 'TYPE':
sch_type = val.upper()
else:
raise ValueError('Schedule type "{}" is not recognized.'.format(command))
# convert temperature values from F to C if type is TEMPERATURE
if sch_type == 'TEMPERATURE':
hour_vals = [round((v - 32.) * (5. / 9.), 2) for v in hour_vals]
return ScheduleDay.from_values_at_timestep(clean_ep_string(u_name), hour_vals)
def _inp_day_schedule_dictionary(day_inp_strings):
"""Get a dictionary of DaySchedule objects from an INP string list."""
day_schedule_dict = {}
for sch_str in day_inp_strings:
sch_str = sch_str.strip()
try:
sch_obj = schedule_day_from_inp(sch_str)
day_schedule_dict[sch_obj.identifier] = sch_obj
except Exception:
pass # not a schedule that can be converted
return day_schedule_dict
def _inp_week_schedule_dictionary(week_inp_strings, day_sch_dict):
"""Get a dictionary of ScheduleRule objects from WEEK-SCHEDULE-PD strings."""
week_schedule_dict = {}
week_designday_dict = {}
for sch_str in week_inp_strings:
sch_str = sch_str.strip()
try:
u_name, rules, holiday, winter_dd, summer_dd = \
extract_all_rules_from_inp_schedule_week(sch_str, day_sch_dict)
week_schedule_dict[u_name] = rules
week_designday_dict[u_name] = [
day_sch_dict[clean_ep_string(holiday)],
day_sch_dict[clean_ep_string(summer_dd)],
day_sch_dict[clean_ep_string(winter_dd)]
]
except Exception:
pass # schedule is not translate-able
return week_schedule_dict, week_designday_dict
def _convert_schedule_year(year_inp_string, week_sch_dict, week_dd_dict):
"""Convert an INP string of a year SCHEDULE or SCHEDULE-PD to a ScheduleRuleset.
Args:
year_inp_string: An INP text string describing a DOE-2 SCHEDULE or SCHEDULE-PD.
week_sch_dict: A dictionary of ScheduleRules from _inp_week_schedule_dictionary.
week_dd_dict: A dictionary of design day ScheduleDay output from the
_inp_week_schedule_dictionary method.
"""
# use the year schedule to bring it all together
u_name, command, keywords, values = parse_inp_string(year_inp_string)
field_dict = {k: v for k, v in zip(keywords, values)}
schedule_type = schedule_type_limit_from_inp(field_dict['TYPE'])
all_rules = []
if command.upper() == 'SCHEDULE-PD':
week_vals = eval(field_dict['WEEK-SCHEDULES'], {})
if not isinstance(week_vals, tuple): # only one week for the whole year
week_id = week_vals.replace('"', '')
rules = week_sch_dict[week_id]
all_rules.extend(rules)
else:
month_vals = eval(field_dict['MONTH'], {})
day_vals = eval(field_dict['DAY'], {})
prev_month, prev_day = 1, 1
for month, day, week in zip(month_vals, day_vals, week_vals):
week_id = week.replace('"', '')
rules = week_sch_dict[week_id]
st_date = Date(int(prev_month), int(prev_day))
end_date = Date(int(month), int(day))
for rule in rules:
rule.start_date = st_date
rule.end_date = end_date
all_rules.extend(rules)
end_doy = end_date.doy + 1 if end_date.doy != 365 else 365
next_day = Date.from_doy(end_doy)
prev_month, prev_day = next_day.month, next_day.day
elif command.upper() == 'SCHEDULE':
prev_month, prev_day = 1, 1
for key, val in zip(keywords, values):
if key.startswith('THRU'):
week_id = val.replace('"', '')
rules = week_sch_dict[week_id]
st_date = Date(int(prev_month), int(prev_day))
date_vals = key.replace('THRU ', '').split(' ')
date_str = '{} {}'.format(date_vals[1], date_vals[0].title())
end_date = Date.from_date_string(date_str)
for rule in rules:
rule.start_date = st_date
rule.end_date = end_date
all_rules.extend(rules)
end_doy = end_date.doy + 1 if end_date.doy != 365 else 365
next_day = Date.from_doy(end_doy)
prev_month, prev_day = next_day.month, next_day.day
# check to be sure the schedule days don't already have a parent
for rule in all_rules:
if rule.schedule_day._parent is not None:
rule.schedule_day = rule.schedule_day.duplicate()
default_day_schedule = all_rules[0].schedule_day
holiday_sch, summer_dd_sch, winter_dd_sch = week_dd_dict[week_id]
if holiday_sch._parent is not None:
holiday_sch = holiday_sch.duplicate()
if summer_dd_sch._parent is not None:
summer_dd_sch = summer_dd_sch.duplicate()
if winter_dd_sch._parent is not None:
winter_dd_sch = summer_dd_sch.duplicate()
# create the ScheduleRuleset and apply the design days
sched = ScheduleRuleset(clean_ep_string(u_name), default_day_schedule,
all_rules[1:], schedule_type)
ScheduleRuleset._apply_designdays_with_check(
sched, holiday_sch, summer_dd_sch, winter_dd_sch)
return sched
[docs]
def schedule_ruleset_from_inp(year_inp_string, week_inp_strings, day_inp_strings):
"""Create a ScheduleRuleset from a DOE-2 INP text strings.
Args:
year_inp_string: An INP text string describing a DOE-2 SCHEDULE or SCHEDULE-PD.
week_inp_strings: A list of INP text strings for all of the WEEK-SCHEDULE-PD
objects used in the SCHEDULE.
day_inp_strings: A list of text strings for all of the DAY-SCHEDULE or
DAY-SCHEDULE-PD objects used in the week_inp_strings.
"""
# process the schedule components
day_schedule_dict = _inp_day_schedule_dictionary(day_inp_strings)
week_sch_dict, week_dd_dict = _inp_week_schedule_dictionary(
week_inp_strings, day_schedule_dict)
# convert the year_inp_string into a ScheduleRuleset
return _convert_schedule_year(year_inp_string, week_sch_dict, week_dd_dict)
"""______EXTRA UTILITY FUNCTIONS RELATED TO SCHEDULES______"""
[docs]
def energy_trans_sch_to_transmittance(shade_obj):
"""Try to extract the transmittance from the shade energy properties."""
trans = 0
trans_sch = shade_obj.properties.energy.transmittance_schedule
if trans_sch is not None:
if trans_sch.is_constant:
try: # assume ScheduleRuleset
trans = trans_sch.default_day_schedule[0]
except AttributeError: # ScheduleFixedInterval
trans = trans_sch.values[0]
else: # not a constant schedule; use the average transmittance
try: # assume ScheduleRuleset
sch_vals = trans_sch.values()
except Exception: # ScheduleFixedInterval
sch_vals = trans_sch.values
trans = round(sum(sch_vals) / len(sch_vals), 3)
return trans
"""______LIBRARY FILE SCHEDULE RESOLUTION______"""
[docs]
def is_library_entry(schedule_dict):
"""Check if a schedule dict represents a LIBRARY-ENTRY reference.
A LIBRARY-ENTRY schedule will only have '__line__' and 'cmd' keys,
with no other keywords or values in the cmd dict.
Args:
schedule_dict: A dictionary for a schedule from command_dict_from_inp.
Returns:
True if this is a library entry, False otherwise.
"""
return len(schedule_dict) == 2 and 'cmd' in schedule_dict \
and '__line__' in schedule_dict
def _cmd_dict_to_inp_string(name, attrs, command):
"""Convert a cmd_dict entry to an INP string.
Args:
name: The U-name of the object.
attrs: The attributes dict from cmd_dict.
command: The command type (ie 'SPACE' 'SCHEDULE-PD').
Returns:
An INP format string.
"""
keywords = []
values = []
for k, v in attrs.items():
if k in ('__line__', 'cmd'):
continue
keywords.append(k)
values.append(str(v) if not isinstance(v, str) else v)
return generate_inp_string(name, command, keywords, values)
[docs]
def build_schedule_dict(day_schs, wk_schs, annual_schs):
"""Build a dict of ScheduleRuleset objects from cmd_dict schedule entries.
Args:
day_schs: The DAY-SCHEDULE-PD dict from cmd_dict.
wk_schs: The WEEK-SCHEDULE-PD dict from cmd_dict.
annual_schs: The SCHEDULE-PD dict from cmd_dict.
Returns:
A dict mapping schedule names to ScheduleRuleset objects.
"""
schedule_dict = {}
# Convert day schedules to INP strings
day_inp_strings = []
for name, attrs in day_schs.items():
if is_library_entry(attrs):
continue # Skip library entries
inp_str = _cmd_dict_to_inp_string(name, attrs, 'DAY-SCHEDULE-PD')
day_inp_strings.append(inp_str)
# Build day schedule dictionary
day_schedule_dict = _inp_day_schedule_dictionary(day_inp_strings)
# Convert week schedules to INP strings
week_inp_strings = []
for name, attrs in wk_schs.items():
if is_library_entry(attrs):
continue
inp_str = _cmd_dict_to_inp_string(name, attrs, 'WEEK-SCHEDULE-PD')
week_inp_strings.append(inp_str)
# Build week schedule dictionaries
week_sch_dict, week_dd_dict = _inp_week_schedule_dictionary(
week_inp_strings, day_schedule_dict)
# Convert annual schedules to ScheduleRuleset objects
for name, attrs in annual_schs.items():
if is_library_entry(attrs):
continue
try:
inp_str = _cmd_dict_to_inp_string(name, attrs, 'SCHEDULE-PD')
sched = _convert_schedule_year(inp_str, week_sch_dict,
week_dd_dict)
schedule_dict[name] = sched
except Exception:
pass # Schedule not translated
return schedule_dict
[docs]
def resolve_schedule(schedule_name, schedule_dict=None, lib_file_path=None):
"""Resolve a schedule name to a ScheduleRuleset.
Tries the provided schedule_dict first, then falls back to library lookup.
Returns an "Always On" constant schedule if all else fails.
Args:
schedule_name: The name of the schedule to resolve.
schedule_dict: Optional dict of pre-built schedules
(from build_schedule_dict).
lib_file_path: Optional path to eQ_Lib.dat for library lookups.
Returns:
A ScheduleRuleset object. Returns "Always On" if not found.
"""
always_on = ScheduleRuleset.from_constant_value('Always On', 1, fractional)
if not schedule_name:
return always_on
# Clean the schedule name
clean_name = schedule_name.strip()
for ch in ('"', "'", '(', ')'):
clean_name = clean_name.replace(ch, '')
clean_name = clean_name.strip()
if not clean_name or clean_name.lower() == 'always on':
return always_on
# Try the provided schedule dict first
if schedule_dict and clean_name in schedule_dict:
return schedule_dict[clean_name]
# Fall back to the library lookup
lib_sch = resolve_library_schedule(clean_name, lib_file_path)
return lib_sch if lib_sch is not None else always_on
def _lib_block_to_inp(lib_block, command_type):
"""Convert a library block string to INP format string.
Args:
lib_block: Raw library block string starting with $LIBRARY-ENTRY.
command_type: The DOE-2 command type (ie 'SCHEDULE-PD').
Returns:
An INP format string, or None if parsing fails.
"""
try:
u_name, keywords, values = parse_lib_string(lib_block, command_type)
if not keywords:
return None
return generate_inp_string(u_name, command_type, keywords, values)
except Exception:
return None
[docs]
def resolve_library_schedule(schedule_name, lib_file_path=None):
"""Resolve a LIBRARY-ENTRY schedule to a ScheduleRuleset.
This searches the library file for just the requested schedule
and its corresponding week and day schedules.
Args:
schedule_name: The name of the schedule to resolve.
lib_file_path: Path to the eQ_Lib.dat file. If None, will
attempt to find it using find_user_lib_file().
Returns:
A ScheduleRuleset object if resolved, None otherwise.
"""
# Find the library file
if lib_file_path is None:
from .util import find_user_lib_file
lib_file_path = find_user_lib_file()
if lib_file_path is None or not os.path.isfile(lib_file_path):
return None
# Get SCHEDULE-PD blocks
year_blocks = _get_lib_content(lib_file_path, 'SCHEDULE-PD')
# Find the SCHEDULE-PD by name
if schedule_name not in year_blocks:
return None
year_inp_string = _lib_block_to_inp(year_blocks[schedule_name],
'SCHEDULE-PD')
if year_inp_string is None:
return None
# Parse to find referenced week schedules
_, _, keywords, values = parse_inp_string(year_inp_string)
if keywords is None:
return None
field_dict = {k: v for k, v in zip(keywords, values)}
# Collect week schedule names
week_names = set()
if 'WEEK-SCHEDULES' in field_dict:
week_vals = eval(field_dict['WEEK-SCHEDULES'], {})
if isinstance(week_vals, tuple):
for w in week_vals:
week_names.add(w.replace('"', ''))
else:
week_names.add(week_vals.replace('"', ''))
else:
# SCHEDULE format with THRU keywords
for key, val in zip(keywords, values):
if key.startswith('THRU'):
week_names.add(val.replace('"', ''))
# Get WEEK-SCHEDULE-PD blocks
week_blocks = _get_lib_content(lib_file_path, 'WEEK-SCHEDULE-PD')
# Find each WEEK-SCHEDULE-PD and collect day schedule names
day_names = set()
week_inp_strings = []
for wk_name in week_names:
if wk_name not in week_blocks:
continue
wk_inp = _lib_block_to_inp(week_blocks[wk_name], 'WEEK-SCHEDULE-PD')
if wk_inp is None:
continue
week_inp_strings.append(wk_inp)
# Parse to find day schedule references
_, _, wk_keywords, wk_values = parse_inp_string(wk_inp)
if wk_keywords:
wk_field_dict = {k: v for k, v in zip(wk_keywords, wk_values)}
if 'DAY-SCHEDULES' in wk_field_dict:
day_str = wk_field_dict['DAY-SCHEDULES']
day_vals = eval(day_str.replace('&D', '"&D"'), {})
if isinstance(day_vals, tuple):
for d in day_vals:
if d != '&D':
day_names.add(d.replace('"', ''))
else:
day_names.add(day_vals.replace('"', ''))
# Get DAY-SCHEDULE-PD blocks
day_blocks = _get_lib_content(lib_file_path, 'DAY-SCHEDULE-PD')
# Find each DAY-SCHEDULE-PD
day_inp_strings = []
for day_name in day_names:
if day_name not in day_blocks:
continue
day_inp = _lib_block_to_inp(day_blocks[day_name], 'DAY-SCHEDULE-PD')
if day_inp:
day_inp_strings.append(day_inp)
# Create the ScheduleRuleset
try:
return schedule_ruleset_from_inp(
year_inp_string, week_inp_strings, day_inp_strings)
except Exception:
return None