Psychrometric Programming Functions

()

Library of psychrometric functions to calculate thermodynamic properties of air for Python, C, C#, Fortran, JavaScript and VBA/Excel

Overview

Psychrometrics are the study of physical and thermodynamic properties of moist air. These properties include, for example, the air’s dew point temperature, its wet bulb temperature, relative humidity, humidity ratio, enthalpy.

The estimation of these properties is critical in several engineering and scientific applications such as heating, ventilation, and air conditioning (HVAC) and meteorology. Although formulae to calculate the psychrometric properties of air are widely available in the literature (@Stull2011@Wexler1983@Stoecker1982@Dilley1968@Humphreys1920), their implementation in computer programs or spreadsheets can be challenging and time consuming.

PsychroLib is a library of functions to enable calculating psychrometric properties of moist and dry air. The library is available for Python, C, C#, Fortran, JavaScript, Microsoft Excel Visual Basic for Applications (VBA). It works in both metric (SI) and imperial (IP) systems of units. The functions are based of formulae from the 2017 ASHRAE Handbook — Fundamentals, Chapter 1, SI and IP editions. Functions can be grouped into two categories:

  1. Functions for the calculation of dew point temperature, wet-bulb temperature, partial vapour pressure of water, humidity ratio or relative humidity, knowing any other of these and dry bulb temperature and atmospheric pressure.
  2. Functions for the calculation of other moist air properties. All these use the humidity ratio as input.

Python

   1
   2
   3
   4
   5
   6
   7
   8
   9
  10
  11
  12
  13
  14
  15
  16
  17
  18
  19
  20
  21
  22
  23
  24
  25
  26
  27
  28
  29
  30
  31
  32
  33
  34
  35
  36
  37
  38
  39
  40
  41
  42
  43
  44
  45
  46
  47
  48
  49
  50
  51
  52
  53
  54
  55
  56
  57
  58
  59
  60
  61
  62
  63
  64
  65
  66
  67
  68
  69
  70
  71
  72
  73
  74
  75
  76
  77
  78
  79
  80
  81
  82
  83
  84
  85
  86
  87
  88
  89
  90
  91
  92
  93
  94
  95
  96
  97
  98
  99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
 126
 127
 128
 129
 130
 131
 132
 133
 134
 135
 136
 137
 138
 139
 140
 141
 142
 143
 144
 145
 146
 147
 148
 149
 150
 151
 152
 153
 154
 155
 156
 157
 158
 159
 160
 161
 162
 163
 164
 165
 166
 167
 168
 169
 170
 171
 172
 173
 174
 175
 176
 177
 178
 179
 180
 181
 182
 183
 184
 185
 186
 187
 188
 189
 190
 191
 192
 193
 194
 195
 196
 197
 198
 199
 200
 201
 202
 203
 204
 205
 206
 207
 208
 209
 210
 211
 212
 213
 214
 215
 216
 217
 218
 219
 220
 221
 222
 223
 224
 225
 226
 227
 228
 229
 230
 231
 232
 233
 234
 235
 236
 237
 238
 239
 240
 241
 242
 243
 244
 245
 246
 247
 248
 249
 250
 251
 252
 253
 254
 255
 256
 257
 258
 259
 260
 261
 262
 263
 264
 265
 266
 267
 268
 269
 270
 271
 272
 273
 274
 275
 276
 277
 278
 279
 280
 281
 282
 283
 284
 285
 286
 287
 288
 289
 290
 291
 292
 293
 294
 295
 296
 297
 298
 299
 300
 301
 302
 303
 304
 305
 306
 307
 308
 309
 310
 311
 312
 313
 314
 315
 316
 317
 318
 319
 320
 321
 322
 323
 324
 325
 326
 327
 328
 329
 330
 331
 332
 333
 334
 335
 336
 337
 338
 339
 340
 341
 342
 343
 344
 345
 346
 347
 348
 349
 350
 351
 352
 353
 354
 355
 356
 357
 358
 359
 360
 361
 362
 363
 364
 365
 366
 367
 368
 369
 370
 371
 372
 373
 374
 375
 376
 377
 378
 379
 380
 381
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
# PsychroLib (version 2.3.0) (https://github.com/psychrometrics/psychrolib)
# Copyright (c) 2018 D. Thevenard and D. Meyer for the current library implementation
# Copyright (c) 2017 ASHRAE Handbook — Fundamentals for ASHRAE equations and coefficients
# Licensed under the MIT License.

""" psychrolib.py
Contains functions for calculating thermodynamic properties of gas-vapor mixtures
and standard atmosphere suitable for most engineering, physical and meteorological
applications.
Most of the functions are an implementation of the formulae found in the
2017 ASHRAE Handbook - Fundamentals, in both International System (SI),
and Imperial (IP) units. Please refer to the information included in
each function for their respective reference.
Example
    >>> import psychrolib
    >>> # Set the unit system, for example to SI (can be either psychrolib.SI or psychrolib.IP)
    >>> psychrolib.SetUnitSystem(psychrolib.SI)
    >>> # Calculate the dew point temperature for a dry bulb temperature of 25 C and a relative humidity of 80%
    >>> TDewPoint = psychrolib.GetTDewPointFromRelHum(25.0, 0.80)
    >>> print(TDewPoint)
    21.309397163661785
Copyright
    - For the current library implementation
        Copyright (c) 2018 D. Thevenard and D. Meyer.
    - For equations and coefficients published ASHRAE Handbook — Fundamentals, Chapter 1
        Copyright (c) 2017 ASHRAE Handbook — Fundamentals (https://www.ashrae.org)
License
    MIT (https://github.com/psychrometrics/psychrolib/LICENSE.txt)
Note from the Authors
    We have made every effort to ensure that the code is adequate, however, we make no
    representation with respect to its accuracy. Use at your own risk. Should you notice
    an error, or if you have a suggestion, please notify us through GitHub at
    https://github.com/psychrometrics/psychrolib/issues.
"""


import math
from enum import Enum, auto
from typing import Optional


#######################################################################################################
# Global constants
#######################################################################################################

ZERO_FAHRENHEIT_AS_RANKINE = 459.67
"""float: Zero degree Fahrenheit (°F) expressed as degree Rankine (°R)
    Units:
        °R
    Reference:
        ASHRAE Handbook - Fundamentals (2017) ch. 39
"""

ZERO_CELSIUS_AS_KELVIN = 273.15
"""float: Zero degree Celsius (°C) expressed as Kelvin (K)
    Units:
        K
    Reference:
        ASHRAE Handbook - Fundamentals (2017) ch. 39
"""

R_DA_IP = 53.350
"""float: Universal gas constant for dry air (IP version)
    Units:
        ft lb_Force lb_DryAir⁻¹ R⁻¹
    Reference:
        ASHRAE Handbook - Fundamentals (2017) ch. 1
"""

R_DA_SI = 287.042
"""float: Universal gas constant for dry air (SI version)
    Units:
        J kg_DryAir⁻¹ K⁻¹
    Reference:
        ASHRAE Handbook - Fundamentals (2017) ch. 1
"""

MAX_ITER_COUNT = 100
"""int: Maximum number of iterations before exiting while loops.
"""

MIN_HUM_RATIO = 1e-7
"""float: Minimum acceptable humidity ratio used/returned by any functions.
          Any value above 0 or below the MIN_HUM_RATIO will be reset to this value.
"""

FREEZING_POINT_WATER_IP = 32.0
"""float: Freezing point of water in Fahrenheit.
"""

FREEZING_POINT_WATER_SI = 0.0
"""float: Freezing point of water in Celsius.
"""

TRIPLE_POINT_WATER_IP = 32.018
"""float: Triple point of water in Fahrenheit.
"""

TRIPLE_POINT_WATER_SI = 0.01
"""float: Triple point of water in Celsius.
"""

#######################################################################################################
# Helper functions
#######################################################################################################

# Unit system to use.
class UnitSystem(Enum):
    """
    Private class not exposed used to set automatic enumeration values.
    """
    IP = auto()
    SI = auto()

IP = UnitSystem.IP
SI = UnitSystem.SI

PSYCHROLIB_UNITS = None

PSYCHROLIB_TOLERANCE = 1.0
# Tolerance of temperature calculations

def SetUnitSystem(Units: UnitSystem) -> None:
    """
    Set the system of units to use (SI or IP).
    Args:
        Units: string indicating the system of units chosen (SI or IP)
    Notes:
        This function *HAS TO BE CALLED* before the library can be used
    """
    global PSYCHROLIB_UNITS
    global PSYCHROLIB_TOLERANCE

    if not isinstance(Units, UnitSystem):
        raise ValueError("The system of units has to be either SI or IP.")

    PSYCHROLIB_UNITS = Units

    # Define tolerance on temperature calculations
    # The tolerance is the same in IP and SI
    if Units == IP:
        PSYCHROLIB_TOLERANCE = 0.001 * 9. / 5.
    else:
        PSYCHROLIB_TOLERANCE = 0.001

def GetUnitSystem() -> Optional[UnitSystem]:
    """
    Return system of units in use.
    """
    return PSYCHROLIB_UNITS

def isIP() -> bool:
    """
    Check whether the system in use is IP or SI.
    """
    if PSYCHROLIB_UNITS == IP:
        return True
    elif PSYCHROLIB_UNITS == SI:
        return False
    else:
        raise ValueError('The system of units has not been defined.')


#######################################################################################################
# Conversion between temperature units
#######################################################################################################

def GetTRankineFromTFahrenheit(TFahrenheit: float) -> float:
    """
    Utility function to convert temperature to degree Rankine (°R)
    given temperature in degree Fahrenheit (°F).
    Args:
        TRankine: Temperature in degree Fahrenheit (°F)
    Returns:
        Temperature in degree Rankine (°R)
    Reference:
        Reference: ASHRAE Handbook - Fundamentals (2017) ch. 1 section 3
    Notes:
        Exact conversion.
    """
    TRankine = TFahrenheit + ZERO_FAHRENHEIT_AS_RANKINE
    return TRankine

def GetTFahrenheitFromTRankine(TRankine: float) -> float:
    """
    Utility function to convert temperature to degree Fahrenheit (°F)
    given temperature in degree Rankine (°R).
    Args:
        TRankine: Temperature in degree Rankine (°R)
    Returns:
        Temperature in degree Fahrenheit (°F)
    Reference:
        Reference: ASHRAE Handbook - Fundamentals (2017) ch. 1 section 3
    Notes:
        Exact conversion.
    """
    return TRankine - ZERO_FAHRENHEIT_AS_RANKINE

def GetTKelvinFromTCelsius(TCelsius: float) -> float:
    """
    Utility function to convert temperature to Kelvin (K)
    given temperature in degree Celsius (°C).
    Args:
        TCelsius: Temperature in degree Celsius (°C)
    Returns:
        Temperature in Kelvin (K)
    Reference:
        Reference: ASHRAE Handbook - Fundamentals (2017) ch. 1 section 3
    Notes:
        Exact conversion.
    """
    TKelvin = TCelsius + ZERO_CELSIUS_AS_KELVIN
    return TKelvin

def GetTCelsiusFromTKelvin(TKelvin: float) -> float:
    """
    Utility function to convert temperature to degree Celsius (°C)
    given temperature in Kelvin (K).
    Args:
        TKelvin: Temperature in Kelvin (K)
    Returns:
        Temperature in degree Celsius (°C)
    Reference:
        Reference: ASHRAE Handbook - Fundamentals (2017) ch. 1 section 3
    Notes:
        Exact conversion.
    """
    return TKelvin - ZERO_CELSIUS_AS_KELVIN


#######################################################################################################
# Conversions between dew point, wet bulb, and relative humidity
#######################################################################################################

def GetTWetBulbFromTDewPoint(TDryBulb: float, TDewPoint: float, Pressure: float) -> float:
    """
    Return wet-bulb temperature given dry-bulb temperature, dew-point temperature, and pressure.
    Args:
        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
        TDewPoint : Dew-point temperature in °F [IP] or °C [SI]
        Pressure : Atmospheric pressure in Psi [IP] or Pa [SI]
    Returns:
        Wet-bulb temperature in °F [IP] or °C [SI]
    Reference:
        ASHRAE Handbook - Fundamentals (2017) ch. 1
    """
    if TDewPoint > TDryBulb:
        raise ValueError("Dew point temperature is above dry bulb temperature")

    HumRatio = GetHumRatioFromTDewPoint(TDewPoint, Pressure)
    TWetBulb = GetTWetBulbFromHumRatio(TDryBulb, HumRatio, Pressure)
    return TWetBulb

def GetTWetBulbFromRelHum(TDryBulb: float, RelHum: float, Pressure: float) -> float:
    """
    Return wet-bulb temperature given dry-bulb temperature, relative humidity, and pressure.
    Args:
        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
        RelHum : Relative humidity in range [0, 1]
        Pressure : Atmospheric pressure in Psi [IP] or Pa [SI]
    Returns:
        Wet-bulb temperature in °F [IP] or °C [SI]
    Reference:
        ASHRAE Handbook - Fundamentals (2017) ch. 1
    """
    if RelHum < 0 or RelHum > 1:
        raise ValueError("Relative humidity is outside range [0, 1]")

    HumRatio = GetHumRatioFromRelHum(TDryBulb, RelHum, Pressure)
    TWetBulb = GetTWetBulbFromHumRatio(TDryBulb, HumRatio, Pressure)
    return TWetBulb

def GetRelHumFromTDewPoint(TDryBulb: float, TDewPoint: float) -> float:
    """
    Return relative humidity given dry-bulb temperature and dew-point temperature.
    Args:
        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
        TDewPoint : Dew-point temperature in °F [IP] or °C [SI]
    Returns:
        Relative humidity in range [0, 1]
    Reference:
        ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 22
    """
    if TDewPoint > TDryBulb:
        raise ValueError("Dew point temperature is above dry bulb temperature")

    VapPres = GetSatVapPres(TDewPoint)
    SatVapPres = GetSatVapPres(TDryBulb)
    RelHum = VapPres / SatVapPres
    return RelHum

def GetRelHumFromTWetBulb(TDryBulb: float, TWetBulb: float, Pressure: float) -> float:
    """
    Return relative humidity given dry-bulb temperature, wet bulb temperature and pressure.
    Args:
        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
        TWetBulb : Wet-bulb temperature in °F [IP] or °C [SI]
        Pressure : Atmospheric pressure in Psi [IP] or Pa [SI]
    Returns:
        Relative humidity in range [0, 1]
    Reference:
        ASHRAE Handbook - Fundamentals (2017) ch. 1
    """
    if TWetBulb > TDryBulb:
        raise ValueError("Wet bulb temperature is above dry bulb temperature")

    HumRatio = GetHumRatioFromTWetBulb(TDryBulb, TWetBulb, Pressure)
    RelHum =  GetRelHumFromHumRatio(TDryBulb, HumRatio, Pressure)
    return RelHum

def GetTDewPointFromRelHum(TDryBulb: float, RelHum: float) -> float:
    """
    Return dew-point temperature given dry-bulb temperature and relative humidity.
    Args:
        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
        RelHum: Relative humidity in range [0, 1]
    Returns:
        Dew-point temperature in °F [IP] or °C [SI]
    Reference:
        ASHRAE Handbook - Fundamentals (2017) ch. 1
    """
    if RelHum < 0 or RelHum > 1:
        raise ValueError("Relative humidity is outside range [0, 1]")

    VapPres = GetVapPresFromRelHum(TDryBulb, RelHum)
    TDewPoint = GetTDewPointFromVapPres(TDryBulb, VapPres)
    return TDewPoint

def GetTDewPointFromTWetBulb(TDryBulb: float, TWetBulb: float, Pressure: float) -> float:
    """
    Return dew-point temperature given dry-bulb temperature, wet-bulb temperature, and pressure.
    Args:
        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
        TWetBulb : Wet-bulb temperature in °F [IP] or °C [SI]
        Pressure : Atmospheric pressure in Psi [IP] or Pa [SI]
    Returns:
        Dew-point temperature in °F [IP] or °C [SI]
    Reference:
        ASHRAE Handbook - Fundamentals (2017) ch. 1
    """
    if TWetBulb > TDryBulb:
        raise ValueError("Wet bulb temperature is above dry bulb temperature")

    HumRatio = GetHumRatioFromTWetBulb(TDryBulb, TWetBulb, Pressure)
    TDewPoint = GetTDewPointFromHumRatio(TDryBulb, HumRatio, Pressure)
    return TDewPoint


#######################################################################################################
# Conversions between dew point, or relative humidity and vapor pressure
#######################################################################################################

def GetVapPresFromRelHum(TDryBulb: float, RelHum: float) -> float:
    """
    Return partial pressure of water vapor as a function of relative humidity and temperature.
    Args:
        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
        RelHum : Relative humidity in range [0, 1]
    Returns:
        Partial pressure of water vapor in moist air in Psi [IP] or Pa [SI]
    Reference:
        ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 12, 22
    """
    if RelHum < 0 or RelHum > 1:
        raise ValueError("Relative humidity is outside range [0, 1]")

    VapPres = RelHum * GetSatVapPres(TDryBulb)
    return VapPres

def GetRelHumFromVapPres(TDryBulb: float, VapPres: float) -> float:
    """
    Return relative humidity given dry-bulb temperature and vapor pressure.
    Args:
        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
        VapPres: Partial pressure of water vapor in moist air in Psi [IP] or Pa [SI]
    Returns:
        Relative humidity in range [0, 1]
    Reference:
        ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 12, 22
    """
    if VapPres < 0:
        raise ValueError("Partial pressure of water vapor in moist air cannot be negative")

    RelHum = VapPres / GetSatVapPres(TDryBulb)
    return RelHum

def dLnPws_(TDryBulb: float) -> float:
    """
    Helper function returning the derivative of the natural log of the saturation vapor pressure 
    as a function of dry-bulb temperature.
    Args:
        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
    Returns:
        Derivative of natural log of vapor pressure of saturated air in Psi [IP] or Pa [SI]
    Reference:
        ASHRAE Handbook - Fundamentals (2017) ch. 1  eqn 5 & 6
    """
    if isIP():
        T = GetTRankineFromTFahrenheit(TDryBulb)
        if TDryBulb <= TRIPLE_POINT_WATER_IP:
            dLnPws = 1.0214165E+04 / math.pow(T, 2) - 5.3765794E-03 + 2 * 1.9202377E-07 * T \
                  + 3 * 3.5575832E-10 * math.pow(T, 2) - 4 * 9.0344688E-14 * math.pow(T, 3) + 4.1635019 / T
        else:
            dLnPws = 1.0440397E+04 / math.pow(T, 2) - 2.7022355E-02 + 2 * 1.2890360E-05 * T \
                  - 3 * 2.4780681E-09 * math.pow(T, 2) + 6.5459673 / T
    else:
        T = GetTKelvinFromTCelsius(TDryBulb)
        if TDryBulb <= TRIPLE_POINT_WATER_SI:
            dLnPws = 5.6745359E+03 / math.pow(T, 2) - 9.677843E-03 + 2 * 6.2215701E-07 * T \
                  + 3 * 2.0747825E-09 * math.pow(T, 2) - 4 * 9.484024E-13 * math.pow(T, 3) + 4.1635019 / T
        else:
            dLnPws = 5.8002206E+03 / math.pow(T, 2) - 4.8640239E-02 + 2 * 4.1764768E-05 * T \
                  - 3 * 1.4452093E-08 * math.pow(T, 2) + 6.5459673 / T

    return dLnPws

def GetTDewPointFromVapPres(TDryBulb: float, VapPres: float) -> float:
    """
    Return dew-point temperature given dry-bulb temperature and vapor pressure.
    Args:
        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
        VapPres: Partial pressure of water vapor in moist air in Psi [IP] or Pa [SI]
    Returns:
        Dew-point temperature in °F [IP] or °C [SI]
    Reference:
        ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn. 5 and 6
    Notes:
        The dew point temperature is solved by inverting the equation giving water vapor pressure
        at saturation from temperature rather than using the regressions provided
        by ASHRAE (eqn. 37 and 38) which are much less accurate and have a
        narrower range of validity.
        The Newton-Raphson (NR) method is used on the logarithm of water vapour
        pressure as a function of temperature, which is a very smooth function
        Convergence is usually achieved in 3 to 5 iterations. 
        TDryBulb is not really needed here, just used for convenience.
    """
    if isIP():
        BOUNDS = [-148, 392]
    else:
        BOUNDS = [-100, 200]

    # Validity check -- bounds outside which a solution cannot be found
    if VapPres < GetSatVapPres(BOUNDS[0]) or VapPres > GetSatVapPres(BOUNDS[1]):
        raise ValueError("Partial pressure of water vapor is outside range of validity of equations")

    # We use NR to approximate the solution.
    # First guess
    TDewPoint = TDryBulb        # Calculated value of dew point temperatures, solved for iteratively
    lnVP = math.log(VapPres)    # Partial pressure of water vapor in moist air

    index = 1

    while True:
        TDewPoint_iter = TDewPoint   # TDewPoint used in NR calculation
        lnVP_iter = math.log(GetSatVapPres(TDewPoint_iter))

        # Derivative of function, calculated analytically
        d_lnVP = dLnPws_(TDewPoint_iter)

        # New estimate, bounded by the search domain defined above
        TDewPoint = TDewPoint_iter - (lnVP_iter - lnVP) / d_lnVP
        TDewPoint = max(TDewPoint, BOUNDS[0])
        TDewPoint = min(TDewPoint, BOUNDS[1])

        if ((math.fabs(TDewPoint - TDewPoint_iter) <= PSYCHROLIB_TOLERANCE)):
            break

        if (index > MAX_ITER_COUNT):
            raise ValueError("Convergence not reached in GetTDewPointFromVapPres. Stopping.")

        index = index + 1

    TDewPoint = min(TDewPoint, TDryBulb)
    return TDewPoint

def GetVapPresFromTDewPoint(TDewPoint: float) -> float:
    """
    Return vapor pressure given dew point temperature.
    Args:
        TDewPoint : Dew-point temperature in °F [IP] or °C [SI]
    Returns:
        Partial pressure of water vapor in moist air in Psi [IP] or Pa [SI]
    Reference:
        ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 36
    """
    VapPres = GetSatVapPres(TDewPoint)
    return VapPres


#######################################################################################################
# Conversions from wet-bulb temperature, dew-point temperature, or relative humidity to humidity ratio
#######################################################################################################

def GetTWetBulbFromHumRatio(TDryBulb: float, HumRatio: float, Pressure: float) -> float:
    """
    Return wet-bulb temperature given dry-bulb temperature, humidity ratio, and pressure.
    Args:
        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
        HumRatio : Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]
        Pressure : Atmospheric pressure in Psi [IP] or Pa [SI]
    Returns:
        Wet-bulb temperature in °F [IP] or °C [SI]
    Reference:
        ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 33 and 35 solved for Tstar
    """
    if HumRatio < 0:
        raise ValueError("Humidity ratio cannot be negative")
    BoundedHumRatio = max(HumRatio, MIN_HUM_RATIO)

    TDewPoint = GetTDewPointFromHumRatio(TDryBulb, BoundedHumRatio, Pressure)

    # Initial guesses
    TWetBulbSup = TDryBulb
    TWetBulbInf = TDewPoint
    TWetBulb = (TWetBulbInf + TWetBulbSup) / 2

    index = 1
    # Bisection loop
    while ((TWetBulbSup - TWetBulbInf) > PSYCHROLIB_TOLERANCE):

        # Compute humidity ratio at temperature Tstar
        Wstar = GetHumRatioFromTWetBulb(TDryBulb, TWetBulb, Pressure)

        # Get new bounds
        if Wstar > BoundedHumRatio:
            TWetBulbSup = TWetBulb
        else:
            TWetBulbInf = TWetBulb

        # New guess of wet bulb temperature
        TWetBulb = (TWetBulbSup + TWetBulbInf) / 2

        if (index >= MAX_ITER_COUNT):
            raise ValueError("Convergence not reached in GetTWetBulbFromHumRatio. Stopping.")

        index = index + 1
    return TWetBulb

def GetHumRatioFromTWetBulb(TDryBulb: float, TWetBulb: float, Pressure: float) -> float:
    """
    Return humidity ratio given dry-bulb temperature, wet-bulb temperature, and pressure.
    Args:
        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
        TWetBulb : Wet-bulb temperature in °F [IP] or °C [SI]
        Pressure : Atmospheric pressure in Psi [IP] or Pa [SI]
    Returns:
        Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]
    Reference:
        ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 33 and 35
    """
    if TWetBulb > TDryBulb:
        raise ValueError("Wet bulb temperature is above dry bulb temperature")

    Wsstar = GetSatHumRatio(TWetBulb, Pressure)

    if isIP():
       if TWetBulb >= FREEZING_POINT_WATER_IP:
           HumRatio = ((1093 - 0.556 * TWetBulb) * Wsstar - 0.240 * (TDryBulb - TWetBulb)) \
                    / (1093 + 0.444 * TDryBulb - TWetBulb)
       else:
           HumRatio = ((1220 - 0.04 * TWetBulb) * Wsstar - 0.240 * (TDryBulb - TWetBulb)) \
                    / (1220 + 0.444 * TDryBulb - 0.48*TWetBulb)
    else:
       if TWetBulb >= FREEZING_POINT_WATER_SI:
           HumRatio = ((2501. - 2.326 * TWetBulb) * Wsstar - 1.006 * (TDryBulb - TWetBulb)) \
                    / (2501. + 1.86 * TDryBulb - 4.186 * TWetBulb)
       else:
           HumRatio = ((2830. - 0.24 * TWetBulb) * Wsstar - 1.006 * (TDryBulb - TWetBulb)) \
                    / (2830. + 1.86 * TDryBulb - 2.1 * TWetBulb)
    # Validity check.
    return max(HumRatio, MIN_HUM_RATIO)

def GetHumRatioFromRelHum(TDryBulb: float, RelHum: float, Pressure: float) -> float:
    """
    Return humidity ratio given dry-bulb temperature, relative humidity, and pressure.
    Args:
        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
        RelHum : Relative humidity in range [0, 1]
        Pressure : Atmospheric pressure in Psi [IP] or Pa [SI]
    Returns:
        Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]
    Reference:
        ASHRAE Handbook - Fundamentals (2017) ch. 1
    """
    if RelHum < 0 or RelHum > 1:
        raise ValueError("Relative humidity is outside range [0, 1]")

    VapPres = GetVapPresFromRelHum(TDryBulb, RelHum)
    HumRatio = GetHumRatioFromVapPres(VapPres, Pressure)
    return HumRatio

def GetRelHumFromHumRatio(TDryBulb: float, HumRatio: float, Pressure: float) -> float:
    """
    Return relative humidity given dry-bulb temperature, humidity ratio, and pressure.
    Args:
        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
        HumRatio : Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]
        Pressure : Atmospheric pressure in Psi [IP] or Pa [SI]
    Returns:
        Relative humidity in range [0, 1]
    Reference:
        ASHRAE Handbook - Fundamentals (2017) ch. 1
    """
    if HumRatio < 0:
        raise ValueError("Humidity ratio cannot be negative")

    VapPres = GetVapPresFromHumRatio(HumRatio, Pressure)
    RelHum = GetRelHumFromVapPres(TDryBulb, VapPres)
    return RelHum

def GetHumRatioFromTDewPoint(TDewPoint: float, Pressure: float) -> float:
    """
    Return humidity ratio given dew-point temperature and pressure.
    Args:
        TDewPoint : Dew-point temperature in °F [IP] or °C [SI]
        Pressure : Atmospheric pressure in Psi [IP] or Pa [SI]
    Returns:
        Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]
    Reference:
        ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 13
    """
    VapPres = GetSatVapPres(TDewPoint)
    HumRatio = GetHumRatioFromVapPres(VapPres, Pressure)
    return HumRatio

def GetTDewPointFromHumRatio(TDryBulb: float, HumRatio: float, Pressure: float) -> float:
    """
    Return dew-point temperature given dry-bulb temperature, humidity ratio, and pressure.
    Args:
        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
        HumRatio : Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]
        Pressure : Atmospheric pressure in Psi [IP] or Pa [SI]
    Returns:
        Dew-point temperature in °F [IP] or °C [SI]
    Reference:
        ASHRAE Handbook - Fundamentals (2017) ch. 1
    """
    if HumRatio < 0:
        raise ValueError("Humidity ratio cannot be negative")

    VapPres = GetVapPresFromHumRatio(HumRatio, Pressure)
    TDewPoint = GetTDewPointFromVapPres(TDryBulb, VapPres)
    return TDewPoint


#######################################################################################################
# Conversions between humidity ratio and vapor pressure
#######################################################################################################

def GetHumRatioFromVapPres(VapPres: float, Pressure: float) -> float:
    """
    Return humidity ratio given water vapor pressure and atmospheric pressure.
    Args:
        VapPres : Partial pressure of water vapor in moist air in Psi [IP] or Pa [SI]
        Pressure : Atmospheric pressure in Psi [IP] or Pa [SI]
    Returns:
        Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]
    Reference:
        ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 20
    """
    if VapPres < 0:
        raise ValueError("Partial pressure of water vapor in moist air cannot be negative")

    HumRatio = 0.621945 * VapPres / (Pressure - VapPres)

    # Validity check.
    return max(HumRatio, MIN_HUM_RATIO)

def GetVapPresFromHumRatio(HumRatio: float, Pressure: float) -> float:
    """
    Return vapor pressure given humidity ratio and pressure.
    Args:
        HumRatio : Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]
        Pressure : Atmospheric pressure in Psi [IP] or Pa [SI]
    Returns:
        Partial pressure of water vapor in moist air in Psi [IP] or Pa [SI]
    Reference:
        ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 20 solved for pw
    """
    if HumRatio < 0:
        raise ValueError("Humidity ratio is negative")
    BoundedHumRatio = max(HumRatio, MIN_HUM_RATIO)

    VapPres = Pressure * BoundedHumRatio / (0.621945 + BoundedHumRatio)
    return VapPres


#######################################################################################################
# Conversions between humidity ratio and specific humidity
#######################################################################################################

def GetSpecificHumFromHumRatio(HumRatio: float) -> float:
    """
    Return the specific humidity from humidity ratio (aka mixing ratio).
    Args:
        HumRatio : Humidity ratio in lb_H₂O lb_Dry_Air⁻¹ [IP] or kg_H₂O kg_Dry_Air⁻¹ [SI]
    Returns:
        Specific humidity in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]
    Reference:
        ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 9b
    """
    if HumRatio < 0:
        raise ValueError("Humidity ratio cannot be negative")
    BoundedHumRatio = max(HumRatio, MIN_HUM_RATIO)

    SpecificHum = BoundedHumRatio / (1.0 + BoundedHumRatio)
    return SpecificHum

def GetHumRatioFromSpecificHum(SpecificHum: float) -> float:
    """
    Return the humidity ratio (aka mixing ratio) from specific humidity.
    Args:
        SpecificHum : Specific humidity in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]
    Returns:
        Humidity ratio in lb_H₂O lb_Dry_Air⁻¹ [IP] or kg_H₂O kg_Dry_Air⁻¹ [SI]
    Reference:
        ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 9b (solved for humidity ratio)
    """
    if SpecificHum < 0.0 or SpecificHum >= 1.0:
        raise ValueError("Specific humidity is outside range [0, 1[")

    HumRatio = SpecificHum / (1.0 - SpecificHum)

    # Validity check.
    return max(HumRatio, MIN_HUM_RATIO)


#######################################################################################################
# Dry Air Calculations
#######################################################################################################

def GetDryAirEnthalpy(TDryBulb: float) -> float:
    """
    Return dry-air enthalpy given dry-bulb temperature.
    Args:
        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
    Returns:
        Dry air enthalpy in Btu lb⁻¹ [IP] or J kg⁻¹ [SI]
    Reference:
        ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 28
    """
    if isIP():
        DryAirEnthalpy = 0.240 * TDryBulb
    else:
        DryAirEnthalpy = 1006 * TDryBulb
    return DryAirEnthalpy

def GetDryAirDensity(TDryBulb: float, Pressure: float) -> float:
    """
    Return dry-air density given dry-bulb temperature and pressure.
    Args:
        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
        Pressure : Atmospheric pressure in Psi [IP] or Pa [SI]
    Returns:
        Dry air density in lb ft⁻³ [IP] or kg m⁻³ [SI]
    Reference:
        ASHRAE Handbook - Fundamentals (2017) ch. 1
    Notes:
        Eqn 14 for the perfect gas relationship for dry air.
        Eqn 1 for the universal gas constant.
        The factor 144 in IP is for the conversion of Psi = lb in⁻² to lb ft⁻².
    """
    if isIP():
        DryAirDensity = (144 * Pressure) / R_DA_IP / GetTRankineFromTFahrenheit(TDryBulb)
    else:
        DryAirDensity = Pressure / R_DA_SI / GetTKelvinFromTCelsius(TDryBulb)
    return DryAirDensity

def GetDryAirVolume(TDryBulb: float, Pressure: float) -> float:
    """
    Return dry-air volume given dry-bulb temperature and pressure.
    Args:
        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
        Pressure : Atmospheric pressure in Psi [IP] or Pa [SI]
    Returns:
        Dry air volume in ft³ lb⁻¹ [IP] or in m³ kg⁻¹ [SI]
    Reference:
        ASHRAE Handbook - Fundamentals (2017) ch. 1
    Notes:
        Eqn 14 for the perfect gas relationship for dry air.
        Eqn 1 for the universal gas constant.
        The factor 144 in IP is for the conversion of Psi = lb in⁻² to lb ft⁻².
    """
    if isIP():
        DryAirVolume = R_DA_IP * GetTRankineFromTFahrenheit(TDryBulb) / (144 * Pressure)
    else:
        DryAirVolume = R_DA_SI * GetTKelvinFromTCelsius(TDryBulb) / Pressure
    return DryAirVolume


def GetTDryBulbFromEnthalpyAndHumRatio(MoistAirEnthalpy: float, HumRatio: float) -> float:
    """
    Return dry bulb temperature from enthalpy and humidity ratio.
    Args:
        MoistAirEnthalpy : Moist air enthalpy in Btu lb⁻¹ [IP] or J kg⁻¹
        HumRatio : Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]
    Returns:
        Dry-bulb temperature in °F [IP] or °C [SI]
    Reference:
        ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 30
    Notes:
        Based on the `GetMoistAirEnthalpy` function, rearranged for temperature.
    """
    if HumRatio < 0:
        raise ValueError("Humidity ratio is negative")
    BoundedHumRatio = max(HumRatio, MIN_HUM_RATIO)

    if isIP():
        TDryBulb  = (MoistAirEnthalpy - 1061.0 * BoundedHumRatio) / (0.240 + 0.444 * BoundedHumRatio)
    else:
        TDryBulb  = (MoistAirEnthalpy / 1000.0 - 2501.0 * BoundedHumRatio) / (1.006 + 1.86 * BoundedHumRatio)
    return TDryBulb

def GetHumRatioFromEnthalpyAndTDryBulb(MoistAirEnthalpy: float, TDryBulb: float) -> float:
    """
    Return humidity ratio from enthalpy and dry-bulb temperature.
    Args:
        MoistAirEnthalpy : Moist air enthalpy in Btu lb⁻¹ [IP] or J kg⁻¹
        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
    Returns:
        Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]
    Reference:
        ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 30.
    Notes:
        Based on the `GetMoistAirEnthalpy` function, rearranged for humidity ratio.
    """
    if isIP():
        HumRatio  = (MoistAirEnthalpy - 0.240 * TDryBulb) / (1061.0 + 0.444 * TDryBulb)
    else:
        HumRatio  = (MoistAirEnthalpy / 1000.0 - 1.006 * TDryBulb) / (2501.0 + 1.86 * TDryBulb)

    # Validity check.
    return max(HumRatio, MIN_HUM_RATIO)


#######################################################################################################
# Saturated Air Calculations
#######################################################################################################

def GetSatVapPres(TDryBulb: float) -> float:
    """
    Return saturation vapor pressure given dry-bulb temperature.
    Args:
        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
    Returns:
        Vapor pressure of saturated air in Psi [IP] or Pa [SI]
    Reference:
        ASHRAE Handbook - Fundamentals (2017) ch. 1  eqn 5 & 6
        Important note: the ASHRAE formulae are defined above and below the freezing point but have
        a discontinuity at the freezing point. This is a small inaccuracy on ASHRAE's part: the formulae
        should be defined above and below the triple point of water (not the feezing point) in which case
        the discontinuity vanishes. It is essential to use the triple point of water otherwise function
        GetTDewPointFromVapPres, which inverts the present function, does not converge properly around
        the freezing point.
    """
    if isIP():
        if (TDryBulb < -148 or TDryBulb > 392):
            raise ValueError("Dry bulb temperature must be in range [-148, 392]°F")

        T = GetTRankineFromTFahrenheit(TDryBulb)

        if (TDryBulb <= TRIPLE_POINT_WATER_IP):
            LnPws = (-1.0214165E+04 / T - 4.8932428 - 5.3765794E-03 * T + 1.9202377E-07 * T**2 \
                  + 3.5575832E-10 * math.pow(T, 3) - 9.0344688E-14 * math.pow(T, 4) + 4.1635019 * math.log(T))
        else:
            LnPws = -1.0440397E+04 / T - 1.1294650E+01 - 2.7022355E-02* T + 1.2890360E-05 * T**2 \
                  - 2.4780681E-09 * math.pow(T, 3) + 6.5459673 * math.log(T)
    else:
        if (TDryBulb < -100 or TDryBulb > 200):
            raise ValueError("Dry bulb temperature must be in range [-100, 200]°C")

        T = GetTKelvinFromTCelsius(TDryBulb)

        if (TDryBulb <= TRIPLE_POINT_WATER_SI):
            LnPws = -5.6745359E+03 / T + 6.3925247 - 9.677843E-03 * T + 6.2215701E-07 * T**2 \
                  + 2.0747825E-09 * math.pow(T, 3) - 9.484024E-13 * math.pow(T, 4) + 4.1635019 * math.log(T)
        else:
            LnPws = -5.8002206E+03 / T + 1.3914993 - 4.8640239E-02 * T + 4.1764768E-05 * T**2 \
                  - 1.4452093E-08 * math.pow(T, 3) + 6.5459673 * math.log(T)

    SatVapPres = math.exp(LnPws)
    return SatVapPres

def GetSatHumRatio(TDryBulb: float, Pressure: float) -> float:
    """
    Return humidity ratio of saturated air given dry-bulb temperature and pressure.
    Args:
        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
        Pressure : Atmospheric pressure in Psi [IP] or Pa [SI]
    Returns:
        Humidity ratio of saturated air in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]
    Reference:
        ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 36, solved for W
    """
    SatVaporPres = GetSatVapPres(TDryBulb)
    SatHumRatio = 0.621945 * SatVaporPres / (Pressure - SatVaporPres)

    # Validity check.
    return max(SatHumRatio, MIN_HUM_RATIO)

def GetSatAirEnthalpy(TDryBulb: float, Pressure: float) -> float:
    """
    Return saturated air enthalpy given dry-bulb temperature and pressure.
    Args:
        TDryBulb: Dry-bulb temperature in °F [IP] or °C [SI]
        Pressure: Atmospheric pressure in Psi [IP] or Pa [SI]
    Returns:
        Saturated air enthalpy in Btu lb⁻¹ [IP] or J kg⁻¹ [SI]
    Reference:
        ASHRAE Handbook - Fundamentals (2017) ch. 1
    """
    SatHumRatio = GetSatHumRatio(TDryBulb, Pressure)
    SatAirEnthalpy = GetMoistAirEnthalpy(TDryBulb, SatHumRatio)
    return SatAirEnthalpy


#######################################################################################################
# Moist Air Calculations
#######################################################################################################

def GetVaporPressureDeficit(TDryBulb: float, HumRatio: float, Pressure: float) -> float:
    """
    Return Vapor pressure deficit given dry-bulb temperature, humidity ratio, and pressure.
    Args:
        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
        HumRatio : Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]
        Pressure : Atmospheric pressure in Psi [IP] or Pa [SI]
    Returns:
        Vapor pressure deficit in Psi [IP] or Pa [SI]
    Reference:
        Oke (1987) eqn 2.13a
    """
    if HumRatio < 0:
        raise ValueError("Humidity ratio is negative")

    RelHum = GetRelHumFromHumRatio(TDryBulb, HumRatio, Pressure)
    VaporPressureDeficit = GetSatVapPres(TDryBulb) * (1 - RelHum)
    return VaporPressureDeficit

def GetDegreeOfSaturation(TDryBulb: float, HumRatio: float, Pressure: float) -> float:
    """
    Return the degree of saturation (i.e humidity ratio of the air / humidity ratio of the air at saturation
    at the same temperature and pressure) given dry-bulb temperature, humidity ratio, and atmospheric pressure.
    Args:
        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
        HumRatio : Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]
        Pressure : Atmospheric pressure in Psi [IP] or Pa [SI]
    Returns:
        Degree of saturation in arbitrary unit
    Reference:
        ASHRAE Handbook - Fundamentals (2009) ch. 1 eqn 12
    Notes:
        This definition is absent from the 2017 Handbook. Using 2009 version instead.
    """
    if HumRatio < 0:
        raise ValueError("Humidity ratio is negative")
    BoundedHumRatio = max(HumRatio, MIN_HUM_RATIO)

    SatHumRatio = GetSatHumRatio(TDryBulb, Pressure)
    DegreeOfSaturation = BoundedHumRatio / SatHumRatio
    return DegreeOfSaturation

def GetMoistAirEnthalpy(TDryBulb: float, HumRatio: float) -> float:
    """
    Return moist air enthalpy given dry-bulb temperature and humidity ratio.
    Args:
        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
        HumRatio : Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]
    Returns:
        Moist air enthalpy in Btu lb⁻¹ [IP] or J kg⁻¹
    Reference:
        ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 30
    """
    if HumRatio < 0:
        raise ValueError("Humidity ratio is negative")
    BoundedHumRatio = max(HumRatio, MIN_HUM_RATIO)

    if isIP():
        MoistAirEnthalpy = 0.240 * TDryBulb + BoundedHumRatio * (1061 + 0.444 * TDryBulb)
    else:
        MoistAirEnthalpy = (1.006 * TDryBulb + BoundedHumRatio * (2501. + 1.86 * TDryBulb)) * 1000
    return MoistAirEnthalpy

def GetMoistAirVolume(TDryBulb: float, HumRatio: float, Pressure: float) -> float:
    """
    Return moist air specific volume given dry-bulb temperature, humidity ratio, and pressure.
    Args:
        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
        HumRatio : Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]
        Pressure : Atmospheric pressure in Psi [IP] or Pa [SI]
    Returns:
        Specific volume of moist air in ft³ lb⁻¹ of dry air [IP] or in m³ kg⁻¹ of dry air [SI]
    Reference:
        ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 26
    Notes:
        In IP units, R_DA_IP / 144 equals 0.370486 which is the coefficient appearing in eqn 26
        The factor 144 is for the conversion of Psi = lb in⁻² to lb ft⁻².
    """
    if HumRatio < 0:
        raise ValueError("Humidity ratio is negative")
    BoundedHumRatio = max(HumRatio, MIN_HUM_RATIO)

    if isIP():
        MoistAirVolume = R_DA_IP * GetTRankineFromTFahrenheit(TDryBulb) * (1 + 1.607858 * BoundedHumRatio) / (144 * Pressure)
    else:
        MoistAirVolume = R_DA_SI * GetTKelvinFromTCelsius(TDryBulb) * (1 + 1.607858 * BoundedHumRatio) / Pressure
    return MoistAirVolume

def GetTDryBulbFromMoistAirVolumeAndHumRatio(MoistAirVolume: float, HumRatio: float, Pressure: float) -> float:
    """
    Return dry-bulb temperature given moist air specific volume, humidity ratio, and pressure.
    Args:
        MoistAirVolume: Specific volume of moist air in ft³ lb⁻¹ of dry air [IP] or in m³ kg⁻¹ of dry air [SI]
        HumRatio : Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]
        Pressure : Atmospheric pressure in Psi [IP] or Pa [SI]
    Returns:
        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
    Reference:
        ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 26
    Notes:
        In IP units, R_DA_IP / 144 equals 0.370486 which is the coefficient appearing in eqn 26
        The factor 144 is for the conversion of Psi = lb in⁻² to lb ft⁻².
        Based on the `GetMoistAirVolume` function, rearranged for dry-bulb temperature.
    """
    if HumRatio < 0:
        raise ValueError("Humidity ratio is negative")
    BoundedHumRatio = max(HumRatio, MIN_HUM_RATIO)

    if isIP():
        TDryBulb = GetTFahrenheitFromTRankine(MoistAirVolume * (144 * Pressure)
                        / (R_DA_IP * (1 + 1.607858 * BoundedHumRatio)))
    else:
        TDryBulb = GetTCelsiusFromTKelvin(MoistAirVolume * Pressure
                        / (R_DA_SI * (1 + 1.607858 * BoundedHumRatio)))
    return TDryBulb

def GetMoistAirDensity(TDryBulb: float, HumRatio: float, Pressure:float) -> float:
    """
    Return moist air density given humidity ratio, dry bulb temperature, and pressure.
    Args:
        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
        HumRatio : Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]
        Pressure : Atmospheric pressure in Psi [IP] or Pa [SI]
    Returns:
        MoistAirDensity: Moist air density in lb ft⁻³ [IP] or kg m⁻³ [SI]
    Reference:
        ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 11
    """
    if HumRatio < 0:
        raise ValueError("Humidity ratio is negative")
    BoundedHumRatio = max(HumRatio, MIN_HUM_RATIO)

    MoistAirVolume = GetMoistAirVolume(TDryBulb, BoundedHumRatio, Pressure)
    MoistAirDensity = (1 + BoundedHumRatio) / MoistAirVolume
    return MoistAirDensity


#######################################################################################################
# Standard atmosphere
#######################################################################################################

def GetStandardAtmPressure(Altitude: float) -> float:
    """
    Return standard atmosphere barometric pressure, given the elevation (altitude).
    Args:
        Altitude: Altitude in ft [IP] or m [SI]
    Returns:
        Standard atmosphere barometric pressure in Psi [IP] or Pa [SI]
    Reference:
        ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 3
    """

    if isIP():
        StandardAtmPressure = 14.696 * math.pow(1 - 6.8754e-06 * Altitude, 5.2559)
    else:
        StandardAtmPressure = 101325 * math.pow(1 - 2.25577e-05 * Altitude, 5.2559)
    return StandardAtmPressure

def GetStandardAtmTemperature(Altitude: float) -> float:
    """
    Return standard atmosphere temperature, given the elevation (altitude).
    Args:
        Altitude: Altitude in ft [IP] or m [SI]
    Returns:
        Standard atmosphere dry-bulb temperature in °F [IP] or °C [SI]
    Reference:
        ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 4
    """
    if isIP():
        StandardAtmTemperature = 59 - 0.00356620 * Altitude
    else:
        StandardAtmTemperature = 15 - 0.0065 * Altitude
    return StandardAtmTemperature

def GetSeaLevelPressure(StationPressure: float, Altitude: float, TDryBulb: float) -> float:

    """
    Return sea level pressure given dry-bulb temperature, altitude above sea level and pressure.
    Args:
        StationPressure : Observed station pressure in Psi [IP] or Pa [SI]
        Altitude: Altitude in ft [IP] or m [SI]
        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
    Returns:
        Sea level barometric pressure in Psi [IP] or Pa [SI]
    Reference:
        Hess SL, Introduction to theoretical meteorology, Holt Rinehart and Winston, NY 1959,
        ch. 6.5; Stull RB, Meteorology for scientists and engineers, 2nd edition,
        Brooks/Cole 2000, ch. 1.
    Notes:
        The standard procedure for the US is to use for TDryBulb the average
        of the current station temperature and the station temperature from 12 hours ago.
    """
    if isIP():
        # Calculate average temperature in column of air, assuming a lapse rate
        # of 3.6 °F/1000ft
        TColumn = TDryBulb + 0.0036 * Altitude / 2

        # Determine the scale height
        H = 53.351 * GetTRankineFromTFahrenheit(TColumn)
    else:
        # Calculate average temperature in column of air, assuming a lapse rate
        # of 6.5 °C/km
        TColumn = TDryBulb + 0.0065 * Altitude / 2

        # Determine the scale height
        H = 287.055 * GetTKelvinFromTCelsius(TColumn) / 9.807

    # Calculate the sea level pressure
    SeaLevelPressure = StationPressure * math.exp(Altitude / H)
    return SeaLevelPressure

def GetStationPressure(SeaLevelPressure: float, Altitude: float, TDryBulb: float) -> float:
    """
    Return station pressure from sea level pressure.
    Args:
        SeaLevelPressure : Sea level barometric pressure in Psi [IP] or Pa [SI]
        Altitude: Altitude in ft [IP] or m [SI]
        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
    Returns:
        Station pressure in Psi [IP] or Pa [SI]
    Reference:
        See 'GetSeaLevelPressure'
    Notes:
        This function is just the inverse of 'GetSeaLevelPressure'.
    """
    StationPressure = SeaLevelPressure / GetSeaLevelPressure(1, Altitude, TDryBulb)
    return StationPressure


######################################################################################################
# Functions to set all psychrometric values
#######################################################################################################

def CalcPsychrometricsFromTWetBulb(TDryBulb: float, TWetBulb: float, Pressure: float) -> tuple:
    """
    Utility function to calculate humidity ratio, dew-point temperature, relative humidity,
    vapour pressure, moist air enthalpy, moist air volume, and degree of saturation of air given
    dry-bulb temperature, wet-bulb temperature, and pressure.
    Args:
        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
        TWetBulb : Wet-bulb temperature in °F [IP] or °C [SI]
        Pressure : Atmospheric pressure in Psi [IP] or Pa [SI]
    Returns:
        Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]
        Dew-point temperature in °F [IP] or °C [SI]
        Relative humidity in range [0, 1]
        Partial pressure of water vapor in moist air in Psi [IP] or Pa [SI]
        Moist air enthalpy in Btu lb⁻¹ [IP] or J kg⁻¹ [SI]
        Specific volume of moist air in ft³ lb⁻¹ [IP] or in m³ kg⁻¹ [SI]
        Degree of saturation [unitless]
    """
    HumRatio = GetHumRatioFromTWetBulb(TDryBulb, TWetBulb, Pressure)
    TDewPoint = GetTDewPointFromHumRatio(TDryBulb, HumRatio, Pressure)
    RelHum = GetRelHumFromHumRatio(TDryBulb, HumRatio, Pressure)
    VapPres = GetVapPresFromHumRatio(HumRatio, Pressure)
    MoistAirEnthalpy = GetMoistAirEnthalpy(TDryBulb, HumRatio)
    MoistAirVolume = GetMoistAirVolume(TDryBulb, HumRatio, Pressure)
    DegreeOfSaturation = GetDegreeOfSaturation(TDryBulb, HumRatio, Pressure)
    return HumRatio, TDewPoint, RelHum, VapPres, MoistAirEnthalpy, MoistAirVolume, DegreeOfSaturation

def CalcPsychrometricsFromTDewPoint(TDryBulb: float, TDewPoint: float, Pressure: float) -> tuple:
    """
    Utility function to calculate humidity ratio, wet-bulb temperature, relative humidity,
    vapour pressure, moist air enthalpy, moist air volume, and degree of saturation of air given
    dry-bulb temperature, dew-point temperature, and pressure.
    Args:
        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
        TDewPoint : Dew-point temperature in °F [IP] or °C [SI]
        Pressure : Atmospheric pressure in Psi [IP] or Pa [SI]
    Returns:
        Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]
        Wet-bulb temperature in °F [IP] or °C [SI]
        Relative humidity in range [0, 1]
        Partial pressure of water vapor in moist air in Psi [IP] or Pa [SI]
        Moist air enthalpy in Btu lb⁻¹ [IP] or J kg⁻¹ [SI]
        Specific volume of moist air in ft³ lb⁻¹ [IP] or in m³ kg⁻¹ [SI]
        Degree of saturation [unitless]
    """
    HumRatio = GetHumRatioFromTDewPoint(TDewPoint, Pressure)
    TWetBulb = GetTWetBulbFromHumRatio(TDryBulb, HumRatio, Pressure)
    RelHum = GetRelHumFromHumRatio(TDryBulb, HumRatio, Pressure)
    VapPres = GetVapPresFromHumRatio(HumRatio, Pressure)
    MoistAirEnthalpy = GetMoistAirEnthalpy(TDryBulb, HumRatio)
    MoistAirVolume = GetMoistAirVolume(TDryBulb, HumRatio, Pressure)
    DegreeOfSaturation = GetDegreeOfSaturation(TDryBulb, HumRatio, Pressure)
    return HumRatio, TWetBulb, RelHum, VapPres, MoistAirEnthalpy, MoistAirVolume, DegreeOfSaturation

def CalcPsychrometricsFromRelHum(TDryBulb: float, RelHum: float, Pressure: float) -> tuple:
    """
    Utility function to calculate humidity ratio, wet-bulb temperature, dew-point temperature,
    vapour pressure, moist air enthalpy, moist air volume, and degree of saturation of air given
    dry-bulb temperature, relative humidity and pressure.
    Args:
        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
        RelHum : Relative humidity in range [0, 1]
        Pressure : Atmospheric pressure in Psi [IP] or Pa [SI]
    Returns:
        Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]
        Wet-bulb temperature in °F [IP] or °C [SI]
        Dew-point temperature in °F [IP] or °C [SI].
        Partial pressure of water vapor in moist air in Psi [IP] or Pa [SI]
        Moist air enthalpy in Btu lb⁻¹ [IP] or J kg⁻¹ [SI]
        Specific volume of moist air in ft³ lb⁻¹ [IP] or in m³ kg⁻¹ [SI]
        Degree of saturation [unitless]
    """
    HumRatio = GetHumRatioFromRelHum(TDryBulb, RelHum, Pressure)
    TWetBulb = GetTWetBulbFromHumRatio(TDryBulb, HumRatio, Pressure)
    TDewPoint = GetTDewPointFromHumRatio(TDryBulb, HumRatio, Pressure)
    VapPres = GetVapPresFromHumRatio(HumRatio, Pressure)
    MoistAirEnthalpy = GetMoistAirEnthalpy(TDryBulb, HumRatio)
    MoistAirVolume = GetMoistAirVolume(TDryBulb, HumRatio, Pressure)
    DegreeOfSaturation = GetDegreeOfSaturation(TDryBulb, HumRatio, Pressure)
    return HumRatio, TWetBulb, TDewPoint, VapPres, MoistAirEnthalpy, MoistAirVolume, DegreeOfSaturation

C_sharp

   1
   2
   3
   4
   5
   6
   7
   8
   9
  10
  11
  12
  13
  14
  15
  16
  17
  18
  19
  20
  21
  22
  23
  24
  25
  26
  27
  28
  29
  30
  31
  32
  33
  34
  35
  36
  37
  38
  39
  40
  41
  42
  43
  44
  45
  46
  47
  48
  49
  50
  51
  52
  53
  54
  55
  56
  57
  58
  59
  60
  61
  62
  63
  64
  65
  66
  67
  68
  69
  70
  71
  72
  73
  74
  75
  76
  77
  78
  79
  80
  81
  82
  83
  84
  85
  86
  87
  88
  89
  90
  91
  92
  93
  94
  95
  96
  97
  98
  99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
 126
 127
 128
 129
 130
 131
 132
 133
 134
 135
 136
 137
 138
 139
 140
 141
 142
 143
 144
 145
 146
 147
 148
 149
 150
 151
 152
 153
 154
 155
 156
 157
 158
 159
 160
 161
 162
 163
 164
 165
 166
 167
 168
 169
 170
 171
 172
 173
 174
 175
 176
 177
 178
 179
 180
 181
 182
 183
 184
 185
 186
 187
 188
 189
 190
 191
 192
 193
 194
 195
 196
 197
 198
 199
 200
 201
 202
 203
 204
 205
 206
 207
 208
 209
 210
 211
 212
 213
 214
 215
 216
 217
 218
 219
 220
 221
 222
 223
 224
 225
 226
 227
 228
 229
 230
 231
 232
 233
 234
 235
 236
 237
 238
 239
 240
 241
 242
 243
 244
 245
 246
 247
 248
 249
 250
 251
 252
 253
 254
 255
 256
 257
 258
 259
 260
 261
 262
 263
 264
 265
 266
 267
 268
 269
 270
 271
 272
 273
 274
 275
 276
 277
 278
 279
 280
 281
 282
 283
 284
 285
 286
 287
 288
 289
 290
 291
 292
 293
 294
 295
 296
 297
 298
 299
 300
 301
 302
 303
 304
 305
 306
 307
 308
 309
 310
 311
 312
 313
 314
 315
 316
 317
 318
 319
 320
 321
 322
 323
 324
 325
 326
 327
 328
 329
 330
 331
 332
 333
 334
 335
 336
 337
 338
 339
 340
 341
 342
 343
 344
 345
 346
 347
 348
 349
 350
 351
 352
 353
 354
 355
 356
 357
 358
 359
 360
 361
 362
 363
 364
 365
 366
 367
 368
 369
 370
 371
 372
 373
 374
 375
 376
 377
 378
 379
 380
 381
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
/*
 * PsychroLib (version 2.3.0) (https://github.com/psychrometrics/psychrolib)
 * Copyright (c) 2018 D. Thevenard and D. Meyer, D. Gosnell for the current library implementation
 * Copyright (c) 2017 ASHRAE Handbook — Fundamentals for ASHRAE equations and coefficients
 * Ported to C# by https://github.com/DJGosnell
 * Licensed under the MIT License.
 */

using System;

namespace PsychroLib
{
    /// <summary>
    /// Class of functions to enable the calculation of psychrometric properties of moist and dry air.
    /// </summary>
    public class Psychrometrics
    {
        /******************************************************************************************************
         * Global constants
         *****************************************************************************************************/

        /// <summary>
        /// Zero degree Fahrenheit (°F) expressed as degree Rankine (°R).
        /// Reference: ASHRAE Handbook - Fundamentals (2017) ch. 39.
        /// </summary>
        private const double ZERO_FAHRENHEIT_AS_RANKINE = 459.67;

        /// <summary>
        /// Zero degree Celsius (°C) expressed as Kelvin (K).
        /// Reference: ASHRAE Handbook - Fundamentals (2017) ch. 39.
        /// </summary>
        private const double ZERO_CELSIUS_AS_KELVIN = 273.15;

        /// <summary>
        /// Universal gas constant for dry air (IP version) in ft lb_Force lb_DryAir⁻¹ R⁻¹.
        /// Reference: ASHRAE Handbook - Fundamentals (2017) ch. 1.
        /// </summary>
        private const double R_DA_IP = 53.350;

        /// <summary>
        /// Universal gas constant for dry air (SI version) in J kg_DryAir⁻¹ K⁻¹.
        /// Reference: ASHRAE Handbook - Fundamentals (2017) ch. 1.
        /// </summary>
        private const double R_DA_SI = 287.042;

        /// <summary>
        /// Invalid value (dimensionless).
        /// </summary>
        private const double INVALID = -99999;

        /// <summary>
        /// Maximum number of iterations before exiting while loops.
        /// </summary>
        private const double MAX_ITER_COUNT = 100;

        /// <summary>
        /// Minimum acceptable humidity ratio used/returned by any functions.
        /// Any value above 0 or below the MIN_HUM_RATIO will be reset to this value.
        /// </summary>
        private const double MIN_HUM_RATIO = 1e-7;

        /// <summary>
        /// Freezing point of water in Fahrenheit.
        /// </summary>
        private const double FREEZING_POINT_WATER_IP = 32.0;

        /// <summary>
        /// Freezing point of water in Celsius.
        /// </summary>
        private const double FREEZING_POINT_WATER_SI = 0.0;

        /// <summary>
        /// Triple point of water in Fahrenheit.
        /// </summary>
        private const double TRIPLE_POINT_WATER_IP = 32.018;

        /// <summary>
        /// Triple point of water in Celsius.
        /// </summary>
        private const double TRIPLE_POINT_WATER_SI = 0.01;

        /// <summary>
        /// Gets or Sets the current system of units for the calculations.
        /// </summary>
        public UnitSystem UnitSystem
        {
            get => _unitSystem;
            set
            {
                _unitSystem = value;
                if (value == UnitSystem.IP)
                    PSYCHROLIB_TOLERANCE = 0.001 * 9.0 / 5.0;
                else
                    PSYCHROLIB_TOLERANCE = 0.001;
            }
        }

        private double PSYCHROLIB_TOLERANCE;
        private UnitSystem _unitSystem;

        /// <summary>
        /// Constructor to create instance with the specified unit system.
        /// </summary>
        /// <param name="unitSystem">System of units to utilize for calculations.</param>
        public Psychrometrics(UnitSystem unitSystem)
        {
            UnitSystem = unitSystem;
        }


        /******************************************************************************************************
         * Conversion between temperature units
         *****************************************************************************************************/

        /// <summary>
        /// Utility function to convert temperature to degree Rankine (°R)
        /// given temperature in degree Fahrenheit (°F).
        /// Reference: ASHRAE Handbook - Fundamentals (2017) ch. 1 section 3
        /// </summary>
        /// <param name="tF">Temperature in Fahrenheit (°F)</param>
        /// <returns>Rankine (°R)</returns>
        public double GetTRankineFromTFahrenheit(double tF)
        {
            return tF + ZERO_FAHRENHEIT_AS_RANKINE; /* exact */
        }

        /// <summary>
        /// Utility function to convert temperature to degree Fahrenheit (°F)
        /// given temperature in degree Rankine (°R).
        /// Reference: ASHRAE Handbook - Fundamentals (2017) ch. 1 section 3
        /// </summary>
        /// <param name="tR">Temperature in Rankine (°R)</param>
        /// <returns>Fahrenheit (°F)</returns>
        public double GetTFahrenheitFromTRankine(double tR)
        {
            return tR - ZERO_FAHRENHEIT_AS_RANKINE; /* exact */
        }

        /// <summary>
        /// Utility function to convert temperature to Kelvin (K)
        /// given temperature in degree Celsius (°C).
        /// Reference: ASHRAE Handbook - Fundamentals (2017) ch. 1 section 3
        /// </summary>
        /// <param name="tC">Temperature in Celsius (°C)</param>
        /// <returns>Rankine (°R)</returns>
        public double GetTKelvinFromTCelsius(double tC)
        {
            return tC + ZERO_CELSIUS_AS_KELVIN; /* exact */
        }

        /// <summary>
        /// Utility function to convert temperature to degree Celsius (°C)
        /// given temperature in Kelvin (K).
        /// Reference: ASHRAE Handbook - Fundamentals (2017) ch. 1 section 3
        /// </summary>
        /// <param name="tK">Temperature in Rankine (°R)</param>
        /// <returns>Celsius (°C)</returns>
        public double GetTCelsiusFromTKelvin(double tK)
        {
            return tK - ZERO_CELSIUS_AS_KELVIN; /* exact */
        }


        /******************************************************************************************************
         * Conversions between dew point, wet bulb, and relative humidity
         *****************************************************************************************************/

        /// <summary>
        /// Return wet-bulb temperature given dry-bulb temperature, dew-point temperature, and pressure.
        /// Reference: ASHRAE Handbook - Fundamentals (2017) ch. 1
        /// </summary>
        /// <param name="tDryBulb">Dry bulb temperature in °F [IP] or °C [SI]</param>
        /// <param name="tDewPoint">Dew point temperature in °F [IP] or °C [SI]</param>
        /// <param name="pressure">Atmospheric pressure in Psi [IP] or Pa [SI]</param>
        /// <returns>Wet bulb temperature in °F [IP] or °C [SI]</returns>
        public double GetTWetBulbFromTDewPoint(double tDryBulb, double tDewPoint, double pressure)
        {
            if (!(tDewPoint <= tDryBulb))
                throw new InvalidOperationException("Dew point temperature is above dry bulb temperature");

            var humRatio = GetHumRatioFromTDewPoint(tDewPoint, pressure);
            return GetTWetBulbFromHumRatio(tDryBulb, humRatio, pressure);
        }


        /// <summary>
        /// Return wet-bulb temperature given dry-bulb temperature, relative humidity, and pressure.
        /// Reference: ASHRAE Handbook - Fundamentals (2017) ch. 1
        /// </summary>
        /// <param name="tDryBulb">Dry bulb temperature in °F [IP] or °C [SI]</param>
        /// <param name="relHum">Relative humidity [0-1]</param>
        /// <param name="pressure">Atmospheric pressure in Psi [IP] or Pa [SI]</param>
        /// <returns>Wet bulb temperature in °F [IP] or °C [SI]</returns>
        public double GetTWetBulbFromRelHum(double tDryBulb, double relHum, double pressure)
        {
            if (!(relHum >= 0.0 && relHum <= 1.0))
                throw new InvalidOperationException("Relative humidity is outside range [0,1]");

            var humRatio = GetHumRatioFromRelHum(tDryBulb, relHum, pressure);
            return GetTWetBulbFromHumRatio(tDryBulb, humRatio, pressure);
        }


        /// <summary>
        /// Return relative humidity given dry-bulb temperature and dew-point temperature.
        /// Reference: ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 22
        /// </summary>
        /// <param name="tDryBulb">Dry bulb temperature in °F [IP] or °C [SI]</param>
        /// <param name="tDewPoint">Dew point temperature in °F [IP] or °C [SI]</param>
        /// <returns>Relative humidity [0-1]</returns>
        public double GetRelHumFromTDewPoint(double tDryBulb, double tDewPoint)
        {
            if (!(tDewPoint <= tDryBulb))
                throw new InvalidOperationException("Dew point temperature is above dry bulb temperature");

            var vapPres = GetSatVapPres(tDewPoint);
            var satVapPres = GetSatVapPres(tDryBulb);
            return vapPres / satVapPres;
        }

        /// <summary>
        /// Return relative humidity given dry-bulb temperature, wet bulb temperature and pressure.
        /// Reference: ASHRAE Handbook - Fundamentals (2017) ch. 1
        /// </summary>
        /// <param name="tDryBulb">Dry bulb temperature in °F [IP] or °C [SI]</param>
        /// <param name="tWetBulb">Wet bulb temperature in °F [IP] or °C [SI]</param>
        /// <param name="pressure">Atmospheric pressure in Psi [IP] or Pa [SI]</param>
        /// <returns>Relative humidity [0-1]</returns>
        public double GetRelHumFromTWetBulb(double tDryBulb, double tWetBulb, double pressure)
        {
            if (!(tWetBulb <= tDryBulb))
                throw new InvalidOperationException("Wet bulb temperature is above dry bulb temperature");

            var humRatio = GetHumRatioFromTWetBulb(tDryBulb, tWetBulb, pressure);
            return GetRelHumFromHumRatio(tDryBulb, humRatio, pressure);
        }

        /// <summary>
        /// Return dew-point temperature given dry-bulb temperature and relative humidity.
        /// Reference: ASHRAE Handbook - Fundamentals (2017) ch. 1
        /// </summary>
        /// <param name="tDryBulb">Dry bulb temperature in °F [IP] or °C [SI]</param>
        /// <param name="relHum">Relative humidity [0-1]</param>
        /// <returns>Dew Point temperature in °F [IP] or °C [SI]</returns>
        public double GetTDewPointFromRelHum(double tDryBulb, double relHum)
        {
            if (!(relHum >= 0.0 && relHum <= 1.0))
                throw new InvalidOperationException("Relative humidity is outside range [0,1]");

            var vapPres = GetVapPresFromRelHum(tDryBulb, relHum);
            return GetTDewPointFromVapPres(tDryBulb, vapPres);
        }

        /// <summary>
        /// Return dew-point temperature given dry-bulb temperature, wet-bulb temperature, and pressure.
        /// Reference: ASHRAE Handbook - Fundamentals (2017) ch. 1
        /// </summary>
        /// <param name="tDryBulb">Dry bulb temperature in °F [IP] or °C [SI]</param>
        /// <param name="tWetBulb">Wet bulb temperature in °F [IP] or °C [SI]</param>
        /// <param name="pressure">Atmospheric pressure in Psi [IP] or Pa [SI]</param>
        /// <returns>Dew Point temperature in °F [IP] or °C [SI]</returns>
        public double GetTDewPointFromTWetBulb(double tDryBulb, double tWetBulb, double pressure)
        {
            if (!(tWetBulb <= tDryBulb))
                throw new InvalidOperationException("Wet bulb temperature is above dry bulb temperature");

            var humRatio = GetHumRatioFromTWetBulb(tDryBulb, tWetBulb, pressure);
            return GetTDewPointFromHumRatio(tDryBulb, humRatio, pressure);
        }


        /******************************************************************************************************
         * Conversions between dew point, or relative humidity and vapor pressure
         *****************************************************************************************************/

        /// <summary>
        /// Return partial pressure of water vapor as a function of relative humidity and temperature.
        /// Reference: ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 12, 22
        /// </summary>
        /// <param name="tDryBulb">Dry bulb temperature in °F [IP] or °C [SI]</param>
        /// <param name="relHum">Relative humidity [0-1]</param>
        /// <returns>Partial pressure of water vapor in moist air in Psi [IP] or Pa [SI]</returns>
        public double GetVapPresFromRelHum(double tDryBulb, double relHum)
        {
            if (!(relHum >= 0.0 && relHum <= 1.0))
                throw new InvalidOperationException("Relative humidity is outside range [0,1]");

            return relHum * GetSatVapPres(tDryBulb);
        }

        /// <summary>
        /// Return relative humidity given dry-bulb temperature and vapor pressure.
        /// Reference: ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 12, 22
        /// </summary>
        /// <param name="tDryBulb">Dry bulb temperature in °F [IP] or °C [SI]</param>
        /// <param name="vapPres">Partial pressure of water vapor in moist air in Psi [IP] or Pa [SI]</param>
        /// <returns>Relative humidity [0-1]</returns>
        public double GetRelHumFromVapPres(double tDryBulb, double vapPres)
        {
            if (!(vapPres >= 0.0))
                throw new InvalidOperationException("Partial pressure of water vapor in moist air is negative");

            return vapPres / GetSatVapPres(tDryBulb);
        }

        /// <summary>
        /// Helper function returning the derivative of the natural log of the saturation vapor pressure
        /// as a function of dry-bulb temperature.
        /// Reference: ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn. 5 &amp; 6
        /// </summary>
        /// <param name="tDryBulb">Dry bulb temperature in °F [IP] or °C [SI]</param>
        /// <returns>Derivative of natural log of vapor pressure of saturated air in Psi [IP] or Pa [SI]</returns>
        private double dLnPws_(double tDryBulb)
        {
            double dLnPws, T;

            if (UnitSystem == UnitSystem.IP)
            {
                T = GetTRankineFromTFahrenheit(tDryBulb);

                if (tDryBulb <= TRIPLE_POINT_WATER_IP)
                    dLnPws = 1.0214165E+04 / Math.Pow(T, 2) - 5.3765794E-03 + 2 * 1.9202377E-07 * T
                             + 3 * 3.5575832E-10 * Math.Pow(T, 2) - 4 * 9.0344688E-14 * Math.Pow(T, 3) 
                             + 4.1635019 / T;
                else
                    dLnPws = 1.0440397E+04 / Math.Pow(T, 2) - 2.7022355E-02 + 2 * 1.2890360E-05 * T
                             - 3 * 2.4780681E-09 * Math.Pow(T, 2) + 6.5459673 / T;
            }
            else
            {
                T = GetTKelvinFromTCelsius(tDryBulb);

                if (tDryBulb <= TRIPLE_POINT_WATER_SI)
                    dLnPws = 5.6745359E+03 / Math.Pow(T, 2) - 9.677843E-03 + 2 * 6.2215701E-07 * T
                             + 3 * 2.0747825E-09 * Math.Pow(T, 2) - 4 * 9.484024E-13 * Math.Pow(T, 3) 
                             + 4.1635019 / T;
                else
                    dLnPws = 5.8002206E+03 / Math.Pow(T, 2) - 4.8640239E-02 + 2 * 4.1764768E-05 * T
                             - 3 * 1.4452093E-08 * Math.Pow(T, 2) + 6.5459673 / T;
            }

            return dLnPws;
        }

        /// <summary>
        /// Return dew-point temperature given dry-bulb temperature and vapor pressure.
        /// Reference: ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn. 5 and 6
        /// Notes: the dew point temperature is solved by inverting the equation giving water vapor pressure
        /// at saturation from temperature rather than using the regressions provided
        /// by ASHRAE (eqn. 37 and 38) which are much less accurate and have a
        /// narrower range of validity.
        /// The Newton-Raphson (NR) method is used on the logarithm of water vapour
        /// pressure as a function of temperature, which is a very smooth function
        /// Convergence is usually achieved in 3 to 5 iterations.
        /// tDryBulb is not really needed here, just used for convenience.
        /// </summary>
        /// <param name="tDryBulb">Dry bulb temperature in °F [IP] or °C [SI]</param>
        /// <param name="vapPres">Partial pressure of water vapor in moist air in Psi [IP] or Pa [SI]</param>
        /// <returns>(o) Dew Point temperature in °F [IP] or °C [SI]</returns>
        public double GetTDewPointFromVapPres(double tDryBulb, double vapPres)
        {
            // Bounds function of the system of units

            var bounds = UnitSystem == UnitSystem.IP
                ? new[] {-148.0, 392.0}
                : new[] {-100.0, 200.0};

            // Bounds outside which a solution cannot be found
            if (vapPres < GetSatVapPres(bounds[0]) || vapPres > GetSatVapPres(bounds[1]))
                throw new InvalidOperationException(
                    "Partial pressure of water vapor is outside range of validity of equations");

            // We use NR to approximate the solution.
            // First guess
            var tDewPoint =
                tDryBulb; // Calculated value of dew point temperatures, solved for iteratively in °F [IP] or °C [SI]
            var lnVP = Math.Log(vapPres); // Natural logarithm of partial pressure of water vapor pressure in moist air

            double tDewPoint_iter; // Value of tDewPoint used in NR calculation
            double lnVP_iter; // Value of log of vapor water pressure used in NR calculation
            var index = 1;
            do
            {
                // Current point
                tDewPoint_iter = tDewPoint;
                lnVP_iter = Math.Log(GetSatVapPres(tDewPoint_iter));

                // Derivative of function, calculated analytically
                var d_lnVP = dLnPws_(tDewPoint_iter);

                // New estimate, bounded by domain of validity of eqn. 5 and 6
                tDewPoint = tDewPoint_iter - (lnVP_iter - lnVP) / d_lnVP;
                tDewPoint = Math.Max(tDewPoint, bounds[0]);
                tDewPoint = Math.Min(tDewPoint, bounds[1]);

                if (index > MAX_ITER_COUNT)
                    throw new InvalidOperationException(
                        "Convergence not reached in GetTDewPointFromVapPres. Stopping.");

                index++;
            } while (Math.Abs(tDewPoint - tDewPoint_iter) > PSYCHROLIB_TOLERANCE);

            return Math.Min(tDewPoint, tDryBulb);
        }

        /// <summary>
        /// Return vapor pressure given dew point temperature.
        /// Reference: ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn. 36
        /// </summary>
        /// <param name="tDewPoint">Dew point temperature in °F [IP] or °C [SI]</param>
        /// <returns>Partial pressure of water vapor in moist air in Psi [IP] or Pa [SI]</returns>
        public double GetVapPresFromTDewPoint(double tDewPoint)
        {
            return GetSatVapPres(tDewPoint);
        }


        /******************************************************************************************************
         * Conversions from wet-bulb temperature, dew-point temperature, or relative humidity to humidity ratio
         *****************************************************************************************************/

        /// <summary>
        /// Return wet-bulb temperature given dry-bulb temperature, humidity ratio, and pressure.
        /// Reference: ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 33 and 35 solved for Tstar
        /// </summary>
        /// <param name="tDryBulb">Dry bulb temperature in °F [IP] or °C [SI]</param>
        /// <param name="humRatio">Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]</param>
        /// <param name="pressure">Atmospheric pressure in Psi [IP] or Pa [SI]</param>
        /// <returns>Wet bulb temperature in °F [IP] or °C [SI]</returns>
        public double GetTWetBulbFromHumRatio(double tDryBulb, double humRatio, double pressure)
        {
            // Declarations
            double Wstar;
            double tDewPoint, tWetBulb, tWetBulbSup, tWetBulbInf, boundedHumRatio;
            var index = 1;

            if (!(humRatio >= 0.0))
                throw new InvalidOperationException("Humidity ratio is negative");
            boundedHumRatio = Math.Max(humRatio, MIN_HUM_RATIO);

            tDewPoint = GetTDewPointFromHumRatio(tDryBulb, boundedHumRatio, pressure);

            // Initial guesses
            tWetBulbSup = tDryBulb;
            tWetBulbInf = tDewPoint;
            tWetBulb = (tWetBulbInf + tWetBulbSup) / 2.0;

            // Bisection loop
            while ((tWetBulbSup - tWetBulbInf) > PSYCHROLIB_TOLERANCE)
            {
                // Compute humidity ratio at temperature Tstar
                Wstar = GetHumRatioFromTWetBulb(tDryBulb, tWetBulb, pressure);

                // Get new bounds
                if (Wstar > boundedHumRatio)
                    tWetBulbSup = tWetBulb;
                else
                    tWetBulbInf = tWetBulb;

                // New guess of wet bulb temperature
                tWetBulb = (tWetBulbSup + tWetBulbInf) / 2.0;

                if (index > MAX_ITER_COUNT)
                    throw new InvalidOperationException(
                        "Convergence not reached in GetTWetBulbFromHumRatio. Stopping.");

                index++;
            }

            return tWetBulb;
        }

        /// <summary>
        /// Return humidity ratio given dry-bulb temperature, wet-bulb temperature, and pressure.
        /// Reference: ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 33 and 35
        /// </summary>
        /// <param name="tDryBulb">Dry bulb temperature in °F [IP] or °C [SI]</param>
        /// <param name="tWetBulb">Wet bulb temperature in °F [IP] or °C [SI]</param>
        /// <param name="pressure">Atmospheric pressure in Psi [IP] or Pa [SI]</param>
        /// <returns>Humidity Ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]</returns>
        public double GetHumRatioFromTWetBulb(double tDryBulb, double tWetBulb, double pressure)
        {
            double wsstar;
            double humRatio = INVALID;

            if (!(tWetBulb <= tDryBulb))
                throw new InvalidOperationException("Wet bulb temperature is above dry bulb temperature");

            wsstar = GetSatHumRatio(tWetBulb, pressure);

            if (UnitSystem == UnitSystem.IP)
            {
                if (tWetBulb >= FREEZING_POINT_WATER_IP)
                    humRatio = ((1093.0 - 0.556 * tWetBulb) * wsstar - 0.240 * (tDryBulb - tWetBulb))
                               / (1093.0 + 0.444 * tDryBulb - tWetBulb);
                else
                    humRatio = ((1220.0 - 0.04 * tWetBulb) * wsstar - 0.240 * (tDryBulb - tWetBulb))
                               / (1220.0 + 0.444 * tDryBulb - 0.48 * tWetBulb);
            }
            else
            {
                if (tWetBulb >= FREEZING_POINT_WATER_SI)
                    humRatio = ((2501.0 - 2.326 * tWetBulb) * wsstar - 1.006 * (tDryBulb - tWetBulb))
                               / (2501.0 + 1.86 * tDryBulb - 4.186 * tWetBulb);
                else
                    humRatio = ((2830.0 - 0.24 * tWetBulb) * wsstar - 1.006 * (tDryBulb - tWetBulb))
                               / (2830.0 + 1.86 * tDryBulb - 2.1 * tWetBulb);
            }

            // Validity check.
            return Math.Max(humRatio, MIN_HUM_RATIO);
        }


        /// <summary>
        /// Return humidity ratio given dry-bulb temperature, relative humidity, and pressure.
        /// Reference: ASHRAE Handbook - Fundamentals (2017) ch. 1
        /// </summary>
        /// <param name="tDryBulb">Dry bulb temperature in °F [IP] or °C [SI]</param>
        /// <param name="relHum">Relative humidity [0-1]</param>
        /// <param name="pressure">Atmospheric pressure in Psi [IP] or Pa [SI]</param>
        /// <returns>Humidity Ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]</returns>
        public double GetHumRatioFromRelHum(double tDryBulb, double relHum, double pressure)
        {
            if (!(relHum >= 0.0 && relHum <= 1.0))
                throw new InvalidOperationException("Relative humidity is outside range [0,1]");

            var vapPres = GetVapPresFromRelHum(tDryBulb, relHum);
            return GetHumRatioFromVapPres(vapPres, pressure);
        }


        /// <summary>
        /// Return relative humidity given dry-bulb temperature, humidity ratio, and pressure.
        /// Reference: ASHRAE Handbook - Fundamentals (2017) ch. 1
        /// </summary>
        /// <param name="tDryBulb">Dry bulb temperature in °F [IP] or °C [SI]</param>
        /// <param name="humRatio">Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]</param>
        /// <param name="pressure">Atmospheric pressure in Psi [IP] or Pa [SI]</param>
        /// <returns>Relative humidity [0-1]</returns>
        public double GetRelHumFromHumRatio(double tDryBulb, double humRatio, double pressure)
        {
            if (!(humRatio >= 0.0))
                throw new InvalidOperationException("Humidity ratio is negative");

            var vapPres = GetVapPresFromHumRatio(humRatio, pressure);
            return GetRelHumFromVapPres(tDryBulb, vapPres);
        }

        /// <summary>
        /// Return humidity ratio given dew-point temperature and pressure.
        /// Reference: ASHRAE Handbook - Fundamentals (2017) ch. 1
        /// </summary>
        /// <param name="tDewPoint">Dew point temperature in °F [IP] or °C [SI]</param>
        /// <param name="pressure">Atmospheric pressure in Psi [IP] or Pa [SI]</param>
        /// <returns>Humidity Ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]</returns>
        public double GetHumRatioFromTDewPoint(double tDewPoint, double pressure)
        {
            var vapPres = GetSatVapPres(tDewPoint);
            return GetHumRatioFromVapPres(vapPres, pressure);
        }

        /// <summary>
        /// Return dew-point temperature given dry-bulb temperature, humidity ratio, and pressure.
        /// Reference: ASHRAE Handbook - Fundamentals (2017) ch. 1
        /// </summary>
        /// <param name="tDryBulb">Dry bulb temperature in °F [IP] or °C [SI]</param>
        /// <param name="humRatio">Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]</param>
        /// <param name="pressure">Atmospheric pressure in Psi [IP] or Pa [SI]</param>
        /// <returns>Dew Point temperature in °F [IP] or °C [SI]</returns>
        public double GetTDewPointFromHumRatio(double tDryBulb, double humRatio, double pressure)
        {
            if (!(humRatio >= 0.0))
                throw new InvalidOperationException("Humidity ratio is negative");

            var vapPres = GetVapPresFromHumRatio(humRatio, pressure);
            return GetTDewPointFromVapPres(tDryBulb, vapPres);
        }


        /******************************************************************************************************
         * Conversions between humidity ratio and vapor pressure
         *****************************************************************************************************/

        /// <summary>
        /// Return humidity ratio given water vapor pressure and atmospheric pressure.
        /// Reference: ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 20
        /// </summary>
        /// <param name="vapPres">Partial pressure of water vapor in moist air in Psi [IP] or Pa [SI]</param>
        /// <param name="pressure">Atmospheric pressure in Psi [IP] or Pa [SI]</param>
        /// <returns>Humidity Ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]</returns>
        public double GetHumRatioFromVapPres(double vapPres, double pressure)
        {
            if (!(vapPres >= 0.0))
                throw new InvalidOperationException("Partial pressure of water vapor in moist air is negative");

            var humRatio = 0.621945 * vapPres / (pressure - vapPres);

            // Validity check.
            return Math.Max(humRatio, MIN_HUM_RATIO);
        }


        /// <summary>
        /// Return vapor pressure given humidity ratio and pressure.
        /// Reference: ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 20 solved for pw
        /// </summary>
        /// <param name="humRatio">Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]</param>
        /// <param name="pressure">Atmospheric pressure in Psi [IP] or Pa [SI]</param>
        /// <returns>Partial pressure of water vapor in moist air in Psi [IP] or Pa [SI]</returns>
        public double GetVapPresFromHumRatio(double humRatio, double pressure)
        {
            if (!(humRatio >= 0.0))
                throw new InvalidOperationException("Humidity ratio is negative");
            var boundedHumRatio = Math.Max(humRatio, MIN_HUM_RATIO);

            var vapPres = pressure * boundedHumRatio / (0.621945 + boundedHumRatio);
            return vapPres;
        }


        /******************************************************************************************************
         * Conversions between humidity ratio and specific humidity
         *****************************************************************************************************/

        /// <summary>
        /// Return the specific humidity from humidity ratio (aka mixing ratio)
        /// Reference: ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 9b
        /// </summary>
        /// <param name="humRatio">Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]</param>
        /// <returns>Specific humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]</returns>
        public double GetSpecificHumFromHumRatio(double humRatio)
        {
            if (!(humRatio >= 0.0))
                throw new InvalidOperationException("Humidity ratio is negative");
            var boundedHumRatio = Math.Max(humRatio, MIN_HUM_RATIO);

            return boundedHumRatio / (1.0 + boundedHumRatio);
        }


        /// <summary>
        /// Return the humidity ratio (aka mixing ratio) from specific humidity
        /// Reference: ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 9b (solved for humidity ratio)
        /// </summary>
        /// <param name="specificHum"></param>
        /// <returns>Humidity ratio in lb_H₂O lb_Dry_Air⁻¹ [IP] or kg_H₂O kg_Dry_Air⁻¹ [SI]</returns>
        public double GetHumRatioFromSpecificHum(double specificHum)
        {
            if (!(specificHum >= 0.0 && specificHum < 1.0))
                throw new InvalidOperationException("Specific humidity is outside range [0, 1]");

            var humRatio = specificHum / (1.0 - specificHum);

            // Validity check
            return Math.Max(humRatio, MIN_HUM_RATIO);
        }


        /******************************************************************************************************
         * Dry Air Calculations
         *****************************************************************************************************/

        /// <summary>
        /// Return dry-air enthalpy given dry-bulb temperature.
        /// Reference: ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn. 28
        /// </summary>
        /// <param name="tDryBulb">Dry bulb temperature in °F [IP] or °C [SI]</param>
        /// <returns>Dry air enthalpy in Btu lb⁻¹ [IP] or J kg⁻¹ [SI]</returns>
        public double GetDryAirEnthalpy(double tDryBulb)
        {
            if (UnitSystem == UnitSystem.IP)
                return 0.240 * tDryBulb;

            return 1006.0 * tDryBulb;
        }


        /// <summary>
        /// Return dry-air density given dry-bulb temperature and pressure.
        /// Reference: ASHRAE Handbook - Fundamentals (2017) ch. 1
        /// Notes: eqn 14 for the perfect gas relationship for dry air.
        /// Eqn 1 for the universal gas constant.
        /// The factor 144 in IP is for the conversion of Psi = lb in⁻² to lb ft⁻².
        /// </summary>
        /// <param name="tDryBulb">Dry bulb temperature in °F [IP] or °C [SI]</param>
        /// <param name="pressure">Atmospheric pressure in Psi [IP] or Pa [SI]</param>
        /// <returns>Dry air density in lb ft⁻³ [IP] or kg m⁻³ [SI]</returns>
        public double GetDryAirDensity(double tDryBulb, double pressure)
        {
            if (UnitSystem == UnitSystem.IP)
                return (144.0 * pressure) / R_DA_IP / GetTRankineFromTFahrenheit(tDryBulb);

            return pressure / R_DA_SI / GetTKelvinFromTCelsius(tDryBulb);
        }


        /// <summary>
        /// Return dry-air volume given dry-bulb temperature and pressure.
        /// Reference: ASHRAE Handbook - Fundamentals (2017) ch. 1.
        /// Notes: eqn 14 for the perfect gas relationship for dry air.
        /// Eqn 1 for the universal gas constant.
        /// The factor 144 in IP is for the conversion of Psi = lb in⁻² to lb ft⁻².
        /// </summary>
        /// <param name="tDryBulb">Dry bulb temperature in °F [IP] or °C [SI]</param>
        /// <param name="pressure">Atmospheric pressure in Psi [IP] or Pa [SI]</param>
        /// <returns>Dry air volume ft³ lb⁻¹ [IP] or in m³ kg⁻¹ [SI]</returns>
        public double GetDryAirVolume(double tDryBulb, double pressure)
        {
            if (UnitSystem == UnitSystem.IP)
                return R_DA_IP * GetTRankineFromTFahrenheit(tDryBulb) / (144.0 * pressure);

            return R_DA_SI * GetTKelvinFromTCelsius(tDryBulb) / pressure;
        }


        /// <summary>
        /// Return dry bulb temperature from enthalpy and humidity ratio.
        /// Reference: ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 30.
        /// Notes: based on the `GetMoistAirEnthalpy` function, rearranged for temperature.
        /// </summary>
        /// <param name="moistAirEnthalpy">Moist air enthalpy in Btu lb⁻¹ [IP] or J kg⁻¹</param>
        /// <param name="humRatio">Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]</param>
        /// <returns>Dry-bulb temperature in °F [IP] or °C [SI]</returns>
        public double GetTDryBulbFromEnthalpyAndHumRatio(double moistAirEnthalpy, double humRatio)
        {
            if (!(humRatio >= 0.0))
                throw new InvalidOperationException("Humidity ratio is negative");
            var boundedHumRatio = Math.Max(humRatio, MIN_HUM_RATIO);

            if (UnitSystem == UnitSystem.IP)
                return (moistAirEnthalpy - 1061.0 * boundedHumRatio) / (0.240 + 0.444 * boundedHumRatio);

            return (moistAirEnthalpy / 1000.0 - 2501.0 * boundedHumRatio) / (1.006 + 1.86 * boundedHumRatio);
        }


        /// <summary>
        /// Return humidity ratio from enthalpy and dry-bulb temperature.
        /// Reference: ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 30.
        /// Notes: based on the `GetMoistAirEnthalpy` function, rearranged for humidity ratio.
        /// </summary>
        /// <param name="moistAirEnthalpy">Moist air enthalpy in Btu lb⁻¹ [IP] or J kg⁻¹</param>
        /// <param name="tDryBulb">Dry bulb temperature in °F [IP] or °C [SI]</param>
        /// <returns>Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻</returns>
        public double GetHumRatioFromEnthalpyAndTDryBulb(double moistAirEnthalpy, double tDryBulb)
        {
            {
                double humRatio;
                if (UnitSystem == UnitSystem.IP)
                    humRatio = (moistAirEnthalpy - 0.240 * tDryBulb) / (1061.0 + 0.444 * tDryBulb);
                else
                    humRatio = (moistAirEnthalpy / 1000.0 - 1.006 * tDryBulb) / (2501.0 + 1.86 * tDryBulb);

                // Validity check.
                return Math.Max(humRatio, MIN_HUM_RATIO);
            }
        }


        /******************************************************************************************************
         * Saturated Air Calculations
         *****************************************************************************************************/

        /// <summary>
        /// Return saturation vapor pressure given dry-bulb temperature.
        /// Reference: ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn. 5 &amp; 6
        /// Important note: the ASHRAE formulae are defined above and below the freezing point but have
        /// a discontinuity at the freezing point. This is a small inaccuracy on ASHRAE's part: the formulae
        /// should be defined above and below the triple point of water (not the feezing point) in which case
        /// the discontinuity vanishes. It is essential to use the triple point of water otherwise function
        /// GetTDewPointFromVapPres, which inverts the present function, does not converge properly around
        /// the freezing point.
        /// </summary>
        /// <param name="tDryBulb">Dry bulb temperature in °F [IP] or °C [SI]</param>
        /// <returns>Vapor pressure of saturated air in Psi [IP] or Pa [SI]</returns>
        public double GetSatVapPres(double tDryBulb)
        {
            double lnPws;

            if (UnitSystem == UnitSystem.IP)
            {
                if (!(tDryBulb >= -148.0 && tDryBulb <= 392.0))
                    throw new InvalidOperationException("Dry bulb temperature is outside range [-148, 392]");

                var T = GetTRankineFromTFahrenheit(tDryBulb);
                if (tDryBulb <= TRIPLE_POINT_WATER_IP)
                    lnPws = (-1.0214165E+04 / T - 4.8932428 - 5.3765794E-03 * T + 1.9202377E-07 * T * T
                                                                                + 3.5575832E-10 * Math.Pow(T, 3) -
                             9.0344688E-14 * Math.Pow(T, 4) + 4.1635019 * Math.Log(T));
                else
                    lnPws = -1.0440397E+04 / T - 1.1294650E+01 - 2.7022355E-02 * T + 1.2890360E-05 * T * T
                            - 2.4780681E-09 * Math.Pow(T, 3) + 6.5459673 * Math.Log(T);
            }
            else
            {
                if (!(tDryBulb >= -100.0 && tDryBulb <= 200.0))
                    throw new InvalidOperationException("Dry bulb temperature is outside range [-100, 200]");

                var T = GetTKelvinFromTCelsius(tDryBulb);
                if (tDryBulb <= TRIPLE_POINT_WATER_SI)
                    lnPws = -5.6745359E+03 / T + 6.3925247 - 9.677843E-03 * T + 6.2215701E-07 * T * T
                                                                              + 2.0747825E-09 * Math.Pow(T, 3) -
                            9.484024E-13 * Math.Pow(T, 4) + 4.1635019 * Math.Log(T);
                else
                    lnPws = -5.8002206E+03 / T + 1.3914993 - 4.8640239E-02 * T + 4.1764768E-05 * T * T
                            - 1.4452093E-08 * Math.Pow(T, 3) + 6.5459673 * Math.Log(T);
            }

            return Math.Exp(lnPws);
        }


        /// <summary>
        /// Return humidity ratio of saturated air given dry-bulb temperature and pressure.
        /// Reference: ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 36, solved for W
        /// </summary>
        /// <param name="tDryBulb">Dry bulb temperature in °F [IP] or °C [SI]</param>
        /// <param name="pressure">Atmospheric pressure in Psi [IP] or Pa [SI]</param>
        /// <returns>Humidity ratio of saturated air in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]</returns>
        public double GetSatHumRatio(double tDryBulb, double pressure)
        {
            var satVaporPres = GetSatVapPres(tDryBulb);
            var satHumRatio = 0.621945 * satVaporPres / (pressure - satVaporPres);

            // Validity check.
            return Math.Max(satHumRatio, MIN_HUM_RATIO);
        }

        /// <summary>
        /// Return saturated air enthalpy given dry-bulb temperature and pressure.
        /// Reference: ASHRAE Handbook - Fundamentals (2017) ch. 1
        /// </summary>
        /// <param name="tDryBulb">Dry bulb temperature in °F [IP] or °C [SI]</param>
        /// <param name="pressure">Atmospheric pressure in Psi [IP] or Pa [SI]</param>
        /// <returns>Saturated air enthalpy in Btu lb⁻¹ [IP] or J kg⁻¹ [SI]</returns>
        public double GetSatAirEnthalpy(double tDryBulb, double pressure)
        {
            return GetMoistAirEnthalpy(tDryBulb, GetSatHumRatio(tDryBulb, pressure));
        }


        /******************************************************************************************************
         * Moist Air Calculations
         *****************************************************************************************************/

        /// <summary>
        /// Return Vapor pressure deficit given dry-bulb temperature, humidity ratio, and pressure.
        /// Reference: see Oke (1987) eqn. 2.13a
        /// </summary>
        /// <param name="tDryBulb">Dry bulb temperature in °F [IP] or °C [SI]</param>
        /// <param name="humRatio">Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]</param>
        /// <param name="pressure">Atmospheric pressure in Psi [IP] or Pa [SI]</param>
        /// <returns>Vapor pressure deficit in Psi [IP] or Pa [SI]</returns>
        public double GetVaporPressureDeficit(double tDryBulb, double humRatio, double pressure)
        {
            if (!(humRatio >= 0.0))
                throw new InvalidOperationException("Humidity ratio is negative");

            var relHum = GetRelHumFromHumRatio(tDryBulb, humRatio, pressure);
            return GetSatVapPres(tDryBulb) * (1.0 - relHum);
        }


        /// <summary>
        /// Return the degree of saturation (i.e humidity ratio of the air / humidity ratio of the air at saturation
        /// at the same temperature and pressure) given dry-bulb temperature, humidity ratio, and atmospheric pressure.
        /// Reference: ASHRAE Handbook - Fundamentals (2009) ch. 1 eqn. 12
        /// Notes: the definition is absent from the 2017 Handbook
        /// </summary>
        /// <param name="tDryBulb">Dry bulb temperature in °F [IP] or °C [SI]</param>
        /// <param name="humRatio">Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]</param>
        /// <param name="pressure">Atmospheric pressure in Psi [IP] or Pa [SI]</param>
        /// <returns>Degree of saturation (unitless)</returns>
        public double GetDegreeOfSaturation(double tDryBulb, double humRatio, double pressure)
        {
            if (!(humRatio >= 0.0))
                throw new InvalidOperationException("Humidity ratio is negative");
            var boundedHumRatio = Math.Max(humRatio, MIN_HUM_RATIO);

            return boundedHumRatio / GetSatHumRatio(tDryBulb, pressure);
        }

        /// <summary>
        /// Return moist air enthalpy given dry-bulb temperature and humidity ratio.
        /// Reference: ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn. 30
        /// </summary>
        /// <param name="tDryBulb">Dry bulb temperature in °F [IP] or °C [SI]</param>
        /// <param name="humRatio">Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]</param>
        /// <returns>Moist Air Enthalpy in Btu lb⁻¹ [IP] or J kg⁻¹ [SI]</returns>
        public double GetMoistAirEnthalpy(double tDryBulb, double humRatio)
        {
            if (!(humRatio >= 0.0))
                throw new InvalidOperationException("Humidity ratio is negative");

            var boundedHumRatio = Math.Max(humRatio, MIN_HUM_RATIO);

            if (UnitSystem == UnitSystem.IP)
                return 0.240 * tDryBulb + boundedHumRatio * (1061.0 + 0.444 * tDryBulb);

            return (1.006 * tDryBulb + boundedHumRatio * (2501.0 + 1.86 * tDryBulb)) * 1000.0;
        }


        /// <summary>
        /// Return moist air specific volume given dry-bulb temperature, humidity ratio, and pressure.
        /// Reference: ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn. 26
        /// Notes: in IP units, R_DA_IP / 144 equals 0.370486 which is the coefficient appearing in eqn 26.
        /// The factor 144 is for the conversion of Psi = lb in⁻² to lb ft⁻².
        /// </summary>
        /// <param name="tDryBulb">Dry bulb temperature in °F [IP] or °C [SI]</param>
        /// <param name="humRatio">Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]</param>
        /// <param name="pressure">Atmospheric pressure in Psi [IP] or Pa [SI]</param>
        /// <returns>Specific Volume ft³ lb⁻¹ [IP] or in m³ kg⁻¹ [SI]</returns>
        public double GetMoistAirVolume(double tDryBulb, double humRatio, double pressure)
        {
            if (!(humRatio >= 0.0))
                throw new InvalidOperationException("Humidity ratio is negative");
            var boundedHumRatio = Math.Max(humRatio, MIN_HUM_RATIO);

            if (UnitSystem == UnitSystem.IP)
                return R_DA_IP * GetTRankineFromTFahrenheit(tDryBulb) * (1.0 + 1.607858 * boundedHumRatio) /
                       (144.0 * pressure);

            return R_DA_SI * GetTKelvinFromTCelsius(tDryBulb) * (1.0 + 1.607858 * boundedHumRatio) / pressure;
        }


        /// <summary>
        /// Return dry-bulb temperature given moist air specific volume, humidity ratio, and pressure.
        /// Reference:
        /// ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 26
        /// Notes:
        /// In IP units, R_DA_IP / 144 equals 0.370486 which is the coefficient appearing in eqn 26
        /// The factor 144 is for the conversion of Psi = lb in⁻² to lb ft⁻².
        /// Based on the `GetMoistAirVolume` function, rearranged for dry-bulb temperature.
        /// </summary>
        /// <param name="MoistAirVolume">Specific volume of moist air in ft³ lb⁻¹ of dry air [IP] or in m³ kg⁻¹ of dry air [SI]</param>
        /// <param name="humRatio">Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]</param>
        /// <param name="pressure">Atmospheric pressure in Psi [IP] or Pa [SI]</param>
        /// <returns>Dry-bulb temperature in °F [IP] or °C [SI]</returns>
        public double GetTDryBulbFromMoistAirVolumeAndHumRatio(double MoistAirVolume, double humRatio, double pressure)
        {
            if (!(humRatio >= 0.0))
                throw new InvalidOperationException("Humidity ratio is negative");
            var boundedHumRatio = Math.Max(humRatio, MIN_HUM_RATIO);

            if (UnitSystem == UnitSystem.IP)
                return  GetTFahrenheitFromTRankine(MoistAirVolume * (144 * pressure) / (R_DA_IP * (1 + 1.607858 * boundedHumRatio)));

            return GetTCelsiusFromTKelvin(MoistAirVolume * pressure / (R_DA_SI * (1 + 1.607858 * boundedHumRatio)));
        }


        /// <summary>
        /// Return moist air density given humidity ratio, dry bulb temperature, and pressure.
        /// Reference: ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn. 11
        /// </summary>
        /// <param name="tDryBulb">Dry bulb temperature in °F [IP] or °C [SI]</param>
        /// <param name="humRatio">Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]</param>
        /// <param name="pressure">Atmospheric pressure in Psi [IP] or Pa [SI]</param>
        /// <returns>Moist air density in lb ft⁻³ [IP] or kg m⁻³ [SI]</returns>
        public double GetMoistAirDensity(double tDryBulb, double humRatio, double pressure)
        {
            if (!(humRatio >= 0.0))
                throw new InvalidOperationException("Humidity ratio is negative");

            var boundedHumRatio = Math.Max(humRatio, MIN_HUM_RATIO);

            return (1.0 + boundedHumRatio) / GetMoistAirVolume(tDryBulb, boundedHumRatio, pressure);
        }


        /******************************************************************************************************
         * Standard atmosphere
         *****************************************************************************************************/

        /// <summary>
        /// Return standard atmosphere barometric pressure, given the elevation (altitude).
        /// Reference: ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 3
        /// </summary>
        /// <param name="altitude">altitude in ft [IP] or m [SI]</param>
        /// <returns>Standard atmosphere barometric pressure in Psi [IP] or Pa [SI]</returns>
        public double GetStandardAtmPressure(double altitude)
        {
            if (UnitSystem == UnitSystem.IP)
                return 14.696 * Math.Pow(1.0 - 6.8754e-06 * altitude, 5.2559);

            return 101325.0 * Math.Pow(1.0 - 2.25577e-05 * altitude, 5.2559);
        }


        /// <summary>
        /// Return standard atmosphere temperature, given the elevation (altitude).
        /// Reference: ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 4
        /// </summary>
        /// <param name="altitude">altitude in ft [IP] or m [SI]</param>
        /// <returns> Standard atmosphere dry bulb temperature in °F [IP] or °C [SI]</returns>
        public double GetStandardAtmTemperature(double altitude)
        {
            if (UnitSystem == UnitSystem.IP)
                return 59.0 - 0.00356620 * altitude;

            return 15.0 - 0.0065 * altitude;
        }

        /// <summary>
        /// Return sea level pressure given dry-bulb temperature, altitude above sea level and pressure.
        /// Reference: Hess SL, Introduction to theoretical meteorology, Holt Rinehart and Winston, NY 1959,
        /// ch. 6.5; Stull RB, Meteorology for scientists and engineers, 2nd edition,
        /// Brooks/Cole 2000, ch. 1.
        /// Notes: the standard procedure for the US is to use for tDryBulb the average
        /// of the current station temperature and the station temperature from 12 hours ago.
        /// </summary>
        /// <param name="stnPressure">Observed station pressure in Psi [IP] or Pa [SI]</param>
        /// <param name="altitude">Altitude above sea level in ft [IP] or m [SI]</param>
        /// <param name="tDryBulb">Dry bulb temperature in °F [IP] or °C [SI]</param>
        /// <returns>Sea level barometric pressure in Psi [IP] or Pa [SI]</returns>
        public double GetSeaLevelPressure(double stnPressure, double altitude, double tDryBulb)
        {
            double h;
            if (UnitSystem == UnitSystem.IP)
            {
                // Calculate average temperature in column of air, assuming a lapse rate
                // of 3.6 °F/1000ft
                var tColumn = tDryBulb + 0.0036 * altitude / 2.0;

                // Determine the scale height
                h = 53.351 * GetTRankineFromTFahrenheit(tColumn);
            }
            else
            {
                // Calculate average temperature in column of air, assuming a lapse rate
                // of 6.5 °C/km
                var tColumn = tDryBulb + 0.0065 * altitude / 2.0;

                // Determine the scale height
                h = 287.055 * GetTKelvinFromTCelsius(tColumn) / 9.807;
            }

            // Calculate the sea level pressure
            var seaLevelPressure = stnPressure * Math.Exp(altitude / h);
            return seaLevelPressure;
        }


        /// <summary>
        /// Return station pressure from sea level pressure
        /// Reference: see 'GetSeaLevelPressure'
        /// Notes: this function is just the inverse of 'GetSeaLevelPressure'.
        /// </summary>
        /// <param name="seaLevelPressure">Sea level barometric pressure in Psi [IP] or Pa [SI]</param>
        /// <param name="altitude">Altitude above sea level in ft [IP] or m [SI]</param>
        /// <param name="tDryBulb">Dry bulb temperature in °F [IP] or °C [SI]</param>
        /// <returns>Station pressure in Psi [IP] or Pa [SI]</returns>
        public double GetStationPressure(double seaLevelPressure, double altitude, double tDryBulb)
        {
            return seaLevelPressure / GetSeaLevelPressure(1.0, altitude, tDryBulb);
        }


        /******************************************************************************************************
         * Functions to set all psychrometric values
         *****************************************************************************************************/

        /// <summary>
        /// Utility function to calculate humidity ratio, dew-point temperature, relative humidity,
        /// vapour pressure, moist air enthalpy, moist air volume, and degree of saturation of air given
        /// dry-bulb temperature, wet-bulb temperature, and pressure.
        /// </summary>
        /// <param name="tDryBulb">Dry bulb temperature in °F [IP] or °C [SI]</param>
        /// <param name="tWetBulb">Wet bulb temperature in °F [IP] or °C [SI]</param>
        /// <param name="pressure">Atmospheric pressure in Psi [IP] or Pa [SI]</param>
        /// <returns>Calculated values.</returns>
        public PsychrometricValue CalcPsychrometricsFromTWetBulb(double tDryBulb, double tWetBulb, double pressure)
        {
            var value = new PsychrometricValue
            {
                TDryBulb = tDryBulb,
                TWetBulb = tWetBulb,
                Pressure = pressure
            };

            value.HumRatio = GetHumRatioFromTWetBulb(tDryBulb, tWetBulb, pressure);
            value.TDewPoint = GetTDewPointFromHumRatio(tDryBulb, value.HumRatio, pressure);
            value.RelHum = GetRelHumFromHumRatio(tDryBulb, value.HumRatio, pressure);
            value.VapPres = GetVapPresFromHumRatio(value.HumRatio, pressure);
            value.MoistAirEnthalpy = GetMoistAirEnthalpy(tDryBulb, value.HumRatio);
            value.MoistAirVolume = GetMoistAirVolume(tDryBulb, value.HumRatio, pressure);
            value.DegreeOfSaturation = GetDegreeOfSaturation(tDryBulb, value.HumRatio, pressure);

            return value;
        }


        /// <summary>
        /// Utility function to calculate humidity ratio, wet-bulb temperature, relative humidity,
        /// vapour pressure, moist air enthalpy, moist air volume, and degree of saturation of air given
        /// dry-bulb temperature, dew-point temperature, and pressure.
        /// </summary>
        /// <param name="tDryBulb">Dry bulb temperature in °F [IP] or °C [SI]</param>
        /// <param name="tDewPoint">Dew point temperature in °F [IP] or °C [SI]</param>
        /// <param name="pressure">Atmospheric pressure in Psi [IP] or Pa [SI]</param>
        /// <returns>Calculated values.</returns>
        public PsychrometricValue CalcPsychrometricsFromTDewPoint(double tDryBulb, double tDewPoint, double pressure)
        {
            var value = new PsychrometricValue
            {
                TDryBulb = tDryBulb,
                TDewPoint = tDewPoint,
                Pressure = pressure
            };

            value.HumRatio = GetHumRatioFromTDewPoint(tDewPoint, pressure);
            value.TWetBulb = GetTWetBulbFromHumRatio(tDryBulb, value.HumRatio, pressure);
            value.RelHum = GetRelHumFromHumRatio(tDryBulb, value.HumRatio, pressure);
            value.VapPres = GetVapPresFromHumRatio(value.HumRatio, pressure);
            value.MoistAirEnthalpy = GetMoistAirEnthalpy(tDryBulb, value.HumRatio);
            value.MoistAirVolume = GetMoistAirVolume(tDryBulb, value.HumRatio, pressure);
            value.DegreeOfSaturation = GetDegreeOfSaturation(tDryBulb, value.HumRatio, pressure);

            return value;
        }


        /// <summary>
        /// Utility function to calculate humidity ratio, wet-bulb temperature, dew-point temperature,
        /// vapour pressure, moist air enthalpy, moist air volume, and degree of saturation of air given
        /// dry-bulb temperature, relative humidity and pressure.
        /// </summary>
        /// <param name="tDryBulb">Dry bulb temperature in °F [IP] or °C [SI]</param>
        /// <param name="relHum">Relative humidity [0-1]</param>
        /// <param name="pressure">Atmospheric pressure in Psi [IP] or Pa [SI]</param>
        /// <returns>Calculated values.</returns>
        public PsychrometricValue CalcPsychrometricsFromRelHum(double tDryBulb, double relHum, double pressure)
        {
            var value = new PsychrometricValue
            {
                TDryBulb = tDryBulb,
                RelHum = relHum,
                Pressure = pressure
            };

            value.HumRatio = GetHumRatioFromRelHum(tDryBulb, relHum, pressure);
            value.TWetBulb = GetTWetBulbFromHumRatio(tDryBulb, value.HumRatio, pressure);
            value.TDewPoint = GetTDewPointFromHumRatio(tDryBulb, value.HumRatio, pressure);
            value.VapPres = GetVapPresFromHumRatio(value.HumRatio, pressure);
            value.MoistAirEnthalpy = GetMoistAirEnthalpy(tDryBulb, value.HumRatio);
            value.MoistAirVolume = GetMoistAirVolume(tDryBulb, value.HumRatio, pressure);
            value.DegreeOfSaturation = GetDegreeOfSaturation(tDryBulb, value.HumRatio, pressure);

            return value;
        }
    }

    /// <summary>
    /// Contains output results of a Psychrometric calculation.
    /// </summary>
    public class PsychrometricValue
    {
        /// <summary>
        /// Dry bulb temperature in °F [IP] or °C [SI]
        /// </summary>
        public double TDryBulb { get; set; }

        /// <summary>
        /// Wet bulb temperature in °F [IP] or °C [SI]
        /// </summary>
        public double TWetBulb { get; set; }

        /// <summary>
        /// Atmospheric pressure in Psi [IP] or Pa [SI]
        /// </summary>
        public double Pressure { get; set; }

        /// <summary>
        /// Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]
        /// </summary>
        public double HumRatio { get; set; }

        /// <summary>
        /// Dew point temperature in °F [IP] or °C [SI]
        /// </summary>
        public double TDewPoint { get; set; }

        /// <summary>
        /// Relative humidity [0-1]
        /// </summary>
        public double RelHum { get; set; }

        /// <summary>
        /// Partial pressure of water vapor in moist air in Psi [IP] or Pa [SI]
        /// </summary>
        public double VapPres { get; set; }

        /// <summary>
        /// Moist air enthalpy in Btu lb⁻¹ [IP] or J kg⁻¹ [SI]
        /// </summary>
        public double MoistAirEnthalpy { get; set; }

        /// <summary>
        /// Specific volume ft³ lb⁻¹ [IP] or in m³ kg⁻¹ [SI]
        /// </summary>
        public double MoistAirVolume { get; set; }

        /// <summary>
        /// Degree of saturation [unitless]
        /// </summary>
        public double DegreeOfSaturation { get; set; }
    }

    /// <summary>
    /// Standard unit systems
    /// </summary>
    public enum UnitSystem
    {
        /// <summary>
        /// Imperial Units
        /// </summary>
        IP = 1,

        /// <summary>
        /// Metric System Units
        /// </summary>
        SI = 2
    }
}

Fortran

   1
   2
   3
   4
   5
   6
   7
   8
   9
  10
  11
  12
  13
  14
  15
  16
  17
  18
  19
  20
  21
  22
  23
  24
  25
  26
  27
  28
  29
  30
  31
  32
  33
  34
  35
  36
  37
  38
  39
  40
  41
  42
  43
  44
  45
  46
  47
  48
  49
  50
  51
  52
  53
  54
  55
  56
  57
  58
  59
  60
  61
  62
  63
  64
  65
  66
  67
  68
  69
  70
  71
  72
  73
  74
  75
  76
  77
  78
  79
  80
  81
  82
  83
  84
  85
  86
  87
  88
  89
  90
  91
  92
  93
  94
  95
  96
  97
  98
  99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
 126
 127
 128
 129
 130
 131
 132
 133
 134
 135
 136
 137
 138
 139
 140
 141
 142
 143
 144
 145
 146
 147
 148
 149
 150
 151
 152
 153
 154
 155
 156
 157
 158
 159
 160
 161
 162
 163
 164
 165
 166
 167
 168
 169
 170
 171
 172
 173
 174
 175
 176
 177
 178
 179
 180
 181
 182
 183
 184
 185
 186
 187
 188
 189
 190
 191
 192
 193
 194
 195
 196
 197
 198
 199
 200
 201
 202
 203
 204
 205
 206
 207
 208
 209
 210
 211
 212
 213
 214
 215
 216
 217
 218
 219
 220
 221
 222
 223
 224
 225
 226
 227
 228
 229
 230
 231
 232
 233
 234
 235
 236
 237
 238
 239
 240
 241
 242
 243
 244
 245
 246
 247
 248
 249
 250
 251
 252
 253
 254
 255
 256
 257
 258
 259
 260
 261
 262
 263
 264
 265
 266
 267
 268
 269
 270
 271
 272
 273
 274
 275
 276
 277
 278
 279
 280
 281
 282
 283
 284
 285
 286
 287
 288
 289
 290
 291
 292
 293
 294
 295
 296
 297
 298
 299
 300
 301
 302
 303
 304
 305
 306
 307
 308
 309
 310
 311
 312
 313
 314
 315
 316
 317
 318
 319
 320
 321
 322
 323
 324
 325
 326
 327
 328
 329
 330
 331
 332
 333
 334
 335
 336
 337
 338
 339
 340
 341
 342
 343
 344
 345
 346
 347
 348
 349
 350
 351
 352
 353
 354
 355
 356
 357
 358
 359
 360
 361
 362
 363
 364
 365
 366
 367
 368
 369
 370
 371
 372
 373
 374
 375
 376
 377
 378
 379
 380
 381
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
! PsychroLib (version 2.3.0) (https://github.com/psychrometrics/psychrolib)
! Copyright (c) 2018 D. Thevenard and D. Meyer for the current library implementation
! Copyright (c) 2017 ASHRAE Handbook — Fundamentals for ASHRAE equations and coefficients
! Licensed under the MIT License.

module psychrolib
  !+ Module overview
  !+  Contains functions for calculating thermodynamic properties of gas-vapor mixtures
  !+  and standard atmosphere suitable for most engineering, physical, and meteorological
  !+  applications.
  !+
  !+  Most of the functions are an implementation of the formulae found in the
  !+  2017 ASHRAE Handbook - Fundamentals, in both International System (SI),
  !+  and Imperial (IP) units. Please refer to the information included in
  !+  each function for their respective reference.
  !+
  !+ Example
  !+  use psychrolib, only: GetTDewPointFromRelHum, SetUnitSystem, SI
  !+  ! Set the unit system, for example to SI (can be either 'SI' or 'IP')
  !+  call SetUnitSystem(SI)
  !+  ! Calculate the dew point temperature for a dry bulb temperature of 25 C and a relative humidity of 80%
  !+  print *, GetTDewPointFromRelHum(25.0, 0.80)
  !+ 21.3094
  !+
  !+ Copyright
  !+  - For the current library implementation
  !+     Copyright (c) 2018 D. Thevenard and D. Meyer.
  !+  - For equations and coefficients published ASHRAE Handbook — Fundamentals, Chapter 1
  !+     Copyright (c) 2017 ASHRAE Handbook — Fundamentals (https://www.ashrae.org)
  !+
  !+ License
  !+  MIT (https://github.com/psychrometrics/psychrolib/LICENSE.txt)
  !+
  !+ Note from the Authors
  !+  We have made every effort to ensure that the code is adequate, however, we make no
  !+  representation with respect to its accuracy. Use at your own risk. Should you notice
  !+  an error, or if you have a suggestion, please notify us through GitHub at
  !+  https://github.com/psychrometrics/psychrolib/issues.


  implicit none

  private
  public :: IP
  public :: SI
  public :: SetUnitSystem
  public :: GetUnitSystem
  public :: isIP
  public :: GetTRankineFromTFahrenheit
  public :: GetTFahrenheitFromTRankine
  public :: GetTKelvinFromTCelsius
  public :: GetTCelsiusFromTKelvin
  public :: GetTWetBulbFromTDewPoint
  public :: GetTWetBulbFromRelHum
  public :: GetRelHumFromTDewPoint
  public :: GetRelHumFromTWetBulb
  public :: GetTDewPointFromRelHum
  public :: GetTDewPointFromTWetBulb
  public :: GetVapPresFromRelHum
  public :: GetRelHumFromVapPres
  public :: GetTDewPointFromVapPres
  public :: GetVapPresFromTDewPoint
  public :: GetTWetBulbFromHumRatio
  public :: GetHumRatioFromTWetBulb
  public :: GetHumRatioFromRelHum
  public :: GetRelHumFromHumRatio
  public :: GetHumRatioFromTDewPoint
  public :: GetTDewPointFromHumRatio
  public :: GetHumRatioFromVapPres
  public :: GetVapPresFromHumRatio
  public :: GetDryAirEnthalpy
  public :: GetDryAirDensity
  public :: GetDryAirVolume
  public :: GetTDryBulbFromEnthalpyAndHumRatio
  public :: GetHumRatioFromEnthalpyAndTDryBulb
  public :: GetSatVapPres
  public :: GetSatHumRatio
  public :: GetSatAirEnthalpy
  public :: GetVaporPressureDeficit
  public :: GetDegreeOfSaturation
  public :: GetMoistAirEnthalpy
  public :: GetMoistAirVolume
  public :: GetTDryBulbFromMoistAirVolumeAndHumRatio
  public :: GetMoistAirDensity
  public :: GetStandardAtmPressure
  public :: GetStandardAtmTemperature
  public :: GetSeaLevelPressure
  public :: GetStationPressure
  public :: GetSpecificHumFromHumRatio
  public :: GetHumRatioFromSpecificHum
  public :: CalcPsychrometricsFromTWetBulb
  public :: CalcPsychrometricsFromTDewPoint
  public :: CalcPsychrometricsFromRelHum
  public :: dLnPws_


  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
  ! Global constants
  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

  real, parameter ::  ZERO_FAHRENHEIT_AS_RANKINE = 459.67
    !+ Zero degree Fahrenheit (°F) expressed as degree Rankine (°R).
    !+ Reference:
    !+ ASHRAE Handbook - Fundamentals (2017) ch. 39.

  real, parameter ::  ZERO_CELSIUS_AS_KELVIN = 273.15
    !+ Zero degree Celsius (°C) expressed as Kelvin (K).
    !+ Reference:
    !+ ASHRAE Handbook - Fundamentals (2017) ch. 39.

  real, parameter ::  R_DA_IP = 53.350
    !+ Universal gas constant for dry air (IP version) in ft lb_Force lb_DryAir⁻¹ R⁻¹.
    !+ Reference:
    !+ ASHRAE Handbook - Fundamentals (2017) ch. 1.

  real, parameter ::  R_DA_SI = 287.042
    !+ Universal gas constant for dry air (SI version) in J kg_DryAir⁻¹ K⁻¹.
    !+ Reference:
    !+ ASHRAE Handbook - Fundamentals (2017) ch. 1.

  integer, parameter :: IP = 1
  integer, parameter :: SI = 2

  integer  :: PSYCHROLIB_UNITS = 0 ! 0 = undefined.
    !+ Unit system to use.

  real ::  PSYCHROLIB_TOLERANCE = 1.0
    !+ Tolerance of temperature calculations.

  integer, parameter  :: MAX_ITER_COUNT = 100
    !+ Maximum number of iterations before exiting while loops.

  real, parameter  :: MIN_HUM_RATIO = 1e-7
    !+ Minimum acceptable humidity ratio used/returned by any functions.
    !+ Any value above 0 or below the MIN_HUM_RATIO will be reset to this value.

  real, parameter  :: FREEZING_POINT_WATER_IP = 32.0
    !+ float: Freezing point of water in Fahrenheit.

  real, parameter  :: FREEZING_POINT_WATER_SI = 0.0
    !+ float: Freezing point of water in Celsius.

  real, parameter  :: TRIPLE_POINT_WATER_IP = 32.018
    !+ float: Triple point of water in Fahrenheit.

  real, parameter  :: TRIPLE_POINT_WATER_SI = 0.01
    !+ float: Triple point of water in Celsius.


  contains


  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
  ! Helper functions
  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

  subroutine SetUnitSystem(UnitSystem)
  !+ Set the system of units to use (SI or IP).
  !+ Notes: this function *HAS TO BE CALLED* before the library can be used

    integer, intent(in)    :: UnitSystem
    !+ Units: string indicating the system of units chosen (SI or IP)

    if (.not. (UnitSystem == SI .or. UnitSystem == IP)) then
      error stop "The system of units has to be either SI or IP."
    end if

    PSYCHROLIB_UNITS = UnitSystem

    ! Define tolerance on temperature calculations
    ! The tolerance is the same in IP and SI
    if (UnitSystem == IP) then
      PSYCHROLIB_TOLERANCE = 0.001 * 9.0 / 5.0
    else
      PSYCHROLIB_TOLERANCE = 0.001
    end if
  end subroutine SetUnitSystem

  function GetUnitSystem() result(UnitSystem)
    !+ Return the system of units in use.
    integer :: UnitSystem
    UnitSystem = PSYCHROLIB_UNITS
  end function GetUnitSystem

  function isIP()
    !+ Check whether the system in use is IP or SI
    logical :: isIP
    if (PSYCHROLIB_UNITS == IP) then
      isIP = .true.
    else if (PSYCHROLIB_UNITS == SI) then
      isIP = .false.
    else
      error stop "The system of units has not been defined."
    end if
  end function isIP


  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
  ! Conversion between temperature units
  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

  function GetTRankineFromTFahrenheit(TFahrenheit) result(TRankine)
    !+ Utility function to convert temperature to degree Rankine (°R)
    !+ given temperature in degree Fahrenheit (°F).
    !+ Reference: ASHRAE Handbook - Fundamentals (2017) ch. 1 section 3

    real, intent(in)  :: TFahrenheit
      !+ Temperature in degree Fahrenheit
    real              :: TRankine
      !+ Temperature in degree Rankine

    TRankine = TFahrenheit + ZERO_FAHRENHEIT_AS_RANKINE
  end function GetTRankineFromTFahrenheit

  function GetTFahrenheitFromTRankine(TRankine) result(TFahrenheit)
    !+ Utility function to convert temperature to degree Fahrenheit (°F)
    !+ given temperature in degree Rankine (°R).
    !+ Reference: ASHRAE Handbook - Fundamentals (2017) ch. 1 section 3

    real, intent(in)  :: TRankine
      !+ Temperature in degree Rankine
    real              :: TFahrenheit
      !+ Temperature in degree Fahrenheit

    TFahrenheit = TRankine - ZERO_FAHRENHEIT_AS_RANKINE
  end function GetTFahrenheitFromTRankine

  function GetTKelvinFromTCelsius(TCelsius) result(TKelvin)
    !+ Utility function to convert temperature to Kelvin (K)
    !+ given temperature in degree Celsius (°C).
    !+ Reference: ASHRAE Handbook - Fundamentals (2017) ch. 1 section 3

    real, intent(in)  :: TCelsius
      !+ Temperature in degree Celsius
    real              :: TKelvin
      !+ Tempearatyre in Kelvin

    TKelvin = TCelsius + ZERO_CELSIUS_AS_KELVIN
  end function GetTKelvinFromTCelsius

  function GetTCelsiusFromTKelvin(TKelvin) result(TCelsius)
    !+ Utility function to convert temperature to degree Celsius (°C)
    !+ given temperature in Kelvin (K).
    !+ Reference: ASHRAE Handbook - Fundamentals (2017) ch. 1 section 3

    real, intent(in)  :: TKelvin
      !+ Tempearatyre in Kelvin
    real              :: TCelsius
      !+ Temperature in degree Celsius

    TCelsius = TKelvin - ZERO_CELSIUS_AS_KELVIN
  end function GetTCelsiusFromTKelvin


  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
  ! Conversions between dew point, wet bulb, and relative humidity
  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

  function GetTWetBulbFromTDewPoint(TDryBulb, TDewPoint, Pressure) result(TWetBulb)
    !+ Return wet-bulb temperature given dry-bulb temperature, dew-point temperature, and pressure.
    !+ References:
    !+ ASHRAE Handbook - Fundamentals (2017) ch. 1

    real, intent(in)  :: TDryBulb
      !+ Dry-bulb temperature in °F [IP] or °C [SI]
    real, intent(in)  :: TDewPoint
      !+ Dew-point temperature in °F [IP] or °C [SI]
    real, intent(in)  :: Pressure
      !+ Atmospheric pressure in Psi [IP] or Pa [SI]
    real              :: TWetBulb
      !+ Wet-bulb temperature in °F [IP] or °C [SI]
    real              :: HumRatio
      !+ Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]

    if (TDewPoint > TDryBulb) then
      error stop "Error: dew point temperature is above dry bulb temperature"
    end if

    HumRatio = GetHumRatioFromTDewPoint(TDewPoint, Pressure)
    TWetBulb = GetTWetBulbFromHumRatio(TDryBulb, HumRatio, Pressure)
  end function GetTWetBulbFromTDewPoint

  function GetTWetBulbFromRelHum(TDryBulb, RelHum, Pressure) result(TWetBulb)
    !+ Return wet-bulb temperature given dry-bulb temperature, relative humidity, and pressure.
    !+ References:
    !+ ASHRAE Handbook - Fundamentals (2017) ch. 1

    real, intent(in)  ::  TDryBulb
      !+ Dry-bulb temperature in °F [IP] or °C [SI]
    real, intent(in)  ::  RelHum
      !+ Relative humidity in range [0, 1]
    real, intent(in)  ::  Pressure
      !+ Atmospheric pressure in Psi [IP] or Pa [SI]
    real              ::  TWetBulb
      !+ Wet-bulb temperature in °F [IP] or °C [SI]
    real              ::  HumRatio
      !+ Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]

    if (RelHum < 0.0 .or. RelHum > 1.0) then
      error stop "Error: relative humidity is outside range [0,1]"
    end if

    HumRatio = GetHumRatioFromRelHum(TDryBulb, RelHum, Pressure)
    TWetBulb = GetTWetBulbFromHumRatio(TDryBulb, HumRatio, Pressure)
  end function GetTWetBulbFromRelHum

  function GetRelHumFromTDewPoint(TDryBulb, TDewPoint) result(RelHum)
    !+ Return relative humidity given dry-bulb temperature and dew-point temperature.
    !+ References:
    !+ ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 22

    real, intent(in)  ::  TDryBulb
      !+ Dry-bulb temperature in °F [IP] or °C [SI]
    real, intent(in)  ::  TDewPoint
      !+ Dew-point temperature in °F [IP] or °C [SI]
    real              ::  RelHum
      !+ Relative humidity in range [0, 1]
    real              ::  VapPres
      !+ Partial pressure of water vapor in moist air in Psi [IP] or Pa [SI]
    real              ::  SatVapPres
      !+ Vapor pressure of saturated air in Psi [IP] or Pa [SI]

    if (TDewPoint > TDryBulb) then
      error stop "Error: dew point temperature is above dry bulb temperature"
    end if

    VapPres     = GetSatVapPres(TDewPoint)
    SatVapPres  = GetSatVapPres(TDryBulb)
    RelHum      = VapPres / SatVapPres
  end function GetRelHumFromTDewPoint

  function GetRelHumFromTWetBulb(TDryBulb, TWetBulb, Pressure) result(RelHum)
    !+ Return relative humidity given dry-bulb temperature, wet bulb temperature and pressure.
    !+ References:
    !+ ASHRAE Handbook - Fundamentals (2017) ch. 1

    real, intent(in)  ::  TDryBulb
      !+ Dry-bulb temperature in °F [IP] or °C [SI]
    real, intent(in)  ::  TWetBulb
      !+ Wet-bulb temperature in °F [IP] or °C [SI]
    real, intent(in)  ::  Pressure
      !+ Atmospheric pressure in Psi [IP] or Pa [SI]
    real              ::  RelHum
      !+ Relative humidity in range [0, 1]
    real              ::  HumRatio
      !+ Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]

    if (TWetBulb > TDryBulb) then
      error stop "Error: wet bulb temperature is above dry bulb temperature"
    end if

    HumRatio = GetHumRatioFromTWetBulb(TDryBulb, TWetBulb, Pressure)
    RelHum   = GetRelHumFromHumRatio(TDryBulb, HumRatio, Pressure)
  end function GetRelHumFromTWetBulb

  function GetTDewPointFromRelHum(TDryBulb, RelHum) result(TDewPoint)
    !+ Return dew-point temperature given dry-bulb temperature and relative humidity.
    !+ References:
    !+ ASHRAE Handbook - Fundamentals (2017) ch. 1

    real, intent(in)  ::  TDryBulb
      !+ Dry-bulb temperature in °F [IP] or °C [SI]
    real, intent(in)  ::  RelHum
      !+ Relative humidity in range [0, 1]
    real              ::  TDewPoint
      !+ Dew-point temperature in °F [IP] or °C [SI]
    real              ::  VapPres
      !+ Partial pressure of water vapor in moist air in Psi [IP] or Pa [SI]

    if (RelHum < 0.0 .or. RelHum > 1.0) then
      error stop "Error: relative humidity is outside range [0,1]"
    end if

    VapPres   = GetVapPresFromRelHum(TDryBulb, RelHum)
    TDewPoint = GetTDewPointFromVapPres(TDryBulb, VapPres)
  end function GetTDewPointFromRelHum

  function GetTDewPointFromTWetBulb(TDryBulb, TWetBulb, Pressure) result(TDewPoint)
    !+ Return dew-point temperature given dry-bulb temperature, wet-bulb temperature, and pressure.
    !+ References:
    !+ ASHRAE Handbook - Fundamentals (2017) ch. 1

    real, intent(in)  ::  TDryBulb
      !+ Dry-bulb temperature in °F [IP] or °C [SI]
    real, intent(in)  ::  TWetBulb
      !+ Wet-bulb temperature in °F [IP] or °C [SI]
    real, intent(in)  ::  Pressure
      !+ Atmospheric pressure in Psi [IP] or Pa [SI]
    real              ::  TDewPoint
      !+ Dew-point temperature in °F [IP] or °C [SI]
    real              ::  HumRatio
      !+ Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]

    if (TWetBulb > TDryBulb) then
      error stop "Error: wet bulb temperature is above dry bulb temperature"
    end if

    HumRatio  = GetHumRatioFromTWetBulb(TDryBulb, TWetBulb, Pressure)
    TDewPoint = GetTDewPointFromHumRatio(TDryBulb, HumRatio, Pressure)
  end function GetTDewPointFromTWetBulb


  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
  ! Conversions between dew point, or relative humidity and vapor pressure
  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

  function GetVapPresFromRelHum(TDryBulb, RelHum) result(VapPres)
    !+ Return partial pressure of water vapor as a function of relative humidity and temperature.
    !+ References:
    !+ ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 12, 22

    real, intent(in)  ::  TDryBulb
      !+ Dry-bulb temperature in °F [IP] or °C [SI]
    real, intent(in)  ::  RelHum
      !+ Relative humidity in range [0, 1]
    real              ::  VapPres
      !+ Partial pressure of water vapor in moist air in Psi [IP] or Pa [SI]

    if (RelHum < 0.0 .or. RelHum > 1.0) then
      error stop "Error: relative humidity is outside range [0,1]"
    end if

    VapPres = RelHum * GetSatVapPres(TDryBulb)
  end function GetVapPresFromRelHum

  function GetRelHumFromVapPres(TDryBulb, VapPres) result(RelHum)
    !+ Return relative humidity given dry-bulb temperature and vapor pressure.
    !+ References:
    !+ ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 12, 22

    real, intent(in)  ::  TDryBulb
      !+ Dry-bulb temperature in °F [IP] or °C [SI]
    real, intent(in)  ::  VapPres
      !+ Partial pressure of water vapor in moist air in Psi [IP] or Pa [SI]
    real              ::  RelHum
      !+ Relative humidity in range [0, 1]

    if (VapPres < 0.0) then
      error stop "Error: partial pressure of water vapor in moist air cannot be negative"
    end if

    RelHum = VapPres / GetSatVapPres(TDryBulb)
  end function GetRelHumFromVapPres

  function dLnPws_(TDryBulb) result(dLnPws)
    !+ Helper function returning the derivative of the natural log of the saturation vapor pressure
    !+ as a function of dry-bulb temperature.
    !+ Reference:
    !+ ASHRAE Handbook - Fundamentals (2017) ch. 1  eqn 5

    real, intent(in)  ::  TDryBulb
      !+ Dry-bulb temperature in °F [IP] or °C [SI]
    real              ::  dLnPws
      !+ Derivative of natural log of vapor pressure of saturated air in Psi [IP] or Pa [SI]
    real              ::  T
      !+ Dry bulb temperature in R [IP] or K [SI]

    if (isIP()) then

      T = GetTRankineFromTFahrenheit(TDryBulb)

      if (TDryBulb <= TRIPLE_POINT_WATER_IP) then
        dLnPws = 1.0214165E+04 / T**2 - 5.3765794E-03 + 2 * 1.9202377E-07 * T &
                 + 3 * 3.5575832E-10 * T**2 - 4 * 9.0344688E-14 * T**3 + 4.1635019 / T
      else
        dLnPws = 1.0440397E+04 / T**2 - 2.7022355E-02 + 2 * 1.2890360E-05 * T &
                 - 3 * 2.4780681E-09 * T**2 + 6.5459673 / T
      end if

    else

      T = GetTKelvinFromTCelsius(TDryBulb)

      if (TDryBulb <= TRIPLE_POINT_WATER_SI) then
        dLnPws = 5.6745359E+03 / T**2 - 9.677843E-03 + 2 * 6.2215701E-07 * T &
                 + 3 * 2.0747825E-09 * T**2 - 4 * 9.484024E-13 * T**3 + 4.1635019 / T
      else
        dLnPws = 5.8002206E+03 / T**2 - 4.8640239E-02 + 2 * 4.1764768E-05 * T &
                 - 3 * 1.4452093E-08 * T**2 + 6.5459673 / T
      end if
    end if
  end function dLnPws_

  function GetTDewPointFromVapPres(TDryBulb, VapPres) result(TDewPoint)
    !+ Return dew-point temperature given dry-bulb temperature and vapor pressure.
    !+ References:
    !+ ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn. 5 and 6
    !+ Notes:
    !+ The dew point temperature is solved by inverting the equation giving water vapor pressure
    !+ at saturation from temperature rather than using the regressions provided
    !+ by ASHRAE (eqn. 37 and 38) which are much less accurate and have a
    !+ narrower range of validity.
    !+ The Newton-Raphson (NR) method is used on the logarithm of water vapour
    !+ pressure as a function of temperature, which is a very smooth function
    !+ Convergence is usually achieved in 3 to 5 iterations.
    !+ TDryBulb is not really needed here, just used for convenience.

    real, intent(in)    ::  TDryBulb
      !+ Dry-bulb temperature in °F [IP] or °C [SI]
    real, intent(in)    ::  VapPres
      !+ Partial pressure of water vapor in moist air in Psi [IP] or Pa [SI]
    real                ::  TDewPoint
      !+ Dew-point temperature in °F [IP] or °C [SI]
    real                ::  lnVP
      !+ Natural logarithm of partial pressure of water vapor pressure in moist air
    real                ::  d_lnVP
      !+ Derivative of function, calculated numerically
    real                ::  lnVP_iter
      !+ Value of log of vapor water pressure used in NR calculation
    real                ::  TDewPoint_iter
      !+ Value of TDewPoint used in NR calculation
    real, dimension(2)  ::  BOUNDS
      !+ Valid temperature range in °F [IP] or °C [SI]
    integer             :: index
      !+ Index used in the calculation

    ! Bounds and step size as a function of the system of units
    if (isIP()) then
        BOUNDS(1) = -148.0
        BOUNDS(2) =  392.0
    else
        BOUNDS(1) = -100.0
        BOUNDS(2) =  200.0
    end if

    ! Validity check -- bounds outside which a solution cannot be found
    if (VapPres < GetSatVapPres(BOUNDS(1)) .or. VapPres > GetSatVapPres(BOUNDS(2))) then
      error stop "Error: partial pressure of water vapor is outside range of validity of equations"
    end if

    ! We use NR to approximate the solution.
    TDewPoint = TDryBulb
    lnVP = log(VapPres)
    index = 1

    do while (.true.)
      TDewPoint_iter = TDewPoint ! TDewPoint_iter used in NR calculation
      lnVP_iter = log(GetSatVapPres(TDewPoint_iter))

      ! Derivative of function, calculated analytically
      d_lnVP = dLnPws_(TDewPoint_iter)

      ! New estimate, bounded by the search domain defined above
      TDewPoint = TDewPoint_iter - (lnVP_iter - lnVP) / d_lnVP
      TDewPoint = max(TDewPoint, BOUNDS(1))
      TDewPoint = min(TDewPoint, BOUNDS(2))

      if (abs(TDewPoint - TDewPoint_iter) <= PSYCHROLIB_TOLERANCE) then
        exit
      end if

      if (index > MAX_ITER_COUNT) then
        error stop "Convergence not reached in GetTDewPointFromVapPres. Stopping."
      end if

      index = index + 1
    end do

  TDewPoint = min(TDewPoint, TDryBulb)
  end function GetTDewPointFromVapPres

  function GetVapPresFromTDewPoint(TDewPoint) result(VapPres)
    !+ Return vapor pressure given dew point temperature.
    !+ References:
    !+ ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 36

    real, intent(in)  ::  TDewPoint
      !+ Dew-point temperature in °F [IP] or °C [SI]
    real              ::  VapPres
      !+ Partial pressure of water vapor in moist air in Psi [IP] or Pa [SI]

    VapPres = GetSatVapPres(TDewPoint)
  end function GetVapPresFromTDewPoint


  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
  ! Conversions from wet-bulb temperature, dew-point temperature, or relative humidity to humidity ratio
  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

  function GetTWetBulbFromHumRatio(TDryBulb, HumRatio, Pressure) result(TWetBulb)
    !+ Return wet-bulb temperature given dry-bulb temperature, humidity ratio, and pressure.
    !+ References:
    !+ ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 33 and 35 solved for Tstar

    real, intent(in)  ::  TDryBulb
      !+ Dry-bulb temperature in °F [IP] or °C [SI]
    real, intent(in)  ::  HumRatio
      !+ Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]
    real, intent(in)  ::  Pressure
      !+ Atmospheric pressure in Psi [IP] or Pa [SI]
    real              ::  TWetBulb
      !+ Wet-bulb temperature in °F [IP] or °C [SI]
    real              ::  TDewPoint
      !+ TDewPoint : Dew-point temperature in °F [IP] or °C [SI]
    real              ::  TWetBulbSup
      !+ Upper value of wet bulb temperature in bissection method (initial guess is from dry bulb temperature) in °F [IP] or °C [SI]
    real              ::  TWetBulbInf
      !+ Lower value of wet bulb temperature in bissection method (initial guess is from dew point temperature) in °F [IP] or °C [SI]
    real              ::  Wstar
      !+ Humidity ratio at temperature Tstar in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]
    real              ::  BoundedHumRatio
      !+ Humidity ratio bounded to MIN_HUM_RATIO
    integer           ::  index
      !+ index used in iteration

    if (HumRatio < 0.0) then
      error stop "Error: humidity ratio cannot be negative"
    end if
    BoundedHumRatio = max(HumRatio, MIN_HUM_RATIO)

    TDewPoint = GetTDewPointFromHumRatio(TDryBulb, BoundedHumRatio, Pressure)

    ! Initial guesses
    TWetBulbSup = TDryBulb
    TWetBulbInf = TDewPoint
    TWetBulb = (TWetBulbInf + TWetBulbSup) / 2.0

    index = 1
    ! Bisection loop
    do while ((TWetBulbSup - TWetBulbInf) > PSYCHROLIB_TOLERANCE)

    ! Compute humidity ratio at temperature Tstar
    Wstar = GetHumRatioFromTWetBulb(TDryBulb, TWetBulb, Pressure)

    ! Get new bounds
    if (Wstar > BoundedHumRatio) then
      TWetBulbSup = TWetBulb
    else
      TWetBulbInf = TWetBulb
    end if

    ! New guess of wet bulb temperature
    TWetBulb = (TWetBulbSup + TWetBulbInf) / 2.0

      if (index > MAX_ITER_COUNT) then
        error stop "Convergence not reached in GetTWetBulbFromHumRatio. Stopping."
      end if

    index = index + 1
    end do
  end function GetTWetBulbFromHumRatio

  function GetHumRatioFromTWetBulb(TDryBulb, TWetBulb, Pressure) result(HumRatio)
    !+ Return humidity ratio given dry-bulb temperature, wet-bulb temperature, and pressure.
    !+ References:
    !+ ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 33 and 35

    real, intent(in)  ::  TDryBulb
      !+ Dry-bulb temperature in °F [IP] or °C [SI]
    real, intent(in)  ::  TWetBulb
      !+ Wet-bulb temperature in °F [IP] or °C [SI]
    real, intent(in)  ::  Pressure
      !+ Atmospheric pressure in Psi [IP] or Pa [SI]
    real              ::  HumRatio
      !+ Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]
    real              ::  Wsstar
      !+ Humidity ratio at temperature Tstar in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]

    if (TWetBulb > TDryBulb) then
      error stop "Error: wet bulb temperature is above dry bulb temperature"
    end if

    Wsstar = GetSatHumRatio(TWetBulb, Pressure)

    if (isIP()) then
      if (TWetBulb >= FREEZING_POINT_WATER_IP) then
        HumRatio = ((1093.0 - 0.556 * TWetBulb) * Wsstar - 0.240 * (TDryBulb - TWetBulb))   &
                   / (1093.0 + 0.444 * TDryBulb - TWetBulb)
      else
        HumRatio = ((1220.0 - 0.04 * TWetBulb) * Wsstar - 0.240 * (TDryBulb - TWetBulb))    &
                   / (1220.0 + 0.444 * TDryBulb - 0.48 * TWetBulb)
      end if
    else
      if (TWetBulb >= FREEZING_POINT_WATER_SI) then
        HumRatio = ((2501.0 - 2.326 * TWetBulb) * Wsstar - 1.006 * (TDryBulb - TWetBulb))   &
                   / (2501.0 + 1.86 * TDryBulb - 4.186 * TWetBulb)
      else
           HumRatio = ((2830.0 - 0.24 * TWetBulb) * Wsstar - 1.006 * (TDryBulb - TWetBulb)) &
                      / (2830.0 + 1.86 * TDryBulb - 2.1 * TWetBulb)
      end if
    end if

    ! Validity check.
    HumRatio = max(HumRatio, MIN_HUM_RATIO)
  end function GetHumRatioFromTWetBulb

  function GetHumRatioFromRelHum(TDryBulb, RelHum, Pressure) result(HumRatio)
    !+ Return humidity ratio given dry-bulb temperature, relative humidity, and pressure.
    !+ Reference:
    !+ ASHRAE Handbook - Fundamentals (2017) ch. 1

    real, intent(in)  ::  TDryBulb
      !+ Dry-bulb temperature in °F [IP] or °C [SI]
    real, intent(in)  ::  RelHum
      !+ Relative humidity in range [0, 1]
    real, intent(in)  ::  Pressure
      !+ Atmospheric pressure in Psi [IP] or Pa [SI]
    real              ::  HumRatio
      !+ Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]
    real              ::  VapPres
      !+ Partial pressure of water vapor in moist air in Psi [IP] or Pa [SI]

    if (RelHum < 0.0 .or. RelHum > 1.0) then
      error stop "Error: relative humidity is outside range [0,1]"
    end if

    VapPres   = GetVapPresFromRelHum(TDryBulb, RelHum)
    HumRatio  = GetHumRatioFromVapPres(VapPres, Pressure)
  end function GetHumRatioFromRelHum

  function GetRelHumFromHumRatio(TDryBulb, HumRatio, Pressure) result(RelHum)
  !+ Return relative humidity given dry-bulb temperature, humidity ratio, and pressure.
  !+ Reference:
  !+ ASHRAE Handbook - Fundamentals (2017) ch. 1

    real, intent(in)  ::  TDryBulb
      !+ Dry-bulb temperature in °F [IP] or °C [SI]
    real, intent(in)  ::  HumRatio
      !+ Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]
    real, intent(in)  ::  Pressure
      !+ Atmospheric pressure in Psi [IP] or Pa [SI]
    real              ::  RelHum
      !+ Relative humidity in range [0, 1]
    real              ::  VapPres
      !+ Partial pressure of water vapor in moist air in Psi [IP] or Pa [SI]

    if (HumRatio < 0.0) then
      error stop "Error: humidity ratio cannot be negative"
    end if

    VapPres = GetVapPresFromHumRatio(HumRatio, Pressure)
    RelHum  = GetRelHumFromVapPres(TDryBulb, VapPres)
  end function GetRelHumFromHumRatio

  function GetHumRatioFromTDewPoint(TDewPoint, Pressure) result(HumRatio)
    !+ Return humidity ratio given dew-point temperature and pressure.
    !+ Reference:
    !+ ASHRAE Handbook - Fundamentals (2017) ch. 1

    real, intent(in)  ::  TDewPoint
      !+ Dew-point temperature in °F [IP] or °C [SI]
    real, intent(in)  ::  Pressure
      !+ Atmospheric pressure in Psi [IP] or Pa [SI]
    real              ::  HumRatio
      !+ Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]
    real              ::  VapPres
      !+ Partial pressure of water vapor in moist air in Psi [IP] or Pa [SI]

    VapPres   = GetSatVapPres(TDewPoint)
    HumRatio  = GetHumRatioFromVapPres(VapPres, Pressure)
  end function GetHumRatioFromTDewPoint

  function GetTDewPointFromHumRatio(TDryBulb, HumRatio, Pressure) result(TDewPoint)
    !+ Return dew-point temperature given dry-bulb temperature, humidity ratio, and pressure.
    !+ Reference:
    !+ ASHRAE Handbook - Fundamentals (2017) ch. 1

    real, intent(in)  ::  TDryBulb
      !+ Dry-bulb temperature in °F [IP] or °C [SI]
    real, intent(in)  ::  HumRatio
      !+ Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]
    real, intent(in)  ::  Pressure
      !+ Atmospheric pressure in Psi [IP] or Pa [SI]
    real              ::  TDewPoint
      !+ Dew-point temperature in °F [IP] or °C [SI]
    real              ::  VapPres
      !+ Partial pressure of water vapor in moist air in Psi [IP] or Pa [SI]

    if (HumRatio < 0.0) then
      error stop "Error: humidity ratio cannot be negative"
    end if

    VapPres   = GetVapPresFromHumRatio(HumRatio, Pressure)
    TDewPoint = GetTDewPointFromVapPres(TDryBulb, VapPres)
  end function GetTDewPointFromHumRatio


  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
  ! Conversions between humidity ratio and vapor pressure
  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

  function GetHumRatioFromVapPres(VapPres, Pressure) result(HumRatio)
    !+ Return humidity ratio given water vapor pressure and atmospheric pressure.
    !+ Reference:
    !+ ASHRAE Fundamentals (2005) ch. 6 eqn. 22;
    !+ ASHRAE Fundamentals (2009) ch. 1 eqn. 22.

    real, intent(in)  ::  VapPres
      !+ Partial pressure of water vapor in moist air in Psi [IP] or Pa [SI]
    real, intent(in)  ::  Pressure
      !+ Atmospheric pressure in Psi [IP] or Pa [SI]
    real              ::  HumRatio
      !+ Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]

    if (VapPres < 0.0) then
      error stop "Error: partial pressure of water vapor in moist air cannot be negative"
    end if

    HumRatio = 0.621945 * VapPres / (Pressure-VapPres)

    ! Validity check.
    HumRatio = max(HumRatio, MIN_HUM_RATIO)
  end function GetHumRatioFromVapPres

  function GetVapPresFromHumRatio(HumRatio, Pressure) result(VapPres)
    !+ Return vapor pressure given humidity ratio and pressure.
    !+ Reference:
    !+ ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 20 solved for pw

    real, intent(in)  ::  HumRatio
      !+ Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]
    real, intent(in)  ::  Pressure
      !+ Atmospheric pressure in Psi [IP] or Pa [SI]
    real              ::  VapPres
      !+ Partial pressure of water vapor in moist air in Psi [IP] or Pa [SI]
    real              ::  BoundedHumRatio
      !+ Humidity ratio bounded to MIN_HUM_RATIO

    if (HumRatio < 0.0) then
      error stop "Error: humidity ratio is negative"
    end if
    BoundedHumRatio = max(HumRatio, MIN_HUM_RATIO)

    VapPres = Pressure * BoundedHumRatio / (0.621945 + BoundedHumRatio)
  end function GetVapPresFromHumRatio


  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
  ! Conversions between humidity ratio and specific humidity
  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

  function GetSpecificHumFromHumRatio(HumRatio) result(SpecificHum)
    !+ Return the specific humidity from humidity ratio (aka mixing ratio).
    !+ Reference:
    !+ ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 9b

    real, intent(in) :: HumRatio
      !+ Humidity ratio in lb_H₂O lb_Dry_Air⁻¹ [IP] or kg_H₂O kg_Dry_Air⁻¹ [SI]
    real             :: SpecificHum
      !+ Specific humidity in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]
    real              ::  BoundedHumRatio
      !+ Humidity ratio bounded to MIN_HUM_RATIO

    if (HumRatio < 0.0) then
      error stop "Error: humidity ratio cannot be negative"
    end if
    BoundedHumRatio = max(HumRatio, MIN_HUM_RATIO)

    SpecificHum = BoundedHumRatio / (1.0 + BoundedHumRatio)
  end function GetSpecificHumFromHumRatio

  function GetHumRatioFromSpecificHum(SpecificHum) result(HumRatio)
    !+ Return the humidity ratio (aka mixing ratio) from specific humidity.
    !+ Reference:
    !+ ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 9b (solved for humidity ratio)

    real, intent(in)  :: SpecificHum
      !+ Specific humidity in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]
    real              :: HumRatio
      !+ Humidity ratio in lb_H₂O lb_Dry_Air⁻¹ [IP] or kg_H₂O kg_Dry_Air⁻¹ [SI]

    if (SpecificHum < 0.0 .or. SpecificHum >= 1.0) then
      error stop "Error: specific humidity is outside range [0, 1["
    end if

    HumRatio = SpecificHum / (1.0 - SpecificHum)

    ! Validity check.
    HumRatio = max(HumRatio, MIN_HUM_RATIO)
  end function GetHumRatioFromSpecificHum


  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
  ! Dry Air Calculations
  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

  function GetDryAirEnthalpy(TDryBulb) result(DryAirEnthalpy)
    !+ Return dry-air enthalpy given dry-bulb temperature.
    !+ Reference:
    !+ ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 28

    real, intent(in)  ::  TDryBulb
      !+ Dry-bulb temperature in °F [IP] or °C [SI]
    real              ::  DryAirEnthalpy
      !+ Dry air enthalpy in Btu lb⁻¹ [IP] or J kg⁻¹ [SI]

    if (isIP()) then
      DryAirEnthalpy = 0.240 * TDryBulb
    else
      DryAirEnthalpy = 1006 * TDryBulb
    end if
  end function GetDryAirEnthalpy

  function GetDryAirDensity(TDryBulb, Pressure) result(DryAirDensity)
    !+ Return dry-air density given dry-bulb temperature and pressure.
    !+ Reference:
    !+ ASHRAE Handbook - Fundamentals (2017) ch. 1
    !+ Notes:
    !+ Eqn 14 for the perfect gas relationship for dry air.
    !+ Eqn 1 for the universal gas constant.
    !+ The factor 144 in IP is for the conversion of Psi = lb in⁻² to lb ft⁻².

    real, intent(in)  ::  TDryBulb
      !+ Dry-bulb temperature in °F [IP] or °C [SI]
    real, intent(in)  ::  Pressure
      !+ Atmospheric pressure in Psi [IP] or Pa [SI]
    real              ::  DryAirDensity
      !+ Dry air density in lb ft⁻³ [IP] or kg m⁻³ [SI]

    if (isIP()) then
      DryAirDensity = (144 * Pressure) / R_DA_IP / GetTRankineFromTFahrenheit(TDryBulb)
    else
      DryAirDensity = Pressure / R_DA_SI / GetTKelvinFromTCelsius(TDryBulb)
    end if
  end function GetDryAirDensity

  function GetDryAirVolume(TDryBulb, Pressure) result(DryAirVolume)
    !+ Return dry-air volume given dry-bulb temperature and pressure.
    !+ Reference:
    !+ ASHRAE Handbook - Fundamentals (2017) ch. 1
    !+ Notes:
    !+ Eqn 14 for the perfect gas relationship for dry air.
    !+ Eqn 1 for the universal gas constant.
    !+ The factor 144 in IP is for the conversion of Psi = lb in⁻² to lb ft⁻².

    real, intent(in)  ::  TDryBulb
      !+ Dry-bulb temperature in °F [IP] or °C [SI]
    real, intent(in)  ::  Pressure
      !+ Atmospheric pressure in Psi [IP] or Pa [SI]
    real              ::  DryAirVolume
      !+ Dry air volume in ft³ lb⁻¹ [IP] or in m³ kg⁻¹ [SI]

    if (isIP()) then
      DryAirVolume = GetTRankineFromTFahrenheit(TDryBulb) * R_DA_IP / (144 * Pressure)
    else
      DryAirVolume = GetTKelvinFromTCelsius(TDryBulb) * R_DA_SI / Pressure
    end if
  end function GetDryAirVolume

  function GetTDryBulbFromEnthalpyAndHumRatio(MoistAirEnthalpy, HumRatio) result(TDryBulb)
    !+ Return dry bulb temperature from enthalpy and humidity ratio.
    !+ Reference:
    !+ ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 30
    !+ Notes:
    !+ Based on the `GetMoistAirEnthalpy` function, rearranged for temperature.

    real, intent(in)  ::  MoistAirEnthalpy
      !+ Moist air enthalpy in Btu lb⁻¹ [IP] or J kg⁻¹
    real, intent(in)  ::  HumRatio
      !+ Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]
    real              ::  TDryBulb
      !+ Dry-bulb temperature in °F [IP] or °C [SI]
    real              ::  BoundedHumRatio
      !+ Humidity ratio bounded to MIN_HUM_RATIO

    if (HumRatio < 0.0) then
      error stop "Error: humidity ratio is negative"
    end if
    BoundedHumRatio = max(HumRatio, MIN_HUM_RATIO)

    if (isIP()) then
      TDryBulb  = (MoistAirEnthalpy - 1061.0 * BoundedHumRatio) / (0.240 + 0.444 * BoundedHumRatio)
    else
      TDryBulb  = (MoistAirEnthalpy / 1000.0 - 2501.0 * BoundedHumRatio) / (1.006 + 1.86 * BoundedHumRatio)
    end if
  end function GetTDryBulbFromEnthalpyAndHumRatio

  function GetHumRatioFromEnthalpyAndTDryBulb(MoistAirEnthalpy, TDryBulb) result(HumRatio)
    !+ Return humidity ratio from enthalpy and dry-bulb temperature.
    !+ Reference:
    !+ ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 30
    !+ Notes:
    !+ Based on the `GetMoistAirEnthalpy` function, rearranged for humidity ratio.

    real, intent(in)  ::  MoistAirEnthalpy
      !+ Moist air enthalpy in Btu lb⁻¹ [IP] or J kg⁻¹
    real, intent(in)  ::  TDryBulb
      !+ Dry-bulb temperature in °F [IP] or °C [SI]
    real              ::  HumRatio
      !+ Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]

    if (isIP()) then
      HumRatio  = (MoistAirEnthalpy - 0.240 * TDryBulb) / (1061.0 + 0.444 * TDryBulb)
    else
      HumRatio  = (MoistAirEnthalpy / 1000.0 - 1.006 * TDryBulb) / (2501.0 + 1.86 * TDryBulb)
    end if

    ! Validity check.
    HumRatio = max(HumRatio, MIN_HUM_RATIO)
  end function GetHumRatioFromEnthalpyAndTDryBulb


  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
  ! Saturated Air Calculations
  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

  function GetSatVapPres(TDryBulb) result(SatVapPres)
    !+ Return saturation vapor pressure given dry-bulb temperature.
    !+ Reference:
    !+ ASHRAE Handbook - Fundamentals (2017) ch. 1  eqn 5
    !+ Important note: the ASHRAE formulae are defined above and below the freezing point but have
    !+ a discontinuity at the freezing point. This is a small inaccuracy on ASHRAE's part: the formulae
    !+ should be defined above and below the triple point of water (not the feezing point) in which case 
    !+ the discontinuity vanishes. It is essential to use the triple point of water otherwise function
    !+ GetTDewPointFromVapPres, which inverts the present function, does not converge properly around
    !+ the freezing point.

    real, intent(in)  ::  TDryBulb
      !+ Dry-bulb temperature in °F [IP] or °C [SI]
    real              ::  SatVapPres
      !+ Vapor pressure of saturated air in Psi [IP] or Pa [SI]
    real              ::  LnPws
      !+ Log of Vapor Pressure of saturated air (dimensionless)
    real              ::  T
      !+ Dry bulb temperature in R [IP] or K [SI]

    if (isIP()) then
      if (TDryBulb < -148.0 .or. TDryBulb > 392.0) then
        error stop "Error: dry bulb temperature must be in range [-148, 392]°F"
      end if

      T = GetTRankineFromTFahrenheit(TDryBulb)

      if (TDryBulb <= TRIPLE_POINT_WATER_IP) then
        LnPws = (-1.0214165E+04 / T - 4.8932428 - 5.3765794E-03 * T + 1.9202377E-07 * T**2    &
                + 3.5575832E-10 * T**3 - 9.0344688E-14 * T**4 + 4.1635019 * log(T))
      else
        LnPws = -1.0440397E+04 / T - 1.1294650E+01 - 2.7022355E-02* T + 1.2890360E-05 * T**2  &
                - 2.4780681E-09 * T**3 + 6.5459673 * log(T)
      end if

      else
        if (TDryBulb < -100.0 .or. TDryBulb > 200.0) then
          error stop "Error: dry bulb temperature must be in range [-100, 200]°C"
        end if

        T = GetTKelvinFromTCelsius(TDryBulb)

        if (TDryBulb <= TRIPLE_POINT_WATER_SI) then
          LnPws = -5.6745359E+03 / T + 6.3925247 - 9.677843E-03 * T + 6.2215701E-07 * T**2    &
                  + 2.0747825E-09 * T**3 - 9.484024E-13 * T**4 + 4.1635019 * log(T)
        else
          LnPws = -5.8002206E+03 / T + 1.3914993 - 4.8640239E-02 * T + 4.1764768E-05 * T**2   &
                  - 1.4452093E-08 * T**3 + 6.5459673 * log(T)
        end if
      end if

    SatVapPres = exp(LnPws)
  end function GetSatVapPres

  function GetSatHumRatio(TDryBulb, Pressure) result(SatHumRatio)
    !+ Return humidity ratio of saturated air given dry-bulb temperature and pressure.
    !+ Reference:
    !+ ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 36, solved for W

    real, intent(in)  ::  TDryBulb
      !+ Dry-bulb temperature in °F [IP] or °C [SI]
    real, intent(in)  ::  Pressure
      !+ Atmospheric pressure in Psi [IP] or Pa [SI]
    real              ::  SatHumRatio
      !+ Humidity ratio of saturated air in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]
    real              ::  SatVaporPres
      !+ Vapor pressure of saturated air in in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]

    SatVaporPres  = GetSatVapPres(TDryBulb)
    SatHumRatio   = 0.621945 * SatVaporPres / (Pressure-SatVaporPres)

    ! Validity check.
    SatHumRatio = max(SatHumRatio, MIN_HUM_RATIO)
  end function GetSatHumRatio

  function GetSatAirEnthalpy(TDryBulb, Pressure) result(SatAirEnthalpy)
    !+ Return saturated air enthalpy given dry-bulb temperature and pressure.
    !+ Reference:
    !+ ASHRAE Handbook - Fundamentals (2017) ch. 1

    real, intent(in)  ::  TDryBulb
      !+ Dry-bulb temperature in °F [IP] or °C [SI]
    real, intent(in)  ::  Pressure
      !+ Atmospheric pressure in Psi [IP] or Pa [SI]
    real              ::  SatAirEnthalpy
      !+ Saturated air enthalpy in Btu lb⁻¹ [IP] or J kg⁻¹ [SI]

    SatAirEnthalpy = GetMoistAirEnthalpy(TDryBulb, GetSatHumRatio(TDryBulb, Pressure))
  end function GetSatAirEnthalpy


  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
  ! Moist Air Calculations
  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

  function GetVaporPressureDeficit(TDryBulb, HumRatio, Pressure) result(VaporPressureDeficit)
    !+ Return Vapor pressure deficit given dry-bulb temperature, humidity ratio, and pressure.
    !+ Reference:
    !+ Oke (1987) eqn 2.13a

    real, intent(in)  ::  TDryBulb
      !+ Dry-bulb temperature in °F [IP] or °C [SI]
    real, intent(in)  ::  HumRatio
      !+ Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]
    real, intent(in)  ::  Pressure
      !+ Atmospheric pressure in Psi [IP] or Pa [SI]
    real              ::  VaporPressureDeficit
      !+ Vapor pressure deficit in Psi [IP] or Pa [SI]
    real              ::  RelHum
      !+ Relative humidity in range [0, 1]

    if (HumRatio < 0.0) then
      error stop "Error: humidity ratio is negative"
    end if

    RelHum = GetRelHumFromHumRatio(TDryBulb, HumRatio, Pressure)
    VaporPressureDeficit = GetSatVapPres(TDryBulb) * (1.0 - RelHum)
  end function GetVaporPressureDeficit

  function GetDegreeOfSaturation(TDryBulb, HumRatio, Pressure) result(DegreeOfSaturation)
    !+ Return the degree of saturation (i.e humidity ratio of the air / humidity ratio of the air at saturation
    !+ at the same temperature and pressure) given dry-bulb temperature, humidity ratio, and atmospheric pressure.
    !+ Reference:
    !+ ASHRAE Handbook - Fundamentals (2009) ch. 1 eqn 12
    !+ Notes:
    !+ This definition is absent from the 2017 Handbook. Using 2009 version instead.

    real, intent(in)  ::  TDryBulb
      !+ Dry-bulb temperature in °F [IP] or °C [SI]
    real, intent(in)  ::  HumRatio
      !+ Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]
    real, intent(in)  ::  Pressure
      !+ Atmospheric pressure in Psi [IP] or Pa [SI]
    real              ::  DegreeOfSaturation
      !+ Degree of saturation in arbitrary unit
    real              ::  BoundedHumRatio
      !+ Humidity ratio bounded to MIN_HUM_RATIO

    if (HumRatio < 0.0) then
      error stop "Error: humidity ratio is negative"
    end if
    BoundedHumRatio = max(HumRatio, MIN_HUM_RATIO)

    DegreeOfSaturation = BoundedHumRatio / GetSatHumRatio(TDryBulb, Pressure)
  end function GetDegreeOfSaturation

  function GetMoistAirEnthalpy(TDryBulb, HumRatio) result(MoistAirEnthalpy)
    !+ Return moist air enthalpy given dry-bulb temperature and humidity ratio.
    !+ Reference:
    !+ ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 30

    real, intent(in)  ::  TDryBulb
      !+ Dry-bulb temperature in °F [IP] or °C [SI]
    real, intent(in)  ::  HumRatio
      !+ Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]
    real              ::  MoistAirEnthalpy
      !+ Moist air enthalpy in Btu lb⁻¹ [IP] or J kg⁻¹
    real              ::  BoundedHumRatio
      !+ Humidity ratio bounded to MIN_HUM_RATIO

    if (HumRatio < 0.0) then
      error stop "Error: humidity ratio is negative"
    end if
    BoundedHumRatio = max(HumRatio, MIN_HUM_RATIO)

    if (isIP()) then
        MoistAirEnthalpy = 0.240 * TDryBulb + BoundedHumRatio * (1061.0 + 0.444 * TDryBulb)
    else
        MoistAirEnthalpy = (1.006 * TDryBulb + BoundedHumRatio * (2501.0 + 1.86 * TDryBulb)) * 1000.0
    end if
  end function GetMoistAirEnthalpy

  function GetMoistAirVolume(TDryBulb, HumRatio, Pressure) result(MoistAirVolume)
    !+ Return moist air specific volume given dry-bulb temperature, humidity ratio, and pressure.
    !+ Reference:
    !+ ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 26
    !+ Notes:
    !+ In IP units, R_DA_IP / 144 equals 0.370486 which is the coefficient appearing in eqn 26
    !+ The factor 144 is for the conversion of Psi = lb in⁻² to lb ft⁻².

    real, intent(in)  ::  TDryBulb
      !+ Dry-bulb temperature in °F [IP] or °C [SI]
    real, intent(in)  ::  HumRatio
      !+ Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]
    real, intent(in)  ::  Pressure
      !+ Atmospheric pressure in Psi [IP] or Pa [SI]
    real              ::  MoistAirVolume
      !+ Specific volume of moist air in ft³ lb⁻¹ of dry air [IP] or in m³ kg⁻¹ of dry air [SI]
    real              ::  BoundedHumRatio
      !+ Humidity ratio bounded to MIN_HUM_RATIO

    if (HumRatio < 0.0) then
      error stop "Error: humidity ratio is negative"
    end if
    BoundedHumRatio = max(HumRatio, MIN_HUM_RATIO)

    if (isIP()) then
        MoistAirVolume = R_DA_IP * GetTRankineFromTFahrenheit(TDryBulb) * (1.0 + 1.607858 * BoundedHumRatio) / (144.0 * Pressure)
    else
        MoistAirVolume = R_DA_SI * GetTKelvinFromTCelsius(TDryBulb) * (1.0 + 1.607858 * BoundedHumRatio) / Pressure
    end if
  end function GetMoistAirVolume

  function GetTDryBulbFromMoistAirVolumeAndHumRatio(MoistAirVolume, HumRatio, Pressure) result(TDryBulb)
    !+ Return dry-bulb temperature given moist air specific volume, humidity ratio, and pressure.
    !+ Reference:
    !+ ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 26
    !+ Notes:
    !+ In IP units, R_DA_IP / 144 equals 0.370486 which is the coefficient appearing in eqn 26
    !+ The factor 144 is for the conversion of Psi = lb in⁻² to lb ft⁻².
    !+ Based on the `GetMoistAirVolume` function, rearranged for dry-bulb temperature.

    real, intent(in)  ::  MoistAirVolume
      !+ Specific volume of moist air in ft³ lb⁻¹ of dry air [IP] or in m³ kg⁻¹ of dry air [SI]
    real, intent(in)  ::  HumRatio
      !+ Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]
    real, intent(in)  ::  Pressure
      !+ Atmospheric pressure in Psi [IP] or Pa [SI]
    real              ::  TDryBulb
      !+ Dry-bulb temperature in °F [IP] or °C [SI]
    real              ::  BoundedHumRatio
      !+ Humidity ratio bounded to MIN_HUM_RATIO

    if (HumRatio < 0.0) then
      error stop "Error: humidity ratio is negative"
    end if
    BoundedHumRatio = max(HumRatio, MIN_HUM_RATIO)

    if (isIP()) then
      TDryBulb = GetTFahrenheitFromTRankine(MoistAirVolume * (144 * Pressure) &
                                / (R_DA_IP * (1 + 1.607858 * BoundedHumRatio)))
    else
      TDryBulb = GetTCelsiusFromTKelvin(MoistAirVolume * Pressure &
                                / (R_DA_SI * (1 + 1.607858 * BoundedHumRatio)))
    end if
  end function GetTDryBulbFromMoistAirVolumeAndHumRatio

  function GetMoistAirDensity(TDryBulb, HumRatio, Pressure) result(MoistAirDensity)
    !+ Return moist air density given humidity ratio, dry bulb temperature, and pressure.
    !+ Reference:
    !+ ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 11

    real, intent(in)  ::  TDryBulb
      !+ Dry-bulb temperature in °F [IP] or °C [SI]
    real, intent(in)  ::  HumRatio
      !+ Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]
    real, intent(in)  ::  Pressure
      !+ Atmospheric pressure in Psi [IP] or Pa [SI]
    real              ::  MoistAirDensity
      !+ Moist air density in lb ft⁻³ [IP] or kg m⁻³ [SI]
    real              ::  BoundedHumRatio
      !+ Humidity ratio bounded to MIN_HUM_RATIO

    if (HumRatio < 0.0) then
      error stop "Error: humidity ratio is negative"
    end if
    BoundedHumRatio = max(HumRatio, MIN_HUM_RATIO)

    MoistAirDensity = (1.0 + BoundedHumRatio) / GetMoistAirVolume(TDryBulb, BoundedHumRatio, Pressure)
  end function GetMoistAirDensity


  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
  ! Standard atmosphere
  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

  function GetStandardAtmPressure(Altitude) result(StandardAtmPressure)
    !+ Return standard atmosphere barometric pressure, given the elevation (altitude).
    !+ Reference:
    !+ ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 3

    real, intent(in)  ::  Altitude
      !+ Altitude in ft [IP] or m [SI]
    real              ::  StandardAtmPressure
      !+ Standard atmosphere barometric pressure in Psi [IP] or Pa [SI]

    if (isIP()) then
        StandardAtmPressure = 14.696 * (1.0 - 6.8754e-06 * Altitude)**5.2559
    else
        StandardAtmPressure = 101325 * (1 - 2.25577e-05 * Altitude)**5.2559
    end if
  end function GetStandardAtmPressure

  function GetStandardAtmTemperature(Altitude) result(StandardAtmTemperature)
    !+ Return standard atmosphere temperature, given the elevation (altitude).
    !+ Reference:
    !+ ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 4

    real, intent(in)  ::  Altitude
      !+ Altitude in ft [IP] or m [SI]
    real              ::  StandardAtmTemperature
      !+ Standard atmosphere dry-bulb temperature in °F [IP] or °C [SI]

    if (isIP()) then
        StandardAtmTemperature = 59.0 - 0.00356620 * Altitude
    else
        StandardAtmTemperature = 15.0 - 0.0065 * Altitude
    end if
  end function GetStandardAtmTemperature

  function GetSeaLevelPressure(StnPressure, Altitude, TDryBulb) result(SeaLevelPressure)
    !+ Return sea level pressure given dry-bulb temperature, altitude above sea level and pressure.
    !+ Reference:
    !+ Hess SL, Introduction to theoretical meteorology, Holt Rinehart and Winston, NY 1959,
    !+ ch. 6.5; Stull RB, Meteorology for scientists and engineers, 2nd edition,
    !+ Brooks/Cole 2000, ch. 1.
    !+ Notes:
    !+ The standard procedure for the US is to use for TDryBulb the average
    !+ of the current station temperature and the station temperature from 12 hours ago.

    real, intent(in)  ::  StnPressure
      !+ Observed station pressure in Psi [IP] or Pa [SI]
    real, intent(in)  ::  Altitude
      !+ Altitude in ft [IP] or m [SI]
    real, intent(in)  ::  TDryBulb
      !+ Dry-bulb temperature in °F [IP] or °C [SI]
    real              ::  SeaLevelPressure
      !+ Sea level barometric pressure in Psi [IP] or Pa [SI]
    real              ::  TColumn
      !+ Average temperature in column of air in R [IP] or K [SI]
    real              ::  H
      !+ scale height (dimensionless)

    if (isIP()) then
      ! Calculate average temperature in column of air, assuming a lapse rate
      ! of 3.6 °F/1000ft
      TColumn = TDryBulb + 0.0036 * Altitude / 2.0

      ! Determine the scale height
      H = 53.351 * GetTRankineFromTFahrenheit(TColumn)
    else
      ! Calculate average temperature in column of air, assuming a lapse rate
      ! of 6.5 °C/km
      TColumn = TDryBulb + 0.0065 * Altitude / 2.0

      ! Determine the scale height
      H = 287.055 * GetTKelvinFromTCelsius(TColumn) / 9.807
    end if

    ! Calculate the sea level pressure
    SeaLevelPressure = StnPressure * exp(Altitude / H)
  end function GetSeaLevelPressure

  function GetStationPressure(SeaLevelPressure, Altitude, TDryBulb) result(StationPressure)
    !+ Return station pressure from sea level pressure.
    !+ Reference:
    !+ See 'GetSeaLevelPressure'
    !+ Notes:
    !+ This function is just the inverse of 'GetSeaLevelPressure'.

    real, intent(in)  ::  SeaLevelPressure
      !+ Sea level barometric pressure in Psi [IP] or Pa [SI]
    real, intent(in)  ::  Altitude
      !+ Altitude in ft [IP] or m [SI]
    real, intent(in)  ::  TDryBulb
      !+ Dry-bulb temperature in °F [IP] or °C [SI]
    real              ::  StationPressure
      !+ Station pressure in Psi [IP] or Pa [SI]

    StationPressure = SeaLevelPressure / GetSeaLevelPressure(1.0, Altitude, TDryBulb)
  end function GetStationPressure


  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
  ! Functions to set all psychrometric values
  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

  subroutine CalcPsychrometricsFromTWetBulb(TDryBulb,           &
                                            TWetBulb,           &
                                            Pressure,           &
                                            HumRatio,           &
                                            TDewPoint,          &
                                            RelHum,             &
                                            VapPres,            &
                                            MoistAirEnthalpy,   &
                                            MoistAirVolume,     &
                                            DegreeOfSaturation)

    !+ Utility function to calculate humidity ratio, dew-point temperature, relative humidity,
    !+ vapour pressure, moist air enthalpy, moist air volume, and degree of saturation of air given
    !+ dry-bulb temperature, wet-bulb temperature, and pressure.

    real, intent(in)    ::  TDryBulb
      !+ Dry-bulb temperature in °F [IP] or °C [SI]
    real, intent(in)    ::  TWetBulb
      !+ Wet-bulb temperature in °F [IP] or °C [SI]
    real, intent(in)    ::  Pressure
      !+ Atmospheric pressure in Psi [IP] or Pa [SI]
    real, intent(out)   ::  HumRatio
      !+ Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]
    real, intent(out)   ::  TDewPoint
      !+ Dew-point temperature in °F [IP] or °C [SI]
    real, intent(out)   ::  RelHum
      !+ Relative humidity in range [0, 1]
    real, intent(out)   ::  VapPres
      !+ Partial pressure of water vapor in moist air in Psi [IP] or Pa [SI]
    real, intent(out)   ::  MoistAirEnthalpy
      !+ Moist air enthalpy in Btu lb⁻¹ [IP] or J kg⁻¹ [SI]
    real, intent(out)   ::  MoistAirVolume
      !+ Specific volume of moist air in ft³ lb⁻¹ [IP] or in m³ kg⁻¹ [SI]
    real, intent(out)   ::  DegreeOfSaturation
      !+ Degree of saturation [unitless]

    HumRatio            = GetHumRatioFromTWetBulb(TDryBulb, TWetBulb, Pressure)
    TDewPoint           = GetTDewPointFromHumRatio(TDryBulb, HumRatio, Pressure)
    RelHum              = GetRelHumFromHumRatio(TDryBulb, HumRatio, Pressure)
    VapPres             = GetVapPresFromHumRatio(HumRatio, Pressure)
    MoistAirEnthalpy    = GetMoistAirEnthalpy(TDryBulb, HumRatio)
    MoistAirVolume      = GetMoistAirVolume(TDryBulb, HumRatio, Pressure)
    DegreeOfSaturation  = GetDegreeOfSaturation(TDryBulb, HumRatio, Pressure)
  end subroutine CalcPsychrometricsFromTWetBulb

  subroutine CalcPsychrometricsFromTDewPoint(TDryBulb,           &
                                             TDewPoint,          &
                                             Pressure,           &
                                             HumRatio,           &
                                             TWetBulb,           &
                                             RelHum,             &
                                             VapPres,            &
                                             MoistAirEnthalpy,   &
                                             MoistAirVolume,     &
                                             DegreeOfSaturation)

    !+ Utility function to calculate humidity ratio, wet-bulb temperature, relative humidity,
    !+ vapour pressure, moist air enthalpy, moist air volume, and degree of saturation of air given
    !+ dry-bulb temperature, dew-point temperature, and pressure.

    real, intent(in)    ::  TDryBulb
      !+ Dry-bulb temperature in °F [IP] or °C [SI]
    real, intent(in)    ::  TDewPoint
      !+ Dew-point temperature in °F [IP] or °C [SI]
    real, intent(in)    ::  Pressure
      !+ Atmospheric pressure in Psi [IP] or Pa [SI]
    real, intent(out)   ::  HumRatio
      !+ Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]
    real, intent(out)   ::  TWetBulb
      !+ Wet-bulb temperature in °F [IP] or °C [SI]
    real, intent(out)   ::  RelHum
      !+ Relative humidity in range [0, 1]
    real, intent(out)   ::  VapPres
      !+ Partial pressure of water vapor in moist air in Psi [IP] or Pa [SI]
    real, intent(out)   ::  MoistAirEnthalpy
      !+ Moist air enthalpy in Btu lb⁻¹ [IP] or J kg⁻¹ [SI]
    real, intent(out)   ::  MoistAirVolume
      !+ Specific volume of moist air in ft³ lb⁻¹ [IP] or in m³ kg⁻¹ [SI]
    real, intent(out)   ::  DegreeOfSaturation
      !+ Degree of saturation [unitless]

    HumRatio            = GetHumRatioFromTDewPoint(TDewPoint, Pressure)
    TWetBulb            = GetTWetBulbFromHumRatio(TDryBulb, HumRatio, Pressure)
    RelHum              = GetRelHumFromHumRatio(TDryBulb, HumRatio, Pressure)
    VapPres             = GetVapPresFromHumRatio(HumRatio, Pressure)
    MoistAirEnthalpy    = GetMoistAirEnthalpy(TDryBulb, HumRatio)
    MoistAirVolume      = GetMoistAirVolume(TDryBulb, HumRatio, Pressure)
    DegreeOfSaturation  = GetDegreeOfSaturation(TDryBulb, HumRatio, Pressure)
  end subroutine CalcPsychrometricsFromTDewPoint

  subroutine CalcPsychrometricsFromRelHum(TDryBulb,           &
                                          RelHum,             &
                                          Pressure,           &
                                          HumRatio,           &
                                          TWetBulb,           &
                                          TDewPoint,          &
                                          VapPres,            &
                                          MoistAirEnthalpy,   &
                                          MoistAirVolume,     &
                                          DegreeOfSaturation)

    !+ Utility function to calculate humidity ratio, wet-bulb temperature, dew-point temperature,
    !+ vapour pressure, moist air enthalpy, moist air volume, and degree of saturation of air given
    !+ dry-bulb temperature, relative humidity and pressure.

    real, intent(in)    ::  TDryBulb
      !+ Dry-bulb temperature in °F [IP] or °C [SI]
    real, intent(in)    ::  RelHum
      !+ Relative humidity in range [0, 1]
    real, intent(in)    ::  Pressure
      !+ Atmospheric pressure in Psi [IP] or Pa [SI]
    real, intent(out)   ::  HumRatio
      !+ Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]
    real, intent(out)   ::  TWetBulb
      !+ Wet-bulb temperature in °F [IP] or °C [SI]
    real, intent(out)   ::  TDewPoint
      !+ Dew-point temperature in °F [IP] or °C [SI]
    real, intent(out)   ::  VapPres
      !+ Partial pressure of water vapor in moist air in Psi [IP] or Pa [SI]
    real, intent(out)   ::  MoistAirEnthalpy
      !+ Moist air enthalpy in Btu lb⁻¹ [IP] or J kg⁻¹ [SI]
    real, intent(out)   ::  MoistAirVolume
      !+ Specific volume of moist air in ft³ lb⁻¹ [IP] or in m³ kg⁻¹ [SI]
    real, intent(out)   ::  DegreeOfSaturation
      !+ Degree of saturation [unitless]

    HumRatio            = GetHumRatioFromRelHum(TDryBulb, RelHum, Pressure)
    TWetBulb            = GetTWetBulbFromHumRatio(TDryBulb, HumRatio, Pressure)
    TDewPoint           = GetTDewPointFromHumRatio(TDryBulb, HumRatio, Pressure)
    VapPres             = GetVapPresFromHumRatio(HumRatio, Pressure)
    MoistAirEnthalpy    = GetMoistAirEnthalpy(TDryBulb, HumRatio)
    MoistAirVolume      = GetMoistAirVolume(TDryBulb, HumRatio, Pressure)
    DegreeOfSaturation  = GetDegreeOfSaturation(TDryBulb, HumRatio, Pressure)
  end subroutine CalcPsychrometricsFromRelHum


end module psychrolib

Visual Basic, VBA

   1
   2
   3
   4
   5
   6
   7
   8
   9
  10
  11
  12
  13
  14
  15
  16
  17
  18
  19
  20
  21
  22
  23
  24
  25
  26
  27
  28
  29
  30
  31
  32
  33
  34
  35
  36
  37
  38
  39
  40
  41
  42
  43
  44
  45
  46
  47
  48
  49
  50
  51
  52
  53
  54
  55
  56
  57
  58
  59
  60
  61
  62
  63
  64
  65
  66
  67
  68
  69
  70
  71
  72
  73
  74
  75
  76
  77
  78
  79
  80
  81
  82
  83
  84
  85
  86
  87
  88
  89
  90
  91
  92
  93
  94
  95
  96
  97
  98
  99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
 126
 127
 128
 129
 130
 131
 132
 133
 134
 135
 136
 137
 138
 139
 140
 141
 142
 143
 144
 145
 146
 147
 148
 149
 150
 151
 152
 153
 154
 155
 156
 157
 158
 159
 160
 161
 162
 163
 164
 165
 166
 167
 168
 169
 170
 171
 172
 173
 174
 175
 176
 177
 178
 179
 180
 181
 182
 183
 184
 185
 186
 187
 188
 189
 190
 191
 192
 193
 194
 195
 196
 197
 198
 199
 200
 201
 202
 203
 204
 205
 206
 207
 208
 209
 210
 211
 212
 213
 214
 215
 216
 217
 218
 219
 220
 221
 222
 223
 224
 225
 226
 227
 228
 229
 230
 231
 232
 233
 234
 235
 236
 237
 238
 239
 240
 241
 242
 243
 244
 245
 246
 247
 248
 249
 250
 251
 252
 253
 254
 255
 256
 257
 258
 259
 260
 261
 262
 263
 264
 265
 266
 267
 268
 269
 270
 271
 272
 273
 274
 275
 276
 277
 278
 279
 280
 281
 282
 283
 284
 285
 286
 287
 288
 289
 290
 291
 292
 293
 294
 295
 296
 297
 298
 299
 300
 301
 302
 303
 304
 305
 306
 307
 308
 309
 310
 311
 312
 313
 314
 315
 316
 317
 318
 319
 320
 321
 322
 323
 324
 325
 326
 327
 328
 329
 330
 331
 332
 333
 334
 335
 336
 337
 338
 339
 340
 341
 342
 343
 344
 345
 346
 347
 348
 349
 350
 351
 352
 353
 354
 355
 356
 357
 358
 359
 360
 361
 362
 363
 364
 365
 366
 367
 368
 369
 370
 371
 372
 373
 374
 375
 376
 377
 378
 379
 380
 381
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
' PsychroLib (version 2.3.0) (https://github.com/psychrometrics/psychrolib)
' Copyright (c) 2018 D. Thevenard and D. Meyer for the current library implementation
' Copyright (c) 2017 ASHRAE Handbook — Fundamentals for ASHRAE equations and coefficients
' Licensed under the MIT License.
'
' psychrolib.vba
'
' Contains functions for calculating thermodynamic properties of gas-vapor mixtures
' and standard atmosphere suitable for most engineering, physical and meteorological
' applications.
'
' Most of the functions are an implementation of the formulae found in the
' 2017 ASHRAE Handbook - Fundamentals, in both International System (SI),
' and Imperial (IP) units. Please refer to the information included in
' each function for their respective reference.
'
' Example
'     ' Set the unit system, for example to SI (can be either ' SI'  or ' IP' )
'     ' by uncommenting the following line in the psychrolib module
'     Const PSYCHROLIB_UNITS = UnitSystem.SI
'
'     ' Calculate the dew point temperature for a dry bulb temperature of 25 C and a relative humidity of 80%
'     TDewPoint = GetTDewPointFromRelHum(25.0, 0.80)
'     Debug.Print(TDewPoint)
'     21.309397163661785
'
' Copyright
'     - For the current library implementation
'         Copyright (c) 2018 D. Thevenard and D. Meyer.
'     - For equations and coefficients published ASHRAE Handbook — Fundamentals, Chapter 1
'         Copyright (c) 2017 ASHRAE Handbook — Fundamentals (https://www.ashrae.org)
'
' License
'     MIT (https://github.com/psychrometrics/psychrolib/LICENSE.txt)
'
' Note from the Authors
'     We have made every effort to ensure that the code is adequate, however, we make no
'     representation with respect to its accuracy. Use at your own risk. Should you notice
'     an error, or if you have a suggestion, please notify us through GitHub at
'     https://github.com/psychrometrics/psychrolib/issues.
'

Option Explicit


'******************************************************************************************************
' IMPORTANT: Manually uncomment the system of units to use
'******************************************************************************************************

'Enumeration to define systems of units
Enum UnitSystem
  IP = 1
  SI = 2
End Enum

' Uncomment one of these two lines to define the system of units ("IP" or "SI")
'Const PSYCHROLIB_UNITS = UnitSystem.IP
'Const PSYCHROLIB_UNITS = UnitSystem.SI


'******************************************************************************************************
' Global constants
'******************************************************************************************************

Private Const ZERO_FAHRENHEIT_AS_RANKINE = 459.67   ' Zero degree Fahrenheit (°F) expressed as degree Rankine (°R).
                                                    'Reference: ASHRAE Handbook - Fundamentals (2017) ch. 39.

Private Const ZERO_CELSIUS_AS_KELVIN = 273.15       ' Zero degree Celsius (°C) expressed as Kelvin (K).
                                                    ' Reference: ASHRAE Handbook - Fundamentals (2017) ch. 39.

Private Const R_DA_IP = 53.35                 ' Universal gas constant for dry air (IP version) in ft lbf/lb_DryAir/R.

Private Const R_DA_SI = 287.042               ' Universal gas constant for dry air (SI version) in J/kg_DryAir/K.

Private Const MAX_ITER_COUNT = 100            ' Maximum number of iterations before exiting while loops.

Private Const MIN_HUM_RATIO = 1e-7            ' Minimum acceptable humidity ratio used/returned by any functions.
                                              ' Any value above 0 or below the MIN_HUM_RATIO will be reset to this value.

Private Const FREEZING_POINT_WATER_IP = 32.0  ' Freezing point of water, in °F

Private Const FREEZING_POINT_WATER_SI = 0.0   ' Freezing point of water, in °C

Private Const TRIPLE_POINT_WATER_IP = 32.018  ' Triple point of water, in °F

Private Const TRIPLE_POINT_WATER_SI = 0.01    ' Triple point of water, in °C

'******************************************************************************************************
' Helper functions
'******************************************************************************************************

Function GetUnitSystem() As UnitSystem
'
' This function returns the system of units currently in use (SI or IP).
'
' Args:
'        none
'
' Returns:
'        The system of units currently in use ('SI' or 'IP')
'
' Note:
'
'        If you get an error here, it's because you have not uncommented one of the two lines
'        defining PSYCHROLIB_UNITS (see Global Constants section)
'
    GetUnitSystem = PSYCHROLIB_UNITS

End Function

Private Function isIP() As Variant
'
' This function checks whether the system of units currently in use is IP or SI.
'
' Args:
'         none
'
' Returns:
'         True if IP, False if SI, and raises error if undefined
'
  If (PSYCHROLIB_UNITS = UnitSystem.IP) Then
    isIP = True
  ElseIf (PSYCHROLIB_UNITS = UnitSystem.SI) Then
    isIP = False
  Else
    MsgBox ("The system of units has not been defined.")
    isIP = CVErr(xlErrNA)
  End If

End Function

Private Function GetTol() As Variant
'
' This function returns the tolerance on temperatures used for iterative solving.
' The value is physically the same in IP or SI.
'
' Args:
'         none
'
' Returns:
'         Tolerance on temperatures
'
  If (PSYCHROLIB_UNITS = UnitSystem.IP) Then
    GetTol = 0.001 * 9 / 5
  Else
    GetTol = 0.001
  End If
End Function

Private Sub MyMsgBox(ByVal ErrMsg As String)
'
' Error message output
' Override this function with your own if needed, or comment its code out if you don't want to see the messages
'
' Message disabled by default
'  MsgBox (ErrMsg)

End Sub

Private Function Min(ByVal Num1 As Variant, ByVal Num2 As Variant) As Variant
'
' Min function to return minimum of two numbers
'
  If (Num1 <= Num2) Then
    Min = Num1
  Else
    Min = Num2
  End If

End Function

Private Function Max(ByVal Num1 As Variant, ByVal Num2 As Variant) As Variant
'
' Max function to return maximum of two numbers
'
  If (Num1 >= Num2) Then
    Max = Num1
  Else
    Max = Num2
  End If

End Function


'*****************************************************************************
' Conversions between temperature units
'*****************************************************************************

Function GetTRankineFromTFahrenheit(ByVal T_Fahrenheit As Variant) As Variant
'
' Utility function to convert temperature to degree Rankine (°R)
' given temperature in degree Fahrenheit (°F).
'
'Args:
'        T_Fahrenheit: Temperature in degree Fahrenheit (°F)
'
'Returns:
'        Temperature in degree Rankine (°R)
'
'Reference:
'        Reference: ASHRAE Handbook - Fundamentals (2017) ch. 1 section 3
'
'Notes:
'        Exact conversion.
'
  On Error GoTo ErrHandler

  GetTRankineFromTFahrenheit = (T_Fahrenheit + ZERO_FAHRENHEIT_AS_RANKINE)
  Exit Function

ErrHandler:
  GetTRankineFromTFahrenheit = CVErr(xlErrNA)

End Function

Function GetTFahrenheitFromTRankine(ByVal T_Rankine As Variant) As Variant
'
' Utility function to convert temperature to degree Fahrenheit (°F)
' given temperature in degree Rankine (°R).
'
'Args:
'        TRankine: Temperature in degree Rankine (°R)
'
'Returns:
'        Temperature in degree Fahrenheit (°F)
'
'Reference:
'        Reference: ASHRAE Handbook - Fundamentals (2017) ch. 1 section 3
'
'Notes:
'        Exact conversion.
'
  On Error GoTo ErrHandler

  GetTFahrenheitFromTRankine = (T_Rankine - ZERO_FAHRENHEIT_AS_RANKINE)
  Exit Function

ErrHandler:
  GetTFahrenheitFromTRankine = CVErr(xlErrNA)

End Function

Function GetTKelvinFromTCelsius(ByVal T_Celsius As Variant) As Variant
'
' Utility function to convert temperature to Kelvin (K)
' given temperature in degree Celsius (°C).
'
'Args:
'        TCelsius: Temperature in degree Celsius (°C)
'
'Returns:
'        Temperature in Kelvin (K)
'
'Reference:
'        Reference: ASHRAE Handbook - Fundamentals (2017) ch. 1 section 3
'
'Notes:
'        Exact conversion.
'
  On Error GoTo ErrHandler

  GetTKelvinFromTCelsius = (T_Celsius + ZERO_CELSIUS_AS_KELVIN)
  Exit Function

ErrHandler:
  GetTKelvinFromTCelsius = CVErr(xlErrNA)

End Function

Function GetTCelsiusFromTKelvin(ByVal T_Kelvin As Variant) As Variant
'
' Utility function to convert temperature to degree Celsius (°C)
' given temperature in Kelvin (K).
'
'Args:
'        TKelvin: Temperature in Kelvin (K)
'
'Returns:
'        Temperature in degree Celsius (°C)
'
'Reference:
'        Reference: ASHRAE Handbook - Fundamentals (2017) ch. 1 section 3
'
'Notes:
'        Exact conversion.
'
  On Error GoTo ErrHandler

  GetTCelsiusFromTKelvin = (T_Kelvin - ZERO_CELSIUS_AS_KELVIN)
  Exit Function

ErrHandler:
  GetTCelsiusFromTKelvin = CVErr(xlErrNA)

End Function


'******************************************************************************************************
' Conversions between dew point, wet bulb, and relative humidity
'******************************************************************************************************

Function GetTWetBulbFromTDewPoint(ByVal TDryBulb As Variant, ByVal TDewPoint As Variant, ByVal Pressure As Variant) As Variant
'
' Return wet-bulb temperature given dry-bulb temperature, dew-point temperature, and pressure.
'
' Args:
'        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
'        TDewPoint : Dew-point temperature in °F [IP] or °C [SI]
'        Pressure : Atmospheric pressure in Psi [IP] or Pa [SI]
'
' Returns:
'        Wet-bulb temperature in °F [IP] or °C [SI]
'
' Reference:
'        ASHRAE Handbook - Fundamentals (2017) ch. 1
'
  Dim HumRatio As Variant

  On Error GoTo ErrHandler

  If TDewPoint > TDryBulb Then
    MyMsgBox ("Dew point temperature is above dry bulb temperature")
    GoTo ErrHandler
  End If

  HumRatio = GetHumRatioFromTDewPoint(TDewPoint, Pressure)
  GetTWetBulbFromTDewPoint = GetTWetBulbFromHumRatio(TDryBulb, HumRatio, Pressure)
  Exit Function

ErrHandler:
  GetTWetBulbFromTDewPoint = CVErr(xlErrNA)

End Function

Function GetTWetBulbFromRelHum(ByVal TDryBulb As Variant, ByVal RelHum As Variant, ByVal Pressure As Variant) As Variant
'
' Return wet-bulb temperature given dry-bulb temperature, relative humidity, and pressure.
'
' Args:
'        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
'        RelHum : Relative humidity in range [0, 1]
'        Pressure : Atmospheric pressure in Psi [IP] or Pa [SI]
'
' Returns:
'        Wet-bulb temperature in °F [IP] or °C [SI]
'
' Reference:
'        ASHRAE Handbook - Fundamentals (2017) ch. 1
'
  Dim HumRatio As Variant

  On Error GoTo ErrHandler

  If (RelHum < 0 Or RelHum > 1) Then
    MyMsgBox ("Relative humidity is outside range [0,1]")
    GoTo ErrHandler
  End If

  HumRatio = GetHumRatioFromRelHum(TDryBulb, RelHum, Pressure)
  GetTWetBulbFromRelHum = GetTWetBulbFromHumRatio(TDryBulb, HumRatio, Pressure)
  Exit Function

ErrHandler:
  GetTWetBulbFromRelHum = CVErr(xlErrNA)

End Function

Function GetRelHumFromTDewPoint(ByVal TDryBulb As Variant, ByVal TDewPoint As Variant) As Variant
'
' Return relative humidity given dry-bulb temperature and dew-point temperature.
'
' Args:
'        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
'        TDewPoint : Dew-point temperature in °F [IP] or °C [SI]
'
' Returns:
'        Relative humidity in range [0, 1]
'
' Reference:
'        ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 22
'
  Dim VapPres As Variant
  Dim SatVapPres As Variant

  On Error GoTo ErrHandler

  If (TDewPoint > TDryBulb) Then
    MyMsgBox ("Dew point temperature is above dry bulb temperature")
    GoTo ErrHandler
  End If

  VapPres = GetSatVapPres(TDewPoint)
  SatVapPres = GetSatVapPres(TDryBulb)
  GetRelHumFromTDewPoint = VapPres / SatVapPres
  Exit Function

ErrHandler:
  GetRelHumFromTDewPoint = CVErr(xlErrNA)

End Function

Function GetRelHumFromTWetBulb(ByVal TDryBulb As Variant, ByVal TWetBulb As Variant, ByVal Pressure As Variant) As Variant
'
' Return relative humidity given dry-bulb temperature, wet bulb temperature and pressure.
'
' Args:
'        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
'        TWetBulb : Wet-bulb temperature in °F [IP] or °C [SI]
'        Pressure : Atmospheric pressure in Psi [IP] or Pa [SI]
'
' Returns:
'        Relative humidity in range [0, 1]
'
' Reference:
'        ASHRAE Handbook - Fundamentals (2017) ch. 1
'
  Dim HumRatio As Variant

  On Error GoTo ErrHandler

  If TWetBulb > TDryBulb Then
    MyMsgBox ("Wet bulb temperature is above dry bulb temperature")
    GoTo ErrHandler
  End If

  HumRatio = GetHumRatioFromTWetBulb(TDryBulb, TWetBulb, Pressure)
  GetRelHumFromTWetBulb = GetRelHumFromHumRatio(TDryBulb, HumRatio, Pressure)
  Exit Function

ErrHandler:
  GetRelHumFromTWetBulb = CVErr(xlErrNA)

End Function

Function GetTDewPointFromRelHum(ByVal TDryBulb As Variant, ByVal RelHum As Variant) As Variant
'
' Return dew-point temperature given dry-bulb temperature and relative humidity.
'
' Args:
'        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
'        RelHum: Relative humidity in range [0, 1]
'
' Returns:
'        Dew-point temperature in °F [IP] or °C [SI]
'
' Reference:
'        ASHRAE Handbook - Fundamentals (2017) ch. 1
'

  Dim VapPres As Variant

  On Error GoTo ErrHandler

  If RelHum < 0 Or RelHum > 1 Then
    MyMsgBox ("Relative humidity is outside range [0, 1]")
    GoTo ErrHandler
  End If

  VapPres = GetVapPresFromRelHum(TDryBulb, RelHum)
  GetTDewPointFromRelHum = GetTDewPointFromVapPres(TDryBulb, VapPres)
  Exit Function

ErrHandler:
  GetTDewPointFromRelHum = CVErr(xlErrNA)

End Function

Function GetTDewPointFromTWetBulb(ByVal TDryBulb As Variant, ByVal TWetBulb As Variant, ByVal Pressure As Variant) As Variant
'
' Return dew-point temperature given dry-bulb temperature, wet-bulb temperature, and pressure.
'
' Args:
'        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
'        TWetBulb : Wet-bulb temperature in °F [IP] or °C [SI]
'        Pressure : Atmospheric pressure in Psi [IP] or Pa [SI]
'
' Returns:
'        Dew-point temperature in °F [IP] or °C [SI]
'
' Reference:
'        ASHRAE Handbook - Fundamentals (2017) ch. 1
'
  Dim HumRatio As Variant

  On Error GoTo ErrHandler

  If TWetBulb > TDryBulb Then
    MyMsgBox ("Wet bulb temperature is above dry bulb temperature")
    GoTo ErrHandler
  End If

  HumRatio = GetHumRatioFromTWetBulb(TDryBulb, TWetBulb, Pressure)
  GetTDewPointFromTWetBulb = GetTDewPointFromHumRatio(TDryBulb, HumRatio, Pressure)
  Exit Function

ErrHandler:
  GetTDewPointFromTWetBulb = CVErr(xlErrNA)

End Function


'******************************************************************************************************
'  Conversions between dew point, or relative humidity and vapor pressure
'******************************************************************************************************

Function GetVapPresFromRelHum(ByVal TDryBulb As Variant, ByVal RelHum As Variant) As Variant
'
' Return partial pressure of water vapor as a function of relative humidity and temperature.
'
' Args:
'        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
'        RelHum : Relative humidity in range [0, 1]
'
' Returns:
'        Partial pressure of water vapor in moist air in Psi [IP] or Pa [SI]
'
' Reference:
'        ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 12, 22
'
  On Error GoTo ErrHandler

  If RelHum < 0 Or RelHum > 1 Then
    MyMsgBox ("Relative humidity is outside range [0, 1]")
    GoTo ErrHandler
  End If

  GetVapPresFromRelHum = RelHum * GetSatVapPres(TDryBulb)
  Exit Function

ErrHandler:
  GetVapPresFromRelHum = CVErr(xlErrNA)

End Function

Function GetRelHumFromVapPres(ByVal TDryBulb As Variant, ByVal VapPres As Variant) As Variant
' Return relative humidity given dry-bulb temperature and vapor pressure.
'
' Args:
'        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
'        VapPres: Partial pressure of water vapor in moist air in Psi [IP] or Pa [SI]
'
' Returns:
'        Relative humidity in range [0, 1]
'
' Reference:
'        ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 12, 22
'
  On Error GoTo ErrHandler

  If (VapPres < 0) Then
    MyMsgBox ("Partial pressure of water vapor in moist air is negative")
    GoTo ErrHandler
  End If

  GetRelHumFromVapPres = VapPres / GetSatVapPres(TDryBulb)
  Exit Function

ErrHandler:
  GetRelHumFromVapPres = CVErr(xlErrNA)

End Function


Private Function dLnPws_(TDryBulb As Variant) As Variant
'
'    Helper function returning the derivative of the natural log of the saturation vapor pressure
'    as a function of dry-bulb temperature.
'
' Args:
'        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
'
' Returns:
'        Derivative of natural log of vapor pressure of saturated air in Psi [IP] or Pa [SI]
'
' Reference:
'        ASHRAE Handbook - Fundamentals (2017) ch. 1  eqn 5 & 6
'
  Dim T As Variant
  If (isIP()) Then
    T = GetTRankineFromTFahrenheit(TDryBulb)
    If (TDryBulb <= TRIPLE_POINT_WATER_IP) Then
      dLnPws_ = 10214.165 / T ^ 2 - 0.0053765794 + 2 * 0.00000019202377 * T _
             + 3 * 3.5575832E-10 * T ^ 2 - 4 * 9.0344688E-14 * T ^ 3 + 4.1635019 / T
    Else
      dLnPws_ = 10440.397 / T ^ 2 - 0.027022355 + 2 * 0.00001289036 * T _
             - 3 * 2.4780681E-09 * T ^ 2 + 6.5459673 / T
    End If
  Else
    T = GetTKelvinFromTCelsius(TDryBulb)
    If (TDryBulb <= TRIPLE_POINT_WATER_SI) Then
      dLnPws_ = 5674.5359 / T ^ 2 - 0.009677843 + 2 * 0.00000062215701 * T _
             + 3 * 2.0747825E-09 * T ^ 2 - 4 * 9.484024E-13 * T ^ 3 + 4.1635019 / T
    Else
      dLnPws_ = 5800.2206 / T ^ 2 - 0.048640239 + 2 * 0.000041764768 * T _
             - 3 * 0.000000014452093 * T ^ 2 + 6.5459673 / T
    End If
  End If
End Function

Function GetTDewPointFromVapPres(ByVal TDryBulb As Variant, ByVal VapPres As Variant) As Variant
'
' Return dew-point temperature given dry-bulb temperature and vapor pressure.
'
' Args:
'        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
'        VapPres: Partial pressure of water vapor in moist air in Psi [IP] or Pa [SI]
'
' Returns:
'        Dew-point temperature in °F [IP] or °C [SI]
'
' Reference:
'        ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn. 5 and 6
'
' Notes:
'        The dew point temperature is solved by inverting the equation giving water vapor pressure
'        at saturation from temperature rather than using the regressions provided
'        by ASHRAE (eqn. 37 and 38) which are much less accurate and have a
'        narrower range of validity.
'        The Newton-Raphson (NR) method is used on the logarithm of water vapour
'        pressure as a function of temperature, which is a very smooth function
'        Convergence is usually achieved in 3 to 5 iterations.
'        TDryBulb is not really needed here, just used for convenience.
'
  Dim BOUNDS(2) As Variant
  Dim PSYCHROLIB_TOLERANCE As Variant

  If (isIP()) Then
    BOUNDS(1) = -148.
    BOUNDS(2) = 392.
  Else
    BOUNDS(1) = -100.
    BOUNDS(2) = 200.
  End If

  On Error GoTo ErrHandler

  If ((VapPres < GetSatVapPres(BOUNDS(1))) Or (VapPres > GetSatVapPres(BOUNDS(2)))) Then
    MyMsgBox ("Partial pressure of water vapor is outside range of validity of equations")
    GoTo ErrHandler
  End If

  PSYCHROLIB_TOLERANCE = GetTol()

  Dim TDewPoint As Variant
  Dim lnVP As Variant
  Dim d_lnVP As Variant
  Dim TDewPoint_iter As Variant
  Dim lnVP_iter
  Dim index As Variant
  index = 1

  ' We use NR to approximate the solution.
  ' First guess
  TDewPoint = TDryBulb        ' Calculated value of dew point temperatures, solved for iteratively
  lnVP = Log(VapPres)         ' Partial pressure of water vapor in moist air

  ' Iteration
  Do
    TDewPoint_iter = TDewPoint   ' Value of Tdp used in NR calculation
    lnVP_iter = Log(GetSatVapPres(TDewPoint_iter))

    ' Derivative of function, calculated analytically
    d_lnVP = dLnPws_(TDewPoint_iter)

    ' New estimate, bounded by domain of validity of eqn. 5 and 6 and by the freezing point
    TDewPoint = TDewPoint_iter - (lnVP_iter - lnVP) / d_lnVP
    TDewPoint = Max(TDewPoint, BOUNDS(1))
    TDewPoint = Min(TDewPoint, BOUNDS(2))

    If (index > MAX_ITER_COUNT) Then
      GoTo ErrHandler
    End If

    index = index + 1

  Loop While (Abs(TDewPoint - TDewPoint_iter) > PSYCHROLIB_TOLERANCE)

  TDewPoint = Min(TDewPoint, TDryBulb)
  GetTDewPointFromVapPres = TDewPoint
  Exit Function

ErrHandler:
  GetTDewPointFromVapPres = CVErr(xlErrNA)

End Function

Function GetVapPresFromTDewPoint(ByVal TDewPoint As Variant) As Variant
'
' Return vapor pressure given dew point temperature.
'
' Args:
'        TDewPoint : Dew-point temperature in °F [IP] or °C [SI]
'
' Returns:
'        Partial pressure of water vapor in moist air in Psi [IP] or Pa [SI]
'
' Reference:
'        ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 36
'
  On Error GoTo ErrHandler
  GetVapPresFromTDewPoint = GetSatVapPres(TDewPoint)
  Exit Function

ErrHandler:
  GetVapPresFromTDewPoint = CVErr(xlErrNA)

End Function


'******************************************************************************************************
'  Conversions from wet-bulb temperature, dew-point temperature, or relative humidity to humidity ratio
'******************************************************************************************************

Function GetTWetBulbFromHumRatio(ByVal TDryBulb As Variant, ByVal HumRatio As Variant, ByVal Pressure As Variant) As Variant
'
' Return wet-bulb temperature given dry-bulb temperature, humidity ratio, and pressure.
'
' Args:
'        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
'        HumRatio : Humidity ratio in lb_H2O/lb_Air [IP] or kg_H2O/kg_Air [SI]
'        Pressure : Atmospheric pressure in Psi [IP] or Pa [SI]
'
' Returns:
'        Wet-bulb temperature in °F [IP] or °C [SI]
'
' Reference:
'        ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 33 and 35 solved for Tstar
'

  ' Declarations
  Dim Wstar As Variant
  Dim TDewPoint As Variant, TWetBulb As Variant, TWetBulbSup As Variant, TWetBulbInf As Variant
  Dim Tol As Variant, BoundedHumRatio As Variant, index As Variant

  On Error GoTo ErrHandler

  If HumRatio < 0 Then
    MyMsgBox ("Humidity ratio cannot be negative")
    GoTo ErrHandler
  End If
  BoundedHumRatio = max(HumRatio, MIN_HUM_RATIO)

  TDewPoint = GetTDewPointFromHumRatio(TDryBulb, BoundedHumRatio, Pressure)

  ' Initial guesses
  TWetBulbSup = TDryBulb
  TWetBulbInf = TDewPoint
  TWetBulb = (TWetBulbInf + TWetBulbSup) / 2

  ' Bisection loop
  Tol = GetTol()
  index = 0
  While ((TWetBulbSup - TWetBulbInf) > Tol)

    ' Compute humidity ratio at temperature Tstar
    Wstar = GetHumRatioFromTWetBulb(TDryBulb, TWetBulb, Pressure)

    ' Get new bounds
    If (Wstar > BoundedHumRatio) Then
      TWetBulbSup = TWetBulb
    Else
      TWetBulbInf = TWetBulb
    End If

    ' New guess of wet bulb temperature
    TWetBulb = (TWetBulbSup + TWetBulbInf) / 2

    If (index > MAX_ITER_COUNT) Then
      GoTo ErrHandler
    End If

    index = index + 1
  Wend

  GetTWetBulbFromHumRatio = TWetBulb
  Exit Function

ErrHandler:
  GetTWetBulbFromHumRatio = CVErr(xlErrNA)

End Function

Function GetHumRatioFromTWetBulb(ByVal TDryBulb As Variant, ByVal TWetBulb As Variant, ByVal Pressure As Variant) As Variant
'
' Return humidity ratio given dry-bulb temperature, wet-bulb temperature, and pressure.
'
' Args:
'        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
'        TWetBulb : Wet-bulb temperature in °F [IP] or °C [SI]
'        Pressure : Atmospheric pressure in Psi [IP] or Pa [SI]
'
' Returns:
'        Humidity ratio in lb_H2O/lb_Air [IP] or kg_H2O/kg_Air [SI]
'
' Reference:
'        ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 33 and 35

  Dim Wsstar As Variant, HumRatio As Variant
  Wsstar = GetSatHumRatio(TWetBulb, Pressure)

  On Error GoTo ErrHandler

  If TWetBulb > TDryBulb Then
    MyMsgBox ("Wet bulb temperature is above dry bulb temperature")
    GoTo ErrHandler
  End If

  If isIP() Then
    If (TWetBulb >= FREEZING_POINT_WATER_IP) Then
      HumRatio = ((1093 - 0.556 * TWetBulb) * Wsstar - 0.24 * (TDryBulb - TWetBulb)) / (1093 + 0.444 * TDryBulb - TWetBulb)
    Else
      HumRatio = ((1220 - 0.04 * TWetBulb) * Wsstar - 0.24 * (TDryBulb - TWetBulb)) / (1220 + 0.444 * TDryBulb - 0.48 * TWetBulb)
    End If
  Else
    If (TWetBulb >= FREEZING_POINT_WATER_SI) Then
      HumRatio = ((2501 - 2.326 * TWetBulb) * Wsstar - 1.006 * (TDryBulb - TWetBulb)) / (2501 + 1.86 * TDryBulb - 4.186 * TWetBulb)
    Else
      HumRatio = ((2830 - 0.24 * TWetBulb) * Wsstar - 1.006 * (TDryBulb - TWetBulb)) / (2830 + 1.86 * TDryBulb - 2.1 * TWetBulb)
    End If
  End If
  ' Validity check.
  GetHumRatioFromTWetBulb = max(HumRatio, MIN_HUM_RATIO)
  Exit Function

ErrHandler:
  GetHumRatioFromTWetBulb = CVErr(xlErrNA)

End Function

Function GetHumRatioFromRelHum(ByVal TDryBulb As Variant, ByVal RelHum As Variant, ByVal Pressure As Variant) As Variant
'
' Return humidity ratio given dry-bulb temperature, relative humidity, and pressure.
'
' Args:
'        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
'        RelHum : Relative humidity in range [0, 1]
'        Pressure : Atmospheric pressure in Psi [IP] or Pa [SI]
'
' Returns:
'        Humidity ratio in lb_H2O/lb_Air [IP] or kg_H2O/kg_Air [SI]
'
' Reference:
'        ASHRAE Handbook - Fundamentals (2017) ch. 1
'
  Dim VapPres As Variant

  On Error GoTo ErrHandler

  If RelHum < 0 Or RelHum > 1 Then
    MyMsgBox ("Relative humidity is outside range [0, 1]")
    GoTo ErrHandler
  End If

  VapPres = GetVapPresFromRelHum(TDryBulb, RelHum)
  GetHumRatioFromRelHum = GetHumRatioFromVapPres(VapPres, Pressure)
  Exit Function

ErrHandler:
  GetHumRatioFromRelHum = CVErr(xlErrNA)

End Function

Function GetRelHumFromHumRatio(ByVal TDryBulb As Variant, ByVal HumRatio As Variant, ByVal Pressure As Variant) As Variant
'
'    Return relative humidity given dry-bulb temperature, humidity ratio, and pressure.
'
' Args:
'        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
'        HumRatio : Humidity ratio in lb_H2O/lb_Air [IP] or kg_H2O/kg_Air [SI]
'        Pressure : Atmospheric pressure in Psi [IP] or Pa [SI]
'
' Returns:
'        Relative humidity in range [0, 1]
'
' Reference:
'        ASHRAE Handbook - Fundamentals (2017) ch. 1
'
  Dim VapPres As Variant

  On Error GoTo ErrHandler

  If HumRatio < 0 Then
    MyMsgBox ("Humidity ratio is negative")
    GoTo ErrHandler
  End If

  VapPres = GetVapPresFromHumRatio(HumRatio, Pressure)
  GetRelHumFromHumRatio = GetRelHumFromVapPres(TDryBulb, VapPres)
  Exit Function

ErrHandler:
  GetRelHumFromHumRatio = CVErr(xlErrNA)

End Function


Function GetHumRatioFromTDewPoint(ByVal TDewPoint As Variant, ByVal Pressure As Variant) As Variant
'
' Return humidity ratio given dew-point temperature and pressure.
'
' Args:
'        TDewPoint : Dew-point temperature in °F [IP] or °C [SI]
'        Pressure : Atmospheric pressure in Psi [IP] or Pa [SI]
'
' Returns:
'        Humidity ratio in lb_H2O/lb_Air [IP] or kg_H2O/kg_Air [SI]
'
' Reference:
'        ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 13
'
  Dim VapPres As Variant

  On Error GoTo ErrHandler

  VapPres = GetSatVapPres(TDewPoint)
  GetHumRatioFromTDewPoint = GetHumRatioFromVapPres(VapPres, Pressure)
  Exit Function

ErrHandler:
  GetHumRatioFromTDewPoint = CVErr(xlErrNA)

End Function

Function GetTDewPointFromHumRatio(ByVal TDryBulb As Variant, ByVal HumRatio As Variant, ByVal Pressure As Variant) As Variant
'
' Return dew-point temperature given dry-bulb temperature, humidity ratio, and pressure.
'
' Args:
'        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
'        HumRatio : Humidity ratio in lb_H2O/lb_Air [IP] or kg_H2O/kg_Air [SI]
'        Pressure : Atmospheric pressure in Psi [IP] or Pa [SI]
'
' Returns:
'        Dew-point temperature in °F [IP] or °C [SI]
'
' Reference:
'        ASHRAE Handbook - Fundamentals (2017) ch. 1
'
  Dim VapPres As Variant

  On Error GoTo ErrHandler

  If HumRatio < 0 Then
    MyMsgBox ("Humidity ratio is negative")
    GoTo ErrHandler
  End If

  VapPres = GetVapPresFromHumRatio(HumRatio, Pressure)
  GetTDewPointFromHumRatio = GetTDewPointFromVapPres(TDryBulb, VapPres)
  Exit Function

ErrHandler:
  GetTDewPointFromHumRatio = CVErr(xlErrNA)
End Function


'******************************************************************************************************
'       Conversions between humidity ratio and vapor pressure
'******************************************************************************************************

Function GetHumRatioFromVapPres(ByVal VapPres As Variant, ByVal Pressure As Variant) As Variant
'
' Return humidity ratio given water vapor pressure and atmospheric pressure.
'
' Args:
'        VapPres : Partial pressure of water vapor in moist air in Psi [IP] or Pa [SI]
'        Pressure : Atmospheric pressure in Psi [IP] or Pa [SI]
'
' Returns:
'        Humidity ratio in lb_H2O/lb_Air [IP] or kg_H2O/kg_Air [SI]
'
' Reference:
'        ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 20
'
  Dim HumRatio As Variant

  On Error GoTo ErrHandler

  If VapPres < 0 Then
    MyMsgBox ("Partial pressure of water vapor in moist air is negative")
    GoTo ErrHandler
  End If

  HumRatio = 0.621945 * VapPres / (Pressure - VapPres)
  ' Validity check.
  GetHumRatioFromVapPres = max(HumRatio, MIN_HUM_RATIO)
  Exit Function

ErrHandler:
  GetHumRatioFromVapPres = CVErr(xlErrNA)

End Function

Function GetVapPresFromHumRatio(ByVal HumRatio As Variant, ByVal Pressure As Variant) As Variant
'
' Return vapor pressure given humidity ratio and pressure.
'
' Args:
'        HumRatio : Humidity ratio in lb_H2O/lb_Air [IP] or kg_H2O/kg_Air [SI]
'        Pressure : Atmospheric pressure in Psi [IP] or Pa [SI]
'
' Returns:
'        Partial pressure of water vapor in moist air in Psi [IP] or Pa [SI]
'
' Reference:
'        ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 20 solved for pw
'

  Dim VapPres As Variant, BoundedHumRatio As Variant

  On Error GoTo ErrHandler

  If HumRatio < 0 Then
    MyMsgBox ("Humidity ratio is negative")
    GoTo ErrHandler
  End If
  BoundedHumRatio = max(HumRatio, MIN_HUM_RATIO)

  VapPres = Pressure * BoundedHumRatio / (0.621945 + BoundedHumRatio)
  GetVapPresFromHumRatio = VapPres
  Exit Function

ErrHandler:
  GetVapPresFromHumRatio = CVErr(xlErrNA)

End Function


'******************************************************************************************************
'       Conversions between humidity ratio and specific humidity
'******************************************************************************************************

Function GetSpecificHumFromHumRatio(ByVal HumRatio As Variant) As Variant
'
' Return the specific humidity from humidity ratio (aka mixing ratio).
'
' Args:
'     HumRatio : Humidity ratio in lb_H₂O lb_Dry_Air⁻¹ [IP] or kg_H₂O kg_Dry_Air⁻¹ [SI]
'
' Returns:
'     Specific humidity in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]
'
' Reference:
'     ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 9b
'
'
  Dim SpecificHum As Variant

  On Error GoTo ErrHandler

  If (HumRatio < 0) Then
    MyMsgBox ("Humidity ratio is negative")
    GoTo ErrHandler
  End If

  SpecificHum = HumRatio / (1.0 + HumRatio)
  GetSpecificHumFromHumRatio = SpecificHum
  Exit Function

ErrHandler:
  GetSpecificHumFromHumRatio = CVErr(xlErrNA)

End Function

Function GetHumRatioFromSpecificHum(ByVal SpecificHum As Variant) As Variant
'
' Return the humidity ratio (aka mixing ratio) from specific humidity.
'
' Args:
'     SpecificHum : Specific Humidity in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]
'
' Returns:
'     Humidity ratio in lb_H₂O lb_Dry_Air⁻¹ [IP] or kg_H₂O kg_Dry_Air⁻¹ [SI]
'
' Reference:
'     ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 9b (solved for humidity ratio)
'
'
  Dim HumRatio as Variant

  On Error GoTo ErrHandler

  If (SpecificHum < 0 Or SpecificHum >= 1) Then
    MyMsgBox ("Specific humidity is outside range [0, 1[")
    GoTo ErrHandler
  End If

    HumRatio = SpecificHum / (1.0 - SpecificHum)
    GetHumRatioFromSpecificHum = max(HumRatio, MIN_HUM_RATIO)
  Exit Function

ErrHandler:
  GetHumRatioFromSpecificHum = CVErr(xlErrNA)

End Function


'******************************************************************************************************
' Dry Air Calculations
'******************************************************************************************************

Function GetDryAirEnthalpy(ByVal TDryBulb As Variant) As Variant
'
' Return dry-air enthalpy given dry-bulb temperature.
'
' Args:
'        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
'
' Returns:
'        Dry air enthalpy in Btu/lb [IP] or J/kg [SI]
'
' Reference:
'        ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 28
'
  On Error GoTo ErrHandler

  If (isIP()) Then
    GetDryAirEnthalpy = 0.24 * TDryBulb
  Else
    GetDryAirEnthalpy = 1006 * TDryBulb
  End If
  Exit Function

ErrHandler:
  GetDryAirEnthalpy = CVErr(xlErrNA)

End Function

Function GetDryAirDensity(ByVal TDryBulb As Variant, ByVal Pressure As Variant) As Variant
'
' Return dry-air density given dry-bulb temperature and pressure.
'
' Args:
'        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
'        Pressure : Atmospheric pressure in Psi [IP] or Pa [SI]
'
' Returns:
'        Dry air density in lb/ft³ [IP] or kg/m³ [SI]
'
' Reference:
'        ASHRAE Handbook - Fundamentals (2017) ch. 1
'
' Notes:
'        Eqn 14 for the perfect gas relationship for dry air.
'        Eqn 1 for the universal gas constant.
'        The factor 144 in IP is for the conversion of Psi = lb/in² to lb/ft².
'
  On Error GoTo ErrHandler

  If (isIP()) Then
    GetDryAirDensity = (144 * Pressure) / R_DA_IP / GetTRankineFromTFahrenheit(TDryBulb)
  Else
    GetDryAirDensity = Pressure / R_DA_SI / GetTKelvinFromTCelsius(TDryBulb)
  End If
  Exit Function

ErrHandler:
  GetDryAirDensity = CVErr(xlErrNA)

End Function

Function GetDryAirVolume(ByVal TDryBulb As Variant, ByVal Pressure As Variant) As Variant
'
' Return dry-air volume given dry-bulb temperature and pressure.
'
' Args:
'        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
'        Pressure : Atmospheric pressure in Psi [IP] or Pa [SI]
'
' Returns:
'        Dry air volume in ft³/lb [IP] or in m³/kg [SI]
'
' Reference:
'        ASHRAE Handbook - Fundamentals (2017) ch. 1
'
' Notes:
'        Eqn 14 for the perfect gas relationship for dry air.
'        Eqn 1 for the universal gas constant.
'        The factor 144 in IP is for the conversion of Psi = lb/in² to lb/ft².
'
  On Error GoTo ErrHandler

  If (isIP()) Then
    GetDryAirVolume = GetTRankineFromTFahrenheit(TDryBulb) * R_DA_IP / (144 * Pressure)
  Else:
    GetDryAirVolume = GetTKelvinFromTCelsius(TDryBulb) * R_DA_SI / Pressure
  End If
  Exit Function

ErrHandler:
  GetDryAirVolume = CVErr(xlErrNA)

End Function

Function GetTDryBulbFromEnthalpyAndHumRatio(ByVal MoistAirEnthalpy As Variant, ByVal HumRatio As Variant) As Variant
'
' Return dry bulb temperature from enthalpy and humidity ratio.
'
'
' Args:
'     MoistAirEnthalpy : Moist air enthalpy in Btu lb⁻¹ [IP] or J kg⁻¹
'     HumRatio : Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]
'
' Returns:
'     Dry-bulb temperature in °F [IP] or °C [SI]
'
' Reference:
'     ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 30
'
' Notes:
'     Based on the `GetMoistAirEnthalpy` function, rearranged for temperature.
'

  On Error GoTo ErrHandler

  If HumRatio < 0 Then
    MyMsgBox ("Humidity ratio is negative")
    GoTo ErrHandler
  End If

  If (isIP()) Then
    GetTDryBulbFromEnthalpyAndHumRatio = (MoistAirEnthalpy - 1061.0 * HumRatio) / (0.24 + 0.444 * HumRatio)
  Else:
    GetTDryBulbFromEnthalpyAndHumRatio = (MoistAirEnthalpy / 1000.0 - 2501.0 * HumRatio) / (1.006 + 1.86 * HumRatio)
  End If
  Exit Function

ErrHandler:
  GetTDryBulbFromEnthalpyAndHumRatio = CVErr(xlErrNA)

End Function

Function GetHumRatioFromEnthalpyAndTDryBulb(ByVal MoistAirEnthalpy As Variant, ByVal TDryBulb As Variant) As Variant
'
' Return humidity ratio from enthalpy and dry-bulb temperature.
'
'
' Args:
'     MoistAirEnthalpy : Moist air enthalpy in Btu lb⁻¹ [IP] or J kg⁻¹
'     TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
'
' Returns:
'     Humidity ratio in lb_H₂O lb_Air⁻¹ [IP] or kg_H₂O kg_Air⁻¹ [SI]
'
' Reference:
'     ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 30
'
' Notes:
'     Based on the `GetMoistAirEnthalpy` function, rearranged for humidity ratio.
'

  On Error GoTo ErrHandler

  If (isIP()) Then
    GetHumRatioFromEnthalpyAndTDryBulb = (MoistAirEnthalpy - 0.24 * TDryBulb) / (1061.0 + 0.444 * TDryBulb)
  Else:
    GetHumRatioFromEnthalpyAndTDryBulb = (MoistAirEnthalpy / 1000.0 - 1.006 * TDryBulb) / (2501.0 + 1.86 * TDryBulb)
  End If
  Exit Function

ErrHandler:
  GetHumRatioFromEnthalpyAndTDryBulb = CVErr(xlErrNA)

End Function


'******************************************************************************************************
' Saturated Air Calculations
'******************************************************************************************************

Function GetSatVapPres(ByVal TDryBulb As Variant) As Variant
'
' Return saturation vapor pressure given dry-bulb temperature.
'
' Args:
'        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
'
' Returns:
'        Vapor pressure of saturated air in Psi [IP] or Pa [SI]
'
' Reference:
'        ASHRAE Handbook - Fundamentals (2017) ch. 1  eqn 5 & 6
'        Important note: the ASHRAE formulae are defined above and below the freezing point but have
'        a discontinuity at the freezing point. This is a small inaccuracy on ASHRAE's part: the formulae
'        should be defined above and below the triple point of water (not the feezing point) in which case
'        the discontinuity vanishes. It is essential to use the triple point of water otherwise function
'        GetTDewPointFromVapPres, which inverts the present function, does not converge properly around
'        the freezing point.
'
  Dim LnPws As Variant, T As Variant

  On Error GoTo ErrHandler

  If (isIP()) Then
    If (TDryBulb < -148 Or TDryBulb > 392) Then
      MyMsgBox ("Dry bulb temperature is outside range [-148, 392] °F")
      GoTo ErrHandler
    End If

    T = GetTRankineFromTFahrenheit(TDryBulb)

    If (TDryBulb <= TRIPLE_POINT_WATER_IP) Then
      LnPws = (-10214.165 / T - 4.8932428 - 0.0053765794 * T + 0.00000019202377 * T ^ 2 _
            + 3.5575832E-10 * T ^ 3 - 9.0344688E-14 * T ^ 4 + 4.1635019 * Log(T))
    Else
      LnPws = -10440.397 / T - 11.29465 - 0.027022355 * T + 0.00001289036 * T ^ 2 _
            - 2.4780681E-09 * T ^ 3 + 6.5459673 * Log(T)
    End If

  Else
    If (TDryBulb < -100 Or TDryBulb > 200) Then
      MyMsgBox ("Dry bulb temperature is outside range [-100, 200] °C")
      GoTo ErrHandler
    End If

    T = GetTKelvinFromTCelsius(TDryBulb)

    If (TDryBulb <= TRIPLE_POINT_WATER_SI) Then
        LnPws = -5674.5359 / T + 6.3925247 - 0.009677843 * T + 0.00000062215701 * T ^ 2 _
              + 2.0747825E-09 * T ^ 3 - 9.484024E-13 * T ^ 4 + 4.1635019 * Log(T)
    Else
        LnPws = -5800.2206 / T + 1.3914993 - 0.048640239 * T + 0.000041764768 * T ^ 2 _
              - 0.000000014452093 * T ^ 3 + 6.5459673 * Log(T)
    End If
  End If

  GetSatVapPres = Exp(LnPws)
  Exit Function

ErrHandler:
  GetSatVapPres = CVErr(xlErrNA)

End Function

Function GetSatHumRatio(ByVal TDryBulb As Variant, ByVal Pressure As Variant) As Variant
'
' Return humidity ratio of saturated air given dry-bulb temperature and pressure.
'
' Args:
'        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
'        Pressure : Atmospheric pressure in Psi [IP] or Pa [SI]
'
' Returns:
'        Humidity ratio of saturated air in lb_H2O/lb_Air [IP] or kg_H2O/kg_Air [SI]
'
' Reference:
'        ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 36, solved for W
'
  Dim SatVaporPres As Variant, SatHumRatio As Variant

  On Error GoTo ErrHandler

  SatVaporPres = GetSatVapPres(TDryBulb)
  SatHumRatio = 0.621945 * SatVaporPres / (Pressure - SatVaporPres)
  GetSatHumRatio = max(SatHumRatio, MIN_HUM_RATIO)
  Exit Function

ErrHandler:
  GetSatHumRatio = CVErr(xlErrNA)

End Function

Function GetSatAirEnthalpy(ByVal TDryBulb As Variant, ByVal Pressure As Variant) As Variant
'
' Return saturated air enthalpy given dry-bulb temperature and pressure.
'
' Args:
'        TDryBulb: Dry-bulb temperature in °F [IP] or °C [SI]
'        Pressure: Atmospheric pressure in Psi [IP] or Pa [SI]
'
' Returns:
'        Saturated air enthalpy in Btu/lb [IP] or J/kg [SI]
'
' Reference:
'        ASHRAE Handbook - Fundamentals (2017) ch. 1
'
  On Error GoTo ErrHandler

  GetSatAirEnthalpy = GetMoistAirEnthalpy(TDryBulb, GetSatHumRatio(TDryBulb, Pressure))
  Exit Function

ErrHandler:
  GetSatAirEnthalpy = CVErr(xlErrNA)

End Function


'******************************************************************************************************
' Moist Air Calculations
'******************************************************************************************************


Function GetVaporPressureDeficit(ByVal TDryBulb As Variant, ByVal HumRatio As Variant, ByVal Pressure As Variant) As Variant
'
' Return Vapor pressure deficit given dry-bulb temperature, humidity ratio, and pressure.
'
' Args:
'        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
'        HumRatio : Humidity ratio in lb_H2O/lb_Air [IP] or kg_H2O/kg_Air [SI]
'        Pressure : Atmospheric pressure in Psi [IP] or Pa [SI]
'
' Returns:
'        Vapor pressure deficit in Psi [IP] or Pa [SI]
'
' Reference:
'        Oke (1987) eqn 2.13a
'
  Dim RelHum As Variant

  On Error GoTo ErrHandler

  If HumRatio < 0 Then
    MyMsgBox ("Humidity ratio is negative")
    GoTo ErrHandler
  End If

  RelHum = GetRelHumFromHumRatio(TDryBulb, HumRatio, Pressure)
  GetVaporPressureDeficit = GetSatVapPres(TDryBulb) * (1 - RelHum)
  Exit Function

ErrHandler:
  GetVaporPressureDeficit = CVErr(xlErrNA)

End Function

Function GetDegreeOfSaturation(ByVal TDryBulb As Variant, ByVal HumRatio As Variant, ByVal Pressure As Variant) As Variant
'
' Return the degree of saturation (i.e humidity ratio of the air / humidity ratio of the air at saturation
' at the same temperature and pressure) given dry-bulb temperature, humidity ratio, and atmospheric pressure.
'
' Args:
'        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
'        HumRatio : Humidity ratio in lb_H2O/lb_Air [IP] or kg_H2O/kg_Air [SI]
'        Pressure : Atmospheric pressure in Psi [IP] or Pa [SI]
'
' Returns:
'        Degree of saturation in arbitrary unit
'
' Reference:
'        ASHRAE Handbook - Fundamentals (2009) ch. 1 eqn 12
'
' Notes:
'        This definition is absent from the 2017 Handbook. Using 2009 version instead.
'
  Dim BoundedHumRatio As Variant

  On Error GoTo ErrHandler

  If HumRatio < 0 Then
    MyMsgBox ("Humidity ratio is negative")
    GoTo ErrHandler
  End If
  BoundedHumRatio = max(HumRatio, MIN_HUM_RATIO)

  GetDegreeOfSaturation = BoundedHumRatio / GetSatHumRatio(TDryBulb, Pressure)
  Exit Function

ErrHandler:
  GetDegreeOfSaturation = CVErr(xlErrNA)

End Function

Function GetMoistAirEnthalpy(ByVal TDryBulb As Variant, ByVal HumRatio As Variant) As Variant
'
' Return moist air enthalpy given dry-bulb temperature and humidity ratio.
'
' Args:
'        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
'        HumRatio : Humidity ratio in lb_H2O/lb_Air [IP] or kg_H2O/kg_Air [SI]
'
' Returns:
'        Moist air enthalpy in Btu/lb [IP] or J/kg
'
' Reference:
'        ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 30
'
  Dim BoundedHumRatio As Variant

  On Error GoTo ErrHandler

  If (HumRatio < 0) Then
    MyMsgBox ("Humidity ratio is negative")
    GoTo ErrHandler
  End If
  BoundedHumRatio = max(HumRatio, MIN_HUM_RATIO)

  If (isIP()) Then
    GetMoistAirEnthalpy = 0.24 * TDryBulb + BoundedHumRatio * (1061 + 0.444 * TDryBulb)
  Else
    GetMoistAirEnthalpy = (1.006 * TDryBulb + BoundedHumRatio * (2501 + 1.86 * TDryBulb)) * 1000
  End If
  Exit Function

ErrHandler:
  GetMoistAirEnthalpy = CVErr(xlErrNA)

End Function

Function GetMoistAirVolume(ByVal TDryBulb As Variant, ByVal HumRatio As Variant, ByVal Pressure As Variant) As Variant
'
' Return moist air specific volume given dry-bulb temperature, humidity ratio, and pressure.
'
' Args:
'        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
'        HumRatio : Humidity ratio in lb_H2O/lb_Air [IP] or kg_H2O/kg_Air [SI]
'        Pressure : Atmospheric pressure in Psi [IP] or Pa [SI]
'
' Returns:
'        Specific volume of moist air in ft³/lb of dry air [IP] or in m³/kg of dry air [SI]
'
' Reference:
'        ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 26
'
' Notes:
'        In IP units, R_DA_IP / 144 equals 0.370486 which is the coefficient appearing in eqn 26
'        The factor 144 is for the conversion of Psi = lb/in² to lb/ft².
'
  Dim BoundedHumRatio As Variant

  On Error GoTo ErrHandler

  If (HumRatio < 0) Then
    MyMsgBox ("Humidity ratio is negative")
    GoTo ErrHandler
  End If
  BoundedHumRatio = max(HumRatio, MIN_HUM_RATIO)

  If (isIP()) Then
    GetMoistAirVolume = R_DA_IP * GetTRankineFromTFahrenheit(TDryBulb) * (1 + 1.607858 * BoundedHumRatio) / (144 * Pressure)
  Else
    GetMoistAirVolume = R_DA_SI * GetTKelvinFromTCelsius(TDryBulb) * (1 + 1.607858 * BoundedHumRatio) / Pressure
  End If
  Exit Function

ErrHandler:
  GetMoistAirVolume = CVErr(xlErrNA)

End Function

Function GetTDryBulbFromMoistAirVolumeAndHumRatio(ByVal MoistAirVolume As Variant, ByVal HumRatio As Variant, ByVal Pressure As Variant) As Variant
'
' Return dry-bulb temperature given moist air specific volume, humidity ratio, and pressure.
'
' Args:
'        MoistAirVolume: Specific volume of moist air in ft³ lb⁻¹ of dry air [IP] or in m³ kg⁻¹ of dry air [SI]
'        HumRatio : Humidity ratio in lb_H2O/lb_Air [IP] or kg_H2O/kg_Air [SI]
'        Pressure : Atmospheric pressure in Psi [IP] or Pa [SI]
'
' Returns:
'        Specific volume of moist air in ft³/lb of dry air [IP] or in m³/kg of dry air [SI]
'
' Reference:
'        ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 26
'
' Notes:
'        In IP units, R_DA_IP / 144 equals 0.370486 which is the coefficient appearing in eqn 26
'        The factor 144 is for the conversion of Psi = lb/in² to lb/ft².
'        Based on the `GetMoistAirVolume` function, rearranged for dry-bulb temperature.
'
  Dim BoundedHumRatio As Variant

  On Error GoTo ErrHandler

  If (HumRatio < 0) Then
    MyMsgBox ("Humidity ratio is negative")
    GoTo ErrHandler
  End If
  BoundedHumRatio = max(HumRatio, MIN_HUM_RATIO)

  If (isIP()) Then
    GetTDryBulbFromMoistAirVolumeAndHumRatio = GetTFahrenheitFromTRankine(MoistAirVolume * (144 * Pressure) / (R_DA_IP * (1 + 1.607858 * BoundedHumRatio)))
  Else
    GetTDryBulbFromMoistAirVolumeAndHumRatio = GetTCelsiusFromTKelvin(MoistAirVolume * Pressure / (R_DA_SI * (1 + 1.607858 * BoundedHumRatio)))
  End If
  Exit Function

ErrHandler:
  GetTDryBulbFromMoistAirVolumeAndHumRatio = CVErr(xlErrNA)

End Function

Function GetMoistAirDensity(ByVal TDryBulb As Variant, ByVal HumRatio As Variant, ByVal Pressure As Variant) As Variant
'
' Return moist air density given humidity ratio, dry bulb temperature, and pressure.
'
' Args:
'        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
'        HumRatio : Humidity ratio in lb_H2O/lb_Air [IP] or kg_H2O/kg_Air [SI]
'        Pressure : Atmospheric pressure in Psi [IP] or Pa [SI]
'
' Returns:
'        MoistAirDensity: Moist air density in lb/ft³ [IP] or kg/m³ [SI]
'
' Reference:
'        ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 11
'
  Dim MoistAirVolume As Variant, BoundedHumRatio As Variant

  On Error GoTo ErrHandler

  If (HumRatio < 0) Then
    MyMsgBox ("Humidity ratio is negative")
    GoTo ErrHandler
  End If
  BoundedHumRatio = max(HumRatio, MIN_HUM_RATIO)

  MoistAirVolume = GetMoistAirVolume(TDryBulb, BoundedHumRatio, Pressure)
  GetMoistAirDensity = (1 + BoundedHumRatio) / MoistAirVolume
  Exit Function

ErrHandler:
  GetMoistAirDensity = CVErr(xlErrNA)

End Function


'******************************************************************************************************
' Standard atmosphere
'******************************************************************************************************

Function GetStandardAtmPressure(ByVal Altitude As Variant) As Variant
'
' Return standard atmosphere barometric pressure, given the elevation (altitude).
'
' Args:
'        Altitude: Altitude in ft [IP] or m [SI]
'
' Returns:
'        Standard atmosphere barometric pressure in Psi [IP] or Pa [SI]
'
' Reference:
'        ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 3
'
  On Error GoTo ErrHandler

  If (isIP()) Then
    GetStandardAtmPressure = 14.696 * (1 - 0.0000068754 * Altitude) ^ 5.2559
  Else
    GetStandardAtmPressure = 101325 * (1 - 0.0000225577 * Altitude) ^ 5.2559
  End If
  Exit Function

ErrHandler:
  GetStandardAtmPressure = CVErr(xlErrNA)

End Function

Function GetStandardAtmTemperature(ByVal Altitude As Variant) As Variant
'
' Return standard atmosphere temperature, given the elevation (altitude).
'
' Args:
'        Altitude: Altitude in ft
'
' Returns:
'        Standard atmosphere dry-bulb temperature in °F [IP] or °C [SI]
'
' Reference:
'        ASHRAE Handbook - Fundamentals (2017) ch. 1 eqn 4
'
  On Error GoTo ErrHandler

  If (isIP()) Then
    GetStandardAtmTemperature = 59 - 0.0035662 * Altitude
  Else
    GetStandardAtmTemperature = 15 - 0.0065 * Altitude
  End If
  Exit Function

ErrHandler:
  GetStandardAtmTemperature = CVErr(xlErrNA)

End Function

Function GetSeaLevelPressure(ByVal StationPressure As Variant, ByVal Altitude As Variant, ByVal TDryBulb As Variant) As Variant
'
' Return sea level pressure given dry-bulb temperature, altitude above sea level and pressure.
'
' Args:
'        StationPressure : Observed station pressure in Psi [IP] or Pa [SI]
'        Altitude: Altitude in ft [IP] or m [SI]
'        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
'
' Returns:
'        Sea level barometric pressure in Psi [IP] or Pa [SI]
'
' Reference:
'        Hess SL, Introduction to theoretical meteorology, Holt Rinehart and Winston, NY 1959,
'        ch. 6.5; Stull RB, Meteorology for scientists and engineers, 2nd edition,
'        Brooks/Cole 2000, ch. 1.
'
' Notes:
'        The standard procedure for the US is to use for TDryBulb the average
'        of the current station temperature and the station temperature from 12 hours ago.
'

  ' Calculate average temperature in column of air, assuming a lapse rate
  ' of 6.5 °C/km
  Dim TColumn As Variant
  Dim H As Variant

  On Error GoTo ErrHandler

  If (isIP()) Then
    ' Calculate average temperature in column of air, assuming a lapse rate
    ' of 3.6 °F/1000ft
    TColumn = TDryBulb + 0.0036 * Altitude / 2

    ' Determine the scale height
    H = 53.351 * GetTRankineFromTFahrenheit(TColumn)
  Else
    ' Calculate average temperature in column of air, assuming a lapse rate
    ' of 6.5 °C/km
    TColumn = TDryBulb + 0.0065 * Altitude / 2

    ' Determine the scale height
    H = 287.055 * GetTKelvinFromTCelsius(TColumn) / 9.807
  End If

  ' Calculate the sea level pressure
  GetSeaLevelPressure = StationPressure * Exp(Altitude / H)
  Exit Function

ErrHandler:
  GetSeaLevelPressure = CVErr(xlErrNA)

End Function

Function GetStationPressure(ByVal SeaLevelPressure As Variant, ByVal Altitude As Variant, ByVal TDryBulb As Variant) As Variant
'
' Args:
'        SeaLevelPressure : Sea level barometric pressure in Psi [IP] or Pa [SI]
'        Altitude: Altitude in ft [IP] or m [SI]
'        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
'
' Returns:
'        Station pressure in Psi [IP] or Pa [SI]
'
' Reference:
'        See 'GetSeaLevelPressure'
'
' Notes:
'        This function is just the inverse of 'GetSeaLevelPressure'.
'
  On Error GoTo ErrHandler

  GetStationPressure = SeaLevelPressure / GetSeaLevelPressure(1, Altitude, TDryBulb)
  Exit Function

ErrHandler:
  GetStationPressure = CVErr(xlErrNA)

End Function

'******************************************************************************************************
' Functions to set all psychrometric values
'******************************************************************************************************

Sub CalcPsychrometricsFromTWetBulb(ByVal TDryBulb As Variant, ByVal TWetBulb As Variant, ByVal Pressure As Variant, _
    ByRef HumRatio As Variant, ByRef TDewPoint As Variant, ByRef RelHum As Variant, ByRef VapPres As Variant, _
    ByRef MoistAirEnthalpy As Variant, ByRef MoistAirVolume As Variant, ByRef DegreeOfSaturation As Variant)
'
' Utility function to calculate humidity ratio, dew-point temperature, relative humidity,
' vapour pressure, moist air enthalpy, moist air volume, and degree of saturation of air given
' dry-bulb temperature, wet-bulb temperature, and pressure.
'
' Args:
'        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
'        TWetBulb : Wet-bulb temperature in °F [IP] or °C [SI]
'        Pressure : Atmospheric pressure in Psi [IP] or Pa [SI]
'
' Returns:
'        Humidity ratio in lb_H2O/lb_Air [IP] or kg_H2O/kg_Air [SI]
'        Dew-point temperature in °F [IP] or °C [SI]
'        Relative humidity in range [0, 1]
'        Partial pressure of water vapor in moist air in Psi [IP] or Pa [SI]
'        Moist air enthalpy in Btu/lb [IP] or J/kg [SI]
'        Specific volume of moist air in ft³/lb [IP] or in m³/kg [SI]
'        Degree of saturation [unitless]
'
  HumRatio = GetHumRatioFromTWetBulb(TDryBulb, TWetBulb, Pressure)
  TDewPoint = GetTDewPointFromHumRatio(TDryBulb, HumRatio, Pressure)
  RelHum = GetRelHumFromHumRatio(TDryBulb, HumRatio, Pressure)
  VapPres = GetVapPresFromHumRatio(HumRatio, Pressure)
  MoistAirEnthalpy = GetMoistAirEnthalpy(TDryBulb, HumRatio)
  MoistAirVolume = GetMoistAirVolume(TDryBulb, HumRatio, Pressure)
  DegreeOfSaturation = GetDegreeOfSaturation(TDryBulb, HumRatio, Pressure)

End Sub

Sub CalcPsychrometricsFromTDewPoint(ByVal TDryBulb As Variant, ByVal TDewPoint As Variant, ByVal Pressure As Variant, _
    ByRef HumRatio As Variant, ByRef TWetBulb As Variant, ByRef RelHum As Variant, ByRef VapPres As Variant, _
    ByRef MoistAirEnthalpy As Variant, ByRef MoistAirVolume As Variant, ByRef DegreeOfSaturation As Variant)
'
' Utility function to calculate humidity ratio, wet-bulb temperature, relative humidity,
' vapour pressure, moist air enthalpy, moist air volume, and degree of saturation of air given
' dry-bulb temperature, dew-point temperature, and pressure.
'
' Args:
'        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
'        TDewPoint : Dew-point temperature in °F [IP] or °C [SI]
'        Pressure : Atmospheric pressure in Psi [IP] or Pa [SI]
'
' Returns:
'        Humidity ratio in lb_H2O/lb_Air [IP] or kg_H2O/kg_Air [SI]
'        Wet-bulb temperature in °F [IP] or °C [SI]
'        Relative humidity in range [0, 1]
'        Partial pressure of water vapor in moist air in Psi [IP] or Pa [SI]
'        Moist air enthalpy in Btu/lb [IP] or J/kg [SI]
'        Specific volume of moist air in ft³/lb [IP] or in m³/kg [SI]
'        Degree of saturation [unitless]
'
  HumRatio = GetHumRatioFromTDewPoint(TDewPoint, Pressure)
  TWetBulb = GetTWetBulbFromHumRatio(TDryBulb, HumRatio, Pressure)
  RelHum = GetRelHumFromHumRatio(TDryBulb, HumRatio, Pressure)
  VapPres = GetVapPresFromHumRatio(HumRatio, Pressure)
  MoistAirEnthalpy = GetMoistAirEnthalpy(TDryBulb, HumRatio)
  MoistAirVolume = GetMoistAirVolume(TDryBulb, HumRatio, Pressure)
  DegreeOfSaturation = GetDegreeOfSaturation(TDryBulb, HumRatio, Pressure)

End Sub

Sub CalcPsychrometricsFromRelHum(ByVal TDryBulb As Variant, ByVal RelHum As Variant, ByVal Pressure As Variant, _
    ByRef HumRatio As Variant, ByRef TWetBulb As Variant, ByRef TDewPoint As Variant, ByRef VapPres As Variant, _
    ByRef MoistAirEnthalpy As Variant, ByRef MoistAirVolume As Variant, ByRef DegreeOfSaturation As Variant)
'
' Utility function to calculate humidity ratio, wet-bulb temperature, dew-point temperature,
' vapour pressure, moist air enthalpy, moist air volume, and degree of saturation of air given
' dry-bulb temperature, relative humidity and pressure.
'
' Args:
'        TDryBulb : Dry-bulb temperature in °F [IP] or °C [SI]
'        RelHum : Relative humidity in range [0, 1]
'        Pressure : Atmospheric pressure in Psi [IP] or Pa [SI]
'
' Returns:
'        Humidity ratio in lb_H2O/lb_Air [IP] or kg_H2O/kg_Air [SI]
'        Wet-bulb temperature in °F [IP] or °C [SI]
'        Dew-point temperature in °F [IP] or °C [SI].
'        Partial pressure of water vapor in moist air in Psi [IP] or Pa [SI]
'        Moist air enthalpy in Btu/lb [IP] or J/kg [SI]
'        Specific volume of moist air in ft³/lb [IP] or in m³/kg [SI]
'        Degree of saturation [unitless]
'
  HumRatio = GetHumRatioFromRelHum(TDryBulb, RelHum, Pressure)
  TWetBulb = GetTWetBulbFromHumRatio(TDryBulb, HumRatio, Pressure)
  TDewPoint = GetTDewPointFromHumRatio(TDryBulb, HumRatio, Pressure)
  VapPres = GetVapPresFromHumRatio(HumRatio, Pressure)
  MoistAirEnthalpy = GetMoistAirEnthalpy(TDryBulb, HumRatio)
  MoistAirVolume = GetMoistAirVolume(TDryBulb, HumRatio, Pressure)
  DegreeOfSaturation = GetDegreeOfSaturation(TDryBulb, HumRatio, Pressure)

End Sub

Javascript

   1
   2
   3
   4
   5
   6
   7
   8
   9
  10
  11
  12
  13
  14
  15
  16
  17
  18
  19
  20
  21
  22
  23
  24
  25
  26
  27
  28
  29
  30
  31
  32
  33
  34
  35
  36
  37
  38
  39
  40
  41
  42
  43
  44
  45
  46
  47
  48
  49
  50
  51
  52
  53
  54
  55
  56
  57
  58
  59
  60
  61
  62
  63
  64
  65
  66
  67
  68
  69
  70
  71
  72
  73
  74
  75
  76
  77
  78
  79
  80
  81
  82
  83
  84
  85
  86
  87
  88
  89
  90
  91
  92
  93
  94
  95
  96
  97
  98
  99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
 126
 127
 128
 129
 130
 131
 132
 133
 134
 135
 136
 137
 138
 139
 140
 141
 142
 143
 144
 145
 146
 147
 148
 149
 150
 151
 152
 153
 154
 155
 156
 157
 158
 159
 160
 161
 162
 163
 164
 165
 166
 167
 168
 169
 170
 171
 172
 173
 174
 175
 176
 177
 178
 179
 180
 181
 182
 183
 184
 185
 186
 187
 188
 189
 190
 191
 192
 193
 194
 195
 196
 197
 198
 199
 200
 201
 202
 203
 204
 205
 206
 207
 208
 209
 210
 211
 212
 213
 214
 215
 216
 217
 218
 219
 220
 221
 222
 223
 224
 225
 226
 227
 228
 229
 230
 231
 232
 233
 234
 235
 236
 237
 238
 239
 240
 241
 242
 243
 244
 245
 246
 247
 248
 249
 250
 251
 252
 253
 254
 255
 256
 257
 258
 259
 260
 261
 262
 263
 264
 265
 266
 267
 268
 269
 270
 271
 272
 273
 274
 275
 276
 277
 278
 279
 280
 281
 282
 283
 284
 285
 286
 287
 288
 289
 290
 291
 292
 293
 294
 295
 296
 297
 298
 299
 300
 301
 302
 303
 304
 305
 306
 307
 308
 309
 310
 311
 312
 313
 314
 315
 316
 317
 318
 319
 320
 321
 322
 323
 324
 325
 326
 327
 328
 329
 330
 331
 332
 333
 334
 335
 336
 337
 338
 339
 340
 341
 342
 343
 344
 345
 346
 347
 348
 349
 350
 351
 352
 353
 354
 355
 356
 357
 358
 359
 360
 361
 362
 363
 364
 365
 366
 367
 368
 369
 370
 371
 372
 373
 374
 375
 376
 377
 378
 379
 380
 381
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
/**
 * PsychroLib (version 2.3.0) (https://github.com/psychrometrics/psychrolib)
 * Copyright (c) 2018 D. Thevenard and D. Meyer for the current library implementation
 * Copyright (c) 2017 ASHRAE Handbook — Fundamentals for ASHRAE equations and coefficients
 * Licensed under the MIT License.
 */

function Psychrometrics() {
  /**
   * Module overview
   *  Contains functions for calculating thermodynamic properties of gas-vapor mixtures
   *  and standard atmosphere suitable for most engineering, physical and meteorological
   *  applications.
   *
   *  Most of the functions are an implementation of the formulae found in the
   *  2017 ASHRAE Handbook - Fundamentals, in both International System (SI),
   *  and Imperial (IP) units. Please refer to the information included in
   *  each function for their respective reference.
   *
   * Example (e.g. Node.JS)
   *  // Import the PsychroLib
   *  var psychrolib = require('psychrolib.js')
   *  // Set unit system
   *  psychrolib.SetUnitSystem(psychrolib.SI)
   *  // Calculate the dew point temperature for a dry bulb temperature of 25 C and a relative humidity of 80%
   *  var TDewPoint = psychrolib.GetTDewPointFromRelHum(25.0, 0.80);
   *  console.log('TDewPoint: %d', TDewPoint);
   * 21.3094
   *
   * Copyright
   *  - For the current library implementation
   *     Copyright (c) 2018 D. Thevenard and D. Meyer.
   *  - For equations and coefficients published ASHRAE Handbook — Fundamentals, Chapter 1
   *     Copyright (c) 2017 ASHRAE Handbook — Fundamentals (https://www.ashrae.org)
   *
   * License
   *  MIT (https://github.com/psychrometrics/psychrolib/LICENSE.txt)
   *
   * Note from the Authors
   *  We have made every effort to ensure that the code is adequate, however, we make no
   *  representation with respect to its accuracy. Use at your own risk. Should you notice
   *  an error, or if you have a suggestion, please notify us through GitHub at
   *  https://github.com/psychrometrics/psychrolib/issues.
   */


  // Standard functions
  var log = Math.log;
  var exp = Math.exp;
  var pow = Math.pow;
  var min = Math.min;
  var max = Math.max;
  var abs = Math.abs;


  /******************************************************************************************************
   * Global constants
   *****************************************************************************************************/

  var ZERO_FAHRENHEIT_AS_RANKINE = 459.67;  // Zero degree Fahrenheit (°F) expressed as degree Rankine (°R).
                                            // Reference: ASHRAE Handbook - Fundamentals (2017) ch. 39.

  var ZERO_CELSIUS_AS_KELVIN = 273.15;      // Zero degree Celsius (°C) expressed as Kelvin (K).
                                            // Reference: ASHRAE Handbook - Fundamentals (2017) ch. 39.

  var R_DA_IP = 53.350;               // Universal gas constant for dry air (IP version) in ft lb_Force lb_DryAir⁻¹ R⁻¹.
                                      // Reference: ASHRAE Handbook - Fundamentals (2017) ch. 1.

  var R_DA_SI = 287.042;              // Universal gas constant for dry air (SI version) in J kg_DryAir⁻¹ K⁻¹.
                                      // Reference: ASHRAE Handbook - Fundamentals (2017) ch. 1.

  var INVALID = -99999;               // Invalid value (dimensionless).

  var MAX_ITER_COUNT = 100            // Maximum number of iterations before exiting while loops.

  var MIN_HUM_RATIO = 1e-7            // Minimum acceptable humidity ratio used/returned by any functions.
                                      // Any value above 0 or below the MIN_HUM_RATIO will be reset to this value.

  var FREEZING_POINT_WATER_IP = 32.0  // Freezing point of water in Fahrenheit.

  var FREEZING_POINT_WATER_SI = 0.0   // Freezing point of water in Celsius.

  var TRIPLE_POINT_WATER_IP = 32.018  // Triple point of water in Fahrenheit.

  var TRIPLE_POINT_WATER_SI = 0.01    // Triple point of water in Celsius.



  /******************************************************************************************************
   * Helper functions
   *****************************************************************************************************/

  // Systems of units (IP or SI)
  var PSYCHROLIB_UNITS = undefined;

  this.IP = 1;
  this.SI = 2;

  // Function to set the system of units
  // Note: this function *HAS TO BE CALLED* before the library can be used
  this.SetUnitSystem = function(UnitSystem) {
    if (UnitSystem != this.IP && UnitSystem != this.SI) {
      throw new Error('UnitSystem must be IP or SI');
    }
    PSYCHROLIB_UNITS = UnitSystem;
    // Define tolerance of temperature calculations
    // The tolerance is the same in IP and SI
    if (PSYCHROLIB_UNITS == this.IP)
      PSYCHROLIB_TOLERANCE = 0.001 * 9. / 5.;
    else
      PSYCHROLIB_TOLERANCE = 0.001;
  }

  // Return system of units in use.
  this.GetUnitSystem = function() {
    return PSYCHROLIB_UNITS;
  }