-
Notifications
You must be signed in to change notification settings - Fork 1
GPS time correlation on log file parsing #71
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
b509f39
correlate function to try to convert daq timestamps to real time
LelsersLasers 20e9b03
Switching to a fn because we need to map every timestamp in the csv t…
LelsersLasers 627ce39
also return the chunked because it took ownernship
LelsersLasers c54ff6f
using time_correlate_chunks
LelsersLasers 9dd5094
rename
LelsersLasers c1e725b
update table to use row 0 as correlated time and row 1 as daq time
LelsersLasers a434c5b
using first row real time as log name when possible
LelsersLasers eecc219
fmt
LelsersLasers 5f49c59
year is based from 2000?
LelsersLasers ea303ed
skip timestamps with weird years (init not finished yet)
LelsersLasers 95e9d1a
only hr min, s, ns in parsed csv
LelsersLasers aff2793
fix output?
LelsersLasers 240cd7d
trying linear regression instead?
LelsersLasers e74f828
fmt
LelsersLasers abad09d
fix warnings
LelsersLasers 499a0c3
fix output for real
LelsersLasers 5ee2c09
avoid unwrap
LelsersLasers 1791fdf
remove debug print
LelsersLasers f6c98a3
adjust table output format
LelsersLasers 965b6c2
increase year bound
LelsersLasers efbc62c
update comments
LelsersLasers File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,244 @@ | ||
| use chrono::Datelike as _; | ||
| use chrono::TimeZone as _; | ||
|
|
||
| use crate::daq_log_parse::parse::ParsedMessage; | ||
|
|
||
| pub struct CorrelationFunction { | ||
| /// real_time ~= slope * log_time_ms + intercept_ms | ||
| /// | ||
| /// Stored as: | ||
| /// unix_ms = slope * log_ts_ms + intercept_ms | ||
| slope: f64, | ||
| intercept_ms: f64, | ||
| } | ||
|
|
||
| impl CorrelationFunction { | ||
| pub fn correlate(&self, log_ts: u32) -> Option<chrono::DateTime<chrono::Local>> { | ||
| let unix_ms = self.slope * log_ts as f64 + self.intercept_ms; | ||
|
|
||
| match chrono::DateTime::from_timestamp_millis(unix_ms.round() as i64) { | ||
| Some(dt) => Some(dt.with_timezone(&chrono::Local)), | ||
| None => { | ||
| log::error!( | ||
| "Correlated time {} ms for log time {} ms is out of range for chrono::DateTime", | ||
| unix_ms, | ||
| log_ts | ||
| ); | ||
| None | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| pub struct CorrelationChunkResult { | ||
| pub parsed_msgs: Vec<ParsedMessage>, | ||
| pub correlation_fn: Option<CorrelationFunction>, | ||
| } | ||
|
|
||
| pub fn time_correlate_chunks(chunks: Vec<Vec<ParsedMessage>>) -> Vec<CorrelationChunkResult> { | ||
| chunks.into_iter().map(time_correlate_chunk).collect() | ||
| } | ||
|
|
||
| impl CorrelationChunkResult { | ||
| pub fn uncorrelated_new(chunk: Vec<ParsedMessage>) -> Self { | ||
| Self { | ||
| parsed_msgs: chunk, | ||
| correlation_fn: None, | ||
| } | ||
| } | ||
|
|
||
| pub fn correlated_new(chunk: Vec<ParsedMessage>, correlation_fn: CorrelationFunction) -> Self { | ||
| Self { | ||
| parsed_msgs: chunk, | ||
| correlation_fn: Some(correlation_fn), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| pub fn sig_to_value(dsv: &can_decode::DecodedSignalValue) -> u64 { | ||
| match &dsv { | ||
| can_decode::DecodedSignalValue::Numeric(v) => v.round() as u64, | ||
| can_decode::DecodedSignalValue::Enum(v, _) => *v as u64, | ||
| } | ||
| } | ||
|
|
||
| pub fn time_correlate_chunk(chunk: Vec<ParsedMessage>) -> CorrelationChunkResult { | ||
| // Idea: in the chunk, look for GPS messages which have both a timestamp and a corresponding real time | ||
| // Use those to create a mapping from the log's timestamps to real time, and use that mapping to convert | ||
| // all messages in the chunk to have real timestamps. | ||
| // If the correlation is successful, we return the orginal messages along with a correlation function. | ||
| // If we can't find any GPS messages, or if the correlation fails, return just the original messages. | ||
|
|
||
| // First, find all GPS messages and extract their timestamps and real times | ||
| let mut gps_points = Vec::new(); | ||
| for msg in &chunk { | ||
| if msg.decoded.name == "gps_time" { | ||
| let millisecond = msg | ||
| .decoded | ||
| .signals | ||
| .get("millisecond") | ||
| .map(|sig| sig_to_value(&sig.value)); | ||
| let second = msg | ||
| .decoded | ||
| .signals | ||
| .get("second") | ||
| .map(|sig| sig_to_value(&sig.value)); | ||
| let minute = msg | ||
| .decoded | ||
| .signals | ||
| .get("minute") | ||
| .map(|sig| sig_to_value(&sig.value)); | ||
| let hour = msg | ||
| .decoded | ||
| .signals | ||
| .get("hour") | ||
| .map(|sig| sig_to_value(&sig.value)); | ||
| let day = msg | ||
| .decoded | ||
| .signals | ||
| .get("day") | ||
| .map(|sig| sig_to_value(&sig.value)); | ||
| let month = msg | ||
| .decoded | ||
| .signals | ||
| .get("month") | ||
| .map(|sig| sig_to_value(&sig.value)); | ||
| let year = msg | ||
| .decoded | ||
| .signals | ||
| .get("year") | ||
| .map(|sig| sig_to_value(&sig.value)); | ||
|
|
||
| if let (Some(ms), Some(s), Some(min), Some(h), Some(d), Some(mon), Some(y)) = | ||
| (millisecond, second, minute, hour, day, month, year) | ||
| { | ||
| let full_year = if y < 100 { 2000 + y as i32 } else { y as i32 }; | ||
| // Construct a chrono::DateTime from the extracted values | ||
| if let Some(dt) = chrono::NaiveDate::from_ymd_opt(full_year, mon as u32, d as u32) | ||
| .and_then(|date| { | ||
| date.and_hms_milli_opt(h as u32, min as u32, s as u32, ms as u32) | ||
| }) | ||
| { | ||
| let dt_utc = chrono::Utc.from_utc_datetime(&dt); | ||
| let dt_local = chrono::DateTime::<chrono::Local>::from(dt_utc); | ||
|
|
||
| let current_year = chrono::Local::now().year(); | ||
| if dt_local.year() < current_year - 2 || dt_local.year() > current_year + 2 { | ||
| log::warn!( | ||
| "GPS message at {} ms has suspicious year value {}, skipping", | ||
| msg.timestamp, | ||
| dt_local.year() | ||
| ); | ||
| continue; | ||
| } | ||
|
|
||
| gps_points.push((msg.timestamp, dt_local)); | ||
| } else { | ||
| log::error!( | ||
| "GPS message at {} ms has invalid date/time values, skipping", | ||
| msg.timestamp | ||
| ); | ||
| continue; | ||
| } | ||
| } else { | ||
| log::error!( | ||
| "GPS message at {} ms is missing some time signals, skipping", | ||
| msg.timestamp | ||
| ); | ||
| continue; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if gps_points.is_empty() { | ||
| // No GPS points found, can't correlate | ||
| return CorrelationChunkResult::uncorrelated_new(chunk); | ||
| } | ||
|
|
||
| // Attempt to fit a line to the GPS points to find the correlation function | ||
| let points: Vec<Point> = gps_points | ||
| .iter() | ||
| .map(|(log_ts, real_ts)| Point { | ||
| x: *log_ts as f64, | ||
| y: real_ts.timestamp_millis() as f64, | ||
| }) | ||
| .collect(); | ||
| let (slope, intercept) = match linear_regression(&points) { | ||
| Some(v) => v, | ||
| None => { | ||
| log::error!("Failed to refit correlation line"); | ||
| return CorrelationChunkResult::uncorrelated_new(chunk); | ||
| } | ||
| }; | ||
|
|
||
| // Print debug info about the correlation quality | ||
| let rms_error_ms = { | ||
| let mse = points | ||
| .iter() | ||
| .map(|p| { | ||
| let predicted = slope * p.x + intercept; | ||
| let error = p.y - predicted; | ||
| error * error | ||
| }) | ||
| .sum::<f64>() | ||
| / points.len() as f64; | ||
|
|
||
| mse.sqrt() | ||
| }; | ||
| log::info!( | ||
| "GPS correlation successful: slope={:.9}, intercept_ms={:.3}, rms_error_ms={:.2}, points={}", | ||
| slope, | ||
| intercept, | ||
| rms_error_ms, | ||
| points.len() | ||
| ); | ||
|
|
||
| CorrelationChunkResult::correlated_new( | ||
| chunk, | ||
| CorrelationFunction { | ||
| slope, | ||
| intercept_ms: intercept, | ||
| }, | ||
| ) | ||
| } | ||
|
|
||
| struct Point { | ||
| x: f64, // log timestamp ms | ||
| y: f64, // unix timestamp ms | ||
| } | ||
|
|
||
| /// Least squares linear regression. | ||
| /// | ||
| /// Fits: | ||
| /// | ||
| /// y = slope * x + intercept | ||
| fn linear_regression(points: &[Point]) -> Option<(f64, f64)> { | ||
| if points.len() < 2 { | ||
| return None; | ||
| } | ||
|
|
||
| let n = points.len() as f64; | ||
|
|
||
| let mut sum_x = 0.0; | ||
| let mut sum_y = 0.0; | ||
| let mut sum_xy = 0.0; | ||
| let mut sum_x2 = 0.0; | ||
|
|
||
| for p in points { | ||
| sum_x += p.x; | ||
| sum_y += p.y; | ||
| sum_xy += p.x * p.y; | ||
| sum_x2 += p.x * p.x; | ||
| } | ||
|
|
||
| let denom = n * sum_x2 - sum_x * sum_x; | ||
|
|
||
| if denom.abs() < 1e-9 { | ||
| return None; | ||
| } | ||
|
|
||
| let slope = (n * sum_xy - sum_x * sum_y) / denom; | ||
| let intercept = (sum_y - slope * sum_x) / n; | ||
|
|
||
| Some((slope, intercept)) | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| pub mod consts; | ||
| pub mod correlate; | ||
| pub mod parse; | ||
| pub mod table; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.