Class: AddDaylightSensors
- Inherits:
-
OpenStudio::Measure::ModelMeasure
- Object
- OpenStudio::Measure::ModelMeasure
- AddDaylightSensors
- Defined in:
- lib/measures/AddDaylightSensors/measure.rb
Overview
start the measure
Instance Method Summary collapse
-
#arguments(model) ⇒ Object
define the arguments that the user will input.
-
#description ⇒ Object
human readable description.
-
#get_total_costs_for_objects(objects) ⇒ Object
helper that loops through lifecycle costs getting total costs under “Construction” or “Salvage” category and add to counter if occurs during year 0.
-
#modeler_description ⇒ Object
human readable description of modeling approach.
-
#name ⇒ Object
define the name that a user will see.
-
#neat_numbers(number, roundto = 2) ⇒ Object
short def to make numbers pretty (converts 4125001.25641 to 4,125,001.26 or 4,125,001).
-
#run(model, runner, user_arguments) ⇒ Object
define what happens when the measure is run.
Instance Method Details
#arguments(model) ⇒ Object
define the arguments that the user will input
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 |
# File 'lib/measures/AddDaylightSensors/measure.rb', line 26 def arguments(model) args = OpenStudio::Measure::OSArgumentVector.new # make a choice argument for model objects space_type_handles = OpenStudio::StringVector.new space_type_display_names = OpenStudio::StringVector.new # putting model object and names into hash space_type_args = model.getSpaceTypes space_type_args_hash = {} space_type_args.each do |space_type_arg| space_type_args_hash[space_type_arg.name.to_s] = space_type_arg end # looping through sorted hash of model objects space_type_args_hash.sort.map do |key, value| # only include if space type is used in the model if !value.spaces.empty? space_type_handles << value.handle.to_s space_type_display_names << key end end # make a choice argument for space type space_type = OpenStudio::Measure::OSArgument.makeChoiceArgument('space_type', space_type_handles, space_type_display_names, true) space_type.setDisplayName('Add Daylight Sensors to Spaces of This Space Type') args << space_type # make an argument for setpoint setpoint = OpenStudio::Measure::OSArgument.makeDoubleArgument('setpoint', true) setpoint.setDisplayName('Daylighting Setpoint') setpoint.setUnits('fc') setpoint.setDefaultValue(45.0) args << setpoint # make an argument for control_type chs = OpenStudio::StringVector.new chs << 'None' chs << 'Continuous' chs << 'Stepped' chs << 'Continuous/Off' control_type = OpenStudio::Measure::OSArgument.makeChoiceArgument('control_type', chs) control_type.setDisplayName('Daylighting Control Type') control_type.setDefaultValue('Continuous/Off') args << control_type # make an argument for min_power_fraction min_power_fraction = OpenStudio::Measure::OSArgument.makeDoubleArgument('min_power_fraction', true) min_power_fraction.setDisplayName('Daylighting Minimum Input Power Fraction') min_power_fraction.setDescription('min = 0 max = 0.6') min_power_fraction.setDefaultValue(0.3) args << min_power_fraction # make an argument for min_light_fraction min_light_fraction = OpenStudio::Measure::OSArgument.makeDoubleArgument('min_light_fraction', true) min_light_fraction.setDisplayName('Daylighting Minimum Light Output Fraction') min_light_fraction.setDescription('min = 0 max = 0.6') min_light_fraction.setDefaultValue(0.2) args << min_light_fraction # make an argument for fraction_zone_controlled fraction_zone_controlled = OpenStudio::Measure::OSArgument.makeDoubleArgument('fraction_zone_controlled', true) fraction_zone_controlled.setDisplayName('Fraction of zone controlled by daylight sensors') fraction_zone_controlled.setDefaultValue(1.0) args << fraction_zone_controlled # make an argument for height height = OpenStudio::Measure::OSArgument.makeDoubleArgument('height', true) height.setDisplayName('Sensor Height') height.setUnits('inches') height.setDefaultValue(30.0) args << height # make an argument for material and installation cost material_cost = OpenStudio::Measure::OSArgument.makeDoubleArgument('material_cost', true) material_cost.setDisplayName('Material and Installation Costs per Space for Daylight Sensor') material_cost.setUnits('$') material_cost.setDefaultValue(0.0) args << material_cost # make an argument for demolition cost demolition_cost = OpenStudio::Measure::OSArgument.makeDoubleArgument('demolition_cost', true) demolition_cost.setDisplayName('Demolition Costs per Space for Daylight Sensor') demolition_cost.setUnits('$') demolition_cost.setDefaultValue(0.0) args << demolition_cost # make an argument for duration in years until costs start years_until_costs_start = OpenStudio::Measure::OSArgument.makeIntegerArgument('years_until_costs_start', true) years_until_costs_start.setDisplayName('Years Until Costs Start') years_until_costs_start.setUnits('whole years') years_until_costs_start.setDefaultValue(0) args << years_until_costs_start # make an argument to determine if demolition costs should be included in initial construction demo_cost_initial_const = OpenStudio::Measure::OSArgument.makeBoolArgument('demo_cost_initial_const', true) demo_cost_initial_const.setDisplayName('Demolition Costs Occur During Initial Construction') demo_cost_initial_const.setDefaultValue(false) args << demo_cost_initial_const # make an argument for expected life expected_life = OpenStudio::Measure::OSArgument.makeIntegerArgument('expected_life', true) expected_life.setDisplayName('Expected Life') expected_life.setUnits('whole years') expected_life.setDefaultValue(20) args << expected_life # make an argument for o&m cost om_cost = OpenStudio::Measure::OSArgument.makeDoubleArgument('om_cost', true) om_cost.setDisplayName('O & M Costs per Space for Daylight Sensor') om_cost.setUnits('$') om_cost.setDefaultValue(0.0) args << om_cost # make an argument for o&m frequency om_frequency = OpenStudio::Measure::OSArgument.makeIntegerArgument('om_frequency', true) om_frequency.setDisplayName('O & M Frequency') om_frequency.setUnits('whole years') om_frequency.setDefaultValue(1) args << om_frequency return args end |
#description ⇒ Object
human readable description
16 17 18 |
# File 'lib/measures/AddDaylightSensors/measure.rb', line 16 def description return 'This measure will add daylighting controls to spaces that that have space types assigned with names containing the string in the argument. You can also add a cost per space for sensors added to the model.' end |
#get_total_costs_for_objects(objects) ⇒ Object
helper that loops through lifecycle costs getting total costs under “Construction” or “Salvage” category and add to counter if occurs during year 0
257 258 259 260 261 262 263 264 265 266 267 268 269 270 |
# File 'lib/measures/AddDaylightSensors/measure.rb', line 257 def get_total_costs_for_objects(objects) counter = 0 objects.each do |object| object_LCCs = object.lifeCycleCosts object_LCCs.each do |object_LCC| if (object_LCC.category == 'Construction') || (object_LCC.category == 'Salvage') if object_LCC.yearsFromStart == 0 counter += object_LCC.totalCost end end end end return counter end |
#modeler_description ⇒ Object
human readable description of modeling approach
21 22 23 |
# File 'lib/measures/AddDaylightSensors/measure.rb', line 21 def modeler_description return "Make an array of the spaces that meet the criteria. Locate the sensor x and y values by averaging the min and max X and Y values from floor surfaces in the space. If a space already has a daylighting control, do not add a new one and leave the original in place. Warn the user if the space isn't assigned to a thermal zone, or if the space doesn't have any translucent surfaces. Note that the cost is added to the space not the sensor. If the sensor is removed at a later date, the cost will remain." end |
#name ⇒ Object
define the name that a user will see
11 12 13 |
# File 'lib/measures/AddDaylightSensors/measure.rb', line 11 def name return 'Add Daylight Sensor at the Center of Spaces with a Specified Space Type Assigned' end |
#neat_numbers(number, roundto = 2) ⇒ Object
short def to make numbers pretty (converts 4125001.25641 to 4,125,001.26 or 4,125,001). The definition be called through this measure
246 247 248 249 250 251 252 253 254 |
# File 'lib/measures/AddDaylightSensors/measure.rb', line 246 def neat_numbers(number, roundto = 2) # round to 0 or 2) if roundto == 2 number = format '%.2f', number else number = number.round end # regex to add commas number.to_s.reverse.gsub(/([0-9]{3}(?=([0-9])))/, '\\1,').reverse end |
#run(model, runner, user_arguments) ⇒ Object
define what happens when the measure is run
151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 |
# File 'lib/measures/AddDaylightSensors/measure.rb', line 151 def run(model, runner, user_arguments) super(model, runner, user_arguments) # use the built-in error checking if !runner.validateUserArguments(arguments(model), user_arguments) return false end # assign the user inputs to variables space_type = runner.getOptionalWorkspaceObjectChoiceValue('space_type', user_arguments, model) setpoint = runner.getDoubleArgumentValue('setpoint', user_arguments) control_type = runner.getStringArgumentValue('control_type', user_arguments) min_power_fraction = runner.getDoubleArgumentValue('min_power_fraction', user_arguments) min_light_fraction = runner.getDoubleArgumentValue('min_light_fraction', user_arguments) fraction_zone_controlled = runner.getDoubleArgumentValue('fraction_zone_controlled', user_arguments) height = runner.getDoubleArgumentValue('height', user_arguments) material_cost = runner.getDoubleArgumentValue('material_cost', user_arguments) demolition_cost = runner.getDoubleArgumentValue('demolition_cost', user_arguments) years_until_costs_start = runner.getIntegerArgumentValue('years_until_costs_start', user_arguments) demo_cost_initial_const = runner.getBoolArgumentValue('demo_cost_initial_const', user_arguments) expected_life = runner.getIntegerArgumentValue('expected_life', user_arguments) om_cost = runner.getDoubleArgumentValue('om_cost', user_arguments) om_frequency = runner.getIntegerArgumentValue('om_frequency', user_arguments) # check the space_type for reasonableness if space_type.empty? handle = runner.getStringArgumentValue('space_type', user_arguments) if handle.empty? runner.registerError('No SpaceType was chosen.') else runner.registerError("The selected space type with handle '#{handle}' was not found in the model. It may have been removed by another measure.") end return false else if !space_type.get.to_SpaceType.empty? space_type = space_type.get.to_SpaceType.get else runner.registerError('Script Error - argument not showing up as space type.') return false end end # check the setpoint for reasonableness if (setpoint < 0) || (setpoint > 9999) # dfg need input on good value runner.registerError("A setpoint of #{setpoint} foot-candles is outside the measure limit.") return false elsif setpoint > 999 runner.registerWarning("A setpoint of #{setpoint} foot-candles is abnormally high.") # dfg need input on good value end # check the min_power_fraction for reasonableness if (min_power_fraction < 0.0) || (min_power_fraction > 0.6) runner.registerError("The requested minimum input power fraction of #{min_power_fraction} for continuous dimming control is outside the acceptable range of 0 to 0.6.") return false end # check the min_light_fraction for reasonableness if (min_light_fraction < 0.0) || (min_light_fraction > 0.6) runner.registerError("The requested minimum light output fraction of #{min_light_fraction} for continuous dimming control is outside the acceptable range of 0 to 0.6.") return false end # check the height for reasonableness if (height < -360) || (height > 360) # neg ok because space origin may not be floor runner.registerError("A setpoint of #{height} inches is outside the measure limit.") return false elsif height > 72 runner.registerWarning("A setpoint of #{height} inches is abnormally high.") elseif height < 0 runner.registerWarning('Typically the sensor height should be a positive number, however if your space origin is above the floor then a negative sensor height may be approriate.') end # set flags to use later costs_requested = false warning_cost_assign_to_space = false # check costs for reasonableness if material_cost.abs + demolition_cost.abs + om_cost.abs == 0 runner.registerInfo('No costs were requested for Daylight Sensors.') else costs_requested = true end # check lifecycle arguments for reasonableness if (years_until_costs_start < 0) && (years_until_costs_start > expected_life) runner.registerError('Years until costs start should be a non-negative integer less than Expected Life.') end if (expected_life < 1) && (expected_life > 100) runner.registerError('Choose an integer greater than 0 and less than or equal to 100 for Expected Life.') end if om_frequency < 1 runner.registerError('Choose an integer greater than 0 for O & M Frequency.') end # short def to make numbers pretty (converts 4125001.25641 to 4,125,001.26 or 4,125,001). The definition be called through this measure def neat_numbers(number, roundto = 2) # round to 0 or 2) if roundto == 2 number = format '%.2f', number else number = number.round end # regex to add commas number.to_s.reverse.gsub(/([0-9]{3}(?=([0-9])))/, '\\1,').reverse end # helper that loops through lifecycle costs getting total costs under "Construction" or "Salvage" category and add to counter if occurs during year 0 def get_total_costs_for_objects(objects) counter = 0 objects.each do |object| object_LCCs = object.lifeCycleCosts object_LCCs.each do |object_LCC| if (object_LCC.category == 'Construction') || (object_LCC.category == 'Salvage') if object_LCC.yearsFromStart == 0 counter += object_LCC.totalCost end end end end return counter end # unit conversion from IP units to SI units setpoint_si = OpenStudio.convert(setpoint, 'fc', 'lux').get height_si = OpenStudio.convert(height, 'in', 'm').get # variable to tally the area to which the overall measure is applied area = 0 # variables to aggregate the number of sensors installed and the area affected sensor_count = 0 sensor_area = 0 spaces_using_space_type = space_type.spaces # array with subset of spaces spaces_using_space_type_in_zones_without_sensors = [] affected_zones = [] affected_zone_names = [] # hash to hold sensor objects new_sensor_objects = {} # reporting initial condition of model starting_spaces = model.getSpaces runner.registerInitialCondition("#{spaces_using_space_type.size} spaces are assigned to space type '#{space_type.name}'.") # get starting costs for spaces yr0_capital_totalCosts = -1 * get_total_costs_for_objects(spaces_using_space_type) # test that there is no sensor already in the space, and that zone object doesn't already have sensors assigned. spaces_using_space_type.each do |space_using_space_type| if space_using_space_type.daylightingControls.empty? space_zone = space_using_space_type.thermalZone if !space_zone.empty? space_zone = space_zone.get if space_zone.primaryDaylightingControl.empty? && space_zone.secondaryDaylightingControl.empty? spaces_using_space_type_in_zones_without_sensors << space_using_space_type elsif runner.registerWarning("Thermal zone '#{space_zone.name}' which includes space '#{space_using_space_type.name}' already had a daylighting sensor. No sensor was added to space '#{space_using_space_type.name}'.") end else runner.registerWarning("Space '#{space_using_space_type.name}' is not associated with a thermal zone. It won't be part of the EnergyPlus simulation.") end else runner.registerWarning("Space '#{space_using_space_type.name}' already has a daylighting sensor. No sensor was added.") end end # loop through all spaces, # and add a daylighting sensor with dimming to each space_count = 0 spaces_using_space_type_in_zones_without_sensors.each do |space| space_count += 1 area += space.floorArea # eliminate spaces that don't have exterior natural lighting has_ext_nat_light = false space.surfaces.each do |surface| next if surface.outsideBoundaryCondition != 'Outdoors' surface.subSurfaces.each do |sub_surface| next if sub_surface.subSurfaceType == 'Door' next if sub_surface.subSurfaceType == 'OverheadDoor' has_ext_nat_light = true end end if has_ext_nat_light == false runner.registerWarning("Space '#{space.name}' has no exterior natural lighting. No sensor will be added.") next end # find floors floors = [] space.surfaces.each do |surface| next if surface.surfaceType != 'Floor' floors << surface end # this method only works for flat (non-inclined) floors boundingBox = OpenStudio::BoundingBox.new floors.each do |floor| boundingBox.addPoints(floor.vertices) end xmin = boundingBox.minX.get ymin = boundingBox.minY.get zmin = boundingBox.minZ.get xmax = boundingBox.maxX.get ymax = boundingBox.maxY.get # create a new sensor and put at the center of the space sensor = OpenStudio::Model::DaylightingControl.new(model) sensor.setName("#{space.name} daylighting control") x_pos = (xmin + xmax) / 2 y_pos = (ymin + ymax) / 2 z_pos = zmin + height_si # put it 1 meter above the floor new_point = OpenStudio::Point3d.new(x_pos, y_pos, z_pos) sensor.setPosition(new_point) sensor.setIlluminanceSetpoint(setpoint_si) sensor.setLightingControlType(control_type) sensor.setMinimumInputPowerFractionforContinuousDimmingControl(min_power_fraction) sensor.setMinimumLightOutputFractionforContinuousDimmingControl(min_light_fraction) sensor.setSpace(space) puts sensor # add lifeCycleCost objects if there is a non-zero value in one of the cost arguments if costs_requested == true starting_lcc_counter = space.lifeCycleCosts.size # adding new cost items lcc_mat = OpenStudio::Model::LifeCycleCost.createLifeCycleCost("LCC_Mat - #{sensor.name}", space, material_cost, 'CostPerEach', 'Construction', expected_life, years_until_costs_start) if demo_cost_initial_const lcc_demo = OpenStudio::Model::LifeCycleCost.createLifeCycleCost("LCC_Demo - #{sensor.name}", space, demolition_cost, 'CostPerEach', 'Salvage', expected_life, years_until_costs_start) else lcc_demo = OpenStudio::Model::LifeCycleCost.createLifeCycleCost("LCC_Demo - #{sensor.name}", space, demolition_cost, 'CostPerEach', 'Salvage', expected_life, years_until_costs_start + expected_life) end lcc_om = OpenStudio::Model::LifeCycleCost.createLifeCycleCost("LCC_OM - #{sensor.name}", space, om_cost, 'CostPerEach', 'Maintenance', om_frequency, 0) if space.lifeCycleCosts.size - starting_lcc_counter == 3 if !warning_cost_assign_to_space runner.registerInfo('Cost for daylight sensors was added to spaces. The cost will remain in the model unless the space is removed. Removing only the sensor will not remove the cost.') warning_cost_assign_to_space = true end else runner.registerWarning("The measure did not function as expected. #{space.lifeCycleCosts.size - starting_lcc_counter} LifeCycleCost objects were made, 3 were expected.") end end # push unique zones to array for use later in measure temp_zone = space.thermalZone.get if affected_zone_names.include?(temp_zone.name.to_s) == false affected_zones << temp_zone affected_zone_names << temp_zone.name.to_s end # push sensor object into hash with space name new_sensor_objects[space.name.to_s] = sensor # add floor area to the daylighting area tally sensor_area += space.floorArea # add to sensor count for reporting sensor_count += 1 end if (sensor_count == 0) && (costs_requested == false) runner.registerAsNotApplicable("No spaces that currently don't have sensor required a new sensor, and no lifecycle costs objects were requested.") return true end # loop through thermal Zones for spaces with daylighting controls added affected_zones.each do |zone| zone_spaces = zone.spaces zone_spaces_with_new_sensors = [] zone_spaces.each do |zone_space| if !zone_space.daylightingControls.empty? && (zone_space.spaceType.get == space_type) zone_spaces_with_new_sensors << zone_space end end if !zone_spaces_with_new_sensors.empty? # need to identify the two largest spaces primary_area = 0 secondary_area = 0 primary_space = nil secondary_space = nil three_or_more_sensors = false # dfg temp - need to add another if statement so only get spaces with sensors zone_spaces_with_new_sensors.each do |zone_space| zone_space_area = zone_space.floorArea if zone_space_area > primary_area primary_area = zone_space_area primary_space = zone_space elsif zone_space_area > secondary_area secondary_area = zone_space_area secondary_space = zone_space else # setup flag to warn user that more than 2 sensors can't be added to a space three_or_more_sensors = true end end if primary_space # setup primary sensor sensor_primary = new_sensor_objects[primary_space.name.to_s] zone.setPrimaryDaylightingControl(sensor_primary) zone.setFractionofZoneControlledbyPrimaryDaylightingControl(fraction_zone_controlled * primary_area / (primary_area + secondary_area)) end if secondary_space # setup secondary sensor sensor_secondary = new_sensor_objects[secondary_space.name.to_s] zone.setSecondaryDaylightingControl(sensor_secondary) zone.setFractionofZoneControlledbySecondaryDaylightingControl(fraction_zone_controlled * secondary_area / (primary_area + secondary_area)) end # warn that additional sensors were not used if three_or_more_sensors == true runner.registerWarning("Thermal zone '#{zone.name}' had more than two spaces with sensors. Only two sensors were associated with the thermal zone.") end end end # setup OpenStudio units that we will need unit_area_ip = OpenStudio.createUnit('ft^2').get unit_area_si = OpenStudio.createUnit('m^2').get # define starting units area_si = OpenStudio::Quantity.new(sensor_area, unit_area_si) # unit conversion from IP units to SI units area_ip = OpenStudio.convert(area_si, unit_area_ip).get # get final costs for spaces yr0_capital_totalCosts = get_total_costs_for_objects(spaces_using_space_type) # reporting final condition of model runner.registerFinalCondition("Added daylighting controls to #{sensor_count} spaces, covering #{area_ip}. Initial year costs associated with the daylighting controls is $#{neat_numbers(yr0_capital_totalCosts, 0)}.") return true end |