def __init__( self, # Mandatory parameters. x: SequenceTypes, y: SequenceTypes, ) -> None: ''' Initialize this vector field. Parameters ---------- x : ndarray Two-dimensional sequence whose: * First dimension indexes one or more time steps of this simulation. * Second dimension indexes each X component of each vector in this vector field for this time step. y : ndarray Two-dimensional sequence whose: * First dimension indexes one or more time steps of this simulation. * Second dimension indexes each Y component of each vector in this vector field for this time step. ''' # Classify the passed sequences as Numpy arrays for efficiency. self._x = nparray.from_iterable(x) self._y = nparray.from_iterable(y)
def voltage_polarity(self) -> VectorFieldCellsCache: ''' Vector field cache of all cellular voltage polarities over all time steps of the current simulation phase, originally spatially situated at cell membrane midpoints. ''' # Two-dimensional Numpy arrays of all transmembrane voltages (Vmem) and # Vmem averages across all cell membranes over all time steps. vm_time = nparray.from_iterable(self._phase.sim.vm_time) vm_ave_time = nparray.from_iterable(self._phase.sim.vm_ave_time) # Two-dimensional Numpy array of all transmembrane voltage polarity # vector magnitudes whose: # # * First dimension indexes each time step. # * Second dimension indexes each cell membrane such that each element # is the magnitude of the polarity of the transmembrane voltage across # that membrane for this time step, where this magnitude is defined as # the difference between: # * The transmembrane voltage across that membrane for this time step. # * The average transmembrane voltage across all membranes of the cell # containing that membrane for this time step. polarity_membranes_midpoint_magnitudes = ( vm_time - vm_ave_time[:, self._phase.cells.mem_to_cells]) # Two-dimensional Numpy arrays of the X and Y components of all Vmem # polarity vectors, spatially situated at cell membrane midpoints. polarity_membranes_midpoint_x = ( polarity_membranes_midpoint_magnitudes * self._phase.cells.membranes_normal_unit_x) polarity_membranes_midpoint_y = ( polarity_membranes_midpoint_magnitudes * self._phase.cells.membranes_normal_unit_y) # Two-dimensional Numpy arrays of the X and Y components of all Vmem # polarity vectors, spatially situated at cell centres. polarity_cells_centre_x = ( self._phase.cells.map_membranes_midpoint_to_cells_centre( polarity_membranes_midpoint_x * self._phase.cells.mem_sa) / self._phase.cells.cell_sa) polarity_cells_centre_y = ( self._phase.cells.map_membranes_midpoint_to_cells_centre( polarity_membranes_midpoint_y * self._phase.cells.mem_sa) / self._phase.cells.cell_sa) # Create, return, and cache this vector field. return VectorFieldCellsCache( x=VectorCellsCache(phase=self._phase, times_cells_centre=polarity_cells_centre_x), y=VectorCellsCache(phase=self._phase, times_cells_centre=polarity_cells_centre_y))
def is_cyclic_quad( A: SequenceTypes, B: SequenceTypes, C: SequenceTypes, D: SequenceTypes, ) -> bool: ''' ``True`` only if the quadrilateral represented by the passed four vertices is **cyclic** (i.e., counterclockwise-oriented). Parameters ---------- A, B, C, D : SequenceTypes Four vertices of the quadrilateral to be tested, assumed to be oriented counterclockwise. Returns ---------- bool ``True`` only if this quadrilateral is cyclic. ''' # Avoid circular import dependencies. from betse.lib.numpy import nparray, npscalar # Coerce these sequences to Numpy arrays for efficiency. A = nparray.from_iterable(A) B = nparray.from_iterable(B) C = nparray.from_iterable(C) D = nparray.from_iterable(D) # Lengths of the four edges constructed from these vertices. a = np.linalg.norm(B - A) b = np.linalg.norm(C - B) c = np.linalg.norm(D - C) d = np.linalg.norm(A - D) # Calculate the length of the two diagonals. e = np.sqrt(((a * c + b * d) * (a * d + b * c)) / (a * b + c * d)) f = np.sqrt(((a * c + b * d) * (a * b + c * d)) / (a * d + b * c)) # For a cyclic quad, the product between the two diagonals equals # the product between the two adjacent sides. lhs = np.round(e * f, 15) rhs = np.round(a * c + b * d, 15) # Non-standard Numpy-specific boolean encapsulating this truth value. test_bool = lhs == rhs # Coerce this into a standard Numpy-agnostic boolean for safety. return npscalar.to_python(test_bool)
def _get_cell_times_vmems(self, phase: SimPhase) -> NumpyArrayType: ''' One-dimensional Numpy array of all transmembrane voltages for each sampled time step spatially situated at the centre of the single cell indexed by the ``plot cell index`` entry specified by the passed simulation phase. Parameters ---------- phase : SimPhase Current simulation phase. ''' # 0-based index of the cell to serialize time data for. cell_index = phase.p.visual.single_cell_index if phase.p.is_ecm: cell_times_vmems = [] for vm_at_mem in phase.sim.vm_time: vm_t = mathunit.upscale_units_milli( cell_ave(phase.cells, vm_at_mem)[cell_index]) cell_times_vmems.append(vm_t) else: cell_times_vmems = mathunit.upscale_units_milli(phase.sim.vm_time) return nparray.from_iterable(cell_times_vmems)
def pick_cells_and_mems( self, cells: 'betse.science.cells.Cells', p: 'betse.science.parameters.Parameters', ) -> tuple: ''' 2-tuple ``(cells_index, mems_index)`` of one-dimensional Numpy arrays of the indices of both all cells *and* cell membranes in the passed cell cluster selected by this tissue picker, ignoring extracellular spaces. By default, this method returns the array returned by the subclass implementation of the abstract :meth:`pick_cells` method, mapped from cell to cell membrane indices. Parameters ---------- cells : Cells Current cell cluster. p : Parameters Current simulation configuration. Returns ---------- (ndarray, ndarray) 2-tuple ``(cells_index, mems_index)``, where: * ``cells_index`` is the one-dimensional Numpy array of the indices of all subclass-selected cells. * ``mems_index`` is the one-dimensional Numpy array of the indices of all subclass-selected cell membranes. ''' # One-dimensional Numpy array of the indices of subclass-selected cells. cells_index = self.pick_cells(cells, p) #FIXME: Ideally, this array would be trivially defined as follows: # mems_index = cells.cell_to_mems[cells_index].flatten() #Unfortunately, the two-dimensional Numpy array "cells.cell_to_mems" #actually appears to be a one-dimensional array of lists -- which is a #bit bizarro-world. To compaund matters, this array's "dtype" is #"object" (presumably, because it contains Python lists) rather than #the "dtype" of "int" that one might expect. Since this is the case, #it's infeasible to even coerce the temporary one-dimensional Numpy #array "cells.cell_to_mems[cells_index]" into a two-dimensional array #of ints by calling ndarray.astype(int). Frankly, it's all a bit beyond #me; until this core issue is resolved, however, the current inelegant #and inefficient approach remains. # One-dimensional Numpy array of the indices of subclass-selected cell # membranes mapped from the array of cell indices. # logs.log_debug('cells_index: %r', cells_index) mems_index = cells.cell_to_mems[cells_index] mems_index, _, _ = toolbox.flatten(mems_index) mems_index = nparray.from_iterable(mems_index) # Return these arrays. return cells_index, mems_index
def _upscale_data_in_units( data: NumericOrIterableTypes, factor: NumericSimpleTypes) -> (NumericOrIterableTypes): ''' Upscale the contents of the passed object by the passed multiplier, typically under the assumption that these contents are denominated in the reciprocal units of this multiplier (i.e., ``10**6`` for micrometers). Parameters ---------- data : NumericOrIterableTypes Number or sequence to be upscaled. factor : NumericSimpleTypes Reciprocal of the units this object is denominated in. Returns ---------- NumericOrIterableTypes Either: * If this data is numeric, this number upscaled by this multiplier. * If this data is sequential, this sequence converted into a Numpy array whose elements are upscaled by this multiplier. See Also ---------- :func:`upscale_units_milli` Further details. ''' # If the passed object is numeric, return this number upscaled. if types.is_numeric(data): return factor * data # Else, this object is a sequence. Return this sequence converted into a # Numpy array and then upscaled. else: return factor * nparray.from_iterable(data)
def orient_counterclockwise(polygon: SequenceTypes) -> SequenceTypes: ''' Positively reorient the passed polygon, returning a copy of this polygon whose vertices are **positively oriented** (i.e., sorted in counter-clockwise order). Parameters ---------- polygon : SequenceTypes Two-dimensional sequence of all points defining the possibly non-convex two-dimensional polygon to be positively oriented such that: * The first dimension indexes each such point (in arbitrary order). * The second dimension indexes each coordinate of this point such that: * The first item is the X coordinate of this point. * The second item is the Y coordinate of this point. Note that this function expects the type of this sequence to define an ``__init__()`` method accepting a passed iterable as its first and only positional argument. Unsurprisingly, all builtin sequences (e.g., :class:`tuple`, :class:`list`) *and* :mod:`numpy` sequences (e.g., :class:`numpy.ndarray`) satisfy this requirement. Returns ---------- SequenceTypes Copy of this polygon positively oriented. The type of this sequence is the same as that of the original polygon. ''' # Avoid circular import dependencies. from betse.lib.numpy import nparray # If this sequence is *NOT* a polygon, raise an exception. die_unless_polygon(polygon) # Numpy array corresponding to this sequence. While polygon reorientation # is feasible in pure-Python, the Numpy-based approach is significantly # more efficient as the number of polygon edges increases. poly_verts = nparray.from_iterable(polygon) # Centre point of this polygon, poly_centre = poly_verts.mean(axis=0) # One-dimensional Numpy array indexing each vertex of this polygon such # that each element is the angle in radians between the positive X-axis and # that vertex, derived according to the classic mnemonic SOHCAHTOA: # # opposite (opposite) # -------- (--------) # tan(θ) = adjacent --> arctan(adjacent) = θ # # To sort vertices in a counter-clockwise rotation around the centre point # of this polygon rather than around the origin (i.e., the point (0, 0)), # each vertex is translated from the origin to this centre point *BEFORE* # obtaining this vertex's angle. Assuming standard orientation for a right # triangle sitting at the centre point of this polygon: # # * The opposite edge is the Y coordinate of the current vertex translated # from the origin onto the centre point. # * The adjacent edge is the X coordinate of the current vertex translated # from the origin onto the centre point. # # For safety, the np.arctan2() function intended exactly for this purpose # rather than the np.arctan() function intended for general-purpose # calculation is called. For further details, see: # https://en.wikipedia.org/wiki/Atan2 poly_angles = np.arctan2(poly_verts[:, 1] - poly_centre[1], poly_verts[:, 0] - poly_centre[0]) # One-dimensional Numpy array indexing the index of each vertex of this # polygon, sorted in counter-clockwise order. poly_verts_sorted_index = poly_angles.argsort() # Two-dimensional Numpy array of the polygon to be returned, containing all # vertices sorted in this order. poly_verts_sorted = poly_verts[poly_verts_sorted_index] # Return a sequence of the same type as the passed polygon. return nparray.to_iterable(array=poly_verts_sorted, cls=type(polygon))
def export_cell_series(self, phase: SimPhase, conf: SimConfExportCSV) -> None: ''' Save a plaintext file in comma-separated value (CSV) format containing several cell-specific time series (e.g., ion concentrations, membrane voltages, voltage-gated ion channel pump rates) for the single cell indexed ``plot cell index`` in the current simulation configuration. ''' # 0-based index of the cell to serialize time data for. cell_index = phase.p.visual.single_cell_index # Sequence of key-value pairs containing all simulation data to be # exported for this cell, suitable for passing to the # OrderedArgsDict.__init__() method calleb below. csv_column_name_values = [] # One-dimensional Numpy array of null data of the required length, # suitable for use as CSV column data for columns whose corresponding # simulation feature (e.g., deformations) is disabled. column_data_empty = np.zeros(len(phase.sim.time)) # ................{ TIME STEPS }.................. csv_column_name_values.extend(('time_s', phase.sim.time)) # ................{ VMEM }.................. csv_column_name_values.extend( ('Vmem_mV', self._get_cell_times_vmems(phase))) # ................{ VMEM ~ goldman }.................. if phase.p.GHK_calc: vm_goldman = mathunit.upscale_units_milli([ vm_GHK_time_cells[cell_index] for vm_GHK_time_cells in phase.sim.vm_GHK_time ]) else: vm_goldman = column_data_empty csv_column_name_values.extend(('Goldman_Vmem_mV', vm_goldman)) # ................{ Na K PUMP RATE }.................. if phase.p.is_ecm: pump_rate = [ pump_array[phase.cells.cell_to_mems[cell_index][0]] for pump_array in phase.sim.rate_NaKATP_time ] else: pump_rate = [ pump_array[cell_index] for pump_array in phase.sim.rate_NaKATP_time ] csv_column_name_values.extend(('NaK-ATPase_Rate_mol/m2s', pump_rate)) # ................{ ION CONCENTRATIONS }.................. # Create the header starting with cell concentrations. for i in range(len(phase.sim.ionlabel)): csv_column_name = 'cell_{}_mmol/L'.format(phase.sim.ionlabel[i]) cc_m = [arr[i][cell_index] for arr in phase.sim.cc_time] csv_column_name_values.extend((csv_column_name, cc_m)) # ................{ MEMBRANE PERMEABILITIES }.................. # Create the header starting with membrane permeabilities. for i in range(len(phase.sim.ionlabel)): if phase.p.is_ecm: dd_m = [ arr[i][phase.cells.cell_to_mems[cell_index][0]] for arr in phase.sim.dd_time ] else: dd_m = [arr[i][cell_index] for arr in phase.sim.dd_time] csv_column_name = 'Dm_{}_m2/s'.format(phase.sim.ionlabel[i]) csv_column_name_values.extend((csv_column_name, dd_m)) # ................{ TRANSMEMBRANE CURRENTS }.................. if phase.p.is_ecm: Imem = [ memArray[phase.cells.cell_to_mems[cell_index][0]] for memArray in phase.sim.I_mem_time ] else: Imem = [memArray[cell_index] for memArray in phase.sim.I_mem_time] csv_column_name_values.extend(('I_A/m2', Imem)) # ................{ HYDROSTATIC PRESSURE }.................. p_hydro = [arr[cell_index] for arr in phase.sim.P_cells_time] csv_column_name_values.extend(('HydroP_Pa', p_hydro)) # ................{ OSMOTIC PRESSURE }.................. if phase.p.deform_osmo: p_osmo = [arr[cell_index] for arr in phase.sim.osmo_P_delta_time] else: p_osmo = column_data_empty csv_column_name_values.extend(('OsmoP_Pa', p_osmo)) # ................{ DEFORMATION }.................. if (phase.p.deformation and phase.kind is SimPhaseKind.SIM): # Extract time-series deformation data for the plot cell: dx = nparray.from_iterable( [arr[cell_index] for arr in phase.sim.dx_cell_time]) dy = nparray.from_iterable( [arr[cell_index] for arr in phase.sim.dy_cell_time]) # Get the total magnitude. disp = mathunit.upscale_coordinates(np.sqrt(dx**2 + dy**2)) else: disp = column_data_empty csv_column_name_values.extend(('Displacement_um', disp)) # ................{ CSV EXPORT }.................. # Ordered dictionary mapping from CSV column names to data arrays. csv_column_name_to_values = OrderedArgsDict(*csv_column_name_values) # Export this data to this CSV file. npcsv.write_csv( filename=self._get_csv_filename( phase=phase, basename_sans_filetype='ExportedData'), column_name_to_values=csv_column_name_to_values, )
def to_iterable( # Mandatory parameters. iterable: IterableTypes, # Optional parameters. cls: ClassOrNoneTypes = None, item_cls: ClassOrNoneTypes = None, ) -> IterableTypes: ''' Convert the passed input iterable into an output iterable of the passed iterable type and/or convert each item of this input iterable into an item of this output iterable of the passed item type. Specifically, if this iterable type is: * Either ``None`` *or* of the same type as the passed iterable type, then: * If this item type is non-``None``, this input iterable is converted into an instance of the same iterable type whose items are converted into instances of this item type. * Else, this input iterable is returned as is (i.e., unmodified). * A non-Numpy iterable (e.g., :class:`list`) and the passed iterable type is that of: * Another non-Numpy iterable (e.g., :class:`tuple`), then: * If this item type is non-``None``, this input iterable is converted into an instance of this iterable type whose items are converted into instances of this item type. * Else, this input iterable is merely converted into an instance of this iterable type. In either case, this output iterable's ``__init__`` method is required to accept this input iterable as a single positional argument. * A Numpy array, then: * If this item type is non-``None``, an exception is raised. Numpy scalar types map poorly to Python scalar types. * Else, this input iterable is converted to a Numpy array via the :func:`betse.lib.numpy.nparray.from_iterable` function. * A Numpy array, then: * If this item type is non-``None``, an exception is raised. Numpy scalar types map poorly to Python scalar types. * Else, this input iterable is converted to a Numpy array via the :func:`betse.lib.numpy.nparray.from_iterable` function. Parameters ---------- iterable: IterableTypes Input iterable to be converted. cls : ClassOrNoneTypes Type of the output iterable to convert this input iterable into. Defaults to ``None``, in which case the same type as that of this input iterable is defaulted to. item_cls : ClassOrNoneTypes Type to convert each item of the output iterable into. Defaults to ``None``, in which case these items are preserved as is. Returns ---------- IterableTypes Output iterable converted from this input iterable. Raises ---------- BetseIterableException If converting either to or from a Numpy array *and* this item type is non-``None``. ''' # Avoid importing third-party packages at the top level, for safety. from betse.lib.numpy import nparray from betse.util.type.cls import classes from betse.util.type.iterable import generators from numpy import ndarray # Type of the input iterable. iterable_src_cls = type(iterable) # Type of the output iterable. iterable_trg_cls = cls # If the caller requested no explicit type conversion... if iterable_trg_cls is None: # If the input iterable is a generator, default the type of the output # iterable to the optimally space- and time-efficient iterable: tuple. # Why? Because generators *CANNOT* be explicitly instantiated. if generators.is_generator(iterable): iterable_trg_cls = tuple # Else, the input iterable is *NOT* a generator and hence is assumed to # be of a standard type that *CAN* be explicitly instantiated. In this # case, default the type of the output iterable to this type. else: iterable_trg_cls = iterable_src_cls # if cls is not None else iterable_src_cls # If the input and output iterables are of the same type *AND* no item # conversion was requested, efficiently reduce to a noop. if iterable_src_cls is iterable_trg_cls and item_cls is None: return iterable # Else if the input iterable is a Numpy array... if iterable_src_cls is ndarray: # If the caller requested item conversion, raise an exception. if item_cls is not None: raise BetseIterableException( 'Numpy array not convertible to item class "{}".'.format( classes.get_name_unqualified(item_cls))) # Defer to logic elsewhere. return nparray.to_iterable(array=iterable, cls=iterable_trg_cls) # Else if the output iterable is a Numpy array, defer to logic elsewhere. if iterable_trg_cls is ndarray: # If the caller requested item conversion, raise an exception. if item_cls is not None: raise BetseIterableException( 'Numpy array not convertible to item class "{}".'.format( classes.get_name_unqualified(item_cls))) return nparray.from_iterable(iterable) # Else, neither the input or output iterables are Numpy arrays. In this # case, return an output iterable of the desired type containing the items # of this input iterable either... return ( # Unmodified if no item conversion was requested *OR*... iterable_trg_cls(iterable) if item_cls is None else # Converted to this item type otherwise. iterable_trg_cls(item_cls(item) for item in iterable))
def __init__(self, times_cells_centre: IterableOrNoneTypes = None, times_grids_centre: IterableOrNoneTypes = None, times_membranes_midpoint: IterableOrNoneTypes = None, **kwargs) -> None: ''' Initialize this cache. Parameters ---------- times_cells_centre : optional[IterableTypes] Two-dimensional iterable of all cell data for a single cell membrane-specific modelled variable (e.g., cell electric field magnitude) for all simulation time steps, whose: #. First dimension indexes each sampled time step. #. Second dimension indexes each cell, such that each element is arbitrary cell data spatially situated at the centre of this cell for this time step. Defaults to ``None``, in which case at least one of the ``times_grids_centre`` and ``times_membranes_midpoint`` parameters must be non-``None``. times_grids_centre : optional[IterableTypes] Two-dimensional iterable of all grid data for a single intra- and/or extracellular modelled variable (e.g., total current density) for all simulation time steps, whose: #. First dimension indexes each sampled time step. #. Second dimension indexes each grid space (in either dimension), such that each element is arbitrary grid data spatially situated at the centre of this grid space for this time step. Defaults to ``None``, in which case at least one of the ``times_cells_centre`` and ``times_membranes_midpoint`` parameters must be non-``None``. times_membranes_midpoint : optional[IterableTypes] Two-dimensional iterable of all cell membrane data for a single cell membrane-specific modelled variable (e.g., cell membrane voltage) for all simulation time steps, whose: #. First dimension indexes each sampled time step. #. Second dimension indexes each cell membrane, such that each element is arbitrary cell membrane data spatially situated at the midpoint of this membrane for this time step. Defaults to ``None``, in which case at least one of the ``times_cells_centre`` and ``times_grids_centre`` parameters must be non-``None``. All remaining keyword arguments are passed as is to the superclass :meth:`SimPhaseCacheABC.__init__` method. Raises ---------- BetseSimVectorException If exactly one of the ``times_cells_centre``, ``times_grids_centre``, and ``times_membranes_midpoint`` parameters is *not* passed. BetseSequenceException If any of the ``times_cells_centre``, ``times_grids_centre``, and ``times_membranes_midpoint`` parameters that are passed are empty (i.e., sequences of length 0). ''' # Initialize our superclass. super().__init__(**kwargs) # If no iterable was passed, raise an exception. if (times_cells_centre is None and times_grids_centre is None and times_membranes_midpoint is None): raise BetseSimVectorException( 'Parameters "times_cells_centre", "times_grids_centre", and ' '"times_membranes_midpoint" not passed.') # Convert each passed iterable into a Numpy array for efficiency, # raising an exception if any such iterable is empty. if times_cells_centre is not None: times_cells_centre = nparray.from_iterable(times_cells_centre) sequences.die_if_empty( sequence=times_cells_centre, exception_message='Sequence "times_cells_centre" empty.') if times_grids_centre is not None: times_grids_centre = nparray.from_iterable(times_grids_centre) sequences.die_if_empty( sequence=times_grids_centre, exception_message='Sequence "times_grids_centre" empty.') if times_membranes_midpoint is not None: times_membranes_midpoint = nparray.from_iterable( times_membranes_midpoint) sequences.die_if_empty( sequence=times_membranes_midpoint, exception_message='Sequence "times_membranes_midpoint" empty.') # Classify all passed parameters. self._times_cells_centre = times_cells_centre self._times_grids_centre = times_grids_centre self._times_membranes_midpoint = times_membranes_midpoint