diff --git a/mslib/_tests/test_thermolib.py b/mslib/_tests/test_thermolib.py index 8e5f05844..a33c9360f 100644 --- a/mslib/_tests/test_thermolib.py +++ b/mslib/_tests/test_thermolib.py @@ -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 diff --git a/mslib/msui/flighttrack.py b/mslib/msui/flighttrack.py index c6b6bdc78..6f20f36bd 100755 --- a/mslib/msui/flighttrack.py +++ b/mslib/msui/flighttrack.py @@ -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 diff --git a/mslib/msui/mpl_pathinteractor.py b/mslib/msui/mpl_pathinteractor.py index 6f18661e9..19d606333 100644 --- a/mslib/msui/mpl_pathinteractor.py +++ b/mslib/msui/mpl_pathinteractor.py @@ -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 diff --git a/mslib/msui/mpl_qtwidget.py b/mslib/msui/mpl_qtwidget.py index 32fc3e78f..330de2d73 100644 --- a/mslib/msui/mpl_qtwidget.py +++ b/mslib/msui/mpl_qtwidget.py @@ -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 diff --git a/mslib/msui/sideview.py b/mslib/msui/sideview.py index 02b29c9d9..8d1240b24 100644 --- a/mslib/msui/sideview.py +++ b/mslib/msui/sideview.py @@ -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 diff --git a/mslib/mswms/_tests/test_utils.py b/mslib/mswms/_tests/test_utils.py index 65ac01339..95be67e00 100644 --- a/mslib/mswms/_tests/test_utils.py +++ b/mslib/mswms/_tests/test_utils.py @@ -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) diff --git a/mslib/mswms/dataaccess.py b/mslib/mswms/dataaccess.py index c4810259e..6c79a29ad 100644 --- a/mslib/mswms/dataaccess.py +++ b/mslib/mswms/dataaccess.py @@ -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): @@ -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) diff --git a/mslib/mswms/mpl_hsec.py b/mslib/mswms/mpl_hsec.py index 8f08f9a36..290aaf9e6 100644 --- a/mslib/mswms/mpl_hsec.py +++ b/mslib/mswms/mpl_hsec.py @@ -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 diff --git a/mslib/mswms/mpl_hsec_styles.py b/mslib/mswms/mpl_hsec_styles.py index 1f2f038b6..8f434a043 100644 --- a/mslib/mswms/mpl_hsec_styles.py +++ b/mslib/mswms/mpl_hsec_styles.py @@ -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): diff --git a/mslib/mswms/mpl_lsec.py b/mslib/mswms/mpl_lsec.py index 210948610..70f73b3e8 100644 --- a/mslib/mswms/mpl_lsec.py +++ b/mslib/mswms/mpl_lsec.py @@ -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' diff --git a/mslib/mswms/mpl_lsec_styles.py b/mslib/mswms/mpl_lsec_styles.py index 0dc4413c5..84318f5ec 100644 --- a/mslib/mswms/mpl_lsec_styles.py +++ b/mslib/mswms/mpl_lsec_styles.py @@ -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 diff --git a/mslib/mswms/mpl_vsec.py b/mslib/mswms/mpl_vsec.py index d9ca2c17c..02190ff87 100644 --- a/mslib/mswms/mpl_vsec.py +++ b/mslib/mswms/mpl_vsec.py @@ -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' @@ -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], diff --git a/mslib/mswms/mpl_vsec_styles.py b/mslib/mswms/mpl_vsec_styles.py index 99233ed93..2784c1552 100644 --- a/mslib/mswms/mpl_vsec_styles.py +++ b/mslib/mswms/mpl_vsec_styles.py @@ -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 diff --git a/mslib/thermolib.py b/mslib/thermolib.py index 2f6823566..172168f1d 100644 --- a/mslib/thermolib.py +++ b/mslib/thermolib.py @@ -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()) diff --git a/mslib/utils.py b/mslib/utils/__init__.py similarity index 94% rename from mslib/utils.py rename to mslib/utils/__init__.py index b54d17e19..e7ee8bdf2 100644 --- a/mslib/utils.py +++ b/mslib/utils/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ - mslib.mss_util + mslib.utils ~~~~~~~~~~~~~~ Collection of utility routines for the Mission Support System. @@ -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 @@ -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: @@ -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:"): @@ -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 @@ -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 = [] @@ -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) diff --git a/mslib/_tests/test_convert.py b/mslib/utils/_tests/test_units.py similarity index 61% rename from mslib/_tests/test_convert.py rename to mslib/utils/_tests/test_units.py index 55da9cb72..372bb7eea 100644 --- a/mslib/_tests/test_convert.py +++ b/mslib/utils/_tests/test_units.py @@ -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(): diff --git a/mslib/_tests/test_utils.py b/mslib/utils/_tests/tests.py similarity index 99% rename from mslib/_tests/test_utils.py rename to mslib/utils/_tests/tests.py index 8b9648c23..e1de87d79 100644 --- a/mslib/_tests/test_utils.py +++ b/mslib/utils/_tests/tests.py @@ -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") @@ -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 diff --git a/mslib/utils/units.py b/mslib/utils/units.py new file mode 100644 index 000000000..af5e4e0b1 --- /dev/null +++ b/mslib/utils/units.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +""" + + mslib.units + ~~~~~~~~~~~~~~ + + Collection of unit conversion related routines for the Mission Support System. + + This file is part of mss. + + :copyright: Copyright 2008-2014 Deutsches Zentrum fuer Luft- und Raumfahrt e.V. + :copyright: Copyright 2011-2014 Marc Rautenhaus (mr) + :copyright: Copyright 2016-2021 by the mss team, see AUTHORS. + :license: APACHE-2.0, see LICENSE for details. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" + +import logging + +from metpy.units import units, check_units # noqa +import pint + +units.define("PVU = m^2 s^-1 uK kg^-1") +units.define("degrees_south = -degrees = degrees_S = degreesS = degree_south = degree_S = degreeS") +units.define("degrees_west = -degrees = degrees_W = degreesW = degree_west = degree_W = degreeW") + +# Follow metpy 1.0.1 style of defining dimensionless units +for _name, _factor in (("level", 1), + ("sigma", 1), + ("permille", 1e-3), + ("ppm", 1e-6), + ("ppmv", 1e-6), + ("ppb", 1e-9), + ("ppbv", 1e-9), + ("ppt", 1e-12), + ("pptv", 1e-12)): + units.define(pint.unit.UnitDefinition(_name, '', (), + pint.converters.ScaleConverter(_factor))) + + +def convert_to(value, from_unit, to_unit, default=1.): + try: + value_unit = units.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 units(to_unit).to_base_units().units == units.m: + try: + result = (value_unit / units.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