Skip to content

Commit 8e63da1

Browse files
committed
add all the new stuff that didn't get comitted earlier
1 parent 67f2f57 commit 8e63da1

File tree

31 files changed

+18683
-0
lines changed

31 files changed

+18683
-0
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from dataclasses import dataclass
2+
from typing import Callable
3+
4+
from alfalfa_worker.lib.models import Point
5+
6+
7+
@dataclass
8+
class AlfalfaPoint:
9+
point: Point
10+
handle: int
11+
converter: Callable[[float], float] = lambda x: x
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
"""insert your copyright here.
2+
3+
# see the URL below for information on how to write OpenStudio measures
4+
# http://nrel.github.io/OpenStudio-user-documentation/reference/measure_writing_guide/
5+
"""
6+
7+
import os
8+
from pathlib import Path
9+
10+
import openstudio
11+
12+
13+
class AlfalfaPythonEnvironment(openstudio.measure.EnergyPlusMeasure):
14+
"""An EnergyPlusMeasure."""
15+
16+
def name(self):
17+
"""Returns the human readable name.
18+
19+
Measure name should be the title case of the class name.
20+
The measure name is the first contact a user has with the measure;
21+
it is also shared throughout the measure workflow, visible in the OpenStudio Application,
22+
PAT, Server Management Consoles, and in output reports.
23+
As such, measure names should clearly describe the measure's function,
24+
while remaining general in nature
25+
"""
26+
return "AlfalfaPythonEnvironment"
27+
28+
def description(self):
29+
"""Human readable description.
30+
31+
The measure description is intended for a general audience and should not assume
32+
that the reader is familiar with the design and construction practices suggested by the measure.
33+
"""
34+
return "Add alfalfa python environment to IDF"
35+
36+
def modeler_description(self):
37+
"""Human readable description of modeling approach.
38+
39+
The modeler description is intended for the energy modeler using the measure.
40+
It should explain the measure's intent, and include any requirements about
41+
how the baseline model must be set up, major assumptions made by the measure,
42+
and relevant citations or references to applicable modeling resources
43+
"""
44+
return "Add python script path to IDF"
45+
46+
def arguments(self, workspace: openstudio.Workspace):
47+
"""Prepares user arguments for the measure.
48+
49+
Measure arguments define which -- if any -- input parameters the user may set before running the measure.
50+
"""
51+
args = openstudio.measure.OSArgumentVector()
52+
53+
return args
54+
55+
def run(
56+
self,
57+
workspace: openstudio.Workspace,
58+
runner: openstudio.measure.OSRunner,
59+
user_arguments: openstudio.measure.OSArgumentMap,
60+
):
61+
"""Defines what happens when the measure is run."""
62+
super().run(workspace, runner, user_arguments) # Do **NOT** remove this line
63+
64+
if not (runner.validateUserArguments(self.arguments(workspace), user_arguments)):
65+
return False
66+
67+
run_dir = os.getenv("RUN_DIR")
68+
if run_dir:
69+
venv_dir = Path(run_dir) / '.venv'
70+
if venv_dir.exists():
71+
python_paths = openstudio.IdfObject(openstudio.IddObjectType("PythonPlugin:SearchPaths"))
72+
python_paths.setString(0, "Alfalfa Virtual Environment Path")
73+
python_paths.setString(1, 'No')
74+
python_paths.setString(2, 'No')
75+
python_paths.setString(3, 'No')
76+
python_paths.setString(4, str(venv_dir / 'lib' / 'python3.12' / 'site-packages'))
77+
78+
workspace.addObject(python_paths)
79+
80+
return True
81+
82+
83+
# register the measure to be used by the application
84+
AlfalfaPythonEnvironment().registerWithApplication()
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import threading
2+
from ctypes import c_wchar_p
3+
from datetime import datetime
4+
from multiprocessing import Manager, Process
5+
from time import time
6+
7+
from alfalfa_worker.jobs.step_run_base import StepRunBase
8+
from alfalfa_worker.lib.constants import DATETIME_FORMAT
9+
from alfalfa_worker.lib.job import message
10+
from alfalfa_worker.lib.job_exception import (
11+
JobExceptionExternalProcess,
12+
JobExceptionMessageHandler
13+
)
14+
from alfalfa_worker.lib.utils import exc_to_str
15+
16+
17+
class StepRunProcess(StepRunBase):
18+
19+
def __init__(self, run_id: str, realtime: bool, timescale: int, external_clock: bool, start_datetime: str, end_datetime: str, **kwargs) -> None:
20+
super().__init__(run_id, realtime, timescale, external_clock, start_datetime, end_datetime)
21+
self.manager = Manager()
22+
self.advance_event = self.manager.Event()
23+
self.running_event = self.manager.Event()
24+
self.stop_event = self.manager.Event()
25+
self.error_event = self.manager.Event()
26+
self.error_log = self.manager.Value(c_wchar_p, '')
27+
self.simulation_process: Process
28+
self.subprocess: bool = False
29+
self.timestamp = self.manager.Value(c_wchar_p, '')
30+
31+
def initialize_simulation(self) -> None:
32+
self.simulation_process = Process(target=StepRunProcess._start_simulation_process, args=(self,))
33+
self.simulation_process.start()
34+
35+
self._wait_for_event(self.running_event, self.options.start_timeout, desired_event_set=True)
36+
self.update_run_time()
37+
38+
def set_run_time(self, sim_time: datetime):
39+
if self.subprocess:
40+
self.timestamp.value = sim_time.strftime(DATETIME_FORMAT)
41+
else:
42+
return super().set_run_time(sim_time)
43+
44+
def update_run_time(self) -> None:
45+
if self.subprocess:
46+
super().update_run_time()
47+
else:
48+
self.set_run_time(datetime.strptime(self.timestamp.value, DATETIME_FORMAT))
49+
50+
def _start_simulation_process(self) -> None:
51+
self.subprocess = True
52+
try:
53+
return self.start_simulation_process()
54+
except Exception:
55+
self.catch_exception()
56+
57+
def start_simulation_process(self) -> None:
58+
raise NotImplementedError
59+
60+
def handle_process_error(self) -> None:
61+
if self.simulation_process.is_alive():
62+
self.simulation_process.kill()
63+
raise JobExceptionExternalProcess(self.error_log.value)
64+
65+
def catch_exception(self, notes: list[str]) -> None:
66+
if self.subprocess:
67+
exception_log = exc_to_str()
68+
self.error_log.value = exception_log
69+
if len(notes) > 0:
70+
self.error_log.value += "\n\n" + '\n'.join(notes)
71+
self.error_event.set()
72+
73+
def check_simulation_stop_conditions(self) -> bool:
74+
return not self.simulation_process.is_alive()
75+
76+
def check_for_errors(self):
77+
exit_code = self.simulation_process.exitcode
78+
if exit_code:
79+
raise JobExceptionExternalProcess(f"Simulation process exited with non-zero exit code: {exit_code}")
80+
81+
def _wait_for_event(self, event: threading.Event, timeout: float, desired_event_set: bool = False):
82+
wait_until = time() + timeout
83+
while (event.is_set() != desired_event_set
84+
and time() < wait_until
85+
and self.simulation_process.is_alive()
86+
and not self.error_event.is_set()):
87+
self.check_for_errors()
88+
if desired_event_set:
89+
event.wait(1)
90+
if self.error_event.is_set():
91+
self.handle_process_error()
92+
if not self.simulation_process.is_alive():
93+
self.check_for_errors()
94+
raise JobExceptionExternalProcess("Simulation process exited without returning an error")
95+
if time() > wait_until:
96+
self.simulation_process.kill()
97+
raise TimeoutError("Timedout waiting for simulation process to toggle event")
98+
99+
@message
100+
def advance(self) -> None:
101+
self.logger.info(f"Advance called at {self.run.sim_time}")
102+
if self.advance_event.is_set():
103+
raise JobExceptionMessageHandler("Cannot advance, simulation is already advancing")
104+
self.advance_event.set()
105+
self._wait_for_event(self.advance_event, timeout=self.options.advance_timeout, desired_event_set=False)
106+
self.update_run_time()
107+
108+
@message
109+
def stop(self):
110+
self.logger.info("Stop called, stopping")
111+
if not self.stop_event.is_set():
112+
stop_start = time()
113+
self.stop_event.set()
114+
while (self.simulation_process.is_alive()
115+
and time() - stop_start < self.options.stop_timeout
116+
and not self.error_event.is_set()):
117+
pass
118+
if time() - stop_start > self.options.stop_timeout and self.simulation_process.is_alive():
119+
self.simulation_process.kill()
120+
raise JobExceptionExternalProcess("Simulation process stopped responding and was killed.")
121+
if self.error_event.is_set():
122+
self.handle_process_error()

alfalfa_worker/lib/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
DATETIME_FORMAT = '%Y-%m-%d %H:%M:%S'
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
class JobException(Exception):
2+
pass
3+
4+
5+
class JobExceptionMessageHandler(JobException):
6+
"""Thrown when there is an exception that occurs in an message handler.
7+
This is caught and reported back to the caller via redis."""
8+
9+
10+
class JobExceptionInvalidModel(JobException):
11+
"""Thrown when working on a model.
12+
ex: missing osw"""
13+
14+
15+
class JobExceptionInvalidRun(JobException):
16+
"""Thrown when working on run.
17+
ex. run does not have necessary files"""
18+
19+
20+
class JobExceptionExternalProcess(JobException):
21+
"""Thrown when an external process throws an error.
22+
ex. E+ can't run idf"""
23+
24+
25+
class JobExceptionFailedValidation(JobException):
26+
"""Thrown when the job fails validation for any reason.
27+
ex. file that should have been generated was not"""
28+
29+
30+
class JobExceptionSimulation(JobException):
31+
"""Thrown when there is a simulation issue.
32+
ex. Simulation falls too far behind in timescale run"""
33+
34+
35+
class JobExceptionTimeout(JobException):
36+
"""Thrown when a timeout is triggered in the job"""
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from logging import Handler, LogRecord
2+
3+
from alfalfa_worker.lib.alfalfa_connections_manager import (
4+
AlafalfaConnectionsManager
5+
)
6+
from alfalfa_worker.lib.models import Run
7+
8+
9+
class RedisLogHandler(Handler):
10+
def __init__(self, run: Run, level: int | str = 0) -> None:
11+
super().__init__(level)
12+
connections_manager = AlafalfaConnectionsManager()
13+
self.redis = connections_manager.redis
14+
self.run = run
15+
16+
def emit(self, record: LogRecord) -> None:
17+
self.redis.rpush(f"run:{self.run.ref_id}:log", self.format(record))
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"seed_file" : "small_office.osm",
3+
"weather_file": "USA_OH_Dayton-Wright.Patterson.AFB.745700_TMY3.epw",
4+
"measure_paths": [
5+
"./measures/"
6+
],
7+
"file_paths": [
8+
"./weather/"
9+
],
10+
"run_directory": "./run/",
11+
"steps" : [
12+
{
13+
"measure_dir_name" : "python_bad_callback",
14+
"name" : "PythonEMS",
15+
"description" : "Add python EMS to IDF",
16+
"modeler_description" : "Add python EMS to IDF",
17+
}
18+
]
19+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"seed_file" : "small_office.osm",
3+
"weather_file": "USA_OH_Dayton-Wright.Patterson.AFB.745700_TMY3.epw",
4+
"measure_paths": [
5+
"./measures/"
6+
],
7+
"file_paths": [
8+
"./weather/"
9+
],
10+
"run_directory": "./run/",
11+
"steps" : [
12+
{
13+
"measure_dir_name" : "python_bad_constructor",
14+
"name" : "PythonEMS",
15+
"description" : "Add python EMS to IDF",
16+
"modeler_description" : "Add python EMS to IDF",
17+
}
18+
]
19+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"seed_file" : "small_office.osm",
3+
"weather_file": "USA_OH_Dayton-Wright.Patterson.AFB.745700_TMY3.epw",
4+
"measure_paths": [
5+
"./measures/"
6+
],
7+
"file_paths": [
8+
"./weather/"
9+
],
10+
"run_directory": "./run/",
11+
"steps" : [
12+
{
13+
"measure_dir_name" : "python_bad_module_class",
14+
"name" : "PythonEMS",
15+
"description" : "Add python EMS to IDF",
16+
"modeler_description" : "Add python EMS to IDF",
17+
}
18+
]
19+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"seed_file" : "small_office.osm",
3+
"weather_file": "USA_OH_Dayton-Wright.Patterson.AFB.745700_TMY3.epw",
4+
"measure_paths": [
5+
"./measures/"
6+
],
7+
"file_paths": [
8+
"./weather/"
9+
],
10+
"run_directory": "./run/",
11+
"steps" : [
12+
{
13+
"measure_dir_name" : "python_bad_module_name",
14+
"name" : "PythonEMS",
15+
"description" : "Add python EMS to IDF",
16+
"modeler_description" : "Add python EMS to IDF",
17+
}
18+
]
19+
}

0 commit comments

Comments
 (0)