cue: calibration calculator — solution prep, conductivity, cell constant from EIS
This commit is contained in:
parent
4beb9f4408
commit
6351a8baa0
166
cue/src/app.rs
166
cue/src/app.rs
|
|
@ -31,6 +31,7 @@ pub enum Tab {
|
||||||
Amp,
|
Amp,
|
||||||
Chlorine,
|
Chlorine,
|
||||||
Ph,
|
Ph,
|
||||||
|
Calibrate,
|
||||||
Browse,
|
Browse,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -97,6 +98,13 @@ pub enum Message {
|
||||||
/* pH */
|
/* pH */
|
||||||
PhStabilizeChanged(String),
|
PhStabilizeChanged(String),
|
||||||
StartPh,
|
StartPh,
|
||||||
|
/* Calibration */
|
||||||
|
CalVolumeChanged(String),
|
||||||
|
CalNaclChanged(String),
|
||||||
|
CalClChanged(String),
|
||||||
|
CalBleachChanged(String),
|
||||||
|
CalTempChanged(String),
|
||||||
|
CalComputeK,
|
||||||
/* Global */
|
/* Global */
|
||||||
PollTemp,
|
PollTemp,
|
||||||
NativeMenuTick,
|
NativeMenuTick,
|
||||||
|
|
@ -221,6 +229,14 @@ pub struct App {
|
||||||
clean_v: String,
|
clean_v: String,
|
||||||
clean_dur: String,
|
clean_dur: String,
|
||||||
|
|
||||||
|
/* Calibration */
|
||||||
|
cal_volume_gal: String,
|
||||||
|
cal_nacl_ppm: String,
|
||||||
|
cal_cl_ppm: String,
|
||||||
|
cal_bleach_pct: String,
|
||||||
|
cal_temp_c: String,
|
||||||
|
cal_cell_constant: Option<f32>,
|
||||||
|
|
||||||
/* Global */
|
/* Global */
|
||||||
temp_c: f32,
|
temp_c: f32,
|
||||||
midi_gen: u64,
|
midi_gen: u64,
|
||||||
|
|
@ -271,6 +287,34 @@ fn fmt_cl(pts: &[ClPoint]) -> String {
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn calc_salt_grams(volume_gal: f32, target_ppm: f32) -> f32 {
|
||||||
|
let liters = volume_gal * 3.78541;
|
||||||
|
target_ppm * liters / 1000.0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn calc_bleach_ml(volume_gal: f32, target_cl_ppm: f32, bleach_pct: f32) -> f32 {
|
||||||
|
let liters = volume_gal * 3.78541;
|
||||||
|
let cl_needed_mg = target_cl_ppm * liters;
|
||||||
|
let bleach_mg_per_ml = bleach_pct * 10.0;
|
||||||
|
cl_needed_mg / bleach_mg_per_ml
|
||||||
|
}
|
||||||
|
|
||||||
|
fn theoretical_conductivity_ms_cm(nacl_ppm: f32, temp_c: f32) -> f32 {
|
||||||
|
let kappa_25 = nacl_ppm * 2.0 / 1000.0;
|
||||||
|
kappa_25 * (1.0 + 0.0212 * (temp_c - 25.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_rs(eis_points: &[EisPoint]) -> Option<f32> {
|
||||||
|
eis_points.iter()
|
||||||
|
.map(|p| p.z_real)
|
||||||
|
.filter(|r| r.is_finite() && *r > 0.0)
|
||||||
|
.min_by(|a, b| a.partial_cmp(b).unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cell_constant(kappa_ms_cm: f32, rs_ohm: f32) -> f32 {
|
||||||
|
(kappa_ms_cm / 1000.0) * rs_ohm
|
||||||
|
}
|
||||||
|
|
||||||
const SQUIRCLE: f32 = 8.0;
|
const SQUIRCLE: f32 = 8.0;
|
||||||
|
|
||||||
fn btn_style(bg: Color, fg: Color) -> impl Fn(&Theme, button::Status) -> ButtonStyle {
|
fn btn_style(bg: Color, fg: Color) -> impl Fn(&Theme, button::Status) -> ButtonStyle {
|
||||||
|
|
@ -417,6 +461,13 @@ impl App {
|
||||||
clean_v: "1200".into(),
|
clean_v: "1200".into(),
|
||||||
clean_dur: "30".into(),
|
clean_dur: "30".into(),
|
||||||
|
|
||||||
|
cal_volume_gal: "25".into(),
|
||||||
|
cal_nacl_ppm: "2500".into(),
|
||||||
|
cal_cl_ppm: "5".into(),
|
||||||
|
cal_bleach_pct: "7.825".into(),
|
||||||
|
cal_temp_c: "40".into(),
|
||||||
|
cal_cell_constant: None,
|
||||||
|
|
||||||
temp_c: 25.0,
|
temp_c: 25.0,
|
||||||
midi_gen: 0,
|
midi_gen: 0,
|
||||||
transport: TransportMode::Midi,
|
transport: TransportMode::Midi,
|
||||||
|
|
@ -717,7 +768,7 @@ impl App {
|
||||||
Tab::Lsv => self.lsv_data.perform(action),
|
Tab::Lsv => self.lsv_data.perform(action),
|
||||||
Tab::Amp => self.amp_data.perform(action),
|
Tab::Amp => self.amp_data.perform(action),
|
||||||
Tab::Chlorine => self.cl_data.perform(action),
|
Tab::Chlorine => self.cl_data.perform(action),
|
||||||
Tab::Ph | Tab::Browse => {}
|
Tab::Ph | Tab::Calibrate | Tab::Browse => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -870,7 +921,7 @@ impl App {
|
||||||
Tab::Amp => { self.amp_ref = None; self.status = "Amp reference cleared".into(); }
|
Tab::Amp => { self.amp_ref = None; self.status = "Amp reference cleared".into(); }
|
||||||
Tab::Chlorine => { self.cl_ref = None; self.status = "Chlorine reference cleared".into(); }
|
Tab::Chlorine => { self.cl_ref = None; self.status = "Chlorine reference cleared".into(); }
|
||||||
Tab::Ph => { self.ph_ref = None; self.status = "pH reference cleared".into(); }
|
Tab::Ph => { self.ph_ref = None; self.status = "pH reference cleared".into(); }
|
||||||
Tab::Browse => {}
|
Tab::Calibrate | Tab::Browse => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* Global */
|
/* Global */
|
||||||
|
|
@ -887,6 +938,24 @@ impl App {
|
||||||
Message::CloseSysInfo => {
|
Message::CloseSysInfo => {
|
||||||
self.show_sysinfo = false;
|
self.show_sysinfo = false;
|
||||||
}
|
}
|
||||||
|
/* Calibration */
|
||||||
|
Message::CalVolumeChanged(s) => self.cal_volume_gal = s,
|
||||||
|
Message::CalNaclChanged(s) => self.cal_nacl_ppm = s,
|
||||||
|
Message::CalClChanged(s) => self.cal_cl_ppm = s,
|
||||||
|
Message::CalBleachChanged(s) => self.cal_bleach_pct = s,
|
||||||
|
Message::CalTempChanged(s) => self.cal_temp_c = s,
|
||||||
|
Message::CalComputeK => {
|
||||||
|
let ppm = self.cal_nacl_ppm.parse::<f32>().unwrap_or(2500.0);
|
||||||
|
let temp = self.cal_temp_c.parse::<f32>().unwrap_or(40.0);
|
||||||
|
let kappa = theoretical_conductivity_ms_cm(ppm, temp);
|
||||||
|
if let Some(rs) = extract_rs(&self.eis_points) {
|
||||||
|
let k = cell_constant(kappa, rs);
|
||||||
|
self.cal_cell_constant = Some(k);
|
||||||
|
self.status = format!("Cell constant: {:.4} cm-1 (Rs={:.1} ohm)", k, rs);
|
||||||
|
} else {
|
||||||
|
self.status = "No valid EIS data for Rs extraction".into();
|
||||||
|
}
|
||||||
|
}
|
||||||
/* Clean */
|
/* Clean */
|
||||||
Message::CleanVChanged(s) => self.clean_v = s,
|
Message::CleanVChanged(s) => self.clean_v = s,
|
||||||
Message::CleanDurChanged(s) => self.clean_dur = s,
|
Message::CleanDurChanged(s) => self.clean_dur = s,
|
||||||
|
|
@ -1128,6 +1197,7 @@ impl App {
|
||||||
tab_btn("Amperometry", Tab::Amp, self.tab == Tab::Amp),
|
tab_btn("Amperometry", Tab::Amp, self.tab == Tab::Amp),
|
||||||
tab_btn("Chlorine", Tab::Chlorine, self.tab == Tab::Chlorine),
|
tab_btn("Chlorine", Tab::Chlorine, self.tab == Tab::Chlorine),
|
||||||
tab_btn("pH", Tab::Ph, self.tab == Tab::Ph),
|
tab_btn("pH", Tab::Ph, self.tab == Tab::Ph),
|
||||||
|
tab_btn("Cal", Tab::Calibrate, self.tab == Tab::Calibrate),
|
||||||
tab_btn("Browse", Tab::Browse, self.tab == Tab::Browse),
|
tab_btn("Browse", Tab::Browse, self.tab == Tab::Browse),
|
||||||
]
|
]
|
||||||
.spacing(4)
|
.spacing(4)
|
||||||
|
|
@ -1158,7 +1228,7 @@ impl App {
|
||||||
Tab::Amp => self.amp_ref.is_some(),
|
Tab::Amp => self.amp_ref.is_some(),
|
||||||
Tab::Chlorine => self.cl_ref.is_some(),
|
Tab::Chlorine => self.cl_ref.is_some(),
|
||||||
Tab::Ph => self.ph_ref.is_some(),
|
Tab::Ph => self.ph_ref.is_some(),
|
||||||
Tab::Browse => false,
|
Tab::Calibrate | Tab::Browse => false,
|
||||||
};
|
};
|
||||||
let has_data = match self.tab {
|
let has_data = match self.tab {
|
||||||
Tab::Eis => !self.eis_points.is_empty(),
|
Tab::Eis => !self.eis_points.is_empty(),
|
||||||
|
|
@ -1166,7 +1236,7 @@ impl App {
|
||||||
Tab::Amp => !self.amp_points.is_empty(),
|
Tab::Amp => !self.amp_points.is_empty(),
|
||||||
Tab::Chlorine => self.cl_result.is_some(),
|
Tab::Chlorine => self.cl_result.is_some(),
|
||||||
Tab::Ph => self.ph_result.is_some(),
|
Tab::Ph => self.ph_result.is_some(),
|
||||||
Tab::Browse => false,
|
Tab::Calibrate | Tab::Browse => false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut ref_row = row![].spacing(4).align_y(iced::Alignment::Center);
|
let mut ref_row = row![].spacing(4).align_y(iced::Alignment::Center);
|
||||||
|
|
@ -1264,6 +1334,8 @@ impl App {
|
||||||
self.view_browse_body()
|
self.view_browse_body()
|
||||||
} else if self.tab == Tab::Ph {
|
} else if self.tab == Tab::Ph {
|
||||||
self.view_ph_body()
|
self.view_ph_body()
|
||||||
|
} else if self.tab == Tab::Calibrate {
|
||||||
|
self.view_cal_body()
|
||||||
} else {
|
} else {
|
||||||
pane_grid::PaneGrid::new(&self.panes, |_pane, &pane_id, _maximized| {
|
pane_grid::PaneGrid::new(&self.panes, |_pane, &pane_id, _maximized| {
|
||||||
let el = match pane_id {
|
let el = match pane_id {
|
||||||
|
|
@ -1501,7 +1573,7 @@ impl App {
|
||||||
.align_y(iced::Alignment::End)
|
.align_y(iced::Alignment::End)
|
||||||
.into(),
|
.into(),
|
||||||
|
|
||||||
Tab::Browse => row![].into(),
|
Tab::Calibrate | Tab::Browse => row![].into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1561,7 +1633,7 @@ impl App {
|
||||||
column![result_text, plot].spacing(4).height(Length::Fill).into()
|
column![result_text, plot].spacing(4).height(Length::Fill).into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Tab::Ph | Tab::Browse => text("").into(),
|
Tab::Ph | Tab::Calibrate | Tab::Browse => text("").into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1571,7 +1643,7 @@ impl App {
|
||||||
Tab::Lsv => &self.lsv_data,
|
Tab::Lsv => &self.lsv_data,
|
||||||
Tab::Amp => &self.amp_data,
|
Tab::Amp => &self.amp_data,
|
||||||
Tab::Chlorine => &self.cl_data,
|
Tab::Chlorine => &self.cl_data,
|
||||||
Tab::Ph | Tab::Browse => return text("").into(),
|
Tab::Ph | Tab::Calibrate | Tab::Browse => return text("").into(),
|
||||||
};
|
};
|
||||||
text_editor(content)
|
text_editor(content)
|
||||||
.on_action(Message::DataAction)
|
.on_action(Message::DataAction)
|
||||||
|
|
@ -1582,6 +1654,86 @@ impl App {
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn view_cal_body(&self) -> Element<'_, Message> {
|
||||||
|
let vol = self.cal_volume_gal.parse::<f32>().unwrap_or(0.0);
|
||||||
|
let ppm = self.cal_nacl_ppm.parse::<f32>().unwrap_or(0.0);
|
||||||
|
let cl = self.cal_cl_ppm.parse::<f32>().unwrap_or(0.0);
|
||||||
|
let bleach = self.cal_bleach_pct.parse::<f32>().unwrap_or(7.825);
|
||||||
|
let temp = self.cal_temp_c.parse::<f32>().unwrap_or(40.0);
|
||||||
|
|
||||||
|
let salt_g = calc_salt_grams(vol, ppm);
|
||||||
|
let salt_tbsp = salt_g / 17.0;
|
||||||
|
let bleach_ml = calc_bleach_ml(vol, cl, bleach);
|
||||||
|
let bleach_tsp = bleach_ml / 5.0;
|
||||||
|
let kappa = theoretical_conductivity_ms_cm(ppm, temp);
|
||||||
|
|
||||||
|
let inputs = column![
|
||||||
|
text("Calibration Solution").size(16),
|
||||||
|
iced::widget::horizontal_rule(1),
|
||||||
|
row![
|
||||||
|
column![
|
||||||
|
text("Volume (gal)").size(12),
|
||||||
|
text_input("25", &self.cal_volume_gal)
|
||||||
|
.on_input(Message::CalVolumeChanged).width(80),
|
||||||
|
].spacing(2),
|
||||||
|
column![
|
||||||
|
text("NaCl ppm").size(12),
|
||||||
|
text_input("2500", &self.cal_nacl_ppm)
|
||||||
|
.on_input(Message::CalNaclChanged).width(80),
|
||||||
|
].spacing(2),
|
||||||
|
column![
|
||||||
|
text("Cl ppm").size(12),
|
||||||
|
text_input("5", &self.cal_cl_ppm)
|
||||||
|
.on_input(Message::CalClChanged).width(80),
|
||||||
|
].spacing(2),
|
||||||
|
column![
|
||||||
|
text("Bleach %").size(12),
|
||||||
|
text_input("7.825", &self.cal_bleach_pct)
|
||||||
|
.on_input(Message::CalBleachChanged).width(80),
|
||||||
|
].spacing(2),
|
||||||
|
column![
|
||||||
|
text("Temp C").size(12),
|
||||||
|
text_input("40", &self.cal_temp_c)
|
||||||
|
.on_input(Message::CalTempChanged).width(80),
|
||||||
|
].spacing(2),
|
||||||
|
].spacing(10).align_y(iced::Alignment::End),
|
||||||
|
].spacing(6);
|
||||||
|
|
||||||
|
let mut results = column![
|
||||||
|
text("Results").size(16),
|
||||||
|
iced::widget::horizontal_rule(1),
|
||||||
|
text(format!("Salt: {:.1} g ({:.1} tbsp sea salt)", salt_g, salt_tbsp)).size(14),
|
||||||
|
text(format!("Bleach: {:.1} mL ({:.1} tsp)", bleach_ml, bleach_tsp)).size(14),
|
||||||
|
text(format!("Theoretical kappa at {:.0} C: {:.3} mS/cm", temp, kappa)).size(14),
|
||||||
|
].spacing(4);
|
||||||
|
|
||||||
|
let rs = extract_rs(&self.eis_points);
|
||||||
|
if let Some(rs_val) = rs {
|
||||||
|
results = results.push(text(format!("Rs from sweep: {:.1} ohm", rs_val)).size(14));
|
||||||
|
} else {
|
||||||
|
results = results.push(text("Rs from sweep: (no EIS data)").size(14));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(k) = self.cal_cell_constant {
|
||||||
|
results = results.push(text(format!("Cell constant K: {:.4} cm-1", k)).size(14));
|
||||||
|
}
|
||||||
|
|
||||||
|
let compute_btn = button(text("Calculate K from Sweep").size(13))
|
||||||
|
.style(style_action())
|
||||||
|
.padding([6, 16])
|
||||||
|
.on_press(Message::CalComputeK);
|
||||||
|
results = results.push(compute_btn);
|
||||||
|
|
||||||
|
row![
|
||||||
|
container(inputs).width(Length::FillPortion(2)),
|
||||||
|
iced::widget::vertical_rule(1),
|
||||||
|
container(results).width(Length::FillPortion(3)),
|
||||||
|
]
|
||||||
|
.spacing(12)
|
||||||
|
.height(Length::Fill)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
fn view_ph_body(&self) -> Element<'_, Message> {
|
fn view_ph_body(&self) -> Element<'_, Message> {
|
||||||
if let Some(r) = &self.ph_result {
|
if let Some(r) = &self.ph_result {
|
||||||
let nernst_slope = 0.1984 * (r.temp_c as f64 + 273.15);
|
let nernst_slope = 0.1984 * (r.temp_c as f64 + 273.15);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue