From 499d8594785633391b073a182985851b3e7ad2cd Mon Sep 17 00:00:00 2001 From: Peter Stockings Date: Sun, 6 Sep 2026 14:59:57 +1000 Subject: [PATCH] Anchor forecast top sets to latest rep target --- utils.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/utils.py b/utils.py index 5787206..46bf0e3 100644 --- a/utils.py +++ b/utils.py @@ -173,8 +173,9 @@ def _round_weight(value, increment, direction='nearest'): def infer_rep_targets(repetitions, buffer=5): """Return an expanded rep range based on the currently visible history.""" - numeric_reps = sorted({int(round(float(value))) for value in repetitions if value is not None}) - if not numeric_reps: + numeric_rep_values = [int(round(float(value))) for value in repetitions if value is not None] + numeric_reps = sorted(set(numeric_rep_values)) + if not numeric_rep_values: return [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 5, None, None observed_min = min(numeric_reps) @@ -182,9 +183,10 @@ def infer_rep_targets(repetitions, buffer=5): target_min = max(1, observed_min - buffer) target_max = min(20, observed_max + buffer) target_reps = list(range(target_min, target_max + 1)) - centered_reps = int(round(float(np.median(numeric_reps)))) - centered_reps = max(target_min, min(target_max, centered_reps)) - return target_reps, centered_reps, observed_min, observed_max + # Default to the latest recorded rep target so the first forecast comparison + # is like-for-like. The full expanded range remains available in the table. + default_reps = max(target_min, min(target_max, numeric_rep_values[-1])) + return target_reps, default_reps, observed_min, observed_max def _top_set_estimates(e1rm, lower_e1rm, upper_e1rm, target_reps, increment): @@ -349,7 +351,7 @@ def get_exercise_graph_model(title, estimated_1rm, repetitions, weight, start_da forecast_start_position = None if forecast.get('available'): forecast_start_position = round((observed_max_date - observed_min_date).days / total_span, 3) - for point in forecast['points']: + for point_index, point in enumerate(forecast['points']): position = round((point['date'] - observed_min_date).days / total_span, 3) forecast_points.append((round(((point['value'] - min_e1rm) / e1rm_range) * vb_height, 1), position)) forecast_band_lower.append((round(((point['lower'] - min_e1rm) / e1rm_range) * vb_height, 1), position)) @@ -358,8 +360,17 @@ def get_exercise_graph_model(title, estimated_1rm, repetitions, weight, start_da (top_set for top_set in point['top_sets'] if top_set['repetitions'] == forecast['selected_reps']), None ) - top_set_message = '' - if selected_top_set: + if point_index == 0: + top_set_message = f" · Recorded {_format_kg(weight[-1])}kg × {int(repetitions[-1])}" + forecast_details.append({ + 'position': position, + 'date': point['date'].strftime('%d %b %y'), + 'kind': 'Current reading', + 'e1rm': f"{_format_kg(forecast['last_value'])}kg", + 'top_set': f"{_format_kg(weight[-1])}kg × {int(repetitions[-1])} reps", + 'range': 'Observed value' + }) + elif selected_top_set: top_set_message = ( f" · Est. {selected_top_set['weight_display']}kg × {selected_top_set['repetitions']} " f"(range {selected_top_set['lower_weight_display']}–{selected_top_set['upper_weight_display']}kg)"