supereeg icon indicating copy to clipboard operation
supereeg copied to clipboard

API ideas brainstorm

Open jeremymanning opened this issue 6 years ago • 2 comments

Nifti:

  • Add a Nifti.get_locs() function:
  • One idea: first convert to a brain object, and then return bo.get_locs().
  • Another idea: compute these locations when we initialize the model, and then store in self.locs. Then self.get_locs() would just return self.locs. But this gets messy because we'd need to also add kurtosis info and filter info to mirror syntax of Brain objects. In the interest of minimizing repeated code, I think option one would be more elegant.
  • This will mean that Model, Brain, and Nifti objects all have a get_locs function attached, which will simplify syntax in a bunch of places

Model:

  • Add a "set_locs" function that blurs the correlation matrix from the correct set of locations to a new given set of locations. (If include_original_locs=True (default: False), blur out the current locations to the union of self.get_locs() and the new locs.) Syntax: mo.set_locs(locs, inlude_original_locs=False). self.locs should also be updated to reflect the new locations.
  • Update should do the following:
    • a = Model(new_object)
    • self.set_locs(a.get_locs(), include_original_locs=True)
    • a.set_locs(self.get_locs()) #will already include original locations
    • If we're continuing with numerator/denominator approach
      • self.numerator.real = np.logaddexp(self.numerator.real, a.numerator.real)
      • self.numerator.imag = np.logaddexp(self.numerator.imag, a.numerator.imag)
      • self.denominator = np.logaddexp(self.denominator, a.denominator)
    • If we're doing the "combine at the correlation matrix level" approach:
      • m1 = self.get_model()
      • n1 = self.n_subs
      • m2 = a.get_model()
      • n2 = a.n_subs
      • self.numerator = _to_log_complex(_r2z(np.add(np.multiply(n1, m1), np.multiply(n2, m2))))
      • self.denominator = np.log(np.multiply(n1 + n2, np.ones_like(self.denominator))
      • self.n_subs += a.numerator.n_subs
  • If initializing a model with a list, first we should compute the union of all locations represented by that list (all_locs). Then create a model from each element of the list, specifying locs=all_locs. Internally, if the locs field is specified during init, at the end call self.set_locs(locs)
  • Predict:
    • syntax: mo.predict(data, update_model=True)
    • x = self.copy() #add this function
    • if update_model=True (default), first call x.update(data, include_original_locs=True)
    • if update_locs=True (default), then if update_model=False, call x.set_locs(data.get_locs(), include_original_locs=True)
      • if update_locs=False, don't run this step
    • Then convert data to a brain object
    • Use _timeseries_recon to predict the new data and turn it into a brain object. The final brain object should have the same locations as x
    • Note: if update_model=False and update_locs=False, it's possible that the returned brain object will have a subset of the locations in data. That's "correct" behavior. This should be taken into account of by x.set_locs and _timeseries_recon– i.e. no computations need to be performed (just take a subset of the existing correlation matrix and data)
  • Also add a flag for calling Model.save directly from the init function (default: false).
    • Option 1: we could have the user specify a "cache" directory and automatically compute the save location of the model object (creating a file name by using md5 to hash the inputted data). If the user calls init twice on the same input, the second call should load the answer from disk.
    • Option 2: we could use memoize to cache answers in memory. Benefit: easy to implement. Cost: doesn't save across sessions. (No save filename needed)
    • Option 3: user could specify a filename to save to, and we could use it only for saving (but overwrite and output a warning that the existing file is being overwritten if the same filename is passed in again, to ensure that the saved file always contains the expected model.
    • If we go with Options 1 or 2, we could also add a "se.Model.clear_cache" function (or possibly se.clear_cache) that deletes/clears all saved models

Brain:

  • Add a Brain.set_locs(). Syntax: bo.set_locs(new_locs, nearest_neighbors=False, include_original_locations=False, rbf_width=20, match_threshold='auto').
    • If nearest_neighbors=False (default), blur out the current set of locations to the new locations. Otherwise round all current locations to their nearest equivalent, within the parameters of match_threshold (behave like the current nearest_neighbors options in Model objects). Also remove nearest_neighbors stuff from Model objects. If nearest_neighbor=False, ignore the match_threshold argument.
    • If include_original_locations=True, also include the original data (just add to it). Idea: this might be a good (alternative) null model– e.g. if you just use spatial blur on the raw data, how well can you fill in missing data. It'll be similar to using just the subject's data to compute the correlation matrix, but it won't factor in long-distance correlations.
    • Use the rbf_width parameter (default: 20) to do the blurring
    • This should updated self.locs reflect the new locations
    • All "blurring" should be done via self.get_locs() and self.get_data() (i.e. filtering out "bad" electrodes).
  • Kurtosis values still need to be re-computed for all new data
    • If no blurring is needed (e.g. if new_locs is a subset of self.get_locs, or if nearest_neighbors=True), just re-use the existing kurtosis values

For consideration:

  • Add a "se.Locations" object that stores locations for Model
  • We could attach some convenience functions to the object (e.g. automatically store the unique locations, do sorting, support indexing, etc.)
  • We could also attach "mapping" functions: e.g. locs.get_mapper() would return a function that accepts a model or brain object (with any locations) and returns a new model (or brain) with the same locations as Locations. This would be something like: return lambda x: x.update_locs(self.locs)
    • We could also easily add (crude) support for Nifti objects by adding an update_locs to Nifti objects (just convert to a brain object, then call update_locs, then convert back to a nifti object). Then all main SuperEEG datatypes would be supported.
    • This would make mapping between different models convenient– e.g. using one subject's model to make predictions about someone else's data. We could also provide pickled functions (if pickle supports functions) that take any model, brain object, or nifti object, and convert it to a pre-specified set of locations (e.g. the set of locations in the standard 2mm MNI brain). This could be convenient for generating figures.

jeremymanning avatar Apr 23 '18 20:04 jeremymanning

I've implemented some of the simple changes above. Still to do (will make separate issues and close this one):

  • Locations objects
  • Brain.set_locs()

jeremymanning avatar Apr 23 '18 20:04 jeremymanning

check in about this issue... see which of these we'd still like to incorporate.

lucywowen avatar Oct 03 '18 16:10 lucywowen