Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ colorous = "1"
num-complex = "0.4"
rustfft = "6.4"
serde = { version = "1.0", features = ["derive", "rc"] }
serde_json = "1.0"
serde_json = { version = "1.0", features = ["float_roundtrip"] }
semver = "1"
sha2 = "0.10"
ureq = { version = "2", default-features = false, features = ["tls"] }
Expand Down
17 changes: 16 additions & 1 deletion crates/app/src/ui/properties/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ pub(crate) mod fixture;
pub(crate) use search::property_hits;

use plotx_core::properties::{
PropertyDefinition, PropertyId, Tier, apodization, contour, definition, export_dpi, line,
PropertyDefinition, PropertyId, Tier, apodization, contour, definition, export_dpi, ilt, line,
typography,
};
use plotx_core::state::{SettingsCategory, WorkflowTab};
Expand Down Expand Up @@ -211,6 +211,11 @@ const EXPORT_PREFERENCES_HOME: HomeRoute = HomeRoute {
section: SettingsCategory::Export.section_id(),
};

const PROCESSING_PREFERENCES_HOME: HomeRoute = HomeRoute {
panel: PanelRoute::Preferences,
section: SettingsCategory::Processing.section_id(),
};

pub const PRESENTATIONS: &[PropertyPresentation] = &[
PropertyPresentation {
id: contour::BASE_MAGNITUDE,
Expand Down Expand Up @@ -319,6 +324,16 @@ pub const PRESENTATIONS: &[PropertyPresentation] = &[
home_route: EXPORT_PREFERENCES_HOME,
canvas_step: false,
},
PropertyPresentation {
id: ilt::DEFAULT_LAMBDA,
localized_label: LocalizedText("Default ILT regularization"),
localized_aliases: &[
LocalizedText("ILT lambda"),
LocalizedText("DOSY regularization"),
],
home_route: PROCESSING_PREFERENCES_HOME,
canvas_step: false,
},
];

pub fn presentation(id: PropertyId) -> Option<&'static PropertyPresentation> {
Expand Down
18 changes: 16 additions & 2 deletions crates/app/src/ui/settings_dialog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ use super::*;
use egui::{Align, Align2, CornerRadius, FontId, Layout, RichText, pos2, vec2};
use egui_phosphor::regular as icon;
use plotx_core::settings::{
GraphicsPowerPreference, MAX_EXPORT_DPI, MIN_EXPORT_DPI, Settings, ThemeMode,
GraphicsPowerPreference, MAX_EXPORT_DPI, MAX_ILT_LAMBDA, MIN_EXPORT_DPI, MIN_ILT_LAMBDA,
Settings, ThemeMode,
};
use plotx_core::state::{MonitorScaleStatus, SettingsCategory, SettingsDialog};
use plotx_core::update::{UpdateChannelSetting, UpdateService, UpdateStatus};
Expand Down Expand Up @@ -312,7 +313,20 @@ fn render_category(
);
}
SettingsCategory::Processing => {
empty_state(ui, "Nothing to configure here yet.");
setting_row(
ui,
"Default ILT regularization (λ)",
Some(
"Regularization used to build an ILT DOSY map for a dataset that has no earlier ILT result.",
),
|ui| {
ui.add(
egui::DragValue::new(&mut draft.processing.ilt_lambda)
.speed(0.001)
.range(MIN_ILT_LAMBDA..=MAX_ILT_LAMBDA),
);
},
);
}
SettingsCategory::Export => {
setting_row(
Expand Down
73 changes: 45 additions & 28 deletions crates/app/src/ui/tools/pseudo.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use egui::{Button, DragValue, Ui};
use plotx_core::actions::{Action, DatasetProcessingState};
use plotx_core::settings::{MAX_ILT_LAMBDA, MIN_ILT_LAMBDA};
use plotx_core::state::PlotxApp;
use plotx_processing::{Layout2D, Preset2D};

Expand Down Expand Up @@ -160,7 +161,7 @@ fn parse_indices(text: &str) -> Result<Vec<usize>, String> {
fn pseudo_group(app: &mut PlotxApp, di: usize, ui: &mut Ui) {
use plotx_core::{DosyMethod, PseudoDisplay};

let (is_dosy, is_gradient, is_ilt, axis_summary, diff_summary, cur_display) = {
let (is_dosy, is_gradient, is_ilt, axis_summary, diff_summary, provenance_warning, cur_display) = {
let n = app.doc.datasets[di].as_nmr2d().unwrap();
let axis_summary = n.data.pseudo_axis.as_ref().map(|axis| {
format!(
Expand Down Expand Up @@ -192,6 +193,9 @@ fn pseudo_group(app: &mut PlotxApp, di: usize, ui: &mut Ui) {
matches!(n.dosy_method, DosyMethod::Ilt(_)),
axis_summary,
diff_summary,
n.dosy_provenance_warning
.clone()
.or_else(|| n.missing_selected_map_note().map(str::to_owned)),
n.display,
)
};
Expand All @@ -202,6 +206,9 @@ fn pseudo_group(app: &mut PlotxApp, di: usize, ui: &mut Ui) {
if let Some(s) = &diff_summary {
ui.small(s);
}
if let Some(warning) = &provenance_warning {
ui.colored_label(ui.visuals().warn_fg_color, warning);
}

ui.horizontal(|ui| {
ui.label("Show");
Expand All @@ -228,9 +235,7 @@ fn pseudo_group(app: &mut PlotxApp, di: usize, ui: &mut Ui) {
ui.horizontal(|ui| {
ui.label("DOSY method");
if ui.selectable_label(!is_ilt, "Per-column").clicked() && is_ilt {
if let Some(d2) = app.doc.datasets[di].as_nmr2d_mut() {
d2.dosy_method = DosyMethod::MonoExp;
}
app.set_pseudo_dosy_method(di, DosyMethod::MonoExp);
app.set_pseudo_display(di, PseudoDisplay::DosyMap);
}
if ui
Expand All @@ -242,44 +247,56 @@ fn pseudo_group(app: &mut PlotxApp, di: usize, ui: &mut Ui) {
.clicked()
&& !is_ilt
{
let params = app.session.ui.ilt_params;
if let Some(d2) = app.doc.datasets[di].as_nmr2d_mut() {
d2.dosy_method = DosyMethod::Ilt(params);
}
let params = app.resolve_ilt_params_for(di, app.explicit_ilt_input_for(di));
app.set_pseudo_dosy_method(di, DosyMethod::Ilt(params));
app.set_pseudo_display(di, PseudoDisplay::DosyMap);
}
});
if is_ilt {
// Show what this build would actually use — the explicit input if the
// user has entered one for this dataset, otherwise what the lifecycle
// resolves to. Only an edit here records an explicit input, so simply
// opening the panel never turns a resolved value into an override.
let resolved = app.resolve_ilt_params_for(di, app.explicit_ilt_input_for(di));
let mut draft = resolved;
let mut edited = false;
ui.horizontal(|ui| {
ui.label("λ");
ui.add(
DragValue::new(&mut app.session.ui.ilt_params.lambda)
.speed(0.001)
.range(1e-6..=1e3),
);
edited |= ui
.add(
DragValue::new(&mut draft.lambda)
.speed(0.001)
.range(MIN_ILT_LAMBDA..=MAX_ILT_LAMBDA),
)
.changed();
});
ui.horizontal(|ui| {
ui.label("D min");
ui.add(
DragValue::new(&mut app.session.ui.ilt_params.d_min)
.speed(1e-12)
.range(1e-13..=1e-7),
);
edited |= ui
.add(
DragValue::new(&mut draft.d_min)
.speed(1e-12)
.range(1e-13..=1e-7),
)
.changed();
ui.label("D max");
ui.add(
DragValue::new(&mut app.session.ui.ilt_params.d_max)
.speed(1e-10)
.range(1e-12..=1e-6),
);
edited |= ui
.add(
DragValue::new(&mut draft.d_max)
.speed(1e-10)
.range(1e-12..=1e-6),
)
.changed();
});
ui.horizontal(|ui| {
ui.label("Grid points");
ui.add(
DragValue::new(&mut app.session.ui.ilt_params.n_grid)
.speed(1.0)
.range(16..=512),
);
edited |= ui
.add(DragValue::new(&mut draft.n_grid).speed(1.0).range(16..=512))
.changed();
});
if edited {
app.set_explicit_ilt_input(di, draft);
}
}
}

Expand Down
62 changes: 54 additions & 8 deletions crates/core/src/automation/properties_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
//! planner the panel controls use.

use super::*;
use crate::properties::ilt_tests::ilt_app;
use crate::properties::tests::{contour_app, contour_spec};
use crate::properties::{
AggregateValue, PropertyAddress, PropertyValue, apodization, contour, definition_by_key,
AggregateValue, PropertyAddress, PropertyValue, apodization, contour, definition_by_key, ilt,
typography,
};
use crate::state::{
Expand Down Expand Up @@ -69,6 +70,43 @@ fn add_line_series(app: &mut PlotxApp) {
plot.binding.series.push(series);
}

#[test]
fn automation_inspects_stored_ilt_provenance_and_refuses_set_and_reset_as_read_only() {
let (mut app, target) = ilt_app(0.07);
let id = target.resource.id.clone();
let inspect = request(
&app,
TOOL_INSPECT,
serde_json::json!({"key": ilt::RESULT_LAMBDA.as_str()}),
vec![id.clone()],
CallerType::Agent,
);
let result = run(&mut app, inspect).expect("properties.inspect reads provenance");
let value = result.value.to_string();
assert!(value.contains("0.07"), "{value}");
assert!(value.contains("read_only"), "{value}");

for (tool, parameters) in [
(
TOOL_SET,
serde_json::json!({"key": ilt::RESULT_LAMBDA.as_str(), "value": 0.2}),
),
(
TOOL_RESET,
serde_json::json!({"key": ilt::RESULT_LAMBDA.as_str()}),
),
] {
let error = plan_tool(
&app,
request(&app, tool, parameters, vec![id.clone()], CallerType::Agent),
)
.expect_err("non-inspect automation must refuse read-only provenance");
let message = error.to_string();
assert!(message.contains("read-only"), "{message}");
assert!(message.contains(ilt::RESULT_LAMBDA.as_str()), "{message}");
}
}

#[test]
fn an_unknown_property_key_is_refused_rather_than_skipped() {
let (mut app, _) = contour_app();
Expand Down Expand Up @@ -425,14 +463,22 @@ fn dataset_property_tools_expand_processing_steps_and_report_non_apodization_ski
/// before anything is planned.
#[test]
fn a_read_only_property_cannot_be_written() {
// Every catalog entry is currently writable, so this asserts the guard
// exists rather than exercising a read-only entry that does not yet exist.
assert!(
crate::properties::catalog()
.iter()
.all(|definition| definition.access == crate::properties::PropertyAccess::ReadWrite),
"add a read-only case here once the catalog has one"
let (app, target) = ilt_app(0.07);
let error = plan_tool(
&app,
request(
&app,
TOOL_SET,
serde_json::json!({"key": ilt::RESULT_LAMBDA.as_str(), "value": 0.2}),
vec![target.resource.id],
CallerType::Agent,
),
);
let message = error
.expect_err("the read-only definition must be refused before planning")
.to_string();
assert!(message.contains("read-only"), "{message}");
assert!(message.contains(ilt::RESULT_LAMBDA.as_str()), "{message}");
}

// ---------------------------------------------------------------------------
Expand Down
19 changes: 18 additions & 1 deletion crates/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ pub struct IltParams {
impl Default for IltParams {
fn default() -> Self {
Self {
lambda: 1e-2,
lambda: crate::settings::DEFAULT_ILT_LAMBDA,
d_min: 1e-11,
d_max: 1e-8,
n_grid: 128,
Expand All @@ -143,6 +143,23 @@ pub enum DosyMethod {
Ilt(IltParams),
}

/// The invocation inputs recorded beside a persisted DOSY result.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "method", rename_all = "snake_case")]
pub enum DosyInvocation {
MonoExp { snr_frac: f64 },
Ilt { params: IltParams },
}

/// Reproducibility metadata for one persisted DOSY result.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DosyResultProvenance {
pub algorithm: String,
pub version: u32,
pub input: DosyInvocation,
pub data_fingerprint: String,
}

pub use plotx_analysis::series::ReduceOp;

pub fn extract_region_series(
Expand Down
2 changes: 2 additions & 0 deletions crates/core/src/operation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ pub enum DiagnosticCode {
TableImportWarning,
TableImportFailed,
ProjectLoadSucceeded,
ProjectLoadWarning,
ProjectLoadFailed,
ProjectSaveSucceeded,
ProjectSaveFailed,
Expand Down Expand Up @@ -69,6 +70,7 @@ impl DiagnosticCode {
Self::TableImportWarning => "table.import.warning",
Self::TableImportFailed => "table.import.failed",
Self::ProjectLoadSucceeded => "project.load.succeeded",
Self::ProjectLoadWarning => "project.load.warning",
Self::ProjectLoadFailed => "project.load.failed",
Self::ProjectSaveSucceeded => "project.save.succeeded",
Self::ProjectSaveFailed => "project.save.failed",
Expand Down
Loading
Loading