diff --git a/docs/source/barbs.rst b/docs/source/barbs.rst index 30d2b2e..99a9562 100644 --- a/docs/source/barbs.rst +++ b/docs/source/barbs.rst @@ -34,7 +34,7 @@ A profile must be first plotted before the barbs are associated with that profil dew_point = os.path.join(tephi.DATA_DIR, 'dews.txt') dew_data = tephi.loadtxt(dew_point, column_titles=('pressure', 'dewpoint')) dews = zip(dew_data.pressure, dew_data.dewpoint) - tpg = tephi.Tephigram() + tpg = tephi.TephiAxes() profile = tpg.plot(dews) barbs = [(0, 0, 900), (1, 30, 850), (5, 60, 800), (10, 90, 750), (15, 120, 700), (20, 150, 650), @@ -64,7 +64,7 @@ Note that, the barbs default to the same colour as their associated profile. dews = zip(dew_data.pressure, dew_data.dewpoint) temps = zip(temp_data.pressure, temp_data.temperature) - tpg = tephi.Tephigram() + tpg = tephi.TephiAxes() dprofile = tpg.plot(dews) dbarbs = [(0, 0, 900), (15, 120, 600), (35, 240, 300)] dprofile.barbs(dbarbs) @@ -89,7 +89,7 @@ Barbs may also be plotted using wind speed and wind direction data (associated w barb_data = tephi.loadtxt(winds, column_titles=column_titles) dews = zip(barb_data.pressure, barb_data.dewpoint) barbs = zip(barb_data.wind_speed, barb_data.wind_direction, barb_data.pressure) - tpg = tephi.Tephigram() + tpg = tephi.TephiAxes() profile = tpg.plot(dews) profile.barbs(barbs) plt.show() @@ -113,7 +113,7 @@ This transparency allows full control when plotting barbs on the tephigram. dew_point = os.path.join(tephi.DATA_DIR, 'dews.txt') dew_data = tephi.loadtxt(dew_point, column_titles=('pressure', 'dewpoint')) dews = zip(dew_data.pressure, dew_data.dewpoint) - tpg = tephi.Tephigram() + tpg = tephi.TephiAxes() profile = tpg.plot(dews) barbs = [(0, 0, 900), (1, 30, 850), (5, 60, 800), (10, 90, 750), (15, 120, 700), (20, 150, 650), @@ -141,7 +141,7 @@ By default, the barbs are plotted on the right hand side of the tephigram. The p dew_point = os.path.join(tephi.DATA_DIR, 'dews.txt') dew_data = tephi.loadtxt(dew_point, column_titles=('pressure', 'dewpoint')) dews = zip(dew_data.pressure, dew_data.dewpoint) - tpg = tephi.Tephigram() + tpg = tephi.TephiAxes() profile = tpg.plot(dews) barbs = [(0, 0, 900), (1, 30, 850), (5, 60, 800), (10, 90, 750), (15, 120, 700), (20, 150, 650), diff --git a/docs/source/conf.py b/docs/source/conf.py index a9a01cd..14cfaf2 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -42,6 +42,7 @@ "sphinx.ext.intersphinx", "matplotlib.sphinxext.mathmpl", "matplotlib.sphinxext.plot_directive", + "matplotlib.sphinxext.roles", "sphinx_copybutton", ] diff --git a/docs/source/customise.rst b/docs/source/customise.rst index 8da9370..8430050 100644 --- a/docs/source/customise.rst +++ b/docs/source/customise.rst @@ -10,445 +10,53 @@ This section discusses how finer control of the tephigram isobars, saturated adi import tephi from pprint import pprint - -Isobar control --------------- - -Isobar lines -^^^^^^^^^^^^ - -The default behaviour of the tephigram *isobar line* is controlled by the :data:`tephi.ISOBAR_LINE` dictionary: - - >>> print(tephi.ISOBAR_LINE) - {'color': 'blue', 'linewidth': 0.5, 'clip_on': True} - -This is a dictionary of *key* and *value* pairs that are passed through as keyword arguments to :func:`matplotlib.pyplot.plot`. - -Updating the ``ISOBAR_LINE`` dictionary will subsequently change the default behaviour of how the tephigram isobar lines are plotted. - -.. plot:: - :include-source: - :align: center - - import matplotlib.pyplot as plt - import os.path - - import tephi - - dew_point = os.path.join(tephi.DATA_DIR, 'dews.txt') - dew_data = tephi.loadtxt(dew_point, column_titles=('pressure', 'dewpoint')) - dews = zip(dew_data.pressure, dew_data.dewpoint) - tephi.ISOBAR_LINE.update({'color': 'purple', 'linewidth': 3, 'linestyle': '--'}) - tpg = tephi.Tephigram() - tpg.plot(dews) - plt.show() - -.. plot:: - - import tephi - tephi.ISOBAR_LINE = {'color': 'blue', 'linewidth': 0.5, 'clip_on': True} - - -Isobar text -^^^^^^^^^^^ - -Similarly, the default behaviour of the tephigram *isobar text* is controlled by the :data:`tephi.ISOBAR_TEXT` dictionary: - - >>> pprint(tephi.ISOBAR_TEXT) - {'clip_on': True, 'color': 'blue', 'ha': 'right', 'size': 8, 'va': 'bottom'} - -This is a dictionary of *key* and *value* pairs that are passed through as keyword arguments to :func:`matplotlib.pyplot.text`. - -Updating the ``ISOBAR_TEXT`` dictionary will change the default behaviour of how the tephigram isobar text is plotted. - -.. plot:: - :include-source: - :align: center - - import matplotlib.pyplot as plt - import os.path - - import tephi - - dew_point = os.path.join(tephi.DATA_DIR, 'dews.txt') - dew_data = tephi.loadtxt(dew_point, column_titles=('pressure', 'dewpoint')) - dews = zip(dew_data.pressure, dew_data.dewpoint) - tephi.ISOBAR_TEXT.update({'color': 'purple', 'size': 12}) - tpg = tephi.Tephigram() - tpg.plot(dews) - plt.show() - -.. plot:: - - import tephi - tephi.ISOBAR_TEXT = {'color': 'blue', 'va': 'bottom', 'ha': 'right', 'clip_on': True, 'size': 8} - - -Isobar frequency -^^^^^^^^^^^^^^^^ - -The *frequency* at which isobar lines are plotted on the tephigram is controlled by the :data:`tephi.ISOBAR_SPEC` list: - - >>> print(tephi.ISOBAR_SPEC) - [(25, 0.03), (50, 0.1), (100, 0.25), (200, 1.5)] - -This :term:`line specification` is a sequence of one or more tuple pairs that contain an isobar pressure :term:`line step` and a :term:`zoom level`. - -For example, ``(25, 0.03)`` states that all isobar lines that are a multiple of ``25`` mb will be plotted i.e. visible, when the :term:`zoom level` is at or -below ``0.03``. - -The *overall range* of isobar pressure levels that may be plotted is controlled by the :data:`tephi.MIN_PRESSURE` and -:data:`tephi.MAX_PRESSURE` variables: - - >>> print(tephi.MIN_PRESSURE) - 50 - >>> print(tephi.MAX_PRESSURE) - 1000 - -Note that, it is possible to set a *fixed* isobar pressure :term:`line step` for a tephigram plot by setting the associated :term:`zoom level` to ``None``. -This is opposed to relying on the plot :term:`zoom level` of the tephigram to control line visibility. - -For example, to **always** show isobar lines that are a multiple of 50 mb, irrespective of the :term:`zoom level`, - -.. plot:: - :include-source: - :align: center - - import matplotlib.pyplot as plt - import os.path - - import tephi - - dew_point = os.path.join(tephi.DATA_DIR, 'dews.txt') - dew_data = tephi.loadtxt(dew_point, column_titles=('pressure', 'dewpoint')) - dews = zip(dew_data.pressure, dew_data.dewpoint) - tephi.ISOBAR_SPEC = [(50, None)] - tpg = tephi.Tephigram() - tpg.plot(dews) - plt.show() - -.. plot:: - - import tephi - tephi.ISOBAR_SPEC = [(25, 0.03), (50, 0.1), (100, 0.25), (200, 1.5)] - -It is also possible to control which *individual* isobar lines should be *fixed* via the :data:`tephi.ISOBAR_FIXED` list: - - >>> print(tephi.ISOBAR_FIXED) - [50, 1000] - -By default, the isobar lines at 50 mb and 1000 mb will **always** be plotted. - - -Isobar line extent -^^^^^^^^^^^^^^^^^^ - -The extent of each tephigram *isobar line* is controlled by the :data:`tephi.MIN_THETA` and -:data:`tephi.MAX_THETA` variables: - - >>> print(tephi.MIN_THETA) - 0 - >>> print(tephi.MAX_THETA) - 250 - -For example, to change the isobar line extent behaviour to be between 15 :sup:`o`\ C and 60 :sup:`o`\ C, - -.. plot:: - :include-source: - :align: center - - import matplotlib.pyplot as plt - import os.path - - import tephi - - dew_point = os.path.join(tephi.DATA_DIR, 'dews.txt') - dew_data = tephi.loadtxt(dew_point, column_titles=('pressure', 'dewpoint')) - dews = zip(dew_data.pressure, dew_data.dewpoint) - tephi.MIN_THETA = 15 - tephi.MAX_THETA = 60 - tpg = tephi.Tephigram() - tpg.plot(dews) - plt.show() - -.. plot:: - - import tephi - tephi.MIN_THETA = 0 - tephi.MAX_THETA = 250 - - -Saturated adiabat control -------------------------- - -Saturated adiabat lines -^^^^^^^^^^^^^^^^^^^^^^^ - -The default behaviour of the tephigram *pseudo saturated wet adiabat line* is controlled by the :data:`tephi.WET_ADIABAT_LINE` dictionary: - - >>> print(tephi.WET_ADIABAT_LINE) - {'color': 'orange', 'linewidth': 0.5, 'clip_on': True} - -This is a dictionary of *key* and *value* pairs that are passed through as keyword arguments to :func:`matplotlib.pyplot.plot`. - -Updating the ``WET_ADIABAT_LINE`` dictionary will change the default behaviour of **all** saturated adiabat line plotting. - -.. plot:: - :include-source: - :align: center - - import matplotlib.pyplot as plt - import os.path - - import tephi - - dew_point = os.path.join(tephi.DATA_DIR, 'dews.txt') - dew_data = tephi.loadtxt(dew_point, column_titles=('pressure', 'dewpoint')) - dews = zip(dew_data.pressure, dew_data.dewpoint) - tephi.WET_ADIABAT_LINE.update({'color': 'purple', 'linewidth': 3, 'linestyle': '--'}) - tpg = tephi.Tephigram() - tpg.plot(dews) - plt.show() - -.. plot:: - - import tephi - tephi.WET_ADIABAT_LINE = {'color': 'orange', 'linewidth': 0.5, 'clip_on': True} - - -Saturated adiabat text -^^^^^^^^^^^^^^^^^^^^^^ - -The default behaviour of the tephigram *saturated adiabat text* is controlled by the :data:`tephi.WET_ADIABAT_TEXT` dictionary: - - >>> pprint(tephi.WET_ADIABAT_TEXT) - {'clip_on': True, 'color': 'orange', 'ha': 'left', 'size': 8, 'va': 'bottom'} - -This is a dictionary of *key* and *value* pairs that are passed through as keyword arguments to :func:`matplotlib.pyplot.text`. - -Updating the ``WET_ADIABAT_TEXT`` dictionary will change the default behaviour of how the text of associated saturated adiabat lines are plotted. - -.. plot:: - :include-source: - :align: center - - import matplotlib.pyplot as plt - import os.path - - import tephi - - dew_point = os.path.join(tephi.DATA_DIR, 'dews.txt') - dew_data = tephi.loadtxt(dew_point, column_titles=('pressure', 'dewpoint')) - dews = zip(dew_data.pressure, dew_data.dewpoint) - tephi.WET_ADIABAT_TEXT.update({'color': 'purple', 'size': 12}) - tpg = tephi.Tephigram() - tpg.plot(dews) - plt.show() - -.. plot:: - - import tephi - tephi.WET_ADIABAT_TEXT = {'color': 'orange', 'va': 'bottom', 'ha': 'left', 'clip_on': True, 'size': 8} - - -Saturated adiabat line frequency -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The *frequency* at which saturated adiabat lines are plotted on the tephigram is controlled by the :data:`tephi.WET_ADIABAT_SPEC` list: - - >>> print(tephi.WET_ADIABAT_SPEC) - [(1, 0.05), (2, 0.15), (4, 1.5)] - -This :term:`line specification` is a sequence of one or more tuple pairs that contain a saturated adiabat temperature :term:`line step` and a -:term:`zoom level`. - -For example, ``(2, 0.15)`` states that all saturated adiabat lines that are a multiple of ``2`` :sup:`o`\ C will be plotted i.e. visible, -when the :term:`zoom level` is at or below ``0.15``. - -The *overall range* of saturated adiabat levels that may be plotted is controlled by the :data:`tephi.MIN_WET_ADIABAT` and -:data:`tephi.MAX_WET_ADIABAT` variables: - - >>> print(tephi.MIN_WET_ADIABAT) - 1 - >>> print(tephi.MAX_WET_ADIABAT) - 60 - -Note that, it is possible to set a *fixed* saturated adiabat temperature :term:`line step` for a tephigram plot by setting the -associated :term:`zoom level` to ``None``. - -For example, to **always** show saturated adiabat lines that are a multiple of 5 :sup:`o`\ C, irrespective of the :term:`zoom level`, - -.. plot:: - :include-source: - :align: center - - import matplotlib.pyplot as plt - import os.path - - import tephi - - dew_point = os.path.join(tephi.DATA_DIR, 'dews.txt') - dew_data = tephi.loadtxt(dew_point, column_titles=('pressure', 'dewpoint')) - dews = zip(dew_data.pressure, dew_data.dewpoint) - tephi.WET_ADIABAT_SPEC = [(5, None)] - tpg = tephi.Tephigram() - tpg.plot(dews) - plt.show() - -.. plot:: - - import tephi - tephi.WET_ADIABAT_SPEC = [(1, 0.05), (2, 0.15), (4, 1.5)] - -It is also possible to control which *individual* saturated adiabat lines should be *fixed* via the :data:`tephi.WET_ADIABAT_FIXED` variable: - - >>> print(tephi.WET_ADIABAT_FIXED) - None - -By default, no saturated adiabat lines are fixed. To force saturated adiabat lines with a temperature of ``15`` :sup:`o`\ C and ``17`` :sup:`o`\ C -always to be plotted, - -.. plot:: - :include-source: - :align: center - - import matplotlib.pyplot as plt - import os.path - - import tephi - - dew_point = os.path.join(tephi.DATA_DIR, 'dews.txt') - dew_data = tephi.loadtxt(dew_point, column_titles=('pressure', 'dewpoint')) - dews = zip(dew_data.pressure, dew_data.dewpoint) - tephi.WET_ADIABAT_FIXED = [15, 17] - tpg = tephi.Tephigram() - tpg.plot(dews) - plt.show() - -.. plot:: - - import tephi - tephi.WET_ADIABAT_FIXED = None - - -Humidity mixing ratio control ------------------------------ - -Humidity mixing ratio lines -^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The default behaviour of the tephigram *humidity mixing ratio line* is controlled by the :data:`tephi.MIXING_RATIO_LINE` dictionary: - - >>> print(tephi.MIXING_RATIO_LINE) - {'color': 'green', 'linewidth': 0.5, 'clip_on': True} - -This is a dictionary of *key* and *value* pairs that are passed through as keyword arguments to :func:`matplotlib.pyplot.plot`. - -Updating the ``MIXING_RATIO_LINE`` dictionary will change the default behaviour of **all** humidity mixing ratio line plotting. - -.. plot:: - :include-source: - :align: center - - import matplotlib.pyplot as plt - import os.path - - import tephi - - dew_point = os.path.join(tephi.DATA_DIR, 'dews.txt') - dew_data = tephi.loadtxt(dew_point, column_titles=('pressure', 'dewpoint')) - dews = zip(dew_data.pressure, dew_data.dewpoint) - tephi.MIXING_RATIO_LINE.update({'color': 'purple', 'linewidth': 3, 'linestyle': '--'}) - tpg = tephi.Tephigram() - tpg.plot(dews) - plt.show() - -.. plot:: - - import tephi - tephi.MIXING_RATIO_LINE = {'color': 'green', 'linewidth': 0.5, 'clip_on': True} - - -Humidity mixing ratio text -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The default behaviour of the tephigram *humidity mixing ratio text* is controlled by the :data:`tephi.MIXING_RATIO_TEXT` dictionary: - - >>> pprint(tephi.MIXING_RATIO_TEXT) - {'clip_on': True, 'color': 'green', 'ha': 'right', 'size': 8, 'va': 'bottom'} - -This is a dictionary of *key* and *value* pairs that are passed through as keyword arguments to :func:`matplotlib.pyplot.text`. - -Updating the ``MIXING_RATIO_TEXT`` dictionary will change the default behaviour of how the text of associated humidity mixing ratio lines are plotted. - -.. plot:: - :include-source: - :align: center - - import matplotlib.pyplot as plt - import os.path - - import tephi - - dew_point = os.path.join(tephi.DATA_DIR, 'dews.txt') - dew_data = tephi.loadtxt(dew_point, column_titles=('pressure', 'dewpoint')) - dews = zip(dew_data.pressure, dew_data.dewpoint) - tephi.MIXING_RATIO_TEXT.update({'color': 'purple', 'size': 12}) - tpg = tephi.Tephigram() - tpg.plot(dews) - plt.show() - -.. plot:: - - import tephi - tephi.MIXING_RATIO_TEXT = {'color': 'green', 'va': 'bottom', 'ha': 'right', 'clip_on': True, 'size': 8} - - -Humidity mixing ratio line frequency -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The *frequency* at which humidity mixing ratio lines are plotted on the tephigram is controlled by the :data:`tephi.MIXING_RATIO_SPEC` list: - - >>> print(tephi.MIXING_RATIO_SPEC) - [(1, 0.05), (2, 0.18), (4, 0.3), (8, 1.5)] - -This :term:`line specification` is a sequence of one or more tuple pairs that contain a humidity mixing ratio :term:`line step` and a -:term:`zoom level`. - -For example, ``(4, 0.3)`` states that every *fourth* humidity mixing ratio line will be plotted i.e. visible, when the :term:`zoom level` -is at or below ``0.3``. - -The *overall range* of humidity mixing ratio levels that may be plotted is controlled by the :data:`tephi.MIXING_RATIOS` list: - - >>> print(tephi.MIXING_RATIOS) - [0.001, 0.002, 0.005, 0.01, 0.02, 0.03, 0.05, 0.1, 0.15, 0.2, 0.3, 0.4, 0.5, 0.6, 0.8, 1.0, 1.5, 2.0, 2.5, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 12.0, 14.0, 16.0, 18.0, 20.0, 24.0, 28.0, 32.0, 36.0, 40.0, 44.0, 48.0, 52.0, 56.0, 60.0, 68.0, 80.0] - -Note that, it is possible to control which *individual* humidity mixing ratio lines should be *fixed* i.e. **always** visible, via the :data:`tephi.MIXING_RATIO_FIXED` variable: - - >>> print(tephi.MIXING_RATIO_FIXED) - None - -By default, no humidity mixing ratio lines are fixed. To force humidity mixing ratio lines ``4.0`` g kg\ :sup:`-1`\ and ``6.0`` g kg\ :sup:`-1`\ -always to be plotted independent of the :term:`zoom level`, - -.. plot:: - :include-source: - :align: center - - import matplotlib.pyplot as plt - import os.path - - import tephi - - dew_point = os.path.join(tephi.DATA_DIR, 'dews.txt') - dew_data = tephi.loadtxt(dew_point, column_titles=('pressure', 'dewpoint')) - dews = zip(dew_data.pressure, dew_data.dewpoint) - tephi.MIXING_RATIO_FIXED = [4.0, 6.0] - tpg = tephi.Tephigram() - tpg.plot(dews) - plt.show() - -.. plot:: - - import tephi - tephi.MIXING_RATIO_FIXED = None +There are two main methods to customise tephigram lines: default values, and individual values. Default values apply to +ALL axes by default, whereas individual values affect only the axes you change them on. + +The default values of barbs, isobars, mixing ratios, isopleths and wet adiabats are stored in the +``constants.defaults`` dictionary. Changing these values will change the default behaviour of the tephigram. + +Individual values can only be changed for the three adjustable isopleths (isobars, humidity mixing ratios, and wet +adiabats. + +Barbs +----- +Barb defaults can be altered via the ``constants.defaults`` dictionary. + +from tephi.constants import defaults +defaults["barbs_gutter"] +defaults["barbs_length"] +defaults["barbs_linewidth"] +defaults["barbs_zorder"] + +Isopleths +--------- + +Defaults +^^^^^^^^ +.. note:: + "" can be replaced by any of "isobar", "mixing_ratio" and "wet_adiabat", to change the + respective isopleth defaults. + +from tephi.constants import defaults +defaults["_line"] +defaults["_nbins"] +defaults["_text"] +defaults["_ticks"] +defaults["_min_"] +defaults["_max_"] + +Individual +^^^^^^^^^^ + +If you wish to change the behaviour of the three additional gridlines (isobars, wet adiabats, humidity mixing ratios) +for a specific axes, you can edit the gridline artist properties. + +tephigram = TephiAxes() +tephigram.add_() +tephigram. + +.. note:: + Currently, the only directly editable values are nbins, ticks, and the max\_ and min\_ values for the respective. + isopleth. Other values can be changed through the ``_kwarg`` dictionary, although this should be improved + in the future. diff --git a/docs/source/glossary.rst b/docs/source/glossary.rst index fd0b97c..9dd0163 100644 --- a/docs/source/glossary.rst +++ b/docs/source/glossary.rst @@ -7,7 +7,7 @@ Glossary .. glossary:: :sorted: - anchor + xylim A sequence of two (pressure, temperature) pairs that specify the bottom left-hand corner and the top right-hand corner of the plot. The pressure data points must be in units of mb or hPa, and the temperature data points must be in units of :sup:`o`\ C. diff --git a/docs/source/plotting.rst b/docs/source/plotting.rst index 15a7c09..78f4b68 100644 --- a/docs/source/plotting.rst +++ b/docs/source/plotting.rst @@ -108,7 +108,7 @@ The temperature profile of a single tephigram data set can easily be plotted. dew_point = os.path.join(tephi.DATA_DIR, 'dews.txt') dew_data = tephi.loadtxt(dew_point, column_titles=('pressure', 'dewpoint')) dews = zip(dew_data.pressure, dew_data.dewpoint) - tpg = tephi.Tephigram() + tpg = tephi.TephiAxes() tpg.plot(dews) plt.show() @@ -134,7 +134,7 @@ Plotting more than one data set is achieved by over-plotting each data set indiv dews = zip(dew_data.pressure, dew_data.dewpoint) temps = zip(temp_data.pressure, temp_data.temperature) - tpg = tephi.Tephigram() + tpg = tephi.TephiAxes() tpg.plot(dews) tpg.plot(temps) plt.show() @@ -161,7 +161,7 @@ This transparency allows full control when plotting a temperature profile on the dew_point = os.path.join(tephi.DATA_DIR, 'dews.txt') dew_data = tephi.loadtxt(dew_point, column_titles=('pressure', 'dewpoint')) dews = zip(dew_data.pressure, dew_data.dewpoint) - tpg = tephi.Tephigram() + tpg = tephi.TephiAxes() tpg.plot(dews, label='Dew-point temperature', color='blue', linewidth=2, linestyle='--', marker='s') plt.show() @@ -185,13 +185,13 @@ However, fixed axis tick locations can easily be configured for either axis if r dew_point = os.path.join(tephi.DATA_DIR, 'dews.txt') dew_data = tephi.loadtxt(dew_point, column_titles=('pressure', 'dewpoint')) dews = zip(dew_data.pressure, dew_data.dewpoint) - tpg = tephi.Tephigram(isotherm_locator=tephi.Locator(10), dry_adiabat_locator=tephi.Locator(20)) + tpg = tephi.TephiAxes(isotherm_locator=tephi.Locator(10), dry_adiabat_locator=tephi.Locator(20)) tpg.plot(dews) plt.show() The above may also be achieved without using a :class:`tephi.Locator`:: - tpg = tephi.Tephigram(isotherm_locator=10, dry_adiabat_locator=20) + tpg = tephi.TephiAxes(isotherm_locator=10, dry_adiabat_locator=20) .. _plot-anchor: @@ -202,7 +202,7 @@ Anchoring a plot By default, the tephigram will automatically center the plot around all temperature profiles. This behaviour may not be desirable when comparing separate tephigram plots against one another. -To fix the extent of a plot, simply specify an :term:`anchor` point to the tephigram. +To fix the extent of a plot, simply specify an :term:`xylim` point to the tephigram. .. plot:: :include-source: @@ -216,6 +216,6 @@ To fix the extent of a plot, simply specify an :term:`anchor` point to the tephi dew_point = os.path.join(tephi.DATA_DIR, 'dews.txt') dew_data = tephi.loadtxt(dew_point, column_titles=('pressure', 'dewpoint')) dews = zip(dew_data.pressure, dew_data.dewpoint) - tpg = tephi.Tephigram(anchor=[(1000, 0), (300, 0)]) + tpg = tephi.TephiAxes(xylim=[(1000, 0), (300, 0)]) tpg.plot(dews) plt.show() diff --git a/index.ipynb b/index.ipynb index 678dc30..9fe680b 100644 --- a/index.ipynb +++ b/index.ipynb @@ -87,8 +87,8 @@ "\n", "data_dewpoint, data_drybulb = tephi.loadtxt(fname_dewpoint, fname_drybulb, column_titles=column_titles)\n", "\n", - "dewpoint = list(zip(data_dewpoint.pressure, data_dewpoint.dewpoint))\n", - "drybulb = list(zip(data_drybulb.pressure, data_drybulb.temperature))" + "dewpoint = zip(data_dewpoint.pressure, data_dewpoint.dewpoint)\n", + "drybulb = zip(data_drybulb.pressure, data_drybulb.temperature)" ] }, { @@ -110,7 +110,7 @@ "\n", "data_barbs = tephi.loadtxt(fname_barbs, column_titles=column_titles)\n", "\n", - "barbs = list(zip(data_barbs.wind_speed, data_barbs.wind_direction, data_barbs.pressure))" + "barbs = zip(data_barbs.wind_speed, data_barbs.wind_direction, data_barbs.pressure)" ] }, { diff --git a/requirements/dev.yml b/requirements/dev.yml index bfb4229..0206356 100644 --- a/requirements/dev.yml +++ b/requirements/dev.yml @@ -4,6 +4,7 @@ channels: - nodefaults dependencies: - matplotlib>=3.10 + - shapely - numpy - scipy - pip diff --git a/requirements/rtd.yml b/requirements/rtd.yml index ccf934f..ea41852 100644 --- a/requirements/rtd.yml +++ b/requirements/rtd.yml @@ -4,6 +4,7 @@ channels: - nodefaults dependencies: - matplotlib + - shapely - numpy - scipy - sphinx diff --git a/tephi/__init__.py b/tephi/__init__.py index 4030683..076dbcf 100644 --- a/tephi/__init__.py +++ b/tephi/__init__.py @@ -12,121 +12,28 @@ """ from collections import namedtuple from collections.abc import Iterable -from functools import partial + from matplotlib.font_manager import FontProperties import matplotlib.pyplot as plt +from mpl_toolkits.axisartist import Subplot from mpl_toolkits.axisartist.grid_helper_curvelinear import ( GridHelperCurveLinear, ) -from mpl_toolkits.axisartist import Subplot -import numbers +from mpl_toolkits.axisartist.grid_finder import MaxNLocator import numpy as np import os.path - -from . import isopleths -from . import transforms - +import math +from . import artists, isopleths, transforms __version__ = "0.4.0.dev0" - -# -# Miscellaneous constants. -# -DEFAULT_WIDTH = 700 # in pixels - -ISOBAR_SPEC = [(25, 0.03), (50, 0.10), (100, 0.25), (200, 1.5)] -ISOBAR_LINE = {"color": "blue", "linewidth": 0.5, "clip_on": True} -ISOBAR_TEXT = { - "size": 8, - "color": "blue", - "clip_on": True, - "va": "bottom", - "ha": "right", -} -ISOBAR_FIXED = [50, 1000] - -WET_ADIABAT_SPEC = [(1, 0.05), (2, 0.15), (4, 1.5)] -WET_ADIABAT_LINE = {"color": "orange", "linewidth": 0.5, "clip_on": True} -WET_ADIABAT_TEXT = { - "size": 8, - "color": "orange", - "clip_on": True, - "va": "bottom", - "ha": "left", -} -WET_ADIABAT_FIXED = None - -MIXING_RATIO_SPEC = [(1, 0.05), (2, 0.18), (4, 0.3), (8, 1.5)] -MIXING_RATIO_LINE = {"color": "green", "linewidth": 0.5, "clip_on": True} -MIXING_RATIO_TEXT = { - "size": 8, - "color": "green", - "clip_on": True, - "va": "bottom", - "ha": "right", -} -MIXING_RATIOS = [ - 0.001, - 0.002, - 0.005, - 0.01, - 0.02, - 0.03, - 0.05, - 0.1, - 0.15, - 0.2, - 0.3, - 0.4, - 0.5, - 0.6, - 0.8, - 1.0, - 1.5, - 2.0, - 2.5, - 3.0, - 4.0, - 5.0, - 6.0, - 7.0, - 8.0, - 9.0, - 10.0, - 12.0, - 14.0, - 16.0, - 18.0, - 20.0, - 24.0, - 28.0, - 32.0, - 36.0, - 40.0, - 44.0, - 48.0, - 52.0, - 56.0, - 60.0, - 68.0, - 80.0, -] -MIXING_RATIO_FIXED = None - -MIN_PRESSURE = 50 # mb = hPa -MAX_PRESSURE = 1000 # mb = hPa -MIN_THETA = 0 # degC -MAX_THETA = 250 # degC -MIN_WET_ADIABAT = 1 # degC -MAX_WET_ADIABAT = 60 # degC -MIN_TEMPERATURE = -50 # degC - +from .artists import WetAdiabatArtist, IsobarArtist, HumidityMixingRatioArtist RESOURCES_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), "etc") DATA_DIR = os.path.join(RESOURCES_DIR, "test_data") - +# TODO: Decide on whether to keep this, or come up with an alternate +# method of loading files def loadtxt(*filenames, **kwargs): """ Load one or more text files of pressure, temperature, wind speed and wind @@ -137,49 +44,53 @@ def loadtxt(*filenames, **kwargs): value (degC), wind speed (knots) and wind direction value (degrees from north). - Note that blank lines and comment lines beginning with a '#' are ignored. - - For example: - - >>> import os.path - >>> import tephi - - >>> winds = os.path.join(tephi.DATA_DIR, 'barbs.txt') - >>> columns = ('pressure', 'dewpoint', 'wind_speed', 'wind_direction') - >>> data = tephi.loadtxt(winds, column_titles=columns) - >>> pressure = data.pressure - >>> dews = data.dewpoint - >>> wind_speed = data.wind_speed - >>> wind_direction = data.wind_direction - - .. seealso:: :func:`numpy.loadtxt`. - - Args: - - * filenames: one or more filenames. + Parameters + ---------- + filenames : iterable of str + One or more filenames. - Kwargs: + Other Parameters + ---------------- - * column_titles: + column_titles : list of iterables, optional List of iterables, or None. If specified, should contain one title string for each column of data per specified file. If all of multiple files loaded have the same column titles, then only one tuple of column titles need be specified. - - * delimiter: + delimiter : str, optional The string used to separate values. This is passed directly to :func:`np.loadtxt`, which defaults to using any whitespace as delimiter if this keyword is not specified. - - * dtype: + dtype : type, optional The datatype to cast the data in the text file to. Passed directly to :func:`np.loadtxt`. - Returns: - A :func:`collections.namedtuple` instance containing one tuple, named - with the relevant column title if specified, for each column of data - in the text file loaded. If more than one file is loaded, a sequence - of namedtuples is returned. + Returns + ------- + data : collections.namedtuple + Contains one tuple, named with the relevant column title if specified, + for each column of data in the text file loaded. If more than one file + is loaded, a sequence of namedtuples is returned. + + Notes + ----- + Note that blank lines and comment lines beginning with a '#' are ignored. + + Examples + -------- + >>> import os.path + >>> import tephi + >>> winds = os.path.join(tephi.DATA_DIR, 'barbs.txt') + >>> columns = ('pressure', 'dewpoint', 'wind_speed', 'wind_direction') + >>> data = tephi.loadtxt(winds, column_titles=columns) + >>> pressure = data.pressure + >>> dews = data.dewpoint + >>> wind_speed = data.wind_speed + >>> wind_direction = data.wind_direction + + See Also + -------- + :func:`numpy.loadtxt`. """ @@ -251,21 +162,27 @@ def _repr(nt): return data -class _FormatterTheta: - """Dry adiabats potential temperature axis tick formatter.""" +class _FormatterTheta(object): + """ + Dry adiabats potential temperature axis tick formatter. + + """ def __call__(self, direction, factor, values): - return [r"$\theta={:.1f}$".format(value) for value in values] + return [r"$\theta={}$".format(value) for value in values] -class _FormatterIsotherm: - """Isotherms temperature axis tick formatter.""" +class _FormatterIsotherm(object): + """ + Isotherms temperature axis tick formatter. + + """ def __call__(self, direction, factor, values): - return [r" $T={:.1f}$".format(value) for value in values] + return [r"$T={}$".format(value) for value in values] -class Locator: +class Locator(object): """ Determine the fixed step axis tick locations when called with a tick range. @@ -282,550 +199,237 @@ def __init__(self, step): >>> from tephi import Locator >>> locator = Locator(10) >>> locator(-45, 23) - (array([-50, -40, -30, -20, -10, 0, 10, 20]), 8, 1) + (array([-50, -40, -30, -20, -10, 0, 10, 20, 30]), 9, 1) Args: - * step: the step value for each axis tick. + * step: + The step value for each axis tick. """ self.step = int(step) def __call__(self, start, stop): - """Calculate the axis ticks given the provided tick range.""" + """ + Calculate the axis ticks given the provided tick range. + """ step = self.step - start = (int(start) // step) * step - stop = (int(stop) // step) * step - ticks = np.arange(start, stop + step, step, dtype=int) + start = math.floor(int(start) / step) * step + stop = math.ceil(int(stop) / step) * step + ticks = np.arange(start, stop + step, step) return ticks, len(ticks), 1 -def _refresh_isopleths(axes): - """ - Refresh the plot isobars, wet adiabats and mixing ratios and associated - text labels. - - Args: - - * axes: - Tephigram plotting :class:`matplotlib.axes.AxesSubplot` instance. - - Returns: - Boolean, whether the plot has changed. - - """ - changed = False - - # Determine the current zoom level. - xlim = axes.get_xlim() - delta_xlim = xlim[1] - xlim[0] - ylim = axes.get_ylim() - zoom = delta_xlim / axes.tephigram_original_delta_xlim - - # Determine the display mid-point. - x_point = xlim[0] + delta_xlim * 0.5 - y_point = ylim[0] + (ylim[1] - ylim[0]) * 0.5 - xy = np.array([[x_point, y_point]]) - xy_point = axes.tephigram_inverse.transform(xy)[0] - - for profile in axes.tephigram_profiles: - profile.refresh() +class TephiAxes(Subplot): + name = "tephigram" - for isopleth in axes.tephigram_isopleths: - changed = isopleth.refresh(zoom, xy_point) or changed + def __init__(self, *args, **kwargs): + # Validate the subplot arguments. - return changed - - -def _handler(event): - """Matplotlib event handler.""" - - for axes in event.canvas.figure.axes: - if hasattr(axes, "tephigram"): - if _refresh_isopleths(axes): - event.canvas.figure.show() - - -class _PlotGroup(dict): - """ - Container for a related group of tephigram isopleths. - - Manages the creation and plotting of all isopleths within the group. - - """ - - def __init__( - self, - axes, - plot_func, - text_kwargs, - step, - zoom, - tags, - fixed=None, - xfocus=None, - ): - self.axes = axes - self.text_kwargs = text_kwargs - self.step = step - self.zoom = zoom - - pairs = [] - for tag in tags: - text = plt.text(0, 0, str(tag), **text_kwargs) - text.set_bbox( - dict( - boxstyle="Round,pad=0.3", - facecolor="white", - edgecolor="white", - alpha=0.5, - clip_on=True, - clip_box=self.axes.bbox, + # TODO: Remove limit of super() behaviour. + # Currently, it only accepts format of 123 or (1, 2, 3). + if len(args) == 0: + args = (1, 1, 1) + elif (len(args) == 1 + and isinstance(args[0], tuple) + and len(args[0]) == 3): + args = args[0] + elif len(args) == 1 and isinstance(args[0], int): + args = tuple([int(c) for c in str(args[0])]) + if len(args) != 3: + msg = ( + "Integer subplot specification must be a " + "three digit number. Not {}.".format(len(args)) ) - ) - pairs.append((tag, [plot_func(tag), text])) - - dict.__init__(self, pairs) - for line, text in self.values(): - line.set_visible(True) - text.set_visible(True) - self._visible = True - - if fixed is None: - fixed = [] - - if not isinstance(fixed, Iterable): - fixed = [fixed] - - if zoom is None: - self.fixed = set(tags) - else: - self.fixed = set(tags) & set(fixed) - - self.xfocus = xfocus - - def __setitem__(self, tag, item): - emsg = "Cannot add or set an item into the plot group {!r}" - raise ValueError(emsg.format(self.step)) - - def __getitem__(self, tag): - if tag not in self.keys(): - emsg = "Tag item {!r} is not a member of the plot group {!r}" - raise KeyError(emsg.format(tag, self.step)) - return dict.__getitem__(self, tag) - - def refresh(self, zoom, xy_point): - """ - Refresh all isopleths within the plot group. - - Args: - - * zoom: - Zoom level of the current plot, relative to the initial plot. - * xy_point: - The center point of the current point, transformed into - temperature and potential temperature. - - Returns: - Boolean, whether the plot group has changed. - - """ - if self.zoom is None or zoom <= self.zoom: - changed = self._item_on() - else: - changed = self._item_off() - self._refresh_text(xy_point) - return changed - - def _item_on(self, zoom=None): - changed = False - if zoom is None or self.zoom is None or zoom <= self.zoom: - if not self._visible: - for line, text in self.values(): - line.set_visible(True) - text.set_visible(True) - changed = True - self._visible = True - return changed - - def _item_off(self, zoom=None): - changed = False - if self.zoom is not None and (zoom is None or zoom > self.zoom): - if self._visible: - for tag, (line, text) in self.items(): - if tag not in self.fixed: - line.set_visible(False) - text.set_visible(False) - changed = True - self._visible = False - return changed - - def _generate_text(self, tag, xy_point): - line, text = self[tag] - x_data = line.get_xdata() - y_data = line.get_ydata() - - if self.xfocus: - delta = np.power(x_data - xy_point[0], 2) - else: - delta = np.power(x_data - xy_point[0], 2) + np.power( - y_data - xy_point[1], 2 - ) - index = np.argmin(delta) - text.set_position((x_data[index], y_data[index])) - - def _refresh_text(self, xy_point): - if self._visible: - for tag in self: - self._generate_text(tag, xy_point) - elif self.fixed: - for tag in self.fixed: - self._generate_text(tag, xy_point) - - -class _PlotCollection: - """ - Container for tephigram isopleths. - - Manages the creation and plotting of all tephigram isobars, mixing ratio - lines and pseudo saturated wet adiabats. - - """ - - def __init__( - self, - axes, - spec, - stop, - plot_func, - text_kwargs, - fixed=None, - minimum=None, - xfocus=None, - ): - if isinstance(stop, Iterable): - if minimum and minimum > max(stop): - emsg = "Minimum value of {!r} exceeds all other values" - raise ValueError(emsg.format(minimum)) - - items = [ - [step, zoom, set(stop[step - 1 :: step])] - for step, zoom in sorted(spec, reverse=True) - ] + raise ValueError(msg) else: - if minimum and minimum > stop: - emsg = "Minimum value of {!r} exceeds maximum threshold {!r}" - raise ValueError(emsg.format(minimum, stop)) - - items = [ - [step, zoom, set(range(step, stop + step, step))] - for step, zoom in sorted(spec, reverse=True) - ] - - for index, item in enumerate(items): - if minimum: - item[2] = set([value for value in item[2] if value >= minimum]) - - for subitem in items[index + 1 :]: - subitem[2] -= item[2] - - self.groups = { - item[0]: _PlotGroup( - axes, plot_func, text_kwargs, *item, fixed=fixed, xfocus=xfocus - ) - for item in items - if item[2] - } - - if not self.groups: - emsg = "The plot collection failed to generate any plot groups" - raise ValueError(emsg) - - def refresh(self, zoom, xy_point): - """ - Refresh all isopleth groups within the plot collection. - - Args: - - * zoom: - Zoom level of the current plot, relative to the initial plot. - * xy_point: - The center point of the current plot, transformed into - temperature and potential temperature. - - Returns: - Boolean, whether any plot group has changed. - - """ - changed = False - - for group in self.groups.values(): - changed = group.refresh(zoom, xy_point) or changed + msg = "Invalid arguments: " + ", ".join(["{}" for _ in len(args)]) + raise ValueError(msg.format(*args)) - return changed + # Process the kwargs + figure = kwargs.get("figure") + if figure is None: + figure = plt.gcf() + # TODO: xylim should be split, to mirror the super() + xylim = kwargs.pop("xylim", None) -class Tephigram: - """ - Generate a tephigram of one or more pressure and temperature data sets. - - """ - - def __init__( - self, - figure=None, - isotherm_locator=None, - dry_adiabat_locator=None, - anchor=None, - ): - """ - Initialise the tephigram transformation and plot axes. - - Kwargs: - - * figure: - An existing :class:`matplotlib.figure.Figure` instance for the - tephigram plot. If a figure is not provided, a new figure will - be created by default. - * isotherm_locator: - A :class:`tephi.Locator` instance or a numeric step size - for the isotherm lines. - * dry_adiabat_locator: - A :class:`tephi.Locator` instance or a numeric step size - for the dry adiabat lines. - * anchor: - A sequence of two (pressure, temperature) pairs specifying the extent - of the tephigram plot in terms of the bottom right-hand corner, and - the top left-hand corner. Pressure data points must be in units of - mb or hPa, and temperature data points must be in units of degC. + dry_adiabat_locator = kwargs.pop("dry_adiabat_locator", None) + isotherm_locator = kwargs.pop("isotherm_locator", None) - For example: - - .. plot:: - :include-source: - - import matplotlib.pyplot as plt - from numpy import column_stack - import os.path - import tephi - from tephi import Tephigram - - dew_point = os.path.join(tephi.DATA_DIR, 'dews.txt') - dry_bulb = os.path.join(tephi.DATA_DIR, 'temps.txt') - dew_data, temp_data = tephi.loadtxt(dew_point, dry_bulb) - dews = column_stack((dew_data.pressure, dew_data.temperature)) - temps = column_stack((temp_data.pressure, temp_data.temperature)) - tpg = Tephigram() - tpg.plot(dews, label='Dew-point', color='blue', linewidth=2) - tpg.plot(temps, label='Dry-bulb', color='red', linewidth=2) - plt.show() - - """ - if not figure: - # Create a default figure. - self.figure = plt.figure(0, figsize=(9, 9)) - else: - self.figure = figure - - # Configure the locators. if isotherm_locator and not isinstance(isotherm_locator, Locator): - if not isinstance(isotherm_locator, numbers.Number): - raise ValueError("Invalid isotherm locator") - locator_isotherm = Locator(isotherm_locator) + if isinstance(isotherm_locator, int): + locator_T = MaxNLocator( + nbins=isotherm_locator, + steps=[10], + integer=True + ) + else: + raise ValueError("Invalid isotherm locator.") else: - locator_isotherm = isotherm_locator - - if dry_adiabat_locator and not isinstance( - dry_adiabat_locator, Locator - ): - if not isinstance(dry_adiabat_locator, numbers.Number): - raise ValueError("Invalid dry adiabat locator") - locator_theta = Locator(dry_adiabat_locator) + locator_T = isotherm_locator + + if dry_adiabat_locator and not isinstance(dry_adiabat_locator, Locator): + if isinstance(dry_adiabat_locator, int): + locator_theta = MaxNLocator( + nbins=dry_adiabat_locator, + steps=[10], + integer=True + ) + else: + raise ValueError("Invalid dry adiabat locator.") else: locator_theta = dry_adiabat_locator - # Define the tephigram coordinate-system transformation. - self.tephi_transform = transforms.TephiTransform() - ghelper = GridHelperCurveLinear( - self.tephi_transform, + gridder = GridHelperCurveLinear( + transforms.TephiTransform(), tick_formatter1=_FormatterIsotherm(), - grid_locator1=locator_isotherm, + grid_locator1=locator_T, tick_formatter2=_FormatterTheta(), grid_locator2=locator_theta, ) - self.axes = Subplot(self.figure, 1, 1, 1, grid_helper=ghelper) - self.transform = self.tephi_transform + self.axes.transData - self.axes.axis["isotherm"] = self.axes.new_floating_axis(1, 0) - self.axes.axis["theta"] = self.axes.new_floating_axis(0, 0) - self.axes.axis["left"].get_helper().nth_coord_ticks = 0 - self.axes.axis["left"].toggle(all=True) - self.axes.axis["bottom"].get_helper().nth_coord_ticks = 1 - self.axes.axis["bottom"].toggle(all=True) - self.axes.axis["top"].get_helper().nth_coord_ticks = 0 - self.axes.axis["top"].toggle(all=False) - self.axes.axis["right"].get_helper().nth_coord_ticks = 1 - self.axes.axis["right"].toggle(all=True) - self.axes.gridlines.set_linestyle("solid") - - self.figure.add_subplot(self.axes) - - # Configure default axes. - axis = self.axes.axis["left"] + super(TephiAxes, self).__init__( + figure, *args, grid_helper=gridder, **kwargs + ) + + # The tephigram cache. + transform = transforms.TephiTransform() + self.transData + + self.tephi = dict( + xylim=xylim, + figure=figure.add_subplot(self), + profiles=isopleths.ProfileList(), + transform=transform, + ) + + # Create each axis. + self.axis["isotherm"] = self.new_floating_axis(1, 0) + self.axis["theta"] = self.new_floating_axis(0, 0) + self.axis["left"].get_helper().nth_coord_ticks = 0 + self.axis["left"].toggle(all=True) + self.axis["bottom"].get_helper().nth_coord_ticks = 1 + self.axis["bottom"].toggle(all=True) + self.axis["top"].get_helper().nth_coord_ticks = 0 + self.axis["top"].toggle(all=False) # Turned-off + self.axis["right"].get_helper().nth_coord_ticks = 1 + self.axis["right"].toggle(all=True) + self.gridlines.set_linestyle("solid") + + # Configure each axis. + axis = self.axis["left"] axis.major_ticklabels.set_fontsize(10) axis.major_ticklabels.set_va("baseline") axis.major_ticklabels.set_rotation(135) - axis = self.axes.axis["right"] + axis = self.axis["right"] axis.major_ticklabels.set_fontsize(10) axis.major_ticklabels.set_va("baseline") axis.major_ticklabels.set_rotation(-135) - self.axes.axis["top"].major_ticklabels.set_fontsize(10) - axis = self.axes.axis["bottom"] + self.axis["top"].major_ticklabels.set_fontsize(10) + axis = self.axis["bottom"] axis.major_ticklabels.set_fontsize(10) axis.major_ticklabels.set_ha("left") - axis.major_ticklabels.set_va("top") + axis.major_ticklabels.set_va("bottom") axis.major_ticklabels.set_rotation(-45) # Isotherms: lines of constant temperature (degC). - axis = self.axes.axis["isotherm"] + axis = self.axis["isotherm"] axis.set_axis_direction("right") axis.set_axislabel_direction("-") axis.major_ticklabels.set_rotation(90) - axis.major_ticklabels.set_fontsize(10) + axis.major_ticklabels.set_fontsize(8) axis.major_ticklabels.set_va("bottom") axis.major_ticklabels.set_color("grey") - axis.major_ticklabels.set_visible(False) # turned-off + axis.major_ticklabels.set_visible(False) # Turned-off + axis.major_ticklabels.set_clip_box(self.bbox) # Dry adiabats: lines of constant potential temperature (degC). - axis = self.axes.axis["theta"] + axis = self.axis["theta"] axis.set_axis_direction("right") axis.set_axislabel_direction("+") - axis.major_ticklabels.set_fontsize(10) + axis.major_ticklabels.set_fontsize(8) axis.major_ticklabels.set_va("bottom") axis.major_ticklabels.set_color("grey") - axis.major_ticklabels.set_visible(False) # turned-off + axis.major_ticklabels.set_visible(False) # Turned-off + axis.major_ticklabels.set_clip_box(self.bbox) axis.line.set_linewidth(3) axis.line.set_linestyle("--") # Lock down the aspect ratio. - self.axes.set_aspect(1.0) - self.axes.grid(True) + self.set_aspect("equal") + self.grid(True) # Initialise the text formatter for the navigation status bar. - self.axes.format_coord = self._status_bar - - # Factor in the tephigram transform. - ISOBAR_TEXT["transform"] = self.transform - WET_ADIABAT_TEXT["transform"] = self.transform - MIXING_RATIO_TEXT["transform"] = self.transform - - # Create plot collections for the tephigram isopleths. - func = partial( - isopleths.isobar, - MIN_THETA, - MAX_THETA, - self.axes, - self.transform, - ISOBAR_LINE, - ) - self._isobars = _PlotCollection( - self.axes, - ISOBAR_SPEC, - MAX_PRESSURE, - func, - ISOBAR_TEXT, - fixed=ISOBAR_FIXED, - minimum=MIN_PRESSURE, - ) - - func = partial( - isopleths.wet_adiabat, - MAX_PRESSURE, - MIN_TEMPERATURE, - self.axes, - self.transform, - WET_ADIABAT_LINE, - ) - self._wet_adiabats = _PlotCollection( - self.axes, - WET_ADIABAT_SPEC, - MAX_WET_ADIABAT, - func, - WET_ADIABAT_TEXT, - fixed=WET_ADIABAT_FIXED, - minimum=MIN_WET_ADIABAT, - xfocus=True, - ) + self.format_coord = self._status_bar - func = partial( - isopleths.mixing_ratio, - MIN_PRESSURE, - MAX_PRESSURE, - self.axes, - self.transform, - MIXING_RATIO_LINE, - ) - self._mixing_ratios = _PlotCollection( - self.axes, - MIXING_RATIO_SPEC, - MIXING_RATIOS, - func, - MIXING_RATIO_TEXT, - fixed=MIXING_RATIO_FIXED, - ) - - # Initialise for the tephigram plot event handler. - plt.connect("motion_notify_event", _handler) - self.axes.tephigram = True - self.axes.tephigram_original_delta_xlim = DEFAULT_WIDTH - self.original_delta_xlim = DEFAULT_WIDTH - self.axes.tephigram_transform = self.tephi_transform - self.axes.tephigram_inverse = self.tephi_transform.inverted() - self.axes.tephigram_isopleths = [ - self._isobars, - self._wet_adiabats, - self._mixing_ratios, - ] - - # The tephigram profiles. - self._profiles = [] - self.axes.tephigram_profiles = self._profiles - - # Center the plot around the anchor extent. - self._anchor = anchor - if self._anchor is not None: - self._anchor = np.asarray(anchor) - if ( - self._anchor.ndim != 2 - or self._anchor.shape[-1] != 2 - or len(self._anchor) != 2 - ): + # Center the plot around the xylim extent. + if xylim is not None: + xylim = np.asarray(xylim) + if xylim.shape != (2, 2): msg = ( - "Invalid anchor, expecting [(bottom-right-pressure, " - "bottom-right-temperature), (top-left-pressure, " - "top-left-temperature)]" + "Invalid xylim, expecting [(BLHC-T, BLHC-t)," + "(TRHC-T, TRHC-t)]" ) raise ValueError(msg) - ( - (bottom_pressure, bottom_temp), - (top_pressure, top_temp), - ) = self._anchor - - if (bottom_pressure - top_pressure) < 0: - raise ValueError("Invalid anchor pressure range") - if (bottom_temp - top_temp) < 0: - raise ValueError("Invalid anchor temperature range") - - self._anchor = isopleths.Profile(anchor, self.axes) - self._anchor.plot(visible=False) - xlim, ylim = self._calculate_extents() - self.axes.set_xlim(xlim) - self.axes.set_ylim(ylim) + xlim, ylim = transforms.convert_Tt2xy(xylim[:, 0], xylim[:, 1]) + self.set_xlim(xlim) + self.set_ylim(ylim) + self.tephi["xylim"] = xlim, ylim + + def _search_artists(self, artist): + list_of_relevant_artists = [a for a in self.artists if type(a) == artist] + if len(list_of_relevant_artists) == 1: + return list_of_relevant_artists[0] + elif len(list_of_relevant_artists) == 0: + return None + else: + raise ValueError(f"Found more than one {artist} artist.") + + @property + def wet_adiabat(self): + return self._search_artists(WetAdiabatArtist) + + @wet_adiabat.setter + def wet_adiabat(self, artist): + if type(artist) is WetAdiabatArtist: + old_artist = self._search_artists(WetAdiabatArtist) + if old_artist: + old_artist.remove() + self.add_artist(artist) + else: + raise ValueError(f"Artist {artist} is not of type {WetAdiabatArtist}.") + + @property + def isobar(self): + return self._search_artists(IsobarArtist) + + @isobar.setter + def isobar(self, artist): + if type(artist) is IsobarArtist: + old_artist = self._search_artists(IsobarArtist) + if old_artist: + old_artist.remove() + self.add_artist(artist) + else: + raise ValueError(f"Artist {artist} is not of type {IsobarArtist}.") + + @property + def mixing_ratio(self): + return self._search_artists(HumidityMixingRatioArtist) + + @mixing_ratio.setter + def mixing_ratio(self, artist): + if type(artist) is HumidityMixingRatioArtist: + old_artist = self._search_artists(HumidityMixingRatioArtist) + if old_artist: + old_artist.remove() + self.add_artist(artist) + else: + raise ValueError(f"Artist {artist} is not of type {HumidityMixingRatioArtist}.") def plot(self, data, **kwargs): """ - Plot the environmental lapse rate profile of the pressure and - temperature data points. + Plot the profile of the pressure and temperature data points. The pressure and temperature data points are transformed into potential temperature and temperature data points before plotting. @@ -839,51 +443,47 @@ def plot(self, data, **kwargs): Args: - * data: (pressure, temperature) pair data points. + * data: + Pressure and temperature pair data points. .. note:: All keyword arguments are passed through to :func:`matplotlib.pyplot.plot`. - For example: - .. plot:: :include-source: import matplotlib.pyplot as plt - from tephi import Tephigram + from tephi import TephiAxes - tpg = Tephigram() + ax = TephiAxes() data = [[1006, 26.4], [924, 20.3], [900, 19.8], [850, 14.5], [800, 12.9], [755, 8.3]] - profile = tpg.plot(data, color='red', linestyle='--', - linewidth=2, marker='o') + profile = ax.plot(data, color='red', linestyle='--', + linewidth=2, marker='o') barbs = [(10, 45, 900), (20, 60, 850), (25, 90, 800)] profile.barbs(barbs) plt.show() - For associating wind barbs with an environmental lapse rate profile, - see :meth:`~tephi.isopleths.Profile.barbs`. + For associating wind barbs with the profile, see + :meth:`~tephi.isopleths.Profile.barbs`. """ - profile = isopleths.Profile(data, self.axes) + profile = isopleths.Profile(self, data) profile.plot(**kwargs) - self._profiles.append(profile) + self.tephi["profiles"].append(profile) # Center the tephigram plot around all the profiles. - if self._anchor is None: + if self.tephi["xylim"] is None: xlim, ylim = self._calculate_extents(xfactor=0.25, yfactor=0.05) - self.axes.set_xlim(xlim) - self.axes.set_ylim(ylim) - - # Refresh the tephigram plot isopleths. - _refresh_isopleths(self.axes) + self.set_xlim(xlim) + self.set_ylim(ylim) # Show the plot legend. if "label" in kwargs: font_properties = FontProperties(size="x-small") plt.legend( - loc="upper left", + loc="upper right", fancybox=True, shadow=True, prop=font_properties, @@ -891,46 +491,106 @@ def plot(self, data, **kwargs): return profile + def add_isobars( + self, + ticks=None, + line=None, + text=None, + min_theta=None, + max_theta=None, + nbins=None, + ): + self.isobar = artists.IsobarArtist( + ticks=ticks, + line=line, + text=text, + min_theta=min_theta, + max_theta=max_theta, + nbins=nbins, + ) + + def add_wet_adiabats( + self, + ticks=None, + line=None, + text=None, + min_temperature=None, + max_pressure=None, + nbins=None, + ): + self.wet_adiabat = artists.WetAdiabatArtist( + ticks=ticks, + line=line, + text=text, + min_temperature=min_temperature, + max_pressure=max_pressure, + nbins=nbins, + ) + + def add_mixing_ratios( + self, + ticks=None, + line=None, + text=None, + min_pressure=None, + max_pressure=None, + nbins=None, + ): + self.mixing_ratio = artists.HumidityMixingRatioArtist( + ticks=ticks, + line=line, + text=text, + min_pressure=min_pressure, + max_pressure=max_pressure, + nbins=nbins, + ) + def _status_bar(self, x_point, y_point): - """Generate text for the interactive backend navigation status bar.""" + """ + Generate text for the interactive backend navigation status bar. + """ temperature, theta = transforms.convert_xy2Tt(x_point, y_point) pressure, _ = transforms.convert_Tt2pT(temperature, theta) - xlim = self.axes.get_xlim() - zoom = (xlim[1] - xlim[0]) / self.original_delta_xlim - msg = "T:{:.2f}, theta:{:.2f}, phi:{:.2f} (zoom:{:.3f})" - text = msg.format( - float(temperature), float(theta), float(pressure), zoom - ) - - return text + text = "T={:.2f}\u00b0C, \u03b8={:.2f}\u00b0C, p={:.2f}hPa" + return text.format(float(temperature), float(theta), float(pressure)) def _calculate_extents(self, xfactor=None, yfactor=None): - min_x = min_y = 1e10 - max_x = max_y = -1e-10 - profiles = self._profiles - transform = self.tephi_transform.transform - - if self._anchor is not None: - profiles = [self._anchor] - - for profile in profiles: - temperature = profile.temperature.reshape(-1, 1) - theta = profile.theta.reshape(-1, 1) - xy_points = transform(np.concatenate((temperature, theta), axis=1)) - x_points = xy_points[:, 0] - y_points = xy_points[:, 1] - min_x = np.min([min_x, np.min(x_points)]) - min_y = np.min([min_y, np.min(y_points)]) - max_x = np.max([max_x, np.max(x_points)]) - max_y = np.max([max_y, np.max(y_points)]) - - if xfactor is not None: - delta_x = max_x - min_x - min_x, max_x = min_x - xfactor * delta_x, max_x + xfactor * delta_x - - if yfactor is not None: - delta_y = max_y - min_y - min_y, max_y = min_y - yfactor * delta_y, max_y + yfactor * delta_y - - return ([min_x, max_x], [min_y, max_y]) + min_x = min_y = np.inf + max_x = max_y = -np.inf + + if self.tephi["xylim"] is not None: + xlim, ylim = self.tephi["xylim"] + else: + for profile in self.tephi["profiles"]: + temperature = profile.points.temperature + theta = profile.points.theta + x_points, y_points = transforms.convert_Tt2xy( + temperature, theta + ) + min_x, min_y = ( + np.min([min_x, np.min(x_points)]), + np.min([min_y, np.min(y_points)]), + ) + max_x, max_y = ( + np.max([max_x, np.max(x_points)]), + np.max([max_y, np.max(y_points)]), + ) + + if xfactor is not None: + delta_x = max_x - min_x + min_x, max_x = ( + (min_x - xfactor * delta_x), + (max_x + xfactor * delta_x), + ) + + if yfactor is not None: + delta_y = max_y - min_y + min_y, max_y = ( + (min_y - yfactor * delta_y), + (max_y + yfactor * delta_y), + ) + + xlim, ylim = (min_x, max_x), (min_y, max_y) + + return xlim, ylim diff --git a/tephi/artists.py b/tephi/artists.py new file mode 100644 index 0000000..2759cf2 --- /dev/null +++ b/tephi/artists.py @@ -0,0 +1,307 @@ +import matplotlib.artist +import numpy as np +from scipy.interpolate import interp1d +from shapely.geometry import LineString, Polygon +from shapely.prepared import prep + +from .constants import default +from .isopleths import Isobar, WetAdiabat, HumidityMixingRatio +from .transforms import convert_xy2Tt, convert_Tt2pT + + +class IsoplethArtist(matplotlib.artist.Artist): + def __init__(self): + super(IsoplethArtist, self).__init__() + self._isopleths = None + + def _locator(self, x0, x1, y0, y1): + temperature, theta = convert_xy2Tt([x0, x0, x1, x1], [y0, y1, y1, y0]) + bbox = prep(Polygon(zip(temperature, theta))) + mask = [bbox.intersects(item.geometry) for item in self._isopleths] + mask = np.asarray(mask) + + if self.nbins: + indices = np.where(mask)[0] + if indices.size: + if self.nbins < indices.size: + mask[:] = False + upint = indices.size + self.nbins - 1 + # this is an ugly solution, I'm sure there must be better ones + mask[indices[:: upint // self.nbins + 1]] = True + + return mask + + +class IsobarArtist(IsoplethArtist): + def __init__( + self, + ticks=None, + line=None, + text=None, + min_theta=None, + max_theta=None, + nbins=None, + ): + super(IsobarArtist, self).__init__() + if ticks is None: + ticks = default.get("isobar_ticks") + self.ticks = ticks + self._kwargs = {} + if line is None: + line = default.get("isobar_line") + self._kwargs["line"] = line + if text is None: + text = default.get("isobar_text") + self._kwargs["text"] = text + if min_theta is None: + min_theta = default.get("isobar_min_theta") + self.min_theta = min_theta + if max_theta is None: + max_theta = default.get("isobar_max_theta") + self.max_theta = max_theta + if nbins is None: + nbins = default.get("isobar_nbins") + elif nbins < 2 or isinstance(nbins, str): + nbins = None + self.nbins = nbins + + @matplotlib.artist.allow_rasterization + def draw( + self, renderer, line=None, text=None, min_theta=None, max_theta=None + ): + if not self.get_visible(): + return + axes = self.axes + draw_kwargs = dict(self._kwargs["line"]) + if line is not None: + draw_kwargs.update(line) + text_kwargs = dict(self._kwargs["text"]) + if text is not None: + text_kwargs.update(text) + if min_theta is None: + min_theta = self.min_theta + if max_theta is None: + max_theta = self.max_theta + + if self._isopleths is None: + isobars = [] + for tick in self.ticks: + isobars.append(Isobar(axes, tick, min_theta, max_theta)) + self._isopleths = np.asarray(isobars) + + (x0, x1), (y0, y1) = axes.get_xlim(), axes.get_ylim() + mask = self._locator(x0, x1, y0, y1) + + mx = x0 + axes.viewLim.width * 0.5 + temperature, theta = convert_xy2Tt([mx, mx], [y0, y1]) + text_line = LineString(zip(temperature, theta)) + + temperature, theta = convert_xy2Tt([mx] * 50, np.linspace(y0, y1, 50)) + pressure, _ = convert_Tt2pT(temperature, theta) + func = interp1d(pressure, theta, bounds_error=False) + + for isobar in self._isopleths[mask]: + isobar.draw(renderer, **draw_kwargs) + point = text_line.intersection(isobar.geometry) + if point: + isobar.refresh( + point.x, point.y, renderer=renderer, **text_kwargs + ) + else: + if func(isobar.data) < isobar.extent.theta.lower: + T = isobar.points.temperature[isobar.index.theta.lower] + t = isobar.extent.theta.lower + else: + T = isobar.points.temperature[isobar.index.theta.upper] + t = isobar.extent.theta.upper + isobar.refresh(T, t, renderer=renderer, **text_kwargs) + + +class WetAdiabatArtist(IsoplethArtist): + def __init__( + self, + ticks=None, + line=None, + text=None, + min_temperature=None, + max_pressure=None, + nbins=None, + ): + super(WetAdiabatArtist, self).__init__() + if ticks is None: + ticks = default.get("wet_adiabat_ticks") + self.ticks = sorted(ticks) + self._kwargs = {} + if line is None: + line = default.get("wet_adiabat_line") + self._kwargs["line"] = line + if text is None: + text = default.get("wet_adiabat_text") + self._kwargs["text"] = text + if min_temperature is None: + min_temperature = default.get("wet_adiabat_min_temperature") + self.min_temperature = min_temperature + if max_pressure is None: + max_pressure = default.get("wet_adiabat_max_pressure") + self.max_pressure = max_pressure + if nbins is None: + nbins = default.get("wet_adiabat_nbins") + if nbins < 2 or isinstance(nbins, str): + nbins = None + self.nbins = nbins + + @matplotlib.artist.allow_rasterization + def draw( + self, + renderer, + line=None, + text=None, + min_temperature=None, + max_pressure=None, + ): + if not self.get_visible(): + return + axes = self.axes + draw_kwargs = dict(self._kwargs["line"]) + if line is not None: + draw_kwargs.update(line) + text_kwargs = dict(self._kwargs["text"]) + if text is not None: + text_kwargs.update(text) + if min_temperature is None: + min_temperature = self.min_temperature + if max_pressure is None: + max_pressure = self.max_pressure + + if self._isopleths is None: + adiabats = [] + for tick in self.ticks: + adiabats.append( + WetAdiabat(axes, tick, min_temperature, max_pressure) + ) + self._isopleths = np.asarray(adiabats) + + (x0, x1), (y0, y1) = axes.get_xlim(), axes.get_ylim() + mask = self._locator(x0, x1, y0, y1) + + mx = x0 + axes.viewLim.width * 0.5 + my = y0 + axes.viewLim.height * 0.5 + temperature, theta = convert_xy2Tt([x0, mx, x1], [y0, my, y1]) + text_line = LineString(zip(temperature, theta)) + mT = temperature[1] + snap = None + + for adiabat in self._isopleths[mask]: + adiabat.draw(renderer, **draw_kwargs) + point = text_line.intersection(adiabat.geometry) + if point: + adiabat.refresh( + point.x, point.y, renderer=renderer, **text_kwargs + ) + else: + upper = abs(adiabat.extent.temperature.upper - mT) + lower = abs(adiabat.extent.temperature.lower - mT) + if snap == "upper" or upper < lower: + T = adiabat.extent.temperature.upper + t = adiabat.points.theta[adiabat.index.temperature.upper] + snap = "upper" + else: + T = adiabat.extent.temperature.lower + t = adiabat.points.theta[adiabat.index.temperature.lower] + snap = "lower" + adiabat.refresh(T, t, renderer=renderer, **text_kwargs) + + +class HumidityMixingRatioArtist(IsoplethArtist): + def __init__( + self, + ticks=None, + line=None, + text=None, + min_pressure=None, + max_pressure=None, + nbins=None, + ): + super(HumidityMixingRatioArtist, self).__init__() + if ticks is None: + ticks = default.get("mixing_ratio_ticks") + self.ticks = ticks + self._kwargs = {} + if line is None: + line = default.get("mixing_ratio_line") + self._kwargs["line"] = line + if text is None: + text = default.get("mixing_ratio_text") + self._kwargs["text"] = text + if min_pressure is None: + min_pressure = default.get("mixing_ratio_min_pressure") + self.min_pressure = min_pressure + if max_pressure is None: + max_pressure = default.get("mixing_ratio_max_pressure") + self.max_pressure = max_pressure + if nbins is None: + nbins = default.get("mixing_ratio_nbins") + if nbins < 2 or isinstance(nbins, str): + nbins = None + self.nbins = nbins + + @matplotlib.artist.allow_rasterization + def draw( + self, + renderer, + line=None, + text=None, + min_pressure=None, + max_pressure=None, + ): + if not self.get_visible(): + return + axes = self.axes + draw_kwargs = dict(self._kwargs["line"]) + if line is not None: + draw_kwargs.update(line) + text_kwargs = dict(self._kwargs["text"]) + if text is not None: + text_kwargs.update(text) + if min_pressure is None: + min_pressure = self.min_pressure + if max_pressure is None: + max_pressure = self.max_pressure + + if self._isopleths is None: + ratios = [] + for tick in self.ticks: + ratios.append( + HumidityMixingRatio(axes, tick, min_pressure, max_pressure) + ) + self._isopleths = np.asarray(ratios) + + (x0, x1), (y0, y1) = axes.get_xlim(), axes.get_ylim() + mask = self._locator(x0, x1, y0, y1) + + mx = x0 + axes.viewLim.width * 0.5 + my = y0 + axes.viewLim.height * 0.5 + temperature, theta = convert_xy2Tt([x0, mx, x1], [y1, my, y0]) + text_line = LineString(zip(temperature, theta)) + mt = theta[1] + snap = None + + for ratio in self._isopleths[mask]: + ratio.draw(renderer, **draw_kwargs) + point = text_line.intersection(ratio.geometry) + if point: + ratio.refresh( + point.x, point.y, renderer=renderer, **text_kwargs + ) + else: + upper = abs(ratio.extent.theta.upper - mt) + lower = abs(ratio.extent.theta.lower - mt) + if snap == "upper" or upper < lower: + T = ratio.points.temperature[ratio.index.theta.upper] + t = ratio.extent.theta.upper + snap = "upper" + else: + T = ratio.points.temperature[ratio.index.theta.lower] + t = ratio.extent.theta.lower + snap = "lower" + ratio.refresh(T, t, renderer=renderer, **text_kwargs) diff --git a/tephi/constants.py b/tephi/constants.py new file mode 100644 index 0000000..d162a1f --- /dev/null +++ b/tephi/constants.py @@ -0,0 +1,135 @@ +# Copyright Tephi contributors +# +# This file is part of Tephi and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. +"""Tephigram transform and isopleth constants.""" + +# The specific heat capacity of dry air at a constant pressure, +# in units of J kg-1 K-1. +# TBC: This was originally set to 1.01e3 +Cp = 1004.0 + +# Dimensionless ratio: Rd / Cp. +K = 0.286 + +# Conversion offset between degree Celsius and Kelvin. +KELVIN = 273.15 + +# The specific latent heat of vapourisation of water at 0 degC, +# in units of J kg-1. +L = 2.501e6 + +MA = 300.0 + +# The specific gas constant for dry air, in units of J kg-1 K-1. +Rd = 287.0 + +# The specific gas constant for water vapour, in units of J kg-1 K-1. +Rv = 461.0 + +# Dimensionless ratio: Rd / Rv. +E = 0.622 + +# Base surface pressure. +P_BASE = 1000.0 + +# TODO: add in hodograph and mode defaults +default = { + "barbs_gutter": 0.1, + "barbs_length": 7, + "barbs_linewidth": 1.5, + "barbs_zorder": 10, + "isobar_line": dict(color="blue", linewidth=0.5, clip_on=True), + "isobar_min_theta": 0, + "isobar_max_theta": 250, + "isobar_nbins": None, + "isobar_text": dict( + size=8, color="blue", clip_on=True, va="bottom", ha="right" + ), + "isobar_ticks": [ + 1050, + 1000, + 950, + 900, + 850, + 800, + 700, + 600, + 500, + 400, + 300, + 250, + 200, + 150, + 100, + 70, + 50, + 40, + 30, + 20, + 10, + ], + "isopleth_picker": 3, + "isopleth_zorder": 10, + "mixing_ratio_line": dict(color="green", linewidth=0.5, clip_on=True), + "mixing_ratio_text": dict( + size=8, color="green", clip_on=True, va="bottom", ha="right" + ), + "mixing_ratio_min_pressure": 10, + "mixing_ratio_max_pressure": P_BASE, + "mixing_ratio_nbins": 10, + "mixing_ratio_ticks": [ + 0.001, + 0.002, + 0.005, + 0.01, + 0.02, + 0.03, + 0.05, + 0.1, + 0.15, + 0.2, + 0.3, + 0.4, + 0.5, + 0.6, + 0.8, + 1.0, + 1.5, + 2.0, + 2.5, + 3.0, + 4.0, + 5.0, + 6.0, + 7.0, + 8.0, + 9.0, + 10.0, + 12.0, + 14.0, + 16.0, + 18.0, + 20.0, + 24.0, + 28.0, + 32.0, + 36.0, + 40.0, + 44.0, + 48.0, + 52.0, + 56.0, + 60.0, + 68.0, + 80.0, + ], + "wet_adiabat_line": dict(color="orange", linewidth=0.5, clip_on=True), + "wet_adiabat_min_temperature": -50, + "wet_adiabat_max_pressure": P_BASE, + "wet_adiabat_nbins": 10, + "wet_adiabat_text": dict( + size=8, color="orange", clip_on=True, va="top", ha="left" + ), + "wet_adiabat_ticks": range(1, 61), +} diff --git a/tephi/isopleths.py b/tephi/isopleths.py index f6d698e..b5807b0 100644 --- a/tephi/isopleths.py +++ b/tephi/isopleths.py @@ -7,21 +7,29 @@ environment profiles and barbs. """ + +from __future__ import absolute_import, division, print_function + +from abc import ABCMeta, abstractmethod +from collections import namedtuple import math +import matplotlib.artist from matplotlib.collections import PathCollection from matplotlib.path import Path import matplotlib.pyplot as plt -import matplotlib.transforms as mtransforms +import matplotlib.transforms as mtrans +from mpl_toolkits.axisartist import Subplot import numpy as np +from shapely.geometry import LineString from scipy.interpolate import interp1d -from ._constants import CONST_CP, CONST_L, CONST_KELVIN, CONST_RD, CONST_RV -from . import transforms +import tephi.constants as constants +from tephi.constants import default +import tephi.transforms as transforms # Wind barb speed (knots) ranges used since 1 January 1955. _BARB_BINS = np.arange(20) * 5 + 3 -_BARB_GUTTER = 0.1 _BARB_DTYPE = np.dtype( dict( names=("speed", "angle", "pressure", "barb"), @@ -29,216 +37,51 @@ ) ) -# -# Reference: http://www-nwp/~hadaa/tephigram/tephi_plot.html -# - - -def mixing_ratio( - min_pressure, max_pressure, axes, transform, kwargs, mixing_ratio_value -): - """ - Generate and plot a humidity mixing ratio line. - - A line of constant saturation mixing ratio with respect to a - plane water surface (g kg-1). - - Args: - - * min_pressure: - Minumum pressure, in mb or hPa, for the mixing ratio line extent. - - * max_pressure: - Maximum pressure, in mb or hPa, for the mixing ratio line extent. - - * axes: - Tephigram plotting :class:`matplotlib.axes.AxesSubplot` instance. - - * transform: - Tephigram plotting transformation - :class:`matplotlib.transforms.CompositeGenericTransform` instance. - - * kwargs: - Keyword arguments for the mixing ratio :class:`matplotlib.lines.Line2D` - instance. - - * mixing_ratio_value: - The mixing ratio value to be plotted. - - Returns: - The mixing ratio :class:`matplotlib.lines.Line2D` instance. - - """ - pressures = np.linspace(min_pressure, max_pressure, 100) - temps = transforms.convert_pw2T(pressures, mixing_ratio_value) - _, thetas = transforms.convert_pT2Tt(pressures, temps) - (line,) = axes.plot(temps, thetas, transform=transform, **kwargs) - - return line - - -def isobar(min_theta, max_theta, axes, transform, kwargs, pressure): - """ - Generate and plot an isobar line. - - A line of constant pressure (mb). - - Args: - - * min_theta: - Minimum potential temperature, in degC, for the isobar extent. - - * max_theta: - Maximum potential temperature, in degC, for the isobar extent. - - * axes: - Tephigram plotting :class:`matplotlib.axes.AxesSubplot` instance. - - * transform: - Tephigram plotting transformation - :class:`matplotlib.transforms.CompositeGenericTransform` instance. - - * kwargs: - Keyword arguments for the isobar :class:`matplotlib.lines.Line2D` - instance. - - * pressure: - The isobar pressure value, in mb or hPa, to be plotted. - - Returns: - The isobar :class:`matplotlib.lines.Line2D` instance. - - """ - steps = 100 - thetas = np.linspace(min_theta, max_theta, steps) - _, temps = transforms.convert_pt2pT([pressure] * steps, thetas) - (line,) = axes.plot(temps, thetas, transform=transform, **kwargs) - - return line - - -def _wet_adiabat_gradient(min_temperature, pressure, temperature, dp): - """ - Calculate the wet adiabat change in pressure and temperature. - - Args: - - * min_temperature: - Minimum potential temperature, in degC, for the wet adiabat line - extent. - - * pressure: - Pressure point value, in mb or hPa, from which to calculate the - gradient difference. - - * temperature: - Potential temperature point value, in degC, from which to calculate - the gradient difference. - - * dp: - The wet adiabat change in pressure, in mb or hPa, from which to - calculate the gradient difference. - - Returns: - The gradient change as a (pressure, potential-temperature) value pair. - - """ - - # TODO: Discover the meaning of the magic numbers. - - kelvin = temperature + CONST_KELVIN - lsbc = (CONST_L / CONST_RV) * ((1.0 / CONST_KELVIN) - (1.0 / kelvin)) - rw = 6.11 * np.exp(lsbc) * (0.622 / pressure) - lrwbt = (CONST_L * rw) / (CONST_RD * kelvin) - nume = ((CONST_RD * kelvin) / (CONST_CP * pressure)) * (1.0 + lrwbt) - deno = 1.0 + (lrwbt * ((0.622 * CONST_L) / (CONST_CP * kelvin))) - gradi = nume / deno - dt = dp * gradi - - if (temperature + dt) < min_temperature: - dt = min_temperature - temperature - dp = dt / gradi +# Isopleth defaults. +_DRY_ADIABAT_STEPS = 50 +_HUMIDITY_MIXING_RATIO_STEPS = 50 +_ISOBAR_STEPS = 50 +_ISOTHERM_STEPS = 50 +_SATURATION_ADIABAT_PRESSURE_DELTA = -5.0 - return dp, dt +BOUNDS = namedtuple("BOUNDS", "lower upper") +POINTS = namedtuple("POINTS", "temperature theta pressure") -def wet_adiabat( - max_pressure, min_temperature, axes, transform, kwargs, temperature -): - """ - Generate and plot a pseudo saturated wet adiabat line. - - A line of constant equivalent potential temperature for saturated - air parcels (degC). - - Args: - - * max_pressure: - Maximum pressure, in mb or hPa, for the wet adiabat line extent. - - * min_temperature: - Minimum potential temperature, in degC, for the wet adiabat line - extent. - - * axes: - Tephigram plotting :class:`matplotlib.axes.AxesSubplot` instance. - - * transform: - Tephigram plotting transformation - :class:`matplotlib.transforms.CompositeGenericTransform` instance. - - * kwargs: - Keyword arguments for the mixing ratio :class:`matplotlib.lines.Line2D` - instance. - - * temperature: - The wet adiabat value, in degC, to be plotted. - - Returns: - The wet adiabat :class:`matplotlib.lines.Line2D` instance. - - """ - temps = [temperature] - pressures = [max_pressure] - dp = -5.0 - - for i in range(200): - dp, dt = _wet_adiabat_gradient( - min_temperature, pressures[i], temps[i], dp +class BarbArtist(matplotlib.artist.Artist): + def __init__(self, barbs, **kwargs): + super(BarbArtist, self).__init__() + self._gutter = kwargs.pop("gutter", default.get("barbs_gutter")) + self._kwargs = dict( + length=default.get("barbs_length"), + zorder=default.get("barbs_zorder", 10), ) - temps.append(temps[i] + dt) - pressures.append(pressures[i] + dp) - - _, thetas = transforms.convert_pT2Tt(pressures, temps) - (line,) = axes.plot(temps, thetas, transform=transform, **kwargs) - - return line - - -class Barbs: - """Generate a wind arrow barb.""" - - def __init__(self, axes): - """ - Create a wind arrow barb for the given axes. - - Args: - - * axes: - A :class:`matplotlib.axes.AxesSubplot` instance. - - """ - self.axes = axes - self.barbs = None - self._gutter = None - self._transform = axes.tephigram_transform + axes.transData - self._kwargs = None - self._custom_kwargs = None - self._custom = dict( + self._kwargs.update(kwargs) + self.set_zorder(self._kwargs["zorder"]) + self._path_kwargs = dict( + color=None, + linewidth=default.get("barbs_linewidth"), + zorder=self._kwargs["zorder"], + ) + alias_by_kwarg = dict( color=["barbcolor", "color", "edgecolor", "facecolor"], linewidth=["lw", "linewidth"], linestyle=["ls", "linestyle"], ) + for kwarg, alias in iter(alias_by_kwarg.items()): + common = set(alias).intersection(kwargs) + if common: + self._path_kwargs[kwarg] = kwargs[sorted(common)[0]] + barbs = np.asarray(list(barbs)) + if barbs.ndim != 2 or barbs.shape[-1] != 3: + msg = ( + "The barbs require to be a sequence of wind speed, " + "wind direction and pressure value triples." + ) + raise ValueError(msg) + self.barbs = np.empty(barbs.shape[0], dtype=_BARB_DTYPE) + for i, barb in enumerate(barbs): + self.barbs[i] = tuple(barb) + (None,) @staticmethod def _uv(magnitude, angle): @@ -281,6 +124,7 @@ def _uv(magnitude, angle): def _make_barb(self, temperature, theta, speed, angle): """Add the barb to the plot at the specified location.""" + transform = self.axes.tephi["transform"] u, v = self._uv(speed, angle) if 0 < speed < _BARB_BINS[0]: # Plot the missing barbless 1-2 knots line. @@ -289,8 +133,9 @@ def _make_barb(self, temperature, theta, speed, angle): pivot = self._kwargs.get("pivot", "tip") offset = pivot_points[pivot] verts = [(0.0, offset), (0.0, length + offset)] - rangle = math.radians(-angle) - verts = mtransforms.Affine2D().rotate(rangle).transform(verts) + verts = ( + mtrans.Affine2D().rotate(math.radians(-angle)).transform(verts) + ) codes = [Path.MOVETO, Path.LINETO] path = Path(verts, codes) size = length**2 / 4 @@ -299,165 +144,298 @@ def _make_barb(self, temperature, theta, speed, angle): [path], (size,), offsets=xy, - transOffset=self._transform, - **self._custom_kwargs, + transOffset=transform, + **self._path_kwargs, ) - barb.set_transform(mtransforms.IdentityTransform()) - self.axes.add_collection(barb) + barb.set_transform(mtrans.IdentityTransform()) else: - barb = plt.barbs( - temperature, - theta, - u, - v, - transform=self._transform, - **self._kwargs, + barb = self.axes.barbs( + temperature, theta, u, v, transform=transform, **self._kwargs ) + collections = list(self.axes.collections).remove(barb) + if collections: + self.axes.collections = tuple(collections) return barb - def refresh(self): - """Refresh the plot with the barbs.""" - if self.barbs is not None: - xlim = self.axes.get_xlim() - ylim = self.axes.get_ylim() - y = np.linspace(*ylim)[::-1] - xdelta = xlim[1] - xlim[0] - x = np.ones(y.size) * (xlim[1] - (xdelta * self._gutter)) - xy = np.column_stack((x, y)) - points = self.axes.tephigram_inverse.transform(xy) - temperature, theta = points[:, 0], points[:, 1] - pressure, _ = transforms.convert_Tt2pT(temperature, theta) - min_pressure, max_pressure = np.min(pressure), np.max(pressure) - func = interp1d(pressure, temperature) - for i, (speed, angle, pressure, barb) in enumerate(self.barbs): - if min_pressure < pressure < max_pressure: - p2T = func(pressure) - temperature, theta = transforms.convert_pT2Tt( - pressure, p2T - ) - if barb is None: - self.barbs[i]["barb"] = self._make_barb( - temperature, theta, speed, angle - ) - else: - barb.set_offsets(np.array([[temperature, theta]])) - barb.set_visible(True) + @matplotlib.artist.allow_rasterization + def draw(self, renderer): + if not self.get_visible(): + return + axes = self.axes + x0, x1 = axes.get_xlim() + y0, y1 = axes.get_ylim() + y = np.linspace(y0, y1)[::-1] + x = np.asarray([x1 - ((x1 - x0) * self._gutter)] * y.size) + temperature, theta = transforms.convert_xy2Tt(x, y) + pressure, _ = transforms.convert_Tt2pT(temperature, theta) + min_pressure, max_pressure = np.min(pressure), np.max(pressure) + func = interp1d(pressure, temperature) + for i, (speed, angle, pressure, barb) in enumerate(self.barbs): + if min_pressure < pressure < max_pressure: + temperature, theta = transforms.convert_pT2Tt( + pressure, func(pressure) + ) + if barb is None: + barb = self._make_barb(temperature, theta, speed, angle) + self.barbs[i]["barb"] = barb else: - if barb is not None: - barb.set_visible(False) + barb.set_offsets(np.array([[temperature, theta]])) - def plot(self, barbs, **kwargs): - """ - Plot the sequence of barbs. + # collections are not automatically added to the figure + barb.set_figure(self.axes.figure) + barb.draw(renderer) - Args: +class Isopleth(object): + __metaclass__ = ABCMeta - * barbs: - Sequence of speed, direction and pressure value triples for - each barb. Where speed is measured in units of knots, direction - in units of degrees (clockwise from north), and pressure must - be in units of mb or hPa. + def __init__(self, axes): + self.axes = axes + self._transform = axes.tephi["transform"] + self.points = self._generate_points() + self.geometry = LineString( + np.vstack((self.points.temperature, self.points.theta)).T + ) + self.line = None + self.label = None + self._kwargs = dict(line={}, text={}) + Tmin, Tmax = ( + np.argmin(self.points.temperature), + np.argmax(self.points.temperature), + ) + tmin, tmax = ( + np.argmin(self.points.theta), + np.argmax(self.points.theta), + ) + pmin, pmax = ( + np.argmin(self.points.pressure), + np.argmax(self.points.pressure), + ) + self.index = POINTS( + BOUNDS(Tmin, Tmax), BOUNDS(tmin, tmax), BOUNDS(pmin, pmax) + ) + self.extent = POINTS( + BOUNDS( + self.points.temperature[Tmin], self.points.temperature[Tmax] + ), + BOUNDS(self.points.theta[tmin], self.points.theta[tmax]), + BOUNDS(self.points.pressure[pmin], self.points.pressure[pmax]), + ) - Kwargs: + @abstractmethod + def _generate_points(self): + pass + + def draw(self, renderer, **kwargs): + if self.line is None: + if "zorder" not in kwargs: + kwargs["zorder"] = default.get("isopleth_zorder") + draw_kwargs = dict(self._kwargs["line"]) + draw_kwargs.update(kwargs) + self.line = plt.Line2D( + self.points.temperature, + self.points.theta, + transform=self._transform, + **draw_kwargs, + ) + self.line.set_clip_box(self.axes.bbox) + self.line.draw(renderer) + return self.line - * gutter: - Proportion offset from the right hand side axis to plot the - barbs. Defaults to 0.1 + def plot(self, **kwargs): + """ + Plot the points of the isopleth. - Also see :func:`matplotlib.pyplot.barbs` + Kwargs: + See :func:`matplotlib.pyplot.plot`. + + Returns: + The isopleth :class:`matplotlib.lines.Line2D` """ - self._gutter = kwargs.pop("gutter", _BARB_GUTTER) - # zorder of 4.1 is higher than all MPL defaults, excluding legend. Also - # higher than tephi default for plot-lines. - self._kwargs = dict(length=7, zorder=4.1) - self._kwargs.update(kwargs) - self._custom_kwargs = dict( - color=None, linewidth=1.5, zorder=self._kwargs["zorder"] + if self.line is not None: + if self.line in self.axes.lines: + self.axes.lines.remove(self.line) + if "zorder" not in kwargs: + kwargs["zorder"] = default.get("isopleth_zorder") + if "picker" not in kwargs: + kwargs["picker"] = default.get("isopleth_picker") + plot_kwargs = dict(self._kwargs["line"]) + plot_kwargs.update(kwargs) + (self.line,) = Subplot.plot( + self.axes, + self.points.temperature, + self.points.theta, + transform=self._transform, + **plot_kwargs, ) - for key, values in self._custom.items(): - common = set(values).intersection(kwargs) - if common: - self._custom_kwargs[key] = kwargs[sorted(common)[0]] - if hasattr(barbs, "__next__"): - barbs = list(barbs) - barbs = np.asarray(barbs) - if barbs.ndim != 2 or barbs.shape[-1] != 3: - msg = ( - "The barbs require to be a sequence of wind speed, " - "wind direction and pressure value triples." - ) - raise ValueError(msg) - self.barbs = np.empty(barbs.shape[0], dtype=_BARB_DTYPE) - for i, barb in enumerate(barbs): - self.barbs[i] = tuple(barb) + (None,) - self.refresh() + return self.line + def text(self, temperature, theta, text, **kwargs): + if "zorder" not in kwargs: + kwargs["zorder"] = default.get("isopleth_zorder", 10) + 1 + text_kwargs = dict(self._kwargs["text"]) + text_kwargs.update(kwargs) + if self.label is not None and self.label in self.axes.texts: + self.axes.lines.remove(self.label) + self.label = self.axes.text( + temperature, + theta, + str(text), + transform=self._transform, + **text_kwargs, + ) + self.label.set_bbox( + dict( + boxstyle="Round,pad=0.3", + facecolor="white", + edgecolor="white", + alpha=0.5, + clip_on=True, + clip_box=self.axes.bbox, + ) + ) + return self.label + + def refresh(self, temperature, theta, renderer=None, **kwargs): + if self.label is None: + self.text(temperature, theta, self.data, **kwargs) + if renderer is not None: + try: + self.axes.tests = tuple( + list(self.axes.texts).remove(self.label) + ) + except TypeError: + self.axes.tests = None + else: + self.label.set_position((temperature, theta)) + if renderer is not None: + self.label.draw(renderer) -class Profile: - """Generate an environmental lapse rate profile.""" - def __init__(self, data, axes): - """ - Create an environmental lapse rate profile from the sequence of - pressure and temperature point data. +class DryAdiabat(Isopleth): + def __init__(self, axes, theta, min_pressure, max_pressure): + self.data = theta + self.bounds = BOUNDS(min_pressure, max_pressure) + self._steps = _DRY_ADIABAT_STEPS + super(DryAdiabat, self).__init__(axes) - Args: + def _generate_points(self): + pressure = np.linspace( + self.bounds.lower, self.bounds.upper, self._steps + ) + theta = np.asarray([self.data] * self._steps) + _, temperature = transforms.convert_pt2pT(pressure, theta) + return POINTS(temperature, theta, pressure) - * data: - Sequence of pressure and temperature points defining the - environmental lapse rate. - * axes: - The axes on which to plot the profile. +class HumidityMixingRatio(Isopleth): + def __init__(self, axes, mixing_ratio, min_pressure, max_pressure): + self.data = mixing_ratio + self.bounds = BOUNDS(min_pressure, max_pressure) + self._step = _HUMIDITY_MIXING_RATIO_STEPS + super(HumidityMixingRatio, self).__init__(axes) - """ - if hasattr(data, "__next__"): - data = list(data) - self.data = np.asarray(data) - if self.data.ndim != 2 or self.data.shape[-1] != 2: - msg = ( - "The environment profile data requires to be a sequence " - "of (pressure, temperature) value pairs." - ) - raise ValueError(msg) - self.axes = axes - self._transform = axes.tephigram_transform + axes.transData - self.pressure = self.data[:, 0] - self.temperature = self.data[:, 1] - _, self.theta = transforms.convert_pT2Tt( - self.pressure, self.temperature + def _generate_points(self): + pressure = np.linspace( + self.bounds.lower, self.bounds.upper, self._step ) - self.line = None - self._barbs = Barbs(axes) + temperature = transforms.convert_pw2T(pressure, self.data) + _, theta = transforms.convert_pT2Tt(pressure, temperature) + return POINTS(temperature, theta, pressure) + + +class Isobar(Isopleth): + def __init__(self, axes, pressure, min_theta, max_theta): + self.data = pressure + self.bounds = BOUNDS(min_theta, max_theta) + self._steps = _ISOBAR_STEPS + super(Isobar, self).__init__(axes) + self._kwargs["line"] = default.get("isobar_line") + self._kwargs["text"] = default.get("isobar_text") + + def _generate_points(self): + pressure = np.asarray([self.data] * self._steps) + theta = np.linspace(self.bounds.lower, self.bounds.upper, self._steps) + _, temperature = transforms.convert_pt2pT(pressure, theta) + return POINTS(temperature, theta, pressure) + + +class Isotherm(Isopleth): + def __init__(self, axes, temperature, min_pressure, max_pressure): + self.data = temperature + self.bounds = BOUNDS(min_pressure, max_pressure) + self._steps = _ISOTHERM_STEPS + super(Isotherm, self).__init__(axes) + + def _generate_points(self): + pressure = np.linspace( + self.bounds.lower, self.bounds.upper, self._steps + ) + temperature = np.asarray([self.data] * self._steps) + _, theta = transforms.convert_pT2Tt(pressure, temperature) + return POINTS(temperature, theta, pressure) - def plot(self, **kwargs): + +class Profile(Isopleth): + def __init__(self, axes, data): """ - Plot the environmental lapse rate profile. + Create a profile from the sequence of pressure and temperature points. - Kwargs: + Args: - See :func:`matplotlib.pyplot.plot`. + * axes: + The tephigram axes on which to plot the profile. - Returns: - The profile :class:`matplotlib.lines.Line2D` + * data: + Sequence of pressure and temperature points defining + the profile. """ - if self.line is not None and self.line in self.axes.lines: - self.axes.lines.remove(self.line) - - # zorder of 4 is higher than all MPL defaults, excluding legend. - if "zorder" not in kwargs: - kwargs["zorder"] = 4 + self.data = np.asarray(list(data)) + super(Profile, self).__init__(axes) + self._barbs = None + self._highlight = None + + def has_highlight(self): + return self._highlight is not None + + def highlight(self, state=None): + if state is None: + state = not self.has_highlight() + if state: + if self._highlight is None: + linewidth = self.line.get_linewidth() * 7 + zorder = default.get("isopleth_zorder", 10) - 1 + kwargs = dict( + linewidth=linewidth, + color="grey", + alpha=0.3, + transform=self._transform, + zorder=zorder, + ) + (self._highlight,) = Subplot.plot( + self.axes, + self.points.temperature, + self.points.theta, + **kwargs, + ) + else: + if self._highlight is not None: + self.axes.lines.remove(self._highlight) + self._highlight = None - (self.line,) = self.axes.plot( - self.temperature, self.theta, transform=self._transform, **kwargs - ) - return self.line + def _generate_points(self): + if self.data.ndim != 2 or self.data.shape[-1] != 2: + msg = ( + "The profile data requires to be a sequence " + "of pressure, temperature value pairs." + ) + raise ValueError(msg) - def refresh(self): - """Refresh the plot with the profile and any associated barbs.""" - self._barbs.refresh() + pressure = self.data[:, 0] + temperature = self.data[:, 1] + _, theta = transforms.convert_pT2Tt(pressure, temperature) + return POINTS(temperature, theta, pressure) def barbs(self, barbs, **kwargs): """ @@ -473,10 +451,85 @@ def barbs(self, barbs, **kwargs): Kwargs: + * kwargs: See :func:`matplotlib.pyplot.barbs` """ colors = ["color", "barbcolor", "edgecolor", "facecolor"] if not set(colors).intersection(kwargs): kwargs["color"] = self.line.get_color() - self._barbs.plot(barbs, **kwargs) + self._barbs = BarbArtist(barbs, **kwargs) + self.axes.add_artist(self._barbs) + + def get_barbs(self): + return self._barbs.barbs + + +class WetAdiabat(Isopleth): + def __init__(self, axes, theta_e, min_temperature, max_pressure): + self.data = theta_e + self.bounds = BOUNDS(min_temperature, max_pressure) + self._delta_pressure = _SATURATION_ADIABAT_PRESSURE_DELTA + super(WetAdiabat, self).__init__(axes) + + def _gradient(self, pressure, temperature, dp): + stop = False + + kelvin = temperature + constants.KELVIN + lsbc = (constants.L / constants.Rv) * ( + (1.0 / constants.KELVIN) - (1.0 / kelvin) + ) + rw = 6.11 * np.exp(lsbc) * (constants.E / pressure) + lrwbt = (constants.L * rw) / (constants.Rd * kelvin) + numerator = ((constants.Rd * kelvin) / (constants.Cp * pressure)) * ( + 1.0 + lrwbt + ) + denominator = 1.0 + ( + lrwbt * ((constants.E * constants.L) / (constants.Cp * kelvin)) + ) + grad = numerator / denominator + dt = dp * grad + + if (temperature + dt) < self.bounds.lower: + dt = self.bounds.lower - temperature + dp = dt / grad + stop = True + + return dp, dt, stop + + def _generate_points(self): + temperature = [self.data] + pressure = [self.bounds.upper] + stop = False + dp = self._delta_pressure + + while not stop: + dp, dT, stop = self._gradient(pressure[-1], temperature[-1], dp) + pressure.append(pressure[-1] + dp) + temperature.append(temperature[-1] + dT) + + _, theta = transforms.convert_pT2Tt(pressure, temperature) + return POINTS(temperature, theta, pressure) + + +class ProfileList(list): + def __new__(cls, profiles=None): + profile_list = list.__new__(cls, profiles) + if not all(isinstance(profile, Profile) for profile in profile_list): + msg = "All items in the list must be a Profile instance." + raise TypeError(msg) + return profile_list + + def highlighted(self): + profiles = [profile for profile in self if profile.has_highlight()] + return profiles + + def picker(self, artist): + result = None + for profile in self: + if profile.line == artist: + result = profile + break + if result is None: + raise ValueError("Picker cannot find the profile.") + return result diff --git a/tephi/tests/results/imagerepo.json b/tephi/tests/results/imagerepo.json index 4ca6c2b..f42efa6 100644 --- a/tephi/tests/results/imagerepo.json +++ b/tephi/tests/results/imagerepo.json @@ -1,36 +1,82 @@ { + "test_tephigram.TestSubplots.test_subplot.0": [ + "https://scitools.github.io/test-tephi-imagehash/images/d9aaa4f6a2555b4a1cfee1a14b429a16f035254b875e5abc2de1cb43ea1ca5e1.png" + ], + "test_tephigram.TestTephigramAxes.test_add_humidity_mixing_ratios.0": [ + "https://scitools.github.io/test-tephi-imagehash/images/bfa8907fc0574f801faab87de1c00783941f787c7be08787c41e7878b8e12b87.png" + ], + "test_tephigram.TestTephigramAxes.test_add_isobars.0": [ + "https://scitools.github.io/test-tephi-imagehash/images/bea0907fc15f6f803eaa907de1c00783943f78787be08787c41e7c78b8e12b87.png" + ], + "test_tephigram.TestTephigramAxes.test_add_wet_adiabats.0": [ + "https://scitools.github.io/test-tephi-imagehash/images/bfa8907fc0574f801eaab87de1d00783941f78787be08787c41e7c78b8e12b87.png" + ], + "test_tephigram.TestTephigramAxes.test_plot_dews_locator_adiabat_numeric.0": [ + "https://scitools.github.io/test-tephi-imagehash/images/bc5ace0f91e46631cde5398c96589cc6cb3462619e9b3f139c6649896e733326.png" + ], + "test_tephigram.TestTephigramAxes.test_plot_dews_locator_adiabat_object.0": [ + "https://scitools.github.io/test-tephi-imagehash/images/ea5a9f0d91e32639c5a5198d96729cc6cbb462619f9e3f1398646d093c736326.png", + "https://scitools.github.io/test-tephi-imagehash/images/ea5a9f0d91e46639c5a5398996759cc6cbb462619f8b3f1298646bcc3c736216.png" + ], + "test_tephigram.TestTephigramAxes.test_plot_dews_locator_isotherm_numeric.0": [ + "https://scitools.github.io/test-tephi-imagehash/images/ea5a8f6d91e16630c5ed392c86599cc6cbb46a6196986a929d66ed893e23343e.png" + ], + "test_tephigram.TestTephigramAxes.test_plot_dews_locator_isotherm_object.0": [ + "https://scitools.github.io/test-tephi-imagehash/images/ea5acf6c91e16630c5ed392c96599cc6cb947e6796986a9298662d893473e1e4.png", + "https://scitools.github.io/test-tephi-imagehash/images/ea5a8f2d91e16630c5ed392c96599cc6cb946a6196986a929d66ed893e23363e.png" + ], + "test_tephigram.TestTephigramAxes.test_plot_dews_locator_numeric.0": [ + "https://scitools.github.io/test-tephi-imagehash/images/bc0ece6f91e46630cde5398cd6589cc6cbbc6261929837b39c6649893e13363e.png" + ], + "test_tephigram.TestTephigramAxes.test_plot_dews_locator_object.0": [ + "https://scitools.github.io/test-tephi-imagehash/images/ea5a8f2d91e36639c5e5198d86521cc7cb946e639adc273398646d09347961f6.png", + "https://scitools.github.io/test-tephi-imagehash/images/ea5a8f4d91e46639c5a5398996751cd6cbbc6a61929863b29c664bc93e137616.png" + ], + "test_tephigram.TestTephigramAxes.test_plot_xylim.0": [ + "https://scitools.github.io/test-tephi-imagehash/images/bfe8e15ee0451c03911a63e0063a6c6e79c09b9be335f073944f2ecc55f87ca3.png" + ], "test_tephigram.TestTephigramBarbs.test_barbs.0": [ - "https://scitools.github.io/test-tephi-imagehash/images/e9259e5b92db6d249e9a3386c65969c7c330964f0c9c69233c646e19e5b3c786.png" + "https://scitools.github.io/test-tephi-imagehash/images/e9259e5b92db6d249e9a3386c65969c7c330964f0c9c69233c646e19e5b3c786.png", + "https://scitools.github.io/test-tephi-imagehash/images/e92d96da92d36d25849a938ec64969c7cb30964f8c9c6d339c643e19e1a3e786.png" ], "test_tephigram.TestTephigramBarbs.test_barbs_from_file.0": [ - "https://scitools.github.io/test-tephi-imagehash/images/e96a9f3c92c36639c4a439ac96599cd6c3346261979d7a124966cd8d3c73b686.png" + "https://scitools.github.io/test-tephi-imagehash/images/e96a9f3c92c36639c4a439ac96599cd6c3346261979d7a124966cd8d3c73b686.png", + "https://scitools.github.io/test-tephi-imagehash/images/e95e8f2690d36639c4a4392c96599cc6cbb662619f9d5a12d9666d8d2c733686.png" ], "test_tephigram.TestTephigramBarbs.test_color.0": [ - "https://scitools.github.io/test-tephi-imagehash/images/e92596db92db6d249e9a3386c64969c7c331964f0c9c69233c646e19e5b3c786.png" + "https://scitools.github.io/test-tephi-imagehash/images/e92596db92db6d249e9a3386c64969c7c331964f0c9c69233c646e19e5b3c786.png", + "https://scitools.github.io/test-tephi-imagehash/images/e92d96da92d36d25949a938ec64969c7cb30964f8c9c6d339c643a19e1a3e786.png" ], "test_tephigram.TestTephigramBarbs.test_gutter.0": [ - "https://scitools.github.io/test-tephi-imagehash/images/e46499999b9b6666c999338ec65869c7c330d8cf34dc69233c246e19e5b3c786.png" + "https://scitools.github.io/test-tephi-imagehash/images/e46499999b9b6666c999338ec65869c7c330d8cf34dc69233c246e19e5b3c786.png", + "https://scitools.github.io/test-tephi-imagehash/images/e46c999991936666cd9b9b0ec65a69c7c33098cf96dc6d3398243e19e1a3a786.png" ], "test_tephigram.TestTephigramBarbs.test_length.0": [ - "https://scitools.github.io/test-tephi-imagehash/images/b1ccce31ce73318ece119363929c69c7c330ce6d1cde69983c646e19e5b39586.png" + "https://scitools.github.io/test-tephi-imagehash/images/b1ccce31ce73318ece119363929c69c7c330ce6d1cde69983c646e19e5b39586.png", + "https://scitools.github.io/test-tephi-imagehash/images/b98cce71c673318ccc919a63921c79c7c338ce659cde69989c643e19e1e3b386.png" ], "test_tephigram.TestTephigramBarbs.test_pivot.0": [ - "https://scitools.github.io/test-tephi-imagehash/images/edb09a6992cb65b69bcd334cc65a6dc6cf31924d3c9a49333c2c6319c5e72494.png" + "https://scitools.github.io/test-tephi-imagehash/images/edb09a6992cb65b69bcd334cc65a6dc6cf31924d3c9a49333c2c6319c5e72494.png", + "https://scitools.github.io/test-tephi-imagehash/images/edb0926992cb6db69bcd934cc65668c7cf30924d8c986d339c6c6199b1e72694.png" ], "test_tephigram.TestTephigramBarbs.test_rotate.0": [ - "https://scitools.github.io/test-tephi-imagehash/images/e9259e5992cf64b69e9b3324865a69c3c334964f1c9c69333c646c19e5b3c786.png" + "https://scitools.github.io/test-tephi-imagehash/images/e9259e5992cf64b69e9b3324865a69c3c334964f1c9c69333c646c19e5b3c786.png", + "https://scitools.github.io/test-tephi-imagehash/images/e9399e5992c76436cc9b9b2cc65a69c3cb34964f8c986d339c642819b1e3e786.png" ], "test_tephigram.TestTephigramPlot.test_plot_anchor.0": [ "https://scitools.github.io/test-tephi-imagehash/images/fba8c82d8a55b03da4dd2c2f899faf22827f03cad48a3ab0ba256f9c6a2970cb.png" ], "test_tephigram.TestTephigramPlot.test_plot_dews.0": [ - "https://scitools.github.io/test-tephi-imagehash/images/e85a9f2d90c56630cce539ac96599ce6c734626197997a924966cd8dbc733686.png" + "https://scitools.github.io/test-tephi-imagehash/images/e85a9f2d90c56630cce539ac96599ce6c734626197997a924966cd8dbc733686.png", + "https://scitools.github.io/test-tephi-imagehash/images/ea5a8f2d91e16631c5ad392c96799ce6c9b46261979b5a9298662d893c7327a6.png" ], "test_tephigram.TestTephigramPlot.test_plot_dews_custom.0": [ - "https://scitools.github.io/test-tephi-imagehash/images/e85a986d99e66631c66d698e971999a6c7966261979b7a9269668c893c7332a6.png" + "https://scitools.github.io/test-tephi-imagehash/images/e85a986d99e66631c66d698e971999a6c7966261979b7a9269668c893c7332a6.png", + "https://scitools.github.io/test-tephi-imagehash/images/e85b996d99a66631c66d398c921999e6cd967261979a5a9249666d896c736726.png" ], "test_tephigram.TestTephigramPlot.test_plot_dews_label.0": [ - "https://scitools.github.io/test-tephi-imagehash/images/e85a992d90c56630cced39afc6198cf6c734e26197997a9269669d8d387330e6.png" + "https://scitools.github.io/test-tephi-imagehash/images/e85a992d90c56630cced39afc6198cf6c734e26197997a9269669d8d387330e6.png", + "https://scitools.github.io/test-tephi-imagehash/images/e85acd2d91e16631c5ed992c965198e6c3b4f261979b5a9249666d896c7365a6.png" ], "test_tephigram.TestTephigramPlot.test_plot_dews_locator_adiabat_numeric.0": [ "https://scitools.github.io/test-tephi-imagehash/images/e85a9e0c91a3663ccda5398c965a9cf6cb3462619b9c3f134c66cd099e737346.png", @@ -57,19 +103,24 @@ "https://scitools.github.io/test-tephi-imagehash/images/e85a9f0991e76639c5a53989965d1cd6cf346a63939867b30c664bcc1e1b7216.png" ], "test_tephigram.TestTephigramPlot.test_plot_dews_temps.0": [ - "https://scitools.github.io/test-tephi-imagehash/images/e969cc3992c76726cd973326c65869c6c3319e4d9c9849333c2c6399e5a7a7a4.png" + "https://scitools.github.io/test-tephi-imagehash/images/e969cc3992c76726cd973326c65869c6c3319e4d9c9849333c2c6399e5a7a7a4.png", + "https://scitools.github.io/test-tephi-imagehash/images/e978cc3492c76637cd87932cc65a69c6c3313e4f9c9869331c6c6099e1e73794.png" ], "test_tephigram.TestTephigramPlot.test_plot_dews_temps_custom.0": [ - "https://scitools.github.io/test-tephi-imagehash/images/f1289c9996c76726cd9d333cc658cc66c731ce4c3cde499934247199b1e73326.png" + "https://scitools.github.io/test-tephi-imagehash/images/f1289c9996c76726cd9d333cc658cc66c731ce4c3cde499934247199b1e73326.png", + "https://scitools.github.io/test-tephi-imagehash/images/f338cc99c4e76627c98db32c925a98679331964c9c9e4d9924646199e5e76736.png" ], "test_tephigram.TestTephigramPlot.test_plot_temps.0": [ - "https://scitools.github.io/test-tephi-imagehash/images/e9698e1892c76636cd9a3386c65a69c3c330de4f9c9869333c646e19e5b3c786.png" + "https://scitools.github.io/test-tephi-imagehash/images/e9698e1892c76636cd9a3386c65a69c3c330de4f9c9869333c646e19e5b3c786.png", + "https://scitools.github.io/test-tephi-imagehash/images/e969cc3c92c76636cd96930ec65a69c3c330664f9c986d339c643e19e1a3e786.png" ], "test_tephigram.TestTephigramPlot.test_plot_temps_custom.0": [ - "https://scitools.github.io/test-tephi-imagehash/images/e161999996c76677c998330ec65a4c63c731ce4c8cde4d9b3c24399961b3b3a6.png" + "https://scitools.github.io/test-tephi-imagehash/images/e161999996c76677c998330ec65a4c63c731ce4c8cde4d9b3c24399961b3b3a6.png", + "https://scitools.github.io/test-tephi-imagehash/images/e969cc9884c76673c99c930e965a996393319a4c9cdc6d933c646c99e5a367a6.png" ], "test_tephigram.TestTephigramPlot.test_plot_temps_label.0": [ "https://scitools.github.io/test-tephi-imagehash/images/e9699f1992c76636cd9e3326c65a6cc3c730ce6f9c9869333824321961b3b786.png", - "https://scitools.github.io/test-tephi-imagehash/images/e9699e1992cf6636cd9a3326c65a6cc3c730ce6f9c98693338243e1961b3b586.png" + "https://scitools.github.io/test-tephi-imagehash/images/e9699e1992cf6636cd9a3326c65a6cc3c730ce6f9c98693338243e1961b3b586.png", + "https://scitools.github.io/test-tephi-imagehash/images/e969ccbc96c76637cd96932e965a19c39330924f9c986d331c646c9965a36786.png" ] } \ No newline at end of file diff --git a/tephi/tests/test_tephigram.py b/tephi/tests/test_tephigram.py index bd4ce2a..3d1dd7b 100644 --- a/tephi/tests/test_tephigram.py +++ b/tephi/tests/test_tephigram.py @@ -6,6 +6,8 @@ Tests the tephigram plotting capability provided by tephi. """ +import matplotlib + # Import tephi test package first so that some things can be initialised # before importing anything else. import tephi.tests as tests @@ -14,7 +16,7 @@ import pytest import tephi -from tephi import Tephigram +from tephi import TephiAxes def _load_result(filename): @@ -27,10 +29,12 @@ def _load_result(filename): _expected_temps = _load_result("temps.npz") _expected_barbs = _load_result("barbs.npz") +# make the default size for this session 8x8in +matplotlib.rcParams['figure.figsize'] = (8, 8) class TestTephigramLoadTxt(tests.TephiTest): @pytest.fixture(autouse=True) - def setup(self): + def _setup(self): self.filename_dews = tephi.tests.get_data_path("dews.txt") self.filename_temps = tephi.tests.get_data_path("temps.txt") self.filename_barbs = tephi.tests.get_data_path("barbs.txt") @@ -98,44 +102,39 @@ def test_dtype(self): assert dews.pressure[0].dtype == np.int32 assert dews.temperature[0].dtype == np.int32 - @pytest.mark.graphical @pytest.mark.usefixtures("close_plot", "nodeid") class TestTephigramPlot(tests.GraphicsTest): @pytest.fixture(autouse=True) - def setup(self): + def _setup(self): self.dews = _expected_dews.T self.temps = _expected_temps.T + self.tephigram = TephiAxes() + def test_plot_dews(self, nodeid): - tephigram = Tephigram() - tephigram.plot(self.dews) + self.tephigram.plot(self.dews) self.check_graphic(nodeid) def test_plot_temps(self, nodeid): - tephigram = Tephigram() - tephigram.plot(self.temps) + self.tephigram.plot(self.temps) self.check_graphic(nodeid) def test_plot_dews_temps(self, nodeid): - tephigram = Tephigram() - tephigram.plot(self.dews) - tephigram.plot(self.temps) + self.tephigram.plot(self.dews) + self.tephigram.plot(self.temps) self.check_graphic(nodeid) def test_plot_dews_label(self, nodeid): - tephigram = Tephigram() - tephigram.plot(self.dews, label="Dew-point temperature") + self.tephigram.plot(self.dews, label="Dew-point temperature") self.check_graphic(nodeid) def test_plot_temps_label(self, nodeid): - tephigram = Tephigram() - tephigram.plot(self.temps, label="Dry-bulb temperature") + self.tephigram.plot(self.temps, label="Dry-bulb temperature") self.check_graphic(nodeid) def test_plot_dews_custom(self, nodeid): - tephigram = Tephigram() - tephigram.plot( + self.tephigram.plot( self.dews, label="Dew-point temperature", linewidth=2, @@ -145,8 +144,7 @@ def test_plot_dews_custom(self, nodeid): self.check_graphic(nodeid) def test_plot_temps_custom(self, nodeid): - tephigram = Tephigram() - tephigram.plot( + self.tephigram.plot( self.temps, label="Dry-bulb temperature", linewidth=2, @@ -156,15 +154,14 @@ def test_plot_temps_custom(self, nodeid): self.check_graphic(nodeid) def test_plot_dews_temps_custom(self, nodeid): - tephigram = Tephigram() - tephigram.plot( + self.tephigram.plot( self.dews, label="Dew-point temperature", linewidth=2, color="blue", marker="s", ) - tephigram.plot( + self.tephigram.plot( self.temps, label="Dry-bulb temperature", linewidth=2, @@ -173,58 +170,86 @@ def test_plot_dews_temps_custom(self, nodeid): ) self.check_graphic(nodeid) +@pytest.mark.graphical +@pytest.mark.usefixtures("close_plot", "nodeid") +class TestTephigramAxes(tests.GraphicsTest): + @pytest.fixture(autouse=True) + def _setup(self): + self.dews = _expected_dews.T + self.temps = _expected_temps.T + def test_plot_dews_locator_isotherm_numeric(self, nodeid): - tephigram = Tephigram(isotherm_locator=10) + tephigram = TephiAxes(isotherm_locator=30) tephigram.plot(self.dews) self.check_graphic(nodeid) def test_plot_dews_locator_isotherm_object(self, nodeid): - tephigram = Tephigram(isotherm_locator=tephi.Locator(10)) + tephigram = TephiAxes(isotherm_locator=tephi.Locator(10)) tephigram.plot(self.dews) self.check_graphic(nodeid) def test_plot_dews_locator_adiabat_numeric(self, nodeid): - tephigram = Tephigram(dry_adiabat_locator=10) + tephigram = TephiAxes(dry_adiabat_locator=10) tephigram.plot(self.dews) self.check_graphic(nodeid) def test_plot_dews_locator_adiabat_object(self, nodeid): - tephigram = Tephigram(dry_adiabat_locator=tephi.Locator(10)) + tephigram = TephiAxes(dry_adiabat_locator=tephi.Locator(10)) tephigram.plot(self.dews) self.check_graphic(nodeid) def test_plot_dews_locator_numeric(self, nodeid): - tephigram = Tephigram(isotherm_locator=10, dry_adiabat_locator=10) + tephigram = TephiAxes(isotherm_locator=10, dry_adiabat_locator=10) tephigram.plot(self.dews) self.check_graphic(nodeid) def test_plot_dews_locator_object(self, nodeid): locator = tephi.Locator(10) - tephigram = Tephigram( + tephigram = TephiAxes( isotherm_locator=locator, dry_adiabat_locator=locator ) tephigram.plot(self.dews) self.check_graphic(nodeid) - def test_plot_anchor(self, nodeid): - tephigram = Tephigram(anchor=[(1000, 0), (300, 0)]) + def test_plot_xylim(self, nodeid): + tephigram = TephiAxes(xylim=[(0, 0), (40, 200)]) tephigram.plot(self.dews) self.check_graphic(nodeid) + def test_add_wet_adiabats(self, nodeid): + # the xylim is needed so that the isopleths actually appear + tephigram = TephiAxes(xylim=[(0, 0), (40, 70)]) + + tephigram.add_wet_adiabats() + self.check_graphic(nodeid) + + def test_add_humidity_mixing_ratios(self, nodeid): + # the xylim is needed so that the isopleths actually appear + tephigram = TephiAxes(xylim=[(0, 0), (40, 70)]) + + tephigram.add_mixing_ratios() + self.check_graphic(nodeid) + + def test_add_isobars(self, nodeid): + # the xylim is needed so that the isopleths actually appear + tephigram = TephiAxes(xylim=[(0, 0), (40, 70)]) + + tephigram.add_isobars() + self.check_graphic(nodeid) @pytest.mark.graphical @pytest.mark.usefixtures("close_plot", "nodeid") class TestTephigramBarbs(tests.GraphicsTest): @pytest.fixture(autouse=True) - def setup(self): + def _setup(self): self.dews = _expected_dews.T self.temps = _expected_temps.T magnitude = np.hstack(([0], np.arange(20) * 5 + 2, [102])) self.barbs = [(m, 45, 1000 - i * 35) for i, m in enumerate(magnitude)] + self.tephigram = TephiAxes() def test_rotate(self, nodeid): - tephigram = Tephigram() - profile = tephigram.plot(self.temps) + profile = self.tephigram.plot(self.temps) profile.barbs( [ (0, 0, 900), @@ -246,43 +271,53 @@ def test_rotate(self, nodeid): self.check_graphic(nodeid) def test_barbs(self, nodeid): - tephigram = Tephigram() - profile = tephigram.plot(self.temps) + profile = self.tephigram.plot(self.temps) profile.barbs(self.barbs, zorder=10) self.check_graphic(nodeid) def test_barbs_from_file(self, nodeid): - tephigram = Tephigram() dews = _expected_barbs.T[:, :2] barbs = np.column_stack( (_expected_barbs[2], _expected_barbs[3], _expected_barbs[0]) ) - profile = tephigram.plot(dews) - profile.barbs(barbs, zorder=10) + profile = self.tephigram.plot(dews) + profile.barbs(barbs, zorder=200) self.check_graphic(nodeid) def test_gutter(self, nodeid): - tephigram = Tephigram() - profile = tephigram.plot(self.temps) + profile = self.tephigram.plot(self.temps) profile.barbs(self.barbs, gutter=0.5, zorder=10) self.check_graphic(nodeid) def test_length(self, nodeid): - tephigram = Tephigram() - profile = tephigram.plot(self.temps) + profile = self.tephigram.plot(self.temps) profile.barbs(self.barbs, gutter=0.9, length=10, zorder=10) self.check_graphic(nodeid) def test_color(self, nodeid): - tephigram = Tephigram() - profile = tephigram.plot(self.temps) + profile = self.tephigram.plot(self.temps) profile.barbs(self.barbs, color="green", zorder=10) self.check_graphic(nodeid) def test_pivot(self, nodeid): - tephigram = Tephigram() - tprofile = tephigram.plot(self.temps) + tprofile = self.tephigram.plot(self.temps) tprofile.barbs(self.barbs, gutter=0.2, pivot="tip", length=8) - dprofile = tephigram.plot(self.dews) + dprofile = self.tephigram.plot(self.dews) dprofile.barbs(self.barbs, gutter=0.3, pivot="middle", length=8) self.check_graphic(nodeid) + +class TestSubplots(tests.GraphicsTest): + @pytest.fixture(autouse=True) + def _setup(self): + self.dews = _expected_dews.T + self.temps = _expected_temps.T + + def test_subplot(self, nodeid): + tephi_one = TephiAxes(133) + tephi_two = TephiAxes((1,3,1)) + + tephi_one.plot(self.temps) + tephi_one.plot(self.dews) + tephi_two.plot(self.dews) + + self.check_graphic(nodeid) diff --git a/tephi/transforms.py b/tephi/transforms.py index a904cf8..7183e66 100644 --- a/tephi/transforms.py +++ b/tephi/transforms.py @@ -6,15 +6,11 @@ Tephigram transform support. """ + from matplotlib.transforms import Transform import numpy as np -from ._constants import CONST_K, CONST_KELVIN, CONST_L, CONST_MA, CONST_RV - - -# -# Reference: http://www-nwp/~hadaa/tephigram/tephi_plot.html -# +import tephi.constants as constants def convert_Tt2pT(temperature, theta): @@ -37,12 +33,12 @@ def convert_Tt2pT(temperature, theta): temperature, theta = np.asarray(temperature), np.asarray(theta) # Convert temperature and theta from degC to kelvin. - kelvin = temperature + CONST_KELVIN - theta = theta + CONST_KELVIN + kelvin = temperature + constants.KELVIN + theta = theta + constants.KELVIN # Calculate the associated pressure given the temperature and # potential temperature. - pressure = 1000.0 * np.power(kelvin / theta, 1 / CONST_K) + pressure = constants.P_BASE * np.power(kelvin / theta, 1 / constants.K) return pressure, temperature @@ -67,13 +63,13 @@ def convert_pT2Tt(pressure, temperature): pressure, temperature = np.asarray(pressure), np.asarray(temperature) # Convert temperature from degC to kelvin. - kelvin = temperature + CONST_KELVIN + kelvin = temperature + constants.KELVIN # Calculate the potential temperature given the pressure and temperature. - theta = kelvin * ((1000.0 / pressure) ** CONST_K) + theta = kelvin * ((constants.P_BASE / pressure) ** constants.K) # Convert potential temperature from kelvin to degC. - return temperature, theta - CONST_KELVIN + return temperature, theta - constants.KELVIN def convert_pt2pT(pressure, theta): @@ -95,14 +91,14 @@ def convert_pt2pT(pressure, theta): pressure, theta = np.asarray(pressure), np.asarray(theta) # Convert potential temperature from degC to kelvin. - theta = theta + CONST_KELVIN + theta = theta + constants.KELVIN - # Calculate the temperature given the pressure and - # potential temperature. - kelvin = theta * (pressure**CONST_K) / (1000.0**CONST_K) + # Calculate the temperature given the pressure and potential temperature. + denom = constants.P_BASE**constants.K + kelvin = theta * (pressure**constants.K) / denom # Convert temperature from kelvin to degC. - return pressure, kelvin - CONST_KELVIN + return pressure, kelvin - constants.KELVIN def convert_Tt2xy(temperature, theta): @@ -125,13 +121,13 @@ def convert_Tt2xy(temperature, theta): temperature, theta = np.asarray(temperature), np.asarray(theta) # Convert potential temperature from degC to kelvin. - theta = theta + CONST_KELVIN + theta = theta + constants.KELVIN theta = np.clip(theta, 1, 1e10) phi = np.log(theta) - x_data = phi * CONST_MA + temperature - y_data = phi * CONST_MA - temperature + x_data = phi * constants.MA + temperature + y_data = phi * constants.MA - temperature return x_data, y_data @@ -155,10 +151,10 @@ def convert_xy2Tt(x_data, y_data): """ x_data, y_data = np.asarray(x_data), np.asarray(y_data) - phi = (x_data + y_data) / (2 * CONST_MA) + phi = (x_data + y_data) / (2 * constants.MA) temperature = (x_data - y_data) / 2.0 - theta = np.exp(phi) - CONST_KELVIN + theta = np.exp(phi) - constants.KELVIN return temperature, theta @@ -173,21 +169,22 @@ def convert_pw2T(pressure, mixing_ratio): Pressure in mb in hPa. * mixing_ratio: - Dimensionless mixing ratios. + Mixing ratio in g kg-1. Returns: Temperature in degC. """ - pressure = np.array(pressure) + pressure = np.asarray(pressure) # Calculate the dew-point. - vapp = pressure * (8.0 / 5.0) * (mixing_ratio / 1000.0) + vapp = pressure * (8.0 / 5.0) * (mixing_ratio / constants.P_BASE) temp = 1.0 / ( - (1.0 / CONST_KELVIN) - ((CONST_RV / CONST_L) * np.log(vapp / 6.11)) + (1.0 / constants.KELVIN) + - ((constants.Rv / constants.L) * np.log(vapp / 6.11)) ) - return temp - CONST_KELVIN + return temp - constants.KELVIN class TephiTransform(Transform): @@ -214,7 +211,7 @@ def transform_non_affine(self, values): """ return np.concatenate( - convert_Tt2xy(values[:, 0:1], values[:, 1:2]), axis=1 + convert_Tt2xy(values[:, 0:1], values[:, 1:2]), axis=-1 ) def inverted(self): @@ -247,7 +244,7 @@ def transform_non_affine(self, values): """ return np.concatenate( - convert_xy2Tt(values[:, 0:1], values[:, 1:2]), axis=1 + convert_xy2Tt(values[:, 0:1], values[:, 1:2]), axis=-1 ) def inverted(self):