Source code for datavalidation.core.geometryparameter

#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
GeometryParameter
----------------------------------

Object that holds information about a parameter of a BikeGeometry. It contains several utility functions
to retrieve the parameter value, its confidence, calculated value, etc.

It also provides logic to convert from/to JSON representation.

Author: Javier Chiyah, Heriot-Watt University, 2019
"""


import logging

from .constants import GEOMETRY_PARAMETERS


[docs]class GeometryParameter: """ A GeometryParameter can be initialised with its name and value. The value can be None if it is used as a placeholder for a future value. Example initialisation:: >> GeometryParamter("head_angle", 73) GeometryParameter() object # or >> GeometryParamter("head_angle", "73") # here "73" is casted to float without having to normalise GeometryParameter() object # or >> GeometryParamter("head_angle", None) GeometryParameter() object Normalising or validating the parameter changes the object in place and expand the available functionality with additional class member variables such as calculated_value. :param name: name of the parameter :param value: value of the parameter, either as string or the correct value :param extra_values: if there is anything else to store in the parameter, but not used (e.g. "id" field) """ _name = None _value = None _original_value = None _calculated_value = None _type = str _confidence = None _extra_values = {} def __init__(self, name: str, value, extra_values: dict = None): self._name = name self._original_value = value self._extra_values = extra_values if extra_values is not None else {} self._resolve_type() # try to set the value to the correct type directly try: self.set_normalised_value(value) except (ValueError, TypeError): if value is not None and value != "": logging.debug("GeometryParameter('{}') needs normalisation (v: '{}', {})".format( self.name, self.original_value, self.type ))
[docs] @classmethod def from_dict(cls, json_dict: dict): """ Creates a new GeometryParameter from a dictionary. Example dict:: { "p": "parameter_name", "v": "parameter_value", "id": "optional additional field", ... } :param json_dict: dict of the parameter (aka JSON) :return: GeometryParameter """ pname = json_dict['p'] pvalue = json_dict['v'] new_dict = dict(json_dict) del new_dict['p'] del new_dict['v'] return cls( name=pname, value=pvalue, extra_values=new_dict )
@property def name(self) -> str: """ Gets the name of the parameter. :return: string name """ return self._name @property def value(self): """ Gets the most up to date value of the parameter (it can be the normalised value, the calculated value or the original value if none of those is available). It will return the value in the correct type (e.g. integers will return as int) and it will not produce exceptions if there is something wrong with the GeometryParameter value. In order, it provides: - the calculated value if such exists - the normalised value if the parameter has been normalised - the original value in the correct type (if it can cast it, e.g. "17" can be easily casted to float) - the original value as a string if all above fails You should check other functions if you require one of these values in particular instead. :return: [int|float|str] value """ if self._calculated_value: return self.calculated_value elif self._value is None and self._original_value: return str(self._original_value) else: return self._value @property def normalised_value(self): """ Gets the normalised value of the parameter, without taking the calculated value into account. It will return the value in the correct type (e.g. integers will return as int). If the GeometryParameter has not been normalised, it will return the original parameter instead. :return: [int|float|str] value """ if self._value is None and self._original_value: return str(self._original_value) else: return self._value @property def original_value(self) -> str: """ Gets the original value of the GeometryParameter as a string, regardless of its true type. :return: string value """ return self._original_value @property def calculated_value(self): """ Gets the calculated value of the parameter if one exists. It will return the value in the correct type (e.g. integers will return as int). :return: [int|float|str] value or None """ return self._calculated_value @property def type(self): """ Gets the type of the GeometryParameter. :return: type """ return self._type @property def confidence(self) -> float: """ Gets the confidence score of the GeometryParameter. :return: float confidence score """ return self._confidence
[docs] def set_normalised_value(self, new_value): """ Sets the value of the GeometryParameter after normalising its value. It will raise an exception if the new value is not a valid type. It gets the new value (e.g., a str) and casts it to the correct type (e.g., float) to make sure it does not violate any constraints of normalised parameters. It can also accept a list, which means a range of values (e.g., [190, 170]). :param new_value: normalised value :return: None :raise ValueError: raised if the new value cannot be casted into the correct type :raise TypeError: raised if the new value is None or the empty string """ if isinstance(new_value, list): # handle ranges self._value = [self.type(x) for x in new_value] else: self._value = self.type(new_value)
# logging.debug("GeometryParameter('{}') has a new normalised value: '{}'".format(self.name, self._value))
[docs] def set_calculated_value(self, new_value, change_confidence: bool = True): """ Sets the calculated value of the GeometryParameter. It will raise an exception if the new value is not a valid type. :param new_value: calculated value :param change_confidence: if True, it will change confidence, default is False :return: None :raise ValueError: raised if the new value cannot be casted into the correct type :raise TypeError: raised if the new value is None or the empty string """ if self._calculated_value is None: if isinstance(new_value, list): if len(new_value) == 1: self._calculated_value = self.type(new_value[0]) else: self._calculated_value = [self.type(x) for x in new_value] else: self._calculated_value = self.type(new_value) if self._value is None: logging.info("GeometryParameter('{}') has a new calculated value: {}".format(self.name, self.value)) if change_confidence and self._confidence is None: self.set_confidence(0.75) else: # do not change the calculated value logging.warning("GeometryParameter('{}') already has a calculated value (current={}, new={})".format( self.name, self._calculated_value, new_value ))
[docs] def set_confidence(self, confidence: float, force: bool = False): """ Sets the confidence score of the GeometryParameter. Note that it will average the current confidence value with the one given to smooth differences in precision of the equations. It also rounds the confidence to be within the range of 0 to 1 (inclusive) :param confidence: new confidence value :param force: if True, it will force the confidence change. Otherwise, it chooses the highest (default is False) :return: None """ if self._confidence is None or force or self.calculated_value is None: if self._confidence is None: self._confidence = confidence else: self._confidence = (self._confidence + confidence) / 2 if self.confidence < 0 or self.confidence > 1: logging.warning("GeometryParameter('{}') has confidence value of '{}' (outside the 0-1 range)".format( self.name, self.confidence)) self._confidence = 0 if self.confidence < 0 else 1
[docs] def is_number(self) -> bool: """ Checks if the GeometryParameter is a number type (float or int, but not a string or unknown). It is used to check if the GeometryParameter needs to be normalised or validated. :return: bool, True if the GeometryParameter is a float or an int """ return self.type is not str
[docs] def to_dict(self, string_values: bool = True) -> dict: """ Returns the GeometryParameter as a dictionary ready to be serialised into a JSON response. It packs all the information currently contained in the GeometryParameter. :param string_values: whether to return the values in string format or not, default is True :return: dict """ json_dict = { "p": self.name, **self._extra_values } if self.value is not None: json_dict['v'] = self._format_parameter_value(self.value) if string_values else self.value else: json_dict['v'] = "" if self.original_value != json_dict['v']: json_dict['original_v'] = self.original_value if self.calculated_value is not None: json_dict['v'] = self._value if self._value is not None else self.calculated_value json_dict['v'] = self._format_parameter_value(json_dict['v']) if string_values else json_dict['v'] json_dict['calculated_v'] = self._format_parameter_value(self.calculated_value) \ if string_values else self.calculated_value if self.confidence is not None: json_dict['confidence'] = self.confidence return json_dict
def _resolve_type(self): """ Resolves the type of the GeometryParameter from the list of names. :return: None """ try: self._type = GEOMETRY_PARAMETERS[self.name] except KeyError: logging.warning("Unknown GeometryParameter name '{}', defaulting to type str".format(self.name)) @staticmethod def _format_parameter_value(value) -> str: """ Formats a value to a string, taking the final 0s if the value is a float. E.g. "1.000000" becomes "1" :param value: value to convert to string :return: string """ if "." in str(value): return str(value).rstrip("0").rstrip(".") else: return str(value)