"""Base class for SciUnit scores."""
from copy import copy
import numpy as np
from sciunit.base import SciUnit
from sciunit.utils import log, config_get
from sciunit.errors import InvalidScoreError
from typing import Union, Tuple
from quantities import Quantity
[docs]class Score(SciUnit):
"""Abstract base class for scores."""
[docs] def __init__(self, score: Union['Score', float, int, Quantity], related_data: dict=None):
"""Abstract base class for scores.
Args:
score (Union['Score', float, int, Quantity], bool): A raw value to wrap in a Score class.
related_data (dict, optional): Artifacts to store with the score.
"""
self.check_score(score)
if related_data is None:
related_data = {}
self.score, self.related_data = score, related_data
if isinstance(score, Exception):
# Set to error score to use its summarize().
self.__class__ = ErrorScore
super(Score, self).__init__()
score = None
"""The score itself."""
_best = None
"""The best possible score of this type"""
_worst = None
"""The best possible score of this type"""
_allowed_types = None
"""List of allowed types for the score argument"""
_allowed_types_message = ("Score of type %s is not an instance "
"of one of the allowed types: %s")
"""Error message when score argument is not one of these types"""
_description = ""
"""A description of this score, i.e. how to interpret it.
Provided in the score definition"""
description = ""
"""A description of this score, i.e. how to interpret it.
For the user to set in bind_score"""
_raw = None
"""A raw number arising in a test's compute_score,
used to determine this score. Can be set for reporting a raw value
determined in Test.compute_score before any transformation,
e.g. by a Converter"""
related_data = None
"""Data specific to the result of a test run on a model."""
test = None
"""The test taken. Set automatically by Test.judge."""
model = None
"""The model judged. Set automatically by Test.judge."""
[docs] def check_score(self, score: 'Score') -> None:
"""Check the score with imposed additional constraints in the subclass on the score, e.g. the range of the allowed score.
Args:
score (Score): A sciunit score instance.
Raises:
InvalidScoreError: Exception raised if `score` is not a instance of sciunit score.
"""
if self._allowed_types and \
not isinstance(score, self._allowed_types+(Exception,)):
raise InvalidScoreError(self._allowed_types_message %
(type(score), self._allowed_types))
self._check_score(score)
[docs] def _check_score(self, score: 'Score') -> None:
"""A method for each Score subclass to impose additional constraints on the score, e.g. the range of the allowed score.
Args:
score (Score): A sciunit score instance.
"""
pass
[docs] @classmethod
def compute(cls, observation: dict, prediction: dict):
"""Compute whether the observation equals the prediction.
Args:
observation (dict): The observation from the real world.
prediction (dict): The prediction generated by a model.
Returns:
NotImplementedError: Not implemented error.
"""
return NotImplementedError("")
@property
def norm_score(self) -> 'Score':
"""A floating point version of the score used for sorting.
If normalized = True, this must be in the range 0.0 to 1.0,
where larger is better (used for sorting and coloring tables).
Returns:
Score: The [0-1] normalized score.
"""
return self.score
@property
def log_norm_score(self) -> np.ndarray:
"""The natural logarithm of the `norm_score`.
This is useful for guaranteeing convexity in an error surface.
Returns:
np.ndarray: The natural logarithm of the `norm_score`.
"""
return np.log(self.norm_score) if self.norm_score is not None else None
@property
def log2_norm_score(self) -> np.ndarray:
"""The logarithm base 2 of the `norm_score`.
This is useful for guaranteeing convexity in an error surface.
Returns:
np.ndarray: The logarithm base 2 of the `norm_score`.
"""
return np.log2(self.norm_score) if self.norm_score is not None else None
@property
def log10_norm_score(self) -> np.ndarray:
"""The logarithm base 10 of the `norm_score`.
This is useful for guaranteeing convexity in an error surface.
Returns:
np.ndarray: The logarithm base 10 of the `norm_score`.
"""
return np.log10(self.norm_score) if self.norm_score is not None else None
[docs] def color(self, value: Union[float, 'Score']=None) -> tuple:
"""Turn the score intp an RGB color tuple of three 8-bit integers.
Args:
value (Union[float,, optional): The score that will be turned to an RGB color. Defaults to None.
Returns:
tuple: A tuple of three 8-bit integers that represents an RGB color.
"""
if value is None:
value = self.norm_score
rgb = Score.value_color(value)
return rgb
[docs] @classmethod
def value_color(cls, value: Union[float, 'Score']) -> tuple:
"""Get a RGB color based on the Score.
Args:
value (Union[float,): [description]
Returns:
tuple: [description]
"""
import matplotlib.cm as cm
if value is None or np.isnan(value):
rgb = (128, 128, 128)
else:
cmap_low = config_get('cmap_low', 38)
cmap_high = config_get('cmap_high', 218)
cmap_range = cmap_high - cmap_low
cmap = cm.RdYlGn(int(cmap_range*value+cmap_low))[:3]
rgb = tuple([x*256 for x in cmap])
return rgb
@property
def summary(self) -> str:
"""Summarize the performance of a model on a test.
Returns:
str: The summary of this score.
"""
return "=== Model %s achieved score %s on test '%s'. ===" % \
(str(self.model), str(self), self.test)
[docs] def summarize(self):
"""[summary]
"""
if self.score is not None:
log("%s" % self.summary)
[docs] def _describe(self) -> str:
"""Get the description of this score.
Returns:
str: The description of this score.
"""
result = "No description available"
if self.score is not None:
if self.description:
result = "%s" % self.description
elif self.test.score_type.__doc__:
result = self.describe_from_docstring()
return result
[docs] def describe_from_docstring(self) -> str:
"""Get the description of this score from the docstring.
Returns:
str: The description of this score.
"""
s = [self.test.score_type.__doc__.strip().
replace('\n', '').replace(' ', '')]
if self.test.converter:
s += [self.test.converter.description]
s += [self._description]
result = '\n'.join(s)
return result
[docs] def describe(self, quiet: bool=False) -> Union[str, None]:
"""Get the description of this score instance.
Args:
quiet (bool, optional): If `True`, then log the description, return the description otherwise.
Defaults to False.
Returns:
Union[str, None]: If not `quiet`, then return the description of this score instance.
Otherwise, `None`.
"""
d = self._describe()
if quiet:
return d
else:
log(d)
@property
def raw(self) -> str:
"""The raw score in string type.
Returns:
str: The raw score.
"""
value = self._raw if self._raw else self.score
if isinstance(value, (float, np.ndarray)):
string = '%.4g' % value
if hasattr(value, 'magnitude'):
string += ' %s' % str(value.units)[4:]
else:
string = None
return string
[docs] def get_raw(self) -> float:
"""Get the raw score. If there is not raw score, then get score.
Returns:
float: The raw score.
"""
value = copy(self._raw) if self._raw else copy(self.score)
return value
[docs] def set_raw(self, raw: float) -> None:
"""Set the raw score.
Args:
raw (float): The raw score to be set.
"""
self._raw = raw
[docs] def __repr__(self) -> str:
"""[summary]
"""
return self.__str__()
[docs] def __str__(self) -> str:
"""[summary]
"""
return '%s' % self.score
[docs] def __eq__(self, other: Union['Score', float]) -> bool:
"""[summary]
"""
if isinstance(other, Score):
result = self.norm_score == other.norm_score
else:
result = self.score == other
return result
[docs] def __ne__(self, other: Union['Score', float]) -> bool:
"""[summary]
"""
if isinstance(other, Score):
result = self.norm_score != other.norm_score
else:
result = self.score != other
return result
[docs] def __gt__(self, other: Union['Score', float]) -> bool:
"""[summary]
"""
if isinstance(other, Score):
result = self.norm_score > other.norm_score
else:
result = self.score > other
return result
[docs] def __ge__(self, other: Union['Score', float]) -> bool:
"""[summary]
"""
if isinstance(other, Score):
result = self.norm_score >= other.norm_score
else:
result = self.score >= other
return result
[docs] def __lt__(self, other: Union['Score', float]) -> bool:
"""[summary]
"""
if isinstance(other, Score):
result = self.norm_score < other.norm_score
else:
result = self.score < other
return result
[docs] def __le__(self, other: Union['Score', float]) -> bool:
"""[summary]
"""
if isinstance(other, Score):
result = self.norm_score <= other.norm_score
else:
result = self.score <= other
return result
@property
def score_type(self):
"""The type of the score.
Returns:
str: the name of the score class.
"""
return self.__class__.__name__
[docs]class ErrorScore(Score):
"""A score returned when an error occurs during testing."""
@property
def norm_score(self) -> float:
"""Get the norm score, which is 0.0 for `ErrorScore` instance.
Returns:
float: The norm score.
"""
return 0.0
@property
def summary(self) -> str:
"""Summarize the performance of a model on a test.
Returns:
str: A textual summary of the score.
"""
return "== Model %s did not complete test %s due to error '%s'. ==" %\
(str(self.model), str(self.test), str(self.score))
[docs] def _describe(self) -> str:
return self.summary
[docs] def __str__(self) -> str:
return 'Error'