class ProjectionSurfaceMEGData(ProjectionMatrixData): """ Specific projection, from a CorticalSurface to MEG sensors. ... warning :: PLACEHOLDER """ brain_skull = surfaces_module.BrainSkull(label = "Brain Skull", default = None, required = False, doc = """Boundary between skull and cortex domains.""") skull_skin = surfaces_module.SkullSkin(label = "Skull Skin", default = None, required = False, doc = """Boundary between skull and skin domains.""") skin_air = surfaces_module.SkinAir( label = "Skin Air", default = None, required = False, doc = """Boundary between skin and air domains.""") conductances = basic.Dict(label = "Domain conductances", required = False, default = {'air': 0.0, 'skin': 1.0, 'skull': 0.01, 'brain': 1.0}, doc = """ A dictionary representing the conductances of ... """) sensors = sensors_module.SensorsMEG sources = surfaces_module.CorticalSurface
class ProjectionData(MappedType): """ Base DataType for representing a ProjectionMatrix. The projection is between a source of type CorticalSurface and a set of Sensors. """ projection_type = basic.String __mapper_args__ = {'polymorphic_on': 'projection_type'} brain_skull = surfaces.BrainSkull( label="Brain Skull", default=None, required=False, doc="""Boundary between skull and cortex domains.""") skull_skin = surfaces.SkullSkin( label="Skull Skin", default=None, required=False, doc="""Boundary between skull and skin domains.""") skin_air = surfaces.SkinAir( label="Skin Air", default=None, required=False, doc="""Boundary between skin and air domains.""") conductances = basic.Dict( label="Domain conductances", required=False, default={ 'air': 0.0, 'skin': 1.0, 'skull': 0.01, 'brain': 1.0 }, doc=""" A dictionary representing the conductances of ... """) sources = surfaces.CorticalSurface(label="surface or region", default=None, required=True) sensors = sensors.Sensors( label="Sensors", default=None, required=False, doc=""" A set of sensors to compute projection matrix for them. """) projection_data = arrays.FloatArray(label="Projection Matrix Data", default=None, required=True)
class ProjectionMatrix(core.Type): """ Provides the mechanisms necessary to access OpenMEEG for the calculation of EEG and MEG projection matrices, ie matrices that map source activity to sensor activity. It is initialised with datatypes of TVB and ultimately returns the projection matrix as a Numpy ndarray. """ brain_skull = surfaces_module.BrainSkull( label="Boundary between skull and skin domains", default=None, required=True, doc="""A ... surface on which ... including ...""") skull_skin = surfaces_module.SkullSkin( label="surface and auxillary for surface sim", default=None, required=True, doc="""A ... surface on which ... including ...""") skin_air = surfaces_module.SkinAir( label="surface and auxillary for surface sim", default=None, required=True, doc="""A ... surface on which ... including ...""") conductances = basic.Dict( label="Domain conductances", default={ 'air': 0.0, 'skin': 1.0, 'skull': 0.01, 'brain': 1.0 }, required=True, doc="""A dictionary representing the conductances of ...""") sources = surfaces_module.Cortex( label="surface and auxillary for surface sim", default=None, required=True, doc="""A cortical surface on which ... including ...""") sensors = sensors_module.Sensors( label="surface and auxillary for surface sim", default=None, required=False, doc="""A cortical surface on which ... including ... If left as None then EEG is assumed and skin_air is expected to already has sensors associated""") def __init__(self, **kwargs): """ Initialse traited attributes and attributes that will hold OpenMEEG objects. """ super(ProjectionMatrix, self).__init__(**kwargs) LOG.debug(str(kwargs)) #OpenMEEG attributes self.om_head = None self.om_sources = None self.om_sensors = None self.om_head2sensor = None self.om_inverse_head = None self.om_source_matrix = None self.om_source2sensor = None #For MEG, not used for EEG def configure(self): """ Converts TVB objects into a for accessible to OpenMEEG, then uses the OpenMEEG library to calculate the intermediate matrices needed in obtaining the final projection matrix. """ super(ProjectionMatrix, self).configure() if self.sensors is None: self.sensors = self.skin_air.sensors if isinstance(self.sensors, sensors_module.SensorsEEG): self.skin_air.sensors = self.sensors self.skin_air.sensor_locations = self.sensors.sensors_to_surface( self.skin_air) # Create OpenMEEG objects from TVB objects. self.om_head = self.create_om_head() self.om_sources = self.create_om_sources() self.om_sensors = self.create_om_sensors() # Calculate based on type of sources if isinstance(self.sources, surfaces_module.Cortex): self.om_source_matrix = self.surface_source() #NOTE: ~1 hr elif isinstance(self.sources, connectivity_module.Connectivity): self.om_source_matrix = self.dipole_source() # Calculate based on type of sensors if isinstance(self.sensors, sensors_module.SensorsEEG): self.om_head2sensor = self.head2eeg() elif isinstance(self.sensors, sensors_module.SensorsMEG): self.om_head2sensor = self.head2meg() if isinstance(self.sources, surfaces_module.Cortex): self.om_source2sensor = self.surf2meg() elif isinstance(self.sources, connectivity_module.Connectivity): self.om_source2sensor = self.dip2meg() #NOTE: ~1 hr self.om_inverse_head = self.inverse_head(inv_head_mat_file="hminv_uid") def __call__(self): """ Having configured the ProjectionMatrix instance, that is having run the configure() method or otherwise provided the intermedite OpenMEEG (om_*) attributes, the oblect can be called as a function -- returning a projection matrix as a Numpy array. """ #Check source type and sensor type, then call appripriate methods to #generate intermediate data, cascading all the way back to geometry #calculation if it wasn't already done. #Then return a projection matrix... # NOTE: returned projection_matrix is a numpy.ndarray if isinstance(self.sensors, sensors_module.SensorsEEG): projection_matrix = self.eeg_gain() elif isinstance(self.sensors, sensors_module.SensorsMEG): projection_matrix = self.meg_gain() return projection_matrix ##------------------------------------------------------------------------## ##--------------- Methods for creating openmeeg objects ------------------## ##------------------------------------------------------------------------## def create_om_head(self): #TODO: Prob. need to make file names specifiable """ Generates 5 files:: skull_skin.tri skin_air.tri brain_skull.tri head_model.geom head_model.cond Containing the specification of a head in a form that can be read by OpenMEEG, then creates and returns an OpenMEEG Geometry object containing this information. """ surface_files = [] surface_files.append(self._tvb_surface_to_tri("skull_skin.tri")) surface_files.append(self._tvb_surface_to_tri("brain_skull.tri")) surface_files.append(self._tvb_surface_to_tri("skin_air.tri")) geometry_file = self._write_head_geometry(surface_files, "head_model.geom") conductances_file = self._write_conductances("head_model.cond") LOG.info("Creating OpenMEEG Geometry object for the head...") om_head = om.Geometry() om_head.read(geometry_file, conductances_file) #om_head.selfCheck() #Didn't catch bad order... LOG.info("OpenMEEG Geometry object for the head successfully created.") return om_head def create_om_sources( self): #TODO: Prob. should make file names specifiable """ Take a TVB Connectivity or Cortex object and return an OpenMEEG object that specifies sources, a Matrix object for region level sources or a Mesh object for a cortical surface source. """ if isinstance(self.sources, connectivity_module.Connectivity): sources_file = self._tvb_connectivity_to_txt("sources.txt") om_sources = om.Matrix() elif isinstance(self.sources, surfaces_module.Cortex): sources_file = self._tvb_surface_to_tri("sources.tri") om_sources = om.Mesh() else: LOG.error("sources must be either a Connectivity or Cortex.") om_sources.load(sources_file) return om_sources def create_om_sensors(self, file_name=None): """ Take a TVB Sensors object and return an OpenMEEG Sensors object. """ if isinstance(self.sensors, sensors_module.SensorsEEG): file_name = file_name or "eeg_sensors.txt" sensors_file = self._tvb_eeg_sensors_to_txt(file_name) elif isinstance(self.sensors, sensors_module.SensorsMEG): file_name = file_name or "meg_sensors.squid" sensors_file = self._tvb_meg_sensors_to_squid(file_name) else: LOG.error("sensors should be either SensorsEEG or SensorsMEG") LOG.info("Wrote sensors to temporary file: %s" % str(file_name)) om_sensors = om.Sensors() om_sensors.load(sensors_file) return om_sensors ##------------------------------------------------------------------------## ##--------- Methods for calling openmeeg methods, with logging. ----------## ##------------------------------------------------------------------------## def surf2meg(self): """ Create a matrix that can be used to map an OpenMEEG surface source to an OpenMEEG MEG Sensors object. NOTE: This source to sensor mapping is not required for EEG. """ LOG.info("Computing DipSource2MEGMat...") surf2meg_mat = om.SurfSource2MEGMat(self.om_sources, self.om_sensors) LOG.info("surf2meg: %d x %d" % (surf2meg_mat.nlin(), surf2meg_mat.ncol())) return surf2meg_mat def dip2meg(self): """ Create an OpenMEEG Matrix that can be used to map OpenMEEG dipole sources to an OpenMEEG MEG Sensors object. NOTE: This source to sensor mapping is not required for EEG. """ LOG.info("Computing DipSource2MEGMat...") dip2meg_mat = om.DipSource2MEGMat(self.om_sources, self.om_sensors) LOG.info("dip2meg: %d x %d" % (dip2meg_mat.nlin(), dip2meg_mat.ncol())) return dip2meg_mat def head2eeg(self): """ Call OpenMEEG's Head2EEGMat method to calculate the head to EEG sensor matrix. """ LOG.info("Computing Head2EEGMat...") h2s_mat = om.Head2EEGMat(self.om_head, self.om_sensors) LOG.info("head2eeg: %d x %d" % (h2s_mat.nlin(), h2s_mat.ncol())) return h2s_mat def head2meg(self): """ Call OpenMEEG's Head2MEGMat method to calculate the head to MEG sensor matrix. """ LOG.info("Computing Head2MEGMat...") h2s_mat = om.Head2MEGMat(self.om_head, self.om_sensors) LOG.info("head2meg: %d x %d" % (h2s_mat.nlin(), h2s_mat.ncol())) return h2s_mat def surface_source(self, gauss_order=3, surf_source_file=None): """ Call OpenMEEG's SurfSourceMat method to calculate a surface source matrix. Optionaly saving the matrix for later use. """ LOG.info("Computing SurfSourceMat...") ssm = om.SurfSourceMat(self.om_head, self.om_sources, gauss_order) LOG.info("surface_source_mat: %d x %d" % (ssm.nlin(), ssm.ncol())) if surf_source_file is not None: LOG.info("Saving surface_source matrix as %s..." % surf_source_file) ssm.save( os.path.join(OM_STORAGE_DIR, surf_source_file + OM_SAVE_SUFFIX)) #~3GB return ssm def dipole_source(self, gauss_order=3, use_adaptive_integration=True, dip_source_file=None): """ Call OpenMEEG's DipSourceMat method to calculate a dipole source matrix. Optionaly saving the matrix for later use. """ LOG.info("Computing DipSourceMat...") dsm = om.DipSourceMat(self.om_head, self.om_sources, gauss_order, use_adaptive_integration) LOG.info("dipole_source_mat: %d x %d" % (dsm.nlin(), dsm.ncol())) if dip_source_file is not None: LOG.info("Saving dipole_source matrix as %s..." % dip_source_file) dsm.save( os.path.join(OM_STORAGE_DIR, dip_source_file + OM_SAVE_SUFFIX)) return dsm def inverse_head(self, gauss_order=3, inv_head_mat_file=None): """ Call OpenMEEG's HeadMat method to calculate a head matrix. The inverse method of the head matrix is subsequently called to invert the matrix. Optionaly saving the inverted matrix for later use. Runtime ~8 hours, mostly in martix inverse as I just use a stock ATLAS install which doesn't appear to be multithreaded (custom building ATLAS should sort this)... Under Windows it should use MKL, not sure for Mac For reg13+potato surfaces, saved file size: hminv ~ 5GB, ssm ~ 3GB. """ LOG.info("Computing HeadMat...") head_matrix = om.HeadMat(self.om_head, gauss_order) LOG.info("head_matrix: %d x %d" % (head_matrix.nlin(), head_matrix.ncol())) LOG.info("Inverting HeadMat...") hminv = head_matrix.inverse() LOG.info("inverse head_matrix: %d x %d" % (hminv.nlin(), hminv.ncol())) if inv_head_mat_file is not None: LOG.info("Saving inverse_head matrix as %s..." % inv_head_mat_file) hminv.save( os.path.join(OM_STORAGE_DIR, inv_head_mat_file + OM_SAVE_SUFFIX)) #~5GB return hminv def eeg_gain(self, eeg_file=None): """ Call OpenMEEG's GainEEG method to calculate the final projection matrix. Optionaly saving the matrix for later use. The OpenMEEG matrix is converted to a Numpy array before return. """ LOG.info("Computing GainEEG...") eeg_gain = om.GainEEG(self.om_inverse_head, self.om_source_matrix, self.om_head2sensor) LOG.info("eeg_gain: %d x %d" % (eeg_gain.nlin(), eeg_gain.ncol())) if eeg_file is not None: LOG.info("Saving eeg_gain as %s..." % eeg_file) eeg_gain.save( os.path.join(OM_STORAGE_DIR, eeg_file + OM_SAVE_SUFFIX)) return om.asarray(eeg_gain) def meg_gain(self, meg_file=None): """ Call OpenMEEG's GainMEG method to calculate the final projection matrix. Optionaly saving the matrix for later use. The OpenMEEG matrix is converted to a Numpy array before return. """ LOG.info("Computing GainMEG...") meg_gain = om.GainMEG(self.om_inverse_head, self.om_source_matrix, self.om_head2sensor, self.om_source2sensor) LOG.info("meg_gain: %d x %d" % (meg_gain.nlin(), meg_gain.ncol())) if meg_file is not None: LOG.info("Saving meg_gain as %s..." % meg_file) meg_gain.save( os.path.join(OM_STORAGE_DIR, meg_file + OM_SAVE_SUFFIX)) return om.asarray(meg_gain) ##------------------------------------------------------------------------## ##------- Methods for writting temporary files loaded by openmeeg --------## ##------------------------------------------------------------------------## def _tvb_meg_sensors_to_squid(self, sensors_file_name): """ Write a tvb meg_sensor datatype to a .squid file, so that OpenMEEG can read it and compute the projection matrix for MEG... """ sensors_file_path = os.path.join(OM_STORAGE_DIR, sensors_file_name) meg_sensors = numpy.hstack( (self.sensors.locations, self.sensors.orientations)) numpy.savetxt(sensors_file_path, meg_sensors) return sensors_file_path def _tvb_connectivity_to_txt(self, dipoles_file_name): """ Write position and orientation information from a TVB connectivity object to a text file that can be read as source dipoles by OpenMEEG. NOTE: Region level simulations lack sufficient detail of source orientation, etc, to provide anything but superficial relevance. It's probably better to do a mapping of region level simulations to a surface and then perform the EEG projection from the mapped data... """ NotImplementedError def _tvb_surface_to_tri(self, surface_file_name): """ Write a tvb surface datatype to .tri format, so that OpenMEEG can read it and compute projection matrices for EEG/MEG/... """ surface_file_path = os.path.join(OM_STORAGE_DIR, surface_file_name) #TODO: check file doesn't already exist LOG.info("Writing TVB surface to .tri file: %s" % surface_file_path) file_handle = file(surface_file_path, "a") file_handle.write("- %d \n" % self.sources.number_of_vertices) verts_norms = numpy.hstack( (self.sources.vertices, self.sources.vertex_normals)) numpy.savetxt(file_handle, verts_norms) tri_str = "- " + (3 * (str(self.sources.number_of_triangles) + " ")) + "\n" file_handle.write(tri_str) numpy.savetxt(file_handle, self.sources.triangles, fmt="%d") file_handle.close() LOG.info("%s written successfully." % surface_file_name) return surface_file_path def _tvb_eeg_sensors_to_txt(self, sensors_file_name): """ Write a tvb eeg_sensor datatype (after mapping to the head surface to be used) to a .txt file, so that OpenMEEG can read it and compute leadfield/projection/forward_solution matrices for EEG... """ sensors_file_path = os.path.join(OM_STORAGE_DIR, sensors_file_name) LOG.info("Writing TVB sensors to .txt file: %s" % sensors_file_path) numpy.savetxt(sensors_file_path, self.skin_air.sensor_locations) LOG.info("%s written successfully." % sensors_file_name) return sensors_file_path #TODO: enable specifying ?or determining? domain surface relationships... def _write_head_geometry(self, boundary_file_names, geom_file_name): """ Write a geometry file that is read in by OpenMEEG, this file specifies the files containng the boundary surfaces and there relationship to the domains that comprise the head. NOTE: Currently the list of files is expected to be in a specific order, namely:: skull_skin brain_skull skin_air which is reflected in the static setting of domains. Should be generalised. """ geom_file_path = os.path.join(OM_STORAGE_DIR, geom_file_name) #TODO: Check that the file doesn't already exist. LOG.info("Writing head geometry file: %s" % geom_file_path) file_handle = file(geom_file_path, "a") file_handle.write("# Domain Description 1.0\n\n") file_handle.write("Interfaces %d Mesh\n\n" % len(boundary_file_names)) for file_name in boundary_file_names: file_handle.write("%s\n" % file_name) file_handle.write("\nDomains %d\n\n" % (len(boundary_file_names) + 1)) file_handle.write("Domain Scalp %s %s\n" % (1, -3)) file_handle.write("Domain Brain %s %s\n" % ("-2", "shared")) file_handle.write("Domain Air %s\n" % 3) file_handle.write("Domain Skull %s %s\n" % (2, -1)) file_handle.close() LOG.info("%s written successfully." % geom_file_path) return geom_file_path def _write_conductances(self, cond_file_name): """ Write a conductance file that is read in by OpenMEEG, this file specifies the conductance of each of the domains making up the head. NOTE: Vaules are restricted to have 2 decimal places, ie #.##, setting values of the form 0.00# will result in 0.01 or 0.00, for numbers greater or less than ~0.00499999999999999967, respecitvely... """ cond_file_path = os.path.join(OM_STORAGE_DIR, cond_file_name) #TODO: Check that the file doesn't already exist. LOG.info("Writing head conductance file: %s" % cond_file_path) file_handle = file(cond_file_path, "a") file_handle.write("# Properties Description 1.0 (Conductivities)\n\n") file_handle.write("Air %4.2f\n" % self.conductances["air"]) file_handle.write("Scalp %4.2f\n" % self.conductances["skin"]) file_handle.write("Brain %4.2f\n" % self.conductances["brain"]) file_handle.write("Skull %4.2f\n" % self.conductances["skull"]) file_handle.close() LOG.info("%s written successfully." % cond_file_path) return cond_file_path #TODO: Either make these utility functions or have them load directly into # the appropriate attribute... ##------------------------------------------------------------------------## ##---- Methods for loading precomputed matrices into openmeeg objects ----## ##------------------------------------------------------------------------## def _load_om_inverse_head_mat(self, file_name): """ Load a previously stored inverse head matrix into an OpenMEEG SymMatrix object. """ inverse_head_martix = om.SymMatrix() inverse_head_martix.load(file_name) return inverse_head_martix def _load_om_source_mat(self, file_name): """ Load a previously stored source matrix into an OpenMEEG Matrix object. """ source_matrix = om.Matrix() source_matrix.load(file_name) return source_matrix
def test_skullskin(self): dt = surfaces.SkullSkin() self.assertEqual(dt.get_data_shape('vertices'), (4096, 3)) self.assertEqual(dt.get_data_shape('vertex_normals'), (4096, 3)) self.assertEqual(dt.get_data_shape('triangles'), (8188, 3))
class ProjectionMatrix(MappedType): """ Base DataType for representing a ProjectionMatrix. The projection is between a source of type CorticalSurface and a set of Sensors. """ projection_type = basic.String __mapper_args__ = {'polymorphic_on': 'projection_type'} brain_skull = surfaces.BrainSkull( label="Brain Skull", default=None, required=False, doc="""Boundary between skull and cortex domains.""") skull_skin = surfaces.SkullSkin( label="Skull Skin", default=None, required=False, doc="""Boundary between skull and skin domains.""") skin_air = surfaces.SkinAir( label="Skin Air", default=None, required=False, doc="""Boundary between skin and air domains.""") conductances = basic.Dict( label="Domain conductances", required=False, default={ 'air': 0.0, 'skin': 1.0, 'skull': 0.01, 'brain': 1.0 }, doc=""" A dictionary representing the conductances of ... """) sources = surfaces.CorticalSurface(label="surface or region", default=None, required=True) sensors = sensors.Sensors( label="Sensors", default=None, required=False, doc=""" A set of sensors to compute projection matrix for them. """) projection_data = arrays.FloatArray(label="Projection Matrix Data", default=None, required=True) @property def shape(self): return self.projection_data.shape @classmethod def from_file(cls, source_file, matlab_data_name=None, is_brainstorm=False, instance=None): if instance is None: proj = cls() else: proj = instance source_full_path = try_get_absolute_path("tvb_data.projectionMatrix", source_file) reader = FileReader(source_full_path) if is_brainstorm: proj.projection_data = reader.read_gain_from_brainstorm() else: proj.projection_data = reader.read_array( matlab_data_name=matlab_data_name) return proj
class ProjectionMatrix(MappedType): """ Base DataType for representing a ProjectionMatrix. The projection is between a source of type CorticalSurface and a set of Sensors. """ projection_type = basic.String __mapper_args__ = {'polymorphic_on': 'projection_type'} brain_skull = surfaces.BrainSkull( label="Brain Skull", default=None, required=False, doc="""Boundary between skull and cortex domains.""") skull_skin = surfaces.SkullSkin( label="Skull Skin", default=None, required=False, doc="""Boundary between skull and skin domains.""") skin_air = surfaces.SkinAir( label="Skin Air", default=None, required=False, doc="""Boundary between skin and air domains.""") conductances = basic.Dict( label="Domain conductances", required=False, default={ 'air': 0.0, 'skin': 1.0, 'skull': 0.01, 'brain': 1.0 }, doc=""" A dictionary representing the conductances of ... """) sources = surfaces.CorticalSurface(label="surface or region", default=None, required=True) sensors = sensors.Sensors( label="Sensors", default=None, required=False, doc=""" A set of sensors to compute projection matrix for them. """) projection_data = arrays.FloatArray(label="Projection Matrix Data", default=None, required=True) @property def shape(self): return self.projection_data.shape @staticmethod def load_surface_projection_matrix(result, source_file): source_full_path = try_get_absolute_path("tvb_data.projectionMatrix", source_file) if source_file.endswith(".mat"): # consider we have a brainstorm format mat = scipy.io.loadmat(source_full_path) gain, loc, ori = (mat[field] for field in 'Gain GridLoc GridOrient'.split()) result.projection_data = (gain.reshape( (gain.shape[0], -1, 3)) * ori).sum(axis=-1) elif source_file.endswith(".npy"): # numpy array with the projection matrix already computed result.projection_data = numpy.load(source_full_path) else: raise Exception( "The projection matrix must be either a numpy array or a brainstorm mat file" ) return result @classmethod def from_file(cls, source_file): proj = cls() ProjectionMatrix.load_surface_projection_matrix(proj, source_file) return proj
def test_skullskin(self): dt = surfaces.SkullSkin(load_default=True) assert isinstance(dt, surfaces.SkullSkin) assert dt.get_data_shape('vertices') == (4096, 3) assert dt.get_data_shape('vertex_normals') == (4096, 3) assert dt.get_data_shape('triangles') == (8188, 3)
def test_skullskin(self): dt = surfaces.SkullSkin(load_default=True) self.assertTrue(isinstance(dt, surfaces.SkullSkin)) self.assertEqual(dt.get_data_shape('vertices'), (4096, 3)) self.assertEqual(dt.get_data_shape('vertex_normals'), (4096, 3)) self.assertEqual(dt.get_data_shape('triangles'), (8188, 3))
def test_skullskin(self): dt = surfaces.SkullSkin(load_file="outer_skull_4096.zip") assert isinstance(dt, surfaces.SkullSkin) assert dt.vertices.shape == (4096, 3) assert dt.vertex_normals.shape == (4096, 3) assert dt.triangles.shape == (8188, 3)