Skip to content

Commit

Permalink
Refactored and improved unit handling to its own submodule. (#1130)
Browse files Browse the repository at this point in the history
Fix #1124
  • Loading branch information
joernu76 authored Aug 5, 2021
1 parent 7d4d924 commit 3b3608f
Show file tree
Hide file tree
Showing 18 changed files with 137 additions and 102 deletions.
2 changes: 1 addition & 1 deletion mslib/_tests/test_thermolib.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@

import numpy as np
import pytest
from metpy.units import units

from mslib.utils.units import units
import mslib.thermolib as tl


Expand Down
5 changes: 3 additions & 2 deletions mslib/msui/flighttrack.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,11 @@
import fs
import xml.dom.minidom
import xml.parsers.expat
from metpy.units import units

from mslib.msui.mss_qt import variant_to_string, variant_to_float
from PyQt5 import QtGui, QtCore, QtWidgets

from mslib.msui.mss_qt import variant_to_string, variant_to_float
from mslib.utils.units import units
from mslib import utils, __version__
from mslib import thermolib
from mslib.utils import config_loader, find_location, save_settings_qsettings, load_settings_qsettings
Expand Down
2 changes: 1 addition & 1 deletion mslib/msui/mpl_pathinteractor.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@
import datetime
import matplotlib.path as mpath
import matplotlib.patches as mpatches
from metpy.units import units
from PyQt5 import QtCore, QtWidgets
from mslib.utils.units import units
from mslib.utils import get_distance, find_location, path_points, latlon_points
from mslib.thermolib import pressure2flightlevel

Expand Down
2 changes: 1 addition & 1 deletion mslib/msui/mpl_qtwidget.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@
import logging
import numpy as np
import matplotlib
from metpy.units import units
from fs import open_fs
from fslib.fs_filepicker import getSaveFileNameAndFilter
from matplotlib import cbook, figure
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT, FigureCanvasQTAgg
import matplotlib.backend_bases
from mslib.utils.units import units
from mslib import thermolib
from mslib.utils import config_loader, FatalUserError
from mslib.msui import mpl_pathinteractor as mpl_pi
Expand Down
6 changes: 4 additions & 2 deletions mslib/msui/sideview.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@

import logging
import functools
from metpy.units import units
from mslib.utils import config_loader, save_settings_qsettings, load_settings_qsettings, convert_to

from PyQt5 import QtGui, QtWidgets

from mslib.utils.units import units, convert_to
from mslib.utils import config_loader, save_settings_qsettings, load_settings_qsettings
from mslib.msui.mss_qt import ui_sideview_window as ui
from mslib.msui.mss_qt import ui_sideview_options as ui_opt
from mslib.msui.viewwindows import MSSMplViewWindow
Expand Down
4 changes: 2 additions & 2 deletions mslib/mswms/_tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
from mslib.utils import UR
from mslib.utils.units import units
from mslib.mswms.utils import Targets


def test_targets():
for standard_name in Targets.get_targets():
unit = Targets.get_unit(standard_name)
UR(unit[0]) # ensure that the unit may be parsed
units(unit[0]) # ensure that the unit may be parsed
assert unit[1] == 1 # no conversion outside pint!
Targets.get_range(standard_name)
Targets.get_thresholds(standard_name)
Expand Down
4 changes: 2 additions & 2 deletions mslib/mswms/dataaccess.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
import pint

from mslib import netCDF4tools
from mslib.utils import UR
from mslib.utils.units import units


class NWPDataAccess(metaclass=ABCMeta):
Expand Down Expand Up @@ -289,7 +289,7 @@ def _parse_file(self, filename):
continue
if ncvar.standard_name != "time":
try:
UR(ncvar.units)
units(ncvar.units)
except (AttributeError, ValueError, pint.UndefinedUnitError, pint.DefinitionSyntaxError):
logging.error("Skipping variable '%s' in file '%s': unparseable units attribute '%s'",
ncvarname, filename, ncvar.units)
Expand Down
3 changes: 2 additions & 1 deletion mslib/mswms/mpl_hsec.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@
import PIL.Image

from mslib.mswms import mss_2D_sections
from mslib.utils import get_projection_params, convert_to
from mslib.utils import get_projection_params
from mslib.utils.units import convert_to
from mslib.mswms.utils import make_cbar_labels_readable


Expand Down
2 changes: 1 addition & 1 deletion mslib/mswms/mpl_hsec_styles.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
from mslib.mswms.mpl_hsec import MPLBasemapHorizontalSectionStyle
from mslib.mswms.utils import Targets, get_style_parameters, get_cbar_label_format, make_cbar_labels_readable
from mslib import thermolib
from mslib.utils import convert_to
from mslib.utils.units import convert_to


class HS_CloudsStyle_01(MPLBasemapHorizontalSectionStyle):
Expand Down
2 changes: 1 addition & 1 deletion mslib/mswms/mpl_lsec.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
from pint import Quantity

from mslib.mswms import mss_2D_sections
from mslib.utils import convert_to
from mslib.utils.units import convert_to

mpl.rcParams['xtick.direction'] = 'out'
mpl.rcParams['ytick.direction'] = 'out'
Expand Down
3 changes: 2 additions & 1 deletion mslib/mswms/mpl_lsec_styles.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,14 @@

from mslib.mswms.mpl_lsec import AbstractLinearSectionStyle
import mslib.thermolib as thermolib
from mslib.utils import convert_to
from mslib.utils.units import convert_to


class LS_DefaultStyle(AbstractLinearSectionStyle):
"""
Style for single variables that require no further calculation
"""

def __init__(self, driver, variable="air_temperature", filetype="ml"):
super(AbstractLinearSectionStyle, self).__init__(driver=driver)
self.variable = variable
Expand Down
6 changes: 3 additions & 3 deletions mslib/mswms/mpl_vsec.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
import mpl_toolkits.axes_grid1

from mslib.mswms import mss_2D_sections
from mslib.utils import convert_to, UR
from mslib.utils.units import convert_to, units
from mslib.mswms.utils import make_cbar_labels_readable

mpl.rcParams['xtick.direction'] = 'out'
Expand Down Expand Up @@ -179,14 +179,14 @@ def plot_vsection(self, data, lats, lons, valid_time, init_time,

# Provide an air_pressured 2-D field in 'Pa' from vertical axis
if (("air_pressure" not in self.data) and
UR(self.driver.vert_units).check("[pressure]")):
units(self.driver.vert_units).check("[pressure]")):
self.data_units["air_pressure"] = "Pa"
self.data["air_pressure"] = convert_to(
self.driver.vert_data[::-self.driver.vert_order, np.newaxis],
self.driver.vert_units, self.data_units["air_pressure"]).repeat(
len(self.lats), axis=1)
if (("air_potential_temperature" not in self.data) and
UR(self.driver.vert_units).check("[temperature]")):
units(self.driver.vert_units).check("[temperature]")):
self.data_units["air_potential_temperature"] = "K"
self.data["air_potential_temperature"] = convert_to(
self.driver.vert_data[::-self.driver.vert_order, np.newaxis],
Expand Down
2 changes: 1 addition & 1 deletion mslib/mswms/mpl_vsec_styles.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@

from mslib.mswms.mpl_vsec import AbstractVerticalSectionStyle
from mslib.mswms.utils import Targets, get_style_parameters, get_cbar_label_format, make_cbar_labels_readable
from mslib.utils import convert_to
from mslib.utils.units import convert_to
from mslib import thermolib


Expand Down
4 changes: 3 additions & 1 deletion mslib/thermolib.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,13 @@
import numpy
import scipy.integrate
import logging

from mslib.utils.units import units, check_units

from metpy.package_tools import Exporter
from metpy.constants import Rd, g
from metpy.xarray import preprocess_and_wrap
import metpy.calc as mpcalc
from metpy.units import units, check_units

exporter = Exporter(globals())

Expand Down
87 changes: 10 additions & 77 deletions mslib/utils.py → mslib/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
"""
mslib.mss_util
mslib.utils
~~~~~~~~~~~~~~
Collection of utility routines for the Mission Support System.
Expand Down Expand Up @@ -33,9 +33,7 @@
import logging
import netCDF4 as nc
import numpy as np
from metpy.units import units
import os
import pint
from fs import open_fs, errors
from scipy.interpolate import interp1d
from scipy.ndimage import map_coordinates
Expand All @@ -52,54 +50,11 @@
import pyproj

from mslib.msui import constants, MissionSupportSystemDefaultConfig
from mslib.utils.units import units as _units
from mslib.thermolib import pressure2flightlevel
from PyQt5 import QtCore, QtWidgets, QtGui
from mslib.msui.constants import MSS_CONFIG_PATH

UR = units
UR.define("PVU = 10^-6 m^2 s^-1 K kg^-1")
UR.define("degrees_north = degrees")
UR.define("degrees_south = -degrees")
UR.define("degrees_east = degrees")
UR.define("degrees_west = -degrees")

UR.define("degrees_N = degrees")
UR.define("degrees_S = -degrees")
UR.define("degrees_E = degrees")
UR.define("degrees_W = -degrees")

UR.define("degreesN = degrees")
UR.define("degreesS = -degrees")
UR.define("degreesE = degrees")
UR.define("degreesW = -degrees")

UR.define("degree_north = degrees")
UR.define("degree_south = -degrees")
UR.define("degree_east = degrees")
UR.define("degree_west = -degrees")

UR.define("degree_N = degrees")
UR.define("degree_S = -degrees")
UR.define("degree_E = degrees")
UR.define("degree_W = -degrees")

UR.define("degreeN = degrees")
UR.define("degreeS = -degrees")
UR.define("degreeE = degrees")
UR.define("degreeW = -degrees")

UR.define("fraction = [] = frac")
UR.define("sigma = 1 fraction")
UR.define("level = sigma")
UR.define("percent = 1e-2 fraction")
UR.define("permille = 1e-3 fraction")
UR.define("ppm = 1e-6 fraction")
UR.define("ppmv = 1e-6 fraction")
UR.define("ppb = 1e-9 fraction")
UR.define("ppbv = 1e-9 fraction")
UR.define("ppt = 1e-12 fraction")
UR.define("pptv = 1e-12 fraction")


def parse_iso_datetime(string):
try:
Expand Down Expand Up @@ -311,10 +266,6 @@ def rotate_point(point, angle, origin=(0, 0)):
return temp_point


def convertHPAToKM(press):
return (288.15 / 0.0065) * (1. - (press / 1013.25) ** (1. / 5.255)) / 1000.


def get_projection_params(proj):
proj = proj.lower()
if proj.startswith("crs:"):
Expand Down Expand Up @@ -611,33 +562,13 @@ def convert_pressure_to_vertical_axis_measure(vertical_axis, pressure):
if vertical_axis == "pressure":
return float(pressure / 100)
elif vertical_axis == "flight level":
return pressure2flightlevel(pressure * units.Pa).magnitude
return pressure2flightlevel(pressure * _units.Pa).magnitude
elif vertical_axis == "pressure altitude":
return pressure2flightlevel(pressure * units.Pa).to(units.km).magnitude
return pressure2flightlevel(pressure * _units.Pa).to(_units.km).magnitude
else:
return pressure


def convert_to(value, from_unit, to_unit, default=1.):
try:
value_unit = UR.Quantity(value, from_unit)
result = value_unit.to(to_unit).magnitude
except pint.UndefinedUnitError:
logging.error("Error in unit conversion (undefined) '%s'/'%s'", from_unit, to_unit)
result = value * default
except pint.DimensionalityError:
if UR(to_unit).to_base_units().units == UR.m:
try:
result = (value_unit / UR.Quantity(9.81, "m s^-2")).to(to_unit).magnitude
except pint.DimensionalityError:
logging.error("Error in unit conversion (dimensionality) %s/%s", from_unit, to_unit)
result = value * default
else:
logging.error("Error in unit conversion (dimensionality) %s/%s", from_unit, to_unit)
result = value * default
return result


def setup_logging(args):
logger = logging.getLogger()
# this is necessary as "someone" has already initialized logging, preventing basicConfig from doing stuff
Expand Down Expand Up @@ -957,6 +888,7 @@ class NonQtCallback:
Callbacks are run on the same thread as the caller of emit, as opposed to the caller of connect.
Keep in mind if this causes issues.
"""

def __init__(self):
self.callbacks = []

Expand Down Expand Up @@ -1192,10 +1124,11 @@ def update_airspace(force_download=False, countries=["de"]):
is_outdated = file_exists and (time.time() - os.path.getmtime(location)) > 60 * 60 * 24 * 30

if (force_download or is_outdated or not file_exists) \
and QtWidgets.QMessageBox.question(None, "Allow download",
f"The selected {country} airspace needs to be downloaded ({data[-1]})"
f"\nIs now a good time?",
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) \
and QtWidgets.QMessageBox.question(
None, "Allow download",
f"The selected {country} airspace needs to be downloaded ({data[-1]})"
f"\nIs now a good time?",
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) \
== QtWidgets.QMessageBox.Yes:
download_progress(location, url)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,36 @@
"""

import pytest
from mslib.utils import convert_to
from mslib.utils.units import convert_to, units


def test_units():
assert units("10 degree_north").to("degree").m == 10
assert units("10 degrees_north").to("degree").m == 10
assert units("10 degreeN").to("degree").m == 10
assert units("10 degree_N").to("degree").m == 10
assert units("-10 degree_south").to("degree").m == 10
assert units("-10 degrees_south").to("degree").m == 10
assert units("-10 degreeS").to("degree").m == 10
assert units("-10 degree_S").to("degree").m == 10
assert units("10 degree_east").to("degree").m == 10
assert units("10 degrees_east").to("degree").m == 10
assert units("10 degreeE").to("degree").m == 10
assert units("10 degree_E").to("degree").m == 10
assert units("-10 degree_west").to("degree").m == 10
assert units("-10 degrees_west").to("degree").m == 10
assert units("-10 degreeW").to("degree").m == 10
assert units("-10 degree_W").to("degree").m == 10
assert units("1 percent"). to("dimensionless").m == 0.01
assert units("1 permille"). to("dimensionless").m == 0.001
assert units("1 ppm"). to("dimensionless").m == pytest.approx(1e-6)
assert units("1 ppb"). to("dimensionless").m == pytest.approx(1e-9)
assert units("1 ppt"). to("dimensionless").m == pytest.approx(1e-12)
assert units("1 ppmv"). to("dimensionless").m == pytest.approx(1e-6)
assert units("1 ppbv"). to("dimensionless").m == pytest.approx(1e-9)
assert units("1 pptv"). to("dimensionless").m == pytest.approx(1e-12)

assert units("1 PVU").to_base_units().m == pytest.approx(units("1E-6 m^2 s^-1 K kg^-1").to_base_units().m)


def test_convert_to():
Expand Down
5 changes: 1 addition & 4 deletions mslib/_tests/test_utils.py → mslib/utils/_tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ class TestConfigLoader(object):
"""
tests config file for client
"""

def teardown(self):
if fs.open_fs(MSS_CONFIG_PATH).exists("mss_settings.json"):
fs.open_fs(MSS_CONFIG_PATH).remove("mss_settings.json")
Expand Down Expand Up @@ -252,10 +253,6 @@ def test_rotate_point(self):


class TestConverter(object):
def test_convert_pressure_to_altitude(self):
assert utils.convertHPAToKM(1013.25) == 0
assert int(utils.convertHPAToKM(25) * 1000) == 22415

def test_convert_pressure_to_vertical_axis_measure(self):
assert utils.convert_pressure_to_vertical_axis_measure('pressure', 10000) == 100
assert utils.convert_pressure_to_vertical_axis_measure('flightlevel', 400) == 400
Expand Down
Loading

0 comments on commit 3b3608f

Please sign in to comment.