Skip to content

Commit 196cc7f

Browse files
authored
Merge pull request #160 from ImperialCollegeLondon/task/improve-imap-event
task: Improve IMAP mode change events detection
2 parents b1870db + d50ced6 commit 196cc7f

File tree

9 files changed

+165
-131
lines changed

9 files changed

+165
-131
lines changed

resources/release-notes.md

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,4 @@
1-
## App
2-
3-
- (IMAP) CPT view settings must match pattern only once finished typing (not while typing)
4-
51
## Software
62

7-
- (All) Force figures to appear in the figures container in MATLAB R2025a and above
8-
- (IMAP) Events recorded before the start of a test are discarded
9-
10-
## Project
11-
12-
- Update code analyzer configuration to 1.0.1 (remove regex for enumeration naming)
3+
- (All) Add option to specify PNG resolution when saving figures
4+
- (IMAP) Improve estimation of mode change events

src/data/+mag/+event/Event.m

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,8 @@
170170
locTimedCommand = ~ismissing(eventtableThis.Duration) & (eventtableThis.Duration ~= 0);
171171
idxTimedCommand = find(locTimedCommand);
172172

173+
autoEventtable = this.generateEmptyEventtable();
174+
173175
for i = idxTimedCommand(:)'
174176

175177
autoEvent = eventtableThis(i, :);
@@ -184,10 +186,16 @@
184186
autoEvent.Label = compose("Burst (%d, %d)", autoEvent.PrimaryBurstRate, autoEvent.SecondaryBurstRate);
185187
end
186188

187-
eventtableThis = [eventtableThis; autoEvent]; %#ok<AGROW>
189+
% If the time of automated event is after the next event,
190+
% correct it.
191+
if (i < height(eventtableThis)) && (autoEvent.Time >= eventtableThis(i + 1, :).Time)
192+
autoEvent.Time = eventtableThis(i + 1, :).Time - mag.time.Constant.Eps;
193+
end
194+
195+
autoEventtable = [autoEventtable; autoEvent]; %#ok<AGROW>
188196
end
189197

190-
eventtableThis = sortrows(eventtableThis);
198+
eventtableThis = sortrows([eventtableThis; autoEventtable]);
191199
eventtableThis = eventtable(eventtableThis, EventLabelsVariable = "Label");
192200

193201
eventtableThis = this.fillMissingDetails(eventtableThis);

src/mission/imap/+mag/+imap/@Analysis/Analysis.m

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -251,8 +251,8 @@ function load(this)
251251

252252
arguments (Input)
253253
this (1, 1) mag.imap.Analysis
254-
options.PrimaryPattern (1, :) double = [2, 64, 4, 64, 4, 128]
255-
options.SecondaryPattern (1, :) double = [2, 8, 1, 64, 4, 128]
254+
options.PrimaryPattern (1, :) double = [2, 64, 2, 4, 64, 4, 4, 128]
255+
options.SecondaryPattern (1, :) double = [2, 8, 2, 1, 64, 1, 4, 128]
256256
end
257257

258258
arguments (Output)
@@ -457,6 +457,12 @@ function load(this)
457457
end
458458
end
459459

460+
methods (Hidden, Static)
461+
462+
% FINDMODECHANGES Improve estimate of mode change times.
463+
eventData = findModeChanges(data, eventData, name)
464+
end
465+
460466
methods (Hidden, Sealed, Static)
461467

462468
function loadedObject = loadobj(object)
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
function events = findModeChanges(data, events, name)
2+
3+
arguments (Input)
4+
data timetable
5+
events timetable
6+
name (1, 1) string
7+
end
8+
9+
arguments (Output)
10+
events timetable
11+
end
12+
13+
timeColumn = data.Properties.DimensionNames{1};
14+
15+
% If there no events were detected, find mode changes by looking at
16+
% timestamp cadence.
17+
if isempty(events)
18+
19+
data = sortrows(data);
20+
21+
% Find changes in timestamp cadence.
22+
t = data.(timeColumn);
23+
dt = milliseconds(diff(t));
24+
25+
idxRemove = find(ismissing(dt) | (dt < 1) | (dt > 1000)) + 1;
26+
idxRemove(idxRemove > height(data)) = height(data);
27+
28+
t(idxRemove) = [];
29+
dt = milliseconds(diff(t));
30+
31+
idxChange = findchangepts(dt, MinThreshold = 1);
32+
idxChange(diff(idxChange) == 1) = [];
33+
34+
% Correct for data that was filtered out.
35+
for i = idxRemove'
36+
37+
locUpdate = idxChange >= i;
38+
idxChange(locUpdate) = idxChange(locUpdate) + 1;
39+
end
40+
41+
% Create event details.
42+
idxChange = [1; idxChange; height(data) + 1];
43+
44+
for i = 1:(numel(idxChange) - 1)
45+
46+
d = data(idxChange(i):(idxChange(i+1) - 1), :);
47+
f = round(1 / seconds(mode(diff(d.(timeColumn)))));
48+
49+
if f < 8
50+
m = "Normal";
51+
else
52+
m = "Burst";
53+
end
54+
55+
e = struct2table(struct(Mode = m, ...
56+
DataFrequency = f, ...
57+
PacketFrequency = NaN, ...
58+
Duration = 0, ...
59+
Range = NaN, ...
60+
Label = compose("%s %s (%d)", name, m, f), ...
61+
Reason = "Command"));
62+
t = table2timetable(e, RowTimes = d.(timeColumn)(1));
63+
64+
events = [events; eventtable(t, EventLabelsVariable = "Label")]; %#ok<AGROW>
65+
end
66+
67+
% Remove duplicate events.
68+
events(find(diff(events.DataFrequency) == 0) + 1, :) = [];
69+
else
70+
71+
searchWindow = seconds(30);
72+
data = sortrows(data);
73+
74+
% Update timestamps for mode changes.
75+
idxMode = find([true; diff(events.DataFrequency) ~= 0] & ~ismissing(events.DataFrequency) & ~ismissing(events.Duration));
76+
77+
for i = 1:numel(idxMode)
78+
79+
e = idxMode(i);
80+
81+
if i == 1
82+
83+
events.Time(e) = data.t(1);
84+
continue;
85+
end
86+
87+
% Find window around event and compute actual timestamp
88+
% difference.
89+
t = events.Time(e);
90+
eventWindow = data(withtol(t, searchWindow), :);
91+
92+
% Remove artificial timestamps.
93+
eventWindow = eventWindow(eventWindow.quality.isScience(), :);
94+
95+
% Remove data belonging to the previous event (whose estimate
96+
% was already improved).
97+
eventWindow(eventWindow.t < events.Time(idxMode(i - 1)), :) = [];
98+
99+
if isempty(eventWindow)
100+
continue;
101+
end
102+
103+
% Find time differences and filter out ones that are not
104+
% conventional.
105+
dt = seconds(diff(eventWindow.(timeColumn)));
106+
107+
idxOutlier = find(~ismembertol(dt, [0, 1 ./ 2.^(0:7)], 1e-3));
108+
dt(idxOutlier) = dt(idxOutlier - 1);
109+
110+
ddt = diff(dt);
111+
112+
if all(ddt == 0)
113+
events.Time(e) = eventWindow.t(1);
114+
elseif all(ddt < 1e-3)
115+
116+
if seconds(t - eventWindow.t(1)) < 1.5
117+
events.Time(e) = eventWindow.t(1);
118+
else
119+
120+
[~, idxMin] = min(abs(t - eventWindow.(timeColumn)));
121+
events.Time(e) = eventWindow.(timeColumn)(idxMin);
122+
end
123+
else
124+
125+
[~, idxChange] = max(ddt, [], ComparisonMethod = "abs");
126+
events.Time(e) = eventWindow.(timeColumn)(idxChange + 1);
127+
end
128+
end
129+
end
130+
end

src/mission/imap/+mag/+imap/@Analysis/generateEventTable.m

Lines changed: 3 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
function eventTable = generateEventTable(~, data, sensorEvents)
1+
function eventTable = generateEventTable(this, data, sensorEvents)
22

33
arguments (Input)
4-
~
4+
this (1, 1) mag.imap.Analysis
55
data (1, 1) mag.Science
66
sensorEvents timetable
77
end
@@ -35,7 +35,7 @@
3535
sensorEvents = updateRampModeTimestamps(sensorEvents, data.Data);
3636

3737
% Improve estimates of mode changes.
38-
sensorEvents = findModeChanges(data.Data, sensorEvents, sensorName);
38+
sensorEvents = this.findModeChanges(data.Data, sensorEvents, sensorName);
3939

4040
% Basic structure for events table.
4141
eventTable = mag.Science.generateEmptyEventtable();
@@ -164,108 +164,6 @@
164164
end
165165
end
166166

167-
function events = findModeChanges(data, events, name)
168-
169-
timeColumn = data.Properties.DimensionNames{1};
170-
171-
% If there no events were detected, find mode changes by looking at
172-
% timestamp cadence.
173-
if isempty(events)
174-
175-
data = sortrows(data);
176-
177-
% Find changes in timestamp cadence.
178-
t = data.(timeColumn);
179-
dt = milliseconds(diff(t));
180-
181-
idxRemove = find(ismissing(dt) | (dt < 1) | (dt > 1000)) + 1;
182-
idxRemove(idxRemove > height(data)) = height(data);
183-
184-
t(idxRemove) = [];
185-
dt = milliseconds(diff(t));
186-
187-
idxChange = findchangepts(dt, MinThreshold = 1);
188-
idxChange(diff(idxChange) == 1) = [];
189-
190-
% Correct for data that was filtered out.
191-
for i = idxRemove'
192-
193-
locUpdate = idxChange >= i;
194-
idxChange(locUpdate) = idxChange(locUpdate) + 1;
195-
end
196-
197-
% Create event details.
198-
idxChange = [1; idxChange; height(data) + 1];
199-
200-
for i = 1:(numel(idxChange) - 1)
201-
202-
d = data(idxChange(i):(idxChange(i+1) - 1), :);
203-
f = round(1 / seconds(mode(diff(d.(timeColumn)))));
204-
205-
if f < 8
206-
m = "Normal";
207-
else
208-
m = "Burst";
209-
end
210-
211-
e = struct2table(struct(Mode = m, ...
212-
DataFrequency = f, ...
213-
PacketFrequency = NaN, ...
214-
Duration = 0, ...
215-
Range = NaN, ...
216-
Label = compose("%s %s (%d)", name, m, f), ...
217-
Reason = "Command"));
218-
t = table2timetable(e, RowTimes = d.(timeColumn)(1));
219-
220-
events = [events; eventtable(t, EventLabelsVariable = "Label")]; %#ok<AGROW>
221-
end
222-
223-
% Remove duplicate events.
224-
events(find(diff(events.DataFrequency) == 0) + 1, :) = [];
225-
else
226-
227-
searchWindow = seconds(30);
228-
data = sortrows(data);
229-
230-
% Update timestamps for mode changes.
231-
idxMode = find([true; diff(events.DataFrequency) ~= 0] & ~ismissing(events.DataFrequency) & ~ismissing(events.Duration));
232-
233-
for i = idxMode'
234-
235-
% Find window around event and compute actual timestamp difference.
236-
t = events.Time(i);
237-
eventWindow = data(withtol(t, searchWindow), :);
238-
239-
% Remove artificial timestamps.
240-
eventWindow = eventWindow(eventWindow.quality.isScience(), :);
241-
242-
if isempty(eventWindow)
243-
continue;
244-
end
245-
246-
dt = seconds(diff(eventWindow.(timeColumn)));
247-
ddt = diff(dt);
248-
249-
if all(ddt == 0)
250-
events.Time(i) = eventWindow.t(1);
251-
elseif all(ddt < 1e-3)
252-
253-
if seconds(t - eventWindow.t(1)) < 1.5
254-
events.Time(i) = eventWindow.t(1);
255-
else
256-
257-
[~, idxMin] = min(abs(t - eventWindow.(timeColumn)));
258-
events.Time(i) = eventWindow.(timeColumn)(idxMin);
259-
end
260-
else
261-
262-
[~, idxChange] = max(diff(dt), [], ComparisonMethod = "abs");
263-
events.Time(i) = eventWindow.(timeColumn)(idxChange + 1);
264-
end
265-
end
266-
end
267-
end
268-
269167
function [rangeTable, events] = findRangeChanges(ranges, events, sensorName)
270168

271169
% Find range changes.

src/transform/+mag/+transform/Spectrogram.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@
156156
window = [];
157157
end
158158

159-
if overlap >= numel(y)
159+
if overlap >= (numel(y) / 2)
160160
overlap = [];
161161
end
162162

src/visualize/+mag/+graphics/savePlots.m

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ function savePlots(figures, location, options)
44
arguments
55
figures (1, :) matlab.ui.Figure
66
location (1, 1) string = "results"
7+
options.Resolution (1, 1) double = 300
78
options.ColonReplacement (1, 1) string = ""
89
options.DotReplacement (1, 1) string = "_"
910
options.SlashReplacement (1, 1) string = "_"
@@ -21,22 +22,21 @@ function savePlots(figures, location, options)
2122

2223
mustBeFolder(location);
2324

24-
for f = figures
25+
figures = figures(isvalid(figures));
2526

26-
if isvalid(f)
27+
for f = figures
2728

28-
name = replace(f.Name, [":", ".", "/", "\", """"], [options.ColonReplacement, options.DotReplacement, options.SlashReplacement, options.SlashReplacement, options.QuoteReplacement]);
29-
name = fullfile(location, name);
29+
name = replace(f.Name, [":", ".", "/", "\", """"], [options.ColonReplacement, options.DotReplacement, options.SlashReplacement, options.SlashReplacement, options.QuoteReplacement]);
30+
name = fullfile(location, name);
3031

31-
exportgraphics(f, fullfile(name + ".png"), Resolution = 300);
32+
exportgraphics(f, fullfile(name + ".png"), Resolution = options.Resolution);
3233

33-
if options.SaveAsFig
34+
if options.SaveAsFig
3435

35-
try
36-
savefig(f, name);
37-
catch exception
38-
warning("Could not save figure ""%s"":\n%s", f.Name, exception.message);
39-
end
36+
try
37+
savefig(f, name);
38+
catch exception
39+
warning("Could not save figure ""%s"":\n%s", f.Name, exception.message);
4040
end
4141
end
4242
end
1.42 KB
Binary file not shown.
Binary file not shown.

0 commit comments

Comments
 (0)