Skip to content

Commit 693a4ae

Browse files
committed
added metrics selection
1 parent d1a1f8b commit 693a4ae

14 files changed

Lines changed: 4640 additions & 417 deletions

File tree

Cargo.lock

Lines changed: 3617 additions & 165 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ egui_extras = { version = "0.33.3", default-features = false, features = [
2121
"svg_text",
2222
] }
2323
egui_plot = "0.34.1"
24+
trunk = "0.21.14"
2425

2526
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
2627
sys-locale = "0.3.2"

src/app.rs

Lines changed: 100 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,10 @@ use self::formula::{formula_svg_bytes, formula_svg_uri};
2626
use self::i18n::{
2727
center_origin_icon_image, clear_icon_image, family_label, fit_icon_image,
2828
fit_to_content_icon_image, github_mark_image, language_flag_image, model_choice_label,
29-
param_init_method_disabled_label, param_init_method_label, param_init_method_name_en,
30-
redo_icon_image, reset_icon_image, spline_extrapolation_label, spline_knot_strategy_label,
31-
spray_brush_label, stop_icon_image, tool_icon_image, tool_label, tr, undo_icon_image,
29+
optimization_loss_metric_label, param_init_method_disabled_label, param_init_method_label,
30+
param_init_method_name_en, redo_icon_image, reset_icon_image, spline_extrapolation_label,
31+
spline_knot_strategy_label, spray_brush_label, stop_icon_image, tool_icon_image, tool_label,
32+
tr, undo_icon_image,
3233
};
3334
use self::optimizer::{
3435
LbfgsInputState, NelderMeadInputState, OptimizerPreset, OptimizerUiMode,
@@ -47,15 +48,16 @@ use crate::domain::{
4748
};
4849
use crate::fit::{
4950
FitError, SplineConfig, SplineDuplicateXPolicy, SplineExtrapolation, SplineFamilyKind,
50-
SplineKnotStrategy, SplineResult, calculate_metrics, default_spline_initial_knot_y,
51-
fit_curve_with_progress_and_optimizer_config, sample_curve,
51+
SplineKnotStrategy, SplineResult, calculate_iteration_metrics, default_spline_initial_knot_y,
52+
fit_curve_with_progress_and_optimizer_config_and_loss_metric, sample_curve,
5253
};
5354
#[cfg(target_arch = "wasm32")]
5455
use crate::fit::{
5556
IncrementalFitRunner, IncrementalFitStep, IncrementalSplineFitRunner, IncrementalSplineFitStep,
5657
};
5758
#[cfg(not(target_arch = "wasm32"))]
5859
use crate::fit::{IncrementalSplineFitRunner, IncrementalSplineFitStep};
60+
use crate::fit::{IterationMetricSnapshot, OptimizationLossMetric};
5961

6062
#[cfg(not(target_arch = "wasm32"))]
6163
use std::sync::atomic::{AtomicBool, Ordering};
@@ -76,6 +78,10 @@ const SPLINE_AUTO_SAMPLES_PER_KNOT: usize = 30;
7678
const SPLINE_AUTO_SAMPLES_PER_POINT: usize = 3;
7779
const DIAGNOSTICS_PANEL_DEFAULT_HEIGHT: f32 = 230.0;
7880
const DIAGNOSTICS_PANEL_MIN_HEIGHT: f32 = 120.0;
81+
const LEFT_PANEL_DEFAULT_WIDTH: f32 = 350.0;
82+
const LEFT_PANEL_MIN_WIDTH: f32 = 350.0;
83+
const RIGHT_PANEL_DEFAULT_WIDTH: f32 = 300.0;
84+
const RIGHT_PANEL_MIN_WIDTH: f32 = 300.0;
7985
const POINTS_PARSE_DEBOUNCE_MS: u64 = 180;
8086
const POINTS_HISTORY_LIMIT: usize = 256;
8187
const POINTS_PARSE_ERROR_PREFIX: &str = "Points parse error: ";
@@ -435,12 +441,12 @@ impl StatusMessage {
435441
enum FitWorkerMessage {
436442
Iteration {
437443
iteration: u64,
438-
mse: f64,
444+
metrics: IterationMetricSnapshot,
439445
params: CurveParams,
440446
},
441447
SplineIteration {
442448
iteration: u64,
443-
mse: f64,
449+
metrics: IterationMetricSnapshot,
444450
knot_y: Vec<f64>,
445451
curve: Vec<[f64; 2]>,
446452
},
@@ -469,6 +475,7 @@ pub struct CurveFitApp {
469475
parameter_inputs: Vec<String>,
470476
optimizer_method: OptimizerMethod,
471477
optimizer_mode: OptimizerUiMode,
478+
optimization_loss_metric: OptimizationLossMetric,
472479
lbfgs_inputs: LbfgsInputState,
473480
lbfgs_preset: OptimizerPreset,
474481
nelder_mead_inputs: NelderMeadInputState,
@@ -490,6 +497,7 @@ pub struct CurveFitApp {
490497
show_left_panel: bool,
491498
show_right_panel: bool,
492499
show_diagnostics_panel: bool,
500+
diagnostics_hide_non_loss_by_default_pending: bool,
493501
diagnostics_shared_axis_width: f32,
494502
iteration_delay_seconds: f64,
495503
spline_knots: usize,
@@ -498,6 +506,7 @@ pub struct CurveFitApp {
498506
spline_duplicate_x_policy: SplineDuplicateXPolicy,
499507
spline_initial_knot_y_inputs: Vec<String>,
500508
fit_in_progress: bool,
509+
fit_loss_metric: OptimizationLossMetric,
501510
fit_preview_params: Option<CurveParams>,
502511
fit_preview_iteration: Option<u64>,
503512
fit_result: Option<FitResult>,
@@ -833,6 +842,7 @@ impl CurveFitApp {
833842
self.spline_plot_curve = None;
834843
self.sampled_curve_cache = None;
835844
self.iteration_diagnostics.clear();
845+
self.diagnostics_hide_non_loss_by_default_pending = true;
836846
self.clear_fit_preview();
837847
}
838848

@@ -938,6 +948,51 @@ impl CurveFitApp {
938948
CurveParams::try_from_values(family, values).map_err(|error| error.to_string())
939949
}
940950

951+
fn has_fitted_params_for_family(&self, family: CurveFamily) -> bool {
952+
self.fit_result
953+
.as_ref()
954+
.is_some_and(|result| result.family == family)
955+
}
956+
957+
fn build_fitted_initial_params(&self, family: CurveFamily) -> Result<CurveParams, String> {
958+
let Some(result) = &self.fit_result else {
959+
return Err("No fitted model parameters are available for initialization".to_string());
960+
};
961+
962+
if result.family != family {
963+
return Err(format!(
964+
"Fitted model family mismatch: expected {family}, got {}",
965+
result.family
966+
));
967+
}
968+
969+
Ok(result.params.clone())
970+
}
971+
972+
fn apply_fitted_param_init(&mut self) {
973+
let Some(family) = self.resolved_model().parametric_family() else {
974+
self.status = Some(StatusMessage::Error(
975+
"Current model is non-parametric and has no initial parameters".to_string(),
976+
));
977+
return;
978+
};
979+
980+
match self.build_fitted_initial_params(family) {
981+
Ok(params) => {
982+
self.parameter_inputs = params
983+
.values()
984+
.into_iter()
985+
.map(|value| value.to_string())
986+
.collect();
987+
self.clear_fit_outputs();
988+
self.status = Some(StatusMessage::Ready);
989+
}
990+
Err(error) => {
991+
self.status = Some(StatusMessage::Error(error));
992+
}
993+
}
994+
}
995+
941996
fn apply_param_init_method(&mut self, method: ParamInitMethod) {
942997
let Some(family) = self.resolved_model().parametric_family() else {
943998
self.status = Some(StatusMessage::Error(
@@ -1229,6 +1284,7 @@ impl Default for CurveFitApp {
12291284
.collect(),
12301285
optimizer_method: OptimizerMethod::Lbfgs,
12311286
optimizer_mode: OptimizerUiMode::Basic,
1287+
optimization_loss_metric: OptimizationLossMetric::default(),
12321288
lbfgs_inputs: LbfgsInputState::from_config(&default_lbfgs),
12331289
lbfgs_preset: infer_lbfgs_preset(&default_lbfgs),
12341290
nelder_mead_inputs: NelderMeadInputState::from_config(&default_nelder_mead),
@@ -1257,9 +1313,11 @@ impl Default for CurveFitApp {
12571313
spline_initial_knot_y_inputs: Vec::new(),
12581314
show_right_panel: true,
12591315
show_diagnostics_panel: true,
1316+
diagnostics_hide_non_loss_by_default_pending: true,
12601317
diagnostics_shared_axis_width: 0.0,
12611318
iteration_delay_seconds: 0.25,
12621319
fit_in_progress: false,
1320+
fit_loss_metric: OptimizationLossMetric::default(),
12631321
fit_preview_params: None,
12641322
fit_preview_iteration: None,
12651323
fit_result: None,
@@ -1324,26 +1382,54 @@ impl eframe::App for CurveFitApp {
13241382

13251383
if self.show_left_panel {
13261384
egui::SidePanel::left("points_panel")
1327-
.default_width(340.0)
1385+
.default_width(LEFT_PANEL_DEFAULT_WIDTH)
1386+
.min_width(LEFT_PANEL_MIN_WIDTH)
13281387
.resizable(true)
13291388
.frame(Self::side_panel_frame(panel_style))
13301389
.show(ctx, |ui| {
13311390
ui.spacing_mut().item_spacing = egui::vec2(10.0, 8.0);
1332-
Self::panel_card_frame(ui).show(ui, |ui| self.ui_tools(ui));
1333-
Self::panel_card_frame(ui).show(ui, |ui| self.ui_points_editor(ui));
1391+
ui.set_width(ui.available_width());
1392+
Self::panel_card_frame(ui).show(ui, |ui| {
1393+
ui.set_min_width(ui.available_width());
1394+
self.ui_tools(ui);
1395+
});
1396+
// ui.set_width(ui.available_width());
1397+
Self::panel_card_frame(ui).show(ui, |ui| {
1398+
// ui.set_min_width(ui.available_width());
1399+
self.ui_points_editor(ui);
1400+
});
13341401
});
13351402
}
13361403

13371404
if self.show_right_panel {
13381405
egui::SidePanel::right("settings_panel")
1339-
.default_width(320.0)
1406+
.default_width(RIGHT_PANEL_DEFAULT_WIDTH)
1407+
.min_width(RIGHT_PANEL_MIN_WIDTH)
13401408
.resizable(true)
13411409
.frame(Self::side_panel_frame(panel_style))
13421410
.show(ctx, |ui| {
13431411
ui.spacing_mut().item_spacing = egui::vec2(10.0, 8.0);
1344-
Self::panel_card_frame(ui).show(ui, |ui| self.ui_family_and_params(ui));
1345-
Self::panel_card_frame(ui).show(ui, |ui| self.ui_optimizer(ui));
1346-
Self::panel_card_frame(ui).show(ui, |ui| self.ui_result(ui));
1412+
ui.set_width(ui.available_width());
1413+
Self::panel_card_frame(ui).show(ui, |ui| {
1414+
ui.set_min_width(ui.available_width());
1415+
// ui.set_width(ui.available_width());
1416+
self.ui_family_and_params(ui);
1417+
});
1418+
Self::panel_card_frame(ui).show(ui, |ui| {
1419+
ui.set_min_width(ui.available_width());
1420+
// ui.set_width(ui.available_width());
1421+
self.ui_optimization_metric(ui);
1422+
});
1423+
Self::panel_card_frame(ui).show(ui, |ui| {
1424+
ui.set_min_width(ui.available_width());
1425+
// ui.set_width(ui.available_width());
1426+
self.ui_optimizer(ui);
1427+
});
1428+
Self::panel_card_frame(ui).show(ui, |ui| {
1429+
ui.set_min_width(ui.available_width());
1430+
// ui.set_width(ui.available_width());
1431+
self.ui_result(ui);
1432+
});
13471433
});
13481434
}
13491435

src/app/diagnostics.rs

Lines changed: 59 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
use super::*;
22

33
#[derive(Debug, Clone, Default)]
4+
/// Накопленные диагностические ряды по итерациям оптимизации.
45
pub(super) struct IterationDiagnostics {
56
pub(super) family: Option<CurveFamily>,
67
pub(super) spline_parameter_count: Option<usize>,
7-
pub(super) loss_mse_points: Vec<[f64; 2]>,
8+
pub(super) loss_points: Vec<[f64; 2]>,
9+
pub(super) mse_points: Vec<[f64; 2]>,
10+
pub(super) rmse_points: Vec<[f64; 2]>,
11+
pub(super) mae_points: Vec<[f64; 2]>,
12+
pub(super) soft_l1_points: Vec<[f64; 2]>,
13+
pub(super) r2_abs_points: Vec<[f64; 2]>,
14+
pub(super) max_abs_error_points: Vec<[f64; 2]>,
815
pub(super) parameter_names: Vec<String>,
916
pub(super) parameter_series: Vec<Vec<[f64; 2]>>,
1017
}
@@ -13,19 +20,29 @@ impl IterationDiagnostics {
1320
pub(super) fn clear(&mut self) {
1421
self.family = None;
1522
self.spline_parameter_count = None;
16-
self.loss_mse_points.clear();
23+
self.clear_metric_points();
1724
self.parameter_names.clear();
1825
self.parameter_series.clear();
1926
}
2027

21-
pub(super) fn initialize(&mut self, points: &Points, params: &CurveParams) {
28+
pub(super) fn initialize(
29+
&mut self,
30+
points: &Points,
31+
params: &CurveParams,
32+
loss_metric: OptimizationLossMetric,
33+
) {
2234
let family = params.family();
2335
self.reset_for_family(family);
24-
let (mse, _) = calculate_metrics(points, params);
25-
self.append(0, mse, params);
36+
let metrics = calculate_iteration_metrics(points, params, loss_metric);
37+
self.append(0, metrics, params);
2638
}
2739

28-
pub(super) fn append(&mut self, iteration: u64, mse: f64, params: &CurveParams) {
40+
pub(super) fn append(
41+
&mut self,
42+
iteration: u64,
43+
metrics: IterationMetricSnapshot,
44+
params: &CurveParams,
45+
) {
2946
let family = params.family();
3047
if self.family != Some(family) || self.spline_parameter_count.is_some() {
3148
self.reset_for_family(family);
@@ -37,29 +54,48 @@ impl IterationDiagnostics {
3754
}
3855

3956
let iteration = iteration as f64;
40-
upsert_iteration_point(&mut self.loss_mse_points, iteration, mse);
57+
self.upsert_metrics(iteration, metrics);
4158
for (series, value) in self.parameter_series.iter_mut().zip(values) {
4259
upsert_iteration_point(series, iteration, value);
4360
}
4461
}
4562

46-
pub(super) fn append_spline(&mut self, iteration: u64, mse: f64, knot_y: &[f64]) {
63+
pub(super) fn append_spline(
64+
&mut self,
65+
iteration: u64,
66+
metrics: IterationMetricSnapshot,
67+
knot_y: &[f64],
68+
) {
4769
let parameter_count = knot_y.len();
4870
if self.family.is_some() || self.spline_parameter_count != Some(parameter_count) {
4971
self.reset_for_spline(parameter_count);
5072
}
5173

5274
let iteration = iteration as f64;
53-
upsert_iteration_point(&mut self.loss_mse_points, iteration, mse);
75+
self.upsert_metrics(iteration, metrics);
5476
for (series, value) in self.parameter_series.iter_mut().zip(knot_y.iter().copied()) {
5577
upsert_iteration_point(series, iteration, value);
5678
}
5779
}
5880

81+
fn upsert_metrics(&mut self, iteration: f64, metrics: IterationMetricSnapshot) {
82+
upsert_iteration_point(&mut self.loss_points, iteration, metrics.loss);
83+
upsert_iteration_point(&mut self.mse_points, iteration, metrics.mse);
84+
upsert_iteration_point(&mut self.rmse_points, iteration, metrics.rmse);
85+
upsert_iteration_point(&mut self.mae_points, iteration, metrics.mae);
86+
upsert_iteration_point(&mut self.soft_l1_points, iteration, metrics.soft_l1);
87+
upsert_iteration_point(&mut self.r2_abs_points, iteration, metrics.r2.abs());
88+
upsert_iteration_point(
89+
&mut self.max_abs_error_points,
90+
iteration,
91+
metrics.max_abs_error,
92+
);
93+
}
94+
5995
fn reset_for_family(&mut self, family: CurveFamily) {
6096
self.family = Some(family);
6197
self.spline_parameter_count = None;
62-
self.loss_mse_points.clear();
98+
self.clear_metric_points();
6399
self.parameter_names = family
64100
.parameter_names()
65101
.iter()
@@ -73,16 +109,28 @@ impl IterationDiagnostics {
73109
fn reset_for_spline(&mut self, parameter_count: usize) {
74110
self.family = None;
75111
self.spline_parameter_count = Some(parameter_count);
76-
self.loss_mse_points.clear();
112+
self.clear_metric_points();
77113
self.parameter_names = (0..parameter_count)
78114
.map(|index| format!("knot_y[{index}]"))
79115
.collect();
80116
self.parameter_series = (0..self.parameter_names.len())
81117
.map(|_| Vec::new())
82118
.collect();
83119
}
120+
121+
/// Очищает только временные ряды метрик, не трогая метаданные параметров.
122+
fn clear_metric_points(&mut self) {
123+
self.loss_points.clear();
124+
self.mse_points.clear();
125+
self.rmse_points.clear();
126+
self.mae_points.clear();
127+
self.soft_l1_points.clear();
128+
self.r2_abs_points.clear();
129+
self.max_abs_error_points.clear();
130+
}
84131
}
85132

133+
/// Добавляет точку в ряд или обновляет последнюю, если итерация совпадает.
86134
fn upsert_iteration_point(series: &mut Vec<[f64; 2]>, iteration: f64, value: f64) {
87135
if let Some(last) = series.last_mut()
88136
&& (last[0] - iteration).abs() <= f64::EPSILON

0 commit comments

Comments
 (0)