X-Git-Url: https://gerrit.fd.io/r/gitweb?a=blobdiff_plain;f=resources%2Flibraries%2Fpython%2FMLRsearch%2Fmeasurement_database.py;fp=resources%2Flibraries%2Fpython%2FMLRsearch%2Fmeasurement_database.py;h=7a6618c0daf5eb93423d10a4a6c66f81eabef273;hb=e5dbe10d9599b9a53fa07e6fadfaf427ba6d69e3;hp=0000000000000000000000000000000000000000;hpb=c6dfb6c09c5dafd1d522f96b4b86c5ec5efc1c83;p=csit.git diff --git a/resources/libraries/python/MLRsearch/measurement_database.py b/resources/libraries/python/MLRsearch/measurement_database.py new file mode 100644 index 0000000000..7a6618c0da --- /dev/null +++ b/resources/libraries/python/MLRsearch/measurement_database.py @@ -0,0 +1,126 @@ +# Copyright (c) 2023 Cisco and/or its affiliates. +# 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. + +"""Module defining MeasurementDatabase class.""" + +from dataclasses import dataclass +from typing import Dict, Tuple + +from .discrete_load import DiscreteLoad +from .discrete_result import DiscreteResult +from .load_stats import LoadStats +from .relevant_bounds import RelevantBounds +from .target_spec import TargetSpec +from .trimmed_stat import TrimmedStat + + +@dataclass +class MeasurementDatabase: + """Structure holding measurement results for multiple durations and loads. + + Several utility methods are added, accomplishing tasks useful for MLRsearch. + + While TargetStats can decide when a single load is a lower bound (or upper), + it does not deal with loss inversion (higher load with less load). + + This class introduces the concept of relevant bounds. + Relevant upper bound is simply the lowest load classified as an upper bound. + But relevant lower bound is only chosen from lower bound loads + strictly smaller than the relevant upper bound. + This way any higher loads with good results are ignored, + so relevant bound give conservative estimate of SUT true performance. + """ + + targets: Tuple[TargetSpec] = None + """Targets to track stats for.""" + load_to_stats: Dict[DiscreteLoad, LoadStats] = None + """Mapping from loads to stats.""" + + def __post_init__(self) -> None: + """Check and sort initial values. + + If no stats yet, initialize empty ones. + + :raises ValueError: If there are no targets. + """ + if not self.targets: + raise ValueError(f"Database needs targets: {self.targets!r}") + if not self.load_to_stats: + self.load_to_stats = {} + self._sort() + + def _sort(self) -> None: + """Sort keys from low to high load.""" + self.load_to_stats = dict(sorted(self.load_to_stats.items())) + + def __getitem__(self, key: DiscreteLoad) -> LoadStats: + """Allow access to stats as if self was load_to_stats. + + This also accepts LoadStats as key, so callers do not need + to care about hashability. + + :param key: The load to get stats for. + :type key: DiscreteLoad + :returns: Stats for the given load. + :rtype LoadStats: + """ + return self.load_to_stats[key.hashable()] + + def add(self, result: DiscreteResult) -> None: + """Incorporate given trial measurement result. + + :param result: Measurement result to add to the database. + :type result: DiscreteResult + """ + discrete_load = result.discrete_load.hashable() + if not discrete_load.is_round: + raise ValueError(f"Not round load: {discrete_load!r}") + if discrete_load not in self.load_to_stats: + self.load_to_stats[discrete_load] = LoadStats.new_empty( + load=discrete_load, + targets=self.targets, + ) + self._sort() + self.load_to_stats[discrete_load].add(result) + + def get_relevant_bounds(self, target: TargetSpec) -> RelevantBounds: + """Return None or a valid trimmed stat, for the two relevant bounds. + + A load is valid only if both optimistic and pessimistic estimates agree. + + If some value is not available, None is returned instead. + The returned stats are trimmed to the argument target. + + The implementation starts from low loads + and the search stops at lowest upper bound, + thus conforming to the conservative definition of relevant bounds. + + :param target: Target to classify loads when finding bounds. + :type target: TargetSpec + :returns: Relevant lower bound, relevant upper bound. + :rtype: RelevantBounds + """ + lower_bound, upper_bound = None, None + for load_stats in self.load_to_stats.values(): + opt, pes = load_stats.estimates(target) + if opt != pes: + continue + if not opt: + upper_bound = load_stats + break + lower_bound = load_stats + if lower_bound: + lower_bound = TrimmedStat.for_target(lower_bound, target) + if upper_bound: + upper_bound = TrimmedStat.for_target(upper_bound, target) + return RelevantBounds(clo=lower_bound, chi=upper_bound)