@q This program is made available for personal and academic use only. @> @q It may not be re-packaged, re-distributed, or sold without consent @> @q of the original author, Bret D. Whissel. @> \def\title{SKEW-T (Version 1.0)} \def\topofcontents{\hrule height0pt depth0pt\vfill \centerline{\titlefont Skew-T} \vskip10pt \centerline{(Version 1.0)} \vskip18pt \centerline{Bret D. Whissel} \vfill\vfill} \def\botofcontents{\vfill\vfill \centerline{Copyright \copyright\ 2011 by Bret D. Whissel} \eject } \secpagedepth=1 @** Skew-T. Create a blank ``skew~$T$--$\ln p$'' diagram. Written by Bret D.~Whissel, Tallahassee, Florida, November~2011. The ordinate coordinates are logarithmic units of pressure in millibars/hPa, with the surface pressure (maximum value) at the bottom. On the abcissa, the quantity is temperature, but skewed by the pressure level, so plotting a temperature point in the $x$-dimension also requires a pressure component. Using this coordinate system, isotherms will be parallel straight lines at $45^\circ$ angles. We'll also draw curved grid lines for dry adiabats, saturated (moist) adiabats, and mixing ratio lines. To draw these various grid lines, we need to calculate temperature and/or pressure quantities that define the curves for particular fixed values. Once we've generated sets of points which define the curves at sufficient resolution, we apply a curve-fitting algorithm to the points to retrieve a new set of control points which we can then pass to the PostScript {\tt curveto} operator. Not only is this a huge data-reduction tactic, but the resulting plot will be able to be printed at extremely high resolutions without revealing any underlying point-to-point straight line segments. This means that our blank chart can be pretty even at poster-size enlargements. @s Vector2 int @s Point2 int @s tm int @c @@; @@; @@; @
@; @ We need math and a few standard libs. In addition, we'll need some definitions from {\it Graphics Gems\/} which are used by the curve-fitting functions. @= #include #include #include #include #include #include "graphics_gems.h" @* Virtual grid definitions. We start by defining a grid on which we'll generate a plot. The number of pixels in each dimension here should be the same, creating a square grid of square pixels, i.e., $dy/dx = 1$. This is necessary in order to define a grid where the isotherms can be generated at exact $45^\circ$ angles. The grid is merely a virtual construct: we don't actually allocate space for it. And since the grid is just a mathematical convenience, it doesn't theoretically matter what its scale is: a mapping from~$0$ to~$1$ is as valid as a mapping from~$-999$ to $10,000$. In practice, however, it is useful to have a scale that includes a substantial component to the left of the decimal point, since we'll be spitting these numbers out to the PostScript engine. @d GRID_SCALE 1000.0 @= double xmax = GRID_SCALE, ymax = GRID_SCALE; @ Now we allocate some variables which we'll use to convert from pressure units to $y$ coordinates. @= double yscale, ybias; @ Calculate the scale and bias in the $y$ dimension. The pressure scale is logarithmic, so we must account for that in setting the scale. The pressure values are given in hPa. @= void calc_ydims(double minp, double maxp) { ybias = log(minp); yscale = ymax/(log(maxp) - log(minp)); } @ This function converts a pressure value (in hPA) to a $y$-value given the previously established values of |ybias| and |yscale|. With increasing pressure, the $y$-value decreases. Here we create a mapping where the minimum pressure will be transformed into the value |ymax|, and the highest pressure will be mapped to a $y$-value of~0.0. We will force a return value of $0.0$ if the calculated value is ``close enough'' (as defined by |eps|). @= double p2y(double p) { static double eps = 1.0e-5; double val = ymax - yscale*(log(p) - ybias); return fabs(val) <= eps ? 0.0 : val; } @ Now we allocate space for variables which we'll use to convert from temperature and pressure units to $x$ coordinates. @= double xscale, xoffset, xbias; @ Calculate the scale, offset, and bias in the $x$ dimension. The $T_{\rm min}$ and $T_{\rm max}$ temperatures are passed in as $\rm{}^\circ C$ and used to determine $x$ scaling parameters. The $x$ scale is linear with respect to temperature, but since it is skewed, the $T_{\rm min}$ and $T_{\rm max}$ values define the $x$ range only when $y=0$. We will offset the $x$ value so that the center of the temperature scale will be transformed to an $x$ value of~0.0. @= void calc_xdims(double mint, double maxt) { xoffset = -xmax/2.0; xbias = mint; xscale = xmax/(maxt - mint); } @ This function converts a temperature in $\rm{}^\circ C$ and pressure in hPa into an $x$ value. Position along the $x$-axis is dependent on {\it both\/} temperature and pressure. Since the ``pixels'' are square, we calculate the pressure-related $x$-offset by calculating what the corresponding $y$-offset for the pressure should be, and then we add that value to the temperature term. An input value of |mint| with a pressure level of |maxp| (where $y=0$) should produce an $x$ value of |-xmax/2|. Likewise, an input value of |maxt| and |maxp| should be transformed to an $x$ value of |xmax/2|. @= double Tp2x(double T, double p) { return xscale*(T - xbias) + p2y(p) + xoffset; } @ It will be convenient to provide a reverse mapping from $(x,y)$ coordinates to $(T,p)$ coordinates, so we provide that conversion here. The calculations are just inversions of the |p2y()| and |Tp2x()| functions. The calculation of $p\Rightarrow y$ and $y\Rightarrow p$ are particularly prone to loss-of-precision errors, i.e., $\lnot\forall p: p - p\big(y(p)\big)=0$, so we must be careful about how the resulting values are used. @= void xy2Tp(double x, double y, double *T, double *p) { if (T) *T = (x - y - xoffset)/xscale + xbias; if (p) *p = exp(ybias - (y - ymax)/yscale); } @* Plot dimensions. We have now defined the transformations $(T,p)\Rightarrow(x,y)$. Eventually these coordinates need to be mapped to positions on a page, and that's what we'll tackle next. We'll define the plot's dimensions with |plotx| and |ploty|. If the ratio $y/x\neq1$, then the plot will be a window on the virtual grid, centered on $x=0$ and anchored at $y=0$. If |plotx>ploty|, then the whole temperature range from |mint| to |maxt| will be visible, but the pressure values at the top of the virtual grid will be out of the window. If |ploty>plotx|, then the whole pressure range from |maxp| up to |minp| will be visible, but temperatures will be windowed out at both extremes near |mint| and |maxt|. The amount of data cut out of the window depends on the ratio of |ploty/plotx|. All of the data on the virtual grid will be visible if $\hbox{\it plotx}=\hbox{\it ploty}$. Since PostScript is our target language to generate the plot, these dimensions should be given in PostScript units (72 points per inch). This defines the size of the plot on the page. The default plot will be portrait-oriented, filling a US~letter-sized page allowing for a quarter-inch border at each edge. @= double plotx = 8.0 * 72.0, ploty = 10.5 * 72.0; @ Now we define the page size, again in PostScript units. The width and height of the paper are always defined in portrait mode (longest edge along the $y$-axis). We're setting up default US~Letter ($8.5\times11\,\rm in$). @= float pgdim_x = 8.5 * 72.0, pgdim_y = 11.0 * 72.0; @ These variables give the offset of the plot on the page, again given in PostScript units. This defines where the lower left corner of the plot will start in relation to the page's lower-left corner (after landscape rotation, if requested). @= double transx = 0.25 * 72.0, transy = 0.25 * 72.0; @ Set |landscape=TRUE| for landscape orientation on the page. By default we start with portrait orientation. @= int landscape = FALSE; @ Set |colormode=TRUE| for color output. Otherwise, the plot will be created in black and white. @= int colormode = TRUE; @* Other configurable parameters. The spacing of the isotherm and dry adiabat gridlines is specified by |tinc|. Reasonable values are $1^\circ$ or $2^\circ$. Black and white mode is more readable using the |LORES_T| setting. @d HIRES_T 1.0; @d LORES_T 2.0; @= double tinc = HIRES_T; @ To do the transformation from data values to grid coordinates, we define the minimum and maximum temperatures and pressures. We give them their default values here. @= double mint = -25.0, maxt = 55.0, minp = 100.0, maxp = 1050.0; @* Meteorological constants. Let's start with a few simple conversions. |C2K(x)| converts Celsius degrees to Kelvin, and |K2C(x)| converts Kelvin to Celsius. @d C2K(x) ((x)+273.15) @d K2C(x) ((x)-273.15) @ We need a few constants in our calculations. $c_{pd}$ is the specific heat of dry air at constant pressure, and $c_{pv}$ is the specific heat of water vapor at constant pressure. $R_d$ is the Ideal Gas Law constant for dry air, while $R_v$ is the constant for moist air. We initialize the standard pressure |std_p=1000|. I tried to find values with the most precision for these constants using several sources (textbooks, journal articles, web). @= double cpd = 1005.7; /* $\rm J\,kg^{-1}\,K^{-1}$ */ double cpv = 1952.0; /* $\rm J\,kg^{-1}\,K^{-1}$ */ double Rd = 287.05307; /* $\rm J\,kg^{-1}\,K^{-1}$ */ double Rv = 461.5; /* $\rm J\,kg^{-1}\,K^{-1}$ */ double std_p = 1000.0; /* $\rm1000\,hPa$ */ @ There are a few values which we will pre-calculate from the constants we've already defined. However, we can't initialize global variables using operations on other variables in {\tt C}, so we'll allocate space for them here, and we'll have to initialize them later. @= double epsilon, Rd_cpd; @ The ratio of the dry air gas constant to the water vapor constant ($R_d / R_v$) is often called $\epsilon$ in meteorological equations. Another ratio that pops up is $R_d / c_{pd}$, and we pre-calculate that here to save operations later. @= epsilon = Rd / Rv; Rd_cpd = Rd / cpd; @** Curve Calculations. We'll generate five different kinds of curves: isobars, isotherms, dry adiabats, moist adiabats, and mixing ratios. The following functions will convert temperature and pressure pairs into $(x,y)$ coordinates. We won't bother with an isobar function since we really only need the |p2y()| function to get the corresponding $y$-coordinate. We'll start with the trivial isotherm function: @= void isotherm(double T, double p, double *x, double *y) { *x = Tp2x(T,p);@+ *y = p2y(p); } @ This function calculates dry adiabats. The potential temperature $\theta$ for a particular temperature $T$ may be calculated for a new pressure $p$ using Poisson's equation: $$ \theta = T\left({p_0 \over p}\right)^{R_d / c_{pd}}, $$ where $p_0$ is our starting point (standard pressure, 1000\thinspace hPa), $c_{pd}$ is the specific heat of dry air at constant pressure, and $R_d$ is the gas constant for dry air. If we wish to draw a curve with constant $\theta$, we can rearrange the equation to find $T$ as a function of pressure and $\theta$: $$ T = \theta \Big/ \left({p_0 \over p}\right)^{R_d / c_{pd}}, $$ with $\theta$ and $T$ being expressed in Kelvin. We'll design the function to accept and return the temperature in ${}^\circ\,\rm C$, and we'll do the conversion from Celsius to Kelvin (and back again) on the fly. @= void dry_adiabat(double theta, double p, double *x, double *y) { double T; T = C2K(theta) / pow(std_p / p, Rd_cpd); *x = Tp2x(K2C(T), p);@+ *y = p2y(p); } @ To draw mixing ratio lines, we use an expression for the saturation mixing ratio $w_s$ as follows: $$ w_s(T,p) = {\epsilon\,e_s(T) \over p - e_s(T)}, $$ where $\epsilon = R_d / R_v$. The symbol $e_s(T)$ is the saturation vapor pressure which is approximated empirically according to the August-Roche-Magnus formula. (See Lawrence~(2005) for historical background.) $$ e_s(T) \approx C\,\exp\left({A\,T_c \over T_c + B}\right), $$ The temperature $T_c$ is expressed in Celsius, not Kelvin, and the result is given in hPa (with suitable setting of constants). (The standard reference formula for $e_s(T)$ is the Goff--Gratch equation, but it is much harder to rearrange to solve for $T$, as we'll be doing next.) If we expand $w_s(T,p)$ using $e_s(T)$, we can then set $w_s(T,p) \equiv W$ (for some constant $W$) and rearrange to solve for temperature as follows: $$ \eqalignno{ W &= {\displaystyle \epsilon\,C\,\exp \left({\displaystyle A\,T_c \over\displaystyle T_c + B}\right) \over \displaystyle p - C\,\exp \left({\displaystyle A\,T_c \over\displaystyle T_c + B}\right)},\cr \noalign{\hbox{rearranging to solve for $T_c$:}} T_c &= { \displaystyle B\,\ln\left({W \cdot p \over C(W + \epsilon)}\right) \over \displaystyle A - \ln\left({W\cdot p \over C(W + \epsilon)}\right)}.\cr } $$ @ Here are the constant values for calculating the saturation vapor pressure using the August-Roche-Magnus formula. These constants are recommended by Alduchov and Eskridge~(1996). @= double Aes = 17.625; /* dimensionless */ double Bes = 243.04; /* $\rm{}^\circ C$ */ double Ces = 6.1094; /* $\rm hPa$ */ @ For the mixing ratio curves, we'll accept particular values of $W$ and $p$, and then we'll calculate the proper value of $T_c$ for those parameters. Since the mixing ratio is often expressed as $\rm g\,kg^{-1}$, we design the function to accept values using those units. However, the equations are designed for $\rm kg\,kg^{-1}$, so we must first make a conversion to the proper units. @= void mixing_ratio(double g_kg, double hpa, double *x, double *y) { double T, logval, kg_kg = g_kg / 1000.0; logval = log( (kg_kg * hpa) / (Ces*(kg_kg + epsilon)) ); T = (Bes * logval) / (Aes - logval); *x = Tp2x(T, hpa);@+ *y = p2y(hpa); } @ And now for the toughest part: drawing saturated adiabatic lines. The AMS {\it Glossary of Meteorology\/} provides this formula for the lapse rate of the pseudoadiabatic process (not reversible): $$ \Gamma_{\!ps}(T,p) = g{\displaystyle \big(1+w_s(T,p)\big)\left(1 + {\displaystyle L_v(T) w_s(T,p) \over\displaystyle R_d T}\right) \over\displaystyle c_{pd} + w_s(T,p) c_{pv} + {\displaystyle L_v(T)^2 w_s(T,p)\cdot(\epsilon + w_s(T,p)) \over\displaystyle R_d T^2} }. $$ $w_s(T,p)$ is the mixing ratio of water vapor, $c_{pd}$ is the specific heat of dry air at constant pressure, and $c_{pv}$ is the specific heat of water vapor at constant pressure. $L_v(T)$ is the latent heat of vaporization/condensation, $R_d$ is the dry air gas constant. $\epsilon$ is the ratio of the gas constants of dry air and water vapor. The lapse rate $\Gamma_{\!ps}$ provides the change in temperature with height, not pressure, so we'll make this adjustment next. The pressure lapse rate is given by $dT/dp = \Gamma_{\!ps}(T,p)/\rho g$, so we can integrate this expression from a starting temperature and pressure to get the new temperature at the new pressure using something like the following: $$ T(p_1) = T(p_0) + \int_{p_0}^{p_1} {\Gamma_{\!ps}(T,p) \over \rho g} dp. $$ This expression isn't completely accurate since $\Gamma_{\!ps}(T,p)$ is a function of both temperature and pressure, and the temperature will also be changing during the integration over pressure. The gravity acceleration constant $g$ cancels. To determine the density $\rho$, we can make use of the following definition from the Ideal Gas Law and Dalton's Law of Partial Pressures: $$ \rho = {p - e_s(T) \over R_d T} + {e_s(T) \over R_v T}. $$ @ With the following function definition, |Tc| is given in $\rm{}^\circ C$, and $p$ in hPa. Output units are $\rm{}^\circ K\,hPa^{-1}$. The calculations for $e_s(T)$ and $w_s(T,p)$ have already been discussed in reference to drawing the mixing ratio curves. @= @@; double dt_dp(double Tc, double p) { double es, ws, rho, Tk = C2K(Tc), L = latent_heat(Tc); double num, denom; es = Ces * exp((Aes * Tc) / (Tc + Bes)); ws = (epsilon * es) / (p - es); rho = (p - es)/(Rd*Tk) + es/(Rv*Tk); @# num = (1.0 + ws) * (1.0 + (L*ws/(Rd*Tk))); denom = cpd + ws*cpv + (L*L*ws * (epsilon + ws) / (Rd*Tk*Tk)); @# return num / (denom * rho); } @ The latent heat of vaporization/condensation varies slightly with temperature. The following is a cubic fit to the data in Table~2.1 in the textbook {\it A Short Course in Cloud Physics}, 3rd ed.~(1989), by R.~R.~Rogers and M.~K.~Yau. The temperature |Tc| is specified in $\rm{}^\circ C$. The returned units are $\rm J\,kg^{-1}$. @= double latent_heat(double Tc) { static double A = -6.14342e-5, B = 1.58927e-3, C = -2.36418, D = 2500.79; double Tc2 = Tc * Tc, Tc3 = Tc2 * Tc; return (A*Tc3 + B*Tc2 + C*Tc + D) * 1000.0; } @ So we know what the rate of change will be for any given position along the temperature/pressure continuum. To draw a moist adiabat, we start with a specified temperature and pressure, and then we integrate up to a new pressure to find what the new temperature should be. We can't use some of the fancier integration methods (i.e., Romberg or Gauss quadrature) since we need to integrate two variables. So we just pick a small step size and hope it will be sufficient for our purposes here. We also make double use of this function: we'll return value of the new temperature as well as calculate $(x,y)$ coordinates, if asked. @= double moist_adiabat(double initT, double p, double *x, double *y) { static double dp = 0.25; double newT, pi; newT = initT; if (p != std_p) { if (p > std_p) for (pi = std_p; pi <= p; pi += dp) newT += dp * dt_dp(newT, pi); else for (pi = std_p; pi >= p; pi -= dp) newT -= dp * dt_dp(newT, pi); } if (x) *x = Tp2x(newT, p); if (y) *y = p2y(p); return newT; } @* Curve fitting. We will convert some of the data points into fitted Bezi\'er curves. To do this, we need a place to store the data points to be fitted. @= Point2 points[200]; @ Draw a Bezi\'er curve using the PostScript {\tt curveto} operator. PostScript assumes that the first point of the curve is already part of the current path. The {\tt curveto} operator pops two $(x,y)$ control points and the terminating point from the stack, rendering the curve from the current point to the final point. Should the function be called several times, a new curve is considered a continuation of a former curve if the final point of the old curve is the same as the starting point of the new curve. To finish off a curve, call the function once setting |n=-1|: this emits the PostScript {\tt stroke} operator and forces the start of a new curve at the next invocation of the function. |DrawBezierCurve()| is called from the function |FitCurve()| whenever that function has finished fitting a portion of the data points to a curve segment. |FitCurve()| is defined externally and needs to be linked into this program. We also call |DrawBezierCurve()| locally to finish off a curve as described in the previous paragraph. @d UNSET_FLAG -9999.0 @= void DrawBezierCurve(int n, Point2 *curve) { static double lastx = UNSET_FLAG, lasty = UNSET_FLAG; if (n < 0) { printf(" s\n"); lastx = lasty = UNSET_FLAG; return; } if (curve[0].x != lastx || curve[0].y != lasty) { if (lastx > UNSET_FLAG) printf(" s\n"); printf(" n %g %g m\n", curve[0].x, curve[0].y); } curve++; n--; while (n > 0) { printf(" %g %g %g %g %g %g c\n",@| curve[0].x, curve[0].y, curve[1].x, curve[1].y, curve[2].x, curve[2].y); n -= 3; curve += 3; } lastx = curve[-1].x; lasty = curve[-1].y; } @** Finding Limits. In order to keep the PostScript output as small as possible, we'll try to limit the curves to those that are actually visible within the windowed area. This isn't easily determined for some of the curves, but we'll give it a good shot. To start, we'll calculate the $(x,y)$ coordinates of the visible boundaries. @= double minvisx, maxvisx, minvisy, maxvisy; @ @= if (ploty > plotx) { maxvisx = (xmax * plotx)/(2.0 * ploty); minvisx = -maxvisx; minvisy = 0.0; maxvisy = ymax; }@+ else { /* |plotx>=ploty| */ maxvisx = xmax/2.0; minvisx = -maxvisx; minvisy = 0.0; maxvisy = (ymax * ploty)/plotx; } @ We can now use the minimum and maximum visible $x$ and $y$ values to calculate the maximum and minimum visible temperatures and pressures. There are four temperature values at each corner of the plot, but only two pressure values at top and bottom. @= double minTminp, minTmaxp, maxTminp, maxTmaxp, minvisp, maxvisp; @ @= xy2Tp(minvisx, maxvisy, &minTminp, &minvisp); xy2Tp(maxvisx, maxvisy, &maxTminp, NULL); xy2Tp(minvisx, minvisy, &minTmaxp, &maxvisp); xy2Tp(maxvisx, minvisy, &maxTmaxp, NULL); @ For each of the curves, we'll figure out the low and high index values to calculate. The isotherm indices are pretty easy to calculate. The step size for plotting isotherms and dry adiabats has already been established by setting |tinc|. We add an additional variable for calculating alternating isotherm color stripes. @= double isotherm_lo, isotherm_hi, stripe_width = 10.0; @ @= isotherm_lo = round_right(minTminp, tinc); isotherm_hi = round_left(maxTmaxp, tinc); @ The isobar indices are also easy to calculate. @= double isobar_lo, isobar_hi, isobar_inc = 50.0; @ Note that we're working with indices here, so ``low'' and ``high'' have to do with initialization and termination of the |for| loop. @= isobar_lo = round_left(minvisp, isobar_inc); isobar_hi = round_left(maxvisp, isobar_inc); @ We are able to calculate the dry adiabit limits directly. @= double dryadiabat_lo, dryadiabat_hi; @ @= dryadiabat_lo = C2K(minTmaxp) * pow((std_p / maxvisp), Rd_cpd); dryadiabat_lo = round_right(K2C(dryadiabat_lo), tinc); dryadiabat_hi = C2K(maxTminp) * pow((std_p / minvisp), Rd_cpd); dryadiabat_hi = round_left(K2C(dryadiabat_hi), tinc); @ Now we'll work on the moist adiabatic curve. @= double moistadiabat_lo, moistadiabat_hi, moistadiabat_inc = 1.0; @ Because the moist adiabatic curve is calculated numerically, it isn't possible to calculate the bounds directly, so we'll need to make some guesses. At lower temperatures, the shape of the moist adiabatic curve moves to the left quickly, so we can assume that a lower bound near |minTmaxp| will be close. In higher temperature ranges, the curve starts out moving right and then curves back to the left at higher elevations. Depending on where |maxt| is set, we may need to test the curve at both the maximum and minimum pressure levels to see if the curve is visible in either location, and we'll use the maximum of either of those two values. @= moistadiabat_lo = find_moist_limit(minTmaxp, maxvisp, minTmaxp-10.0, minTmaxp+5.0); moistadiabat_lo = round_right(moistadiabat_lo, moistadiabat_inc); moistadiabat_hi = find_moist_limit(maxTminp, minvisp, maxTmaxp-20.0, maxTmaxp + 15.0); moistadiabat_hi = round_left(MAX(maxTmaxp, moistadiabat_hi), moistadiabat_inc); @ Another root-finding function. @= double find_moist_limit(double targT, double p, double lo, double hi) { double static tol = 1.0e-2; /* we don't need high precision here */ double t, testT; int iter = 0, maxiter = 30; do { t = (lo + hi)/2.0; testT = moist_adiabat(t, p, NULL, NULL); #ifdef DEBUG fprintf(stderr, "i=%d lo=%g hi=%g t=%g val=%g\n", iter, lo, hi, t, testT); #endif if (testT < targT) lo = t; else hi = t; } while (fabs(testT-targT) > tol && (hi-lo) > tol && ++iter < maxiter); if (iter >= maxiter) fprintf(stderr, "find_moist_limit(): max iterations (%d) reached"@| "without convergence\n", maxiter); return t; } @ We have to do something special for indexing when it comes to mixing ratio lines. The spacing is somewhat logarithmic, and we want to choose the lines and spacing that look reasonably good at values that are somewhat ``nice''. We'll create an array that holds increments and boundary levels. The first element contains the initial value in its |limit|, and the last element should contain a |limit<0|, indicating that we stick with the current increment level until reset. @= double ws_limit; struct ws_idx { double inc, limit; } *wsp, wsidx[] = {{0.0, 0.1},@| {0.1, 0.3}, {0.2, 0.5}, {0.5, 2.0}, {1.0, 4.0}, {2.0, 12.0},@| {3.0, 15.0}, {5.0, 60.0}, {10.0, 100.0}, {25.0, 200.0}, {50.0, -1.0}, }; @ Set things up so that we can use the index array. We'll use this same setup when we draw the mixing ratio labels later. @= double ws_init(void) { wsp = &(wsidx[1]); return wsidx[0].limit; } @ This function calculates what the next value of |ws| should be given the current conditions. If |ws>=wsp->limit|, then we bump up to the next increment level; otherwise, we remain at the current increment level. If the |wsp->limit<0|, we've reached the end of the line, and we stay at the current level. @= double ws_inc(double ws) { if (wsp->limit > 0.0 && ws >= wsp->limit) wsp++; return ws + wsp->inc; } @ We can figure out where to stop plotting mixing ratio lines when the $x$ value at the maximum visible pressure exceeds the maximum visible $x$ value for a particular mixing ratio. @= { double lastws; lastws = ws = ws_init(); while (1) { mixing_ratio(ws, maxvisp, &x, &y); if (x < maxvisx) { lastws = ws; ws = ws_inc(ws); }@+ else break; } ws_limit = lastws; } @ We need functions that will round |v| up or down to the nearest multiple of some value |m|, so here they are. @= double round_left(double v, double m) { return floor(v / m)*m; } @ @= double round_right(double v, double m) { return ceil(v / m)*m; } @** Main Program. @
= int main(int argc, char *argv[]) { double x, y, T, p, ws; int i; @@; @@; @# @@; @# @@; @@; @@; @@; @@; @@; @# @@; @# FinalizeOutput(); } @ @= calc_ydims(minp, maxp); calc_xdims(mint, maxt); @@; @@; @@; @@; @@; @@; @@; @@; InitOutput(); @ For debugging purposes, show the calculated boundaries and indices. @= #if (TESTING || DEBUG) fprintf(stderr, "minvx=%g maxvx=%g minvy=%g maxvy=%g minvp=%g maxvp=%g\n", minvisx, maxvisx, minvisy, maxvisy, minvisp, maxvisp); fprintf(stderr, "minTmaxp=%g maxTmaxp=%g minTminp=%g maxTminp=%g\n", minTmaxp, maxTmaxp, minTminp, maxTminp); fprintf(stderr, "maxp-maxvisp=%g minp-minvisp=%g\n", maxp-maxvisp, minp-minvisp); fprintf(stderr, "isotlo=%g isothi=%g\n", isotherm_lo, isotherm_hi); fprintf(stderr, "isoblo=%g isobhi=%g\n", isobar_lo, isobar_hi); fprintf(stderr, "dalo=%g dahi=%g\n", dryadiabat_lo, dryadiabat_hi); fprintf(stderr, "malo=%g mahi=%g\n", moistadiabat_lo, moistadiabat_hi); fprintf(stderr, "ws limit=%g\n", ws_limit); #endif @* Primary curve plotting. Generate a striped background along isotherms with a width given by |stripe_width|. @= printf("%% Background:\ngs greenband\n"); for (T = round_left(isotherm_lo, stripe_width); T <= round_right(isotherm_hi, stripe_width); T += 2*stripe_width) { isotherm(T, maxp, &x, &y); printf(" n %g %g m ", x, y); isotherm(T, minp, &x, &y); printf("%g %g l ", x, y); isotherm(T+stripe_width, minp, &x, &y); printf("%g %g l ", x, y); isotherm(T+stripe_width, maxp, &x, &y); printf("%g %g l cp f\n", x, y); } printf("gr\n"); @ @= printf("%% Isobars:\ngs darkgray\n"); for (p = isobar_lo; p <= isobar_hi; p += isobar_inc) { if (fmod(p, 100.0) == 0.0) printf(" lmed "); else printf(" lthin "); printf("n %g %g m %g %g l s\n", minvisx, p2y(p), maxvisx, p2y(p)); } printf("gr\n"); @ @= printf("%% Isotherms:\ngs orange\n"); for (T = isotherm_lo; T <= isotherm_hi; T += tinc) { if (fmod(T, 10.0) == 0.0) printf(" lthick "); else if (fmod(T, 5.0) == 0.0) printf(" lmed "); else printf(" lthin "); isotherm(T, maxp, &x, &y); printf("n %g %g m ", x, y); isotherm(T, minp, &x, &y); printf("%g %g l s\n", x, y); } printf("gr\n"); @ @= printf("%% Dry Adiabats:\ngs orange\n"); for (T = dryadiabat_lo; T <= dryadiabat_hi; T += tinc) { for (p=maxp, i=0; p >= minp; p -= 5.0) { dry_adiabat(T, p, &x, &y); points[i].x = x;@+ points[i].y = y;@+ i++; if (x < minvisx) break; } #ifdef DEBUG fprintf(stderr, "last p=%g (x,y)=(%g,%g) p2y=%g\n", p, x, y, p2y(p)); #endif if (fmod(T, 10.0) == 0.0) printf(" lthick "); else if (fmod(T, 5.0) == 0.0) printf(" lmed "); else printf(" lthin "); FitCurve(points, i, 0.0125); DrawBezierCurve(-1, NULL); } printf("gr\n"); @ @= printf("%% Mixing Ratios:\ngs blue lmedth dashed\n"); ws = ws_init(); do { for (p=maxp, i=0; p >= minp; p -= 10.0) { mixing_ratio(ws, p, &x, &y); points[i].x = x;@+ points[i].y = y;@+ i++; if (x > maxvisx) break; } FitCurve(points, i, 0.0125); DrawBezierCurve(-1, NULL); ws = ws_inc(ws); } while (ws <= ws_limit); printf("gr\n"); @ @= printf("%% Moist Adiabats:\ngs green\n"); for (T = moistadiabat_lo; T <= moistadiabat_hi; T += moistadiabat_inc) { for (p = maxp, i=0; p >= minp; p -= 10.0) { moist_adiabat(T, p, &x, &y); points[i].x = x;@+ points[i].y = y;@+ i++; if (x < minvisx) break; } if (fmod(T, 10.0) == 0.0) printf(" lthick "); else if (fmod(T, 5.0) == 0.0) printf(" lmed "); else printf(" lthin "); FitCurve(points, i, 0.0125); DrawBezierCurve(-1, NULL); } printf("gr\n"); @* Plotting labels. @= { double axt, xl, xr, yl, yr; @@; @@; @@; @@; @@; } @ We label the isobars at the left edge of the plot. Nothing too fancy here. @= printf("black 10 /Helvetica-Bold fsf\n"); for (p = 1000.0; p >= 200.0; p -= 100.0) { y = p2y(p); printf("0 1 (%d) el 10 unsc add %g chws\n", (int)p, y); } if (ploty >= plotx) { y = p2y(100.0); printf("0 1 (100) el 10 unsc add %g 6 unsc sub chws\n", y); } @ To label the isotherms, we'll pick an axis along which we would like to draw the labels. If we draw an imaginary line from the upper left corner to the lower right corner of the plot, we'll have identified an axis that provides the largest number of visible intersections with the isotherms. To calculate the intersections, we need the formulae of the axis line and each isotherm line. Using the point/slope form, the equation of the axis line will be $y = m_a(x-A_x) + A_y$ where $m_a ={}$|-ploty/plotx|, and a convenient point $A_{x,y}$ is fixed at the lower right-hand corner where $A_y = 0$. We've already calculated the $(x,y)$ boundaries, so we can just set $A_{x,y}=(\hbox{\it maxvisx},0)$. The point/slope form of an isotherm is $y = (x-T_x) + T_y$, where the slope is~$1$ by definition, and $(T_x,T_y$) is some point along the isotherm for temperature $T$. Again it will be convenient for us to choose a point where $T_y=0$, which is the at the highest pressure reading in our plot. Now we can set these two equations equal and solve for $x$: $$ \eqalign{ x - T_x &= m_a(x - A_x),\cr x&= {T_x - m_a A_x \over 1 - m_a}.\cr } $$ We get the corresponding $y$ value by plugging the $x$ value back into either equation, so we'll use $y=x - T_x$ since it's slightly simpler. @= { double ma = -ploty/plotx, tx; printf("orange 14 /Helvetica-Bold fsf\n"); for (T = round_right(isotherm_lo, 10.0); T <= round_left(isotherm_hi, 10.0); T += 10.0) { tx = Tp2x(T, maxvisp); x = (tx - (ma * maxvisx)) / (1.0 - ma); y = x - tx; printf("1 1 (%d) %g %g chws\n", (int)T, x, y); } } @ Plotting dry adiabat labels gets to be a little trickier. Let's say that we want to place dry adiabat labels along an axis that follows a particular isotherm line. Wherever this imaginary axis crosses a dry adiabat, that's where the label will be centered. Recall the equation which produced the dry adiabatic lines: $$ \theta = T\left({p_0\over p}\right)^{R_d/c_{pd}}. $$ Assuming that we've selected an isotherm line for our axis at temperature $T$ and we know for which adiabat $\theta$ we'll be drawing a label, then we need to rearrange this equation to find the pressure $p$ so that we can convert these values into $(x,y)$ coordinates. Fortunately, it just takes a bit of algebra: $$ \eqalign{ \ln{\theta\over T} &= {R_d\over c_{pd}} \ln{p_0\over p}\cr {c_{pd}\over R_d} \ln {\theta\over T} &= \ln p_0 - \ln p\cr p &= \exp\left[\ln p_0 - \ln\left({\theta\over T}\right)^{c_{pd}/R_d}\right]\cr p &= p_0 \Big/ \left({\theta\over T}\right)^{c_{pd}/R_d}\cr } $$ Next, we need to determine the angle of rotation on the curve at the selected point. This isn't too hard: we just find two adjacent points on the curve to calculate our ``tangent.'' It will be close enough. PostScript has an appropriate arctangent function which converts $dy/dx$ into a rotation angle. We'll plot labels along two isotherm axes. @= printf("orange 11 /Helvetica-Bold fsf\n"); axt = -45.0; while (axt <= 5.0) { for (T = round_right(dryadiabat_lo, 10.0); T <= round_left(dryadiabat_hi, 10.0); T += 10.0) { p = std_p / pow(C2K(T)/C2K(axt), cpd/Rd); x = Tp2x(axt, p); y = p2y(p); dry_adiabat(T, p-4, &xl, &yl); dry_adiabat(T, p+4, &xr, &yr); printf("%g %g (%d) %g %g chws\n", yr-yl, xr-xl, (int)T, x, y); } axt += 50.0; } @ Once again, the moist adiabats provide the most challenging issues. We can't invert the function to solve it for pressure, so we just have to search for the right temperature and pressure values along a given label axis which intersects the curve. We'll also use the same trick for finding the curve tangent that we used for the dry adiabats. We start labeling along one isotherm axis, but if we run off the plot in the $y$ dimension before we've finished labeling, we try again on a new isotherm axis $10^\circ$ to the right. (This may happen if |ploty= printf("green 11 /Helvetica-Bold fsf\n"); axt = -25.0; for (T = round_right(moistadiabat_lo, 5.0); T <= round_left(moistadiabat_hi, 5.0); T += 5.0) { p = moist_find(T, axt); if (p <= minvisp) { axt += 10.0; p = moist_find(T, axt); } x = Tp2x(axt, p); y = p2y(p); moist_adiabat(T, p-4, &xl, &yl); moist_adiabat(T, p+4, &xr, &yr); printf("%g %g (%d) %g %g chws\n", yr-yl, xr-xl, (int)T, x, y); } @ Use a bracketing method to find a pressure $p$ along a moist adiabatic curve for an initial temperature $T$ which matches the target isotherm specified by $a$. We initialize the search setting |hi_p| and |lo_p| outside the bounds of the lowest and highest pressures in the plot. Should the search end at either extreme, it should theoretically place a label outside of the plot area. This method does not converge particularly quickly, but it's simple. @= double moist_find(double T, double a) { double static tol = 1.0e-3; double hi_p = 1200.0, lo_p = 25.0, p, t; int iter = 0, maxiter = 30; do { p = (hi_p + lo_p)/2.0; t = moist_adiabat(T, p, NULL, NULL); if (t < a) lo_p = p; else hi_p = p; } while (fabs(t-a) > tol && fabs(hi_p-lo_p) > tol && ++iter < maxiter); if (iter >= maxiter) fprintf(stderr, "find_moist(): max iterations (%d) reached without "@| "convergence\n", maxiter); return p; } @ Each label-plotting element seems to have its own challenges. To plot the mixing ratio labels, we'll need to use two different axes. If the mixing ratio curve terminates at with the top boundary of the plot, we'll use one labeling axis. We'll use a different labeling axis if the mixing ratio curve terminates at the right boundary of the plot instead. When |minvisp==100|, I found that plotting labels along the |p==104| pressure line to be about right. So we try to keep that spacing no matter what the current minimum visible pressure may be. To do that, we calculate the relative $y$-positions of the 100 and 104\thinspace mb levels, and then we subtract that from the current maximum visible $y$ value. We then transform that $y$ value back into a pressure. @= { double y1 = p2y(100.0), y2 = p2y(104.0); xy2Tp(0.0, maxvisy - (y1-y2), NULL, &p); if (colormode) printf("blue 10 /Helvetica fsf\n"); else printf("blue 10 /Helvetica-Bold fsf\n"); ws = ws_init(); do { mixing_ratio(ws, p, &x, &y); if (x >= maxvisx) { /* switch to right border */ x = maxvisx - 13.0; y = find_mr_y(ws, x); } mixing_ratio(ws, p+25, &xl, &yl); mixing_ratio(ws, p-25, &xr, &yr); printf("%g %g (%g) %g %g chws\n", yr-yl, xr-xl, ws, x, y); ws = ws_inc(ws); } while (ws <= ws_limit); } @ If the mixing ratio line intesects with the right border of the plot, we must find a new $y$ coordinate which matches a defined $x$ coordinate. @= double find_mr_y(double ws, double x) { double static tol = 1.0e-3; double hi_p = 1200.0, lo_p = 25.0, p, t, nx = 0.0, ny = 0.0; int iter = 0, maxiter = 50; do { p = (hi_p + lo_p)/2.0; mixing_ratio(ws, p, &nx, &ny); if (nx < x) hi_p = p; else lo_p = p; } while (fabs(nx-x) > tol && fabs(hi_p-lo_p) > tol && ++iter < maxiter); if (iter >= maxiter) fprintf(stderr, "find_mr_y(): max iterations (%d) reached without "@| "convergence\n", maxiter); return ny; } @* Command line parsing. Here we handle command-line options to change some default settings as required. Once we've set everything up, all the parsing magic happens in the |getopt()| function. @= while (1) { int opt_i = 0, c; @@; c = getopt_long(argc, argv, "bcdDlH:W:x:y:t:T:p:P:?", long_opts, &opt_i); if (c == -1) break; switch (c) { case 0: break; /* flags already set: no further action required */ case 'b': colormode = FALSE;@+ break; case 'c': colormode = TRUE;@+ break; case 'l': landscape = TRUE;@+ break; case 'd': tinc = LORES_T;@+ break; case 'D': tinc = HIRES_T;@+ break; case 'H': pgdim_y = atof(optarg);@+ break; case 'W': pgdim_x = atof(optarg);@+ break; case 'x': plotx = atof(optarg);@+ break; case 'y': ploty = atof(optarg);@+ break; case 't': mint = atof(optarg);@+ break; case 'T': maxt = atof(optarg);@+ break; case 'p': minp = atof(optarg);@+ break; case 'P': maxp = atof(optarg);@+ break; case '?': /* fall through */ default: showhelp(argv);@+ exit(0); } } @ @= static struct option long_opts[] = {@| {"help", no_argument, 0, '?'},@| {"color", no_argument, &colormode, TRUE},@| {"bw", no_argument, &colormode, FALSE},@| {"landscape", no_argument, &landscape, TRUE},@| {"portrait", no_argument, &landscape, FALSE},@| {"lowdensity", no_argument, 0, 'd'},@| {"highdensity", no_argument, 0, 'D'},@| {"paperheight", required_argument, 0, 'H'},@| {"paperwidth", required_argument, 0, 'W'},@| {"xsize", required_argument, 0, 'x'},@| {"ysize", required_argument, 0, 'y'},@| {"mint", required_argument, 0, 't'},@| {"maxt", required_argument, 0, 'T'},@| {"minp", required_argument, 0, 'p'},@| {"maxp", required_argument, 0, 'P'},@| {0, 0, 0, 0} }; @ Help message. @= void showhelp(char **argv) { fprintf(stderr,"\nUsage: %s [options] > output.ps\n\n", *argv); fprintf(stderr,@| " The following options are recognized (*=default):\n"@| " --help | -? : print this message\n"@| " --color | -c : generate color plot*\n"@| " --bw | -b : generate B&W plot\n"@| " --landscape | -l : create plot in landscape mode\n"@| " --portrait : create plot in portrait mode*\n"@| " --lowdensity | -d : low density temperature spacing\n"@| " --highdensity | -D : high density temperature spacing*\n"@| " --mint t | -t t : lowest temperature value at y=0\n"@| " --maxt t | -T t : highest temperature value at y=0\n"@| " --minp p | -p p : minimum pressure value (p > 0)\n"@| " --maxp p | -P p : maximum pressure value\n"@| " Specified in PostScript units (1 in = 72.0):\n"@| " --xsize x | -x x : plot X-size\n"@| " --ysize y | -y y : plot Y-size\n"@| " --paperheight h | -H h : page height\n"@| " --paperwidth w | -W w : page width\n"); } @** PostScript Header Material. Here we initialize the PostScript output. @= void InitOutput(void) { double outscale; char today[50]; @@; @@; @@; @@; @@; @@; } @ Create a date and time string from the current system time. (This is so we can set the {\tt\%\%CreationDate:} field in the PostScript header.) @= { time_t now; struct tm tm_now; time(&now); localtime_r(&now, &tm_now); strftime(today, sizeof(today), "%B %d, %Y %T", &tm_now); } @ So that we know what we're dealing with later, we calculate PostScript scaling factor, which translates dimensions from our data grid coordinates to the output plot on the page. Our data grid remains square, even if the output plot is not, so we have only one scale applied to both the $x$ and $y$ axes. @= if (ploty > plotx) outscale = ploty/ymax; else outscale = plotx/xmax; @ We'll do our best to adhere to Adobe's document structuring conventions. @= printf("%%!PS-Adobe-3.0 EPSF-3.0\n"); @@; printf("%%%%Pages: 1\n%%%%Creator: (skew-t.w by Bret Whissel)\n"@| "%%%%Title: (Blank Skew-T/Log p Chart)\n"@| "%%%%CreationDate: (%s)\n", today); printf("%%%%DocumentNeededResources: font Helvetica Helvetica-Bold\n"@| "%%%%DocumentMedia: Plain %d %d 0 () ()\n", (int)(pgdim_x + 0.5), (int)(pgdim_y +0.5)); printf("%%%%EndComments\n"@| "%%%%BeginDefaults\n"@| "%%%%PageResources: font Helvetica Helvetica-Bold\n"@| "%%%%EndDefaults\n"); @ The PostScript bounding box is given in terms of the PostScript's natural orientation, so if we are creating a page in landscape mode, we must exchange the $x$ and $y$ dimensions accordingly. @= if (landscape) printf("%%%%BoundingBox: %d %d %d %d\n%%%%Orientation: Landscape\n", (int)(pgdim_x - transy - ploty + 0.5), (int)(transx + 0.5), (int)(pgdim_x - ploty), (int)(transx + plotx + 0.5)); else printf("%%%%BoundingBox: %d %d %d %d\n%%%%Orientation: Portrait\n", (int)(transx+0.5), (int)(transy+0.5), (int)(transx+plotx+0.5), (int)(transy+ploty+0.5)); @ @= printf("%%%%BeginProlog\n"@| "50 dict begin\n"); @@; @@; @@; @@; printf("%%%%EndProlog\n"); @ We use several PostScript operators over and over, and to reduce output size, we create brief shortcuts here. @= printf("/bd{bind def}bind def /ed{exch def}bd\n"@| "/gs{gsave}bd /gr{grestore}bd\n"@| "/tr{translate}bd /sc{scale}bd /r{rotate}bd\n"@| "/n{newpath}bd /cp{closepath}bd\n"@| "/s{stroke}bd /f{fill}bd\n"@| "/m{moveto}bd /l{lineto}bd /c{curveto}bd\n"@| "/rm{rmoveto}bd /rl{rlineto}bd\n"@| "/slw{setlinewidth}bd\n"); @ @= if (colormode) printf("/orange{0.902 0.404 0.082 setrgbcolor}def\n"@| "/green{0.439 0.800 0.255 setrgbcolor}def\n"@| "/greenband{0.95 1.0 0.95 setrgbcolor}def\n"@| "/blue{0 0 0.5 setrgbcolor}def\n"); else printf("/orange{0.4 setgray}def\n"@| "/green{0.65 setgray}def\n"@| "/greenband{0.96 setgray}def\n"@| "/blue{0.3 setgray}def\n"); printf("/darkgray{0.25 setgray}def\n"@| "/black{0 setgray}def\n"@| "/white{1 setgray}def\n"); @ Here we specify line thickness in terms of PostScript units. However, when these settings are invoked, we've already established the plot's scaling, so we must undo the scaling first. The scaling factor is saved in |sf|. The lines are drawn a little thicker in black-and-white mode. @= printf("/sf{%0.6lf}def\n", outscale); printf("/unsc{sf div}def\n"@| "/dashed{[5 unsc 3 unsc]0 setdash}def\n"); if (colormode) printf("/lthicker{1.4 unsc slw}def "@| "/lthick{0.8 unsc slw}def\n"@| "/lmed{0.55 unsc slw}def "@| "/lmedth{0.35 unsc slw}def\n"@| "/lthin{0.15 unsc slw}def\n"); else printf("/lthicker{1.7 unsc slw}def "@| "/lthick{1.5 unsc slw}def\n"@| "/lmed{0.9 unsc slw}def "@| "/lmedth{0.75 unsc slw}def\n"@| "/lthin{0.6 unsc slw}def\n"); @ This segment is only to be Adobe DSC compliant. @= printf("%%%%BeginSetup\n"@| "%%%%IncludeResources: font Helvetica\n"@| "%%%%IncludeResources: font Helvetica-Bold\n"@| "%%%%EndSetup\n"); @ @= printf("%%%%Page: 1 1\n"@| "%%%%PageResources: font Helvetica Helvetica-Bold\n"@| "%%%%BeginPageSetup\n/pgsave save def\n"); @@; @@; @@; @@; printf("%%%%EndPageSetup\n"); @ If we are creating a landscape mode page, we must translate the origin to the right edge of the page and then rotate $90^\circ$. @= if (landscape) { printf("%g %g tr\n", pgdim_x, 0.0); printf("90 r\n"); } @ @= printf("gs %g %g tr\n", transx, transy); printf("n 0 0 m %g %g rl %g %g rl %g %g rl cp gs clip\n", plotx, 0.0, 0.0, ploty, -plotx, 0.0); @ We've already calculated the scaling factor. However, before we invoke scaling, we shift the $x$ axis so that $x=0$ is located in the middle of the plot. @= printf("%g 0 tr sf dup sc\n", plotx/2.0); @ Some of the labels need to know where the edges of the plot are located. We've already calculated these, so now we just need to pass them along to the PostScript engine. @= printf("/el{%g}def /er{%g}def /eb{%g}def /et{%g}def\n",@| minvisx, maxvisx, minvisy, maxvisy); @ Create a procedure that prints a character string centered horizontally and vertically at requested coordinates and rotation angle. An example invocation looks like the following: \smallskip \noindent\line{\hss\tt dy dx (label) x y chws\hss} \smallskip \noindent First, we move to the label's location, then we acquire the string's bounding box. Then we rotate to be parallel to the curve's tangent at that point. Next we stroke the string's character outlines in white to clear space around the label characters, and then we fill the character outline. @= printf("/chws{gs n m dup\n"@| " gs n 0 0 m false charpath flattenpath pathbbox gr\n"@| " 4 dict begin /ury ed /urx ed /lly ed /llx ed\n"@| " 3 1 roll atan r urx llx sub -2 div ury lly sub -2 div rm\n"@| " dup gs false charpath white ury lly sub 0.18 mul slw s gr\n"@| " true charpath fill end gr}bd\n"); @ This procedure is merely a shortcut for setting up a font. It is invoked this way: \smallskip \noindent\line{\hss\tt size /Fontname fsf\hss} \smallskip \noindent Since the font is drawn within the scaled domain, the fontsize is first unscaled before being applied. @= printf("/fsf{findfont exch unsc scalefont setfont}bd\n"); @ Now we wrap up the plot. @= void FinalizeOutput(void) { printf("gr 1.4 slw s gr\n"@| "pgsave restore\nshowpage\n"@| "%%%%PageTrailer\n%%%%Trailer\n"@| "end\n%%%%EOF\n"); } @** Index.