Source code for snowprofile.classes

# -*- coding: utf-8 -*-

import typing
import datetime

import pydantic

from snowprofile._constants import cloudiness_attribution, QUALITY_FLAGS
from snowprofile._base_classes import AdditionalData, BaseData, BaseMergeable, \
    datetime_with_tz, datetime_tuple_with_tz, get_dataframe_checker
from snowprofile._utils import get_config

__all__ = ['Person', 'Time', 'Observer', 'Location', 'Weather', 'SurfaceConditions',
           'Environment', 'SolarMask', 'SpectralAlbedo']

conf = get_config()


[docs] class Person(pydantic.BaseModel): """ Class to describe a contact person """ model_config = pydantic.ConfigDict( validate_assignment=True, extra='forbid') id: typing.Optional[str] = None name: typing.Optional[str] = None website: typing.Optional[str] = None comment: typing.Optional[str] = None additional_data: typing.Optional[AdditionalData] = None
[docs] class Time(pydantic.BaseModel, BaseMergeable): """ Class to store the date and time of observation (and additional date/time considerations) If left empty, the time zone will be automatically filled and the time zone is assumed to be UTC. """ model_config = pydantic.ConfigDict( validate_assignment=True, extra='forbid') record_time: datetime_with_tz = pydantic.Field( datetime.datetime.now(), description="Time of the observation or measurement (python datetime object).") record_period: datetime_tuple_with_tz = pydantic.Field( (None, None), description="Time period of the observation " "(tuple of two python datetime objects giving the start time and the end time).") report_time: typing.Optional[datetime_with_tz] = pydantic.Field( None, description="Reporting time of the observation (python datetime object).") last_edition_time: typing.Optional[datetime_with_tz] = pydantic.Field( None, description="Last edition time of the observation (python datetime object).") comment: typing.Optional[str] = pydantic.Field( None, description="Comment on the date and time of observation (str)") additional_data: typing.Optional[AdditionalData] = pydantic.Field( None, description="Field to store additional data for CAAML compatibility (customData), do not use.")
[docs] class Observer(pydantic.BaseModel, BaseMergeable): """ Class to store information about the observer and about the institution / lab. ``source`` refers to the observation institution and ``person`` to the observer. ``source_id`` Unique identifier of the observation institution ``source_name`` Name of the observation institution ``source_comment`` Comment on the observation institution ``contact_persons`` The list of contact persons (or observers) ``additional_data`` Field to store additional data for CAAML compatibility (customData), do not use. """ model_config = pydantic.ConfigDict( validate_assignment=True, extra='forbid') source_id: typing.Optional[str] = conf.get('DEFAULT', 'observer_id', fallback=None) source_name: typing.Optional[str] = conf.get('DEFAULT', 'observer_name', fallback=None) source_website: typing.Optional[str] = None source_comment: typing.Optional[str] = conf.get('DEFAULT', 'observer_comment', fallback=None) source_additional_data: typing.Optional[AdditionalData] = None contact_persons: typing.List[Person] = pydantic.Field([Person( name=conf.get('DEFAULT', 'contact_person_name', fallback=None), id=conf.get('DEFAULT', 'contact_person_id', fallback=None), comment=conf.get('DEFAULT', 'contact_person_comment', fallback=None)), ], min_length=1)
[docs] class Location(pydantic.BaseModel, BaseMergeable): """ Class to store information on the measurement location (geographical position and details of the observation site). The required field is ``name``. ``id`` Unique identifier of the geographical position ``name`` The name of the observation location (str) ``latitude`` Latitude (degrees north) ``longitude`` Longitude (degrees East) ``comment`` Free comment on the location (str) ``elevation`` Point elevation (meters above sea level) ``aspect`` Slope aspect (degrees, int. between 0 and 360) ``slope`` Slope inclination (degrees, int. between 0 and 90) ``point_type`` A point type description (str) ``country`` Country code according to ISO3166 ``region`` Region (detail in the country, optional, str) ``additonal_data`` Field to store additional data for CAAML compatibility (customData), do not use. """ model_config = pydantic.ConfigDict( validate_assignment=True, extra='forbid') id: typing.Optional[str] = None name: str point_type: typing.Optional[str] = None aspect: typing.Optional[int] = pydantic.Field(None, ge=0, le=360) elevation: typing.Optional[int] = None slope: typing.Optional[int] = pydantic.Field(None, ge=0, lt=90) latitude: typing.Optional[float] = None longitude: typing.Optional[float] = None country: typing.Optional[typing.Literal[ "AD", "AE", "AF", "AG", "AL", "AM", "AO", "AR", "AT", "AU", "AZ", "BA", "BB", "BD", "BE", "BF", "BG", "BH", "BI", "BJ", "BN", "BO", "BQ", "BR", "BS", "BT", "BW", "BY", "BZ", "CA", "CD", "CF", "CG", "CH", "CI", "CL", "CM", "CN", "CO", "CR", "CU", "CV", "CY", "CZ", "DE", "DJ", "DK", "DM", "DO", "DZ", "EC", "EE", "EG", "ER", "ES", "ET", "FI", "FJ", "FM", "FR", "GA", "GB", "GD", "GE", "GH", "GL", "GM", "GN", "GQ", "GR", "GT", "GW", "GY", "HN", "HR", "HT", "HU", "ID", "IE", "IL", "IN", "IQ", "IR", "IS", "IT", "JM", "JO", "JP", "KE", "KG", "KH", "KI", "KM", "KN", "KP", "KR", "KW", "KZ", "LA", "LB", "LC", "LI", "LK", "LR", "LS", "LT", "LU", "LV", "LY", "MA", "MC", "MD", "ME", "MG", "MH", "MK", "ML", "MM", "MN", "MR", "MT", "MU", "MV", "MW", "MX", "MY", "MZ", "NA", "NE", "NG", "NI", "NL", "NO", "NP", "NR", "NZ", "OM", "PA", "PE", "PG", "PH", "PK", "PL", "PS", "PT", "PW", "PY", "QA", "RO", "RS", "RU", "RW", "SA", "SB", "SC", "SD", "SE", "SG", "SH", "SI", "SK", "SL", "SM", "SN", "SO", "SR", "SS", "ST", "SV", "SY", "SZ", "TD", "TG", "TH", "TJ", "TL", "TM", "TN", "TO", "TR", "TT", "TV", "TW", "TZ", "UA", "UG", "UM", "US", "UY", "UZ", "VC", "VE", "VN", "VU", "WF", "WS", "YE", "ZA", "ZM", "ZW"]] = None # ISO 3166 region: typing.Optional[str] = None comment: typing.Optional[str] = None additional_data: typing.Optional[AdditionalData] = None @pydantic.field_validator('country', mode='before') def _preprocess_country(country: typing.Optional[str]) -> typing.Optional[str]: """ Ensure country code is upper case """ if country is not None: return country.upper() return None
[docs] class Weather(pydantic.BaseModel, BaseMergeable): """ Class to store the weather at time of observation. ``cloudiness`` The cloudiness in octas (from 0 to 8) or in the form of METAR code: - CLR: clear - FEW: few clouds - SCT: scattered - BKN: broken - OVC: overcast - X: precipitation ``precipitation`` Precipitation type, in the form of METAR code: - **Nil: No precipitation** - DZ: Drizzle - **RA: Rain** - **SN: Snow** (snow flakes) - **SG: Snow grains** (very small opaque grains, generally less than 1 mm) - IC: Ice crystals - PE: Ice pellets - GR: Hail (Grèle) - GS: Small hail and/or graupel (Grésil, grains below 5mm) - UP: Unknown precipitation type - **RASN: Rain and snow** - FZRA: Freezing rain The precipitation type can be preceded by '-' for light intensity or '+' for heavy intensity. The qualifier without +/- is moderate intensity. For a definition and pictures of the precipitation types, see e.g. `the International Cloud Atlas <https://cloudatlas.wmo.int/en/hydrometeors-other-than-clouds-falling.html>`_ ``air_temperature`` Temperature of air (°C) ``air_humidity`` Relative humidity (%) ``wind_speed`` Wind speed (m/s) ``wind_direction`` Wind direction (in degree, from 0 to 360) ``air_temperature_measurement_height`` Height of the air temperature measurement and humidity measurement (m) ``wind_measurement_height`` Height of the wind speed and direction measurement (m) ``comment`` Free comment on the weather ``additonal_data`` Field to store additional data for CAAML compatibility (customData), do not use. """ model_config = pydantic.ConfigDict( validate_assignment=True, extra='forbid') cloudiness: typing.Optional[typing.Literal[ 'CLR', 'FEW', 'SCT', 'BKN', 'OVC', 'X']] = None precipitation: typing.Optional[typing.Literal[ "-DZ", "DZ", "+DZ", "-RA", "RA", "+RA", "-SN", "SN", "+SN", "-SG", "SG", "+SG", "-IC", "IC", "+IC", "-PE", "PE", "+PE", "-GR", "GR", "+GR", "-GS", "GS", "+GS" "UP", "Nil", "RASN", "FZRA"]] = None air_temperature: typing.Optional[float] = None air_humidity: typing.Optional[float] = pydantic.Field(None, ge=0, le=100) wind_speed: typing.Optional[float] = pydantic.Field(None, ge=0) wind_direction: typing.Optional[int] = pydantic.Field(None, ge=0, le=360) air_temperature_measurement_height: typing.Optional[float] = pydantic.Field(None, gt=0) wind_measurement_height: typing.Optional[float] = pydantic.Field(None, gt=0) comment: typing.Optional[str] = None additional_data: typing.Optional[AdditionalData] = None @pydantic.field_validator('cloudiness', mode='before') def _preprocess_cloudiness(cloudiness: typing.Optional[str | int]) -> typing.Optional[str]: """ Ensure cloudiness is upper case and convert octas to METAR code """ if cloudiness is not None: if isinstance(cloudiness, str): return cloudiness.upper() elif isinstance(cloudiness, int): if cloudiness in cloudiness_attribution: return cloudiness_attribution[cloudiness] return cloudiness return None
[docs] class SpectralAlbedo(pydantic.BaseModel, BaseData, BaseMergeable): """ Class to store spectral albedo data The data contains: - ``min_wavelength`` (nm) - ``max_wavelength`` (nm) - ``albedo`` (between 0 and 1) and optionnally ``uncertainty`` (same unit as data) and/or ``quality`` (see :ref:`uncertainty`). """ model_config = pydantic.ConfigDict( validate_assignment=True, extra='forbid', arbitrary_types_allowed=True) comment: typing.Optional[str] = None _data_config = dict( _mode='Spectral', albedo=dict(min=0, max=1), uncertainty=dict(optional=True, nan_allowed=True), quality=dict(optional=True, type='O', values=QUALITY_FLAGS + [None]), ) def __init__(self, data=None, data_dict=None, **kwargs): super().__init__(**kwargs) checker = get_dataframe_checker(**self._data_config) if data is not None: self._data = checker(data) elif data_dict is not None: self._data = checker(data_dict) else: raise ValueError('data key is required')
[docs] class SolarMask(pydantic.BaseModel, BaseData): """ Class to store solar mask The data contains: - ``azimuth`` (degrees from north) - ``elevation`` (in degrees from horizontal) """ model_config = pydantic.ConfigDict( validate_assignment=True, extra='forbid', arbitrary_types_allowed=True) _data_config = dict( _mode='None', azimuth=dict(min=0, max=360), elevation=dict(min=-90, max=90), ) def __init__(self, data=None, data_dict=None, **kwargs): super().__init__(**kwargs) checker = get_dataframe_checker(**self._data_config) if data is not None: self._data = checker(data) elif data_dict is not None: self._data = checker(data_dict) else: raise ValueError('data key is required')
[docs] class SurfaceConditions(pydantic.BaseModel, BaseMergeable): """ Class to describe the snow surface conditions. ``surface_roughness`` Surface roughness according to Fierz et al., 2009: - rsm: smooth - rwa: wavy (ripples) - rcv: concave furrows (ablation hollows, sun cups, penitents, due to melt or sublimation) - rcx: conex furrows (rain or melt groves) - rrd: random furrows (due to wind erosion) ``surface_wind_features`` Wind features observable at the surface: - No observable wind bedforms - Snowdrift around obstacles - Snow ripples - Snow waves - Barchan dunes - Dunes - Loose patches - Pits - Snow steps - Sastrugi - mixed - other ``surface_melt_rain_features`` Other surface features: - Sun cups - Penitents - Melt or rain furrows - other ``surface_features_amplitude`` Amplitude of the surface features (m) ``surface_features_amplitude_max`` Maximum amplitude of the surface features (m) ``surface_features_amplitude_min`` Minimum amplitude of the surface features (m) ``surface_features_wavelength`` Wavelength of the surface features (m) ``surface_features_wavelength_max`` Maximum wavelength of the surface features (m) ``surface_features_wavelength_min`` Minimum wavelength of the surface features (m) ``surface_features_aspect`` Orientation of surface features (degree, from 0 to 360) ``comment`` Free comment on surface conditions ``lap_presence`` Indication of the presence of light absorbing particule at the snow surface. Values among: - No LAP - Black Carbon - Dust - Mixed - other ``surface_temperature`` Snow surface temperature (°C) ``surface_temperature_measurement_method`` Measurement method for the surface temperature: - Thermometer - Hemispheric IR - IR thermometer - other ``surface_albedo`` Snow surface albedo (0 -1) ``surface_albedo_comment`` Free comment on the snow albedo ``spectral_albedo`` Spectral albedo data, see :py:class:`snowprofile.classes.SpectralAlbedo` ``spectral_albedo_comment`` Free comment on the spectral snow albedo ``penetration_foot`` Depth of snowpack penetration by foot (float, m) ``penetration_ram`` Depth of snowpack penetration with the ramsonde (probe alone) (float, m) ``penetration_ski`` Depth of snowpack penetration by ski (float, m) ``additonal_data`` Field to store additional data for CAAML compatibility (customData), do not use. """ model_config = pydantic.ConfigDict( validate_assignment=True, extra='forbid') surface_roughness: typing.Optional[typing.Literal[ 'rsm', 'rwa', 'rcv', 'rcx', 'rrd']] = None surface_wind_features: typing.Optional[typing.Literal[ "No observable wind bedforms", "Snowdrift around obstacles", "Snow ripples", "Snow waves", "Barchan dunes", "Dunes", "Loose patches", "Pits", "Snow steps", "Sastrugi", "mixed", "other"]] = None surface_melt_rain_features: typing.Optional[typing.Literal[ "Sun cups", "Penitents", "Melt or rain furrows", "other"]] = None surface_features_amplitude: typing.Optional[float] = pydantic.Field(None, gt=0) surface_features_amplitude_min: typing.Optional[float] = pydantic.Field(None, gt=0) surface_features_amplitude_max: typing.Optional[float] = pydantic.Field(None, gt=0) surface_features_wavelength: typing.Optional[float] = pydantic.Field(None, gt=0) surface_features_wavelength_min: typing.Optional[float] = pydantic.Field(None, gt=0) surface_features_wavelength_max: typing.Optional[float] = pydantic.Field(None, gt=0) surface_features_aspect: typing.Optional[int] = pydantic.Field(None, ge=0, le=360) lap_presence: typing.Optional[typing.Literal[ "No LAP", "Black Carbon", "Dust", "Mixed", "other"]] = None surface_temperature: typing.Optional[float] = None surface_temperature_measurement_method: typing.Optional[typing.Literal[ 'Thermometer', 'Hemispheric IR', 'IR thermometer', 'other']] = None surface_albedo: typing.Optional[float] = None surface_albedo_comment: typing.Optional[str] = None spectral_albedo: typing.Optional[SpectralAlbedo] = None penetration_ram: typing.Optional[float] = pydantic.Field(None, ge=0) penetration_foot: typing.Optional[float] = pydantic.Field(None, ge=0) penetration_ski: typing.Optional[float] = pydantic.Field(None, ge=0) comment: typing.Optional[str] = None additional_data: typing.Optional[AdditionalData] = None
[docs] class Environment(pydantic.BaseModel, BaseMergeable): """ Description of the site environment, which is independant of the date and time of observation. ``solar_mask`` The solar mask at the observation site. :py:class:`snowprofile.classes.SolarMask` object. ``solar_mask_method_of_measurement`` Measurement method for the solar mask - Theodolite - Manual measurement - From DTM - From DSM - other ``solar_mask_uncertainty`` Quantitative uncertainty of the solar mask measurement. ``solar_mask_quality`` Qualitative quality of the solar mask measurement ``solar_mask_comment`` Free comment on the solar mask measurement ``bed_surface`` Characterization of the surface below the snowpack: - Sea ice - Glacier - Ice cap - Fresh water ice - Wetlands - Grassland - Shrubs - Rocks - Bare ground - Needle litter - Broadleaf litter - Artificial surface - Mixed - Other ``bed_surface_comment`` Free comment on the bed surface ``litter_thickness`` Thickness of the litter, if applicable (m) ``ice_thickness`` Thickness of the ice, if applicable (m) ``low_vegetation_height`` Height of the low vegetation, if applicable (m) ``LAI`` Leaf area index, measured at the vegetation peak (summer, m2/m2). ``forest_presence`` Type of forest, if applicable: - Open Area - Broadleaf forest - Needle forest - Mixed forest - Shrubs - Other ``forest_presence_comment`` Free comment to describe the forest ``sky_view_factor`` In case of forest site, the sky view factor (0 - 1) ``tree_height`` In case of forest site, the mean height of trees (m). ``solar_mask_additional_data`` Field to store additional data for CAAML compatibility (customData), do not use. """ model_config = pydantic.ConfigDict( validate_assignment=True, extra='forbid', arbitrary_types_allowed=True) solar_mask: typing.Optional[SolarMask] = pydantic.Field( None, description="The spectral albedo data.") solar_mask_method_of_measurement: typing.Optional[typing.Literal[ "Theodolite", "Manual measurement", "From DTM", "From DSM", "other"]] = None solar_mask_uncertainty: typing.Optional[float] = pydantic.Field(None, ge=0) solar_mask_quality: typing.Optional[typing.Literal[tuple(QUALITY_FLAGS)]] = None solar_mask_comment: typing.Optional[str] = None bed_surface: typing.Optional[typing.Literal[ "Sea ice", "Glacier", "Ice cap", "Fresh water ice", "Wetlands", "Grassland", "Shrubs", "Rocks", "Bare ground", "Needle litter", "Broadleaf litter", "Artificial surface", "Mixed", "Other"]] = None bed_surface_comment: typing.Optional[str] = None litter_thickness: typing.Optional[float] = pydantic.Field(None, ge=0) ice_thickness: typing.Optional[float] = pydantic.Field(None, ge=0) low_vegetation_height: typing.Optional[float] = pydantic.Field(None, ge=0) LAI: typing.Optional[float] = pydantic.Field(None, ge=0) forest_presence: typing.Optional[typing.Literal[ "Open Area", "Broadleaf forest", "Needle forest", "Mixed forest", "Shrubs", "Other"]] = pydantic.Field( None,) forest_presence_comment: typing.Optional[str] = None sky_view_factor: typing.Optional[float] = pydantic.Field(None, ge=0, le=1) tree_height: typing.Optional[float] = pydantic.Field(None, ge=0) solar_mask_additional_data: typing.Optional[AdditionalData] = None