Commit 27db1576 authored by Paul McCarthy's avatar Paul McCarthy 🚵
Browse files

RF,BF: Fix complex power spectrum series label generation. Adjust relationship

between ComplexPowerSpectrumSeries and Imaginary/Real/Magnitude - there is no
guarantee that CPSS.getData will be called before the others, so we can't
pre-calculate and cache the result. Currently just duplicate calculations for
each series, as the performance shouldn't really be a problem, and it keeps
the code fairly clean.
parent f31a2c7e
......@@ -102,7 +102,7 @@ def phase(data):
return np.arctan2(imag, real)
def normalise(data, dmin=None, dmax=None, nmin=0, nmax=1):
def normalise(data, dmin=None, dmax=None, nmin=-1, nmax=1):
"""Returns ``data``, rescaled to the range [nmin, nmax].
If dmin and dmax are provided, the data is normalised with respect to them,
......@@ -232,12 +232,14 @@ class ComplexPowerSpectrumSeries(VoxelPowerSpectrumSeries):
VoxelPowerSpectrumSeries.__init__(
self, overlay, overlayList, displayCtx, plotPanel)
self.__cachedData = (None, None)
self.__imagps = ImaginaryPowerSpectrumSeries(
# Separate DataSeries for the imaginary/
# magnitude/phase signals, returned by
# the extraSeries method
self.__imagps = ImaginaryPowerSpectrumSeries(
self, overlay, overlayList, displayCtx, plotPanel)
self.__magps = MagnitudePowerSpectrumSeries(
self.__magps = MagnitudePowerSpectrumSeries(
self, overlay, overlayList, displayCtx, plotPanel)
self.__phaseps = PhasePowerSpectrumSeries(
self.__phaseps = PhasePowerSpectrumSeries(
self, overlay, overlayList, displayCtx, plotPanel)
for ps in (self.__imagps, self.__magps, self.__phaseps):
......@@ -247,35 +249,60 @@ class ComplexPowerSpectrumSeries(VoxelPowerSpectrumSeries):
ps.bindProps('lineStyle', self)
def makeLabel(self):
"""Returns a string representation of this
``ComplexPowerSpectrumSeries`` instance.
def makeLabelBase(self):
"""Returns a string to be used as the label prefix for this
``ComplexPowerSpectrumSeries`` instance, and for the imaginary,
magnitude, and phase child series.
"""
return '{} ({})'.format(VoxelPowerSpectrumSeries.makeLabel(self),
strings.labels[self])
return VoxelPowerSpectrumSeries.makeLabel(self)
@property
def cachedData(self):
"""Returns the currently cached data (see :meth:`getData`). """
return self.__cachedData
def makeLabel(self):
"""Returns a label to use for this data series. """
return '{} ({})'.format(self.makeLabelBase(), strings.labels[self])
def getData(self):
def getData(self, component='real'):
"""If :attr:`plotReal` is true, returns the real component of the power
spectrum of the data at the current voxel. Otherwise returns ``(None,
None)``.
Every time this method is called, the power spectrum is calculated,
phase correction is applied, and a reference to the resulting complex
power spectrum (and frequencies) is saved; it is accessible via the
:meth:`cachedData` property, for use by the
:class:`ImaginaryPowerSpectrumSeries`,
:class:`MagnitudePowerSpectrumSeries`, and
:class:`PhasePowerSpectrumSeries`.
Every time this method is called, the power spectrum is retrieved (see
the :class:`VoxelPowerSpectrumSeries` class), phase correction is
applied if set, andthe data is normalised, if set. A tuple containing
the ``(xdata, ydata)`` is returned, with ``ydata`` containing the
requested ``component`` ( ``'real'``, ``'imaginary'``,
``'magnitude'``, or ``'phase'``).
This method is called by the :class:`ImaginarySpectrumPowerSeries`,
:class:`MagnitudeSpectrumPowerSeries`, and
:class:`PhasePowerSpectrumPowerSeries` instances that are associated
with this data series.
"""
xdata, ydata = VoxelPowerSpectrumSeries.getData(self)
if ((component == 'real') and (not self.plotReal)) or \
((component == 'imaginary') and (not self.plotImaginary)) or \
((component == 'magnitude') and (not self.plotMagnitude)) or \
((component == 'phase') and (not self.plotPhase)):
return None, None
# See VoxelPowerSpectrumSeries - the data
# is already fourier-transformed
ydata = self.dataAtCurrentVoxel()
if ydata is None:
return None, None
# All of the calculations below are repeated
# for each real/imag/mag/phase series that
# gets plotted. But keeping the code together
# and clean is currently more important than
# performance, as there is not really any
# performance hit.
overlay = self.overlay
xdata = calcFrequencies(overlay.shape[3],
self.sampleTime,
overlay.dtype)
if self.zeroOrderPhaseCorrection != 0 or \
self.firstOrderPhaseCorrection != 0:
......@@ -284,19 +311,21 @@ class ComplexPowerSpectrumSeries(VoxelPowerSpectrumSeries):
self.zeroOrderPhaseCorrection,
self.firstOrderPhaseCorrection)
# Note that we're assuming that this
# ComplexPowerSpectrumSeries.getData
# method will be called before the
# corresponding call(s) to the
# Imaginary/Magnitude/Phase series
# methods.
self.__cachedData = xdata, ydata
if not self.plotReal:
return None, None
if ydata is not None:
ydata = ydata.real
# Normalise magnitude, real, imaginary
# components with respect to magnitude.
# Normalise phase independently.
if self.varNorm:
mag = magnitude(ydata)
mr = mag.min(), mag.max()
if component == 'phase': ydata = normalise(phase(ydata))
elif component == 'magnitude': ydata = normalise(mag)
elif component == 'real': ydata = normalise(ydata.real, *mr)
elif component == 'imaginary': ydata = normalise(ydata.imag, *mr)
elif component == 'real': ydata = ydata.real
elif component == 'imaginary': ydata = ydata.imag
elif component == 'magnitude': ydata = magnitude(ydata)
elif component == 'phase': ydata = phase(ydata)
return xdata, ydata
......@@ -339,19 +368,13 @@ class ImaginaryPowerSpectrumSeries(dataseries.DataSeries):
"""Returns a string representation of this
``ImaginaryPowerSpectrumSeries`` instance.
"""
return '{} ({})'.format(self.__parent.makeLabel(),
return '{} ({})'.format(self.__parent.makeLabelBase(),
strings.labels[self])
def getData(self):
"""Returns the imaginary component of the power spectrum. """
xdata, ydata = self.__parent.cachedData
if ydata is not None:
ydata = ydata.imag
return xdata, ydata
return self.__parent.getData('imaginary')
class MagnitudePowerSpectrumSeries(dataseries.DataSeries):
......@@ -378,16 +401,13 @@ class MagnitudePowerSpectrumSeries(dataseries.DataSeries):
"""Returns a string representation of this
``MagnitudePowerSpectrumSeries`` instance.
"""
return '{} ({})'.format(self.__parent.makeLabel(),
return '{} ({})'.format(self.__parent.makeLabelBase(),
strings.labels[self])
def getData(self):
"""Returns the magnitude of the complex power spectrum. """
xdata, ydata = self.__parent.cachedData
if ydata is not None:
ydata = magnitude(ydata)
return xdata, ydata
return self.__parent.getData('magnitude')
class PhasePowerSpectrumSeries(dataseries.DataSeries):
......@@ -414,16 +434,13 @@ class PhasePowerSpectrumSeries(dataseries.DataSeries):
"""Returns a string representation of this ``PhasePowerSpectrumSeries``
instance.
"""
return '{} ({})'.format(self.__parent.makeLabel(),
return '{} ({})'.format(self.__parent.makeLabelBase(),
strings.labels[self])
def getData(self):
"""Returns the phase of the complex power spectrum. """
xdata, ydata = self.__parent.cachedData
if ydata is not None:
ydata = phase(ydata)
return xdata, ydata
return self.__parent.getData('phase')
class MelodicPowerSpectrumSeries(dataseries.DataSeries,
......@@ -533,6 +550,6 @@ class MeshPowerSpectrumSeries(dataseries.DataSeries,
vd = opts.getVertexData()
data = vd[vidx, :]
xdata = calcFrequencies( data, self.sampleTime)
ydata = calcPowerSpectrum(data, self.varNorm)
ydata = calcPowerSpectrum(data)
return xdata, ydata
......@@ -1065,7 +1065,7 @@ properties = TypeDict({
'ComplexHistogramSeries.plotMagnitude' : 'Plot magnitude',
'ComplexHistogramSeries.plotPhase' : 'Plot phase',
'PowerSpectrumSeries.varNorm' : 'Normalise to unit variance',
'PowerSpectrumSeries.varNorm' : 'Normalise to [-1, 1]',
'FEATTimeSeries.plotFullModelFit' : 'Plot full model fit',
'FEATTimeSeries.plotEVs' : 'Plot EV{} ({})',
......
......@@ -702,15 +702,14 @@ properties = TypeDict({
'histogram.',
'PowerSpectrumSeries.varNorm' : 'If checked, the data is demeaned and '
'normalised by its standard deviation '
'before its power spectrum is '
'calculated via a fourier transform.',
'PowerSpectrumSeries.varNorm' :
'If checked, the fourier-transformed data is normalised to the range '
'[-1, 1]. Complex valued data are normalised with respect to the '
'absolute value. ',
# Profiles
'OrthoPanel.profile' : 'Switch between view mode '
'and edit mode',
'OrthoPanel.profile' :
'Switch between view mode and edit mode',
'OrthoEditProfile.selectionCursorColour' :
'Colour to use for the selection cursor.',
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment