Skip to content

Commit c859d13

Browse files
committed
added auto-refit
1 parent 66f8026 commit c859d13

4 files changed

Lines changed: 255 additions & 17 deletions

File tree

src/app.rs

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -669,6 +669,32 @@ impl ActiveOptimizerViewMut<'_> {
669669
}
670670
}
671671

672+
#[derive(Debug, Clone, PartialEq)]
673+
enum ActiveOptimizerSnapshot {
674+
Lbfgs(LbfgsInputState),
675+
NelderMead(NelderMeadInputState),
676+
SteepestDescent(SteepestDescentInputState),
677+
NewtonCg(NewtonCgInputState),
678+
Sgd(SgdInputState),
679+
Adam(AdamInputState),
680+
}
681+
682+
#[derive(Debug, Clone, PartialEq)]
683+
struct RightPanelFitSnapshot {
684+
selected_model: ModelChoice,
685+
polynomial_degree: usize,
686+
parameter_inputs: Vec<String>,
687+
spline_knots: usize,
688+
spline_knot_strategy: SplineKnotStrategy,
689+
spline_extrapolation: SplineExtrapolation,
690+
spline_duplicate_x_policy: SplineDuplicateXPolicy,
691+
spline_initial_knot_y_inputs: Vec<String>,
692+
optimization_loss_metric: OptimizationLossMetric,
693+
metric_quantization_enabled: bool,
694+
metric_quantization_decimal_places: u8,
695+
optimizer: ActiveOptimizerSnapshot,
696+
}
697+
672698
#[cfg(not(target_arch = "wasm32"))]
673699
#[derive(Debug)]
674700
enum FitWorkerMessage {
@@ -760,6 +786,9 @@ pub struct CurveFitApp {
760786
spline_extrapolation: SplineExtrapolation,
761787
spline_duplicate_x_policy: SplineDuplicateXPolicy,
762788
spline_initial_knot_y_inputs: Vec<String>,
789+
auto_refit_enabled: bool,
790+
auto_refit_pending_rerun: bool,
791+
last_right_panel_fit_snapshot: Option<RightPanelFitSnapshot>,
763792
fit_in_progress: bool,
764793
fit_loss_metric: OptimizationLossMetric,
765794
fit_metric_quantization: MetricQuantization,
@@ -876,6 +905,79 @@ impl CurveFitApp {
876905
self.active_optimizer_view().config()
877906
}
878907

908+
fn capture_active_optimizer_snapshot(&self) -> ActiveOptimizerSnapshot {
909+
match self.optimizer_method {
910+
OptimizerMethod::Lbfgs => ActiveOptimizerSnapshot::Lbfgs(self.lbfgs_inputs.clone()),
911+
OptimizerMethod::NelderMead => {
912+
ActiveOptimizerSnapshot::NelderMead(self.nelder_mead_inputs.clone())
913+
}
914+
OptimizerMethod::SteepestDescent => {
915+
ActiveOptimizerSnapshot::SteepestDescent(self.steepest_descent_inputs.clone())
916+
}
917+
OptimizerMethod::NewtonCg => {
918+
ActiveOptimizerSnapshot::NewtonCg(self.newton_cg_inputs.clone())
919+
}
920+
OptimizerMethod::Sgd => ActiveOptimizerSnapshot::Sgd(self.sgd_inputs.clone()),
921+
OptimizerMethod::Adam => ActiveOptimizerSnapshot::Adam(self.adam_inputs.clone()),
922+
}
923+
}
924+
925+
fn capture_right_panel_fit_snapshot(&self) -> RightPanelFitSnapshot {
926+
RightPanelFitSnapshot {
927+
selected_model: self.selected_model,
928+
polynomial_degree: self.polynomial_degree,
929+
parameter_inputs: self.parameter_inputs.clone(),
930+
spline_knots: self.spline_knots,
931+
spline_knot_strategy: self.spline_knot_strategy,
932+
spline_extrapolation: self.spline_extrapolation,
933+
spline_duplicate_x_policy: self.spline_duplicate_x_policy,
934+
spline_initial_knot_y_inputs: self.spline_initial_knot_y_inputs.clone(),
935+
optimization_loss_metric: self.optimization_loss_metric,
936+
metric_quantization_enabled: self.metric_quantization_enabled,
937+
metric_quantization_decimal_places: self.metric_quantization_decimal_places,
938+
optimizer: self.capture_active_optimizer_snapshot(),
939+
}
940+
}
941+
942+
fn track_right_panel_fit_changes_and_maybe_refit(&mut self) {
943+
let snapshot = self.capture_right_panel_fit_snapshot();
944+
let Some(last_snapshot) = self.last_right_panel_fit_snapshot.as_mut() else {
945+
self.last_right_panel_fit_snapshot = Some(snapshot);
946+
return;
947+
};
948+
949+
if *last_snapshot == snapshot {
950+
return;
951+
}
952+
953+
*last_snapshot = snapshot;
954+
955+
if !self.auto_refit_enabled {
956+
return;
957+
}
958+
959+
if self.fit_in_progress {
960+
self.auto_refit_pending_rerun = true;
961+
return;
962+
}
963+
964+
self.run_fit();
965+
}
966+
967+
fn maybe_run_pending_auto_refit(&mut self) {
968+
if !self.auto_refit_enabled {
969+
self.auto_refit_pending_rerun = false;
970+
return;
971+
}
972+
973+
if !self.auto_refit_pending_rerun || self.fit_in_progress {
974+
return;
975+
}
976+
977+
self.auto_refit_pending_rerun = false;
978+
self.run_fit();
979+
}
980+
879981
fn auto_spline_samples(points_len: usize, knots: usize) -> usize {
880982
// На больших датасетах используем более плотную дискретизацию,
881983
// но ограничиваем верхний порог ради отзывчивости UI.
@@ -1384,6 +1486,9 @@ impl Default for CurveFitApp {
13841486
spline_extrapolation: SplineExtrapolation::default(),
13851487
spline_duplicate_x_policy: SplineDuplicateXPolicy::default(),
13861488
spline_initial_knot_y_inputs: Vec::new(),
1489+
auto_refit_enabled: false,
1490+
auto_refit_pending_rerun: false,
1491+
last_right_panel_fit_snapshot: None,
13871492
replay: ReplayState::default(),
13881493
fit_in_progress: false,
13891494
fit_loss_metric: OptimizationLossMetric::default(),
@@ -1419,6 +1524,7 @@ impl eframe::App for CurveFitApp {
14191524
fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
14201525
Self::apply_visual_style(ctx);
14211526
self.poll_fit_worker(ctx);
1527+
self.maybe_run_pending_auto_refit();
14221528
self.tick_replay(ctx);
14231529
self.poll_points_clipboard_import(ctx);
14241530
self.maybe_refresh_points_cache_after_debounce();
@@ -1529,6 +1635,7 @@ impl eframe::App for CurveFitApp {
15291635
},
15301636
);
15311637
});
1638+
self.track_right_panel_fit_changes_and_maybe_refit();
15321639
}
15331640

15341641
self.ui_formula_window(&ctx);

src/app/fit_worker.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -634,6 +634,9 @@ impl CurveFitApp {
634634
return;
635635
}
636636

637+
self.last_right_panel_fit_snapshot = Some(self.capture_right_panel_fit_snapshot());
638+
self.auto_refit_pending_rerun = false;
639+
637640
let points = match self.parse_points_strict() {
638641
Ok(points) => points,
639642
Err(error) => {

src/app/tests.rs

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,9 @@ fn optimization_metric_defaults_to_mse() {
148148
assert_eq!(app.optimization_loss_metric, OptimizationLossMetric::Mse);
149149
assert_eq!(app.fit_loss_metric, OptimizationLossMetric::Mse);
150150
assert!(!app.metric_quantization_enabled);
151+
assert!(!app.auto_refit_enabled);
152+
assert!(!app.auto_refit_pending_rerun);
153+
assert!(app.last_right_panel_fit_snapshot.is_none());
151154
assert_eq!(
152155
app.metric_quantization_decimal_places,
153156
super::DEFAULT_METRIC_QUANTIZATION_DECIMAL_PLACES
@@ -1041,6 +1044,102 @@ fn wait_fit_completion(app: &mut CurveFitApp) {
10411044
panic!("fit did not complete in time");
10421045
}
10431046

1047+
#[cfg(not(target_arch = "wasm32"))]
1048+
#[test]
1049+
fn auto_refit_first_snapshot_only_initializes_baseline() {
1050+
let mut app = make_linear_fit_app();
1051+
app.auto_refit_enabled = true;
1052+
1053+
assert!(app.last_right_panel_fit_snapshot.is_none());
1054+
app.track_right_panel_fit_changes_and_maybe_refit();
1055+
assert!(app.last_right_panel_fit_snapshot.is_some());
1056+
assert!(!app.fit_in_progress);
1057+
assert!(!app.auto_refit_pending_rerun);
1058+
}
1059+
1060+
#[cfg(not(target_arch = "wasm32"))]
1061+
#[test]
1062+
fn auto_refit_runs_fit_when_right_panel_settings_change_in_idle() {
1063+
let mut app = make_linear_fit_app();
1064+
app.auto_refit_enabled = true;
1065+
app.track_right_panel_fit_changes_and_maybe_refit();
1066+
1067+
app.optimization_loss_metric = OptimizationLossMetric::Mae;
1068+
app.track_right_panel_fit_changes_and_maybe_refit();
1069+
1070+
assert!(app.fit_in_progress);
1071+
wait_fit_completion(&mut app);
1072+
}
1073+
1074+
#[cfg(not(target_arch = "wasm32"))]
1075+
#[test]
1076+
fn auto_refit_queues_rerun_when_settings_change_during_fit() {
1077+
let mut app = make_linear_fit_app();
1078+
app.auto_refit_enabled = true;
1079+
app.track_right_panel_fit_changes_and_maybe_refit();
1080+
app.run_fit();
1081+
assert!(app.fit_in_progress);
1082+
1083+
app.metric_quantization_enabled = true;
1084+
app.metric_quantization_decimal_places = 2;
1085+
app.track_right_panel_fit_changes_and_maybe_refit();
1086+
1087+
assert!(app.fit_in_progress);
1088+
assert!(app.auto_refit_pending_rerun);
1089+
assert!(!matches!(app.status, Some(StatusMessage::FittingStopping)));
1090+
wait_fit_completion(&mut app);
1091+
}
1092+
1093+
#[cfg(not(target_arch = "wasm32"))]
1094+
#[test]
1095+
fn auto_refit_pending_rerun_starts_after_fit_transitions_to_idle() {
1096+
let mut app = make_linear_fit_app();
1097+
app.auto_refit_enabled = true;
1098+
app.track_right_panel_fit_changes_and_maybe_refit();
1099+
app.run_fit();
1100+
assert!(app.fit_in_progress);
1101+
1102+
app.parameter_inputs[0] = "0.1".to_string();
1103+
app.track_right_panel_fit_changes_and_maybe_refit();
1104+
assert!(app.auto_refit_pending_rerun);
1105+
1106+
wait_fit_completion(&mut app);
1107+
assert!(!app.fit_in_progress);
1108+
assert!(app.auto_refit_pending_rerun);
1109+
1110+
app.maybe_run_pending_auto_refit();
1111+
assert!(app.fit_in_progress);
1112+
assert!(!app.auto_refit_pending_rerun);
1113+
wait_fit_completion(&mut app);
1114+
}
1115+
1116+
#[cfg(not(target_arch = "wasm32"))]
1117+
#[test]
1118+
fn disabling_auto_refit_clears_pending_rerun_without_starting_fit() {
1119+
let mut app = make_linear_fit_app();
1120+
app.auto_refit_enabled = true;
1121+
app.auto_refit_pending_rerun = true;
1122+
1123+
app.auto_refit_enabled = false;
1124+
app.maybe_run_pending_auto_refit();
1125+
1126+
assert!(!app.auto_refit_pending_rerun);
1127+
assert!(!app.fit_in_progress);
1128+
}
1129+
1130+
#[cfg(not(target_arch = "wasm32"))]
1131+
#[test]
1132+
fn run_fit_clears_pending_auto_refit_rerun_flag() {
1133+
let mut app = make_linear_fit_app();
1134+
app.auto_refit_pending_rerun = true;
1135+
1136+
app.run_fit();
1137+
1138+
assert!(app.fit_in_progress);
1139+
assert!(!app.auto_refit_pending_rerun);
1140+
wait_fit_completion(&mut app);
1141+
}
1142+
10441143
#[cfg(not(target_arch = "wasm32"))]
10451144
#[test]
10461145
fn run_fit_snapshots_metric_quantization_at_start() {

src/app/ui/optimizer_panel.rs

Lines changed: 46 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,7 @@ pub(super) fn ui_optimizer(app: &mut CurveFitApp, ui: &mut egui::Ui) {
376376
}
377377

378378
ui.separator();
379-
ui_fit_action_button(app, ui, false);
379+
ui_fit_action_button(app, ui, false, true);
380380
if app.fit_in_progress
381381
&& let Some(iteration) = app.fit_preview_iteration
382382
{
@@ -388,10 +388,15 @@ pub(super) fn ui_optimizer(app: &mut CurveFitApp, ui: &mut egui::Ui) {
388388
}
389389

390390
pub(super) fn ui_optimizer_action_button_compact(app: &mut CurveFitApp, ui: &mut egui::Ui) {
391-
ui_fit_action_button(app, ui, true);
391+
ui_fit_action_button(app, ui, true, false);
392392
}
393393

394-
fn ui_fit_action_button(app: &mut CurveFitApp, ui: &mut egui::Ui, compact: bool) {
394+
fn ui_fit_action_button(
395+
app: &mut CurveFitApp,
396+
ui: &mut egui::Ui,
397+
compact: bool,
398+
show_auto_refit_toggle: bool,
399+
) {
395400
let (fill, stroke, text_color) = CurveFitApp::action_button_style(ui, app.fit_in_progress);
396401
let (icon, text) = if app.fit_in_progress {
397402
(
@@ -405,24 +410,40 @@ fn ui_fit_action_button(app: &mut CurveFitApp, ui: &mut egui::Ui, compact: bool)
405410
)
406411
};
407412

408-
let min_size = if compact {
409-
egui::vec2(COMPACT_FIT_BUTTON_WIDTH, COMPACT_FIT_BUTTON_HEIGHT)
410-
} else {
411-
egui::vec2(ui.available_width(), FULL_FIT_BUTTON_HEIGHT)
412-
};
413-
let action_button =
414-
egui::Button::image_and_text(icon, egui::RichText::new(text).strong().color(text_color))
413+
ui.horizontal(|ui| {
414+
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
415+
if show_auto_refit_toggle {
416+
let auto_refit_response = CurveFitApp::toggle_switch_labeled(
417+
ui,
418+
&mut app.auto_refit_enabled,
419+
tr(app.ui_language, "Auto-refit", "Авто-рефит"),
420+
);
421+
let _ =
422+
CurveFitApp::info_hover(auto_refit_response, auto_refit_hint(app.ui_language));
423+
}
424+
425+
let min_size = if compact {
426+
egui::vec2(COMPACT_FIT_BUTTON_WIDTH, COMPACT_FIT_BUTTON_HEIGHT)
427+
} else {
428+
egui::vec2(ui.available_width(), FULL_FIT_BUTTON_HEIGHT)
429+
};
430+
let action_button = egui::Button::image_and_text(
431+
icon,
432+
egui::RichText::new(text).strong().color(text_color),
433+
)
415434
.min_size(min_size)
416435
.fill(fill)
417436
.stroke(stroke)
418437
.corner_radius(egui::CornerRadius::same(UI_CORNER_RADIUS + 1));
419-
if ui.add(action_button).clicked() {
420-
if app.fit_in_progress {
421-
app.request_stop_fit();
422-
} else {
423-
app.run_fit();
424-
}
425-
}
438+
if ui.add(action_button).clicked() {
439+
if app.fit_in_progress {
440+
app.request_stop_fit();
441+
} else {
442+
app.run_fit();
443+
}
444+
}
445+
});
446+
});
426447
}
427448

428449
fn optimizer_mode_hint(language: UiLanguage, mode: OptimizerUiMode) -> &'static str {
@@ -439,3 +460,11 @@ fn optimizer_mode_hint(language: UiLanguage, mode: OptimizerUiMode) -> &'static
439460
),
440461
}
441462
}
463+
464+
fn auto_refit_hint(language: UiLanguage) -> &'static str {
465+
tr(
466+
language,
467+
"Auto-refit\n- Re-runs fit when right-panel fit settings change\n- If fit is already running, one rerun is queued after completion\n- Applies to model/initial parameters, optimization metric, and optimizer settings",
468+
"Авто-рефит\n- Повторно запускает фит при изменении fit-настроек в правой панели\n- Если фит уже выполняется, один перезапуск ставится в очередь после завершения\n- Применяется к модели/начальным параметрам, метрике оптимизации и настройкам оптимизатора",
469+
)
470+
}

0 commit comments

Comments
 (0)