# This program calculates thermodynamic properties of an air parcel on input of temperature, pressure, and one # of four humidity variables (relative humidity, dewpoint, specific humidity, or mixing ratio). # Program created on 3/11/2007 by Alex DeCaria. # Program requires Ruby interpreter and TK. require 'tk' root = TkRoot.new.title(:ThermoCalc).resizable(0,0) #root.geometry("300x300") # Widgets use CamelCase # Variables used lowercase with underscore, e.g., units_text # Methods use uppercase # SET UP SOME FRAMES------------------------------------------------------------ VariableFrame = TkFrame.new(root, :relief => :groove, :borderwidth => 2).pack(:side => :top) # Frame for variables MSLPFrame = TkFrame.new(root, :relief => :groove, :borderwidth => 2) # Frame for MSLP CalculationFrame = TkFrame.new(root).pack(:side => :top) # Frame for calculate button and messages # DEFINE SOME OF THE VARIABLES AND ARRAYS N = 13 # Number of variables FIGURES = 5 # of significant figures for output $, = ", " NameLabels = [nil]*N # Array for labels for variable names Checkbuttons = [nil]*4 # Array for checkbuttons for certain variables check_status = Array.new # Array for status of checkbuttons ValueWidgets = [nil]*N # Array for variable entry and value widgets ValueWidgets[0] = [nil,nil] ValueWidgets[2] = [nil,nil] # Nested arrays for certain entry and value widgets ValueWidgets[3] = [nil,nil] # " ValueWidgets[4] = [nil,nil] # " ValueWidgets[5] = [nil,nil] # " value_text = Array.new # Holds text values of varibles in local units value_float = Array.new(N) # Holds floating point values in mks units UnitsLabels = [nil]*N k = [0]*N # index for unit toggles units_list = [["mb","in"], ["C","K","F"], "%", ["C","K","F"], :"g/kg", :"g/kg", :"g/kg", "mb", "mb", \ "g/m^3", "kg/m^3", ["C","K","F"], ["K","F","C"]] units = Array.new old_units = Array.new returnedValues = Array.new(N) intermediate = Array.new(N) alt_units = [:m, :ft] kk = 0 # these are used as units counters in the MSLP frame. kkk = 0 mslp_check_status = TkVariable.new(0) # Status of MSLP conversion check button mslp = TkVariable.new # Used in MSLPFrame to store sea-level pressure. elev = TkVariable.new # Used in MSLPFrame to store station elevation. name_label_text = ['Station Pressure', 'Temperature', 'Relative Humidity', \ 'Dewpoint', 'Specific Humidity', 'Mixing Ratio', \ 'Saturation Mixing Ratio', 'Vapor Press.', 'Saturation Vapor Press', \ 'Absolute Humidity', 'Density', 'Virtual Temperature', 'Potential Temperature'] # LIST OF CONSTANTS----------------------------------------------------------------------------------- R = 287.1 # specific gas constant for dry air Rv = 461.5 # specific gas constant for water vapor EPSIL = R/Rv Lv = 2.5e+06 # latent heat of vaporization T0 = 273.0 # reference temperature (K) E0 = 611.0 # saturation vapor pressure at T0 (Pa) P0 = 100000.0 # reference pressure (Pa) Cp = 1005.0 # specific heat at constant pressure g0 = 9.80665 # gravity # METHODS FOR CLAUSIUS-CLAPEYRON EQUATION AND INVERSE C-C EQUATION---------------- def cc_eqn(t) E0*Math.exp((Lv/Rv)*(1.0/T0 - 1.0/t)) end def cc_eqn_invert(e) 1.0/(1.0/T0 - (Rv/Lv)*Math.log(e/E0)) end # METHOD FOR CONVERTING TO SPECIFIED NUMBER OF SIGNIFICANT FIGURES def sig_fig(number, figs) return 0 if number == 0 mod = (number.abs).divmod(1) left, right = mod[0], mod[1] left_count = 0 if left != 0 then while left >= 1 do left = (left.divmod(10))[0] left_count += 1 end if left_count >= figs then m = left_count - figs return (number/(10**m)).round*10**m else m = figs - left_count return (number*10**m).round.to_f/10**m end else right_count = 0 while left == 0 do right = right*10 left = right.divmod(1)[0] right_count += 1 end m = right_count + figs -1 return (number*10**m).round.to_f/10**m end end # DEFINE METHOD FOR CONVERTING FROM LOCAL TEXT VARIABLES TO FLOATING MKS VARIABLES def to_mks(units,value_text,check_status) u = units v_text = value_text stat = check_status v = [nil]*N v[0] = v_text[0].value.to_f v[1] = v_text[1].value.to_f 2.upto(5) { |i| v[i] = v_text[i].value.to_f if stat[i-2] == 1 } # convert variables to mks units if not nil if u[0] == "mb" v[0] = v[0]*100.0 else v[0] = v[0]*101325.0/29.92 end if u[1] == "C" then v[1] = v[1] + 273.15 elsif u[1] == "F" then v[1] = (v[1] - 32.0)/1.8 + 273.15 else v[1] = v[1] end v[2] = v[2]/100.0 if stat[0] == 1 if stat[1] == 1 then if u[3] == "C" then v[3] = v[3] + 273.15 elsif u[3] == "F" then v[3] = (v[3] - 32.0)/1.8 + 273.15 else v[3] = v[3] end end v[4] = v[4]/1000.0 if stat[2] == 1 v[5] = v[5]/1000.0 if stat[3] == 1 return v end # DEFINE METHOD FOR CONVERTING FROM MKS TO LOCAL UNITS def to_local(units,intermediate) u = units v = intermediate # convert variables back to meteorological units if u[0] == "mb" then v[0] = v[0]/100.0 else v[0] = v[0]*29.92/101325.0 end if u[1] == "C" then v[1] = v[1] - 273.15 elsif u[1] == "F" then v[1] = (v[1] - 273.15)*1.8 + 32.0 else v[1] = v[1] end v[2] = v[2]*100.0 if u[3] == "C" then v[3] = v[3] - 273.15 if v[3] != nil elsif u[3] == "F" then v[3] = (v[3] - 273.15)*1.8 + 32.0 if v[3] != nil else v[3] = v[3] end v[4] = v[4]*1000.0 v[5] = v[5]*1000.0 v[6] = v[6]*1000.0 if v[6] != nil v[7] = v[7]/100.0 v[8] = v[8]/100.0 v[9] = v[9]*1000.0 # v[10] stays as is if u[11] == "C" then v[11] = v[11] - 273.15 elsif u[11] == "F" then v[11] = (v[11] - 273.15)*1.8 + 32.0 else v[11] = v[11] end if u[12] == "C" then v[12] = v[12] - 273.15 elsif u[12] == "F" then v[12] = (v[12] - 273.15)*1.8 + 32.0 else v[12] = v[12] end return v end # DEFINE ERROR CHECKING METHOD------------------------------------------------------------------------------ def error_check(ind, value_text, name_label_text, units) stat = [0, 1, ind] v_text = value_text label_text = name_label_text u = units return_value = 0 a = ["-","+",".","0","1","2","3","4","5","6","7","8","9"] # List of allowed characters in input warning_string = "" stat.each do |i| b = v_text[i].value.chomp.lstrip.rstrip # Check for temperture lower than absolute zero if i == 1 then if u[i] == "K" and b.to_f < 0.0 then warning_string = warning_string + "Temperature below absolute zero!\n" return_value = 1 elsif u[i] == "C" and b.to_f < -273.15 then warning_string = warning_string + "Temperature below absolute zero!\n" return_value = 1 elsif u[i] == "F" and b.to_f < -459.67 then warning_string = warning_string + "Temperature below absolute zero!\n" return_value = 1 end end # Check for negative pressure, RH, q, or r if (i != 1 and i != 3) and b.to_f < 0 then warning_string = warning_string + "Negative " + label_text[i] + "!\n" return_value = 1 end # Check for zero pressure if i == 0 and b.to_f == 0 then warning_string = warning_string + label_text[i] + " is zero!\n" return_value = 1 end # Check for too large q if (i == 4) and b.to_f > 999.0 then warning_string = warning_string + label_text[i] + " too large (max is 999 g/kg!)\n" return_value = 1 end # Check for misplaced + and - signs if b.index("+",1) or b.index("-",1) != nil then warning_string = warning_string + "+ or - sign in wrong location in " + label_text[i] + "!\n" return_value = 1 end # Check for more than one decimal mark if (b.count ".") > 1 then warning_string = warning_string + "Extra decimal mark in " + label_text[i] + "!\n" return_value = 1 end a.each { |s| b.delete!(s) } if b.length != 0 then warning_string = warning_string + "Invalid character in " + label_text[i] + "!\n" return_value = 1 end end warning_string = "CALCULATION NOT PERFORMED!!!\n" + warning_string if return_value == 1 MessageLabel.configure(:text => warning_string) return return_value end # DEFINE METHOD FOR CALCULATING OTHER VARIABLES--------------------------- def calculate(check_status,value_float) stat = check_status v = value_float return_value = 0 v[8] = cc_eqn(v[1]) # find saturation vapor pressure if v[2] != nil then # if RH is known v[7] = v[2]*v[8] # find vapor pressure v[5] = EPSIL*v[7]/(v[0] - v[7]) # find mixing ratio v[4] = 1.0/(v[0]/(EPSIL*v[7]) - 0.61) # find specific humidity if v[7] > 0 then v[3] = cc_eqn_invert(v[7]) # find dewpoint else v[3] = nil end elsif v[3] != nil then # if dewpoint is known v[7] = cc_eqn(v[3]) # find vapor pressure v[2] = v[7]/v[8] # find relative humidity v[5] = EPSIL*v[7]/(v[0] - v[7]) # find mixing ratio v[4] = 1.0/(v[0]/(EPSIL*v[7]) - 0.61) # find specific humidity elsif v[4] != nil then # if specific humidity is known v[7] = v[0]*v[4]/(EPSIL*(1.0 + 0.61*v[4])) # find vapor pressure v[5] = EPSIL*v[7]/(v[0] - v[7]) # find mixing ratio v[2] = v[7]/v[8] # find relative humidity if v[7] > 0 then v[3] = cc_eqn_invert(v[7]) # find dewpoint else v[3] = nil end else # if mixing ratio is known v[7] = v[0]*v[5]/(EPSIL + v[5]) # find vapor pressure v[2] = v[7]/v[8] # find relative humidity if v[7] > 0 then v[3] = cc_eqn_invert(v[7]) # find dewpoint else v[3] = nil end v[4] = 1.0/(v[0]/(EPSIL*v[7]) - 0.61) # find specific humidity end if v[0] > v[8] then v[6] = EPSIL*v[8]/(v[0] - v[8]) # saturation mixing ratio else v[6] = nil end v[9] = v[7]/(Rv*v[1]) # absolute humidity v[11] = v[1]*(1.0 + 0.61*v[4]) # virtual temperature v[10] = v[0]/(R*v[11]) # density v[12] = v[1]*(P0/v[0])**(R/Cp) # potential temperature # run some tests on variables to see if solution is physical, and generate warnings warning_string = "" if v[4] > 1.0 then warning_string = warning_string + "Specific humidity exceeds 1000g/kg.\n" return_value = 1 end if v[7] > v[0] then warning_string = warning_string + "Vapor pressure exceeds total pressure.\n" return_value = 1 end if v[10] <= 0 then warning_string = warning_string + "Density is negative.\n" return_value = 1 end if v[2] < 0.0 then warning_string = warning_string + "Relative humidity is negative.\n" return_value = 1 end if return_value == 1 then warning_string = "CALCULATION NOT PERFORMED!\nUnphysical Solution.\n" + warning_string v.each_index { |i| v[i] = nil } else warning_string = warning_string + "Note: Relative humidity exceeds 100%.\n" if v[2] > 1.0 warning_string = warning_string + "Note: Saturation mixing ratio is nil because\n" + "sat. vapor pressure exceeds total pressure.\n" if v[8] > v[0] end MessageLabel.configure(:text => warning_string) return v end #DEFINE METHOD FOR TOGGLING TEMPERATURE UNITS---------------------------------------------------- def temp_convert(new_unit, old_temp) return old_temp if old_temp == nil temp = old_temp.value.to_f if new_unit == "C" then new_temp = (temp - 32.0)/1.8 elsif new_unit == "K" then new_temp = temp + 273.15 else new_temp = (temp - 273.15)*1.8 + 32.0 end return sig_fig(new_temp, FIGURES).to_s end #DEFINE METHOD FOR TOGGLING PRESSURE UNITS---------------------------------------------------------- def press_convert(new_unit, old_press) return old_press if old_press == nil press = old_press.value.to_f if new_unit == "in" then new_press = press*29.92/1013.25 else new_press = press*1013.25/29.92 end return sig_fig(new_press, FIGURES).to_s end #DEFINE METHOD FOR TOGGLING ALTITUDE UNITS-------------------------------------------- def alt_convert(new_unit, old_alt) return old_alt if old_alt == nil alt = old_alt.value.to_f if new_unit == "m" then new_alt = alt*0.3048 else new_alt = alt/0.3048 end return sig_fig(new_alt, FIGURES).to_s end # CREATE CALCULATE BUTTON-------------------------------------------------------------------------------------- CalcButton = TkButton.new(CalculationFrame) do text "Calculate" command do ind = 0 # index for humidity variable specified (initialized to 0) check_status.each_index { |i| ind = i + 2 if check_status[i] == 1 } error_check_status = error_check(ind,value_text,name_label_text,units) # Calls error checking method if error_check_status == 0 then # Only executes calculation block if no errors returned from error checking method value_float = to_mks(units,value_text,check_status) # converts to mks units and floating point values intermediate = calculate(check_status,value_float) # Calls calculation method, passing check_status if intermediate != [nil]*N then returnedValues = to_local(units, intermediate) returnedValues.each_index do |i| if returnedValues[i] != nil && returnedValues[i].nan? != true && returnedValues[i].infinite? == nil then # Block to round to specified number of decimal places and convert to string returnedValues[i] = sig_fig(returnedValues[i], FIGURES) value_text[i].value = returnedValues[i].to_s else value_text[i].value = nil end end else 2.upto(5) { |i| value_text[i].value = nil if check_status[i-2] == 0 } 6.upto(N-1) { |i| value_text[i].value = nil } end end CalcButton.configure(:state => :disabled) # disables calculate button after calculation end pack(:side => :top) end # LOOP TO SET UP VALUE ENTRY AND DISPLAY WIDGETS------------------------------------------------------------ ValueWidgets.each_index do |i| NameLabels[i] = TkLabel.new(VariableFrame) do #set-up name labels text name_label_text[i] grid(:column => 0, :row => i, :sticky => :e) end value_text[i] = TkVariable.new(nil) if i == 0 then # creates entry box and label widget for station pressure ValueWidgets[i][0] = TkEntry.new(VariableFrame) do textvariable value_text[i] justify :right width 10 grid(:column => 2, :row => i, :sticky => :w) end ValueWidgets[i][1] = TkLabel.new(VariableFrame) do textvariable value_text[i] anchor :e justify :left width 10 end elsif i == 1 then # creates entry box for temperature ValueWidgets[i] = TkEntry.new(VariableFrame) do textvariable value_text[i] justify :right width 10 grid(:column => 2, :row => i, :sticky => :w) end elsif i >= 2 and i <=5 then # creates entry boxes and label widgets for RH, Td, q, and r ValueWidgets[i][0] = TkEntry.new(VariableFrame) do textvariable value_text[i] justify :right width 10 end ValueWidgets[i][1] = TkLabel.new(VariableFrame) do textvariable value_text[i] anchor :e justify :left width 10 grid(:column => 2, :row => i, :sticky => :w) end else # creates label widgets for all other variables ValueWidgets[i] = TkLabel.new(VariableFrame) do textvariable value_text[i] anchor :e justify :left width 10 grid(:column => 2, :row => i, :sticky => :w) end end end #CHECK BUTTONS FOR RH, Td, q, and r ---------------------------------------------------------- Checkbuttons.each_index do |i| check_status[i] = TkVariable.new(0) if i == 0 then check_status[i].value = 1 ValueWidgets[i+2][1].ungrid ValueWidgets[i+2][0].grid(:column => 2, :row => i+2, :sticky => :w) end Checkbuttons[i] = TkCheckbutton.new(VariableFrame) do variable check_status[i] command do # Disable other check buttons, and toggle between entry and label widgets check_status.each_index do |j| if i != j then Checkbuttons[j].deselect ValueWidgets[j+2][0].ungrid ValueWidgets[j+2][1].grid(:column => 2, :row => j+2, :sticky => :w) else ValueWidgets[j+2][1].ungrid ValueWidgets[j+2][0].grid(:column => 2, :row => j+2, :sticky => :w) end end end grid(:column => 1, :row => i+2, :sticky => :w) end end #SET UP UNITS LABELS FOR VARIABLES------------------------------------------- [2,4,5,6,7,8,9,10].each do |i| units[i] = TkVariable.new(units_list[i]) UnitsLabels[i] = TkLabel.new(VariableFrame) do textvariable units[i] grid(:column => 3, :row => i, :sticky => :w) end end [0,1,3,11,12].each do |i| units[i] = TkVariable.new units[i].value = units_list[i][0] UnitsLabels[i] = TkButton.new(VariableFrame) do textvariable units[i] command do if i != 0 && i != 12 then [1,3,11].each do |j| k[j] = k[j].succ%3 units[j].value = units_list[j][k[j]] value_text[j].value = temp_convert(units[j].value, value_text[j]) end elsif i == 12 then k[i] = k[i].succ%3 units[i].value = units_list[i][k[i]] value_text[i].value = temp_convert(units[i].value, value_text[i]) elsif i == 0 then k[i] = k[i].succ%2 kkk = kkk.succ%2 units[i].value = units_list[i][k[i]] value_text[i].value = press_convert(units[i].value, value_text[i]) mslp.value = press_convert(units[i].value, mslp) if mslp_check_status == 1 end end anchor :w grid(:column => 3, :row => i, :sticky => :we) end end # CHECKBUTTON TO SHOW MSLP FRAME WHEN CALCULATING STATION PRESSURE FROM SLP----- TkCheckbutton.new(root) do text "Calculate station pressure from sea-level pressure" variable mslp_check_status command do if mslp_check_status == 0 then elev.value = nil mslp.value = nil MSLPFrame.unpack ValueWidgets[0][1].ungrid ValueWidgets[0][0].grid(:in => VariableFrame, :column => 2, :row => 0, :sticky => :w) else elev.value = 0 mslp.value = value_text[0].value MSLPFrame.pack(:side => :top, :pady => 10, :before => VariableFrame) ValueWidgets[0][0].ungrid ValueWidgets[0][1].grid(:in => VariableFrame, :column => 2, :row => 0, :sticky => :w) end end justify :left pack(:side => :top, :before => VariableFrame) end #CREATE WIDGETS FOR MSLP FRAME--------------------------------------------------- TkLabel.new(MSLPFrame, :text => :"Sea-level Pressure").grid(:column =>0, :row => 0, :sticky => :e) MSLPEntry = TkEntry.new(MSLPFrame, :textvariable => mslp, :justify => :right, :width => 10).grid(:column => 1, :row => 0) TkButton.new(MSLPFrame) do textvariable units[0] anchor :w command do kkk = kkk.succ%2 k[0] = k[0].succ%2 units[0].value = units_list[0][kkk] value_text[0].value = press_convert(units[0].value, value_text[0]) mslp.value = press_convert(units[0].value, mslp) end width 5 grid(:column => 2, :row => 0, :sticky => :we) end TkLabel.new(MSLPFrame, :text => :Elevation).grid(:column => 0, :row => 1, :sticky => :e) ElevationEntry = TkEntry.new(MSLPFrame, :textvariable => elev, :justify => :right, :width => 10).grid(:column => 1, :row => 1) au = TkVariable.new(alt_units[0]) TkButton.new(MSLPFrame) do textvariable au anchor :w command do kk = kk.succ%2 au.value = alt_units[kk] elev.value = alt_convert(au.value, elev) end width 5 grid(:column => 2, :row => 1, :sticky => :we) end #LAMBDA CODE BLOCK TO CONVERT FROM SEA-LEVEL PRESSURE TO STATION PRESSURE mslp_to_sta = lambda { if au.value == "ft" then z = elev.value.to_f*0.3048 else z = elev.value.to_f end press = mslp.value.to_f if units[1] == "C" then t = value_text[1].value.to_f + 273.15 elsif units[1] == "F" then t = (value_text[1].value.to_f - 32.0)/1.8 + 273.15 else t = value_text[1].value.to_f end h = R*t/g0 press = press*Math.exp(-z/h) value_text[0].value = sig_fig(press, FIGURES).to_s } # SET UP BINDINGS FOR ENTRY BOXES-------------------------------------------------- ValueWidgets[0][0].bind("KeyRelease") do CalcButton.configure(:state => :normal) # activates calculate button if entry boxes change MessageLabel.configure(:text => "") # resets message label to blank if entry boxes change end ValueWidgets[1].bind("KeyRelease") do CalcButton.configure(:state => :normal) # activates calculate button if entry boxes change MessageLabel.configure(:text => "") # resets message label to blank if entry boxes change mslp_to_sta.call if mslp_check_status == 1 # updates station pressure if temperature changes end 2.upto(5) do |i| ValueWidgets[i][0].bind("KeyRelease") do CalcButton.configure(:state => :normal) # activates calculate button if entry boxes change MessageLabel.configure(:text => "") # resets message label to blank if entry boxes change end end MSLPEntry.bind("KeyRelease") do CalcButton.configure(:state => :normal) # activates calculate button if entry boxes change MessageLabel.configure(:text => "") # resets message label to blank if entry boxes change mslp_to_sta.call # updates station pressure if temperature changes end ElevationEntry.bind("KeyRelease") do CalcButton.configure(:state => :normal) # activates calculate button if entry boxes change MessageLabel.configure(:text => "") # resets message label to blank if entry boxes change mslp_to_sta.call # updates station pressure if temperature changes end #CREATE MESSAGE LABEL----------------------------------------------------------- MessageLabel = TkLabel.new(CalculationFrame).height(10).pack(:side => :top) #CREATE 'ABOUT' BUTTON----------------------------------------------------------- cb_value = TkVariable.new(0) about_label_text = TkVariable.new AboutButton = TkCheckbutton.new(root) do variable cb_value text "About ThermoCalc" command do if cb_value == 1 then @@AboutWindow = TkToplevel.new {title "About ThermoCalc"} @@AboutLabel = TkLabel.new(@@AboutWindow) do textvariable about_label_text wraplength 500 justify :left pack end else @@AboutWindow.destroy end end relief :groove pack(:side => :bottom) end about_label_text.value = "NO WARRANTY IS IMPLIED WITH THE USE OF THIS PROGRAM. It is intended \ for educational purposes only, and is not meant to be used when accuracy and reliability are \ paramount.\n\n\ ThermoCalc was written by Alex DeCaria. This version is from 3/27/2007. ThermoCalc \ may be used and distributed free-of-charge. Please give proper credit to the author.\n\n\ ThermoCalc calculates various thermodynamic properties of an air parcel using \ standard equations. It assumes the air parcel behaves as an ideal gas. There are \ no temperature dependencies for specific heat or latent heat of vaporization. Saturation \ vapor pressure is calculated over liquid water, even at freezing temperatures.\n\nAlthough it will yield \ calculations for very high and low temperatures and pressures, they are probably not \ accurate.\n\n\ The user inputs station pressure, temperature, and one of four humidity variables. \ There is an option to input sea-level pressure and elevation, rather than station pressure. \ In this instance the station pressure is calculated assuming an isothermal atmosphere \ between sea level and the station.\ \n\nThermoCalc is written in Ruby v 1.8.5. The GUI interface is provided by Tk. The version \ of Tk used in writting ThermoCalc was ActiveTC 8.4.14. \ Both Ruby and Tk must be installed in order to run ThermoCalc." Tk.mainloop