Source code for metobs_toolkit.obstypes

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Class defenition for regular observation types. The default observationtypes
are define here aswell.
"""

import sys
import logging
from collections.abc import Iterable

import numpy as np

logger = logging.getLogger(__name__)


# =============================================================================
# Standard toolkit units for each observation type
# =============================================================================

tlk_std_units = {
    "temp": "Celsius",
    "radiation_temp": "Celsius",
    "humidity": "%",
    "precip": "mm/m²",
    "precip_sum": "mm/m² from midnight",
    "wind_speed": "m/s",
    "wind_gust": "m/s",
    "wind_direction": "° from north (CW)",
    "pressure": "pa",
    "pressure_at_sea_level": "pa",
}


# =============================================================================
# Aliases for units
# =============================================================================

temp_aliases = {
    "Celsius": [
        "celsius",
        "°C",
        "°c",
        "celcius",
        "Celcius",
    ],  # for the dyselectic developper..
    "Kelvin": ["K", "kelvin"],
    "Farenheit": ["farenheit"],
}
pressure_aliases = {
    "pa": ["Pascal", "pascal", "Pa"],
    "hpa": ["hecto pascal", "hPa"],
    "psi": ["Psi"],
    "bar": ["Bar"],
}

precip_aliases = {"mm/m²": ["mm", "liter", "liters", "l/m²", "milimeter"]}

wind_aliases = {
    "m/s": ["meters/second", "m/sec"],
    "km/h": ["kilometers/hour", "kph"],
    "mph": ["miles/hour"],
}
direction_aliases = {"° from north (CW)": ["°", "degrees"]}


# conversion between standard-NAMES and aliases
all_units_aliases = {
    "temp": temp_aliases,
    "radiation_temp": temp_aliases,
    "humidity": {"%": ["percent", "percentage"]},
    "pressure": pressure_aliases,
    "pressure_at_sea_level": pressure_aliases,
    "precip": precip_aliases,
    "precip_sum": precip_aliases,
    "wind_speed": wind_aliases,
    "wind_gust": wind_aliases,
    "wind_direction": direction_aliases,
}

# =============================================================================
# Unit conversion expressions
# =============================================================================

all_conversion_table = {
    "temp": {
        "Kelvin": ["x - 273.15"],  # result is in tlk_std_units
        "Farenheit": ["x-32.0", "x/1.8"],
    },  # -->execute from left to write  = (x-32)/1.8
    "radiation_temp": {
        "Kelvin": ["x - 273.15"],  # result is in tlk_std_units
        "Farenheit": ["x-32.0", "x/1.8"],
    },
    "humidity": {},
    "pressure": {"hpa": ["x * 100"], "psi": ["x * 6894.7573"], "bar": ["x * 100000."]},
    "pressure_at_sea_level": {
        "hpa": ["x * 100"],
        "psi": ["x * 6894.7573"],
        "bar": ["x * 100000."],
    },
    "precip": {},
    "precip_sum": {},
    "wind_speed": {"km/h": ["x / 3.6"], "mph": ["x * 0.44704"]},
    "wind_gust": {"km/h": ["x / 3.6"], "mph": ["x * 0.44704"]},
    "wind_direction": {},
}

# =============================================================================
# Observation type class
# =============================================================================


[docs] class Obstype: """Object with all info and methods for a specific observation type."""
[docs] def __init__( self, obsname, std_unit, description=None, unit_aliases={}, unit_conversions={} ): """Initiate an observation type. Parameters ---------- obsname : str The name of the new observation type (i.g. 'sensible_heat_flux'). std_unit : str The standard unit for the observation type (i.g. 'J/m²') obstype_description : str, ptional A more detailed description of the obstype (i.g. '2m SE inside canopy'). The default is None. unit_aliases : dict, optional A dictionary containing unit alias names. Keys represent a unit and values are lists with aliases for the units at the keys. The default is {}. unit_conversions : dict, optional A dictionary containing the conversion information to map to the standard units. Here an example of for temperatures (with Celcius as standard unit): {'Kelvin': ["x - 273.15"], #result is in tlk_std_units 'Farenheit' : ["x-32.0", "x/1.8"]}, # -->execute from left to write = (x-32)/1.8 The default is {}. Returns ------- None. """ self.name = str(obsname) # Standard name for the observation type self.std_unit = str(std_unit) # standard unit fot the observation type self.description = str(description) # Conversion info and mappers self.units_aliases = unit_aliases self.conv_table = unit_conversions # Original column name and units in the data self.original_name = None # Updated on IO self.original_unit = None # updated on IO self._check_attributes()
def __repr__(self): """Instance representation.""" return f"Obstype instance of {self.name}" def __str__(self): """Text representation.""" return f"Obstype instance of {self.name}" # ----- Setters -------
[docs] def set_description(self, desc): """Set the description of the observation type.""" self.description = str(desc)
[docs] def set_original_name(self, columnname): """Set the original name of the observation type.""" self.original_name = str(columnname)
[docs] def set_original_unit(self, original_unit): """Set the original unit of the observation type.""" self.original_unit = str(original_unit)
# ------ Getters --------
[docs] def get_info(self): """Print out detailed information of the observation type. Returns ------- None. """ info_str = f"{self.name} observation with: \n \ * standard unit: {self.std_unit} \n \ * data column as {self.original_name} in {self.original_unit} \n \ * known units and aliases: {self.units_aliases} \n \ * description: {self.description} \n \ * conversions to known units: {self.conv_table} \n \ * originates from data column: {self.original_name} with {self.original_unit} as native unit." print(info_str)
[docs] def get_orig_name(self): """Return the original name of the observation type.""" return self.original_name
[docs] def get_description(self): """Return the descrition of the observation type.""" if self.description == str(None): return "No description available" else: return str(self.description)
[docs] def get_all_units(self): """Return a list with all the known unit (in standard naming).""" units = list(self.units_aliases.keys()) units.append(self.get_standard_unit()) return list(set(units))
[docs] def get_standard_unit(self): """Return the standard unit of the observation type.""" return self.std_unit
[docs] def get_plot_y_label(self, mapname=None): """Return a string to represent the vertical axes of a plot.""" return f"{self.name} ({self.std_unit})"
[docs] def add_unit(self, unit_name, conversion=["x"]): """Add a new unit to an observation type. Parameters ---------- unit_name : str The name of the new unit. conversion : list, optional The conversion description to the standard unit. The default is ["x"]. Returns ------- None. """ # check if unit name is already known known = self.test_if_unit_is_known(unit_name) if known: return # convert expression to list if it is a string if isinstance(conversion, str): conversion = [conversion] # add converstion to the table self.conv_table[str(unit_name)] = conversion # add to alias table (without aliasses) self.units_aliases[unit_name] = [] logger.info( f"{unit_name} is added as a {self.name} unit with coversion: {conversion} to {self.std_unit}" )
[docs] def convert_to_standard_units(self, input_data, input_unit): """Convert data from a knonw unit to the standard unit. The data can be a collection of numeric values or a single numeric value. Parameters ---------- input_data : (collection of) numeric The data to convert to the standard unit. input_unit : str The known unit the inputdata is in. Returns ------- data numeric/numpy.array The data in standard units. """ # check if input unit is known known = self.test_if_unit_is_known(input_unit) # error when unit is not know if not known: sys.exit( f"{input_unit} is an unknown unit for {self.name}. No coversion possible!" ) # Get conversion std_unit_name = self._get_std_unit_name(input_unit) if std_unit_name == self.std_unit: # No conversion needed because already the standard unit return input_data conv_expr_list = self.conv_table[std_unit_name] # covert data data = input_data for conv in conv_expr_list: data = expression_calculator(conv, data) return data
# ------------- Helpers ---------------------------------- def _check_attributes(self): """Add units from the conv_table to the aliases if needed.""" add_to_aliases = {} all_std_unit_names = [] all_aliases = [] for std_unit, alias_units in self.units_aliases.items(): all_std_unit_names.append(std_unit) all_aliases.extend(alias_units) # add empty alias for all obstype present in conv table if no aliases are given for unit in self.conv_table.keys(): if unit not in all_std_unit_names: if unit not in all_aliases: add_to_aliases[unit] = [] # add std unit to aliases if it is not already present if self.get_standard_unit() not in all_std_unit_names: add_to_aliases[self.get_standard_unit()] = [] self.units_aliases.update(add_to_aliases) def _get_std_unit_name(self, unit_name): """Get standard name for a unit name by scanning trough the aliases.""" for std_unit_name, aliases in self.units_aliases.items(): if unit_name == std_unit_name: return unit_name if unit_name in aliases: return std_unit_name sys.exit(f"No standard unit name is found for {unit_name} for {self.name}")
[docs] def test_if_unit_is_known(self, unit_name): """Test is the unit is known. Parameters ---------- unit_name : str The unit name to test. Returns ------- bool True if knonw, False else. """ if unit_name == self.std_unit: return True for std_unit_name, aliases in self.units_aliases.items(): if unit_name == std_unit_name: return True if unit_name in aliases: return True return False
def expression_calculator(equation, x): """Convert array by equation.""" if isinstance(x, Iterable): x = np.array(x) if "+" in equation: y = equation.split("+") return x + float(y[1]) elif "-" in equation: y = equation.split("-") return x - float(y[1]) elif "/" in equation: y = equation.split("/") return x / float(y[1]) elif "*" in equation: y = equation.split("*") return x * float(y[1]) else: sys.exit(f"expression {equation}, can not be converted to mathematical.") # ============================================================================= # Create observation types # ============================================================================= temperature = Obstype( obsname="temp", std_unit=tlk_std_units["temp"], description="2m - temperature", unit_aliases=all_units_aliases["temp"], unit_conversions=all_conversion_table["temp"], ) humidity = Obstype( obsname="humidity", std_unit=tlk_std_units["humidity"], description="2m - relative humidity", unit_aliases=all_units_aliases["humidity"], unit_conversions=all_conversion_table["humidity"], ) radiation_temp = Obstype( obsname="radiation_temp", std_unit=tlk_std_units["radiation_temp"], description="2m - Black globe", unit_aliases=all_units_aliases["radiation_temp"], unit_conversions=all_conversion_table["radiation_temp"], ) pressure = Obstype( obsname="pressure", std_unit=tlk_std_units["pressure"], description="atmospheric pressure (at station)", unit_aliases=all_units_aliases["pressure"], unit_conversions=all_conversion_table["pressure"], ) pressure_at_sea_level = Obstype( obsname="pressure_at_sea_level", std_unit=tlk_std_units["pressure_at_sea_level"], description="atmospheric pressure (at sea level)", unit_aliases=all_units_aliases["pressure_at_sea_level"], unit_conversions=all_conversion_table["pressure_at_sea_level"], ) precip = Obstype( obsname="precip", std_unit=tlk_std_units["precip"], description="precipitation intensity", unit_aliases=all_units_aliases["precip"], unit_conversions=all_conversion_table["precip"], ) precip_sum = Obstype( obsname="precip_sum", std_unit=tlk_std_units["precip"], description="Cummulated precipitation", unit_aliases=all_units_aliases["precip_sum"], unit_conversions=all_conversion_table["precip_sum"], ) wind_speed = Obstype( obsname="wind_speed", std_unit=tlk_std_units["wind_speed"], description="wind speed", unit_aliases=all_units_aliases["wind_speed"], unit_conversions=all_conversion_table["wind_speed"], ) windgust = Obstype( obsname="wind_gust", std_unit=tlk_std_units["wind_gust"], description="wind gust", unit_aliases=all_units_aliases["wind_gust"], unit_conversions=all_conversion_table["wind_gust"], ) wind_direction = Obstype( obsname="wind_direction", std_unit=tlk_std_units["wind_direction"], description="wind direction", unit_aliases=all_units_aliases["wind_direction"], unit_conversions=all_conversion_table["wind_direction"], ) # The order of the dictionary is also the order on how columns in dataset are presetnted tlk_obstypes = { "temp": temperature, "humidity": humidity, "radiation_temp": radiation_temp, "pressure": pressure, "pressure_at_sea_level": pressure_at_sea_level, "precip": precip, "precip_sum": precip_sum, "wind_speed": wind_speed, "wind_gust": windgust, "wind_direction": wind_direction, }