Support existing test types with ASTF
[csit.git] / resources / libraries / python / MLRsearch / MultipleLossRatioSearch.py
1 # Copyright (c) 2020 Cisco and/or its affiliates.
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at:
5 #
6 #     http://www.apache.org/licenses/LICENSE-2.0
7 #
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
13
14 """Module defining MultipleLossRatioSearch class."""
15
16 import logging
17 import math
18 import time
19
20 from .AbstractSearchAlgorithm import AbstractSearchAlgorithm
21 from .NdrPdrResult import NdrPdrResult
22 from .ReceiveRateInterval import ReceiveRateInterval
23
24
25 class MultipleLossRatioSearch(AbstractSearchAlgorithm):
26     """Optimized binary search algorithm for finding NDR and PDR bounds.
27
28     Traditional binary search algorithm needs initial interval
29     (lower and upper bound), and returns final interval after bisecting
30     (until some exit condition is met).
31     The exit condition is usually related to the interval width,
32     (upper bound value minus lower bound value).
33
34     The optimized algorithm contains several improvements
35     aimed to reduce overall search time.
36
37     One improvement is searching for two intervals at once.
38     The intervals are for NDR (No Drop Rate) and PDR (Partial Drop Rate).
39
40     Next improvement is that the initial interval does not need to be valid.
41     Imagine initial interval (10, 11) where 11 is smaller
42     than the searched value.
43     The algorithm will try (11, 13) interval next, and if 13 is still smaller,
44     (13, 17) and so on, doubling width until the upper bound is valid.
45     The part when interval expands is called external search,
46     the part when interval is bisected is called internal search.
47
48     Next improvement is that trial measurements at small trial duration
49     can be used to find a reasonable interval for full trial duration search.
50     This results in more trials performed, but smaller overall duration
51     in general.
52
53     Next improvement is bisecting in logarithmic quantities,
54     so that exit criteria can be independent of measurement units.
55
56     Next improvement is basing the initial interval on receive rates.
57
58     Final improvement is exiting early if the minimal value
59     is not a valid lower bound.
60
61     The complete search consist of several phases,
62     each phase performing several trial measurements.
63     Initial phase creates initial interval based on receive rates
64     at maximum rate and at maximum receive rate (MRR).
65     Final phase and preceding intermediate phases are performing
66     external and internal search steps,
67     each resulting interval is the starting point for the next phase.
68     The resulting interval of final phase is the result of the whole algorithm.
69
70     Each non-initial phase uses its own trial duration and width goal.
71     Any non-initial phase stops searching (for NDR or PDR independently)
72     when minimum is not a valid lower bound (at current duration),
73     or all of the following is true:
74     Both bounds are valid, bound bounds are measured at the current phase
75     trial duration, interval width is less than the width goal
76     for current phase.
77
78     TODO: Review and update this docstring according to rst docs.
79     TODO: Support configurable number of Packet Loss Ratios.
80     """
81
82     class ProgressState:
83         """Structure containing data to be passed around in recursion."""
84
85         def __init__(
86                 self, result, phases, duration, width_goal, packet_loss_ratio,
87                 minimum_transmit_rate, maximum_transmit_rate):
88             """Convert and store the argument values.
89
90             :param result: Current measured NDR and PDR intervals.
91             :param phases: How many intermediate phases to perform
92                 before the current one.
93             :param duration: Trial duration to use in the current phase [s].
94             :param width_goal: The goal relative width for the curreent phase.
95             :param packet_loss_ratio: PDR fraction for the current search.
96             :param minimum_transmit_rate: Minimum target transmit rate
97                 for the current search [pps].
98             :param maximum_transmit_rate: Maximum target transmit rate
99                 for the current search [pps].
100             :type result: NdrPdrResult.NdrPdrResult
101             :type phases: int
102             :type duration: float
103             :type width_goal: float
104             :type packet_loss_ratio: float
105             :type minimum_transmit_rate: float
106             :type maximum_transmit_rate: float
107             """
108             self.result = result
109             self.phases = int(phases)
110             self.duration = float(duration)
111             self.width_goal = float(width_goal)
112             self.packet_loss_ratio = float(packet_loss_ratio)
113             self.minimum_transmit_rate = float(minimum_transmit_rate)
114             self.maximum_transmit_rate = float(maximum_transmit_rate)
115
116     def __init__(
117             self, measurer, final_relative_width=0.005,
118             final_trial_duration=30.0, initial_trial_duration=1.0,
119             number_of_intermediate_phases=2, timeout=600.0, doublings=1):
120         """Store the measurer object and additional arguments.
121
122         :param measurer: Rate provider to use by this search object.
123         :param final_relative_width: Final lower bound transmit rate
124             cannot be more distant that this multiple of upper bound [1].
125         :param final_trial_duration: Trial duration for the final phase [s].
126         :param initial_trial_duration: Trial duration for the initial phase
127             and also for the first intermediate phase [s].
128         :param number_of_intermediate_phases: Number of intermediate phases
129             to perform before the final phase [1].
130         :param timeout: The search will fail itself when not finished
131             before this overall time [s].
132         :param doublings: How many doublings to do in external search step.
133             Default 1 is suitable for fairly stable tests,
134             less stable tests might get better overal duration with 2 or more.
135         :type measurer: AbstractMeasurer.AbstractMeasurer
136         :type final_relative_width: float
137         :type final_trial_duration: float
138         :type initial_trial_duration: float
139         :type number_of_intermediate_phases: int
140         :type timeout: float
141         :type doublings: int
142         """
143         super(MultipleLossRatioSearch, self).__init__(measurer)
144         self.final_trial_duration = float(final_trial_duration)
145         self.final_relative_width = float(final_relative_width)
146         self.number_of_intermediate_phases = int(number_of_intermediate_phases)
147         self.initial_trial_duration = float(initial_trial_duration)
148         self.timeout = float(timeout)
149         self.doublings = int(doublings)
150
151     @staticmethod
152     def double_relative_width(relative_width):
153         """Return relative width corresponding to double logarithmic width.
154
155         :param relative_width: The base relative width to double.
156         :type relative_width: float
157         :returns: The relative width of double logarithmic size.
158         :rtype: float
159         """
160         return 1.99999 * relative_width - relative_width * relative_width
161         # The number should be 2.0, but we want to avoid rounding errors,
162         # and ensure half of double is not larger than the original value.
163
164     @staticmethod
165     def double_step_down(relative_width, current_bound):
166         """Return rate of double logarithmic width below.
167
168         :param relative_width: The base relative width to double.
169         :param current_bound: The current target transmit rate to move [pps].
170         :type relative_width: float
171         :type current_bound: float
172         :returns: Transmit rate smaller by logarithmically double width [pps].
173         :rtype: float
174         """
175         return current_bound * (
176             1.0 - MultipleLossRatioSearch.double_relative_width(relative_width)
177         )
178
179     @staticmethod
180     def expand_down(relative_width, doublings, current_bound):
181         """Return rate of expanded logarithmic width below.
182
183         :param relative_width: The base relative width to double.
184         :param doublings: How many doublings to do for expansion.
185         :param current_bound: The current target transmit rate to move [pps].
186         :type relative_width: float
187         :type doublings: int
188         :type current_bound: float
189         :returns: Transmit rate smaller by logarithmically double width [pps].
190         :rtype: float
191         """
192         for _ in range(doublings):
193             relative_width = MultipleLossRatioSearch.double_relative_width(
194                 relative_width
195             )
196         return current_bound * (1.0 - relative_width)
197
198     @staticmethod
199     def double_step_up(relative_width, current_bound):
200         """Return rate of double logarithmic width above.
201
202         :param relative_width: The base relative width to double.
203         :param current_bound: The current target transmit rate to move [pps].
204         :type relative_width: float
205         :type current_bound: float
206         :returns: Transmit rate larger by logarithmically double width [pps].
207         :rtype: float
208         """
209         return current_bound / (
210             1.0 - MultipleLossRatioSearch.double_relative_width(relative_width)
211         )
212
213     @staticmethod
214     def expand_up(relative_width, doublings, current_bound):
215         """Return rate of expanded logarithmic width above.
216
217         :param relative_width: The base relative width to double.
218         :param doublings: How many doublings to do for expansion.
219         :param current_bound: The current target transmit rate to move [pps].
220         :type relative_width: float
221         :type doublings: int
222         :type current_bound: float
223         :returns: Transmit rate smaller by logarithmically double width [pps].
224         :rtype: float
225         """
226         for _ in range(doublings):
227             relative_width = MultipleLossRatioSearch.double_relative_width(
228                 relative_width
229             )
230         return current_bound / (1.0 - relative_width)
231
232     @staticmethod
233     def half_relative_width(relative_width):
234         """Return relative width corresponding to half logarithmic width.
235
236         :param relative_width: The base relative width to halve.
237         :type relative_width: float
238         :returns: The relative width of half logarithmic size.
239         :rtype: float
240         """
241         return 1.0 - math.sqrt(1.0 - relative_width)
242
243     @staticmethod
244     def half_step_up(relative_width, current_bound):
245         """Return rate of half logarithmic width above.
246
247         :param relative_width: The base relative width to halve.
248         :param current_bound: The current target transmit rate to move [pps].
249         :type relative_width: float
250         :type current_bound: float
251         :returns: Transmit rate larger by logarithmically half width [pps].
252         :rtype: float
253         """
254         return current_bound / (
255             1.0 - MultipleLossRatioSearch.half_relative_width(relative_width)
256         )
257
258     def narrow_down_ndr_and_pdr(self, min_rate, max_rate, packet_loss_ratio):
259         """Perform initial phase, create state object, proceed with next phases.
260
261         :param min_rate: Minimal target transmit rate [tps].
262         :param max_rate: Maximal target transmit rate [tps].
263         :param packet_loss_ratio: Fraction of packets lost, for PDR [1].
264         :type min_rate: float
265         :type max_rate: float
266         :type packet_loss_ratio: float
267         :returns: Structure containing narrowed down intervals
268             and their measurements.
269         :rtype: NdrPdrResult.NdrPdrResult
270         :raises RuntimeError: If total duration is larger than timeout.
271         """
272         minimum_transmit_rate = float(min_rate)
273         maximum_transmit_rate = float(max_rate)
274         packet_loss_ratio = float(packet_loss_ratio)
275         max_measurement = self.measurer.measure(
276             self.initial_trial_duration, maximum_transmit_rate)
277         initial_width_goal = self.final_relative_width
278         for _ in range(self.number_of_intermediate_phases):
279             initial_width_goal = self.double_relative_width(initial_width_goal)
280         max_lo = maximum_transmit_rate * (1.0 - initial_width_goal)
281         mrr = max(minimum_transmit_rate, min(
282             max_lo, max_measurement.relative_receive_rate
283         ))
284         mrr_measurement = self.measurer.measure(
285             self.initial_trial_duration, mrr
286         )
287         # Attempt to get narrower width.
288         if mrr_measurement.loss_fraction > 0.0:
289             max2_lo = mrr * (1.0 - initial_width_goal)
290             mrr2 = min(max2_lo, mrr_measurement.relative_receive_rate)
291         else:
292             mrr2 = mrr / (1.0 - initial_width_goal)
293         if minimum_transmit_rate < mrr2 < maximum_transmit_rate:
294             max_measurement = mrr_measurement
295             mrr_measurement = self.measurer.measure(
296                 self.initial_trial_duration, mrr2)
297             if mrr2 > mrr:
298                 max_measurement, mrr_measurement = \
299                     (mrr_measurement, max_measurement)
300         starting_interval = ReceiveRateInterval(
301             mrr_measurement, max_measurement)
302         starting_result = NdrPdrResult(starting_interval, starting_interval)
303         state = self.ProgressState(
304             starting_result, self.number_of_intermediate_phases,
305             self.final_trial_duration, self.final_relative_width,
306             packet_loss_ratio, minimum_transmit_rate, maximum_transmit_rate
307         )
308         state = self.ndrpdr(state)
309         return state.result
310
311     def _measure_and_update_state(self, state, transmit_rate):
312         """Perform trial measurement, update bounds, return new state.
313
314         :param state: State before this measurement.
315         :param transmit_rate: Target transmit rate for this measurement [pps].
316         :type state: ProgressState
317         :type transmit_rate: float
318         :returns: State after the measurement.
319         :rtype: ProgressState
320         """
321         # TODO: Implement https://stackoverflow.com/a/24683360
322         # to avoid the string manipulation if log verbosity is too low.
323         logging.info(f"result before update: {state.result}")
324         logging.debug(
325             f"relative widths in goals: "
326             f"{state.result.width_in_goals(self.final_relative_width)}"
327         )
328         measurement = self.measurer.measure(state.duration, transmit_rate)
329         ndr_interval = self._new_interval(
330             state.result.ndr_interval, measurement, 0.0
331         )
332         pdr_interval = self._new_interval(
333             state.result.pdr_interval, measurement, state.packet_loss_ratio
334         )
335         state.result = NdrPdrResult(ndr_interval, pdr_interval)
336         return state
337
338     @staticmethod
339     def _new_interval(old_interval, measurement, packet_loss_ratio):
340         """Return new interval with bounds updated according to the measurement.
341
342         :param old_interval: The current interval before the measurement.
343         :param measurement: The new meaqsurement to take into account.
344         :param packet_loss_ratio: Fraction for PDR (or zero for NDR).
345         :type old_interval: ReceiveRateInterval.ReceiveRateInterval
346         :type measurement: ReceiveRateMeasurement.ReceiveRateMeasurement
347         :type packet_loss_ratio: float
348         :returns: The updated interval.
349         :rtype: ReceiveRateInterval.ReceiveRateInterval
350         """
351         old_lo, old_hi = old_interval.measured_low, old_interval.measured_high
352         new_lo = new_hi = None
353         # Priority zero: direct replace if the target Tr is the same.
354         if measurement.target_tr in (old_lo.target_tr, old_hi.target_tr):
355             if measurement.target_tr == old_lo.target_tr:
356                 new_lo = measurement
357             else:
358                 new_hi = measurement
359         # Priority one: invalid lower bound allows only one type of update.
360         elif old_lo.loss_fraction > packet_loss_ratio:
361             # We can only expand down, old bound becomes valid upper one.
362             if measurement.target_tr < old_lo.target_tr:
363                 new_lo, new_hi = measurement, old_lo
364             else:
365                 return old_interval
366
367         # Lower bound is now valid.
368         # Next priorities depend on target Tr.
369         elif measurement.target_tr < old_lo.target_tr:
370             # Lower external measurement, relevant only
371             # if the new measurement has high loss rate.
372             if measurement.loss_fraction > packet_loss_ratio:
373                 # Returning the broader interval as old_lo
374                 # would be invalid upper bound.
375                 new_lo = measurement
376         elif measurement.target_tr > old_hi.target_tr:
377             # Upper external measurement, only relevant for invalid upper bound.
378             if old_hi.loss_fraction <= packet_loss_ratio:
379                 # Old upper bound becomes valid new lower bound.
380                 new_lo, new_hi = old_hi, measurement
381         else:
382             # Internal measurement, replaced boundary
383             # depends on measured loss fraction.
384             if measurement.loss_fraction > packet_loss_ratio:
385                 # We have found a narrow valid interval,
386                 # regardless of whether old upper bound was valid.
387                 new_hi = measurement
388             else:
389                 # In ideal world, we would not want to shrink interval
390                 # if upper bound is not valid.
391                 # In the real world, we want to shrink it for
392                 # "invalid upper bound at maximal rate" case.
393                 new_lo = measurement
394
395         return ReceiveRateInterval(
396             old_lo if new_lo is None else new_lo,
397             old_hi if new_hi is None else new_hi
398         )
399
400     def ndrpdr(self, state):
401         """Perform trials for this phase. Return the new state when done.
402
403         :param state: State before this phase.
404         :type state: ProgressState
405         :returns: The updated state.
406         :rtype: ProgressState
407         :raises RuntimeError: If total duration is larger than timeout.
408         """
409         start_time = time.time()
410         if state.phases > 0:
411             # We need to finish preceding intermediate phases first.
412             saved_phases = state.phases
413             state.phases -= 1
414             # Preceding phases have shorter duration.
415             saved_duration = state.duration
416             duration_multiplier = state.duration / self.initial_trial_duration
417             phase_exponent = float(state.phases) / saved_phases
418             state.duration = self.initial_trial_duration * math.pow(
419                 duration_multiplier, phase_exponent
420             )
421             # Shorter durations do not need that narrow widths.
422             saved_width = state.width_goal
423             state.width_goal = self.double_relative_width(state.width_goal)
424             # Recurse.
425             state = self.ndrpdr(state)
426             # Restore the state for current phase.
427             state.duration = saved_duration
428             state.width_goal = saved_width
429             state.phases = saved_phases  # Not needed, but just in case.
430
431         logging.info(
432             f"starting iterations with duration {state.duration} and relative "
433             f"width goal {state.width_goal}"
434         )
435         while 1:
436             if time.time() > start_time + self.timeout:
437                 raise RuntimeError(u"Optimized search takes too long.")
438             # Order of priorities: invalid bounds (nl, pl, nh, ph),
439             # then narrowing relative Tr widths.
440             # Durations are not priorities yet,
441             # they will settle on their own hopefully.
442             ndr_lo = state.result.ndr_interval.measured_low
443             ndr_hi = state.result.ndr_interval.measured_high
444             pdr_lo = state.result.pdr_interval.measured_low
445             pdr_hi = state.result.pdr_interval.measured_high
446             ndr_rel_width = max(
447                 state.width_goal, state.result.ndr_interval.rel_tr_width
448             )
449             pdr_rel_width = max(
450                 state.width_goal, state.result.pdr_interval.rel_tr_width
451             )
452             # If we are hitting maximal or minimal rate, we cannot shift,
453             # but we can re-measure.
454             new_tr = self._ndrpdr_loss_fraction(
455                 state, ndr_lo, ndr_hi, pdr_lo, pdr_hi, ndr_rel_width,
456                 pdr_rel_width
457             )
458
459             if new_tr is not None:
460                 state = self._measure_and_update_state(state, new_tr)
461                 continue
462
463             # If we are hitting maximum_transmit_rate,
464             # it is still worth narrowing width,
465             # hoping large enough loss fraction will happen.
466             # But if we are hitting the minimal rate (at current duration),
467             # no additional measurement will help with that,
468             # so we can stop narrowing in this phase.
469             if (ndr_lo.target_tr <= state.minimum_transmit_rate
470                     and ndr_lo.loss_fraction > 0.0):
471                 ndr_rel_width = 0.0
472             if (pdr_lo.target_tr <= state.minimum_transmit_rate
473                     and pdr_lo.loss_fraction > state.packet_loss_ratio):
474                 pdr_rel_width = 0.0
475
476             new_tr = self._ndrpdr_width_goal(
477                 state, ndr_lo, pdr_lo, ndr_rel_width, pdr_rel_width
478             )
479
480             if new_tr is not None:
481                 state = self._measure_and_update_state(state, new_tr)
482                 continue
483
484             # We do not need to improve width, but there still might be
485             # some measurements with smaller duration.
486             new_tr = self._ndrpdr_duration(
487                 state, ndr_lo, ndr_hi, pdr_lo, pdr_hi, ndr_rel_width,
488                 pdr_rel_width
489             )
490
491             if new_tr is not None:
492                 state = self._measure_and_update_state(state, new_tr)
493                 continue
494
495             # Widths are narrow (or lower bound minimal), bound measurements
496             # are long enough, we can return.
497             logging.info(u"phase done")
498             break
499         return state
500
501     def _ndrpdr_loss_fraction(
502             self, state, ndr_lo, ndr_hi, pdr_lo, pdr_hi, ndr_rel_width,
503             pdr_rel_width):
504         """Perform loss_fraction-based trials within a ndrpdr phase
505
506         :param state: current state
507         :param ndr_lo: ndr interval measured low
508         :param ndr_hi: ndr interval measured high
509         :param pdr_lo: pdr interval measured low
510         :param pdr_hi: pdr interval measured high
511         :param ndr_rel_width: ndr interval relative width
512         :param pdr_rel_width: pdr interval relative width
513         :type state: ProgressState
514         :type ndr_lo: ReceiveRateMeasurement.ReceiveRateMeasurement
515         :type ndr_hi: ReceiveRateMeasurement.ReceiveRateMeasurement
516         :type pdr_lo: ReceiveRateMeasurement.ReceiveRateMeasurement
517         :type pdr_hi: ReceiveRateMeasurement.ReceiveRateMeasurement
518         :type ndr_rel_width: float
519         :type pdr_rel_width: float
520         :returns: a new transmit rate if one should be applied
521         :rtype: float
522         """
523         result = None
524         if ndr_lo.loss_fraction > 0.0:
525             if ndr_lo.target_tr > state.minimum_transmit_rate:
526                 result = max(
527                     state.minimum_transmit_rate, self.expand_down(
528                         ndr_rel_width, self.doublings, ndr_lo.target_tr
529                     )
530                 )
531                 logging.info(f"ndr lo external {result}")
532             elif ndr_lo.duration < state.duration:
533                 result = state.minimum_transmit_rate
534                 logging.info(u"ndr lo minimal re-measure")
535
536         if result is None and pdr_lo.loss_fraction > state.packet_loss_ratio:
537             if pdr_lo.target_tr > state.minimum_transmit_rate:
538                 result = max(
539                     state.minimum_transmit_rate, self.expand_down(
540                         pdr_rel_width, self.doublings, pdr_lo.target_tr
541                     )
542                 )
543                 logging.info(f"pdr lo external {result}")
544             elif pdr_lo.duration < state.duration:
545                 result = state.minimum_transmit_rate
546                 logging.info(u"pdr lo minimal re-measure")
547
548         if result is None and ndr_hi.loss_fraction <= 0.0:
549             if ndr_hi.target_tr < state.maximum_transmit_rate:
550                 result = min(
551                     state.maximum_transmit_rate, self.expand_up(
552                         ndr_rel_width, self.doublings, ndr_hi.target_tr
553                     )
554                 )
555                 logging.info(f"ndr hi external {result}")
556             elif ndr_hi.duration < state.duration:
557                 result = state.maximum_transmit_rate
558                 logging.info(u"ndr hi maximal re-measure")
559
560         if result is None and pdr_hi.loss_fraction <= state.packet_loss_ratio:
561             if pdr_hi.target_tr < state.maximum_transmit_rate:
562                 result = min(
563                     state.maximum_transmit_rate, self.expand_up(
564                         pdr_rel_width, self.doublings, pdr_hi.target_tr
565                     )
566                 )
567                 logging.info(f"pdr hi external {result}")
568             elif pdr_hi.duration < state.duration:
569                 result = state.maximum_transmit_rate
570                 logging.info(u"ndr hi maximal re-measure")
571         return result
572
573     def _ndrpdr_width_goal(
574             self, state, ndr_lo, pdr_lo, ndr_rel_width, pdr_rel_width):
575         """Perform width_goal-based trials within a ndrpdr phase
576
577         :param state: current state
578         :param ndr_lo: ndr interval measured low
579         :param pdr_lo: pdr interval measured low
580         :param ndr_rel_width: ndr interval relative width
581         :param pdr_rel_width: pdr interval relative width
582         :type state: ProgressState
583         :type ndr_lo: ReceiveRateMeasurement.ReceiveRateMeasurement
584         :type pdr_lo: ReceiveRateMeasurement.ReceiveRateMeasurement
585         :type ndr_rel_width: float
586         :type pdr_rel_width: float
587         :returns: a new transmit rate if one should be applied
588         :rtype: float
589         Return a new transmit rate if one should be applied.
590         """
591         if ndr_rel_width > state.width_goal:
592             # We have to narrow NDR width first, as NDR internal search
593             # can invalidate PDR (but not vice versa).
594             result = self.half_step_up(ndr_rel_width, ndr_lo.target_tr)
595             logging.info(f"Bisecting for NDR at {result}")
596         elif pdr_rel_width > state.width_goal:
597             # PDR internal search.
598             result = self.half_step_up(pdr_rel_width, pdr_lo.target_tr)
599             logging.info(f"Bisecting for PDR at {result}")
600         else:
601             result = None
602         return result
603
604     @staticmethod
605     def _ndrpdr_duration(
606             state, ndr_lo, ndr_hi, pdr_lo, pdr_hi, ndr_rel_width,
607             pdr_rel_width):
608         """Perform duration-based trials within a ndrpdr phase
609
610         :param state: current state
611         :param ndr_lo: ndr interval measured low
612         :param ndr_hi: ndr interval measured high
613         :param pdr_lo: pdr interval measured low
614         :param pdr_hi: pdr interval measured high
615         :param ndr_rel_width: ndr interval relative width
616         :param pdr_rel_width: pdr interval relative width
617         :type state: ProgressState
618         :type ndr_lo: ReceiveRateMeasurement.ReceiveRateMeasurement
619         :type ndr_hi: ReceiveRateMeasurement.ReceiveRateMeasurement
620         :type pdr_lo: ReceiveRateMeasurement.ReceiveRateMeasurement
621         :type pdr_hi: ReceiveRateMeasurement.ReceiveRateMeasurement
622         :type ndr_rel_width: float
623         :type pdr_rel_width: float
624         :returns: a new transmit rate if one should be applied
625         :rtype: float
626         """
627         # We need to re-measure with full duration, possibly
628         # creating invalid bounds to resolve (thus broadening width).
629         if ndr_lo.duration < state.duration:
630             result = ndr_lo.target_tr
631             logging.info(u"re-measuring NDR lower bound")
632         elif pdr_lo.duration < state.duration:
633             result = pdr_lo.target_tr
634             logging.info(u"re-measuring PDR lower bound")
635         # Except when lower bounds have high loss fraction, in that case
636         # we do not need to re-measure _upper_ bounds.
637         elif ndr_hi.duration < state.duration and ndr_rel_width > 0.0:
638             result = ndr_hi.target_tr
639             logging.info(u"re-measuring NDR upper bound")
640         elif pdr_hi.duration < state.duration and pdr_rel_width > 0.0:
641             result = pdr_hi.target_tr
642             logging.info(u"re-measuring PDR upper bound")
643         else:
644             result = None
645         return result