Skip to content

Commit

Permalink
Merge pull request #108 from ImperialCollegeLondon/fix/spectrum-psd-o…
Browse files Browse the repository at this point in the history
…ne-sensor

fix: Spectrum and PSD support one sensor only
  • Loading branch information
mfacchinelli authored Feb 10, 2025
2 parents 633a5e0 + 7773a87 commit b812174
Show file tree
Hide file tree
Showing 11 changed files with 454 additions and 78 deletions.
2 changes: 1 addition & 1 deletion .env
Original file line number Diff line number Diff line change
@@ -1 +1 @@
MAG_DATA_VISUALIZATION_VERSION=7.2.0
MAG_DATA_VISUALIZATION_VERSION=7.2.1
24 changes: 3 additions & 21 deletions app/core/+mag/+app/+manage/ToolbarManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -158,28 +158,10 @@ function debugToggleToolOff(this)

function helpPushToolClicked(this)

% Show progress bar.
closeProgressBar = this.App.AppNotificationHandler.overlayProgressBar("Generating diagnostics..."); %#ok<NASGU>
web("https://github.com/ImperialCollegeLondon/MAG-Data-Visualization-Toolbox/issues/new/choose");

% Instantiate variables to save.
model = this.App.Model;

% Create folder to zip.
statusFolder = tempname();
zipFolder = statusFolder + ".zip";

mkdir(statusFolder);
deleteFolder = onCleanup(@() rmdir(statusFolder, "s"));

% Create MAT file with variables.
save(fullfile(statusFolder, "data.mat"), "model");
exportapp(this.App.UIFigure, fullfile(statusFolder, "app.png"));

zip(zipFolder, statusFolder);
clipboard("copy", zipFolder);

% Show dialog.
this.App.AppNotificationHandler.displayAlert(compose("Share ZIP file ""%s""" + newline() + "with the developer. Path copied to clipboard.", zipFolder), "Share Diagnostics", "info");
this.App.AppNotificationHandler.displayAlert("Create issue on GitHub to share feedback, report issues and ask questions.", ...
"Create GitHub Issue", "info");
end
end
end
20 changes: 2 additions & 18 deletions resources/release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,8 @@
## App

- Add option to set breakpoint for error identifier (as well as error source)
- Replace diagnostics sharing with link to GitHub for creating an issue when help button is pressed

## Software

- Add support for IMAP I-ALiRT data with one sensor only
- Add range and 24-bit scaling for HelioSwarm science data
- Use SPICE to convert time from MET (equivalent to SCLK, but in seconds) to UTC
- Remove unnecessary `fullfile`s in `mag.imap.Analysis` default values
- Remove unnecessary description of processing steps
- Fix issue with `mag.imap.view.Field` not coping with one sensor science data missing, but its temperature being available

## Build

- Use `matlab.addons.toolbox.ToolboxOptions` to replace toolbox template

## CI

- Separate CI tests and packaging into separate GitHub workflows
- `main` branch is no longer "special" and is not packaged up on push
- Tags can be used to create new releases
- Install support packages required for MATLAB tests
- IMAP spectrogram and PSD support only one sensor
83 changes: 63 additions & 20 deletions src/mission/imap/+mag/+imap/+view/PSD.m
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
Duration (1, 1) duration = hours(1)
end

properties (Hidden)
% TRANSFORMATION Transformation for calculating PSD.
Transformation (1, 1) mag.transform.PSD = mag.transform.PSD()
end

methods

function this = PSD(results, options)
Expand All @@ -29,42 +34,80 @@ function visualize(this)
primary = this.Results.Primary;
secondary = this.Results.Secondary;

% PSD.
if ismissing(this.Start) || ~isbetween(this.Start, primary.Time(1), primary.Time(end))
psdStart = primary.Time(1);
numPSDs = 0;
yLine = mag.graphics.chart.Line(Axis = "y", Value = 0.01, Style = "--", Label = "10 pT Hz^{-0.5}");

% Primary.
if ~isempty(primary) && primary.HasData

numPSDs = numPSDs + 1;

primaryPSD = this.computePSD(primary);
primaryCharts = this.getPSDCharts(primaryPSD, primarySensor, yLine);
else
psdStart = this.Start;
primaryCharts = {};
end

if (this.Duration > (primary.Time(end) - psdStart))
psdDuration = primary.Time(end) - psdStart;
% Secondary.
if ~isempty(secondary) && secondary.HasData

numPSDs = numPSDs + 1;

secondaryPSD = this.computePSD(secondary);
secondaryCharts = this.getPSDCharts(secondaryPSD, secondarySensor, yLine);
else
psdDuration = this.Duration;
secondaryCharts = {};
end

psdPrimary = mag.psd(primary, Start = psdStart, Duration = psdDuration);
psdSecondary = mag.psd(secondary, Start = psdStart, Duration = psdDuration);

yLine = mag.graphics.chart.Line(Axis = "y", Value = 0.01, Style = "--", Label = "10 pT Hz^{-0.5}");
% Plot.
if isempty(primaryCharts) && isempty(secondaryCharts)
return;
end

this.Figures = this.Factory.assemble( ...
psdPrimary, mag.graphics.style.Default(Title = compose("%s PSD", primarySensor), XLabel = this.FLabel, YLabel = this.PSDLabel, XScale = "log", YScale = "log", Legend = ["x", "y", "z"], Charts = [mag.graphics.chart.Plot(XVariable = "Frequency", YVariables = ["X", "Y", "Z"]), yLine]), ...
psdSecondary, mag.graphics.style.Default(Title = compose("%s PSD", secondarySensor), XLabel = this.FLabel, YLabel = this.PSDLabel, XScale = "log", YScale = "log", Legend = ["x", "y", "z"], Charts = [mag.graphics.chart.Plot(XVariable = "Frequency", YVariables = ["X", "Y", "Z"]), yLine]), ...
Title = this.getPSDFigureTitle(primary, secondary, psdStart, psdDuration), ...
Name = this.getPSDFigureName(primary, secondary, psdStart), ...
Arrangement = [2, 1], ...
primaryCharts{:}, ...
secondaryCharts{:}, ...
Title = this.getPSDFigureTitle(primary, secondary), ...
Name = this.getPSDFigureName(primary, secondary), ...
Arrangement = [numPSDs, 1], ...
WindowState = "maximized");
end
end

methods (Access = private)

function value = getPSDFigureTitle(this, primary, secondary, psdStart, psdDuration)
value = compose("Start: %s - Duration: %s - (%s, %s)", this.date2str(psdStart), psdDuration, this.getDataFrequency(primary.MetaData), this.getDataFrequency(secondary.MetaData));
function psd = computePSD(this, science)

transformation = this.Transformation;

if ismissing(this.Start) || ~isbetween(this.Start, science.Time(1), science.Time(end))
transformation.Start = science.Time(1);
else
transformation.Start = this.Start;
end

if (this.Duration > (science.Time(end) - transformation.Start))
transformation.Duration = science.Time(end) - psdStart;
else
transformation.Duration = this.Duration;
end

psd = transformation.apply(science);
end

function charts = getPSDCharts(this, psd, sensor, yLine)

charts = {psd, ...
mag.graphics.style.Default(Title = compose("%s PSD", sensor), XLabel = this.FLabel, YLabel = this.PSDLabel, XScale = "log", YScale = "log", Legend = ["x", "y", "z"], ...
Charts = [mag.graphics.chart.Plot(XVariable = "Frequency", YVariables = ["X", "Y", "Z"]), yLine])};
end

function value = getPSDFigureTitle(this, primary, secondary)
value = compose("Start: %s - Duration: %s - (%s, %s)", this.date2str(this.Transformation.Start), this.Transformation.Duration, this.getDataFrequency(primary.MetaData), this.getDataFrequency(secondary.MetaData));
end

function value = getPSDFigureName(this, primary, secondary, psdStart)
value = compose("%s (%s, %s) PSD (%s)", primary.MetaData.getDisplay("Mode"), this.getDataFrequency(primary.MetaData), this.getDataFrequency(secondary.MetaData), this.date2str(psdStart));
function value = getPSDFigureName(this, primary, secondary)
value = compose("%s (%s, %s) PSD (%s)", primary.MetaData.getDisplay("Mode"), this.getDataFrequency(primary.MetaData), this.getDataFrequency(secondary.MetaData), this.date2str(this.Transformation.Start));
end
end
end
66 changes: 51 additions & 15 deletions src/mission/imap/+mag/+imap/+view/Spectrogram.m
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@
Overlap (1, 1) double = missing()
end

properties (Hidden)
% TRANSFORMATION Transformation for calculating spectrogram.
Transformation (1, 1) mag.transform.Spectrogram = mag.transform.Spectrogram()
end

methods

function this = Spectrogram(results, options)
Expand All @@ -36,23 +41,41 @@ function visualize(this)
primary = this.Results.Primary;
secondary = this.Results.Secondary;

% Spectrogram.
primarySpectrum = mag.spectrogram(primary, FrequencyLimits = this.FrequencyLimits, FrequencyPoints = this.FrequencyPoints, ...
Normalize = this.Normalize, Window = this.Window, Overlap = this.Overlap);
numSpectra = 0;

% Primary.
if ~isempty(primary) && primary.HasData

numSpectra = numSpectra + 1;

secondarySpectrum = mag.spectrogram(secondary, FrequencyLimits = this.FrequencyLimits, FrequencyPoints = this.FrequencyPoints, ...
Normalize = this.Normalize, Window = this.Window, Overlap = this.Overlap);
primarySpectrum = this.computeSpectrogram(primary);
primaryCharts = this.getSpectrogramCharts(primary, primarySpectrum, primarySensor, "left");
else
primaryCharts = {};
end

% Secondary.
if ~isempty(secondary) && secondary.HasData

% Field and spectrogram.
primaryCharts = this.getFrequencyCharts(primary, primarySpectrum, primarySensor, "left");
secondaryCharts = this.getFrequencyCharts(secondary, secondarySpectrum, secondarySensor, "right");
numSpectra = numSpectra + 1;

secondarySpectrum = this.computeSpectrogram(secondary);
secondaryCharts = this.getSpectrogramCharts(secondary, secondarySpectrum, secondarySensor, "right");
else
secondaryCharts = {};
end

% Plot.
if isempty(primaryCharts) && isempty(secondaryCharts)
return;
end

this.Figures = this.Factory.assemble( ...
primaryCharts{:}, ...
secondaryCharts{:}, ...
Title = this.getFrequencyFigureTitle(primary, secondary), ...
Name = this.getFrequencyFigureName(primary, secondary), ...
Arrangement = [9, 2], ...
Title = this.getSpectrogramFigureTitle(primary, secondary), ...
Name = this.getSpectrogramFigureName(primary, secondary), ...
Arrangement = [9, numSpectra], ...
LinkXAxes = true, ...
TileIndexing = "columnmajor", ...
WindowState = "maximized");
Expand All @@ -61,7 +84,20 @@ function visualize(this)

methods (Access = private)

function charts = getFrequencyCharts(this, science, spectrum, name, axisLocation)
function spectrum = computeSpectrogram(this, science)

transformation = this.Transformation;

transformation.FrequencyLimits = this.FrequencyLimits;
transformation.FrequencyPoints = this.FrequencyPoints;
transformation.Normalize = this.Normalize;
transformation.Window = this.Window;
transformation.Overlap = this.Overlap;

spectrum = transformation.apply(science);
end

function charts = getSpectrogramCharts(this, science, spectrum, name, axisLocation)

charts = { ...
science, mag.graphics.style.Default(Title = compose("%s x", name), YLabel = "[nT]", YAxisLocation = axisLocation, Charts = mag.graphics.chart.Plot(YVariables = "X")), ...
Expand All @@ -72,12 +108,12 @@ function visualize(this)
spectrum, mag.graphics.style.Colormap(YLabel = this.FLabel, CLabel = this.PLabel, YLimits = "tight", Layout = [2, 1], Charts = mag.graphics.chart.Spectrogram(YVariables = "Z"))};
end

function value = getFrequencyFigureTitle(this, primary, secondary)
function value = getSpectrogramFigureTitle(this, primary, secondary)
value = compose("%s (%s, %s)", primary.MetaData.getDisplay("Mode"), this.getDataFrequency(primary.MetaData), this.getDataFrequency(secondary.MetaData));
end

function value = getFrequencyFigureName(this, primary, secondary)
value = this.getFrequencyFigureTitle(primary, secondary) + compose(" Frequency (%s)", this.date2str(primary.MetaData.Timestamp));
function value = getSpectrogramFigureName(this, primary, secondary)
value = this.getSpectrogramFigureTitle(primary, secondary) + compose(" Frequency (%s)", this.date2str(primary.MetaData.Timestamp));
end
end
end
4 changes: 4 additions & 0 deletions tests/system/analyze/tIMAPAnalysis.m
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ function skipOnGitHub(testCase)
testCase.assumeTrue(isempty(getenv("GITHUB_ACTIONS")), "Tests cannot run on GitHub CI runner.");
end

function checkMICEToolbox(testCase)
testCase.assumeTrue(exist("mice", "file") == 3, "MICE Toolbox not installed. Test skipped.");
end

function useMATLABR2024bOrAbove(testCase)
testCase.assumeTrue(matlabRelease().Release >= "R2024b", "Only MATLAB older than R2024b is supported for this test.");
end
Expand Down
8 changes: 8 additions & 0 deletions tests/unit/analyze/tSpice.m
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
classdef tSpice < MAGAnalysisTestCase
% TSPICE Unit tests for "mag.process.Spice" classes.

methods (TestClassSetup)

% Check that MICE Toolbox is installed.
function checkMICEToolbox(testCase)
testCase.assumeTrue(exist("mice", "file") == 3, "MICE Toolbox not installed. Test skipped.");
end
end

methods (Test)

% Test that conversion from MET to UTC is correct.
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/io/tScienceCSVIn.m
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
methods (TestClassSetup)

% Check that MICE Toolbox is installed.
function checkSPDFCDFToolbox(testCase)
function checkMICEToolbox(testCase)
testCase.assumeTrue(exist("mice", "file") == 3, "MICE Toolbox not installed. Test skipped.");
end
end
Expand Down
4 changes: 2 additions & 2 deletions tests/unit/visualize/view/MAGViewTestCase.m
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
testCase.assertNumElements(actualInputs, numel(expectedInputs), "Number of inputs should be as expected.");

for i = 1:numel(expectedInputs)
testCase.verifyEqual(expectedInputs{i}, actualInputs{i}, compose("Input #%i should match expectation.", i));
testCase.verifyEqual(actualInputs{i}, expectedInputs{i}, compose("Input #%i should match expectation.", i));
end
end

Expand Down Expand Up @@ -48,7 +48,7 @@
end

% Create instrument meta data.
metaInstrument = mag.meta.Instrument(ASW = "5.01", BSW = "0.02", GSE = "10.5.4", Model = "FM", Timestamp = datetime("now", TimeZone = "UTC"));
metaInstrument = mag.meta.Instrument(Mission = "IMAP", ASW = "5.01", BSW = "0.02", GSE = "10.5.4", Model = "FM", Timestamp = datetime("now", TimeZone = "UTC"));

% Create science data.
setup1 = mag.meta.Setup(Can = "None", FEE = "FEE2", Harness = "Some cable", Model = "EM4");
Expand Down
Loading

0 comments on commit b812174

Please sign in to comment.