Skip to content

Commit

Permalink
add all the new stuff that didn't get comitted earlier
Browse files Browse the repository at this point in the history
  • Loading branch information
TShapinsky committed Jan 7, 2025
1 parent 67f2f57 commit 8e63da1
Show file tree
Hide file tree
Showing 31 changed files with 18,683 additions and 0 deletions.
11 changes: 11 additions & 0 deletions alfalfa_worker/jobs/openstudio/lib/alfalfa_point.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from dataclasses import dataclass
from typing import Callable

from alfalfa_worker.lib.models import Point


@dataclass
class AlfalfaPoint:
point: Point
handle: int
converter: Callable[[float], float] = lambda x: x
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"""insert your copyright here.
# see the URL below for information on how to write OpenStudio measures
# http://nrel.github.io/OpenStudio-user-documentation/reference/measure_writing_guide/
"""

import os
from pathlib import Path

import openstudio


class AlfalfaPythonEnvironment(openstudio.measure.EnergyPlusMeasure):
"""An EnergyPlusMeasure."""

def name(self):
"""Returns the human readable name.
Measure name should be the title case of the class name.
The measure name is the first contact a user has with the measure;
it is also shared throughout the measure workflow, visible in the OpenStudio Application,
PAT, Server Management Consoles, and in output reports.
As such, measure names should clearly describe the measure's function,
while remaining general in nature
"""
return "AlfalfaPythonEnvironment"

def description(self):
"""Human readable description.
The measure description is intended for a general audience and should not assume
that the reader is familiar with the design and construction practices suggested by the measure.
"""
return "Add alfalfa python environment to IDF"

def modeler_description(self):
"""Human readable description of modeling approach.
The modeler description is intended for the energy modeler using the measure.
It should explain the measure's intent, and include any requirements about
how the baseline model must be set up, major assumptions made by the measure,
and relevant citations or references to applicable modeling resources
"""
return "Add python script path to IDF"

def arguments(self, workspace: openstudio.Workspace):
"""Prepares user arguments for the measure.
Measure arguments define which -- if any -- input parameters the user may set before running the measure.
"""
args = openstudio.measure.OSArgumentVector()

return args

def run(
self,
workspace: openstudio.Workspace,
runner: openstudio.measure.OSRunner,
user_arguments: openstudio.measure.OSArgumentMap,
):
"""Defines what happens when the measure is run."""
super().run(workspace, runner, user_arguments) # Do **NOT** remove this line

if not (runner.validateUserArguments(self.arguments(workspace), user_arguments)):
return False

run_dir = os.getenv("RUN_DIR")
if run_dir:
venv_dir = Path(run_dir) / '.venv'
if venv_dir.exists():
python_paths = openstudio.IdfObject(openstudio.IddObjectType("PythonPlugin:SearchPaths"))
python_paths.setString(0, "Alfalfa Virtual Environment Path")
python_paths.setString(1, 'No')
python_paths.setString(2, 'No')
python_paths.setString(3, 'No')
python_paths.setString(4, str(venv_dir / 'lib' / 'python3.12' / 'site-packages'))

workspace.addObject(python_paths)

return True


# register the measure to be used by the application
AlfalfaPythonEnvironment().registerWithApplication()
122 changes: 122 additions & 0 deletions alfalfa_worker/jobs/step_run_process.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import threading
from ctypes import c_wchar_p
from datetime import datetime
from multiprocessing import Manager, Process
from time import time

from alfalfa_worker.jobs.step_run_base import StepRunBase
from alfalfa_worker.lib.constants import DATETIME_FORMAT
from alfalfa_worker.lib.job import message
from alfalfa_worker.lib.job_exception import (
JobExceptionExternalProcess,
JobExceptionMessageHandler
)
from alfalfa_worker.lib.utils import exc_to_str


class StepRunProcess(StepRunBase):

def __init__(self, run_id: str, realtime: bool, timescale: int, external_clock: bool, start_datetime: str, end_datetime: str, **kwargs) -> None:
super().__init__(run_id, realtime, timescale, external_clock, start_datetime, end_datetime)
self.manager = Manager()
self.advance_event = self.manager.Event()
self.running_event = self.manager.Event()
self.stop_event = self.manager.Event()
self.error_event = self.manager.Event()
self.error_log = self.manager.Value(c_wchar_p, '')
self.simulation_process: Process
self.subprocess: bool = False
self.timestamp = self.manager.Value(c_wchar_p, '')

def initialize_simulation(self) -> None:
self.simulation_process = Process(target=StepRunProcess._start_simulation_process, args=(self,))
self.simulation_process.start()

self._wait_for_event(self.running_event, self.options.start_timeout, desired_event_set=True)
self.update_run_time()

def set_run_time(self, sim_time: datetime):
if self.subprocess:
self.timestamp.value = sim_time.strftime(DATETIME_FORMAT)
else:
return super().set_run_time(sim_time)

def update_run_time(self) -> None:
if self.subprocess:
super().update_run_time()
else:
self.set_run_time(datetime.strptime(self.timestamp.value, DATETIME_FORMAT))

def _start_simulation_process(self) -> None:
self.subprocess = True
try:
return self.start_simulation_process()
except Exception:
self.catch_exception()

def start_simulation_process(self) -> None:
raise NotImplementedError

def handle_process_error(self) -> None:
if self.simulation_process.is_alive():
self.simulation_process.kill()
raise JobExceptionExternalProcess(self.error_log.value)

def catch_exception(self, notes: list[str]) -> None:
if self.subprocess:
exception_log = exc_to_str()
self.error_log.value = exception_log
if len(notes) > 0:
self.error_log.value += "\n\n" + '\n'.join(notes)
self.error_event.set()

def check_simulation_stop_conditions(self) -> bool:
return not self.simulation_process.is_alive()

def check_for_errors(self):
exit_code = self.simulation_process.exitcode
if exit_code:
raise JobExceptionExternalProcess(f"Simulation process exited with non-zero exit code: {exit_code}")

def _wait_for_event(self, event: threading.Event, timeout: float, desired_event_set: bool = False):
wait_until = time() + timeout
while (event.is_set() != desired_event_set
and time() < wait_until
and self.simulation_process.is_alive()
and not self.error_event.is_set()):
self.check_for_errors()
if desired_event_set:
event.wait(1)
if self.error_event.is_set():
self.handle_process_error()
if not self.simulation_process.is_alive():
self.check_for_errors()
raise JobExceptionExternalProcess("Simulation process exited without returning an error")
if time() > wait_until:
self.simulation_process.kill()
raise TimeoutError("Timedout waiting for simulation process to toggle event")

@message
def advance(self) -> None:
self.logger.info(f"Advance called at {self.run.sim_time}")
if self.advance_event.is_set():
raise JobExceptionMessageHandler("Cannot advance, simulation is already advancing")
self.advance_event.set()
self._wait_for_event(self.advance_event, timeout=self.options.advance_timeout, desired_event_set=False)
self.update_run_time()

@message
def stop(self):
self.logger.info("Stop called, stopping")
if not self.stop_event.is_set():
stop_start = time()
self.stop_event.set()
while (self.simulation_process.is_alive()
and time() - stop_start < self.options.stop_timeout
and not self.error_event.is_set()):
pass
if time() - stop_start > self.options.stop_timeout and self.simulation_process.is_alive():
self.simulation_process.kill()
raise JobExceptionExternalProcess("Simulation process stopped responding and was killed.")
if self.error_event.is_set():
self.handle_process_error()
1 change: 1 addition & 0 deletions alfalfa_worker/lib/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DATETIME_FORMAT = '%Y-%m-%d %H:%M:%S'
36 changes: 36 additions & 0 deletions alfalfa_worker/lib/job_exception.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
class JobException(Exception):
pass


class JobExceptionMessageHandler(JobException):
"""Thrown when there is an exception that occurs in an message handler.
This is caught and reported back to the caller via redis."""


class JobExceptionInvalidModel(JobException):
"""Thrown when working on a model.
ex: missing osw"""


class JobExceptionInvalidRun(JobException):
"""Thrown when working on run.
ex. run does not have necessary files"""


class JobExceptionExternalProcess(JobException):
"""Thrown when an external process throws an error.
ex. E+ can't run idf"""


class JobExceptionFailedValidation(JobException):
"""Thrown when the job fails validation for any reason.
ex. file that should have been generated was not"""


class JobExceptionSimulation(JobException):
"""Thrown when there is a simulation issue.
ex. Simulation falls too far behind in timescale run"""


class JobExceptionTimeout(JobException):
"""Thrown when a timeout is triggered in the job"""
17 changes: 17 additions & 0 deletions alfalfa_worker/lib/redis_log_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from logging import Handler, LogRecord

from alfalfa_worker.lib.alfalfa_connections_manager import (
AlafalfaConnectionsManager
)
from alfalfa_worker.lib.models import Run


class RedisLogHandler(Handler):
def __init__(self, run: Run, level: int | str = 0) -> None:
super().__init__(level)
connections_manager = AlafalfaConnectionsManager()
self.redis = connections_manager.redis
self.run = run

def emit(self, record: LogRecord) -> None:
self.redis.rpush(f"run:{self.run.ref_id}:log", self.format(record))
19 changes: 19 additions & 0 deletions tests/integration/broken_models/small_office/bad_callback.osw
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"seed_file" : "small_office.osm",
"weather_file": "USA_OH_Dayton-Wright.Patterson.AFB.745700_TMY3.epw",
"measure_paths": [
"./measures/"
],
"file_paths": [
"./weather/"
],
"run_directory": "./run/",
"steps" : [
{
"measure_dir_name" : "python_bad_callback",
"name" : "PythonEMS",
"description" : "Add python EMS to IDF",
"modeler_description" : "Add python EMS to IDF",
}
]
}
19 changes: 19 additions & 0 deletions tests/integration/broken_models/small_office/bad_constructor.osw
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"seed_file" : "small_office.osm",
"weather_file": "USA_OH_Dayton-Wright.Patterson.AFB.745700_TMY3.epw",
"measure_paths": [
"./measures/"
],
"file_paths": [
"./weather/"
],
"run_directory": "./run/",
"steps" : [
{
"measure_dir_name" : "python_bad_constructor",
"name" : "PythonEMS",
"description" : "Add python EMS to IDF",
"modeler_description" : "Add python EMS to IDF",
}
]
}
19 changes: 19 additions & 0 deletions tests/integration/broken_models/small_office/bad_module_class.osw
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"seed_file" : "small_office.osm",
"weather_file": "USA_OH_Dayton-Wright.Patterson.AFB.745700_TMY3.epw",
"measure_paths": [
"./measures/"
],
"file_paths": [
"./weather/"
],
"run_directory": "./run/",
"steps" : [
{
"measure_dir_name" : "python_bad_module_class",
"name" : "PythonEMS",
"description" : "Add python EMS to IDF",
"modeler_description" : "Add python EMS to IDF",
}
]
}
19 changes: 19 additions & 0 deletions tests/integration/broken_models/small_office/bad_module_name.osw
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"seed_file" : "small_office.osm",
"weather_file": "USA_OH_Dayton-Wright.Patterson.AFB.745700_TMY3.epw",
"measure_paths": [
"./measures/"
],
"file_paths": [
"./weather/"
],
"run_directory": "./run/",
"steps" : [
{
"measure_dir_name" : "python_bad_module_name",
"name" : "PythonEMS",
"description" : "Add python EMS to IDF",
"modeler_description" : "Add python EMS to IDF",
}
]
}
Loading

0 comments on commit 8e63da1

Please sign in to comment.