Skip to content

Commit

Permalink
fix(tests): Fix several issues discovered while testing
Browse files Browse the repository at this point in the history
  • Loading branch information
chriswmackey committed Jun 7, 2024
1 parent b68a951 commit 42b7dc4
Show file tree
Hide file tree
Showing 4 changed files with 1,360 additions and 37 deletions.
118 changes: 118 additions & 0 deletions honeybee_doe2/cli/translate.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,14 @@
import logging

from ladybug.futil import write_to_file_by_name
from honeybee.typing import clean_doe2_string
from honeybee.model import Model
from honeybee_energy.schedule.ruleset import ScheduleRuleset
from honeybee_energy.schedule.dictutil import dict_to_schedule

from honeybee_doe2.config import RES_CHARS
from honeybee_doe2.util import header_comment_minor
from honeybee_doe2.schedule import extract_all_schedule_ruleset_from_inp_file
from honeybee_doe2.simulation import SimulationPar

_logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -140,3 +146,115 @@ def hbjson_to_inp_file(
sys.exit(1)
else:
sys.exit(0)


@translate.command('schedules-to-inp')
@click.argument('schedule-json', type=click.Path(
exists=True, file_okay=True, dir_okay=False, resolve_path=True))
@click.option('--output-file', '-f', help='Optional INP file to output the INP '
'string of the translation. By default this will be printed out to stdout',
type=click.File('w'), default='-', show_default=True)
def schedule_to_inp(schedule_json, output_file):
"""Translate a Schedule JSON file to an INP.
\b
Args:
schedule_json: Full path to a Schedule JSON file. This file should
either be an array of non-abridged Schedules or a dictionary where
the values are non-abridged Schedules.
"""
try:
# re-serialize the Schedule to Python
with open(schedule_json) as json_file:
data = json.load(json_file)
sch_list = data.values() if isinstance(data, dict) else data
sch_objs = [dict_to_schedule(sch) for sch in sch_list]
type_objs = set()
for sch in sch_objs:
type_objs.add(sch.schedule_type_limit)

# create the INP strings
all_day_scheds, all_week_scheds, all_year_scheds = [], [], []
used_day_sched_ids, used_day_count = {}, 1
all_scheds = sch_objs
for sched in all_scheds:
if isinstance(sched, ScheduleRuleset):
year_schedule, week_schedules = sched.to_inp()
# check that day schedules aren't referenced by other model schedules
day_scheds = []
for day in sched.day_schedules:
if day.identifier not in used_day_sched_ids:
day_scheds.append(day.to_inp(sched.schedule_type_limit))
used_day_sched_ids[day.identifier] = day
elif day != used_day_sched_ids[day.identifier]:
new_day = day.duplicate()
new_day.identifier = 'Schedule Day {}'.format(used_day_count)
day_scheds.append(new_day.to_inp(sched.schedule_type_limit))
for i, week_sch in enumerate(week_schedules):
old_day_id = clean_doe2_string(day.identifier, RES_CHARS)
new_day_id = clean_doe2_string(new_day.identifier, RES_CHARS)
week_schedules[i] = week_sch.replace(old_day_id, new_day_id)
used_day_count += 1
all_day_scheds.extend(day_scheds)
all_week_scheds.extend(week_schedules)
all_year_scheds.append(year_schedule)
else: # ScheduleFixedInterval
year_schedule, week_schedules, year_schedule = sched.to_inp()
all_day_scheds.extend(day_scheds)
all_week_scheds.extend(week_schedules)
all_year_scheds.append(year_schedule)
inp_str_list = ['INPUT ..\n\n']
inp_str_list.append(header_comment_minor('Day Schedules'))
inp_str_list.extend(all_day_scheds)
inp_str_list.append(header_comment_minor('Week Schedules'))
inp_str_list.extend(all_week_scheds)
inp_str_list.append(header_comment_minor('Annual Schedules'))
inp_str_list.extend(all_year_scheds)
inp_str_list.append('END ..\nCOMPUTE ..\nSTOP ..\n')
inp_str = '\n'.join(inp_str_list)

# write out the INP file
output_file.write(inp_str)
except Exception as e:
_logger.exception('Schedule translation failed.\n{}'.format(e))
sys.exit(1)
else:
sys.exit(0)


@translate.command('schedules-from-inp')
@click.argument('schedule-inp', type=click.Path(
exists=True, file_okay=True, dir_okay=False, resolve_path=True))
@click.option(
'--dictionary/--list', ' /-l', help='Flag to note whether a the output JSON '
'should be a list of schedule objects or whether it should be a dictionary '
'where each key is the identifier of the schedule and each value is the '
'schedule object. The dictionary format is the one used by honeybee-standards '
'and is recommended when writing INP schedules into the user standards library.',
default=True, show_default=True)
@click.option(
'--output-file', '-f', help='Optional JSON file to output the JSON string of '
'the translation. By default this will be printed out to stdout',
type=click.File('w'), default='-', show_default=True)
def schedule_from_inp(schedule_inp, dictionary, output_file):
"""Translate a schedule INP file to a honeybee JSON as an array of schedules.
\b
Args:
schedule_inp: Full path to a Schedule INP file.
"""
try:
# re-serialize the schedules to Python
schedules = extract_all_schedule_ruleset_from_inp_file(schedule_inp)
# create the honeybee dictionaries
if dictionary:
hb_objs = {sch.identifier: sch.to_dict() for sch in schedules}
else:
hb_objs = [sch.to_dict() for sch in schedules]
# write out the JSON file
output_file.write(json.dumps(hb_objs))
except Exception as e:
_logger.exception('Schedule translation failed.\n{}'.format(e))
sys.exit(1)
else:
sys.exit(0)
112 changes: 78 additions & 34 deletions honeybee_doe2/schedule.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from ladybug.dt import Date, MONTHNAMES
from ladybug.analysisperiod import AnalysisPeriod
from honeybee.typing import clean_doe2_string
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
Expand Down Expand Up @@ -366,14 +366,17 @@ def schedule_day_from_inp(inp_string):
field_dict = {k: v for k, v in zip(keywords, values)}
sch_type = field_dict['TYPE'].upper()
hour_vals_init = eval(field_dict['VALUES'].replace('&D', '"&D"'), {})
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])
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):
Expand All @@ -393,16 +396,19 @@ def schedule_day_from_inp(inp_string):
# 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(u_name, 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()
sch_obj = schedule_day_from_inp(sch_str)
day_schedule_dict[sch_obj.identifier] = sch_obj
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


Expand Down Expand Up @@ -449,7 +455,7 @@ def extract_all_rules_from_inp_schedule_week(
day_sch_id = prev_day if day_sch_id == '&D' else day_sch_id
prev_day = day_sch_id # increment it for the next item
if day_sch_id not in applied_day_ids: # make a new rule
rule = ScheduleRule(day_schedule_dict[day_sch_id],
rule = ScheduleRule(day_schedule_dict[clean_ep_string(day_sch_id)],
start_date=start_date, end_date=end_date)
if i == 6:
rule.apply_day_by_dow(1)
Expand Down Expand Up @@ -480,32 +486,29 @@ def _inp_week_schedule_dictionary(week_inp_strings, day_sch_dict):
week_designday_dict = {}
for sch_str in week_inp_strings:
sch_str = sch_str.strip()
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[holiday],
day_sch_dict[summer_dd],
day_sch_dict[winter_dd]
]
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 schedule_ruleset_from_inp(year_inp_string, week_inp_strings, day_inp_strings):
"""Create a ScheduleRuleset from a DOE-2 INP text strings.
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_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.
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.
"""
# 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)

# 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)}
Expand Down Expand Up @@ -551,15 +554,46 @@ def schedule_ruleset_from_inp(year_inp_string, week_inp_strings, day_inp_strings
next_day = Date.from_doy(end_doy)
prev_month, prev_day = next_day.month, next_day.day

# create the ScheduleRuleset and apply the design days
# 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]
sched = ScheduleRuleset(u_name, default_day_schedule, all_rules[1:], schedule_type)
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


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)


def extract_all_schedule_ruleset_from_inp_file(inp_file):
"""Extract all ScheduleRuleset objects from a DOE-2 INP file.
Expand Down Expand Up @@ -591,6 +625,16 @@ def extract_all_schedule_ruleset_from_inp_file(inp_file):
year_pattern2 = re.compile(r'(?i)(".*=.*SCHEDULE-PD\n[\s\S]*?\.\.)')
year_sch_str = year_pattern1.findall(file_contents) + \
year_pattern2.findall(file_contents)

# translate each SCHEDULE and check to be sure ScheduleDay objects are unique
schedules = []
for year_sch in year_sch_str:
try:
yr_sch = _convert_schedule_year(year_sch, week_sch_dict, week_dd_dict)
schedules.append(yr_sch)
except Exception:
pass # schedule is not translate-able
return schedules


"""______EXTRA UTILITY FUNCTIONS RELATED TO SCHEDULES______"""
Expand Down
Loading

0 comments on commit 42b7dc4

Please sign in to comment.