Skip to content

Commit 957a483

Browse files
committed
feat: handle formula in config
1 parent 4691f8f commit 957a483

File tree

8 files changed

+232
-37
lines changed

8 files changed

+232
-37
lines changed

rust/Cargo.lock

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

rust/Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ rocket_async_compression = "0.6.1"
6363
async-trait = "0.1.88"
6464
uuid = { version = "1.17.0", features = ["v4"] }
6565
schemars = "0.8"
66+
evalexpr = "12.0.2"
6667

6768
[dev-dependencies]
6869
criterion = "0.6.0" # Benchmarking
@@ -77,6 +78,8 @@ sha2 = "0.10.9" # Pour le hachage SHA-256
7778
base64 = "0.22.1" # Pour l'encodage Base64URL utilisé dans PKCE
7879
serde_json = "1.0.140" # Pour analyser la réponse JSON des tokens
7980
regex = "1.11.1" # Expressions régulières
81+
evalexpr = "12.0.2" # Mathematical expression evaluation
82+
approx = "0.5.1" # Approximate floating-point equality for tests
8083

8184
[build-dependencies]
8285
serde = { version = "1.0.219", features = ["derive"] }
@@ -101,4 +104,4 @@ strip = true
101104

102105
[[bin]]
103106
name = "pid_tuner"
104-
path = "src/bin/pid_tuner.rs"
107+
path = "src/bin/pid_tuner.rs"

rust/config.example.yaml

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -463,12 +463,19 @@ thermal_regulation:
463463
power_range: "0-100%"
464464
max_power_percent: 100.0
465465

466-
# ADC to temperature conversion (polynomial)
466+
# ADC to temperature conversion using NTC thermistor (10kΩ, β=3977)
467+
# Configuration for 10kΩ/10kΩ voltage divider with 5V supply
467468
temperature_conversion:
468-
formula: "0.0001*x^3 - 0.02*x^2 + 1.5*x + 273.15" # Kelvin
469+
# Mathematical formula to convert ADC voltage to temperature in Kelvin
470+
# NTC formula: 1/T = 1/T0 + ln(R/R0)/β where R = R_pullup * voltage / (V_supply - voltage)
471+
# Parameters: R_pullup=10kΩ, R0=10kΩ (resistance at 25°C), β=3977, T0=298.15K, V_supply=5V
472+
# @see https://docs.rs/evalexpr/latest/evalexpr/index.html#builtin-functions for syntax
473+
# Note: Use `math::ln` for neperian logarithm in evalexpr
474+
# Use `math::log` for base-10 logarithm
475+
formula: "1.0 / (1.0 / 298.15 + math::ln(10000.0 * voltage / (5.0 - voltage) / 10000.0) / 3977.0)"
469476
adc_resolution: 16 # bits
470-
voltage_reference: 3.3 # V
471-
conversion_type: "polynomial"
477+
voltage_reference: 5.0 # V (supply voltage for voltage divider)
478+
conversion_type: "ntc_thermistor" # NTC thermistor with β formula
472479

473480
# PID parameters
474481
pid_parameters:
@@ -539,12 +546,16 @@ thermal_regulation:
539546
power_range: "0-100%"
540547
max_power_percent: 100.0
541548

542-
# Temperature conversion for mock sensor
549+
# Temperature conversion for NTC thermistor (10kΩ, β=3977)
550+
# Mock sensor simulating 10kΩ/10kΩ voltage divider with 5V supply
543551
temperature_conversion:
544-
formula: "0.0001*x^3 - 0.02*x^2 + 1.5*x + 273.15" # Kelvin
552+
# Mathematical formula to convert ADC voltage to temperature in Kelvin
553+
# NTC formula: 1/T = 1/T0 + ln(R/R0)/β where R = R_pullup * voltage / (V_supply - voltage)
554+
# Parameters: R_pullup=10kΩ, R0=10kΩ (resistance at 25°C), β=3977, T0=298.15K, V_supply=5V
555+
formula: "1.0 / (1.0 / 298.15 + math::ln(10000.0 * voltage / (5.0 - voltage) / 10000.0) / 3977.0)"
545556
adc_resolution: 16 # bits
546-
voltage_reference: 3.3 # V
547-
conversion_type: "polynomial"
557+
voltage_reference: 5.0 # V (supply voltage for voltage divider)
558+
conversion_type: "ntc_thermistor" # NTC thermistor with β formula
548559

549560
# PID parameters tuned for mock thermal cell simulation
550561
pid_parameters:
@@ -603,11 +614,16 @@ thermal_regulation:
603614
h_bridge_direction: "forward"
604615
power_range: "0-80%"
605616
max_power_percent: 80.0
617+
# Temperature conversion for NTC thermistor (10kΩ, β=3977)
618+
# Detector temperature sensor with 10kΩ/10kΩ voltage divider, 5V supply
606619
temperature_conversion:
607-
formula: "0.0001*x^3 - 0.02*x^2 + 1.5*x + 273.15"
608-
adc_resolution: 16
609-
voltage_reference: 3.3
610-
conversion_type: "polynomial"
620+
# Mathematical formula to convert ADC voltage to temperature in Kelvin
621+
# NTC formula: 1/T = 1/T0 + ln(R/R0)/β where R = R_pullup * voltage / (V_supply - voltage)
622+
# Parameters: R_pullup=10kΩ, R0=10kΩ (resistance at 25°C), β=3977, T0=298.15K, V_supply=5V
623+
formula: "1.0 / (1.0 / 298.15 + math::ln(10000.0 * voltage / (5.0 - voltage) / 10000.0) / 3977.0)"
624+
adc_resolution: 16 # bits
625+
voltage_reference: 5.0 # V (supply voltage for voltage divider)
626+
conversion_type: "ntc_thermistor" # NTC thermistor with β formula
611627
pid_parameters:
612628
kp: 1.5
613629
ki: 0.08

rust/resources/config.schema.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1288,7 +1288,7 @@
12881288
},
12891289
"conversion_type": {
12901290
"type": "string",
1291-
"enum": ["polynomial", "linear", "lookup_table"],
1291+
"enum": ["ntc_thermistor","polynomial", "linear", "lookup_table"],
12921292
"default": "polynomial",
12931293
"description": "Conversion algorithm type"
12941294
}

rust/src/bin/pid_tuner_helper/step_response.rs

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use log::{debug, info};
1414
use rust_photoacoustic::config::{thermal_regulation::ThermalRegulatorConfig, Config};
1515
use rust_photoacoustic::thermal_regulation::drivers::MockI2CDriver;
1616
use rust_photoacoustic::thermal_regulation::I2CBusDriver;
17+
use rust_photoacoustic::utility::convert_voltage_to_temperature;
1718
use std::time::{Duration, Instant};
1819
use tokio::time::sleep;
1920

@@ -285,36 +286,40 @@ fn calculate_step_control_output(step_amplitude: f64) -> f64 {
285286
base_output.clamp(-80.0, 80.0) // Limit to safe range
286287
}
287288

288-
/// Convert raw ADC value to temperature using NTC thermistor calculation
289+
/// Convert raw ADC value to temperature using configurable formula
290+
///
291+
/// This function converts a raw ADC reading to temperature using the formula
292+
/// specified in the thermal regulator configuration. It supports various
293+
/// conversion types including NTC thermistors with β formula.
289294
fn convert_adc_to_temperature(
290295
raw_value: u16,
291296
regulator_config: &ThermalRegulatorConfig,
292297
) -> Result<f64> {
293-
// Convert ADC reading to voltage (16-bit ADC, 0-5V range)
294-
let v_adc = (raw_value as f64 / 65535.0) * 5.0;
295-
296-
// Calculate NTC resistance from voltage divider
297-
// V_adc = 5V * R_ntc / (10000 + R_ntc)
298-
// Solving for R_ntc: R_ntc = 10000 * V_adc / (5 - V_adc)
299-
if v_adc >= 5.0 {
300-
return Err(anyhow!("Invalid ADC voltage: {:.3}V", v_adc));
301-
}
298+
// Get conversion parameters from configuration
299+
let temp_conversion = &regulator_config.temperature_conversion;
300+
let adc_resolution = temp_conversion.adc_resolution;
301+
let voltage_reference = temp_conversion.voltage_reference;
302+
let formula = &temp_conversion.formula;
302303

303-
let r_ntc = 10000.0 * v_adc / (5.0 - v_adc);
304+
// Convert ADC reading to voltage based on configuration
305+
let max_adc_value = (1_u32 << adc_resolution) - 1; // e.g., 65535 for 16-bit
306+
let voltage = (raw_value as f64 / max_adc_value as f64) * voltage_reference as f64;
304307

305-
// Calculate temperature from NTC resistance using β formula
306-
// R = R0 * exp(β * (1/T - 1/T0))
307-
// Solving for T: 1/T = 1/T0 + ln(R/R0)/β
308-
let r0 = 10000.0; // 10kΩ at 25°C
309-
let beta = 3977.0;
310-
let t0 = 298.15; // 25°C in Kelvin
308+
debug!(
309+
"ADC conversion: raw={}, voltage={:.3}V, formula='{}'",
310+
raw_value, voltage, formula
311+
);
311312

312-
if r_ntc <= 0.0 {
313-
return Err(anyhow!("Invalid NTC resistance: {:.1}Ω", r_ntc));
314-
}
313+
// Use the utility function to convert voltage to temperature
314+
let temperature_k = convert_voltage_to_temperature(formula.clone(), voltage as f32)?;
315+
316+
// Convert from Kelvin to Celsius for return value
317+
let temperature_c = temperature_k - 273.15;
315318

316-
let temp_k = 1.0 / (1.0 / t0 + (r_ntc / r0).ln() / beta);
317-
let temp_c = temp_k - 273.15;
319+
debug!(
320+
"Temperature conversion: {:.2}K = {:.2}°C",
321+
temperature_k, temperature_c
322+
);
318323

319-
Ok(temp_c)
324+
Ok(temperature_c)
320325
}

rust/src/config/thermal_regulation.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,8 @@ pub enum ConversionType {
454454
Linear,
455455
/// Lookup table
456456
LookupTable,
457+
/// NTC thermistor conversion using mathematical formula
458+
NtcThermistor,
457459
}
458460

459461
// Additional configuration structures

rust/src/utility/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ pub mod data_source;
1010
pub mod noise_generator;
1111
#[cfg(test)]
1212
pub mod noise_generator_test;
13+
pub mod temperature_conversion;
1314

1415
// Re-exports for use in other modules
1516
pub use data_source::PhotoacousticDataSource;
17+
pub use temperature_conversion::convert_voltage_to_temperature;
1618

1719
/// Macro to include a PNG file as a base64-encoded string
1820
/// This macro reads a PNG file at compile time and encodes it in base64 format.
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
use anyhow::{anyhow, Result};
2+
use evalexpr::{
3+
eval_with_context, Context, ContextWithMutableVariables, DefaultNumericTypes, HashMapContext,
4+
Value,
5+
};
6+
7+
/// Convert voltage to temperature using a configurable mathematical formula
8+
///
9+
/// This function evaluates a mathematical expression where 'voltage' is available as a variable.
10+
/// The formula should return temperature in Kelvin.
11+
///
12+
/// # Arguments
13+
/// * `formula` - Mathematical expression as string (e.g., "1.0 / (1.0 / 298.15 + ln(10000.0 * voltage / (5.0 - voltage) / 10000.0) / 3977.0)")
14+
/// * `voltage` - Input voltage in volts
15+
///
16+
/// # Returns
17+
/// * `Result<f64>` - Temperature in Kelvin, or error if formula evaluation fails
18+
///
19+
/// # Example
20+
/// ```rust
21+
/// use rust_photoacoustic::utility::temperature_conversion::convert_voltage_to_temperature;
22+
///
23+
/// // NTC formula for 10kΩ NTC with β=3977, 10kΩ voltage divider, 5V supply
24+
/// let formula = "1.0 / (1.0 / 298.15 + math::ln(10000.0 * voltage / (5.0 - voltage) / 10000.0) / 3977.0)".to_string();
25+
/// let temp_k = convert_voltage_to_temperature(formula, 2.5).unwrap();
26+
/// assert!((temp_k - 298.15).abs() < 1.0); // Should be close to 25°C (298.15K)
27+
/// ```
28+
pub fn convert_voltage_to_temperature(formula: String, voltage: f32) -> Result<f64> {
29+
// Validate input voltage
30+
if voltage < 0.0 || voltage > 10.0 {
31+
return Err(anyhow!(
32+
"Invalid voltage: {:.3}V (must be between 0V and 10V)",
33+
voltage
34+
));
35+
}
36+
37+
// Validate formula contains 'voltage' variable
38+
if !formula.contains("voltage") {
39+
return Err(anyhow!(
40+
"Formula must contain 'voltage' variable, got: '{}'",
41+
formula
42+
));
43+
}
44+
45+
// Create evaluation context with voltage variable
46+
let mut context = HashMapContext::<DefaultNumericTypes>::new();
47+
context.set_builtin_functions_disabled(false).unwrap();
48+
context.set_value("voltage".into(), Value::Float(voltage as f64))?;
49+
50+
// Evaluate the formula
51+
let result = eval_with_context(&formula, &context).map_err(|e| {
52+
anyhow!(
53+
"Failed to evaluate temperature formula '{}': {}",
54+
formula,
55+
e
56+
)
57+
})?;
58+
59+
// Convert result to f64
60+
let temperature_k = result.as_float().or_else(|_| {
61+
Err(anyhow!(
62+
"Formula did not return a numeric value: '{}'",
63+
formula
64+
))
65+
})?;
66+
67+
// Validate result (reasonable temperature range in Kelvin: -50°C to 100°C)
68+
if temperature_k < 223.15 || temperature_k > 373.15 {
69+
return Err(anyhow!(
70+
"Calculated temperature {:.2}K ({:.2}°C) is outside reasonable range (-50°C to 100°C)",
71+
temperature_k,
72+
temperature_k - 273.15
73+
));
74+
}
75+
76+
Ok(temperature_k)
77+
}
78+
79+
#[cfg(test)]
80+
mod tests {
81+
use super::*;
82+
use approx::assert_relative_eq;
83+
84+
#[test]
85+
fn test_convert_voltage_to_temperature_ntc_formula() {
86+
// Formula for 10kΩ NTC with β=3977, 10kΩ voltage divider, 5V supply
87+
let formula =
88+
"1.0 / (1.0 / 298.15 + math::ln(10000.0 * voltage / (5.0 - voltage) / 10000.0) / 3977.0)"
89+
.to_string();
90+
91+
// Test at 2.5V (should be close to 25°C = 298.15K for balanced voltage divider)
92+
let temp_k = convert_voltage_to_temperature(formula.clone(), 2.5).unwrap();
93+
assert_relative_eq!(temp_k, 298.15, epsilon = 1.0);
94+
95+
// Test at other voltages
96+
let temp_k_low = convert_voltage_to_temperature(formula.clone(), 1.0).unwrap();
97+
let temp_k_high = convert_voltage_to_temperature(formula.clone(), 4.0).unwrap();
98+
99+
// Lower voltage should mean higher temperature (NTC characteristic)
100+
assert!(temp_k_low > temp_k_high);
101+
}
102+
103+
#[test]
104+
fn test_convert_voltage_to_temperature_simple_linear() {
105+
// Simple linear formula: temperature = 273.15 + voltage * 10 (10°C per volt)
106+
let formula = "273.15 + voltage * 10.0".to_string();
107+
108+
let temp_k = convert_voltage_to_temperature(formula, 2.5).unwrap();
109+
assert_relative_eq!(temp_k, 298.15, epsilon = 0.001); // 273.15 + 2.5 * 10 = 298.15K
110+
}
111+
112+
#[test]
113+
fn test_convert_voltage_to_temperature_invalid_voltage() {
114+
let formula = "273.15 + voltage * 10.0".to_string();
115+
116+
// Test negative voltage
117+
assert!(convert_voltage_to_temperature(formula.clone(), -1.0).is_err());
118+
119+
// Test excessive voltage
120+
assert!(convert_voltage_to_temperature(formula, 15.0).is_err());
121+
}
122+
123+
#[test]
124+
fn test_convert_voltage_to_temperature_invalid_formula() {
125+
// Test malformed formula
126+
assert!(convert_voltage_to_temperature("invalid formula".to_string(), 2.5).is_err());
127+
128+
// Test formula with undefined variable
129+
assert!(
130+
convert_voltage_to_temperature("273.15 + unknown_var * 10.0".to_string(), 2.5).is_err()
131+
);
132+
}
133+
134+
#[test]
135+
fn test_convert_voltage_to_temperature_unreasonable_result() {
136+
// Formula that would produce unreasonable temperature
137+
let formula = "1000.0".to_string(); // 1000K = 726.85°C (too hot)
138+
assert!(convert_voltage_to_temperature(formula, 2.5).is_err());
139+
140+
let formula_cold = "100.0".to_string(); // 100K = -173.15°C (too cold)
141+
assert!(convert_voltage_to_temperature(formula_cold, 2.5).is_err());
142+
}
143+
144+
#[test]
145+
fn test_convert_voltage_to_temperature_non_numeric_result() {
146+
// Formula that returns string (should fail)
147+
let formula = "\"not a number\"".to_string();
148+
assert!(convert_voltage_to_temperature(formula, 2.5).is_err());
149+
}
150+
}

0 commit comments

Comments
 (0)