diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index f99c32d..fd68fb5 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -19,7 +19,7 @@ BOSS sits between BuildingSync XML data and OpenStudio workflow execution. Sibli - `../schema` — BuildingSync schema source and examples. Use it to verify XML structure, element names, `ScenarioType`, `PackageOfMeasures`, `Measure`, `MeasureID`, and schema-version behavior. Do not edit it unless the task asks for schema work. - `../BuildingSync-gem` — legacy Ruby translator. Use it for workflow concepts and mapping ideas, but verify every behavior against current BOSS dependencies and BuildingSync v2.7 fixtures. -- `../buildingsync-measures-gem` — related BuildingSync/OpenStudio measure work. Use it as reference for measure patterns. +- `../buildingsync-measures-gem` — custom BuildingSync OpenStudio measures. Preferred measure source for scenario mappings when a measure fits. New measures BOSS needs that do not exist elsewhere belong here, not in BOSS. Only map to measures merged into the ref BOSS loads. - `../openstudio-standards` — OpenStudio standards logic used by the `openstudio-standards` gem. Do not edit it from BOSS unless the issue is proven upstream and the user asks for that change. - OpenStudio measure gems declared in `BOSS.gemspec` — `openstudio-common-measures`, `openstudio-ee`, `openstudio-extension`, `openstudio-model-articulation`, and `openstudio-standards`. Verify scenario mapping measure names and arguments against the installed versions used by BOSS. diff --git a/docs/scenario_workflow_translation_plan.md b/docs/scenario_workflow_translation_plan.md index 5017e89..2aa88fc 100644 --- a/docs/scenario_workflow_translation_plan.md +++ b/docs/scenario_workflow_translation_plan.md @@ -102,7 +102,17 @@ Lookup order: 1. `SystemCategoryAffected` plus `MeasureName`. 2. Technology category element plus `MeasureName`, when `SystemCategoryAffected` is missing or not useful. -The legacy `../BuildingSync-gem` mapping can be used as background, but do not copy it directly. Every measure directory and argument added to BOSS must be checked against the OpenStudio gems declared in `BOSS.gemspec`. +The legacy `../BuildingSync-gem` mapping can be used as background, but do not copy it directly. + +Measure source order for new mappings: + +1. `../buildingsync-measures-gem` custom BuildingSync measures, when one fits the BuildingSync measure semantics. +2. OpenStudio measure gems declared in `BOSS.gemspec`. +3. If neither has an applicable measure, add one to `buildingsync-measures-gem` instead of BOSS. + +Verify every measure directory and argument against the source BOSS will load: the `BOSS.gemspec` gem versions, or the merged `buildingsync-measures-gem` ref. Do not map to measures that exist only on an unmerged branch. + +Keep relative-change mappings (for example "reduce by percentage") on relative measures. Do not swap them for absolute-target measures unless the measure skips models that already meet the target. Conditional arguments should be data-driven. Use context from the reader, such as building type or principal HVAC system type, instead of per-measure Ruby condition chains. @@ -171,8 +181,8 @@ Use this roadmap as the tracker. Keep `Status` and `Delivered by` current when a ### PR 5: Initial Mapping JSON -- Status: Planned -- Delivered by: - +- Status: Done +- Delivered by: PR #10 branch - Scope: Add `lib/BOSS/scenario_measure_map.json` with a small verified mapping set from `building_151.xml`. - Done when: The JSON is valid and each entry has a source category/name, target `measure_dir_name`, and arguments. - Avoid: Copying legacy mappings without checking current measure directories and arguments. @@ -229,8 +239,8 @@ Use this roadmap as the tracker. Keep `Status` and `Delivered by` current when a - Status: Planned - Delivered by: - -- Scope: Deep-copy baseline OSW and append mapped retrofit steps in deterministic order. -- Done when: OSW tests assert baseline steps plus expected mapped measure steps and arguments. +- Scope: Deep-copy baseline OSW and append mapped retrofit steps in deterministic order. Make sure `buildingsync-measures-gem` measures are on the OSW `measure_paths`; `develop` already provides `config/external_measure_repos.yml` for this. +- Done when: OSW tests assert baseline steps plus expected mapped measure steps and arguments, and every mapped `measure_dir_name` resolves from the OSW `measure_paths`. - Avoid: Mutating the baseline OSW, omitting baseline creation steps, or using nondeterministic step order. ### PR 13: Generic OSW Step Helpers @@ -334,6 +344,17 @@ Use this section for stable facts future PRs need. Keep entries short and tied t - Verification: `C:\Ruby32-x64\bin\ruby.exe -S bundle exec rubocop lib/BOSS/buildingsync_reader/buildingsync_reader.rb spec/tests/unit/buildingsync_reader_spec.rb` - still reports existing style debt and new-cop configuration warnings. - Follow-up: PR 5 can add mapping JSON. Mapping-specific warnings remain PR 6/PR 9 scope. +### PR 5: Initial Mapping JSON + +- Status: Done +- Delivered by: PR #10 branch +- Handoff: `lib/BOSS/scenario_measure_map.json` maps 12 `building_151.xml` category/name pairs to OSW-shaped steps with `measure_dir_name` and an argument hash that includes `__SKIP__`. Source strings match reader output exactly. +- Handoff: Roof and wall absolute insulation targets use `buildingsync-measures-gem` `modify_envelope_insulation` with SI R-values (R-30 IP = 5.28, R-25 IP = 4.40 m²K/W). Other mappings use `openstudio-ee` or `openstudio-common-measures`. Relative mappings such as air sealing, ceiling insulation by percentage, and thermal bypasses stay on relative measures. +- Deferred: Daylight controls, package unit replacement, burner replacement, and boiler replacement. The `buildingsync-measures-gem` split HVAC measures (`modify_existing_hvac_equipment_efficiencies`, `modify_existing_plant_equipment`, `hvac_packaged_rooftop_*`) need exact model object or zone names. Map them in PR 8 with context rules after `buildingsync-measures-gem` PR #3 merges. +- Verification: JSON parse passed; every mapped measure directory and argument was checked against installed `BOSS.gemspec` gems and `buildingsync-measures-gem` `develop`. +- Verification: `C:\Ruby32-x64\bin\ruby.exe -S bundle exec rspec spec/tests/unit/scenario_measure_map_spec.rb`. +- Follow-up: PR 6 adds `BOSS::ScenarioMeasureMapper`. PR 12 must put `buildingsync-measures-gem` on OSW `measure_paths`. + ## Verification Commands Use the narrowest command that proves the current PR. Broaden only when the change justifies it. diff --git a/lib/BOSS/scenario_measure_map.json b/lib/BOSS/scenario_measure_map.json new file mode 100644 index 0000000..dbcedfb --- /dev/null +++ b/lib/BOSS/scenario_measure_map.json @@ -0,0 +1,177 @@ +{ + "schema_version": "1.0", + "lookup_order": [ + "system_category_affected_and_measure_name" + ], + "mappings": [ + { + "system_category_affected": "Air Distribution", + "technology_category_element_name": "OtherHVAC", + "measure_name": "Add or repair economizer", + "steps": [ + { + "measure_dir_name": "EnableEconomizerControl", + "arguments": { + "__SKIP__": false, + "economizer_type": "FixedDryBulb" + } + } + ] + }, + { + "system_category_affected": "Air Distribution", + "technology_category_element_name": "OtherHVAC", + "measure_name": "Install demand control ventilation", + "steps": [ + { + "measure_dir_name": "EnableDemandControlledVentilation", + "arguments": { + "__SKIP__": false, + "dcv_type": "EnableDCV" + } + } + ] + }, + { + "system_category_affected": "Ceiling", + "technology_category_element_name": "BuildingEnvelopeModifications", + "measure_name": "Increase ceiling insulation", + "steps": [ + { + "measure_dir_name": "IncreaseInsulationRValueForRoofsByPercentage", + "arguments": { + "__SKIP__": false, + "r_value": 20 + } + } + ] + }, + { + "system_category_affected": "General Controls and Operations", + "technology_category_element_name": "OtherHVAC", + "measure_name": "Upgrade operating protocols, calibration, and/or sequencing", + "steps": [ + { + "measure_dir_name": "AdjustThermostatSetpointsByDegrees", + "arguments": { + "__SKIP__": false, + "cooling_adjustment": 1.0, + "heating_adjustment": -1.0 + } + } + ] + }, + { + "system_category_affected": "Lighting", + "technology_category_element_name": "LightingImprovements", + "measure_name": "Add occupancy sensors", + "steps": [ + { + "measure_dir_name": "ReduceLightingLoadsByPercentage", + "arguments": { + "__SKIP__": false, + "lighting_power_reduction_percent": 5 + } + } + ] + }, + { + "system_category_affected": "Lighting", + "technology_category_element_name": "LightingImprovements", + "measure_name": "Retrofit with light emitting diode technologies", + "steps": [ + { + "measure_dir_name": "SetLightingLoadsByLPD", + "arguments": { + "__SKIP__": false, + "lpd": 0.6 + } + } + ] + }, + { + "system_category_affected": "Plug Load", + "technology_category_element_name": "PlugLoadReductions", + "measure_name": "Install plug load controls", + "steps": [ + { + "measure_dir_name": "ReduceElectricEquipmentLoadsByPercentage", + "arguments": { + "__SKIP__": false, + "elecequip_power_reduction_percent": 20.0 + } + } + ] + }, + { + "system_category_affected": "Refrigeration", + "technology_category_element_name": "Refrigeration", + "measure_name": "Replace ice/refrigeration equipment with high efficiency units", + "steps": [ + { + "measure_dir_name": "ReduceElectricEquipmentLoadsByPercentage", + "arguments": { + "__SKIP__": false, + "elecequip_power_reduction_percent": 5 + } + } + ] + }, + { + "system_category_affected": "Roof", + "technology_category_element_name": "BuildingEnvelopeModifications", + "measure_name": "Increase roof insulation", + "steps": [ + { + "measure_dir_name": "modify_envelope_insulation", + "arguments": { + "__SKIP__": false, + "roof_target_rvalue": 5.28 + } + } + ] + }, + { + "system_category_affected": "Wall", + "technology_category_element_name": "BuildingEnvelopeModifications", + "measure_name": "Air seal envelope", + "steps": [ + { + "measure_dir_name": "ReduceSpaceInfiltrationByPercentage", + "arguments": { + "__SKIP__": false, + "space_infiltration_reduction_percent": 30.0 + } + } + ] + }, + { + "system_category_affected": "Wall", + "technology_category_element_name": "BuildingEnvelopeModifications", + "measure_name": "Increase wall insulation", + "steps": [ + { + "measure_dir_name": "modify_envelope_insulation", + "arguments": { + "__SKIP__": false, + "wall_target_rvalue": 4.4 + } + } + ] + }, + { + "system_category_affected": "Wall", + "technology_category_element_name": "BuildingEnvelopeModifications", + "measure_name": "Insulate thermal bypasses", + "steps": [ + { + "measure_dir_name": "IncreaseInsulationRValueForExteriorWallsByPercentage", + "arguments": { + "__SKIP__": false, + "r_value": 20 + } + } + ] + } + ] +} diff --git a/spec/tests/unit/scenario_measure_map_spec.rb b/spec/tests/unit/scenario_measure_map_spec.rb new file mode 100644 index 0000000..187842d --- /dev/null +++ b/spec/tests/unit/scenario_measure_map_spec.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +require 'json' + +SCENARIO_MEASURE_MAP_PATH = File.expand_path('../../../lib/BOSS/scenario_measure_map.json', __dir__) + +RSpec.describe 'scenario_measure_map.json' do + let(:scenario_measure_map) { JSON.parse(File.read(SCENARIO_MEASURE_MAP_PATH)) } + let(:mappings) { scenario_measure_map.fetch('mappings') } + + it 'defines required mapping fields for each entry' do + expect(scenario_measure_map.fetch('schema_version')).to eq '1.0' + expect(mappings).not_to be_empty + + mappings.each do |mapping| + expect(mapping.fetch('system_category_affected')).not_to be_empty + expect(mapping.fetch('measure_name')).not_to be_empty + expect(mapping.fetch('steps')).not_to be_empty + + mapping.fetch('steps').each do |step| + expect(step.fetch('measure_dir_name')).not_to be_empty + expect(step.fetch('arguments')).to be_a Hash + end + end + end + + it 'uses OSW-shaped argument hashes without conditional mapping rules' do + mappings.each do |mapping| + mapping.fetch('steps').each do |step| + expect(step.fetch('arguments')).to include('__SKIP__') + expect(step.fetch('arguments')).not_to include('condition') + end + end + end + + it 'covers the verified initial building_151 measure set' do + expected_sources = [ + ['Air Distribution', 'Add or repair economizer'], + ['Air Distribution', 'Install demand control ventilation'], + ['Ceiling', 'Increase ceiling insulation'], + ['General Controls and Operations', 'Upgrade operating protocols, calibration, and/or sequencing'], + ['Lighting', 'Add occupancy sensors'], + ['Lighting', 'Retrofit with light emitting diode technologies'], + ['Plug Load', 'Install plug load controls'], + ['Refrigeration', 'Replace ice/refrigeration equipment with high efficiency units'], + ['Roof', 'Increase roof insulation'], + ['Wall', 'Air seal envelope'], + ['Wall', 'Increase wall insulation'], + ['Wall', 'Insulate thermal bypasses'] + ] + + actual_sources = mappings.map do |mapping| + [mapping.fetch('system_category_affected'), mapping.fetch('measure_name')] + end + + expect(actual_sources).to eq expected_sources + end + + it 'uses buildingsync-measures-gem envelope insulation targets in SI units' do + targets = mappings.to_h do |mapping| + [[mapping.fetch('system_category_affected'), mapping.fetch('measure_name')], mapping.fetch('steps').first] + end + + roof_step = targets.fetch(['Roof', 'Increase roof insulation']) + wall_step = targets.fetch(['Wall', 'Increase wall insulation']) + + expect(roof_step.fetch('measure_dir_name')).to eq 'modify_envelope_insulation' + expect(roof_step.fetch('arguments')).to include('roof_target_rvalue' => 5.28) + expect(wall_step.fetch('measure_dir_name')).to eq 'modify_envelope_insulation' + expect(wall_step.fetch('arguments')).to include('wall_target_rvalue' => 4.4) + end +end