diff options
Diffstat (limited to 'resources/libraries/python/MLRsearch/strategy')
8 files changed, 703 insertions, 0 deletions
diff --git a/resources/libraries/python/MLRsearch/strategy/__init__.py b/resources/libraries/python/MLRsearch/strategy/__init__.py new file mode 100644 index 0000000000..a1e0225a17 --- /dev/null +++ b/resources/libraries/python/MLRsearch/strategy/__init__.py @@ -0,0 +1,35 @@ +# 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. + +""" +__init__ file for Python package "strategy". +""" + +from .base import StrategyBase +from .bisect import BisectStrategy +from .extend_hi import ExtendHiStrategy +from .extend_lo import ExtendLoStrategy +from .halve import HalveStrategy +from .refine_hi import RefineHiStrategy +from .refine_lo import RefineLoStrategy + + +STRATEGY_CLASSES = ( + HalveStrategy, + RefineLoStrategy, + RefineHiStrategy, + ExtendLoStrategy, + ExtendHiStrategy, + BisectStrategy, +) +"""Tuple of strategy constructors, in order of priority decreasing.""" diff --git a/resources/libraries/python/MLRsearch/strategy/base.py b/resources/libraries/python/MLRsearch/strategy/base.py new file mode 100644 index 0000000000..0724f882bf --- /dev/null +++ b/resources/libraries/python/MLRsearch/strategy/base.py @@ -0,0 +1,132 @@ +# 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 StrategyBase class.""" + + +from abc import ABC, abstractmethod +from dataclasses import dataclass, field +from typing import Callable, Optional, Tuple + +from ..discrete_interval import DiscreteInterval +from ..discrete_load import DiscreteLoad +from ..discrete_width import DiscreteWidth +from ..expander import TargetedExpander +from ..limit_handler import LimitHandler +from ..relevant_bounds import RelevantBounds +from ..target_spec import TargetSpec + + +@dataclass +class StrategyBase(ABC): + """Abstract class encompassing data common to most strategies. + + A strategy is one piece of logic a selector may use + when nominating a candidate according to its current target. + + The two initial bound arguments may not be bounds at all. + For initial targets, the two values are usually mrr and mrr2. + For subsequent targets, the initial values are usually + the relevant bounds of the preceding target, + but one of them may be None if hitting min or max load. + + The initial values are mainly used as stable alternatives + to relevant bounds of preceding target, + because those bounds may have been unpredictably altered + by nominations from unrelated search goals. + This greatly simplifies reasoning about strategies making progress. + """ + + target: TargetSpec + """The target this strategy is focusing on.""" + expander: TargetedExpander + """Instance to track width expansion during search (if applicable).""" + initial_lower_load: Optional[DiscreteLoad] + """Smaller of the two loads distinguished at instance creation. + Can be None if upper bound is the min load.""" + initial_upper_load: Optional[DiscreteLoad] + """Larger of the two loads distinguished at instance creation. + Can be None if lower bound is the max load.""" + handler: LimitHandler = field(repr=False) + """Reference to the limit handler instance.""" + debug: Callable[[str], None] = field(repr=False) + """Injectable function for debug logging.""" + + @abstractmethod + def nominate( + self, bounds: RelevantBounds + ) -> Tuple[Optional[DiscreteLoad], Optional[DiscreteWidth]]: + """Nominate a load candidate if the conditions activate this strategy. + + A complete candidate refers also to the nominating selector. + To prevent circular dependence (selector refers to nominating strategy), + this function returns only duration and width. + + Width should only be non-None if global current width should be updated + when the candidate based on this becomes winner. + But currently all strategies return non-None width + if they return non-None load. + + :param bounds: Freshly updated bounds relevant for current target. + :type bounds: RelevantBounds + :returns: Two nones or candidate intended load and duration. + :rtype: Tuple[Optional[DiscreteLoad], Optional[DiscreteWidth]] + """ + return None, None + + def won(self, bounds: RelevantBounds, load: DiscreteLoad) -> None: + """Notify the strategy its candidate became the winner. + + Most strategies have no use for this information, + but some strategies may need to update their private information. + + :param bounds: Freshly updated bounds relevant for current target. + :param load: The current load, so strategy does not need to remember. + :type bounds: RelevantBounds + :type load: DiscreteLoad + """ + return + + def not_worth(self, bounds: RelevantBounds, load: DiscreteLoad) -> bool: + """A check on bounds common for multiple strategies. + + The load is worth measuring only if it can create or improve + either relevant bound. + + Each strategy is designed to create a relevant bound for current target, + which is only needed if that (or better) bound does not exist yet. + Conversely, if a strategy does not nominate, it is because + the load it would nominate (if any) is found not worth by this method. + + :param bounds: Current relevant bounds. + :param load: Load of a possible candidate. + :type bounds: RelevantBounds + :type load: DiscreteLoad + :returns: True if the load should NOT be nominated. + :rtype: bool + """ + if bounds.clo and bounds.clo >= load: + return True + if bounds.chi and bounds.chi <= load: + return True + if bounds.clo and bounds.chi: + # We are not hitting min nor max load. + # Measuring at this load will create or improve clo or chi. + # The only reason not to nominate is if interval is narrow already. + wig = DiscreteInterval( + lower_bound=bounds.clo, + upper_bound=bounds.chi, + ).width_in_goals(self.target.discrete_width) + if wig <= 1.0: + return True + return False diff --git a/resources/libraries/python/MLRsearch/strategy/bisect.py b/resources/libraries/python/MLRsearch/strategy/bisect.py new file mode 100644 index 0000000000..894544695e --- /dev/null +++ b/resources/libraries/python/MLRsearch/strategy/bisect.py @@ -0,0 +1,193 @@ +# 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 BisectStrategy class.""" + + +from dataclasses import dataclass +from typing import Optional, Tuple + +from ..discrete_interval import DiscreteInterval +from ..discrete_load import DiscreteLoad +from ..discrete_width import DiscreteWidth +from ..relevant_bounds import RelevantBounds +from .base import StrategyBase + + +@dataclass +class BisectStrategy(StrategyBase): + """Strategy to use when both bounds relevant to curent target are present. + + Primarily, this strategy is there to perform internal search. + As powers of two are fiendly to binary search, + this strategy relies on the splitting logic described in DiscreteInterval. + + The main reason why this class is so long is that a mere existence + of a valid bound for the current target does not imply + that bound is a good approximation of the final conditional throughput. + The bound might become valid due to efforts of a strategy + focusing on an entirely different search goal. + + On the other hand, initial bounds may be better approximations, + but they also may be bad approximations (for example + when SUT behavior strongly depends on trial duration). + + Based on comparison of existing current bounds to intial bounds, + this strategy also mimics what would external search do + (if the one current bound was missing and other initial bound was current). + In case that load value is closer to appropriate inital bound + (compared to how far the simple bisect between current bounds is), + that load is nominated. + + It turns out those "conditional" external search nominations + are quite different from unconditional ones, + at least when it comes to handling limits + and tracking when width expansion should be applied. + That is why that logic is here + and not in some generic external search class. + """ + + expand_on_clo: bool = False + """If extending up, width should be expanded when load becomes clo.""" + expand_on_chi: bool = False + """If extending down, width should be expanded when load becomes chi.""" + + def nominate( + self, bounds: RelevantBounds + ) -> Tuple[Optional[DiscreteLoad], Optional[DiscreteWidth]]: + """Nominate a load candidate between bounds or extending from them. + + The external search logic is offloaded into private methods. + If they return a truthy load, that is returned from here as well. + + Only if the actual bisect is selected, + the per-selector expander is limited to the (smaller) new width. + + :param bounds: Freshly updated bounds relevant for current target. + :type bounds: RelevantBounds + :returns: Two nones or candidate intended load and duration. + :rtype: Tuple[Optional[DiscreteLoad], Optional[DiscreteWidth]] + """ + if not bounds.clo or bounds.clo >= self.handler.max_load: + return None, None + if not bounds.chi or bounds.chi <= self.handler.min_load: + return None, None + interval = DiscreteInterval(bounds.clo, bounds.chi) + if interval.width_in_goals(self.target.discrete_width) <= 1.0: + return None, None + bisect_load = interval.middle(self.target.discrete_width) + load, width = self._extend_lo(bounds, bisect_load) + if load: + self.expand_on_clo, self.expand_on_chi = False, True + self.debug(f"Preferring to extend down: {load}") + return load, width + load, width = self._extend_hi(bounds, bisect_load) + if load: + self.expand_on_clo, self.expand_on_chi = True, False + self.debug(f"Preferring to extend up: {load}") + return load, width + load = bisect_load + if self.not_worth(bounds=bounds, load=load): + return None, None + self.expand_on_clo, self.expand_on_chi = False, False + self.debug(f"Preferring to bisect: {load}") + width_lo = DiscreteInterval(bounds.clo, load).discrete_width + width_hi = DiscreteInterval(load, bounds.chi).discrete_width + width = min(width_lo, width_hi) + self.expander.limit(width) + return load, width + + def _extend_lo( + self, bounds: RelevantBounds, bisect_load: DiscreteLoad + ) -> Tuple[Optional[DiscreteLoad], Optional[DiscreteWidth]]: + """Compute load as if extending down, return it if preferred. + + :param bounds: Freshly updated bounds relevant for current target. + :param bisect_load: Load when bisection is preferred. + :type bounds: RelevantBounds + :type bisect_load: DiscreteLoad + :returns: Two nones or candidate intended load and duration. + :rtype: Tuple[Optional[DiscreteLoad], Optional[DiscreteWidth]] + :raises RuntimeError: If an internal inconsistency is detected. + """ + # TODO: Simplify all the conditions or explain them better. + if not self.initial_upper_load: + return None, None + if bisect_load >= self.initial_upper_load: + return None, None + width = self.expander.get_width() + load = bounds.chi - width + load = self.handler.handle( + load=load, + width=self.target.discrete_width, + clo=bounds.clo, + chi=bounds.chi, + ) + if not load: + return None, None + if load <= bisect_load: + return None, None + if load >= self.initial_upper_load: + return None, None + if self.not_worth(bounds=bounds, load=load): + raise RuntimeError(f"Load not worth: {load}") + return load, width + + def _extend_hi( + self, bounds: RelevantBounds, bisect_load: DiscreteLoad + ) -> Tuple[Optional[DiscreteLoad], Optional[DiscreteWidth]]: + """Compute load as if extending up, return it if preferred. + + :param bounds: Freshly updated bounds relevant for current target. + :param bisect_load: Load when bisection is preferred. + :type bounds: RelevantBounds + :type bisect_load: DiscreteLoad + :returns: Two nones or candidate intended load and duration. + :rtype: Tuple[Optional[DiscreteLoad], Optional[DiscreteWidth]] + :raises RuntimeError: If an internal inconsistency is detected. + """ + # TODO: Simplify all the conditions or explain them better. + if not self.initial_lower_load: + return None, None + if bisect_load <= self.initial_lower_load: + return None, None + width = self.expander.get_width() + load = bounds.clo + width + load = self.handler.handle( + load=load, + width=self.target.discrete_width, + clo=bounds.clo, + chi=bounds.chi, + ) + if not load: + return None, None + if load >= bisect_load: + return None, None + if load <= self.initial_lower_load: + return None, None + if self.not_worth(bounds=bounds, load=load): + raise RuntimeError(f"Load not worth: {load}") + return load, width + + def won(self, bounds: RelevantBounds, load: DiscreteLoad) -> None: + """Expand width when appropriate. + + :param bounds: Freshly updated bounds relevant for current target. + :param load: The current load, so strategy does not need to remember. + :type bounds: RelevantBounds + :type load: DiscreteLoad + """ + if self.expand_on_clo and load == bounds.clo: + self.expander.expand() + elif self.expand_on_chi and load == bounds.chi: + self.expander.expand() diff --git a/resources/libraries/python/MLRsearch/strategy/extend_hi.py b/resources/libraries/python/MLRsearch/strategy/extend_hi.py new file mode 100644 index 0000000000..79c4ad7cf2 --- /dev/null +++ b/resources/libraries/python/MLRsearch/strategy/extend_hi.py @@ -0,0 +1,76 @@ +# 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 ExtendHiStrategy class.""" + + +from dataclasses import dataclass +from typing import Optional, Tuple + +from ..discrete_load import DiscreteLoad +from ..discrete_width import DiscreteWidth +from ..relevant_bounds import RelevantBounds +from .base import StrategyBase + + +@dataclass +class ExtendHiStrategy(StrategyBase): + """This strategy is applied when there is no relevant upper bound. + + Typically this is needed after RefineHiStrategy turned initial upper bound + into a current relevant lower bound. + """ + + def nominate( + self, bounds: RelevantBounds + ) -> Tuple[Optional[DiscreteLoad], Optional[DiscreteWidth]]: + """Nominate current relevant lower bound plus expander width. + + This performs external search in upwards direction, + until a valid upper bound for the current target is found, + or until max load is hit. + Limit handling is used to avoid nominating too close + (or above) the max rate. + + Width expansion is only applied if the candidate becomes a lower bound, + so that is detected in done method. + + :param bounds: Freshly updated bounds relevant for current target. + :type bounds: RelevantBounds + :returns: Two nones or candidate intended load and duration. + :rtype: Tuple[Optional[DiscreteLoad], Optional[DiscreteWidth]] + """ + if bounds.chi or not bounds.clo or bounds.clo >= self.handler.max_load: + return None, None + width = self.expander.get_width() + load = self.handler.handle( + load=bounds.clo + width, + width=self.target.discrete_width, + clo=bounds.clo, + chi=bounds.chi, + ) + if self.not_worth(bounds=bounds, load=load): + return None, None + self.debug(f"No chi, extending up: {load}") + return load, width + + def won(self, bounds: RelevantBounds, load: DiscreteLoad) -> None: + """Expand width if the load became the new lower bound. + + :param bounds: Freshly updated bounds relevant for current target. + :param load: The current load, so strategy does not need to remember. + :type bounds: RelevantBounds + :type load: DiscreteLoad + """ + if load == bounds.clo: + self.expander.expand() diff --git a/resources/libraries/python/MLRsearch/strategy/extend_lo.py b/resources/libraries/python/MLRsearch/strategy/extend_lo.py new file mode 100644 index 0000000000..68d20b6a6a --- /dev/null +++ b/resources/libraries/python/MLRsearch/strategy/extend_lo.py @@ -0,0 +1,76 @@ +# 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 ExtendLoStrategy class.""" + + +from dataclasses import dataclass +from typing import Optional, Tuple + +from ..discrete_load import DiscreteLoad +from ..discrete_width import DiscreteWidth +from ..relevant_bounds import RelevantBounds +from .base import StrategyBase + + +@dataclass +class ExtendLoStrategy(StrategyBase): + """This strategy is applied when there is no relevant lower bound. + + Typically this is needed after RefineLoStrategy turned initial lower bound + into a current relevant upper bound. + """ + + def nominate( + self, bounds: RelevantBounds + ) -> Tuple[Optional[DiscreteLoad], Optional[DiscreteWidth]]: + """Nominate current relevant upper bound minus expander width. + + This performs external search in downwards direction, + until a valid lower bound for the current target is found, + or until min load is hit. + Limit handling is used to avoid nominating too close + (or below) the min rate. + + Width expansion is only applied if the candidate becomes an upper bound, + so that is detected in done method. + + :param bounds: Freshly updated bounds relevant for current target. + :type bounds: RelevantBounds + :returns: Two nones or candidate intended load and duration. + :rtype: Tuple[Optional[DiscreteLoad], Optional[DiscreteWidth]] + """ + if bounds.clo or not bounds.chi or bounds.chi <= self.handler.min_load: + return None, None + width = self.expander.get_width() + load = self.handler.handle( + load=bounds.chi - width, + width=self.target.discrete_width, + clo=bounds.clo, + chi=bounds.chi, + ) + if self.not_worth(bounds=bounds, load=load): + return None, None + self.debug(f"No clo, extending down: {load}") + return load, width + + def won(self, bounds: RelevantBounds, load: DiscreteLoad) -> None: + """Expand width if the load became new upper bound. + + :param bounds: Freshly updated bounds relevant for current target. + :param load: The current load, so strategy does not need to remember. + :type bounds: RelevantBounds + :type load: DiscreteLoad + """ + if load == bounds.chi: + self.expander.expand() diff --git a/resources/libraries/python/MLRsearch/strategy/halve.py b/resources/libraries/python/MLRsearch/strategy/halve.py new file mode 100644 index 0000000000..3188a041c6 --- /dev/null +++ b/resources/libraries/python/MLRsearch/strategy/halve.py @@ -0,0 +1,83 @@ +# 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 HalveStrategy class.""" + + +from dataclasses import dataclass +from typing import Optional, Tuple + +from ..discrete_interval import DiscreteInterval +from ..discrete_load import DiscreteLoad +from ..discrete_width import DiscreteWidth +from ..relevant_bounds import RelevantBounds +from .base import StrategyBase + + +@dataclass +class HalveStrategy(StrategyBase): + """First strategy to apply for a new current target. + + Pick a load between initial lower bound and initial upper bound, + nominate it if it is (still) worth it. + + In a sense, this can be viewed as an extension of preceding target's + bisect strategy. But as the current target may require a different + trial duration, it is better to do it for the new target. + + Alternatively, this is a way to save one application + of subsequent refine strategy, thus avoiding reducing risk of triggering + an external search (slight time saver for highly unstable SUTs). + Either way, minor time save is achieved by preceding target + only needing to reach double of current target width. + + If the distance between initial bounds is already at or below + current target width, the middle point is not nominated. + The reasoning is that in this case external search is likely + to get triggered by the subsequent refine strategies, + so attaining a relevant bound here is not as likely to help. + """ + + def nominate( + self, bounds: RelevantBounds + ) -> Tuple[Optional[DiscreteLoad], Optional[DiscreteWidth]]: + """Nominate the middle between initial lower and upper bound. + + The returned width is the target width, even if initial bounds + happened to be closer together. + + :param bounds: Freshly updated bounds relevant for current target. + :type bounds: RelevantBounds + :returns: Two nones or candidate intended load and duration. + :rtype: Tuple[Optional[DiscreteLoad], Optional[DiscreteWidth]] + """ + if not self.initial_lower_load or not self.initial_upper_load: + return None, None + interval = DiscreteInterval( + lower_bound=self.initial_lower_load, + upper_bound=self.initial_upper_load, + ) + wig = interval.width_in_goals(self.target.discrete_width) + if wig > 2.0: + # Can happen for initial target. + return None, None + if wig <= 1.0: + # Already was narrow enough, refinements shall be sufficient. + return None, None + load = interval.middle(self.target.discrete_width) + if self.not_worth(bounds, load): + return None, None + self.debug(f"Halving available: {load}") + # TODO: Report possibly smaller width? + self.expander.limit(self.target.discrete_width) + return load, self.target.discrete_width diff --git a/resources/libraries/python/MLRsearch/strategy/refine_hi.py b/resources/libraries/python/MLRsearch/strategy/refine_hi.py new file mode 100644 index 0000000000..caa8fc4a7d --- /dev/null +++ b/resources/libraries/python/MLRsearch/strategy/refine_hi.py @@ -0,0 +1,55 @@ +# 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 RefineHiStrategy class.""" + + +from dataclasses import dataclass +from typing import Optional, Tuple + +from ..discrete_load import DiscreteLoad +from ..discrete_width import DiscreteWidth +from ..relevant_bounds import RelevantBounds +from .base import StrategyBase + + +@dataclass +class RefineHiStrategy(StrategyBase): + """If initial upper bound is still worth it, nominate it. + + This usually happens when halving resulted in relevant lower bound, + or if there was no halving (and RefineLoStrategy confirmed initial + lower bound became a relevant lower bound for the new current target). + + This either ensures a matching upper bound (target is achieved) + or moves the relevant lower bound higher (triggering external search). + """ + + def nominate( + self, bounds: RelevantBounds + ) -> Tuple[Optional[DiscreteLoad], Optional[DiscreteWidth]]: + """Nominate the initial upper bound. + + :param bounds: Freshly updated bounds relevant for current target. + :type bounds: RelevantBounds + :returns: Two nones or candidate intended load and duration. + :rtype: Tuple[Optional[DiscreteLoad], Optional[DiscreteWidth]] + """ + if not (load := self.initial_upper_load): + return None, None + if self.not_worth(bounds=bounds, load=load): + return None, None + self.debug(f"Upperbound refinement available: {load}") + # TODO: Limit to possibly smaller than target width? + self.expander.limit(self.target.discrete_width) + return load, self.target.discrete_width diff --git a/resources/libraries/python/MLRsearch/strategy/refine_lo.py b/resources/libraries/python/MLRsearch/strategy/refine_lo.py new file mode 100644 index 0000000000..7927798505 --- /dev/null +++ b/resources/libraries/python/MLRsearch/strategy/refine_lo.py @@ -0,0 +1,53 @@ +# 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 RefineLoStrategy class.""" + + +from dataclasses import dataclass +from typing import Optional, Tuple + +from ..discrete_load import DiscreteLoad +from ..discrete_width import DiscreteWidth +from ..relevant_bounds import RelevantBounds +from .base import StrategyBase + + +@dataclass +class RefineLoStrategy(StrategyBase): + """If initial lower bound is still worth it, nominate it. + + This usually happens when halving resulted in relevant upper bound, + or if there was no halving. + This ensures a relevant bound (upper or lower) for the current target + exists. + """ + + def nominate( + self, bounds: RelevantBounds + ) -> Tuple[Optional[DiscreteLoad], Optional[DiscreteWidth]]: + """Nominate the initial lower bound. + + :param bounds: Freshly updated bounds relevant for current target. + :type bounds: RelevantBounds + :returns: Two nones or candidate intended load and duration. + :rtype: Tuple[Optional[DiscreteLoad], Optional[DiscreteWidth]] + """ + if not (load := self.initial_lower_load): + return None, None + if self.not_worth(bounds=bounds, load=load): + return None, None + self.debug(f"Lowerbound refinement available: {load}") + # TODO: Limit to possibly smaller than target width? + self.expander.limit(self.target.discrete_width) + return load, self.target.discrete_width |