# ********************************************************************************* # URBANopt™, Copyright (c) 2019-2021, Alliance for Sustainable Energy, LLC, and other # contributors. All rights reserved. # Redistribution and use in source and binary forms, with or without modification, # are permitted provided that the following conditions are met: # Redistributions of source code must retain the above copyright notice, this list # of conditions and the following disclaimer. # Redistributions in binary form must reproduce the above copyright notice, this # list of conditions and the following disclaimer in the documentation and/or other # materials provided with the distribution. # Neither the name of the copyright holder nor the names of its contributors may be # used to endorse or promote products derived from this software without specific # prior written permission. # Redistribution of this software, without modification, must refer to the software # by the same designation. Redistribution of a modified version of this software # (i) may not refer to the modified version by the same designation, or by any # confusingly similar designation, and (ii) must refer to the underlying software # originally provided by Alliance as “URBANopt”. Except to comply with the foregoing, # the term “URBANopt”, or any confusingly similar designation may not be used to # refer to any modified version of this software or any modified version of the # underlying software originally provided by Alliance without the prior written # consent of Alliance. # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. # IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED # OF THE POSSIBILITY OF SUCH DAMAGE. # ********************************************************************************* require 'erb' # This measure is originally from https://github.com/urbanopt/DES_HVAC # start the measure class ExportTimeSeriesLoadsCSV < OpenStudio::Measure::ReportingMeasure Dir[File.dirname(__FILE__) + '/resources/*.rb'].each { |file| require file } include OsLib_HelperMethods # human readable name def name # Measure name should be the title case of the class name. 'ExportTimeSeriesLoadsCSV' end def description 'This measure will add the required output variables and create a CSV file with plant loop level mass flow rates and temperatures for use in a Modelica simulation. Note that this measure has certain requirements for naming of hydronic loops (discussed in the modeler description section).' end def modeler_description 'This measure is currently configured to report the temperatures and mass flow rates at the demand outlet and inlet nodes of hot water and chilled water loops, after adding the required output variables to the model. These values can be used to calculate the sum of the demand-side loads, and could thus represent the load on a connection to a district thermal energy system, or on building-level primary equipment. This measure assumes that the model includes hydronic HVAC loops, and that the hot water and chilled water loop names can each be uniquely identified by a user-provided string. This measure also assumes that there is a single heating hot water loop and a single chilled-water loop per building.' end def log(str) puts "#{Time.now}: #{str}" end def arguments(_model) args = OpenStudio::Measure::OSArgumentVector.new hhw_loop_name = OpenStudio::Measure::OSArgument.makeStringArgument('hhw_loop_name', true) hhw_loop_name.setDisplayName('Name or Partial Name of Heating Hot Water Loop, non-case-sensitive') hhw_loop_name.setDefaultValue('hot') args << hhw_loop_name chw_loop_name = OpenStudio::Measure::OSArgument.makeStringArgument('chw_loop_name', true) chw_loop_name.setDisplayName('Name or Partial Name of Chilled Water Loop, non-case-sensitive') chw_loop_name.setDefaultValue('chilled') args << chw_loop_name dec_places_mass_flow = OpenStudio::Measure::OSArgument.makeIntegerArgument('dec_places_mass_flow', true) dec_places_mass_flow.setDisplayName('Number of Decimal Places to Round Mass Flow Rate') dec_places_mass_flow.setDescription('Number of decimal places to which mass flow rate will be rounded') dec_places_mass_flow.setDefaultValue(3) args << dec_places_mass_flow dec_places_temp = OpenStudio::Measure::OSArgument.makeIntegerArgument('dec_places_temp', true) dec_places_temp.setDisplayName('Number of Decimal Places to Round Temperature') dec_places_temp.setDescription('Number of decimal places to which temperature will be rounded') dec_places_temp.setDefaultValue(1) args << dec_places_temp return args end # return a vector of IdfObject's to request EnergyPlus objects needed by the run method def energyPlusOutputRequests(runner, user_arguments) super(runner, user_arguments) result = OpenStudio::IdfObjectVector.new # To use the built-in error checking we need the model... # get the last model and sql file model = runner.lastOpenStudioModel if model.empty? runner.registerError('Cannot find last model.') return false end model = model.get # use the built-in error checking return false unless runner.validateUserArguments(arguments(model), user_arguments) ##Read in argumetns related to variables for output requests hhw_loop_name = runner.getStringArgumentValue('hhw_loop_name', user_arguments) chw_loop_name = runner.getStringArgumentValue('chw_loop_name', user_arguments) #Identify key names for output variables. plantloops = model.getPlantLoops selected_plant_loops = [] i = 0 variable_name1 = 'System Node Mass Flow Rate' variable_name2 = 'System Node Temperature' reporting_frequency = 'timestep' plantloops.each do |plantLoop| log "plant loop name #{plantLoop.name.get.to_s}" if plantLoop.name.get.to_s.downcase.include? chw_loop_name.to_s #Extract plant loop information selected_plant_loops[0]=plantLoop key_value_chw_outlet = selected_plant_loops[0].demandOutletNode.name.to_s key_value_chw_inlet = selected_plant_loops[0].demandInletNode.name.to_s result << OpenStudio::IdfObject.load("Output:Variable,#{key_value_chw_outlet},#{variable_name2},timestep;").get result << OpenStudio::IdfObject.load("Output:Variable,#{key_value_chw_inlet},#{variable_name2},timestep;").get result << OpenStudio::IdfObject.load("Output:Variable,#{key_value_chw_outlet},#{variable_name1},timestep;").get end if plantLoop.name.get.to_s.downcase.include? hhw_loop_name.to_s and !plantLoop.name.get.to_s.downcase.include? "service" and !plantLoop.name.get.to_s.downcase.include? "domestic" #Extract plant loop information selected_plant_loops[1]=plantLoop key_value_hhw_outlet = selected_plant_loops[1].demandOutletNode.name.to_s key_value_hhw_inlet = selected_plant_loops[1].demandInletNode.name.to_s result << OpenStudio::IdfObject.load("Output:Variable,#{key_value_hhw_outlet},#{variable_name2},timestep;").get result << OpenStudio::IdfObject.load("Output:Variable,#{key_value_hhw_inlet},#{variable_name2},timestep;").get result << OpenStudio::IdfObject.load("Output:Variable,#{key_value_hhw_outlet},#{variable_name1},timestep;").get end end result << OpenStudio::IdfObject.load('Output:Variable,,Site Mains Water Temperature,hourly;').get result << OpenStudio::IdfObject.load('Output:Variable,,Site Outdoor Air Drybulb Temperature,hourly;').get result << OpenStudio::IdfObject.load('Output:Variable,,Site Outdoor Air Relative Humidity,hourly;').get result << OpenStudio::IdfObject.load('Output:Meter,Cooling:Electricity,hourly;').get result << OpenStudio::IdfObject.load('Output:Meter,Electricity:Facility,timestep;').get ##Using this for data at timestep interval result << OpenStudio::IdfObject.load('Output:Meter,Heating:Electricity,hourly;').get result << OpenStudio::IdfObject.load('Output:Meter,Heating:NaturalGas,hourly;').get result << OpenStudio::IdfObject.load('Output:Meter,InteriorLights:Electricity,hourly;').get result << OpenStudio::IdfObject.load('Output:Meter,Fans:Electricity,hourly;').get result << OpenStudio::IdfObject.load('Output:Meter,InteriorEquipment:Electricity,hourly;').get # Joules result << OpenStudio::IdfObject.load('Output:Meter,ExteriorLighting:Electricity,hourly;').get # Joules result << OpenStudio::IdfObject.load('Output:Meter,Electricity:Facility,hourly;').get # Joules result << OpenStudio::IdfObject.load('Output:Meter,Gas:Facility,hourly;').get # Joules result << OpenStudio::IdfObject.load('Output:Meter,Heating:EnergyTransfer,hourly;').get # Joules result << OpenStudio::IdfObject.load('Output:Meter,WaterSystems:EnergyTransfer,hourly;').get # Joules # these variables are used for the modelica export. result << OpenStudio::IdfObject.load('Output:Variable,*,Zone Predicted Sensible Load to Setpoint Heat Transfer Rate,hourly;').get # watts according to e+ result << OpenStudio::IdfObject.load('Output:Variable,*,Water Heater Total Demand Heat Transfer Rate,hourly;').get # Watts result end def extract_timeseries_into_matrix(sqlfile, data, variable_name, str, key_value = nil, default_if_empty = 0,dec_places, timestep) log "Executing query for #{variable_name}" #column_name = variable_name if key_value ts = sqlfile.timeSeries('RUN PERIOD 1', 'Zone Timestep', variable_name, key_value) #column_name += "_#{key_value}" column_name=str else #ts = sqlfile.timeSeries('RUN PERIOD 1', 'Hourly', variable_name) ts = sqlfile.timeSeries('RUN PERIOD 1', 'Zone Timestep', variable_name) end log 'Iterating over timeseries' column = [column_name.delete(':').delete(' ')] # Set the header of the data to the variable name, removing : and spaces if ts.empty? log "No time series for #{variable_name}:#{key_value}... defaulting to #{default_if_empty}" # needs to be data.size-1 since the column name is already stored above (+=) column += [default_if_empty] * (data.size - 1) else ts = ts.get if ts.respond_to?(:get) ts = ts.first if ts.respond_to?(:first) start = Time.now # Iterating in OpenStudio can take up to 60 seconds with 10min data. The quick_proc takes 0.03 seconds. # for i in 0..ts.values.size - 1 # log "... at #{i}" if i % 10000 == 0 # column << ts.values[i] # end quick_proc = ts.values.to_s.split(',') quick_proc[0]=quick_proc[0].split('(', 2).last #cleanup necessary to remove opening paren quick_proc=quick_proc.map(&:to_f) x = 0 len = quick_proc.length log "quick proc #{quick_proc}" while(x < len) #Round to the # of decimal places specified quick_proc[x]=(quick_proc[x]).round(dec_places) x=x+1 end quick_proc=quick_proc.map(&:to_s) # the first and last have some cleanup items because of the Vector method quick_proc[0] = quick_proc[0].gsub(/^.*\(/, '') quick_proc[-1] = quick_proc[-1].delete(')') column += quick_proc log "Took #{Time.now - start} to iterate" end log 'Appending column to data' # append the data to the end of the rows if column.size == data.size data.each_index do |index| data[index] << column[index] end end log "Finished extracting #{variable_name}" end def create_new_variable_sum(data, new_var_name, include_str, options=nil) var_info = { name: new_var_name, var_indexes: [] } data.each_with_index do |row, index| if index.zero? # Get the index of the columns to add row.each do |c| var_info[:var_indexes] << row.index(c) if c.include? include_str end # add the new var to the header row data[0] << var_info[:name] else # sum the values sum = 0 var_info[:var_indexes].each do |var| temp_v = row[var].to_f if options.nil? sum += temp_v elsif options[:positive_only] && temp_v > 0 sum += temp_v elsif options[:negative_only] && temp_v < 0 sum += temp_v end end data[index] << sum end end end def run(runner, user_arguments) super(runner, user_arguments) # get the last model and sql file model = runner.lastOpenStudioModel if model.empty? runner.registerError('Cannot find last model.') return false end model = model.get # use the built-in error checking return false unless runner.validateUserArguments(arguments(model), user_arguments) args = OsLib_HelperMethods.createRunVariables(runner, model, user_arguments, arguments(model)) if !args return false end # lookup and replace argument values from upstream measures if args['use_upstream_args'] == true args.each do |arg,value| next if arg == 'use_upstream_args' # this argument should not be changed value_from_osw = OsLib_HelperMethods.check_upstream_measure_for_arg(runner, arg) if !value_from_osw.empty? runner.registerInfo("Replacing argument named #{arg} from current measure with a value of #{value_from_osw[:value]} from #{value_from_osw[:measure_name]}.") new_val = value_from_osw[:value] # TODO: make code to handle non strings more robust. check_upstream_measure_for_arg could pass back the argument type if arg == 'hhw_loop_name' args[arg] = new_val.to_s elsif arg == 'chw_loop_name' args[arg] = new_val.to_s else args[arg] = new_val end end end end hhw_loop_name = args['hhw_loop_name'] chw_loop_name = args['chw_loop_name'] dec_places_temp = args['dec_places_temp'] dec_places_mass_flow = args['dec_places_mass_flow'] # get the last model and sql file model = runner.lastOpenStudioModel if model.empty? runner.registerError('Cannot find last model.') return false end model = model.get timesteps_per_hour=model.getTimestep.numberOfTimestepsPerHour.to_i timestep=60/timesteps_per_hour #timestep in minutes sqlFile = runner.lastEnergyPlusSqlFile if sqlFile.empty? runner.registerError('Cannot find last sql file.') return false end sqlFile = sqlFile.get model.setSqlFile(sqlFile) # create a new csv with the values and save to the reports directory. # assumptions: # - all the variables exist # - data are the same length # initialize the rows with the header log 'Starting to process Timeseries data' # Initial header row rows = [ ['Date Time', 'Month', 'Day', 'Day of Week', 'Hour', 'Minute', 'SecondsFromStart'] ] # just grab one of the variables to get the date/time stamps attribute_name = 'Electricity:Facility' ts = sqlFile.timeSeries('RUN PERIOD 1', 'Zone Timestep', attribute_name) if ts.empty? runner.registerError("This feature does not have the attribute '#{attribute_name}' to enable this measure to work." \ "To resolve, simulate a building with electricity or remove this measure from your workflow.") else ts = ts.first dt_base = nil # Save off the date time values ts.dateTimes.each_with_index do |dt, index| dt_base = DateTime.parse(dt.to_s) if dt_base.nil? dt_current = DateTime.parse(dt.to_s) rows << [ DateTime.parse(dt.to_s).strftime('%m/%d/%Y %H:%M'), dt.date.monthOfYear.value, dt.date.dayOfMonth, dt.date.dayOfWeek.value, dt.time.hours, dt.time.minutes, dt_current.to_time.to_i - dt_base.to_time.to_i + timestep*60 ] end end plantloops = model.getPlantLoops selected_plant_loops = [] i = 0 key_var={} plantloops.each do |plantLoop| if plantLoop.name.get.to_s.downcase.include? chw_loop_name.to_str #Extract plant loop information selected_plant_loops[0]=plantLoop end if plantLoop.name.get.to_s.downcase.include? hhw_loop_name.to_str #Get plant loop information selected_plant_loops[1]=plantLoop end end if !selected_plant_loops[1].nil? #Set up variables for output key_value_hhw_outlet = selected_plant_loops[1].demandOutletNode.name.to_s key_value_hhw_inlet = selected_plant_loops[1].demandInletNode.name.to_s key_var['hhw_outlet_massflow']='massFlowRateHeating' key_var['hhw_outlet_temp']='heatingReturnTemperature[C]' key_var['hhw_inlet_temp']='heatingSupplyTemperature[C]' #Extract time series extract_timeseries_into_matrix(sqlFile, rows, 'System Node Temperature', key_var['hhw_outlet_temp'], key_value_hhw_outlet, 0, dec_places_temp, timestep) extract_timeseries_into_matrix(sqlFile, rows, 'System Node Temperature', key_var['hhw_inlet_temp'], key_value_hhw_inlet, 0, dec_places_temp, timestep) extract_timeseries_into_matrix(sqlFile, rows, 'System Node Mass Flow Rate', key_var['hhw_outlet_massflow'], key_value_hhw_outlet, 0, dec_places_mass_flow, timestep) else runner.registerWarning("No hot water loop found. If one is expected, make sure the hot water loop name argument provides a string present in its name.") end if !selected_plant_loops[0].nil? #Set up variables for outputs key_value_chw_outlet = selected_plant_loops[0].demandOutletNode.name.to_s key_value_chw_inlet = selected_plant_loops[0].demandInletNode.name.to_s key_var['chw_outlet_massflow']='massFlowRateCooling' key_var['chw_outlet_temp']='ChilledWaterReturnTemperature[C]' key_var['chw_inlet_temp']='ChilledWaterSupplyTemperature[C]' #Extract time series extract_timeseries_into_matrix(sqlFile, rows, 'System Node Temperature', key_var['chw_outlet_temp'], key_value_chw_outlet, 0, dec_places_temp,timestep) extract_timeseries_into_matrix(sqlFile, rows, 'System Node Temperature', key_var['chw_inlet_temp'], key_value_chw_inlet, 0, dec_places_temp,timestep) extract_timeseries_into_matrix(sqlFile, rows, 'System Node Mass Flow Rate', key_var['chw_outlet_massflow'], key_value_chw_outlet, 0, dec_places_mass_flow,timestep) else runner.registerWarning("No chilled water loop found. If one is expected, make sure the chilled water loop name argument provides a string present in its name.") end if selected_plant_loops[0].nil? and selected_plant_loops[1].nil? runner.registerWarning("No HVAC plant loops found. If one or more plant loops are expected, make sure they follow the naming conventions mentioned in the previous warnings.") end if !selected_plant_loops.nil? # convert this to CSV object File.open('./building_loads.csv', 'w') do |f| rows.each do |row| f << row.join(',') << "\n" end end end true ensure sqlFile&.close end end # register the measure to be used by the application ExportTimeSeriesLoadsCSV.new.registerWithApplication