<% # ******************************************************************************* # OpenStudio(R), Copyright (c) Alliance for Sustainable Energy, LLC. # See also https://openstudio.net/license # ******************************************************************************* %> <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <style> .rhline { stroke: blue; stroke-width: 1; fill: none; } .rhtext { fill: blue; } .tdbline { stroke: green; stroke-width: 1; fill: none; } .tdbtext { fill: green; } .wline { stroke: red; stroke-width: 1; fill: none; } .wtext { fill: red; } .hline { stroke: black; stroke-width: 1; fill: none; } .htext { fill: black; } .twbline { stroke: orange; stroke-width: 1; fill: none; } .twbtext { fill: orange; } .tdpline { stroke: dimgray; stroke-width: 1; fill: none; } .tdptext { fill: dimgray; } .axis path, .axis line { fill: none; stroke: #000; shape-rendering: crispEdges; } .brush .extent { stroke: #fff; fill: grey; fill-opacity: .750; shape-rendering: crispEdges; } .brush .background { fill: grey; fill-opacity: .125; shape-rendering: crispEdges; } .grid .tick { stroke: lightgrey; opacity: 0.7; } .grid path { stroke-width: 0; } .noselect { -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } </style> <title>HVAC Psychrometric Chart</title> <link href="http://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.2.0/css/bootstrap.css" rel="stylesheet"> <script type="text/javascript" src="http://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script> <script type="text/javascript" src="http://cdnjs.cloudflare.com/ajax/libs/d3/3.3.9/d3.min.js"></script> <script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script> </head> <body> <div id="psychart" class="container"></div> <script> // This variable will be an array of data series to graph var obj = <%= all_series %>; $(document).ready(function() { var elev = 1828; // 6,000 ft var pc = new PsychrometricChart(elev); pc.drawChart("#psychart"); var i = 1; $.each(obj, function(index, all_series) { var series = obj[index]; pc.addPoints(series, i); i += 1; }); }); function PsychrometricChart(elevation) { // Specify the bounds of the chart // TODO make input params? this.tdb_min = -23.3; // -10F this.tdb_max = 48.9; // 120F this.w_min = 0; this.w_max = 0.035; this.elev = elevation; this.pb = pbFnElev(this.elev); // Specify the overall size of the chart plus range selector, etc. this.svgheight = 1000; this.svgwidth = 1200; // Setup the margins around the chart and date range selector this.chartmargin = { top: 50, right: 100, bottom: 70, left: 100 }; this.selectmargin = { top: 0, right: 100, bottom: 50, left: 100 }; // Determine the size of the chart and the selector // based on a fraction of the overall desired size this.chartheightfrac = 0.7; this.totalchartheight = this.svgheight * this.chartheightfrac; this.totalselectheight = this.svgheight * (1 - this.chartheightfrac); this.chartheight = this.totalchartheight - this.chartmargin.top - this.chartmargin.bottom; this.selectheight = this.totalselectheight - this.selectmargin.bottom - this.selectmargin.top; this.chartwidth = this.svgwidth - this.chartmargin.left - this.chartmargin.right; this.selectwidth = this.svgwidth - this.selectmargin.left - this.selectmargin.right; this.reflegendheight = this.chartheight / 4; this.reflegendwidth = this.chartwidth / 4; // Setup the spacing for the legends this.legendSpacing = 25; // Horizontal axis scale SI (C) this.tdb_extent = [this.tdb_min, this.tdb_max]; this.tdb_scale = d3.scale.linear() .range([0, this.chartwidth]) .domain(this.tdb_extent); // Horizontal axis scale IP (F) this.tdb_extent_F = [10, 120]; this.tdb_scale_F = d3.scale.linear() .range([0, this.chartwidth]) .domain(this.tdb_extent_F); // Vertical axis scale SI (C) this.w_extent = [this.w_min, this.w_max]; this.w_scale = d3.scale.linear() .range([this.chartheight, 0]) .domain(this.w_extent); // Generates a line scaled to the x/y axes this.addLine = d3.svg.line() .x(function(d) { return this.tdb_scale(d.tdb); }) .y(function(d) { return this.w_scale(d.w); }) .interpolate("linear"); // Draw the chart, including the reference lines // and the date range selector this.drawChart = function(idOfElementToAppendTo) { // Make an svg area for the chart, including the range selector this.svg = d3.select(idOfElementToAppendTo) .append("svg") .attr("class", "svg-psych").attr("id", "svg-psych") .attr("height", this.svgheight) .attr("width", this.svgwidth); // Make a group for the main chart body this.chartbody = this.svg.append("g") .attr("transform", "translate(" + this.chartmargin.left + "," + this.chartmargin.top + ")"); // Make a clipping mask to keep the lines inside the chart body this.chartbody.append("defs") .append("clipPath") .attr("id", "clip") .append("rect") .attr("width", this.chartwidth) .attr("height", this.chartheight); // Draw the reference lines inside the chart body group this.draw_w_lines(); this.draw_tdb_lines(); this.draw_h_lines(); this.draw_rh_lines(); this.draw_twb_lines(); this.draw_tdp_lines(); this.draw_rh_mask(); // Temporarily draw the points representing the chart bounds var corners = { name: "series 2", color: "black", data: [{ tdb: this.tdb_min, w: this.w_min }, { tdb: this.tdb_max, w: this.w_min }, { tdb: this.tdb_min, w: this.w_max }, { tdb: this.tdb_max, w: this.w_max }] }; //this.addPoints(corners); // Create the horizontal dry bulb temperature (tdb) scale var tdb_axis = d3.svg.axis().scale(this.tdb_scale); var tdb_axis_F = d3.svg.axis().scale(this.tdb_scale_F); // Create the vertical humidity ratio (w) scale var w_axis = d3.svg.axis().scale(this.w_scale).orient("right"); // Create a group containing tdb axis this.tdbaxis = this.chartbody.append("g") .attr("class", "tdb axis") .attr("id", "tdb-axis-C") .attr("transform", "translate(0," + (this.chartheight) + ")") .call(tdb_axis); // Create a group containing the w axis this.waxis = this.chartbody.append("g") .attr("class", "w axis") .attr("transform", "translate(" + (this.chartwidth) + ",0)") .call(w_axis); // Label the tdb axis this.tdbaxis.append("text") .text("Dry-bulb Temperature [°C]") .attr("class", "tdb-unit").attr("id", "tdb-axis-C-label") .attr("x", (this.chartwidth / 2)) // TODO dynamically account for width of axis label to center .attr("y", 50); // Label the w axis this.waxis.append("text") .text("Humidity Ratio [kg water/ kg dry air] ") .attr("transform", "rotate (-90, 0, 0) translate(" + -1 * (this.chartheight / 2) + "," + (this.chartmargin.right / 2 + 10) + ")"); // TODO dynamically account for width of axis label to center // Create a legend for the data series this.primlegend = this.chartbody.append("g") .attr("class", "w axis") .attr("transform", "translate(" + (this.chartwidth / 3) + ",0)"); this.primlegend.append("text").text("Series Names") .style("cursor", " pointer") .attr("y", this.legendSpacing * 1) .attr("class", "noselect"); // Create a legend for the reference lines this.legend = this.chartbody.append("g") .attr("class", "w axis") .attr("transform", "translate(" + (this.chartmargin.left / 2) + ",0)"); this.legend.append("text").text("Reference Lines (Click to Toggle On/Off)") .style("cursor", " pointer") .attr("y", this.legendSpacing * 1) .attr("class", "noselect"); this.legend.append("text").text("Dry Bulb Temperature") .on("click", toggletdblines) .style("cursor", " pointer") .attr("y", this.legendSpacing * 2) .attr("class", "tdbtext noselect"); this.legend.append("text").text("Relative Humidity") .on("click", togglerhlines) .style("cursor", " pointer") .attr("y", this.legendSpacing * 3) .attr("class", "rhtext noselect"); this.legend.append("text").text("Humidity Ratio") .on("click", togglewlines) .style("cursor", " pointer") .attr("y", this.legendSpacing * 4) .attr("class", "wtext noselect"); this.legend.append("text").text("Enthalpy") .on("click", togglehlines) .style("cursor", " pointer") .attr("y", this.legendSpacing * 5) .attr("class", "htext noselect"); this.legend.append("text").text("Wet Bulb Temperature") .on("click", toggletwblines) .style("cursor", " pointer") .attr("y", this.legendSpacing * 6) .attr("class", "twbtext noselect"); this.legend.append("text").text("Dew Point Temperature") .on("click", toggletdplines) .style("cursor", " pointer") .attr("y", this.legendSpacing * 7) .attr("class", "tdptext noselect"); function togglerhlines() { // Select the reference lines var refLines = d3.selectAll("path.rhline"); // Determine if current line is visible if (refLines.attr("active")) { refLines.style("display", "none"); refLines.attr("active", null); } else { refLines.style("display", "inherit"); refLines.attr("active", true); } } function toggletdblines() { // Select the reference lines var refLines = d3.selectAll("path.tdbline"); // Determine if current line is visible if (refLines.attr("active")) { refLines.style("display", "none"); refLines.attr("active", null); } else { refLines.style("display", "inherit"); refLines.attr("active", true); } } function togglewlines() { // Select the reference lines var refLines = d3.selectAll("path.wline"); // Determine if current line is visible if (refLines.attr("active")) { refLines.style("display", "none"); refLines.attr("active", null); } else { refLines.style("display", "inherit"); refLines.attr("active", true); } } function togglehlines() { // Select the reference lines var refLines = d3.selectAll("path.hline"); // Determine if current line is visible if (refLines.attr("active")) { refLines.style("display", "none"); refLines.attr("active", null); } else { refLines.style("display", "inherit"); refLines.attr("active", true); } } function toggletwblines() { // Select the reference lines var refLines = d3.selectAll("path.twbline"); // Determine if current line is visible if (refLines.attr("active")) { refLines.style("display", "none"); refLines.attr("active", null); } else { refLines.style("display", "inherit"); refLines.attr("active", true); } } function toggletdplines() { // Select the reference lines var refLines = d3.selectAll("path.tdpline"); // Determine if current line is visible if (refLines.attr("active")) { refLines.style("display", "none"); refLines.attr("active", null); } else { refLines.style("display", "inherit"); refLines.attr("active", true); } } var startDate = Date.parse("2009/01/01 01:00:00"); // TODO pull start and end date from 1st data series var endDate = Date.parse("2010/01/01 01:00:00"); var startHour = 0; var endHour = 24; // Create an x scale for the selector (date range) this.sel_x_scale = d3.time.scale() .domain([startDate, endDate]) .range([0, this.selectwidth]); // Create a y scale for the selector (hour range) this.sel_y_scale = d3.scale.linear() .domain([startHour, endHour]) .range([0, this.selectheight]); var brush = d3.svg.brush() .x(this.sel_x_scale) .y(this.sel_y_scale) .on("brush", brushed); // Make a group for the selector this.selector = this.svg.append("g") .attr("class", "x brush") .attr("id", "selector") .attr("transform", "translate(" + this.selectmargin.left + "," + (this.chartmargin.top + this.chartheight + this.chartmargin.bottom + this.selectmargin.top) + ")") .call(brush); this.selector.selectAll(".background") //.attr("height", this.selectheight) .style("visibility", "visible"); // Create the selector date x axis var selector_axis = d3.svg.axis().scale(this.sel_x_scale) .orient("bottom") .tickFormat(d3.time.format("%b-%d")); // Create a group containing selector date axis this.selectoraxis = this.selector.append("g") .attr("class", "selector axis") .attr("id", "selector-x-axis") .attr("transform", "translate(0," + (this.selectheight) + ")") .call(selector_axis) .selectAll("text") .style("text-anchor", "end") .attr("transform", function(d) { return "rotate(-65)"; }); // Create the selector time x axis selector_axis = d3.svg.axis().scale(this.sel_y_scale) .orient("left"); //.tickFormat(d3.time.format("%H")); // TODO figure out how to format ticks as time // Create a group containing selector time axis this.selectoraxis = this.selector.append("g") .attr("class", "selector axis") .attr("id", "selector-y-axis") .call(selector_axis) .selectAll("text") .style("text-anchor", "end"); function brushed() { var s = brush.extent(); var ldb = s[0][0]; var udb = s[1][0]; var ltb = s[0][1]; var utb = s[1][1]; // TODO implement brush snapping on the Y (time) axis of selector // like seen here: http://bl.ocks.org/mbostock/6232620 console.log("lower date bound =" + ldb + ", upper date bound = " + udb); console.log("lower time bound =" + ltb + ", upper time bound = " + utb); // Select all the circles var circles = d3.selectAll("circle"); // Hide circles before and after selected range var hiddenCircles = circles.filter(function(d) { return (Date.parse(d.time) <= ldb || Date.parse(d.time) >= udb); }); hiddenCircles.style("visibility", "hidden"); // Show circles in the selected range // TODO make this filter function easier to read! var visibleCircles = circles.filter(function(d) { return (new Date(d.time) > ldb && new Date(d.time) < udb && new Date(d.time).getHours() > ltb && new Date(d.time).getHours() < utb); }); visibleCircles.style("visibility", "visible"); } }; // Make a group for the month buttons // this.selector = this.svg.append("g") // .attr("class", "x brush") // .attr("id", "selector") // .attr("transform", "translate(" + this.selectmargin.left + "," + (this.chartmargin.top + this.chartheight + this.chartmargin.bottom + this.selectmargin.top) + ")") // .call(brush); this.draw_w_lines = function() { // Draw the horizontal humidity ratio lines // at .001 intervals for (var wIter = this.w_min; wIter <= this.w_max; wIter += 0.001) { var psychLine = []; for (var tdbIter = this.tdb_min; tdbIter <= this.tdb_max; tdbIter += 0.5) { psychLine.push({ tdb: tdbIter, w: wIter }); } this.chartbody.append("path") .attr("d", this.addLine(psychLine)) .attr("class", "wline") .attr("active", true) .attr("clip-path", "url(#clip)"); } }; this.draw_tdb_lines = function() { // Draw the vertical dry bulb temperature lines // at 5F intervals (2.78 delta-C) for (var tdbIter = this.tdb_min; tdbIter <= this.tdb_max; tdbIter += 2.78) { var psychLine = []; for (var wIter = this.w_min; wIter <= this.w_max; wIter += 0.001) { psychLine.push({ tdb: tdbIter, w: wIter }); } this.chartbody.append("path") .attr("d", this.addLine(psychLine)) .attr("class", "tdbline") .attr("active", true) .attr("clip-path", "url(#clip)"); } }; this.draw_rh_lines = function() { // Calculate the rh lines // at 10% rh intervals for (var rhIter = 0; rhIter <= 1; rhIter += 0.1) { var psychLine = []; //console.log(rhIter + " %RH") for (var tdbIter = this.tdb_min; tdbIter <= this.tdb_max; tdbIter += 0.5) { var w = psyWFnTdbRhPb(tdbIter, rhIter, this.pb); psychLine.push({ tdb: tdbIter, w: w }); //console.log("---tdb = " + tdbIter + ", rh = " + rhIter + " => w = " + w) } this.chartbody.append("path") .attr("d", this.addLine(psychLine)) .attr("class", "rhline") .attr("active", true) .attr("clip-path", "url(#clip)"); } }; this.draw_h_lines = function() { // Calculate the enthalpy lines // at 5Btu/lb dry air (11630 J/kg) intervals for (var hIter = 15000; hIter <= 139560; hIter += 11630) { // TODO calc min/max h from limits var psychLine = []; //console.log("enthalpy (h) = " + hIter) for (var tdbIter = this.tdb_min; tdbIter <= this.tdb_max; tdbIter += 2) { var w = psyWFnTdbH(tdbIter, hIter); psychLine.push({ tdb: tdbIter, w: w }); //console.log("---tdb = " + tdbIter + ", h = " + hIter + " => w = " + w) } this.chartbody.append("path") .attr("d", this.addLine(psychLine)) .attr("class", "hline") .attr("active", true) .attr("clip-path", "url(#clip)"); } }; this.draw_twb_lines = function() { // Calculate the wetbulb lines // at 5C intervals for (var twbIter = 0; twbIter <= 100; twbIter += 1) { // TODO calc min/max h from limits var psychLine = []; console.log("twb (C) = " + twbIter); for (var tdbIter = this.tdb_min; tdbIter <= this.tdb_max; tdbIter += 2) { var w = psyWFnTdbTwbPb(tdbIter, twbIter, this.pb); psychLine.push({ tdb: tdbIter, w: w }); console.log("---tdb = " + tdbIter + ", twb = " + twbIter + " => w = " + w); } this.chartbody.append("path") .attr("d", this.addLine(psychLine)) .attr("class", "twbline") .attr("active", true) .attr("clip-path", "url(#clip)"); } }; this.draw_tdp_lines = function() { // Calculate the dew-point lines // at 5C intervals for (var tdpIter = -20; tdpIter <= 100; tdpIter += 1) { // TODO calc min/max h from limits var psychLine = []; console.log("tdp (C) = " + tdpIter); for (var tdbIter = this.tdb_min; tdbIter <= this.tdb_max; tdbIter += 2) { var w = psyWFnTdpPb(tdpIter, this.pb); psychLine.push({ tdb: tdbIter, w: w }); console.log("---tdb = " + tdbIter + ", tdp = " + tdpIter + " => w = " + w); } this.chartbody.append("path") .attr("d", this.addLine(psychLine)) .attr("class", "tdpline") .attr("active", true) .attr("clip-path", "url(#clip)"); } }; this.draw_rh_mask = function() { // Calculate the rh line at 100% // and use to make a mask to cover the upper // left region of the chart var rhMax = 1; var psychLine = []; for (var tdbIter = this.tdb_min; tdbIter <= this.tdb_max; tdbIter += 0.5) { var w = psyWFnTdbRhPb(tdbIter, rhMax, this.pb); psychLine.push({ tdb: tdbIter, w: w }); //console.log("---tdb = " + tdbIter + ", rh = " + rhMax + " => w = " + w) } //var wMax = psyWFnTdbRhPb(tdbIter, , this.pb) psychLine.push({ tdb: this.tdb_min, w: this.w_max }); this.chartbody.append("path") // TODO Change this to a clipping mask instead of a shape with fill .attr("d", this.addLine(psychLine)) .attr("class", "rhmask") .attr("clip-path", "url(#clip)") .attr("stroke", "white") // TODO use CSS to style? .attr("fill", "white"); // TODO use CSS to style? }; this.addPoints = function(series, i) { // Assign this chart to a variable // so that we can use its info inside the d3 functions pc = this; // Add the series name to the legend this.primlegend.append("text").text(series.name) .style("cursor", " pointer") .attr("y", this.legendSpacing * (i + 1)) .attr("class", "noselect") .style("fill", series.color); // Add a group to hold the points in the series var seriesGroup = this.chartbody.append("g") .attr("class", "psych-series"); var points = seriesGroup.selectAll("circle") .data(series.data) .enter() .append("circle"); var pointAttributes = points.attr("cx", function(d) { return pc.tdb_scale(d.tdb); }) // .attr("cy", function(d) { return pc.w_scale(d.w); }) .attr("r", 4) .style("fill", series.color) .style("fill-opacity", 0.3) .append("svg:title") .text(function(d) { return d.time + ": Tdb = " + d.tdb.toFixed(1) + "C, W = " + d.w.toFixed(3) + "lb H2O/lb dry air"; }); }; } // This library of psychrometric calculations was adapted from the EnergyPlus // psychrometric calculations (C++) which can be found here: // https://github.com/NREL/EnergyPlus/blob/149f03ea2ce84582828f037faf768892ca674f09/src/EnergyPlus/Psychrometrics.hh // The calculation methodologies are unchanged, however, the mechanisms for caching // function results for later use, and the logging of error messages was removed. var KELVIN_CONV = 273.15; // PURPOSE OF THIS FUNCTION: // This function provides barometric pressure as a function // of elevation. function pbFnElev( elev // elevation (Meters) ) { // FUNCTION INFORMATION: // AUTHOR Andrew Parker // DATE WRITTEN Dec 9, 2014 // MODIFIED na // RE-ENGINEERED na // REFERENCES: // A Quick Derivation relating altitude to air pressure // 2004 Portland State Aerospace Society // http://psas.pdx.edu/RocketScience/PressureAltitude_Derived.pdf // Equation 9 var pb = 100 * Math.pow(((44331.514 - elev) / 11880.516), (1 / 0.1902632)); // Barometric Pressure {Pa} return pb; } // PURPOSE OF THIS FUNCTION: // This function provides the saturation pressure as a function of temperature. function psyPsatFnTemp( tdb // dry-bulb temperature {C} ) { // FUNCTION INFORMATION: // AUTHOR George Shih // DATE WRITTEN May 1976 // MODIFIED NA // RE-ENGINEERED Nov 2003; Rahul Chillar // METHODOLOGY EMPLOYED: // Hyland & Wexler Formulation, range -100C to 200C // REFERENCES: // ASHRAE HANDBOOK OF FUNDAMENTALS, 2005, Chap 6 (Psychrometrics), Eqn 5 & 6. // Compared to Table 3 values (August 2007) with average error of 0.00%, max .30%, // min -.39%. (Spreadsheet available on request - Lawrie). // Return value var pb; // result=> saturation pressure {Pascals} // Convert temperature from Centigrade to Kelvin. var tkel = tdb + KELVIN_CONV; // Dry-bulb in REAL(r64) for function passing // If below -100C,set value of Pressure corresponding to Saturation Temperature of -100C. if (tkel < 173.15) { pb = 0.0017; // If below freezing, calculate saturation pressure over ice. } else if (tkel < KELVIN_CONV) { // tkel >= 173.15 var C1 = -5674.5359; // Coefficient for TKel < KelvinConvK var C2 = 6.3925247; // Coefficient for TKel < KelvinConvK var C3 = -0.9677843e-2; // Coefficient for TKel < KelvinConvK var C4 = 0.62215701e-6; // Coefficient for TKel < KelvinConvK var C5 = 0.20747825e-8; // Coefficient for TKel < KelvinConvK var C6 = -0.9484024e-12; // Coefficient for TKel < KelvinConvK var C7 = 4.1635019; // Coefficient for TKel < KelvinConvK pb = Math.exp(C1 / tkel + C2 + tkel * (C3 + tkel * (C4 + tkel * (C5 + C6 * tkel))) + C7 * Math.log(tkel)); // If above freezing, calculate saturation pressure over liquid water. } else if (tkel <= 473.15) { // tkel >= 173.15 // tkel >= KELVIN_CONV var C8 = -5800.2206; // Coefficient for TKel >= KelvinConvK var C9 = 1.3914993; // Coefficient for TKel >= KelvinConvK var C10 = -0.048640239; // Coefficient for TKel >= KelvinConvK var C11 = 0.41764768e-4; // Coefficient for TKel >= KelvinConvK var C12 = -0.14452093e-7; // Coefficient for TKel >= KelvinConvK var C13 = 6.5459673; // Coefficient for TKel >= KelvinConvK pb = Math.exp(C8 / tkel + C9 + tkel * (C10 + tkel * (C11 + tkel * C12)) + C13 * Math.log(tkel)); // If above 200C, set value of Pressure corresponding to Saturation Temperature of 200C. } else { // tkel >= 173.15 // tkel >= KELVIN_CONV // tkel > 473.15 pb = 1555000; } return pb; } // PURPOSE OF THIS FUNCTION: // This function provides density of air as a function of barometric // pressure, dry bulb temperature, and humidity ratio. function psyRhoAirFnPbTdbW( pb, // barometric pressure (Pascals) tdb, // dry bulb temperature (Celsius) w // humidity ratio (kgWater/kgDryAir) ) { // FUNCTION INFORMATION: // AUTHOR G. S. Wright // DATE WRITTEN June 2, 1994 // MODIFIED na // RE-ENGINEERED na // METHODOLOGY EMPLOYED: // ideal gas law // universal gas const for air 287 J/(kg K) // air/water molecular mass ratio 28.9645/18.01534 // REFERENCES: // Wylan & Sontag, Fundamentals of Classical Thermodynamics. // ASHRAE handbook 1985 Fundamentals, Ch. 6, eqn. (6),(26) var rhoair = pb / (287 * (tdb + KELVIN_CONV) * (1 + 1.6077687 * Math.max(w, 1e-5))); return rhoair; } // PURPOSE OF THIS FUNCTION: // This function provides latent energy of air as function of humidity ratio and temperature. function psyHfgAirFnWTdb( w, // humidity ratio {kgWater/kgDryAir} !unused1208 tdb // input temperature {Celsius} ) { // FUNCTION INFORMATION: // AUTHOR Richard Liesen // DATE WRITTEN May, 2001 // MODIFIED June, 2002 // RE-ENGINEERED na // METHODOLOGY EMPLOYED: // calculates hg and then hf and the difference is Hfg. // REFERENCES: // see ASHRAE Fundamentals psychrometric Chapter // USAGE: hfg = psyHfgAirFnWTdb(w,tdb) // Return value // result => heat of vaporization for moist air {J/kg} // This formulation currently does not use W since it returns results that are in J/kg and the // amount of energy is on a per unit of moisture basis. tdb = Math.max(tdb, 0); // input temperature {Celsius} - corrected for >= 0C var hfg = (2500940 + 1858.95 * Temperature) - (4180 * Temperature); // enthalpy of the gas - enthalpy of the fluid return hfg; } // PURPOSE OF THIS FUNCTION: // This function provides latent energy of the moisture as a gas in the air as // function of humidity ratio and temperature. function psyHgAirFnWTdb( w, // humidity ratio {kgWater/kgDryAir} !unused1208 tdb // input temperature {Celsius} ) { // FUNCTION INFORMATION: // AUTHOR Richard Liesen // DATE WRITTEN May, 2001 // MODIFIED June, 2002 // RE-ENGINEERED na // REFERENCES: // see ASHRAE Fundamentals psychrometric Chapter // USAGE: hg = psyHgAirFnWTdb(w,tdb) // This formulation currently does not use W since it returns results that are in J/kg and the // amount of energy is on a per unit of moisture basis. var hg = 2500940 + 1858.95 * tdb; // enthalpy of the gas {units?} return hg; } // PURPOSE OF THIS FUNCTION: // This function calculates the enthalpy {J/kg} from dry-bulb temperature and humidity ratio. function psyHFnTdbW( tdb, // dry-bulb temperature {C} w // humidity ratio ) { // FUNCTION INFORMATION: // AUTHOR George Shih // DATE WRITTEN May 1976 // MODIFIED na // RE-ENGINEERED na // REFERENCES: // ASHRAE HANDBOOK OF FUNDAMENTALS, 1972, P100, EQN 32 // calculate enthalpy var h = 1.00484e3 * tdb + Math.max(w, 1e-5) * (2.50094e6 + 1.85895e3 * tdb); // enthalpy {J/kg} return h; } // PURPOSE OF THIS FUNCTION: // This function provides the heat capacity of air {J/kg-C} as function of humidity ratio. function psyCpAirFnWTdb( w, // humidity ratio {kgWater/kgDryAir} tdb // input temperature {Celsius} ) { // FUNCTION INFORMATION: // AUTHOR J. C. VanderZee // DATE WRITTEN Feb. 1994 // MODIFIED na // RE-ENGINEERED na // METHODOLOGY EMPLOYED: // take numerical derivative of psyHFnTdbW function // REFERENCES: // see psyHFnTdbW ref. to ASHRAE Fundamentals // USAGE: cpa = psyCpAirFnWTdb(w,tdb) // compute heat capacity of air w = Math.max(w, 1e-5); var cpa = (psyHFnTdbW(tdb + 0.1, w) - psyHFnTdbW(tdb, w)) * 10; // result => heat capacity of air {J/kg-C} return cpa; } // PURPOSE OF THIS FUNCTION: // This function provides air temperature from enthalpy and humidity ratio. function psyTdbFnHW( h, // enthalpy {J/kg} w // humidity ratio ) { // FUNCTION INFORMATION: // AUTHOR J. C. VanderZee // DATE WRITTEN Feb. 1994 // MODIFIED na // RE-ENGINEERED na // REFERENCES: // ASHRAE HANDBOOK OF FUNDAMENTALS, 1972, P100, EQN 32 // by inverting function psyHFnTdbW w = Math.max(w, 1e-5); // humidity ratio var tdb = (h - 2.50094e6 * w) / (1.00484e3 + 1.85895e3 * w); // result=> dry-bulb temperature {C} return tdb; } // PURPOSE OF THIS FUNCTION: // This function provides the Vapor Density in air as a // function of dry bulb temperature, and Relative Humidity. function psyRhovFnTdbRhLBnd0C( tdb, // dry-bulb temperature {C} rh // relative humidity value (0-1) ) { // FUNCTION INFORMATION: // AUTHOR R. J. Liesen // DATE WRITTEN July 2000 // MODIFIED Name change to signify derivation and temperatures were used // with 0C as minimum; LKL January 2008 // RE-ENGINEERED na // METHODOLOGY EMPLOYED: // ideal gas law // Universal gas const for water vapor 461.52 J/(kg K) // REFERENCES: // ASHRAE handbook 1993 Fundamentals, var rhoVaporDensity = rh / (461.52 * (tdb + KELVIN_CONV)) * Math.exp(23.7093 - 4111 / ((tdb + KELVIN_CONV) - 35.45)); // Vapor density in air return rhoVaporDensity; } // PURPOSE OF THIS FUNCTION: // This function provides the Vapor Density in air as a // function of dry bulb temperature, Humidity Ratio, and Barometric Pressure. function psyRhovFnTdbWPb( tdb, // dry-bulb temperature {C} w, // humidity ratio pb // Barometric Pressure {Pascals} ) { // FUNCTION INFORMATION: // AUTHOR R. J. Liesen // DATE WRITTEN July 2000 // MODIFIED na // RE-ENGINEERED na // METHODOLOGY EMPLOYED: // ideal gas law // Universal gas const for water vapor 461.52 J/(kg K) // REFERENCES: // ASHRAE handbook 1993 Fundamentals, w = Math.max(w, 1e-5); // humidity ratio var rhoAir = w * pb / (461.52 * (tdb + KELVIN_CONV) * (w + 0.62198)); return rhoAir; } // PURPOSE OF THIS FUNCTION: // This function provides the Relative Humidity in air as a // function of dry bulb temperature and Vapor Density. function psyRhFnTdbRhovLBnd0C( tdb, // dry-bulb temperature {C} rhovapor // vapor density in air {kg/m3} ) { // FUNCTION INFORMATION: // AUTHOR R. J. Liesen // DATE WRITTEN July 2000 // MODIFIED Name change to signify derivation and temperatures were used // with 0C as minimum; LKL January 2008 // RE-ENGINEERED na // METHODOLOGY EMPLOYED: // ideal gas law // Universal gas const for water vapor 461.52 J/(kg K) // REFERENCES: // ASHRAE handbook 1993 Fundamentals, var rh = rhovapor > 0 ? rhovapor * 461.52 * (tdb + KELVIN_CONV) * Math.exp(-23.7093 + 4111 / ((tdb + KELVIN_CONV) - 35.45)) : 0; if ((rh < 0) || (rh > 1)) { rh = Math.min(Math.max(rh, 0.01), 1); } return rh; } // PURPOSE OF THIS FUNCTION: // This function provides the specific volume from dry-bulb temperature, // humidity ratio and barometric pressure. function psyVFnTdbWPb( tdb, // dry-bulb temperature {C} w, // humidity ratio pb // barometric pressure {Pascals} ) { // FUNCTION INFORMATION: // AUTHOR George Shih // DATE WRITTEN May 1976 // MODIFIED na // RE-ENGINEERED na // REFERENCES: // ASHRAE HANDBOOK OF FUNDAMENTALS, 1972, P99, EQN 28 w = Math.max(w, 1e-5); // humidity ratio var v = 1.59473e2 * (1 + 1.6078 * w) * (1.8 * tdb + 492) / pb; // specific volume {m3/kg} return v; } // PURPOSE OF THIS FUNCTION: // This function provides the humidity ratio from dry-bulb temperature // and enthalpy. function psyWFnTdbH( tdb, // dry-bulb temperature {C} h // enthalpy {J/kg} ) { // FUNCTION INFORMATION: // AUTHOR George Shih // DATE WRITTEN May 1976 // MODIFIED na // RE-ENGINEERED na // REFERENCES: // ASHRAE HANDBOOK OF FUNDAMENTALS, 1972, P100, EQN 32 var w = (h - 1.00484e3 * tdb) / (2.50094e6 + 1.85895e3 * tdb); // humidity ratio // Validity test if (w < 0) { w = 1e-5; } return w; } // PURPOSE OF THIS FUNCTION: // This function provides the Vapor Density in air as a // function of dry bulb temperature, and Relative Humidity. function psyRhovFnTdbRh( tdb, // dry-bulb temperature {C} rh // relative humidity value (0-1) ) { // FUNCTION INFORMATION: // AUTHOR R. J. Liesen // DATE WRITTEN July 2000 // MODIFIED Change temperature range applied (determine pws); Aug 2007; LKL // Function is continuous over temperature spectrum // RE-ENGINEERED na // METHODOLOGY EMPLOYED: // ideal gas law // Universal gas const for water vapor 461.52 J/(kg K) // REFERENCES: // ASHRAE handbook 1993 Fundamentals, ?? // Used values from Table 2, HOF 2005, Chapter 6, to verify that these values match (at saturation) // values from psyRhFnTdbWPb var rhovapordensity = (psyPsatFnTemp(tdb) * rh) / (461.52 * (tdb + KELVIN_CONV)); // Vapor density in air return rhovapordensity; } // PURPOSE OF THIS FUNCTION: // This function provides the Relative Humidity in air as a // function of dry bulb temperature and Vapor Density. function psyRhFnTdbRhov( tdb, // dry-bulb temperature {C} rhovapor // vapor density in air {kg/m3} ) { // FUNCTION INFORMATION: // AUTHOR R. J. Liesen // DATE WRITTEN July 2000 // MODIFIED Change temperature range applied (determine pws); Aug 2007; LKL // Function is continuous over temperature spectrum // RE-ENGINEERED na // METHODOLOGY EMPLOYED: // ideal gas law // Universal gas const for water vapor 461.52 J/(kg K) // REFERENCES: // ASHRAE handbook 1993 Fundamentals, // Used values from Table 2, HOF 2005, Chapter 6, to verify that these values match (at saturation) // values from psyRhFnTdbWPb // FUNCTION PARAMETER DEFINITIONS: var rh = rhovapor > 0 ? rhovapor * 461.52 * (tdb + KELVIN_CONV) / psyPsatFnTemp(tdb) : 0.0; if ((rh < 0) || (rh > 1)) { rh = Math.min(Math.max(rh, 0.01), 1); } return rh; } // PURPOSE OF THIS FUNCTION: // This function provides the relative humidity value (0-1) as a result of // dry-bulb temperature, humidity ratio and barometric pressure. function psyRhFnTdbWPb( tdb, // dry-bulb temperature {C} w, // humidity ratio pb // barometric pressure {Pascals} ) { // FUNCTION INFORMATION: // AUTHOR Richard J. Liesen // DATE WRITTEN Nov 1988 // MODIFIED Aug 1989, Michael J. Witte // RE-ENGINEERED na // REFERENCES: // ASHRAE HANDBOOK FUNDAMENTALS 1985, P6.12, EQN 10,21,23 // FUNCTION PARAMETER DEFINITIONS: var pws = psyPsatFnTemp(tdb); // Pressure -- saturated for pure water // Find Degree Of Saturation w = Math.max(w, 1e-5); // humidity ratio var u = w / (0.62198 * pws / (pb - pws)); // Degree of Saturation // Calculate The Relative Humidity var rh = u / (1 - (1 - u) * (pws / pb)); // Validity test if ((rh < 0) || (rh > 1)) { rh = Math.min(Math.max(rh, 0.01), 1); } return rh; } // PURPOSE OF THIS FUNCTION: // This function provides the humidity ratio from dew-point temperature // and barometric pressure. function psyWFnTdpPb( tdp, // dew-point temperature {C} pb // barometric pressure {Pascals} ) { // FUNCTION INFORMATION: // AUTHOR George Shih // DATE WRITTEN May 1976 // MODIFIED na // RE-ENGINEERED na // REFERENCES: // ASHRAE HANDBOOK OF FUNDAMENTALS, 1972, P99, EQN 22 // FUNCTION PARAMETER DEFINITIONS: var pdew = psyPsatFnTemp(tdp); // saturation pressure at dew-point temperature {Pascals} var w = pdew * 0.62198 / (pb - pdew); // humidity ratio // Validity test if (w < 0) { w = 1e-5; } return w; } // PURPOSE OF THIS FUNCTION: // This function provides the humidity ratio from dry-bulb temperature, // relative humidty (value) and barometric pressure. function psyWFnTdbRhPb( tdb, // dry-bulb temperature {C} rh, // relative humidity value (0-1) pb // barometric pressure {Pascals} ) { // FUNCTION INFORMATION: // AUTHOR George Shih // DATE WRITTEN May 1976 // MODIFIED na // RE-ENGINEERED na // REFERENCES: // ASHRAE HANDBOOK OF FUNDAMENTALS, 1972, P99, EQN 22 // FUNCTION PARAMETER DEFINITIONS: var pdew = rh * psyPsatFnTemp(tdb); // Pressure at dew-point temperature {Pascals} // Numeric error check when the temperature and rh values cause Pdew to equal or exceed // barometric pressure which is physically impossible. An approach limit of 1000 pascals // was chosen to keep the numerics stable as the denominator approaches 0. // THIS EQUATION IN SI UNIT IS FROM ASHRAE HANDBOOK OF FUNDAMENTALS PAGE 99 EQUATION 22 var w = pdew * 0.62198 / Math.max(pb - pdew, 1000); // humidity ratio // Validity test if (w < 0) { w = 1e-5; } return w; } // PURPOSE OF THIS FUNCTION: // This function provides the humidity ratio from dry-bulb temperature, // wet-bulb temperature and barometric pressure. function psyWFnTdbTwbPb( tdb, // dry-bulb temperature {C} twb, // wet-bulb temperature {C} pb // barometric pressure {Pascals} ) { // FUNCTION INFORMATION: // AUTHOR George Shih // DATE WRITTEN May 1976 // MODIFIED na // RE-ENGINEERED na // REFERENCES: // ASHRAE HANDBOOK OF FUNDAMENTALS, 1972, P99, EQ 22,35 // FUNCTION PARAMETER DEFINITIONS: // Validity check if (twb > tdb) { twb = tdb; } // Calculation var pwet = psyPsatFnTemp(twb); // Pressure at wet-bulb temperature {Pascals} var wwb = 0.62198 * pwet / (pb - pwet); // Humidity ratio at wet-bulb temperature var w = ((2501 - 2.381 * twb) * wwb - (tdb - twb)) / (2501 + 1.805 * tdb - 4.186 * twb); // humidity ratio // Validity check if (w < 0) { w = psyWFnTdbRhPb(tdb, 0.0001, pb); } return w; } // PURPOSE OF THIS FUNCTION: // This function provides air enthalpy from temperature and relative humidity. function psyHFnTdbRhPb( tdb, // dry-bulb temperature {C} rh, // relative humidity value (0 - 1) pb // barometric pressure (N/M**2) {Pascals} ) { // FUNCTION INFORMATION: // AUTHOR J. C. VanderZee // DATE WRITTEN Feb. 1994 // MODIFIED na // RE-ENGINEERED na // METHODOLOGY EMPLOYED: // na // REFERENCES: // ASHRAE HANDBOOK OF FUNDAMENTALS, 1972, P100, EQN 32 // by using functions psyWFnTdbRhPb and psyHFnTdbW return psyHFnTdbW(tdb, Math.max(psyWFnTdbRhPb(tdb, rh, pb), 1e-5)); // enthalpy {J/kg} } // PURPOSE OF THIS FUNCTION: // This function calculates the dew-point temperature {C} from humidity ratio and pressure. function psyTdpFnWPb( w, // humidity ratio pb // barometric pressure (N/M**2) {Pascals} ) { // FUNCTION INFORMATION: // AUTHOR George Shih // DATE WRITTEN May 1976 // MODIFIED na // RE-ENGINEERED na // METHODOLOGY EMPLOYED: // na // REFERENCES: // ASHRAE HANDBOOK OF FUNDAMENTALS, 1972, P.99, EQN 22 w = Math.max(w, 1e-5); // limited humidity ratio var pdew = pb * W0 / (0.62198 + W0); // pressure at dew point temperature var tsat = psyTsatFnPb(pdew); return tsat; } // PURPOSE OF THIS FUNCTION: // This function calculates the dew-point temperature {C} from dry-bulb, wet-bulb and pressure. function psyTdpFnTdbTwbPb( tdb, // dry-bulb temperature {C} twb, // wet-bulb temperature {C} pb // barometric pressure (N/M**2) {Pascals} ) { // FUNCTION INFORMATION: // AUTHOR George Shih // DATE WRITTEN May 1976 // MODIFIED na // RE-ENGINEERED na var w = Math.max(psyWFnTdbTwbPb(tdb, twb, pb), 1e-5); var tdp = psyTdpFnWPb(w, pb); if (tdp > twb) { tdp = twb; } return tdp; } function psyTsatFnPb( pb // barometric pressure {Pascals} ) { // FUNCTION INFORMATION: // AUTHOR George Shih // DATE WRITTEN May 1976 // RE-ENGINEERED Dec 2003; Rahul Chillar // PURPOSE OF THIS FUNCTION: // This function provides the saturation temperature from barometric pressure. // METHODOLOGY EMPLOYED: // na // REFERENCES: // 1989 ASHRAE Handbook - Fundamentals // Checked against 2005 HOF, Chap 6, Table 3 (using pressure in, temperature out) with // good correlation from -60C to 160C var itmax = 50; // Maximum number of iterations var convTol = 0.0001; // FUNCTION LOCAL VARIABLE DECLARATIONS: var pbSave = -99999; var tSatSave = -99999; var tSat; // Water temperature guess var iter; // Iteration counter // Check press in range. if (pb == pbSave) { return tSatSave; } pbSave = pb; // Uses an iterative process to determine the saturation temperature at a given // pressure by correlating saturated water vapor as a function of temperature. // Initial guess of boiling temperature tSat = 100; iter = 0; // If above 1555000,set value of Temp corresponding to Saturation Pressure of 1555000 Pascal. if (pb >= 1555000) { tSat = 200; // If below 0.0017,set value of Temp corresponding to Saturation Pressure of 0.0017 Pascal. } else if (pb <= 0.0017) { tSat = -100; // Setting Value of PsyTsatFnPb= 0C, due to non-continuous function for Saturation Pressure at 0C. } else if ((pb > 611) && (pb < 611.25)) { tSat = 0; } else { // Iterate to find the saturation temperature // of water given the total pressure // Set iteration loop parameters // make sure these are initialized var pSat; // Pressure corresponding to temp. guess var error; // Deviation of dependent variable in iteration var x1; // Previous value of independent variable in ITERATE var y1; // Previous value of dependent variable in ITERATE var resultx; // resultx is the final Iteration result passed back to the calling routine var icvg; // Iteration convergence flag for (iter = 1; iter <= itmax; ++iter) { // Calculate saturation pressure for estimated boiling temperature pSat = psyPsatFnTemp(tSat); // Compare with specified pressure and update estimate of temperature error = pb - pSat; //Iterate( ResultX, convTol, tSat, error, X1, Y1, iter, icvg ); var small = 1e-9; var perturb = 0.1; // Check for convergence by comparing change in X if (iter != 1 && (Math.abs(tSat - x1) < convTol || error === 0)) { resultx = tSat; icvg = 1; } else { // Not converged icvg = 0; if (iter === 1) { // New guess is specified by Perturb if (Math.abs(tSat) > small) { resultx = tSat * (1 + perturb); } else { resultx = perturb; } } else { // New guess calculated from LINEAR FIT of most recent two points var dy = error - y1; if (Math.abs(dy) < small) dy = small; // new estimation resultx = (error * x1 - y1 * tSat) / dy; } x1 = tSat; y1 = error; } tSat = resultx; // If converged leave loop iteration if (icvg == 1) break; // Water temperature not converged, repeat calculations with new // estimate of water temperature } // Saturation temperature has not converged after maximum specified // iterations. Print error message, set return error flag, and RETURN } // End If for the Pressure Range Checking // Result is SatTemperature return tSat; // result=> saturation temperature {C} } // PURPOSE OF THIS FUNCTION: // This function provides the wet-bulb temperature from dry-bulb temperature, // humidity ratio and barometric pressure. function psyTwbFnTdbWPb( tdb, // dry-bulb temperature {C} w, // humidity ratio pb // barometric pressure {Pascals} ) { // FUNCTION INFORMATION: // AUTHOR George Shih // DATE WRITTEN May 1976 // MODIFIED na // RE-ENGINEERED Dec 2003; Rahul Chillar // 2011; as time saving measure, cache some values. // METHODOLOGY EMPLOYED: // Uses an Iterative procedure to calculate WetBulbTemperature // Return value var twb; // result=> Temperature Wet-Bulb {C} // Locals // FUNCTION ARGUMENT DEFINITIONS: // FUNCTION PARAMETER DEFINITIONS: var itmax = 100; // Maximum No of Iterations var convTol = 0.0001; // FUNCTION LOCAL VARIABLE DECLARATIONS: var tBoil = 100; // Guess for boiling temperature of water at given pressure var wnew; // Humidity ratio calculated with wet bulb guess var w; // Humidity ratio entered and corrected as necessary var resultx; // resultx is the final Iteration result passed back to the calling routine var error; // Deviation of dependent variable in iteration var x1; // Independent variable in ITERATE var y1; // Dependent variable in ITERATE var wstar; // Humidity ratio as a function of Sat Press of Wet Bulb var psatstar; // Saturation pressure at wet bulb temperature var iter; // Iteration counter var icvg; // Iteration convergence flag var FlagError; // set when errors should be flagged // CHECK tdb IN RANGE. if (tdb <= -100) { console.warning("psyTwbFnTdbWPb - Dry bulb temperature was below -100C, assuming -100C"); tdb = -100; } if (tdb >= 200) { console.warning("psyTwbFnTdbWPb - Dry bulb temperature was above 200C, assuming 200C"); tdb = 200; } // CHECK w IN RANGE. if (w < 0) { w = 1e-5; } // Initial temperature guess at atmospheric pressure tBoil = psyTsatFnPb(pb); // Set initial guess of WetBulbTemp=Entering Dry Bulb Temperature twb = tdb; // Begin iteration loop for (iter = 1; iter <= itmax; iter++) { // Assigning a value to twb if (twb >= (tBoil - 0.09)) { twb = tBoil - 0.1; } // Determine the saturation pressure for wet bulb temperature psatstar = psyPsatFnTemp(twb); // Determine humidity ratio for given saturation pressure wstar = 0.62198 * psatstar / (pb - psatstar); // Calculate new humidity ratio and determine difference from known // humidity ratio which is wStar calculated earlier wnew = ((2501 - 2.381 * twb) * wstar - (tdb - twb)) / (2501 + 1.805 * tdb - 4.186 * twb); // Check error, if not satisfied, calculate new guess and iterate error = w - wnew; // Using Iterative Procedure to Calculate WetBulb //Iterate( resultx, convTol, twb, error, x1, y1, iter, icvg ); var small = 1e-9; var perturb = 0.1; // Check for convergence by comparing change in X if (iter != 1 && (Math.abs(twb - x1) < convTol || error === 0)) { resultx = twb; icvg = 1; } else { // Not converged icvg = 0; if (iter === 1) { // New guess is specified by Perturb if (Math.abs(twb) > small) { resultx = twb * (1 + perturb); } else { resultx = perturb; } } else { // New guess calculated from LINEAR FIT of most recent two points var dy = error - y1; if (Math.abs(dy) < small) dy = small; // new estimation resultx = (error * x1 - y1 * twb) / dy; } x1 = twb; y1 = error; } twb = resultx; // If converged, leave iteration loop. if (icvg === 1) break; // Error Trap for the Discontinuous nature of PsyPsatFnTemp function (Sat Press Curve) at ~0 Deg C. if ((psatstar > 611) && (psatstar < 611.25) && (Math.abs(error) <= 0.0001) && (iter > 4)) break; } // End of Iteration Loop // Wet bulb temperature has not converged after maximum specified // iterations. Print error message, set return error flag, and RETURN if (iter > itmax) { //ShowRecurringWarningErrorAtEnd( "WetBulb not converged after max iterations(PsyTwbFnTdbWPb)", iPsyErrIndex( iPsyTwbFnTdbWPb3 ) ); } // If (TempWetBulb)>(Dry Bulb Temp) , Setting (TempWetBulb)=(DryBulbTemp). if (twb > tdb) { twb = tdb; } return twb; } </script> </body> </html>