STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
calibration_sequence.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""
3Liquid Engine Sensor Calibration Sequence
4Implements automated calibration procedures for all sensors
5"""
6
7import sys
8import time
9import json
10import argparse
11import numpy as np
12from typing import Dict, List, Tuple, Optional
13from dataclasses import dataclass
14from pathlib import Path
15import logging
16
17# Configure logging
18logging.basicConfig(
19 level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
20)
21logger = logging.getLogger(__name__)
22
23
24@dataclass
26 """Represents a single calibration data point"""
27
28 input_value: float
29 reference_value: float
30 timestamp: float
31 environmental_conditions: Dict[str, float]
32 uncertainty: float
33
34
35@dataclass
37 """Represents calibration results for a sensor"""
38
39 sensor_id: str
40 sensor_type: str
41 calibration_points: List[CalibrationPoint]
42 parameters: Dict[str, float]
43 covariance_matrix: List[List[float]]
44 quality_metrics: Dict[str, float]
45 calibration_time: float
46
47
48class SensorCalibrator:
49 """Base class for sensor calibration"""
50
51 def __init__(self, sensor_id: str, sensor_type: str):
52 self.sensor_id = sensor_id
53 self.sensor_type = sensor_type
54 self.calibration_points: List[CalibrationPoint] = []
55
56 def add_calibration_point(
57 self,
58 input_value: float,
59 reference_value: float,
60 environmental_conditions: Dict[str, float],
61 uncertainty: float = 0.01,
62 ):
63 """Add a calibration data point"""
64 point = CalibrationPoint(
65 input_value=input_value,
66 reference_value=reference_value,
67 timestamp=time.time(),
68 environmental_conditions=environmental_conditions,
69 uncertainty=uncertainty,
70 )
71 self.calibration_points.append(point)
72
73 def perform_calibration(self) -> CalibrationResult:
74 """Perform calibration and return results"""
75 raise NotImplementedError
76
77 def validate_calibration(self) -> bool:
78 """Validate calibration quality"""
79 raise NotImplementedError
80
81
82class PressureTransducerCalibrator(SensorCalibrator):
83 """Calibrator for pressure transducers"""
84
85 def __init__(self, sensor_id: str):
86 super().__init__(sensor_id, "pressure_transducer")
87 self.pressure_range = (0.0, 15e6) # Pa
88 self.voltage_range = (0.0, 5.0) # V
89
90 def perform_calibration(self) -> CalibrationResult:
91 """Perform pressure transducer calibration using Bayesian regression"""
92 if len(self.calibration_points) < 5:
93 raise ValueError("Insufficient calibration points")
94
95 # Extract data
96 voltages = np.array([p.input_value for p in self.calibration_points])
97 pressures = np.array([p.reference_value for p in self.calibration_points])
98 uncertainties = np.array([p.uncertainty for p in self.calibration_points])
99
100 # Perform Bayesian regression (simplified implementation)
101 # In practice, this would implement the full Bayesian framework from the paper
102
103 # Linear fit: P = a + b*V + c*V^2 + environmental_terms
104 n_points = len(voltages)
105
106 # Design matrix for linear + quadratic + environmental terms
107 A = np.column_stack(
108 [
109 np.ones(n_points), # Constant term
110 voltages, # Linear term
111 voltages**2, # Quadratic term
112 voltages**3, # Cubic term
113 np.sqrt(voltages), # Square root term
114 np.log(1 + voltages), # Log term
115 ]
116 )
117
118 # Add environmental terms
119 for point in self.calibration_points:
120 env_terms = []
121 for key, value in point.environmental_conditions.items():
122 if key == "temperature":
123 env_terms.extend([value, value**2, value * point.input_value])
124 A = np.column_stack([A, np.array(env_terms)])
125
126 # Weight matrix (inverse of uncertainties)
127 W = np.diag(1.0 / (uncertainties**2 + 1e-6))
128
129 # Bayesian regression: θ = (A^T W A + Σ₀⁻¹)⁻¹ (A^T W b + Σ₀⁻¹ μ₀)
130 # Simplified version without priors
131 try:
132 cov_matrix = np.linalg.inv(A.T @ W @ A)
133 parameters = cov_matrix @ (A.T @ W @ pressures)
134 except np.linalg.LinAlgError:
135 logger.warning("Singular matrix, using regularized solution")
136 regularization = 1e-6 * np.eye(A.shape[1])
137 cov_matrix = np.linalg.inv(A.T @ W @ A + regularization)
138 parameters = cov_matrix @ (A.T @ W @ pressures)
139
140 # Calculate quality metrics
141 predicted_pressures = A @ parameters
142 residuals = pressures - predicted_pressures
143 rmse = np.sqrt(np.mean(residuals**2))
144 nrmse = rmse / (np.max(pressures) - np.min(pressures))
145
146 # Calculate coverage (95% confidence interval)
147 confidence_intervals = 1.96 * np.sqrt(np.diag(A @ cov_matrix @ A.T))
148 coverage_count = np.sum(np.abs(residuals) <= confidence_intervals)
149 coverage_95 = coverage_count / n_points
150
151 # Extrapolation confidence
152 voltage_range = np.max(voltages) - np.min(voltages)
153 extrapolation_confidence = min(1.0, 0.9 * np.exp(-voltage_range / 2.0))
154
155 quality_metrics = {
156 "rmse": float(rmse),
157 "nrmse": float(nrmse),
158 "coverage_95": float(coverage_95),
159 "extrapolation_confidence": float(extrapolation_confidence),
160 "condition_number": float(np.linalg.cond(A.T @ W @ A)),
161 "num_points": len(self.calibration_points),
162 }
163
164 return CalibrationResult(
165 sensor_id=self.sensor_id,
166 sensor_type=self.sensor_type,
167 calibration_points=self.calibration_points.copy(),
168 parameters={
169 "a0": float(parameters[0]),
170 "a1": float(parameters[1]),
171 "a2": float(parameters[2]),
172 "a3": float(parameters[3]),
173 "a4": float(parameters[4]),
174 "a5": float(parameters[5]),
175 },
176 covariance_matrix=cov_matrix.tolist(),
177 quality_metrics=quality_metrics,
178 calibration_time=time.time(),
179 )
180
181
182class RTDCalibrator(SensorCalibrator):
183 """Calibrator for RTD temperature sensors"""
184
185 def __init__(self, sensor_id: str):
186 super().__init__(sensor_id, "rtd_temperature")
187 self.temperature_range = (-50.0, 500.0) # °C
188 self.resistance_range = (80.0, 200.0) # Ohm
189
190 def perform_calibration(self) -> CalibrationResult:
191 """Perform RTD calibration"""
192 if len(self.calibration_points) < 3:
193 raise ValueError("Insufficient calibration points")
194
195 # Extract data
196 resistances = np.array([p.input_value for p in self.calibration_points])
197 temperatures = np.array([p.reference_value for p in self.calibration_points])
198
199 # Callendar-Van Dusen equation: R(T) = R0(1 + AT + BT² + C(T-100)T³)
200 # Simplified linear fit for demonstration
201 A_matrix = np.column_stack(
202 [np.ones(len(resistances)), resistances, resistances**2]
203 )
204
205 try:
206 parameters = np.linalg.lstsq(A_matrix, temperatures, rcond=None)[0]
207 residuals = temperatures - (A_matrix @ parameters)
208 rmse = np.sqrt(np.mean(residuals**2))
209 except np.linalg.LinAlgError:
210 logger.error("Failed to solve RTD calibration")
211 raise
212
213 return CalibrationResult(
214 sensor_id=self.sensor_id,
215 sensor_type=self.sensor_type,
216 calibration_points=self.calibration_points.copy(),
217 parameters={
218 "R0": float(parameters[0]),
219 "alpha": float(parameters[1]),
220 "beta": float(parameters[2]),
221 },
222 covariance_matrix=[[0.01, 0, 0], [0, 0.01, 0], [0, 0, 0.01]], # Simplified
223 quality_metrics={
224 "rmse": float(rmse),
225 "nrmse": float(rmse / (np.max(temperatures) - np.min(temperatures))),
226 "num_points": len(self.calibration_points),
227 },
228 calibration_time=time.time(),
229 )
230
231
232class CalibrationSequence:
233 """Manages the complete calibration sequence"""
234
235 def __init__(self, config_file: str):
236 self.config_file = config_file
237 self.calibrators: Dict[str, SensorCalibrator] = {}
238 self.results: Dict[str, CalibrationResult] = {}
239
240 def load_config(self):
241 """Load calibration configuration"""
242 # In practice, this would load from TOML config
243 logger.info(f"Loading configuration from {self.config_file}")
244
245 def add_sensor(self, sensor_id: str, sensor_type: str):
246 """Add a sensor to the calibration sequence"""
247 if sensor_type == "pressure_transducer":
248 calibrator = PressureTransducerCalibrator(sensor_id)
249 elif sensor_type == "rtd_temperature":
250 calibrator = RTDCalibrator(sensor_id)
251 else:
252 raise ValueError(f"Unsupported sensor type: {sensor_type}")
253
254 self.calibrators[sensor_id] = calibrator
255 logger.info(f"Added {sensor_type} sensor: {sensor_id}")
256
257 def run_pressure_transducer_calibration(
258 self,
259 sensor_id: str,
260 pressure_points: List[float],
261 voltage_readings: List[float],
262 environmental_conditions: Dict[str, float],
263 ):
264 """Run pressure transducer calibration"""
265 if sensor_id not in self.calibrators:
266 self.add_sensor(sensor_id, "pressure_transducer")
267
268 calibrator = self.calibrators[sensor_id]
269
270 logger.info(f"Starting pressure transducer calibration for {sensor_id}")
271
272 # Add calibration points
273 for pressure, voltage in zip(pressure_points, voltage_readings):
274 calibrator.add_calibration_point(
275 input_value=voltage,
276 reference_value=pressure,
277 environmental_conditions=environmental_conditions,
278 uncertainty=0.01, # 1% uncertainty
279 )
280 logger.info(f"Added calibration point: {voltage:.3f}V -> {pressure:.0f}Pa")
281
282 # Perform calibration
283 result = calibrator.perform_calibration()
284 self.results[sensor_id] = result
285
286 logger.info(f"Calibration complete for {sensor_id}")
287 logger.info(
288 f"Quality metrics: RMSE={result.quality_metrics['rmse']:.2f} Pa, "
289 f"NRMSE={result.quality_metrics['nrmse']:.4f}"
290 )
291
292 return result
293
294 def run_rtd_calibration(
295 self,
296 sensor_id: str,
297 temperature_points: List[float],
298 resistance_readings: List[float],
299 ):
300 """Run RTD calibration"""
301 if sensor_id not in self.calibrators:
302 self.add_sensor(sensor_id, "rtd_temperature")
303
304 calibrator = self.calibrators[sensor_id]
305
306 logger.info(f"Starting RTD calibration for {sensor_id}")
307
308 # Add calibration points
309 for temperature, resistance in zip(temperature_points, resistance_readings):
310 calibrator.add_calibration_point(
311 input_value=resistance,
312 reference_value=temperature,
313 environmental_conditions={"temperature": 25.0},
314 uncertainty=0.1, # 0.1°C uncertainty
315 )
316 logger.info(
317 f"Added calibration point: {resistance:.2f}Ω -> {temperature:.1f}°C"
318 )
319
320 # Perform calibration
321 result = calibrator.perform_calibration()
322 self.results[sensor_id] = result
323
324 logger.info(f"Calibration complete for {sensor_id}")
325 logger.info(f"Quality metrics: RMSE={result.quality_metrics['rmse']:.2f} °C")
326
327 return result
328
329 def save_results(self, output_dir: str):
330 """Save calibration results to files"""
331 output_path = Path(output_dir)
332 output_path.mkdir(parents=True, exist_ok=True)
333
334 for sensor_id, result in self.results.items():
335 filename = output_path / f"{sensor_id}_calibration.json"
336
337 # Convert to JSON-serializable format
338 result_dict = {
339 "sensor_id": result.sensor_id,
340 "sensor_type": result.sensor_type,
341 "calibration_time": result.calibration_time,
342 "parameters": result.parameters,
343 "covariance_matrix": result.covariance_matrix,
344 "quality_metrics": result.quality_metrics,
345 "calibration_points": [
346 {
347 "input_value": p.input_value,
348 "reference_value": p.reference_value,
349 "timestamp": p.timestamp,
350 "environmental_conditions": p.environmental_conditions,
351 "uncertainty": p.uncertainty,
352 }
353 for p in result.calibration_points
354 ],
355 }
356
357 with open(filename, "w") as f:
358 json.dump(result_dict, f, indent=2)
359
360 logger.info(f"Saved calibration results to {filename}")
361
362 def generate_report(self) -> str:
363 """Generate calibration report"""
364 report = []
365 report.append("=" * 60)
366 report.append("LIQUID ENGINE SENSOR CALIBRATION REPORT")
367 report.append("=" * 60)
368 report.append(f"Calibration Date: {time.strftime('%Y-%m-%d %H:%M:%S')}")
369 report.append(f"Total Sensors Calibrated: {len(self.results)}")
370 report.append("")
371
372 for sensor_id, result in self.results.items():
373 report.append(f"Sensor ID: {sensor_id}")
374 report.append(f"Type: {result.sensor_type}")
375 report.append(f"Calibration Points: {result.quality_metrics['num_points']}")
376 report.append(f"RMSE: {result.quality_metrics['rmse']:.4f}")
377 if "nrmse" in result.quality_metrics:
378 report.append(f"NRMSE: {result.quality_metrics['nrmse']:.4f}")
379 if "coverage_95" in result.quality_metrics:
380 report.append(
381 f"95% Coverage: {result.quality_metrics['coverage_95']:.2%}"
382 )
383 report.append("")
384
385 return "\n".join(report)
386
387
388def main():
389 parser = argparse.ArgumentParser(description="Liquid Engine Sensor Calibration")
390 parser.add_argument(
391 "--config", default="config_engine.toml", help="Configuration file"
392 )
393 parser.add_argument("--output-dir", default="calibrations", help="Output directory")
394 parser.add_argument("--sensor", help="Specific sensor to calibrate")
395 parser.add_argument(
396 "--interactive", action="store_true", help="Interactive calibration mode"
397 )
398
399 args = parser.parse_args()
400
401 # Initialize calibration sequence
402 sequence = CalibrationSequence(args.config)
403 sequence.load_config()
404
405 if args.interactive:
406 # Interactive calibration mode
407 print("Interactive Calibration Mode")
408 print("=" * 40)
409
410 # Example pressure transducer calibration
411 sensor_id = input("Enter sensor ID: ")
412 sensor_type = input("Enter sensor type (pressure_transducer/rtd_temperature): ")
413
414 if sensor_type == "pressure_transducer":
415 print("\nPressure Transducer Calibration")
416 print("Enter calibration points (voltage, pressure)")
417 print("Enter 'done' when finished")
418
419 voltage_readings = []
420 pressure_points = []
421
422 while True:
423 entry = input("Voltage (V), Pressure (Pa): ")
424 if entry.lower() == "done":
425 break
426 try:
427 voltage, pressure = map(float, entry.split(","))
428 voltage_readings.append(voltage)
429 pressure_points.append(pressure)
430 except ValueError:
431 print("Invalid input. Use format: voltage, pressure")
432
433 environmental_conditions = {"temperature": 25.0, "humidity": 50.0}
434 sequence.run_pressure_transducer_calibration(
435 sensor_id, pressure_points, voltage_readings, environmental_conditions
436 )
437
438 else:
439 # Automated calibration with example data
440 logger.info("Running automated calibration sequence")
441
442 # Example pressure transducer calibration
443 pressure_points = [0.0, 2e6, 5e6, 8e6, 10e6, 12e6, 15e6] # Pa
444 voltage_readings = [0.5, 1.2, 2.1, 2.8, 3.2, 3.6, 4.2] # V
445 environmental_conditions = {"temperature": 25.0, "humidity": 50.0}
446
447 sequence.run_pressure_transducer_calibration(
448 "PT_CHAMBER", pressure_points, voltage_readings, environmental_conditions
449 )
450
451 # Example RTD calibration
452 temperature_points = [0.0, 25.0, 50.0, 100.0, 150.0, 200.0] # °C
453 resistance_readings = [100.0, 109.7, 119.4, 138.5, 157.3, 175.8] # Ohm
454
455 sequence.run_rtd_calibration(
456 "RTD_CHAMBER_WALL", temperature_points, resistance_readings
457 )
458
459 # Save results and generate report
460 sequence.save_results(args.output_dir)
461 report = sequence.generate_report()
462 print(report)
463
464 # Save report to file
465 with open(Path(args.output_dir) / "calibration_report.txt", "w") as f:
466 f.write(report)
467
468 logger.info("Calibration sequence complete")
469
470
471if __name__ == "__main__":
472 main()