aboutsummaryrefslogtreecommitdiffstats
path: root/resources/libraries/python/MLRsearch/strategy
diff options
context:
space:
mode:
Diffstat (limited to 'resources/libraries/python/MLRsearch/strategy')
-rw-r--r--resources/libraries/python/MLRsearch/strategy/__init__.py35
-rw-r--r--resources/libraries/python/MLRsearch/strategy/base.py132
-rw-r--r--resources/libraries/python/MLRsearch/strategy/bisect.py193
-rw-r--r--resources/libraries/python/MLRsearch/strategy/extend_hi.py76
-rw-r--r--resources/libraries/python/MLRsearch/strategy/extend_lo.py76
-rw-r--r--resources/libraries/python/MLRsearch/strategy/halve.py83
-rw-r--r--resources/libraries/python/MLRsearch/strategy/refine_hi.py55
-rw-r--r--resources/libraries/python/MLRsearch/strategy/refine_lo.py53
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