Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ schedules-*.csv
# measures tests
lib/measures/.rubocop.yml
lib/measures/building_sync_to_openstudio/tests/output
vendor/external_measures

# OSW tests for gem
osw_test
Expand Down
26 changes: 25 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,22 @@ BuildingSync OpenStudio Simulator (BOSS) takes in BuildingSync files, creates Op
🌟 bundle install
```

4. Install external non-gem measure repositories declared in the shipped manifest.

```bash
🌟 bundle exec rake measures:install_external
```

5. (Optional) List resolved external measure roots.

```bash
🌟 bundle exec rake measures:list_external
```

The shipped manifest lives at [config/external_measure_repos.yml](config/external_measure_repos.yml) and is preconfigured with:
- local measure roots: `lib/measures` (first in precedence)
- external repository: comstock (`measures` and `resources/measures`)

## Usage
BOSS uses its `Translator` class to 1) write openstudio workflows and 2) run those workflows.
```ruby
Expand All @@ -45,6 +61,14 @@ expect(File.exist?(output_path + "/baseline/out.osw")).to be true
expect(File.exist?(output_path + "/baseline/in.osm")).to be true

```

Measure path precedence is:
1. local measure roots from [config/external_measure_repos.yml](config/external_measure_repos.yml)
2. gem-provided measure directories
3. external non-gem repository measure roots from [config/external_measure_repos.yml](config/external_measure_repos.yml)

The generated `in.osw` will include these paths in order under `measure_paths`.

The file `` does all of the actual writing to the osw. Each function writes one measure. Heres an overview of how each measure is populated.

[set_run_period]: https://github.com/NatLabRockies/openstudio-common-measures-gem/blob/v0.12.3/lib/measures/set_run_period/README.md
Expand Down Expand Up @@ -96,7 +120,7 @@ Check out the repository and then execute:
bundle exec rspec ./spec/tests/translator_write_osw_spec.rb
```

This only runs only files worth of tests, which are integration tests very similar to the code in the usage section. The gem has under gone major rewrites and many of the other tests use dead and/or delete code. Further clean up and testing is underway.
This only runs files worth of tests, which are integration tests very similar to the code in the usage section. The gem has under gone major rewrites and many of the other tests use dead and/or delete code. Further clean up and testing is underway.

# Releasing

Expand Down
27 changes: 27 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ RuboCop::RakeTask.new
# Load in the rake tasks from the base extension gem
require 'openstudio/extension/rake_task'
require 'openstudio/model_articulation'
require_relative './lib/buildingsync/constants'
require_relative './lib/buildingsync/external_measure_repo_manager'
rake_task = OpenStudio::Extension::RakeTask.new
rake_task.set_extension_class(OpenStudio::ModelArticulation::Extension)

Expand Down Expand Up @@ -51,4 +53,29 @@ task :measure_test do

end

namespace :measures do
desc 'Clone/fetch external non-gem measure repositories declared in config/external_measure_repos.yml'
task :install_external do
manager = BuildingSync::ExternalMeasureRepoManager.new

if !manager.manifest_exists?
puts "No external measure manifest found at #{EXTERNAL_MEASURE_REPOS_MANIFEST_PATH}"
next
end

dirs = manager.install_all
puts "Installed external measure roots (#{dirs.length}):"
dirs.each { |dir| puts " - #{dir}" }
end

desc 'List resolved external non-gem measure directories'
task :list_external do
manager = BuildingSync::ExternalMeasureRepoManager.new
dirs = manager.resolved_measure_directories

puts "External measure roots (#{dirs.length}):"
dirs.each { |dir| puts " - #{dir}" }
end
end

task default: :spec
34 changes: 34 additions & 0 deletions config/external_measure_repos.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# External non-gem measure repositories.
#
# Precedence in workflow measure lookup is:
# 1) local_measure_roots below
# 2) gem-provided measure dirs
# 3) external repos listed here
#
# local_measure_roots are resolved relative to this file.
# The default shipped configuration keeps this repository's local measures first.
local_measure_roots:
- ../lib/measures

# For each repo:
# - name: local checkout folder name under vendor/external_measures
# - url: git repository URL
# - ref: pinned tag/branch/commit to checkout
# - measure_roots: one or more paths inside the repo that contain measure directories
#
# Example:
# repos:
# - name: comstock
# url: https://github.com/NatLabRockies/comstock.git
# ref: main
# measure_roots:
# - measures
# - resources/measures

repos:
- name: comstock
url: https://github.com/NatLabRockies/comstock.git
ref: main
measure_roots:
- measures
- resources/measures
3 changes: 3 additions & 0 deletions lib/buildingsync/constants.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
WORKFLOW_MAKER_JSON_FILE_PATH = File.expand_path(File.join(__dir__, 'makers/workflow_maker.json'))
BUILDING_AND_SYSTEMS_FILE_PATH = File.expand_path(File.join(__dir__, 'model_articulation/building_and_system_types.json'))
WEATHER_DIR = File.expand_path(File.join(__dir__, '../data/weather'))
LOCAL_MEASURES_DIR = File.expand_path(File.join(__dir__, '..', 'measures'))
EXTERNAL_MEASURE_REPOS_MANIFEST_PATH = File.expand_path(File.join(__dir__, '..', '..', 'config', 'external_measure_repos.yml'))
EXTERNAL_MEASURE_REPOS_INSTALL_DIR = File.expand_path(File.join(__dir__, '..', '..', 'vendor', 'external_measures'))

# Standards strings
ASHRAE90_1 = 'ASHRAE90.1'
Expand Down
154 changes: 154 additions & 0 deletions lib/buildingsync/external_measure_repo_manager.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
# frozen_string_literal: true

# *******************************************************************************
# OpenStudio(R), Copyright (c) Alliance for Energy Innovation, LLC.
# See also https://github.com/BuildingSync/BuildingSync-gem/blob/develop/LICENSE.md
# *******************************************************************************

require 'fileutils'
require 'open3'
require 'yaml'

module BuildingSync
# Manages non-gem measure repositories declared in a manifest file.
class ExternalMeasureRepoManager
def initialize(manifest_path: EXTERNAL_MEASURE_REPOS_MANIFEST_PATH, install_dir: EXTERNAL_MEASURE_REPOS_INSTALL_DIR)
@manifest_path = manifest_path
@install_dir = install_dir
end

def manifest_exists?
File.file?(@manifest_path)
end

def configured?
!repo_entries.empty?
end

def local_measure_directories
return [LOCAL_MEASURES_DIR] if !manifest_exists?

roots = manifest['local_measure_roots']
roots = [LOCAL_MEASURES_DIR] if !roots.is_a?(Array) || roots.empty?

directories = roots.map do |path|
File.expand_path(path, File.expand_path('..', @manifest_path))
end

directories.select { |path| Dir.exist?(path) }.uniq
end

def install_all
return [] if !manifest_exists?

FileUtils.mkdir_p(@install_dir)

repo_entries.each do |repo_entry|
install_repo(repo_entry)
end

resolved_measure_directories
end

def resolved_measure_directories
return [] if !manifest_exists?

directories = []
repo_entries.each do |repo_entry|
repo_root = repo_checkout_dir(repo_entry)
if !Dir.exist?(repo_root)
OpenStudio.logFree(OpenStudio::Warn, 'BuildingSync.ExternalMeasureRepoManager.resolved_measure_directories', "Repository '#{repo_entry['name']}' is not installed at #{repo_root}. Run rake measures:install_external.")
next
end

repo_entry['measure_roots'].each do |root_rel_path|
root_abs_path = File.expand_path(File.join(repo_root, root_rel_path))
if Dir.exist?(root_abs_path)
directories << root_abs_path
else
OpenStudio.logFree(OpenStudio::Warn, 'BuildingSync.ExternalMeasureRepoManager.resolved_measure_directories', "Configured measure root '#{root_rel_path}' does not exist for repository '#{repo_entry['name']}' (#{root_abs_path}).")
end
end
end

ordered_unique(directories)
end

private

def manifest
@manifest ||= begin
parsed = YAML.safe_load(File.read(@manifest_path), permitted_classes: [], aliases: false)
parsed.is_a?(Hash) ? parsed : {}
end
end

def repo_entries
return [] if !manifest_exists?

repos = manifest.is_a?(Hash) ? manifest['repos'] : nil
return [] if repos.nil?
raise StandardError, "Expected 'repos' array in #{@manifest_path}" if !repos.is_a?(Array)

repos.map do |repo|
validate_repo_entry(repo)
end
end

def validate_repo_entry(repo)
if !repo.is_a?(Hash)
raise StandardError, "Each repo entry in #{@manifest_path} must be a map"
end

required = %w[name url ref measure_roots]
missing = required.select { |key| repo[key].nil? || repo[key].to_s.empty? }
if !missing.empty?
raise StandardError, "Repo entry is missing required keys #{missing.join(', ')} in #{@manifest_path}"
end

if !repo['measure_roots'].is_a?(Array) || repo['measure_roots'].empty?
raise StandardError, "Repo '#{repo['name']}' must define a non-empty measure_roots array"
end

repo
end

def install_repo(repo_entry)
repo_dir = repo_checkout_dir(repo_entry)
measure_roots = repo_entry['measure_roots']

if Dir.exist?(File.join(repo_dir, '.git'))
run_git(%W[-C #{repo_dir} remote set-url origin #{repo_entry['url']}])
else
FileUtils.mkdir_p(File.dirname(repo_dir))
run_git(%W[clone --filter=blob:none --no-checkout #{repo_entry['url']} #{repo_dir}])
end

run_git(%W[-C #{repo_dir} sparse-checkout init --cone])
run_git(['-C', repo_dir, 'sparse-checkout', 'set', *measure_roots])
run_git(%W[-C #{repo_dir} fetch --depth 1 origin #{repo_entry['ref']}])
run_git(%W[-C #{repo_dir} checkout --force FETCH_HEAD])
end

def repo_checkout_dir(repo_entry)
File.join(@install_dir, repo_entry['name'])
end

def run_git(args)
stdout, stderr, status = Open3.capture3('git', *args)
if !status.success?
raise StandardError, "git #{args.join(' ')} failed:\n#{stdout}\n#{stderr}"
end
end

def ordered_unique(paths)
seen = {}
paths.each_with_object([]) do |path, acc|
next if seen[path]

seen[path] = true
acc << path
end
end
end
end
2 changes: 2 additions & 0 deletions lib/buildingsync/makers/osw_arg_populator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,9 @@ def self.populate_replace_baseline_windows_args(osw, facility)

# skip if no window_data
window_data = facility.get_window_data
puts("window data: #{window_data}\n")
if window_data.nil?
puts("WARNING: No window data found, skipping replace_baseline_windows measure.\n")
set_measure_argument.call("__SKIP__", true)
return
end
Expand Down
43 changes: 39 additions & 4 deletions lib/buildingsync/makers/workflow_maker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

require 'buildingsync/extension'
require 'buildingsync/constants'
require 'buildingsync/external_measure_repo_manager'
require 'buildingsync/scenario'
require 'buildingsync/makers/workflow_maker_base'
require 'buildingsync/makers/osw_arg_populator'
Expand Down Expand Up @@ -85,6 +86,8 @@ def prepare_final_xml
def get_available_measures_hash
measures_hash = {}
get_measure_directories_array.each do |potential_measure_path|
next if !Dir.exist?(potential_measure_path)

Dir.chdir(potential_measure_path) do
measures_hash[potential_measure_path] = Dir.glob('*').select { |f| File.directory? f }
end
Expand All @@ -99,15 +102,30 @@ def get_measure_directories_array
model_articulation_instance = OpenStudio::ModelArticulation::Extension.new
ee_measures_instance = OpenStudio::EeMeasures::Extension.new
bldg_sync_instance = BuildingSync::Extension.new
return [common_measures_instance.measures_dir, model_articulation_instance.measures_dir, bldg_sync_instance.measures_dir, ee_measures_instance.measures_dir]
external_repo_manager = BuildingSync::ExternalMeasureRepoManager.new

local_measure_paths = external_repo_manager.local_measure_directories

gem_measure_paths = [
common_measures_instance.measures_dir,
model_articulation_instance.measures_dir,
bldg_sync_instance.measures_dir,
ee_measures_instance.measures_dir
]

external_measure_paths = []
if external_repo_manager.manifest_exists?
external_measure_paths = external_repo_manager.resolved_measure_directories
end

return ordered_unique(local_measure_paths + gem_measure_paths + external_measure_paths)
end

# gets the measure type of a measure given its directory - looking up the measure type in the measure.xml file
# @param measure_dir_name [String] the directory name for the measure, as it appears
# in any of the gems, i.e. openstudio-common-measures-gem/lib/measures/[measure_dir_name]
# @return [String]
def get_measure_type(measure_dir_name)
measure_type = nil
get_measure_directories_array.each do |potential_measure_path|
measure_dir_full_path = "#{potential_measure_path}/#{measure_dir_name}"
if Dir.exist?(measure_dir_full_path)
Expand All @@ -118,12 +136,26 @@ def get_measure_type(measure_dir_name)
measure_xml_doc.elements.each('/measure/attributes/attribute') do |attribute|
attribute_name = attribute.elements['name'].text
if attribute_name == 'Measure Type'
measure_type = attribute.elements['value'].text
return attribute.elements['value'].text
end
end
end
end
return measure_type
return nil
end

def ordered_unique(paths)
seen = {}
ordered = []

paths.each do |path|
next if path.nil? || path.empty? || seen[path]

seen[path] = true
ordered << path
end

return ordered
end

# Based on the MeasureIDs defined by the Scenario, configure the workflow provided
Expand Down Expand Up @@ -337,6 +369,9 @@ def write_baseline_osw(model_dir, epw_file_path)
file = File.read(EMPTY_BASELINE_OSW_PATH)
baseline_osw = JSON.parse(file, symbolize_names: true)

# Keep baseline workflow measure discovery consistent with scenario workflows.
baseline_osw[:measure_paths] = get_measure_directories_array

# parse the facility
@facility.set_all
@facility.set_standard_template
Expand Down
1 change: 0 additions & 1 deletion lib/measures/replace_baseline_windows/LICENSE.md

This file was deleted.

Loading
Loading