-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
SourceTFR #6543
SourceTFR #6543
Conversation
Added Basic SourceTFR structure and Text Signed-off-by: Dirk Gütlin <[email protected]>
Added Basic SourceTFR structure and Text Signed-off-by: Dirk Gütlin <[email protected]>
Updated SourceTFR to version used for reading morlet/multitapers Signed-off-by: Dirk Gütlin <[email protected]>
# | ||
# Authors: Dirk Gütlin <[email protected]> | ||
# TODO: This file uses a lot of stuff from io/base.py and source_estimate.py | ||
# TODO: Maybe name these persons as authors too? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
to me, the authors are the list of people that would know something about that file. It is the people I'm going to send them an email when things go south.
People who wrote the file or closely reviewed.
if not (isinstance(vertices, np.ndarray) or | ||
isinstance(vertices, list)): | ||
raise ValueError('Vertices must be a numpy array or a list of ' | ||
'arrays') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we have functions for checking parameters.
""" | ||
|
||
@verbose | ||
def __init__(self, data, vertices=None, tmin=None, tstep=None, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
break the __init__
with functions. I know that some of our inits are huge, but they should be short easy to read in a glance.
def __init__(foo, bar, data, vertices, bla):
_check_param(foo, ...)
self._data = _prepare_data(data)
self._vercies = _foo(vertices)
...
to me init should be just assigning stuff to the attributes. If _prepare_data
has a signature like _prepare_data(data=data, n_col=vertices.shape[0], moon_color=foo)
it tels me at a glance how data is related to the other things. Sure I would not know the details but I least I would know the possible relations. If I have to read the code and strip all the value errors out on my mind the task of reading the code is much harder.
keep in mind: code is written once but read endless times and when you read code you want to skim it. Not digest it.
self._update_times() | ||
self.subject = _check_subject(None, subject, False) | ||
|
||
def __repr__(self): # noqa: D105 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't use '%' to parse strings (we are no longer in the 90s ;) ).
fstrings are much more readable and they are the way to go (see doc). However, we cannot use them until we drop python 3.5 support. Meanwhile, we could be using 'my name is {name}'.format(name='massich')
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We still use %
all over the code base and have not agreed to change to / prefer .format
-- I think %
is fine for now
""" | ||
if ftype != 'h5': | ||
raise ValueError('%s objects can only be written as HDF5 files.' | ||
% (self.__class__.__name__,)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use checks
raise ValueError('%s objects can only be written as HDF5 files.' | ||
% (self.__class__.__name__,)) | ||
if not fname.endswith('.h5'): | ||
fname += '-stfr.h5' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
avoid if
s without else
s as much as possible. See this awesome talk by @ Katrina Owen
in a case like this one, I would do something like:
fname = fname if fname.endswith('h5') else '{}-stfr.h5'.format(fname)
or any way you like to build the fname. (I like this one because it has no spaces but maybe fname + '-stfr.h5'
is a better choice)
return self._data.shape | ||
return (self._kernel.shape[0], | ||
self._sens_data.shape[1], | ||
self._sens_data.shape[2]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use else!!!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
document what returns.
does this return always the same kind of tuple?
|
||
@times.setter | ||
def times(self, value): | ||
raise ValueError('You cannot write to the .times attribute directly. ' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why is this a value error? isn't it a runtime?
Is there any other way to prevent the setter? maybe not.
from mne.source_tfr import SourceTFR | ||
|
||
|
||
def _fake_stfr(): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixture
def _fake_kernel_stfr(): | ||
"""Create a fake kernelized SourceTFR.""" | ||
kernel = np.random.rand(100, 40) | ||
sens_data = np.random.rand(40, 20, 10) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixture
|
||
# check if data is in correct shape | ||
expected = [100, 10, 30] | ||
assert_allclose([kernel_stfr.shape, full_stfr.shape, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why are you testing it like this? by creating a vector that will have no name and when we read the error we will know nothing about it? why allclose? is it possible to have an array with shape 100+epsilon?
assert kernel_stfr.shape == (100, 10, 30)
assert full_stfr.shape == (100, 10, 30)
assert bla.shape == (100, 10, 30)
assert foo.shape == (watever)
will return a meaningful error that I will read and I would understand what is my next step. otherwise, I'll have to put a breakpoint stop the execution, understand the array you have created think about all those things and make a guess of what could be wrong. I'm not smart enough. I would rather prefer pytest telling me where should I look.
assert_equal(stfr._data.shape[-1], n_times) | ||
assert_array_equal(stfr.times, stfr.tmin + np.arange(n_times) * stfr.tstep) | ||
|
||
assert_array_almost_equal( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
avoid almost_equal either equal or allclose
|
||
# tstep <= 0 is not allowed | ||
pytest.raises(ValueError, attempt_assignment, stfr, 'tstep', 0) | ||
pytest.raises(ValueError, attempt_assignment, stfr, 'tstep', -1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know this is compact, but when I read this I need to go from
attempt_assignment, stfr, 'tstep', 0
to attempt_assignment(foo=foo, bar=bar..)
inside my head. Its better to write with pytest.raises(ValueError, match='AND A MEANINGFUL ERROR MATCH THAT TELLS ME WHY THIS SHOULD FAIL')
pytest.raises(ValueError, attempt_assignment, stfr_kernel, 'data', | ||
[np.arange(100)]) | ||
pytest.raises(ValueError, attempt_assignment, stfr_kernel, 'data', | ||
[[[np.arange(100)]]]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use with
@requires_h5py | ||
def test_io_stfr_h5(): | ||
"""Test IO for stfr files using HDF5.""" | ||
for stfr in [_fake_stfr(), _fake_kernel_stfr()]: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This screams parametrize
stfr_ = _fake_stfr() | ||
kernel_stfr_ = _fake_kernel_stfr() | ||
|
||
for stfr in [stfr_, kernel_stfr_]: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so does this one
|
||
copy_2 = inst.copy() | ||
assert_allclose(copy_2.crop(tmin=0.2).data, inst.data[:, :, 2:]) | ||
assert_allclose(copy_2.times, inst.times[2:]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the reference you compare with is the later assert actual == expected
. Same thing `assert_allclose(actual, expected)
|
||
def test_invalid_params(): | ||
"""Test catching invalid parameters for SourceTFR.""" | ||
data = np.random.rand(40, 10, 20) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
either you use a seed and then you compare against it or use np.empty
.
assert_allclose(copy_2.times, inst.times[2:]) | ||
|
||
|
||
def test_invalid_params(): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
parametrize
...according to review in PR mne-tools#6543 Signed-off-by: Dirk Gütlin <[email protected]>
Usually we try to isolate the things, split them as much as possible and write dependencies between them. (makes review easier) |
This is a draft PR for a tentative Version of a
SourceTFR
class which is used to store time-frequency transformed Source Estimates. It does not yet cover some relevant functions (i.e. plotting or reading from disc).This PR was mainly created, so @massich can review the code.