Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/radiant controls zone occupancy #1571

Merged
merged 13 commits into from
Aug 29, 2023
Merged
Show file tree
Hide file tree
Changes from 8 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
Original file line number Diff line number Diff line change
Expand Up @@ -4528,14 +4528,17 @@ def model_add_four_pipe_fan_coil(model,
# options are 'ZeroFlowPower', 'HalfFlowPower'
# @param include_carpet [Bool] boolean to include thin carpet tile over radiant slab, default to true
# @param carpet_thickness_in [Double] thickness of carpet in inches
# @param model_occ_hr_start [Double] (Optional) Only applies if control_strategy is 'proportional_control'.
# Starting hour of building occupancy.
# @param model_occ_hr_end [Double] (Optional) Only applies if control_strategy is 'proportional_control'.
# Ending hour of building occupancy.
# @param control_strategy [String] name of control strategy. Options are 'proportional_control' and 'none'.
# If control strategy is 'proportional_control', the method will apply the CBE radiant control sequences
# detailed in Raftery et al. (2017), 'A new control strategy for high thermal mass radiant systems'.
# Otherwise no control strategy will be applied and the radiant system will assume the EnergyPlus default controls.
# @param use_zone_occupancy_for_control [Bool] Set to true if radiant system is to use specific zone occupancy objects
# for CBE control strategy. If false, then it will use values in model_occ_hr_start and model_occ_hr_end
# for all radiant zones. default to true.
# @param model_occ_hr_start [Double] (Optional) Only applies if control_strategy is 'proportional_control'.
# Starting hour of building occupancy.
# @param model_occ_hr_end [Double] (Optional) Only applies if control_strategy is 'proportional_control'.
# Ending hour of building occupancy.
# @param proportional_gain [Double] (Optional) Only applies if control_strategy is 'proportional_control'.
# Proportional gain constant (recommended 0.3 or less).
# @param switch_over_time [Double] Time limitation for when the system can switch between heating and cooling
Expand All @@ -4561,9 +4564,10 @@ def model_add_low_temp_radiant(model,
radiant_setpoint_control_type: 'ZeroFlowPower',
include_carpet: true,
carpet_thickness_in: 0.25,
control_strategy: 'proportional_control',
use_zone_occupancy_for_control: true,
model_occ_hr_start: 6.0,
model_occ_hr_end: 18.0,
control_strategy: 'proportional_control',
proportional_gain: 0.3,
switch_over_time: 24.0,
radiant_availability_type: 'precool',
Expand Down Expand Up @@ -4926,6 +4930,7 @@ def model_add_low_temp_radiant(model,
if control_strategy == 'proportional_control'
model_add_radiant_proportional_controls(model, zone, radiant_loop,
radiant_temperature_control_type: radiant_temperature_control_type,
use_zone_occupancy_for_control: use_zone_occupancy_for_control,
model_occ_hr_start: model_occ_hr_start,
model_occ_hr_end: model_occ_hr_end,
proportional_gain: proportional_gain,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@ class Standard
# @param radiant_loop [OpenStudio::Model::ZoneHVACLowTempRadiantVarFlow>] radiant loop in thermal zone
# @param radiant_temperature_control_type [String] determines the controlled temperature for the radiant system
# options are 'SurfaceFaceTemperature', 'SurfaceInteriorTemperature'
# @param use_zone_occupancy_for_control [Bool] Set to true if radiant system is to use specific zone occupancy objects
# for CBE control strategy. If false, then it will use values in model_occ_hr_start and model_occ_hr_end
# for all radiant zones. default to true.
# @param model_occ_hr_start [Double] Starting hour of building occupancy
# @param model_occ_hr_end [Double] Ending hour of building occupancy
# @todo model_occ_hr_start and model_occ_hr_end from zone occupancy schedules
# @param proportional_gain [Double] Proportional gain constant (recommended 0.3 or less).
# @param switch_over_time [Double] Time limitation for when the system can switch between heating and cooling
def model_add_radiant_proportional_controls(model, zone, radiant_loop,
radiant_temperature_control_type: 'SurfaceFaceTemperature',
use_zone_occupancy_for_control: true,
model_occ_hr_start: 6.0,
model_occ_hr_end: 18.0,
proportional_gain: 0.3,
Expand Down Expand Up @@ -53,7 +57,7 @@ def model_add_radiant_proportional_controls(model, zone, radiant_loop,
sch_radiant_switchover = model_add_constant_schedule_ruleset(model,
switch_over_time,
name = "Radiant System Switchover",
sch_type_limit: "Dimensionless")
sch_type_limit: "fraction")
end

# set radiant system switchover schedule
Expand Down Expand Up @@ -103,20 +107,29 @@ def model_add_radiant_proportional_controls(model, zone, radiant_loop,
# List of global variables used in EMS scripts
####

# assign different variable names if using zone occupancy for control
if use_zone_occupancy_for_control
zone_occ_hr_start_name = "#{zone_name}_occ_hr_start"
zone_occ_hr_end_name = "#{zone_name}_occ_hr_end"
else
zone_occ_hr_start_name = "occ_hr_start"
zone_occ_hr_end_name = "occ_hr_end"
end

# Start of occupied time of zone. Valid from 1-24.
occ_hr_start = model.getEnergyManagementSystemGlobalVariableByName('occ_hr_start')
occ_hr_start = model.getEnergyManagementSystemGlobalVariableByName(zone_occ_hr_start_name)
if occ_hr_start.is_initialized
occ_hr_start = occ_hr_start.get
else
occ_hr_start = OpenStudio::Model::EnergyManagementSystemGlobalVariable.new(model, 'occ_hr_start')
occ_hr_start = OpenStudio::Model::EnergyManagementSystemGlobalVariable.new(model, zone_occ_hr_start_name)
end

# End of occupied time of zone. Valid from 1-24.
occ_hr_end = model.getEnergyManagementSystemGlobalVariableByName('occ_hr_end')
occ_hr_end = model.getEnergyManagementSystemGlobalVariableByName(zone_occ_hr_end_name)
if occ_hr_end.is_initialized
occ_hr_end = occ_hr_end.get
else
occ_hr_end = OpenStudio::Model::EnergyManagementSystemGlobalVariable.new(model, 'occ_hr_end')
occ_hr_end = OpenStudio::Model::EnergyManagementSystemGlobalVariable.new(model, zone_occ_hr_end_name)
end

# Proportional gain constant (recommended 0.3 or less).
Expand Down Expand Up @@ -250,44 +263,105 @@ def model_add_radiant_proportional_controls(model, zone, radiant_loop,
zone_rad_heat_operation_trend.setName("#{zone_name}_rad_heat_operation_trend")
zone_rad_heat_operation_trend.setNumberOfTimestepsToBeLogged(zone_timestep * 48)

# use zone occupancy objects for radiant system control if selected
if use_zone_occupancy_for_control
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recommend implementing this with EnergyManagementSystemSensor on the Schedule value, like you have for the temperature setpoint schedules.

Use the schedule value to determine the control, rather than IF ((CurrentTime >= #{zone_occ_hr_start_name}) && (CurrentTime <= #{zone_occ_hr_end_name})),

That would mean changing it so if occupancy control isn't defined, and instead you get starting/stopping hours, making a ruleset schedule that has those hours and then following that schedule value for control.

To summarize:

  1. create an on/off schedule from the zone occupancy schedule
  2. if not zone control, create the on/off schedule from the provided start/stop times
  3. track that zone schedule with a schedule_value EMS sensor
  4. update the logic to check against the sensor value, rather than using IF ((CurrentTime >= #{zone_occ_hr_start_name}) && (CurrentTime <= #{zone_occ_hr_end_name})),

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Use thermal_zone_get_occupancy_schedule(zone, sch_name, occupied_percentage_threshold) to make an on/off schedule
  • name it "#{zone.name} Radiant System Control Schedule"
  • add occupied_percentage_threshold argument to radiant controls method, and pass into thermal_zone_get_occupancy_schedule
  • use the schedule value to control the system

Copy link
Collaborator

@mdahlhausen mdahlhausen Aug 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Example to create new ScheduleRuleset

econ_max_100_pct_oa_sch = OpenStudio::Model::ScheduleRuleset.new(model)
econ_max_100_pct_oa_sch.setName('Economizer Max OA Fraction 100 pct')
econ_max_100_pct_oa_sch.defaultDaySchedule.setName('Economizer Max OA Fraction 100 pct Default')
econ_max_100_pct_oa_sch.defaultDaySchedule.addValue(OpenStudio::Time.new(0, 24, 0, 0), 1.0)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've made the changes you suggested with commit 54dd618 and b471c4a


# get annual occupancy schedule for zone
occ_schedule_ruleset = thermal_zone_get_occupancy_schedule(zone)
occ_values = schedule_ruleset_annual_hourly_values(occ_schedule_ruleset)

# transform annual occupancy into 24 slices and transform
occ_values_2d = occ_values.each_slice(24).to_a.transpose()

# find 24-hour mean using the 365 days
mean_occ_values = (0..23).collect{ |hr| occ_values_2d[hr].sum() / occ_values_2d[hr].size() }

# find start and end hours that meet occupancy threshold
zone_occ_hr_start = mean_occ_values.index{ |n| n >= 0.25 }
zone_occ_hr_end = 24 - mean_occ_values.reverse().index{ |n| n >= 0.25 }

# remove occupancy schedule ruleset that was created
occ_schedule_ruleset.scheduleRules.each { |item| model.removeObject(item.daySchedule.handle) }
occ_schedule_ruleset.children.each { |item| model.removeObject(item.handle) }
model.removeObject(occ_schedule_ruleset.handle)

if zone_occ_hr_start > zone_occ_hr_end
OpenStudio.logFree(OpenStudio::Error, 'openstudio.Model.Model',
"Zone occupancy start hour (#{zone_occ_hr_start}) is greater than zone occupancy end hour (#{zone_occ_hr_end}) in zone #{zone.name.to_s}")
end

if zone_occ_hr_start == zone_occ_hr_end
OpenStudio.logFree(OpenStudio::Warn, 'openstudio.Model.Model',
"Zone occupancy start hour (#{zone_occ_hr_start}) is equal to zone occupancy end hour (#{zone_occ_hr_end}) in zone #{zone.name.to_s}, i.e. no occupancy")
end

else
zone_occ_hr_start = model_occ_hr_start
zone_occ_hr_end = model_occ_hr_end
end

#####
# List of EMS programs to implement the proportional control for the radiant system.
####

# Initialize global constant values used in EMS programs.
set_constant_values_prg = model.getEnergyManagementSystemTrendVariableByName('Set_Constant_Values')
unless set_constant_values_prg.is_initialized
set_constant_values_prg = OpenStudio::Model::EnergyManagementSystemProgram.new(model)
set_constant_values_prg.setName('Set_Constant_Values')
# Exclude occupancy hours variables if specific to zones
if use_zone_occupancy_for_control
set_constant_values_prg_body = <<-EMS
SET prp_k = #{proportional_gain},
SET ctrl_temp_offset = 0.5,
SET upper_slab_sp_lim = 29,
SET lower_slab_sp_lim = 19
EMS
else
set_constant_values_prg_body = <<-EMS
SET occ_hr_start = #{model_occ_hr_start},
SET occ_hr_end = #{model_occ_hr_end},
SET occ_hr_start = #{zone_occ_hr_start},
SET occ_hr_end = #{zone_occ_hr_end},
SET prp_k = #{proportional_gain},
SET ctrl_temp_offset = 0.5,
SET upper_slab_sp_lim = 29,
SET lower_slab_sp_lim = 19
EMS
end

set_constant_values_prg = model.getEnergyManagementSystemProgramByName('Set_Constant_Values')
unless set_constant_values_prg.is_initialized
set_constant_values_prg = OpenStudio::Model::EnergyManagementSystemProgram.new(model)
set_constant_values_prg.setName('Set_Constant_Values')
set_constant_values_prg.setBody(set_constant_values_prg_body)
end

# Initialize zone specific constant values used in EMS programs.
set_constant_zone_values_prg = OpenStudio::Model::EnergyManagementSystemProgram.new(model)
set_constant_zone_values_prg.setName("#{zone_name}_Set_Constant_Values")
set_constant_zone_values_prg_body = <<-EMS
if use_zone_occupancy_for_control
set_constant_zone_values_prg_body = <<-EMS
SET #{zone_occ_hr_start_name} = #{zone_occ_hr_start},
SET #{zone_occ_hr_end_name} = #{zone_occ_hr_end},
SET #{zone_name}_max_ctrl_temp = #{zone_name}_lower_comfort_limit,
SET #{zone_name}_min_ctrl_temp = #{zone_name}_upper_comfort_limit,
SET #{zone_name}_cmd_csp_error = 0,
SET #{zone_name}_cmd_hsp_error = 0,
SET #{zone_name}_cmd_cold_water_ctrl = #{zone_name}_upper_comfort_limit,
SET #{zone_name}_cmd_hot_water_ctrl = #{zone_name}_lower_comfort_limit
EMS
else
set_constant_zone_values_prg_body = <<-EMS
SET #{zone_name}_max_ctrl_temp = #{zone_name}_lower_comfort_limit,
SET #{zone_name}_min_ctrl_temp = #{zone_name}_upper_comfort_limit,
SET #{zone_name}_cmd_csp_error = 0,
SET #{zone_name}_cmd_hsp_error = 0,
SET #{zone_name}_cmd_cold_water_ctrl = #{zone_name}_upper_comfort_limit,
SET #{zone_name}_cmd_hot_water_ctrl = #{zone_name}_lower_comfort_limit
EMS
end
set_constant_zone_values_prg = OpenStudio::Model::EnergyManagementSystemProgram.new(model)
set_constant_zone_values_prg.setName("#{zone_name}_Set_Constant_Values")
set_constant_zone_values_prg.setBody(set_constant_zone_values_prg_body)

# Calculate maximum and minimum 'measured' controlled temperature in the zone
calculate_minmax_ctrl_temp_prg = OpenStudio::Model::EnergyManagementSystemProgram.new(model)
calculate_minmax_ctrl_temp_prg.setName("#{zone_name}_Calculate_Extremes_In_Zone")
calculate_minmax_ctrl_temp_prg_body = <<-EMS
IF ((CurrentTime >= occ_hr_start) && (CurrentTime <= occ_hr_end)),
IF ((CurrentTime >= #{zone_occ_hr_start_name}) && (CurrentTime <= #{zone_occ_hr_end_name})),
IF #{zone_name}_ctrl_temperature > #{zone_name}_max_ctrl_temp,
SET #{zone_name}_max_ctrl_temp = #{zone_name}_ctrl_temperature,
ENDIF,
Expand All @@ -305,7 +379,7 @@ def model_add_radiant_proportional_controls(model, zone, radiant_loop,
calculate_errors_from_comfort_prg = OpenStudio::Model::EnergyManagementSystemProgram.new(model)
calculate_errors_from_comfort_prg.setName("#{zone_name}_Calculate_Errors_From_Comfort")
calculate_errors_from_comfort_prg_body = <<-EMS
IF (CurrentTime >= (occ_hr_end - ZoneTimeStep)) && (CurrentTime <= (occ_hr_end)),
IF (CurrentTime >= (#{zone_occ_hr_end_name} - ZoneTimeStep)) && (CurrentTime <= (#{zone_occ_hr_end_name})),
SET #{zone_name}_cmd_csp_error = (#{zone_name}_upper_comfort_limit - ctrl_temp_offset) - #{zone_name}_max_ctrl_temp,
SET #{zone_name}_cmd_hsp_error = (#{zone_name}_lower_comfort_limit + ctrl_temp_offset) - #{zone_name}_min_ctrl_temp,
ENDIF
Expand All @@ -318,9 +392,9 @@ def model_add_radiant_proportional_controls(model, zone, radiant_loop,
calculate_slab_ctrl_setpoint_prg_body = <<-EMS
SET #{zone_name}_cont_cool_oper = @TrendSum #{zone_name}_rad_cool_operation_trend radiant_switch_over_time/ZoneTimeStep,
SET #{zone_name}_cont_heat_oper = @TrendSum #{zone_name}_rad_heat_operation_trend radiant_switch_over_time/ZoneTimeStep,
IF (#{zone_name}_cont_cool_oper > 0) && (CurrentTime == occ_hr_end),
IF (#{zone_name}_cont_cool_oper > 0) && (CurrentTime == #{zone_occ_hr_end_name}),
SET #{zone_name}_cmd_hot_water_ctrl = #{zone_name}_cmd_hot_water_ctrl + (#{zone_name}_cmd_csp_error*prp_k),
ELSEIF (#{zone_name}_cont_heat_oper > 0) && (CurrentTime == occ_hr_end),
ELSEIF (#{zone_name}_cont_heat_oper > 0) && (CurrentTime == #{zone_occ_hr_end_name}),
SET #{zone_name}_cmd_hot_water_ctrl = #{zone_name}_cmd_hot_water_ctrl + (#{zone_name}_cmd_hsp_error*prp_k),
ELSE,
SET #{zone_name}_cmd_hot_water_ctrl = #{zone_name}_cmd_hot_water_ctrl,
Expand All @@ -338,7 +412,7 @@ def model_add_radiant_proportional_controls(model, zone, radiant_loop,
# List of EMS program manager objects
####

initialize_constant_parameters = model.getEnergyManagementSystemProgramCallingManagerByName('Set_Constant_Values')
initialize_constant_parameters = model.getEnergyManagementSystemProgramCallingManagerByName('Initialize_Constant_Parameters')
if initialize_constant_parameters.is_initialized
initialize_constant_parameters = initialize_constant_parameters.get
else
Expand All @@ -348,7 +422,7 @@ def model_add_radiant_proportional_controls(model, zone, radiant_loop,
initialize_constant_parameters.addProgram(set_constant_values_prg)
end

initialize_constant_parameters_after_warmup = model.getEnergyManagementSystemProgramCallingManagerByName('Set_Constant_Values')
initialize_constant_parameters_after_warmup = model.getEnergyManagementSystemProgramCallingManagerByName('Initialize_Constant_Parameters_After_Warmup')
if initialize_constant_parameters_after_warmup.is_initialized
initialize_constant_parameters_after_warmup = initialize_constant_parameters_after_warmup.get
else
Expand Down
2 changes: 1 addition & 1 deletion openstudio-standards.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ require 'openstudio-standards/version'
Gem::Specification.new do |spec|
spec.name = 'openstudio-standards'
spec.version = OpenstudioStandards::VERSION
spec.authors = ['Andrew Parker', 'Yixing Chen', 'Mark Adams', 'Kaiyu Sun', 'Mini Maholtra', 'David Goldwasser', 'Phylroy Lopez', 'Maria Mottillo', 'Kamel Haddad', 'Julien Marrec', 'Matt Leach', 'Matt Steen', 'Eric Ringold', 'Daniel Macumber', 'Matthew Dahlhausen', 'Jian Zhang', 'Doug Maddox', 'Yunyang Ye', 'Xuechen (Jerry) Lei', 'Juan Gonzalez Matamoros', 'Jeremy Lerond']
spec.authors = ['Andrew Parker', 'Yixing Chen', 'Mark Adams', 'Kaiyu Sun', 'Mini Maholtra', 'David Goldwasser', 'Phylroy Lopez', 'Maria Mottillo', 'Kamel Haddad', 'Julien Marrec', 'Matt Leach', 'Matt Steen', 'Eric Ringold', 'Daniel Macumber', 'Matthew Dahlhausen', 'Jian Zhang', 'Doug Maddox', 'Yunyang Ye', 'Xuechen (Jerry) Lei', 'Juan Gonzalez Matamoros', 'Jeremy Lerond', 'Carlos Duarte']
spec.email = ['andrew.parker@nrel.gov']
spec.homepage = 'http://openstudio.net'
spec.summary = 'Creates DOE Prototype building models and transforms proposed OpenStudio models to baseline OpenStudio models.'
Expand Down