Skip to content
writing-openstudio-model-measures logo

Writing OpenStudio Model Measures

writing-openstudio-model-measures

Write OpenStudio ModelMeasures (Ruby scripts that modify .osm files) for building energy models. Use when creating measures, writing measure.rb files, or modifying OpenStudio models programmatically. Targets OpenStudio 3.9 with best practices from NREL documentation.

mattnigh/skills_collection0installs24stars

SKILL.md

Full skill instructions

Writing OpenStudio Model Measures

Expert guidance for creating OpenStudio ModelMeasures - Ruby scripts that programmatically modify building energy models (.osm files).

Target Version: OpenStudio 3.9 Measure Type: ModelMeasure (modifies OSM files) Measure Directory: C:\Users\mcoalson\OpenStudio\Measures Reference: https://nrel.github.io/OpenStudio-user-documentation/reference/measure_writing_guide/

When to Use This Skill

Invoke when you need to:

  • Write a new OpenStudio measure from scratch
  • Modify existing measure.rb files
  • Create measure test files
  • Understand OpenStudio measure structure and best practices
  • Scaffold a complete measure directory with boilerplate code

Quick Start: Creating a New Measure

Use the Node.js scaffolding script to generate a complete measure structure in your measures directory:

cd "C:\Users\mcoalson\OpenStudio\Measures"
node "C:\Users\mcoalson\Documents\WorkPath\.claude\skills\writing-openstudio-model-measures\scripts\scaffold-measure.js" "Your Measure Name"

This creates:

  • Measure directory with snake_case name
  • measure.rb with complete boilerplate
  • measure.xml (auto-generated by OpenStudio)
  • tests/ directory with MiniTest template
  • resources/ directory for helpers
  • README.md stub

Measure Structure

Every ModelMeasure requires:

Required Files:

Recommended Files:

Core Implementation Pattern

Every ModelMeasure must:

  1. Inherit from base class:

    class YourMeasure < OpenStudio::Measure::ModelMeasure
    
  2. Implement five required methods:

    • name() - User-visible title
    • description() - General audience explanation
    • modeler_description() - Technical details for modelers
    • arguments(model) - Define user inputs
    • run(model, runner, user_arguments) - Main logic
  3. Return boolean from run():

    • true if measure succeeds
    • false if measure fails (after logging error)

Required Methods Details

1. name()

def name
  return "Add Window Overhangs"
end
  • Short, descriptive, general
  • User-visible in OpenStudio Application

2. description()

def description
  return "Adds overhangs to all exterior windows based on user-specified projection factor. Improves solar control and reduces cooling loads."
end
  • General audience explanation
  • Include what measure does and benefits

3. modeler_description()

def modeler_description
  return "Iterates through all SubSurfaces with 'Window' type, creates ShadingSurfaceGroup and ShadingSurface objects with projection calculated as window height × projection factor. Assumes windows face outward from building."
end
  • Technical implementation details
  • Assumptions, algorithms, references

4. arguments(model)

def arguments(model)
  args = OpenStudio::Measure::OSArgumentVector.new

  # Double argument
  projection_factor = OpenStudio::Measure::OSArgument.makeDoubleArgument('projection_factor', true)
  projection_factor.setDisplayName('Projection Factor')
  projection_factor.setDescription('Overhang depth as multiple of window height')
  projection_factor.setDefaultValue(0.5)
  args << projection_factor

  # Boolean argument
  apply_to_north = OpenStudio::Measure::OSArgument.makeBoolArgument('apply_to_north', true)
  apply_to_north.setDisplayName('Apply to North-Facing Windows?')
  apply_to_north.setDefaultValue(false)
  args << apply_to_north

  # Choice argument
  choices = OpenStudio::StringVector.new
  choices << "All Windows"
  choices << "South-Facing Only"
  choices << "East and West Only"
  orientation = OpenStudio::Measure::OSArgument.makeChoiceArgument('orientation', choices, true)
  orientation.setDisplayName('Window Orientation Filter')
  orientation.setDefaultValue("All Windows")
  args << orientation

  return args
end

Argument Types:

  • makeDoubleArgument(name, required) - Real numbers
  • makeIntegerArgument(name, required) - Whole numbers
  • makeBoolArgument(name, required) - True/​false
  • makeStringArgument(name, required) - Text input
  • makeChoiceArgument(name, choices_vector, required) - Dropdown selection

5. run(model, runner, user_arguments)

def run(model, runner, user_arguments)
  super(model, runner, user_arguments)

  # 1. Validate and extract arguments
  if !runner.validateUserArguments(arguments(model), user_arguments)
    return false
  end

  projection_factor = runner.getDoubleArgumentValue('projection_factor', user_arguments)
  apply_to_north = runner.getBoolArgumentValue('apply_to_north', user_arguments)
  orientation = runner.getStringArgumentValue('orientation', user_arguments)

  # 2. Additional validation
  if projection_factor < 0 || projection_factor > 5
    runner.registerError("Projection factor must be between 0 and 5, got #{projection_factor}")
    return false
  end

  # 3. Register initial condition
  initial_overhang_count = model.getShadingSurfaces.length
  runner.registerInitialCondition("Model has #{initial_overhang_count} shading surfaces.")

  # 4. Main measure logic
  windows_modified = 0

  model.getSubSurfaces.each do |sub_surface|
    next unless sub_surface.subSurfaceType == "FixedWindow" || sub_surface.subSurfaceType == "OperableWindow"

    # Your logic here
    windows_modified += 1
    runner.registerInfo("Added overhang to window: #{sub_surface.name}")
  end

  # 5. Handle not applicable case
  if windows_modified == 0
    runner.registerAsNotApplicable("No windows found in model.")
    return true
  end

  # 6. Register final condition
  final_overhang_count = model.getShadingSurfaces.length
  runner.registerFinalCondition("Added #{final_overhang_count - initial_overhang_count} overhangs to #{windows_modified} windows.")

  return true
end

Logging and User Communication

Use runner methods to communicate:

Info Messages (measure continues):

runner.registerInfo("Processing 42 windows...")

Warning Messages (measure continues):

runner.registerWarning("Window '#{name}' has no parent surface, skipping.")

Error Messages (measure stops):

runner.registerError("Invalid projection factor: #{value}")
return false

Condition Tracking:

runner.registerInitialCondition("Before: #{count} objects")
runner.registerFinalCondition("After: #{count} objects, added #{delta}")

Not Applicable:

runner.registerAsNotApplicable("No HVAC systems found in model.")
return true  # Still return true!

OpenStudio API Patterns

Getting Objects from Model

Non-unique objects (can have multiple instances):

# Get all instances
spaces = model.getSpaces
thermal_zones = model.getThermalZones

# Get by name
space = model.getSpaceByName("Office 101")
if space.is_initialized
  space_obj = space.get
  # Use space_obj
end

Unique objects (only one instance):

# Get the single instance
building = model.getBuilding
site = model.getSite

Safe Optional Handling

OpenStudio uses boost::optional types extensively. Always check before using .get():

# UNSAFE - will crash if empty
zone = space.thermalZone.get  # BAD!

# SAFE - check first
if !space.thermalZone.empty?
  zone = space.thermalZone.get
  runner.registerInfo("Space is in zone: #{zone.name}")
else
  runner.registerWarning("Space has no thermal zone assigned.")
end

# Alternative safe pattern
if space.thermalZone.is_initialized
  zone = space.thermalZone.get
  # Use zone
end

Common Model Queries

# Spaces and Zones
model.getSpaces.each do |space|
  puts space.name
end

model.getThermalZones.each do |zone|
  puts zone.name
end

# Surfaces
model.getSurfaces.each do |surface|
  puts "#{surface.name}: #{surface.surfaceType}"
end

# SubSurfaces (windows, doors)
model.getSubSurfaces.each do |sub_surface|
  puts "#{sub_surface.name}: #{sub_surface.subSurfaceType}"
end

# HVAC Equipment
model.getAirLoopHVACs.each do |air_loop|
  puts air_loop.name
end

model.getPlantLoops.each do |plant_loop|
  puts plant_loop.name
end

# Constructions and Materials
model.getConstructions.each do |construction|
  puts construction.name
end

# Schedules
model.getScheduleRulesets.each do |schedule|
  puts schedule.name
end

Testing with MiniTest

Every measure should include tests in tests/​measure_test.rb.

Run tests:

cd /​path/​to/​your_measure
ruby tests/​measure_test.rb

See ./​templates/​measure-test-template.rb for complete test structure.

Basic test pattern:

def test_valid_arguments
  # Load test model
  model = load_test_model

  # Create measure instance
  measure = YourMeasure.new

  # Get arguments
  arguments = measure.arguments(model)

  # Set argument values
  argument_map = OpenStudio::Measure.convertOSArgumentVectorToMap(arguments)
  projection_factor = arguments[0].clone
  assert(projection_factor.setValue(0.5))
  argument_map['projection_factor'] = projection_factor

  # Run measure
  measure.run(model, runner, argument_map)
  result = runner.result

  # Assertions
  assert_equal('Success', result.value.valueName)
  assert(result.info.size > 0)
  assert_equal(0, result.warnings.size)
end

Best Practices

Code Quality

  • Follow Ruby Style Guide
  • Use 2-space indentation
  • Keep methods focused and concise
  • Add comments for complex logic

Input Validation

  • Always validate user arguments
  • Check value ranges and logical constraints
  • Provide clear error messages
  • Use runner.validateUserArguments() first

Error Handling

  • Check for empty optionals before calling .get()
  • Handle edge cases (empty model, missing objects)
  • Use registerAsNotApplicable() when measure doesn't apply
  • Return false only after logging error with registerError()

Performance

  • Avoid nested loops when possible
  • Cache frequently accessed values
  • Use model.getObjectsByType() for specific object types

User Experience

  • Use clear, descriptive argument names
  • Set sensible default values
  • Log progress for long-running operations
  • Report initial and final conditions

OpenStudio 3.9 Specific Notes

Ruby Version: OpenStudio 3.9 uses Ruby 2.7.2 API Documentation: https://openstudio-sdk-documentation.s3.amazonaws.com/index.html

Common gotchas:

  • Method names are camelCase (OpenStudio convention), not snake_case (Ruby convention)
  • Units matter - always check if arguments need IP or SI units
  • measure.xml auto-regenerates - don't manually edit structure, only metadata
  • Test with multiple model types (residential, commercial, different HVAC systems)

Common Measure Patterns

Iterating Through Spaces

model.getSpaces.each do |space|
  runner.registerInfo("Processing space: #{space.name}")

  # Get space properties
  floor_area = space.floorArea  # m²

  # Get thermal zone
  if space.thermalZone.is_initialized
    zone = space.thermalZone.get
    runner.registerInfo("  Zone: #{zone.name}")
  end

  # Get space type
  if space.spaceType.is_initialized
    space_type = space.spaceType.get
    runner.registerInfo("  Type: #{space_type.name}")
  end
end

Modifying Constructions

model.getSurfaces.each do |surface|
  next unless surface.surfaceType == "RoofCeiling"
  next unless surface.outsideBoundaryCondition == "Outdoors"

  # Create or get construction
  new_construction = model.getConstructionByName("High Performance Roof")
  if new_construction.is_initialized
    surface.setConstruction(new_construction.get)
    runner.registerInfo("Updated roof construction: #{surface.name}")
  end
end

Working with Schedules

# Get schedule by name
schedule = model.getScheduleRulesetByName("Office Occupancy")
if schedule.is_initialized
  sched = schedule.get
  # Modify schedule rules
end

# Create new schedule
new_schedule = OpenStudio::Model::ScheduleRuleset.new(model)
new_schedule.setName("Custom Schedule")
new_schedule.defaultDaySchedule.setName("Custom Default")
# Add time-value pairs
new_schedule.defaultDaySchedule.addValue(OpenStudio::Time.new(0,8,0,0), 0)
new_schedule.defaultDaySchedule.addValue(OpenStudio::Time.new(0,18,0,0), 1)

Upgrading to OpenStudio 3.10 (Future)

When ready to upgrade, note these areas that may require changes:

  • API method deprecations
  • Ruby version changes
  • New object types or properties
  • Updated testing frameworks

Document upgrade process in ./​upgrading-to-310.md when that time comes.

Additional Resources

Templates:

Scripts:

External References:

Workflow Summary

  1. Scaffold measure using Node.js script or manually create directory
  2. Write measure.rb using template as starting point
  3. Implement five required methods (name, description, modeler_description, arguments, run)
  4. Add input validation and error handling
  5. Test with MiniTest - create tests in tests/ directory
  6. Run in OpenStudio Application to generate/​update measure.xml
  7. Iterate and refine based on testing with various models

Last Updated: 2025-11-18 Target OpenStudio Version: 3.9.0 Ruby Version: 2.7.2

More skills from mattnigh

spring-framework-patterns logo
mattnigh/skills_collection

spring-framework-patterns

Comprehensive Spring Framework and Spring Boot best practices including dependency injection patterns, bean lifecycle and scopes, REST API development, Spring Data JPA, service layer design, Spring Security, testing strategies, caching, AOP, async processing, error handling, and common anti-patte...

24 0
View

TypeScript厳格モードによる型安全性設計を専門とするスキル。 📖 参照書籍: - 『Effective TypeScript』(Dan Vanderkam): 型設計 📚 リソース参照: - `resources/Level1_basics.md`: レベル1の基礎ガイド - `resources/Level2_intermediate.md`: レベル2の実務ガイド - `resources/Level3_advanced.md`: レベル3の応用ガイド - `resources/Level4_expert.md`: レベル4の専門ガイド - `resources/di...

24 0
View
deploying-cloud-k8s logo
mattnigh/skills_collection

deploying-cloud-k8s

Deploys applications to cloud Kubernetes (AKS/GKE/DOKS) with CI/CD pipelines. Use when deploying to production, setting up GitHub Actions, troubleshooting deployments. Covers build-time vs runtime vars, architecture matching, and battle-tested debugging.

24 0
View
moai-alfred-agent-guide logo
mattnigh/skills_collection

moai-alfred-agent-guide

19-agent team structure, decision trees for agent selection, Haiku vs Sonnet model selection, and agent collaboration principles. Use when deciding which sub-agent to invoke, understanding team responsibilities, or learning multi-agent orchestration.

24 0
View
mermaid-diagram-generator logo
mattnigh/skills_collection

mermaid-diagram-generator

Converts architecture descriptions, module specs, or workflow docs into Mermaid diagrams. Use when visualizing brick module relationships, workflows (DDD, investigation), or system architecture. Supports: flowcharts, sequence diagrams, class diagrams, state machines, entity relationship diagrams,...

24 0
View
pre-flight-check logo
mattnigh/skills_collection

pre-flight-check

INVOKE FIRST before any code work. Validates git workflow (branch, issue, worklog) and checks approach. Use at START of every task and END before completing. Prevents skipped steps.

24 0
View
browser-dev-tools logo
mattnigh/skills_collection

browser-dev-tools

This skill should be used when working with browser-rendered artifacts (i.e. Bun, React/React Native) to proactively validate that development work on localhost is being built appropriately and to support debugging browser-rendered content. Use this skill after making frontend changes to verify t...

24 0
View
lead-dev logo
mattnigh/skills_collection

lead-dev

Lead Développeur - Coordination technique opérationnelle, code review, mentoring et livraison. Pair de web-dev-process au niveau OPÉRATIONS.

24 0
View
worker-monitor logo
mattnigh/skills_collection

worker-monitor

Monitor Docker workers, RQ queue health, and auto-scale workers (max 4). Use when: checking job progress, monitoring queue depth, scaling workers up/down, diagnosing slow processing, or waiting for jobs to complete. Referenced by data-quality skill during reprocessing.

24 0
View
quality-advisor logo
mattnigh/skills_collection

quality-advisor

Proactive quality guidance system that monitors artifact creation and provides real-time feedback on documentation quality

24 0
View
bump-version logo
mattnigh/skills_collection

bump-version

This skill should be used when the user wants to bump the version number in the workspace. It updates versions across all pyproject.toml files (root, tde, and tda packages) and the CHANGELOG.md to keep them synchronized.

24 0
View

Popular AI tools

Kaiber logo
Video

Kaiber

Generate, edit, and beat-sync AI video with leading models in one workspace.

Paid
View
Vimcal logo
Productivity

Vimcal

The world's fastest calendar for remote work

Free
View

Transform Your Design with AI Designer by ImgCreator.ai

Freemium
View
Akool AI logo
Content & writing

Akool AI

Revolutionizing Video Production with AI-Powered Creativity

Paid
View

Extend an image past the frame and let AI fill the new aspect ratio.

Freemium
View
StarByFace logo
Security

StarByFace

Discover your celebrity doppelgänger with StarByFace!

Free
View
C

ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.

Freemium
View
Opus Clip logo
Coding & apps

Opus Clip

Opus.ai: Revolutionize Your Web Experience

Free
View