From e23361adacb1a8c0f6554e92dd07eb33b66f3ee5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 21 Nov 2024 16:18:25 +0000 Subject: [PATCH] Deployed 6e79424 to dev with MkDocs 1.6.1 and mike 2.1.3 --- dev/reference/connect/index.html | 2 +- dev/reference/integration/index.html | 14 +- dev/reference/modules/index.html | 492 ++++++----- dev/reference/utils/index.html | 174 ++-- dev/search/search_index.json | 2 +- dev/tutorial/00_jaxley_api/index.html | 633 +++----------- .../00_jaxley_api_26_1.png | Bin 10057 -> 10067 bytes dev/tutorial/01_morph_neurons/index.html | 12 +- .../01_morph_neurons_21_0.png | Bin 8916 -> 8843 bytes .../01_morph_neurons_37_0.png | Bin 9137 -> 8831 bytes dev/tutorial/02_small_network/index.html | 38 +- .../02_small_network_14_0.png | Bin 34291 -> 34754 bytes .../02_small_network_17_0.png | Bin 35279 -> 35882 bytes .../02_small_network_36_0.png | Bin 18866 -> 17220 bytes dev/tutorial/04_jit_and_vmap/index.html | 2 +- .../05_channel_and_synapse_models/index.html | 2 +- dev/tutorial/06_groups/index.html | 774 +----------------- dev/tutorial/07_gradient_descent/index.html | 2 +- .../08_importing_morphologies/index.html | 152 +++- .../08_importing_morphologies_11_0.png | Bin 73215 -> 23037 bytes .../08_importing_morphologies_13_0.png | Bin 55297 -> 55950 bytes .../08_importing_morphologies_15_0.png | Bin 0 -> 73954 bytes .../08_importing_morphologies_15_1.png | Bin 52550 -> 0 bytes .../08_importing_morphologies_17_0.png | Bin 31380 -> 57783 bytes .../08_importing_morphologies_19_1.png | Bin 0 -> 51434 bytes .../08_importing_morphologies_21_0.png | Bin 0 -> 31380 bytes .../08_importing_morphologies_7_0.png | Bin 23037 -> 0 bytes .../08_importing_morphologies_9_0.png | Bin 55895 -> 0 bytes .../10_advanced_parameter_sharing/index.html | 2 +- 29 files changed, 648 insertions(+), 1653 deletions(-) create mode 100644 dev/tutorial/08_importing_morphologies_files/08_importing_morphologies_15_0.png delete mode 100644 dev/tutorial/08_importing_morphologies_files/08_importing_morphologies_15_1.png create mode 100644 dev/tutorial/08_importing_morphologies_files/08_importing_morphologies_19_1.png create mode 100644 dev/tutorial/08_importing_morphologies_files/08_importing_morphologies_21_0.png delete mode 100644 dev/tutorial/08_importing_morphologies_files/08_importing_morphologies_7_0.png delete mode 100644 dev/tutorial/08_importing_morphologies_files/08_importing_morphologies_9_0.png diff --git a/dev/reference/connect/index.html b/dev/reference/connect/index.html index a6e4e02e..4d270b98 100644 --- a/dev/reference/connect/index.html +++ b/dev/reference/connect/index.html @@ -1719,7 +1719,7 @@

post_rows = post_cell_view.base.nodes.loc[global_post_indices] # Pre-synapse is at the zero-eth branch and zero-eth compartment. - global_pre_indices = pre_cell_view.base._cumsum_nseg_per_cell[pre_syn_neurons] + global_pre_indices = pre_cell_view.base._cumsum_ncomp_per_cell[pre_syn_neurons] pre_rows = pre_cell_view.base.nodes.loc[global_pre_indices] if len(pre_rows) > 0: diff --git a/dev/reference/integration/index.html b/dev/reference/integration/index.html index 0ffff89c..41a6c377 100644 --- a/dev/reference/integration/index.html +++ b/dev/reference/integration/index.html @@ -2520,7 +2520,7 @@

- step_voltage_explicit(voltages, voltage_terms, constant_terms, axial_conductances, internal_node_inds, sinks, sources, types, nseg_per_branch, par_inds, child_inds, nbranches, solver, delta_t, idx, debug_states) + step_voltage_explicit(voltages, voltage_terms, constant_terms, axial_conductances, internal_node_inds, sinks, sources, types, ncomp_per_branch, par_inds, child_inds, nbranches, solver, delta_t, idx, debug_states)

@@ -2580,7 +2580,7 @@

sinks: jnp.ndarray, sources: jnp.ndarray, types: jnp.ndarray, - nseg_per_branch: jnp.ndarray, + ncomp_per_branch: jnp.ndarray, par_inds: jnp.ndarray, child_inds: jnp.ndarray, nbranches: int, @@ -2622,7 +2622,7 @@

- step_voltage_implicit_with_jaxley_spsolve(voltages, voltage_terms, constant_terms, axial_conductances, internal_node_inds, sinks, sources, types, nseg_per_branch, par_inds, child_inds, nbranches, solver, delta_t, idx, debug_states) + step_voltage_implicit_with_jaxley_spsolve(voltages, voltage_terms, constant_terms, axial_conductances, internal_node_inds, sinks, sources, types, ncomp_per_branch, par_inds, child_inds, nbranches, solver, delta_t, idx, debug_states)

@@ -2793,7 +2793,7 @@

sinks: jnp.ndarray, sources: jnp.ndarray, types: jnp.ndarray, - nseg_per_branch: jnp.ndarray, + ncomp_per_branch: jnp.ndarray, par_inds: jnp.ndarray, child_inds: jnp.ndarray, nbranches: int, @@ -2805,7 +2805,7 @@

"""Solve one timestep of branched nerve equations with implicit (backward) Euler.""" # Build diagonals. c2c = np.isin(types, [0, 1, 2]) - total_ncomp = idx.cumsum_nseg[-1] + total_ncomp = idx.cumsum_ncomp[-1] diags = jnp.ones(total_ncomp) # if-case needed because `.at` does not allow empty inputs, but the input is @@ -2906,7 +2906,7 @@

branchpoint_diags, branchpoint_solves, solver, - nseg_per_branch, + ncomp_per_branch, idx, debug_states, ) @@ -2931,7 +2931,7 @@

branchpoint_diags, branchpoint_solves, solver, - nseg_per_branch, + ncomp_per_branch, idx, debug_states, ) diff --git a/dev/reference/modules/index.html b/dev/reference/modules/index.html index 5af0d444..934f63a1 100644 --- a/dev/reference/modules/index.html +++ b/dev/reference/modules/index.html @@ -4429,7 +4429,7 @@

Module """ def __init__(self): - self.nseg: int = None + self.ncomp: int = None self.total_nbranches: int = 0 self.nbranches_per_cell: List[int] = None @@ -4651,7 +4651,7 @@

Module Note: For sake of performance, interpolation is not done for each branch individually, but only once along a concatenated (and padded) array of all branches. - This means for nsegs = [2,4] and normalized cum_branch_lens of [[0,1],[0,1]] we would + This means for ncomps = [2,4] and normalized cum_branch_lens of [[0,1],[0,1]] we would interpolate xyz at the locations comp_ends = [[0,0.5,1], [0,0.25,0.5,0.75,1]], where 0 is the start of the branch and 1 is the end point at the full branch_len. To avoid do this in one go we set comp_ends = [0,0.5,1,2,2.25,2.5,2.75,3], and @@ -4660,10 +4660,10 @@

Module incrementing. """ nodes_by_branches = self.nodes.groupby("global_branch_index") - nsegs = nodes_by_branches["global_comp_index"].nunique().to_numpy() + ncomps = nodes_by_branches["global_comp_index"].nunique().to_numpy() comp_ends = [ - np.linspace(0, 1, nseg + 1) + 2 * i for i, nseg in enumerate(nsegs) + np.linspace(0, 1, ncomp + 1) + 2 * i for i, ncomp in enumerate(ncomps) ] comp_ends = np.hstack(comp_ends) @@ -4681,9 +4681,9 @@

Modulexyz = np.vstack(self.xyzr)[:, :3] xyz = v_interp(comp_ends, cum_branch_lens, xyz).T centers = (xyz[:-1] + xyz[1:]) / 2 # unaware of inter vs intra comp centers - cum_nsegs = np.cumsum(nsegs) + cum_ncomps = np.cumsum(ncomps) # this means centers between comps have to be removed here - between_comp_inds = (cum_nsegs + np.arange(len(cum_nsegs)))[:-1] + between_comp_inds = (cum_ncomps + np.arange(len(cum_ncomps)))[:-1] centers = np.delete(centers, between_comp_inds, axis=0) return centers @@ -4874,15 +4874,15 @@

Module View of the module at the specified branch location.""" global_comp_idxs = [] for i in self._branches_in_view: - nseg = self.base.nseg_per_branch[i] - comp_locs = np.linspace(0, 1, nseg) + ncomp = self.base.ncomp_per_branch[i] + comp_locs = np.linspace(0, 1, ncomp) at = comp_locs if is_str_all(at) else self._reformat_index(at, dtype=float) - comp_edges = np.linspace(0, 1 + 1e-10, nseg + 1) - idx = np.digitize(at, comp_edges) - 1 + self.base.cumsum_nseg[i] + comp_edges = np.linspace(0, 1 + 1e-10, ncomp + 1) + idx = np.digitize(at, comp_edges) - 1 + self.base.cumsum_ncomp[i] global_comp_idxs.append(idx) global_comp_idxs = np.concatenate(global_comp_idxs) orig_scope = self._scope - # global scope needed to select correct comps, for i.e. branches w. nseg=[1,2] + # global scope needed to select correct comps, for i.e. branches w. ncomp=[1,2] # loc(0.9) will correspond to different local branches (0 vs 1). view = self.scope("global").comp(global_comp_idxs).scope(orig_scope) view._current_view = "loc" @@ -5229,7 +5229,7 @@

Moduleview = self.nodes.copy() all_nodes = self.base.nodes start_idx = self.nodes["global_comp_index"].to_numpy()[0] - nseg_per_branch = self.base.nseg_per_branch + ncomp_per_branch = self.base.ncomp_per_branch channel_names = [c._name for c in self.base.channels] channel_param_names = list( chain(*[c.channel_params for c in self.base.channels]) @@ -5309,7 +5309,7 @@

Moduleradius_fns=radius_generating_fns, branch_indices=branch_indices, min_radius=min_radius, - nseg=ncomp, + ncomp=ncomp, ) else: view["radius"] = within_branch_radiuses[0] * np.ones(ncomp) @@ -5330,15 +5330,15 @@

Moduleall_nodes["global_comp_index"] = np.arange(len(all_nodes)) # Update compartment structure arguments. - nseg_per_branch[branch_indices] = ncomp - nseg = int(np.max(nseg_per_branch)) - cumsum_nseg = cumsum_leading_zero(nseg_per_branch) - internal_node_inds = np.arange(cumsum_nseg[-1]) + ncomp_per_branch[branch_indices] = ncomp + ncomp = int(np.max(ncomp_per_branch)) + cumsum_ncomp = cumsum_leading_zero(ncomp_per_branch) + internal_node_inds = np.arange(cumsum_ncomp[-1]) self.base.nodes = all_nodes - self.base.nseg_per_branch = nseg_per_branch - self.base.nseg = nseg - self.base.cumsum_nseg = cumsum_nseg + self.base.ncomp_per_branch = ncomp_per_branch + self.base.ncomp = ncomp + self.base.cumsum_ncomp = cumsum_ncomp self.base._internal_node_inds = internal_node_inds # Update the morphology indexing (e.g., `.comp_edges`). @@ -5370,11 +5370,11 @@

Moduleassert ( self.allow_make_trainable ), "network.cell('all').make_trainable() is not supported. Use a for-loop over cells." - nsegs_per_branch = ( + ncomps_per_branch = ( self.base.nodes["global_branch_index"].value_counts().to_numpy() ) assert np.all( - nsegs_per_branch == nsegs_per_branch[0] + ncomps_per_branch == ncomps_per_branch[0] ), "Parameter sharing is not allowed for modules containing branches with different numbers of compartments." data = self.nodes if key in self.nodes.columns else None @@ -5755,7 +5755,7 @@

Module branchpoint_weights_parents[debug_states["par_inds"]], branchpoint_diags, branchpoint_solves, - debug_states["nseg"], + debug_states["ncomp"], nbranches, ) ) @@ -5765,7 +5765,7 @@

Module ) solution = spsolve(sparse_matrix, solve) solution = solution[:start_ind_for_branchpoints] # Delete branchpoint voltages. - solves = jnp.reshape(solution, (debug_states["nseg"], nbranches)) + solves = jnp.reshape(solution, (debug_states["ncomp"], nbranches)) return solves ``` """ @@ -5775,7 +5775,7 @@

Moduleself.base._child_belongs_to_branchpoint, self.base._par_inds, self.base._child_inds, - self.base.nseg, + self.base.ncomp, self.base.total_nbranches, ) @@ -5791,7 +5791,7 @@

Moduleself.base.debug_states["indices"] = indices self.base.debug_states["indptr"] = indptr - self.base.debug_states["nseg"] = self.base.nseg + self.base.debug_states["ncomp"] = self.base.ncomp self.base.debug_states["child_inds"] = self.base._child_inds self.base.debug_states["par_inds"] = self.base._par_inds @@ -6175,7 +6175,7 @@

Module"sinks": np.asarray(self._comp_edges["sink"].to_list()), "sources": np.asarray(self._comp_edges["source"].to_list()), "types": np.asarray(self._comp_edges["type"].to_list()), - "nseg_per_branch": self.nseg_per_branch, + "ncomp_per_branch": self.ncomp_per_branch, "par_inds": self._par_inds, "child_inds": self._child_inds, "nbranches": self.total_nbranches, @@ -9584,15 +9584,15 @@

View of the module at the specified branch location.""" global_comp_idxs = [] for i in self._branches_in_view: - nseg = self.base.nseg_per_branch[i] - comp_locs = np.linspace(0, 1, nseg) + ncomp = self.base.ncomp_per_branch[i] + comp_locs = np.linspace(0, 1, ncomp) at = comp_locs if is_str_all(at) else self._reformat_index(at, dtype=float) - comp_edges = np.linspace(0, 1 + 1e-10, nseg + 1) - idx = np.digitize(at, comp_edges) - 1 + self.base.cumsum_nseg[i] + comp_edges = np.linspace(0, 1 + 1e-10, ncomp + 1) + idx = np.digitize(at, comp_edges) - 1 + self.base.cumsum_ncomp[i] global_comp_idxs.append(idx) global_comp_idxs = np.concatenate(global_comp_idxs) orig_scope = self._scope - # global scope needed to select correct comps, for i.e. branches w. nseg=[1,2] + # global scope needed to select correct comps, for i.e. branches w. ncomp=[1,2] # loc(0.9) will correspond to different local branches (0 vs 1). view = self.scope("global").comp(global_comp_idxs).scope(orig_scope) view._current_view = "loc" @@ -9787,11 +9787,11 @@

assert ( self.allow_make_trainable ), "network.cell('all').make_trainable() is not supported. Use a for-loop over cells." - nsegs_per_branch = ( + ncomps_per_branch = ( self.base.nodes["global_branch_index"].value_counts().to_numpy() ) assert np.all( - nsegs_per_branch == nsegs_per_branch[0] + ncomps_per_branch == ncomps_per_branch[0] ), "Parameter sharing is not allowed for modules containing branches with different numbers of compartments." data = self.nodes if key in self.nodes.columns else None @@ -10888,7 +10888,7 @@

view = self.nodes.copy() all_nodes = self.base.nodes start_idx = self.nodes["global_comp_index"].to_numpy()[0] - nseg_per_branch = self.base.nseg_per_branch + ncomp_per_branch = self.base.ncomp_per_branch channel_names = [c._name for c in self.base.channels] channel_param_names = list( chain(*[c.channel_params for c in self.base.channels]) @@ -10968,7 +10968,7 @@

radius_fns=radius_generating_fns, branch_indices=branch_indices, min_radius=min_radius, - nseg=ncomp, + ncomp=ncomp, ) else: view["radius"] = within_branch_radiuses[0] * np.ones(ncomp) @@ -10989,15 +10989,15 @@

all_nodes["global_comp_index"] = np.arange(len(all_nodes)) # Update compartment structure arguments. - nseg_per_branch[branch_indices] = ncomp - nseg = int(np.max(nseg_per_branch)) - cumsum_nseg = cumsum_leading_zero(nseg_per_branch) - internal_node_inds = np.arange(cumsum_nseg[-1]) + ncomp_per_branch[branch_indices] = ncomp + ncomp = int(np.max(ncomp_per_branch)) + cumsum_ncomp = cumsum_leading_zero(ncomp_per_branch) + internal_node_inds = np.arange(cumsum_ncomp[-1]) self.base.nodes = all_nodes - self.base.nseg_per_branch = nseg_per_branch - self.base.nseg = nseg - self.base.cumsum_nseg = cumsum_nseg + self.base.ncomp_per_branch = ncomp_per_branch + self.base.ncomp = ncomp + self.base.cumsum_ncomp = cumsum_ncomp self.base._internal_node_inds = internal_node_inds # Update the morphology indexing (e.g., `.comp_edges`). @@ -11723,7 +11723,7 @@

"sinks": np.asarray(self._comp_edges["sink"].to_list()), "sources": np.asarray(self._comp_edges["source"].to_list()), "types": np.asarray(self._comp_edges["type"].to_list()), - "nseg_per_branch": self.nseg_per_branch, + "ncomp_per_branch": self.ncomp_per_branch, "par_inds": self._par_inds, "child_inds": self._child_inds, "nbranches": self.total_nbranches, @@ -12438,12 +12438,12 @@

def __init__(self): super().__init__() - self.nseg = 1 - self.nseg_per_branch = np.asarray([1]) + self.ncomp = 1 + self.ncomp_per_branch = np.asarray([1]) self.total_nbranches = 1 self.nbranches_per_cell = [1] self._cumsum_nbranches = np.asarray([0, 1]) - self.cumsum_nseg = cumsum_leading_zero(self.nseg_per_branch) + self.cumsum_ncomp = cumsum_leading_zero(self.ncomp_per_branch) # Setting up the `nodes` for indexing. self.nodes = pd.DataFrame( @@ -12472,7 +12472,7 @@

def _init_morph_jaxley_spsolve(self): self._solve_indexer = JaxleySolveIndexer( - cumsum_nseg=self.cumsum_nseg, + cumsum_ncomp=self.cumsum_ncomp, branchpoint_group_inds=np.asarray([]).astype(int), children_in_level=[], parents_in_level=[], @@ -12545,8 +12545,7 @@

Source code in jaxley/modules/branch.py -
@@ -12843,109 +12861,127 @@

Source code in jaxley/modules/branch.py -

 17
- 18
+                
 18
  19
  20
  21
@@ -12654,7 +12653,17 @@ 

123 124 125 -126

class Branch(Module):
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
class Branch(Module):
     """Branch class.
 
     This class defines a single branch that can be simulated by itself or
@@ -12666,48 +12675,57 @@ 

branch_params: Dict = {} branch_states: Dict = {} + @deprecated_kwargs("0.6.0", ["nseg"]) def __init__( self, compartments: Optional[Union[Compartment, List[Compartment]]] = None, + ncomp: Optional[int] = None, nseg: Optional[int] = None, ): """ Args: compartments: A single compartment or a list of compartments that make up the branch. - nseg: Number of segments to divide the branch into. If `compartments` is an - a single compartment, than the compartment is repeated `nseg` times to + ncomp: Number of segments to divide the branch into. If `compartments` is an + a single compartment, than the compartment is repeated `ncomp` times to create the branch. """ + # Warnings and errors that deal with the change from `nseg` to `ncomp` change + # in Jaxley v0.5.0. + if ncomp is not None and nseg is not None: + raise ValueError("You passed `ncomp` and `nseg`. Please pass only `ncomp`.") + if ncomp is None and nseg is not None: + ncomp = nseg + super().__init__() assert ( isinstance(compartments, (Compartment, List)) or compartments is None ), "Only Compartment or List[Compartment] is allowed." if isinstance(compartments, Compartment): assert ( - nseg is not None - ), "If `compartments` is not a list then you have to set `nseg`." + ncomp is not None + ), "If `compartments` is not a list then you have to set `ncomp`." compartments = Compartment() if compartments is None else compartments - nseg = 1 if nseg is None else nseg + ncomp = 1 if ncomp is None else ncomp if isinstance(compartments, Compartment): - compartment_list = [compartments] * nseg + compartment_list = [compartments] * ncomp else: compartment_list = compartments - self.nseg = len(compartment_list) - self.nseg_per_branch = np.asarray([self.nseg]) + self.ncomp = len(compartment_list) + self.ncomp_per_branch = np.asarray([self.ncomp]) self.total_nbranches = 1 self.nbranches_per_cell = [1] self._cumsum_nbranches = jnp.asarray([0, 1]) - self.cumsum_nseg = cumsum_leading_zero(self.nseg_per_branch) + self.cumsum_ncomp = cumsum_leading_zero(self.ncomp_per_branch) # Indexing. self.nodes = pd.concat([c.nodes for c in compartment_list], ignore_index=True) self._append_params_and_states(self.branch_params, self.branch_states) - self.nodes["global_comp_index"] = np.arange(self.nseg).tolist() - self.nodes["global_branch_index"] = [0] * self.nseg - self.nodes["global_cell_index"] = [0] * self.nseg + self.nodes["global_comp_index"] = np.arange(self.ncomp).tolist() + self.nodes["global_branch_index"] = [0] * self.ncomp + self.nodes["global_cell_index"] = [0] * self.ncomp self._update_local_indices() self._init_view() @@ -12722,7 +12740,7 @@

self._par_inds, self._child_inds, self._child_belongs_to_branchpoint = ( compute_children_and_parents(self.branch_edges) ) - self._internal_node_inds = jnp.arange(self.nseg) + self._internal_node_inds = jnp.arange(self.ncomp) self._initialize() @@ -12731,7 +12749,7 @@

def _init_morph_jaxley_spsolve(self): self._solve_indexer = JaxleySolveIndexer( - cumsum_nseg=self.cumsum_nseg, + cumsum_ncomp=self.cumsum_ncomp, branchpoint_group_inds=np.asarray([]).astype(int), remapped_node_indices=self._internal_node_inds, children_in_level=[], @@ -12751,8 +12769,8 @@

""" self._comp_edges = pd.DataFrame().from_dict( { - "source": list(range(self.nseg - 1)) + list(range(1, self.nseg)), - "sink": list(range(1, self.nseg)) + list(range(self.nseg - 1)), + "source": list(range(self.ncomp - 1)) + list(range(1, self.ncomp)), + "sink": list(range(1, self.ncomp)) + list(range(self.ncomp - 1)), } ) self._comp_edges["type"] = 0 @@ -12763,7 +12781,7 @@

self._indptr_jax_spsolve = indptr def __len__(self) -> int: - return self.nseg + return self.ncomp

@@ -12783,7 +12801,7 @@

- __init__(compartments=None, nseg=None) + __init__(compartments=None, ncomp=None, nseg=None)

@@ -12822,7 +12840,7 @@

- nseg + ncomp Optional[int] @@ -12830,7 +12848,7 @@

Number of segments to divide the branch into. If compartments is an -a single compartment, than the compartment is repeated nseg times to +a single compartment, than the compartment is repeated ncomp times to create the branch.

29
-30
-31
-32
-33
-34
-35
-36
-37
-38
-39
-40
-41
-42
-43
-44
-45
-46
-47
-48
-49
-50
-51
-52
-53
-54
-55
-56
-57
-58
-59
-60
-61
-62
-63
-64
-65
-66
-67
-68
-69
-70
-71
-72
-73
-74
-75
-76
-77
-78
-79
-80
-81
-82
-83
-84
-85
-86
-87
-88
-89
-90
def __init__(
+              
 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
@deprecated_kwargs("0.6.0", ["nseg"])
+def __init__(
     self,
     compartments: Optional[Union[Compartment, List[Compartment]]] = None,
+    ncomp: Optional[int] = None,
     nseg: Optional[int] = None,
 ):
     """
     Args:
         compartments: A single compartment or a list of compartments that make up the
             branch.
-        nseg: Number of segments to divide the branch into. If `compartments` is an
-            a single compartment, than the compartment is repeated `nseg` times to
+        ncomp: Number of segments to divide the branch into. If `compartments` is an
+            a single compartment, than the compartment is repeated `ncomp` times to
             create the branch.
     """
+    # Warnings and errors that deal with the change from `nseg` to `ncomp` change
+    # in Jaxley v0.5.0.
+    if ncomp is not None and nseg is not None:
+        raise ValueError("You passed `ncomp` and `nseg`. Please pass only `ncomp`.")
+    if ncomp is None and nseg is not None:
+        ncomp = nseg
+
     super().__init__()
     assert (
         isinstance(compartments, (Compartment, List)) or compartments is None
     ), "Only Compartment or List[Compartment] is allowed."
     if isinstance(compartments, Compartment):
         assert (
-            nseg is not None
-        ), "If `compartments` is not a list then you have to set `nseg`."
+            ncomp is not None
+        ), "If `compartments` is not a list then you have to set `ncomp`."
     compartments = Compartment() if compartments is None else compartments
-    nseg = 1 if nseg is None else nseg
+    ncomp = 1 if ncomp is None else ncomp
 
     if isinstance(compartments, Compartment):
-        compartment_list = [compartments] * nseg
+        compartment_list = [compartments] * ncomp
     else:
         compartment_list = compartments
 
-    self.nseg = len(compartment_list)
-    self.nseg_per_branch = np.asarray([self.nseg])
+    self.ncomp = len(compartment_list)
+    self.ncomp_per_branch = np.asarray([self.ncomp])
     self.total_nbranches = 1
     self.nbranches_per_cell = [1]
     self._cumsum_nbranches = jnp.asarray([0, 1])
-    self.cumsum_nseg = cumsum_leading_zero(self.nseg_per_branch)
+    self.cumsum_ncomp = cumsum_leading_zero(self.ncomp_per_branch)
 
     # Indexing.
     self.nodes = pd.concat([c.nodes for c in compartment_list], ignore_index=True)
     self._append_params_and_states(self.branch_params, self.branch_states)
-    self.nodes["global_comp_index"] = np.arange(self.nseg).tolist()
-    self.nodes["global_branch_index"] = [0] * self.nseg
-    self.nodes["global_cell_index"] = [0] * self.nseg
+    self.nodes["global_comp_index"] = np.arange(self.ncomp).tolist()
+    self.nodes["global_branch_index"] = [0] * self.ncomp
+    self.nodes["global_cell_index"] = [0] * self.ncomp
     self._update_local_indices()
     self._init_view()
 
@@ -12960,7 +12996,7 @@ 

self._par_inds, self._child_inds, self._child_belongs_to_branchpoint = ( compute_children_and_parents(self.branch_edges) ) - self._internal_node_inds = jnp.arange(self.nseg) + self._internal_node_inds = jnp.arange(self.ncomp) self._initialize() @@ -13003,8 +13039,7 @@

Source code in jaxley/modules/cell.py -
 29
- 30
+                
 30
  31
  32
  33
@@ -13245,7 +13280,8 @@ 

268 269 270 -271

class Cell(Module):
+271
+272
class Cell(Module):
     """Cell class.
 
     This class defines a single cell that can be simulated by itself or
@@ -13314,18 +13350,18 @@ 

# Compartment structure. These arguments have to be rebuilt when `.set_ncomp()` # is run. - self.nseg_per_branch = np.asarray([branch.nseg for branch in branch_list]) - self.nseg = int(np.max(self.nseg_per_branch)) - self.cumsum_nseg = cumsum_leading_zero(self.nseg_per_branch) - self._internal_node_inds = np.arange(self.cumsum_nseg[-1]) + self.ncomp_per_branch = np.asarray([branch.ncomp for branch in branch_list]) + self.ncomp = int(np.max(self.ncomp_per_branch)) + self.cumsum_ncomp = cumsum_leading_zero(self.ncomp_per_branch) + self._internal_node_inds = np.arange(self.cumsum_ncomp[-1]) # Build nodes. Has to be changed when `.set_ncomp()` is run. self.nodes = pd.concat([c.nodes for c in branch_list], ignore_index=True) - self.nodes["global_comp_index"] = np.arange(self.cumsum_nseg[-1]) + self.nodes["global_comp_index"] = np.arange(self.cumsum_ncomp[-1]) self.nodes["global_branch_index"] = np.repeat( - np.arange(self.total_nbranches), self.nseg_per_branch + np.arange(self.total_nbranches), self.ncomp_per_branch ).tolist() - self.nodes["global_cell_index"] = np.repeat(0, self.cumsum_nseg[-1]).tolist() + self.nodes["global_cell_index"] = np.repeat(0, self.cumsum_ncomp[-1]).tolist() self._update_local_indices() self._init_view() @@ -13367,7 +13403,7 @@

branchpoint_group_inds = build_branchpoint_group_inds( len(self._par_inds), self._child_belongs_to_branchpoint, - self.cumsum_nseg[-1], + self.cumsum_ncomp[-1], ) parents = self.comb_parents children_inds = children_and_parents["children"] @@ -13378,29 +13414,29 @@

parents_in_level = compute_parents_in_level( levels, self._par_inds, parents_inds ) - levels_and_nseg = pd.DataFrame().from_dict( + levels_and_ncomp = pd.DataFrame().from_dict( { "levels": levels, - "nsegs": self.nseg_per_branch, + "ncomps": self.ncomp_per_branch, } ) - levels_and_nseg["max_nseg_in_level"] = levels_and_nseg.groupby("levels")[ - "nsegs" + levels_and_ncomp["max_ncomp_in_level"] = levels_and_ncomp.groupby("levels")[ + "ncomps" ].transform("max") - padded_cumsum_nseg = cumsum_leading_zero( - levels_and_nseg["max_nseg_in_level"].to_numpy() + padded_cumsum_ncomp = cumsum_leading_zero( + levels_and_ncomp["max_ncomp_in_level"].to_numpy() ) # Generate mapping to deal with the masking which allows using the custom - # sparse solver to deal with different nseg per branch. + # sparse solver to deal with different ncomp per branch. remapped_node_indices = remap_index_to_masked( self._internal_node_inds, self.nodes, - padded_cumsum_nseg, - self.nseg_per_branch, + padded_cumsum_ncomp, + self.ncomp_per_branch, ) self._solve_indexer = JaxleySolveIndexer( - cumsum_nseg=padded_cumsum_nseg, + cumsum_ncomp=padded_cumsum_ncomp, branchpoint_group_inds=branchpoint_group_inds, children_in_level=children_in_level, parents_in_level=parents_in_level, @@ -13428,14 +13464,14 @@

pd.DataFrame() .from_dict( { - "source": list(range(cumsum_nseg, nseg - 1 + cumsum_nseg)) - + list(range(1 + cumsum_nseg, nseg + cumsum_nseg)), - "sink": list(range(1 + cumsum_nseg, nseg + cumsum_nseg)) - + list(range(cumsum_nseg, nseg - 1 + cumsum_nseg)), + "source": list(range(cumsum_ncomp, ncomp - 1 + cumsum_ncomp)) + + list(range(1 + cumsum_ncomp, ncomp + cumsum_ncomp)), + "sink": list(range(1 + cumsum_ncomp, ncomp + cumsum_ncomp)) + + list(range(cumsum_ncomp, ncomp - 1 + cumsum_ncomp)), } ) .astype(int) - for nseg, cumsum_nseg in zip(self.nseg_per_branch, self.cumsum_nseg) + for ncomp, cumsum_ncomp in zip(self.ncomp_per_branch, self.cumsum_ncomp) ] ) self._comp_edges["type"] = 0 @@ -13443,15 +13479,15 @@

# Edges from branchpoints to compartments. branchpoint_to_parent_edges = pd.DataFrame().from_dict( { - "source": np.arange(len(self._par_inds)) + self.cumsum_nseg[-1], - "sink": self.cumsum_nseg[self._par_inds + 1] - 1, + "source": np.arange(len(self._par_inds)) + self.cumsum_ncomp[-1], + "sink": self.cumsum_ncomp[self._par_inds + 1] - 1, "type": 1, } ) branchpoint_to_child_edges = pd.DataFrame().from_dict( { - "source": self._child_belongs_to_branchpoint + self.cumsum_nseg[-1], - "sink": self.cumsum_nseg[self._child_inds], + "source": self._child_belongs_to_branchpoint + self.cumsum_ncomp[-1], + "sink": self.cumsum_ncomp[self._child_inds], "type": 2, } ) @@ -13586,8 +13622,7 @@

Source code in jaxley/modules/cell.py -
 40
- 41
+              
 41
  42
  43
  44
@@ -13678,7 +13713,8 @@ 

129 130 131 -132

def __init__(
+132
+133
def __init__(
     self,
     branches: Optional[Union[Branch, List[Branch]]] = None,
     parents: Optional[List[int]] = None,
@@ -13736,18 +13772,18 @@ 

# Compartment structure. These arguments have to be rebuilt when `.set_ncomp()` # is run. - self.nseg_per_branch = np.asarray([branch.nseg for branch in branch_list]) - self.nseg = int(np.max(self.nseg_per_branch)) - self.cumsum_nseg = cumsum_leading_zero(self.nseg_per_branch) - self._internal_node_inds = np.arange(self.cumsum_nseg[-1]) + self.ncomp_per_branch = np.asarray([branch.ncomp for branch in branch_list]) + self.ncomp = int(np.max(self.ncomp_per_branch)) + self.cumsum_ncomp = cumsum_leading_zero(self.ncomp_per_branch) + self._internal_node_inds = np.arange(self.cumsum_ncomp[-1]) # Build nodes. Has to be changed when `.set_ncomp()` is run. self.nodes = pd.concat([c.nodes for c in branch_list], ignore_index=True) - self.nodes["global_comp_index"] = np.arange(self.cumsum_nseg[-1]) + self.nodes["global_comp_index"] = np.arange(self.cumsum_ncomp[-1]) self.nodes["global_branch_index"] = np.repeat( - np.arange(self.total_nbranches), self.nseg_per_branch + np.arange(self.total_nbranches), self.ncomp_per_branch ).tolist() - self.nodes["global_cell_index"] = np.repeat(0, self.cumsum_nseg[-1]).tolist() + self.nodes["global_cell_index"] = np.repeat(0, self.cumsum_ncomp[-1]).tolist() self._update_local_indices() self._init_view() @@ -14388,7 +14424,9 @@

612 613 614 -615

class Network(Module):
+615
+616
+617
class Network(Module):
     """Network class.
 
     This class defines a network of cells that can be connected with synapses.
@@ -14411,10 +14449,12 @@ 

self.xyzr += deepcopy(cell.xyzr) self._cells_list = cells - self.nseg_per_branch = np.concatenate([cell.nseg_per_branch for cell in cells]) - self.nseg = int(np.max(self.nseg_per_branch)) - self.cumsum_nseg = cumsum_leading_zero(self.nseg_per_branch) - self._internal_node_inds = np.arange(self.cumsum_nseg[-1]) + self.ncomp_per_branch = np.concatenate( + [cell.ncomp_per_branch for cell in cells] + ) + self.ncomp = int(np.max(self.ncomp_per_branch)) + self.cumsum_ncomp = cumsum_leading_zero(self.ncomp_per_branch) + self._internal_node_inds = np.arange(self.cumsum_ncomp[-1]) self._append_params_and_states(self.network_params, self.network_states) self.nbranches_per_cell = [cell.total_nbranches for cell in cells] @@ -14422,13 +14462,13 @@

self._cumsum_nbranches = cumsum_leading_zero(self.nbranches_per_cell) self.nodes = pd.concat([c.nodes for c in cells], ignore_index=True) - self.nodes["global_comp_index"] = np.arange(self.cumsum_nseg[-1]) + self.nodes["global_comp_index"] = np.arange(self.cumsum_ncomp[-1]) self.nodes["global_branch_index"] = np.repeat( - np.arange(self.total_nbranches), self.nseg_per_branch + np.arange(self.total_nbranches), self.ncomp_per_branch ).tolist() self.nodes["global_cell_index"] = list( itertools.chain( - *[[i] * int(cell.cumsum_nseg[-1]) for i, cell in enumerate(cells)] + *[[i] * int(cell.cumsum_ncomp[-1]) for i, cell in enumerate(cells)] ) ) self._update_local_indices() @@ -14473,7 +14513,7 @@

branchpoint_group_inds = build_branchpoint_group_inds( len(self._par_inds), self._child_belongs_to_branchpoint, - self.cumsum_nseg[-1], + self.cumsum_ncomp[-1], ) children_in_level = merge_cells( self._cumsum_nbranches, @@ -14487,22 +14527,22 @@

[cell._solve_indexer.parents_in_level for cell in self._cells_list], exclude_first=False, ) - padded_cumsum_nseg = cumsum_leading_zero( + padded_cumsum_ncomp = cumsum_leading_zero( np.concatenate( - [np.diff(cell._solve_indexer.cumsum_nseg) for cell in self._cells_list] + [np.diff(cell._solve_indexer.cumsum_ncomp) for cell in self._cells_list] ) ) # Generate mapping to dealing with the masking which allows using the custom - # sparse solver to deal with different nseg per branch. + # sparse solver to deal with different ncomp per branch. remapped_node_indices = remap_index_to_masked( self._internal_node_inds, self.nodes, - padded_cumsum_nseg, - self.nseg_per_branch, + padded_cumsum_ncomp, + self.ncomp_per_branch, ) self._solve_indexer = JaxleySolveIndexer( - cumsum_nseg=padded_cumsum_nseg, + cumsum_ncomp=padded_cumsum_ncomp, branchpoint_group_inds=branchpoint_group_inds, children_in_level=children_in_level, parents_in_level=parents_in_level, @@ -14516,7 +14556,7 @@

The reason that this function is a bit involved for a `Network` is that Jaxley considers branchpoint nodes to be at the very end of __all__ nodes (i.e. the branchpoints of the first cell are even after the compartments of the second - cell. The reason for this is that, otherwise, `cumsum_nseg` becomes tricky). + cell. The reason for this is that, otherwise, `cumsum_ncomp` becomes tricky). To achieve this, we first loop over all compartments and append them, and then loop over all branchpoints and append those. The code for building the indices @@ -14529,13 +14569,13 @@

`type == 3`: parent-compartment --> branchpoint `type == 4`: child-compartment --> branchpoint """ - self._cumsum_nseg_per_cell = cumsum_leading_zero( - jnp.asarray([cell.cumsum_nseg[-1] for cell in self.cells]) + self._cumsum_ncomp_per_cell = cumsum_leading_zero( + jnp.asarray([cell.cumsum_ncomp[-1] for cell in self.cells]) ) self._comp_edges = pd.DataFrame() # Add all the internal nodes. - for offset, cell in zip(self._cumsum_nseg_per_cell, self._cells_list): + for offset, cell in zip(self._cumsum_ncomp_per_cell, self._cells_list): condition = cell._comp_edges["type"].to_numpy() == 0 rows = cell._comp_edges[condition] self._comp_edges = pd.concat( @@ -14543,13 +14583,13 @@

) # All branchpoint-to-compartment nodes. - start_branchpoints = self.cumsum_nseg[-1] # Index of the first branchpoint. + start_branchpoints = self.cumsum_ncomp[-1] # Index of the first branchpoint. for offset, offset_branchpoints, cell in zip( - self._cumsum_nseg_per_cell, + self._cumsum_ncomp_per_cell, self._cumsum_nbranchpoints_per_cell, self._cells_list, ): - offset_within_cell = cell.cumsum_nseg[-1] + offset_within_cell = cell.cumsum_ncomp[-1] condition = cell._comp_edges["type"].isin([1, 2]) rows = cell._comp_edges[condition] self._comp_edges = pd.concat( @@ -14567,11 +14607,11 @@

# All compartment-to-branchpoint nodes. for offset, offset_branchpoints, cell in zip( - self._cumsum_nseg_per_cell, + self._cumsum_ncomp_per_cell, self._cumsum_nbranchpoints_per_cell, self._cells_list, ): - offset_within_cell = cell.cumsum_nseg[-1] + offset_within_cell = cell.cumsum_ncomp[-1] condition = cell._comp_edges["type"].isin([3, 4]) rows = cell._comp_edges[condition] self._comp_edges = pd.concat( @@ -14931,12 +14971,12 @@

post_loc = loc_of_index( post_nodes["global_comp_index"].to_numpy(), post_nodes["global_branch_index"].to_numpy(), - self.nseg_per_branch, + self.ncomp_per_branch, ) pre_loc = loc_of_index( pre_nodes["global_comp_index"].to_numpy(), pre_nodes["global_branch_index"].to_numpy(), - self.nseg_per_branch, + self.ncomp_per_branch, ) # Define new synapses. Each row is one synapse. @@ -15099,7 +15139,9 @@

106 107 108 -109

def __init__(
+109
+110
+111
def __init__(
     self,
     cells: List[Cell],
 ):
@@ -15113,10 +15155,12 @@ 

self.xyzr += deepcopy(cell.xyzr) self._cells_list = cells - self.nseg_per_branch = np.concatenate([cell.nseg_per_branch for cell in cells]) - self.nseg = int(np.max(self.nseg_per_branch)) - self.cumsum_nseg = cumsum_leading_zero(self.nseg_per_branch) - self._internal_node_inds = np.arange(self.cumsum_nseg[-1]) + self.ncomp_per_branch = np.concatenate( + [cell.ncomp_per_branch for cell in cells] + ) + self.ncomp = int(np.max(self.ncomp_per_branch)) + self.cumsum_ncomp = cumsum_leading_zero(self.ncomp_per_branch) + self._internal_node_inds = np.arange(self.cumsum_ncomp[-1]) self._append_params_and_states(self.network_params, self.network_states) self.nbranches_per_cell = [cell.total_nbranches for cell in cells] @@ -15124,13 +15168,13 @@

self._cumsum_nbranches = cumsum_leading_zero(self.nbranches_per_cell) self.nodes = pd.concat([c.nodes for c in cells], ignore_index=True) - self.nodes["global_comp_index"] = np.arange(self.cumsum_nseg[-1]) + self.nodes["global_comp_index"] = np.arange(self.cumsum_ncomp[-1]) self.nodes["global_branch_index"] = np.repeat( - np.arange(self.total_nbranches), self.nseg_per_branch + np.arange(self.total_nbranches), self.ncomp_per_branch ).tolist() self.nodes["global_cell_index"] = list( itertools.chain( - *[[i] * int(cell.cumsum_nseg[-1]) for i, cell in enumerate(cells)] + *[[i] * int(cell.cumsum_ncomp[-1]) for i, cell in enumerate(cells)] ) ) self._update_local_indices() @@ -15394,9 +15438,7 @@

Source code in jaxley/modules/network.py -
384
-385
-386
+              
386
 387
 388
 389
@@ -15533,7 +15575,9 @@ 

520 521 522 -523

def vis(
+523
+524
+525
def vis(
     self,
     detail: str = "full",
     ax: Optional[Axes] = None,
diff --git a/dev/reference/utils/index.html b/dev/reference/utils/index.html
index dc1fe328..bbf74c32 100644
--- a/dev/reference/utils/index.html
+++ b/dev/reference/utils/index.html
@@ -1504,7 +1504,7 @@ 

Utils

- build_radiuses_from_xyzr(radius_fns, branch_indices, min_radius, nseg) + build_radiuses_from_xyzr(radius_fns, branch_indices, min_radius, ncomp)

@@ -1512,7 +1512,7 @@

Return the radiuses of branches given SWC file xyzr.

-

Returns an array of shape (num_branches, nseg).

+

Returns an array of shape (num_branches, ncomp).

Parameters:

@@ -1576,7 +1576,7 @@

- nseg + ncomp int @@ -1629,21 +1629,21 @@

radius_fns: List[Callable], branch_indices: List[int], min_radius: Optional[float], - nseg: int, + ncomp: int, ) -> jnp.ndarray: """Return the radiuses of branches given SWC file xyzr. - Returns an array of shape `(num_branches, nseg)`. + Returns an array of shape `(num_branches, ncomp)`. Args: radius_fns: Functions which, given compartment locations return the radius. branch_indices: The indices of the branches for which to return the radiuses. min_radius: If passed, the radiuses are clipped to be at least as large. - nseg: The number of compartments that every branch is discretized into. + ncomp: The number of compartments that every branch is discretized into. """ # Compartment locations are at the center of the internal nodes. - non_split = 1 / nseg - range_ = np.linspace(non_split / 2, 1 - non_split / 2, nseg) + non_split = 1 / ncomp + range_ = np.linspace(non_split / 2, 1 - non_split / 2, ncomp) # Build radiuses. radiuses = np.asarray([radius_fns[b](range_) for b in branch_indices]) @@ -1679,9 +1679,7 @@

Source code in jaxley/utils/cell_utils.py -
701
-702
-703
+              
703
 704
 705
 706
@@ -1740,7 +1738,9 @@ 

759 760 761 -762

def compute_axial_conductances(
+762
+763
+764
def compute_axial_conductances(
     comp_edges: pd.DataFrame, params: Dict[str, jnp.ndarray]
 ) -> jnp.ndarray:
     """Given `comp_edges`, radius, length, r_a, cm, compute the axial conductances.
@@ -1823,15 +1823,15 @@ 

Source code in jaxley/utils/cell_utils.py -
765
-766
-767
+              
767
 768
 769
 770
 771
 772
-773
def compute_children_and_parents(
+773
+774
+775
def compute_children_and_parents(
     branch_edges: pd.DataFrame,
 ) -> Tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray, int]:
     """Build indices used during `._init_morph_custom_spsolve()."""
@@ -1918,9 +1918,7 @@ 

Source code in jaxley/utils/cell_utils.py -
512
-513
-514
+              
514
 515
 516
 517
@@ -1929,7 +1927,9 @@ 

520 521 522 -523

def compute_coupling_cond(rad1, rad2, r_a1, r_a2, l1, l2):
+523
+524
+525
def compute_coupling_cond(rad1, rad2, r_a1, r_a2, l1, l2):
     """Return the coupling conductance between two compartments.
 
     Equations taken from `https://en.wikipedia.org/wiki/Compartmental_neuron_models`.
@@ -1972,9 +1972,7 @@ 

Source code in jaxley/utils/cell_utils.py -
526
-527
-528
+              
528
 529
 530
 531
@@ -1992,7 +1990,9 @@ 

543 544 545 -546

def compute_coupling_cond_branchpoint(rad, r_a, l):
+546
+547
+548
def compute_coupling_cond_branchpoint(rad, r_a, l):
     r"""Return the coupling conductance between one compartment and a comp with l=0.
 
     From https://en.wikipedia.org/wiki/Compartmental_neuron_models
@@ -2041,9 +2041,7 @@ 

Source code in jaxley/utils/cell_utils.py -
549
-550
-551
+              
551
 552
 553
 554
@@ -2053,7 +2051,9 @@ 

558 559 560 -561

def compute_impact_on_node(rad, r_a, l):
+561
+562
+563
def compute_impact_on_node(rad, r_a, l):
     r"""Compute the weight with which a compartment influences its node.
 
     In order to satisfy Kirchhoffs current law, the current at a branch point must be
@@ -2088,9 +2088,7 @@ 

Source code in jaxley/utils/cell_utils.py -
645
-646
-647
+              
647
 648
 649
 650
@@ -2106,7 +2104,9 @@ 

660 661 662 -663

def compute_morphology_indices_in_levels(
+663
+664
+665
def compute_morphology_indices_in_levels(
     num_branchpoints,
     child_belongs_to_branchpoint,
     par_inds,
@@ -2215,9 +2215,7 @@ 

Source code in jaxley/utils/cell_utils.py -
610
-611
-612
+              
612
 613
 614
 615
@@ -2232,7 +2230,9 @@ 

624 625 626 -627

def convert_point_process_to_distributed(
+627
+628
+629
def convert_point_process_to_distributed(
     current: jnp.ndarray, radius: jnp.ndarray, length: jnp.ndarray
 ) -> jnp.ndarray:
     """Convert current point process (nA) to distributed current (uA/cm2).
@@ -2260,7 +2260,7 @@ 

- equal_segments(branch_property, nseg_per_branch) + equal_segments(branch_property, ncomp_per_branch)

@@ -2311,7 +2311,7 @@

305 306 307 -308

def equal_segments(branch_property: list, nseg_per_branch: int):
+308
def equal_segments(branch_property: list, ncomp_per_branch: int):
     """Generates segments where some property is the same in each segment.
 
     Args:
@@ -2319,7 +2319,7 @@ 

`len(branch_property) == num_branches`. """ assert isinstance(branch_property, list), "branch_property must be a list." - return jnp.asarray([branch_property] * nseg_per_branch).T + return jnp.asarray([branch_property] * ncomp_per_branch).T

@@ -2330,7 +2330,7 @@

- get_num_neighbours(num_children, nseg_per_branch, num_branches) + get_num_neighbours(num_children, ncomp_per_branch, num_branches)

@@ -2356,15 +2356,15 @@

480 481

def get_num_neighbours(
     num_children: jnp.ndarray,
-    nseg_per_branch: int,
+    ncomp_per_branch: int,
     num_branches: int,
 ):
     """
     Number of neighbours of each compartment.
     """
-    num_neighbours = 2 * jnp.ones((num_branches * nseg_per_branch))
-    num_neighbours = num_neighbours.at[nseg_per_branch - 1].set(1.0)
-    num_neighbours = num_neighbours.at[jnp.arange(num_branches) * nseg_per_branch].set(
+    num_neighbours = 2 * jnp.ones((num_branches * ncomp_per_branch))
+    num_neighbours = num_neighbours.at[ncomp_per_branch - 1].set(1.0)
+    num_neighbours = num_neighbours.at[jnp.arange(num_branches) * ncomp_per_branch].set(
         num_children + 1.0
     )
     return num_neighbours
@@ -2391,9 +2391,7 @@ 

Source code in jaxley/utils/cell_utils.py -
666
-667
-668
+              
668
 669
 670
 671
@@ -2408,7 +2406,9 @@ 

680 681 682 -683

def group_and_sum(
+683
+684
+685
def group_and_sum(
     values_to_sum: jnp.ndarray, inds_to_group_by: jnp.ndarray, num_branchpoints: int
 ) -> jnp.ndarray:
     """Group values by whether they have the same integer and sum values within group.
@@ -2499,9 +2499,7 @@ 

Source code in jaxley/utils/cell_utils.py -
576
-577
-578
+              
578
 579
 580
 581
@@ -2513,7 +2511,9 @@ 

587 588 589 -590

def interpolate_xyzr(loc: float, coords: np.ndarray):
+590
+591
+592
def interpolate_xyzr(loc: float, coords: np.ndarray):
     """Perform a linear interpolation between xyz-coordinates.
 
     Args:
@@ -2538,7 +2538,7 @@ 

- linear_segments(initial_val, endpoint_vals, parents, nseg_per_branch) + linear_segments(initial_val, endpoint_vals, parents, ncomp_per_branch)

@@ -2620,7 +2620,7 @@

332 333 334

def linear_segments(
-    initial_val: float, endpoint_vals: list, parents: jnp.ndarray, nseg_per_branch: int
+    initial_val: float, endpoint_vals: list, parents: jnp.ndarray, ncomp_per_branch: int
 ):
     """Generates segments where some property is linearly interpolated.
 
@@ -2638,11 +2638,11 @@ 

end = endpoint_radiuses[branch_ind] return (end - start) * loc + start - branch_inds_of_each_comp = jnp.tile(jnp.arange(num_branches), nseg_per_branch) - locs_of_each_comp = jnp.linspace(1, 0, nseg_per_branch).repeat(num_branches) + branch_inds_of_each_comp = jnp.tile(jnp.arange(num_branches), ncomp_per_branch) + locs_of_each_comp = jnp.linspace(1, 0, ncomp_per_branch).repeat(num_branches) rad_of_each_comp = compute_rad(branch_inds_of_each_comp, locs_of_each_comp) - return jnp.reshape(rad_of_each_comp, (nseg_per_branch, num_branches)).T + return jnp.reshape(rad_of_each_comp, (ncomp_per_branch, num_branches)).T

@@ -2653,7 +2653,7 @@

- loc_of_index(global_comp_index, global_branch_index, nseg_per_branch) + loc_of_index(global_comp_index, global_branch_index, ncomp_per_branch)

@@ -2664,17 +2664,17 @@

Source code in jaxley/utils/cell_utils.py -
504
-505
-506
+              
506
 507
 508
-509
def loc_of_index(global_comp_index, global_branch_index, nseg_per_branch):
+509
+510
+511
def loc_of_index(global_comp_index, global_branch_index, ncomp_per_branch):
     """Return location corresponding to global compartment index."""
-    cumsum_nseg = cumsum_leading_zero(nseg_per_branch)
-    index = global_comp_index - cumsum_nseg[global_branch_index]
-    nseg = nseg_per_branch[global_branch_index]
-    return (0.5 + index) / nseg
+    cumsum_ncomp = cumsum_leading_zero(ncomp_per_branch)
+    index = global_comp_index - cumsum_ncomp[global_branch_index]
+    ncomp = ncomp_per_branch[global_branch_index]
+    return (0.5 + index) / ncomp
 
@@ -2685,7 +2685,7 @@

- local_index_of_loc(loc, global_branch_ind, nseg_per_branch) + local_index_of_loc(loc, global_branch_ind, ncomp_per_branch)

@@ -2741,7 +2741,7 @@

- nseg_per_branch + ncomp_per_branch int @@ -2800,7 +2800,11 @@

498 499 500 -501

def local_index_of_loc(loc: float, global_branch_ind: int, nseg_per_branch: int) -> int:
+501
+502
+503
def local_index_of_loc(
+    loc: float, global_branch_ind: int, ncomp_per_branch: int
+) -> int:
     """Returns the local index of a comp given a loc [0, 1] and the index of a branch.
 
     This is used because we specify locations such as synapses as a value between 0 and
@@ -2809,13 +2813,13 @@ 

Args: branch_ind: Index of the branch. loc: Location (in [0, 1]) along that branch. - nseg_per_branch: Number of segments of each branch. + ncomp_per_branch: Number of segments of each branch. Returns: The local index of the compartment. """ - nseg = nseg_per_branch[global_branch_ind] # only for convenience. - possible_locs = np.linspace(0.5 / nseg, 1 - 0.5 / nseg, nseg) + ncomp = ncomp_per_branch[global_branch_ind] # only for convenience. + possible_locs = np.linspace(0.5 / ncomp, 1 - 0.5 / ncomp, ncomp) ind_along_branch = np.argmin(np.abs(possible_locs - loc)) return ind_along_branch

@@ -3064,9 +3068,7 @@

Source code in jaxley/utils/cell_utils.py -
593
-594
-595
+              
595
 596
 597
 598
@@ -3078,7 +3080,9 @@ 

604 605 606 -607

def params_to_pstate(
+607
+608
+609
def params_to_pstate(
     params: List[Dict[str, jnp.ndarray]],
     indices_set_by_trainables: List[jnp.ndarray],
 ):
@@ -3120,9 +3124,7 @@ 

Source code in jaxley/utils/cell_utils.py -
686
-687
-688
+              
688
 689
 690
 691
@@ -3132,7 +3134,9 @@ 

695 696 697 -698

def query_channel_states_and_params(d, keys, idcs):
+698
+699
+700
def query_channel_states_and_params(d, keys, idcs):
     """Get dict with subset of keys and values from d.
 
     This is used to restrict a dict where every item contains __all__ states to only
@@ -3167,13 +3171,13 @@ 

Source code in jaxley/utils/cell_utils.py -
- + - + @@ -1044,11 +1044,11 @@

Inspecting and changing syn

- + - + @@ -1059,11 +1059,11 @@

Inspecting and changing syn

- + - + @@ -1074,11 +1074,11 @@

Inspecting and changing syn

- + - + @@ -1089,7 +1089,7 @@

Inspecting and changing syn

- + @@ -1104,11 +1104,11 @@

Inspecting and changing syn

- + - + @@ -1119,11 +1119,11 @@

Inspecting and changing syn

- + - + @@ -1134,7 +1134,7 @@

Inspecting and changing syn

- + @@ -1149,11 +1149,11 @@

Inspecting and changing syn

- + - + @@ -1164,11 +1164,11 @@

Inspecting and changing syn

- + - + diff --git a/dev/tutorial/02_small_network_files/02_small_network_14_0.png b/dev/tutorial/02_small_network_files/02_small_network_14_0.png index 1f1806927289e7378237eeaceeb58b9213dc20c5..b66de4fec34720a23534f7196019520dbe5e9871 100644 GIT binary patch literal 34754 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 ked5GFb2t8q$4HC@#r70c`Y4GK+EhX4Qo literal 34291 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 Vw|iffNWy<-POB>voHD!l{{Tc(JX`<( diff --git a/dev/tutorial/02_small_network_files/02_small_network_17_0.png b/dev/tutorial/02_small_network_files/02_small_network_17_0.png index cc18f2a75f26dbb958f17100f7030edf1f607978..088d9d5a2c5b4a601d263de216616e957a8061c3 100644 GIT binary patch literal 35882 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

6832sz=8tJMjv_T;{RhUSKm;G^mKFJ$vB7HMg; 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 Fe*jt>LAU?_ literal 35279 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 Fe*q^s#6ti8 diff --git a/dev/tutorial/02_small_network_files/02_small_network_36_0.png b/dev/tutorial/02_small_network_files/02_small_network_36_0.png index b7dace75272723465f689327afbc5cc2f49deddc..dd870e980ae9a71889ba6757d3781159864b951b 100644 GIT binary patch literal 17220 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ⅈ*~DS_{Q7t2&a1?Eh$(|gzXor{`n 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 Y?yQO%secE&(E=nRp(tJ408{xXMF0Q* literal 18866 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 Z7_)xhL6*$JNtmC6I%Q#RUS)>=?O&yVS*id4 diff --git a/dev/tutorial/04_jit_and_vmap/index.html b/dev/tutorial/04_jit_and_vmap/index.html index 5425d71d..cb291ac8 100644 --- a/dev/tutorial/04_jit_and_vmap/index.html +++ b/dev/tutorial/04_jit_and_vmap/index.html @@ -1053,7 +1053,7 @@

Building the cell or networkt_max = 10.0 comp = jx.Compartment() -branch = jx.Branch(comp, nseg=4) +branch = jx.Branch(comp, ncomp=4) cell = jx.Cell(branch, parents=[-1, 0, 0, 1, 1, 2, 2]) cell.insert(Na()) diff --git a/dev/tutorial/05_channel_and_synapse_models/index.html b/dev/tutorial/05_channel_and_synapse_models/index.html index 9a6b8d4a..ba2e31ee 100644 --- a/dev/tutorial/05_channel_and_synapse_models/index.html +++ b/dev/tutorial/05_channel_and_synapse_models/index.html @@ -906,7 +906,7 @@

Building ion channel modelsprevious tutorial:

comp = jx.Compartment()
-branch = jx.Branch(comp, nseg=4)
+branch = jx.Branch(comp, ncomp=4)
 cell = jx.Cell(branch, parents=[-1, 0, 0, 1, 1, 2, 2])
 

You have also already learned how to insert preconfigured channels into Jaxley models: diff --git a/dev/tutorial/06_groups/index.html b/dev/tutorial/06_groups/index.html index 1a4bc230..67c28a22 100644 --- a/dev/tutorial/06_groups/index.html +++ b/dev/tutorial/06_groups/index.html @@ -985,7 +985,7 @@

Defining groupsprevious tutorial:

comp = jx.Compartment()
-branch = jx.Branch(comp, nseg=2)
+branch = jx.Branch(comp, ncomp=2)
 cell = jx.Cell(branch, parents=[-1, 0, 0, 1])
 network = jx.Network([cell for _ in range(3)])
 
@@ -997,10 +997,6 @@ 

Defining groupsnetwork.insert(K()) network.insert(Leak())

-
/Users/michaeldeistler/Documents/phd/jaxley/jaxley/modules/base.py:1533: FutureWarning: The behavior of DataFrame concatenation with empty or all-NA entries is deprecated. In a future version, this will no longer exclude empty or all-NA columns when determining the result dtypes. To retain the old behavior, exclude the relevant entries before the concat operation.
-  self.pointer.edges = pd.concat(
-
-

Group: apical dendrites

Assume that, in each of the five neurons in this network, the second and forth branch are apical dendrites. We can define this as:

for cell_ind in range(3):
@@ -1012,340 +1008,8 @@ 

Group: apical dendrites

network.apical.view
 
-
- -

564
-565
-566
+              
- - - - - - - - - @@ -1667,19 +1432,10 @@

Views&

- + - - - - - - - - - @@ -1691,19 +1447,10 @@

Views&

- + - - - - - - - - - @@ -1715,19 +1462,10 @@

Views&

- + - - - - - - - - - @@ -1739,19 +1477,10 @@

Views&

- + - - - - - - - - - @@ -1763,19 +1492,10 @@

Views&

- + - - - - - - - - - @@ -1787,19 +1507,10 @@

Views&

- + - - - - - - - - - @@ -1811,19 +1522,10 @@

Views&

- + - - - - - - - - - @@ -1835,19 +1537,10 @@

Views&

- + - - - - - - - - - @@ -1859,19 +1552,10 @@

Views&

- + - - - - - - - - - @@ -1883,19 +1567,10 @@

Views&

- + - - - - - - - - - @@ -1907,19 +1582,10 @@

Views&

- + - - - - - - - - - @@ -1931,19 +1597,10 @@

Views&

- + - - - - - - - - - @@ -1951,7 +1608,6 @@

Views&

566
 567
 568
 569
-570
def remap_to_consecutive(arr):
+570
+571
+572
def remap_to_consecutive(arr):
     """Maps an array of integers to an array of consecutive integers.
 
     E.g. `[0, 0, 1, 4, 4, 6, 6] -> [0, 0, 1, 2, 2, 3, 3]`
@@ -4428,7 +4432,7 @@ 

lens = np.sqrt(np.nansum(np.diff(locs, axis=0) ** 2, axis=1)) lens = np.cumsum([0] + lens.tolist()) comp_ends = v_interp( - np.linspace(0, lens[-1], module_or_view.nseg + 1), lens, locs + np.linspace(0, lens[-1], module_or_view.ncomp + 1), lens, locs ).T axes = np.diff(comp_ends, axis=0) cylinder_lens = np.sqrt(np.sum(axes**2, axis=1)) diff --git a/dev/search/search_index.json b/dev/search/search_index.json index 06927210..f16ff857 100644 --- a/dev/search/search_index.json +++ b/dev/search/search_index.json @@ -1 +1 @@ -{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Home","text":"

Jaxley is a differentiable simulator for biophysical neuron models in JAX. Its key features are:

  • automatic differentiation, allowing gradient-based optimization of thousands of parameters
  • support for CPU, GPU, or TPU without any changes to the code
  • jit-compilation, making it as fast as other packages while being fully written in python
  • backward-Euler solver for stable numerical solution of multicompartment neurons
  • elegant mechanisms for parameter sharing
"},{"location":"#getting-started","title":"Getting started","text":"

Jaxley allows to simulate biophysical neuron models on CPU, GPU, or TPU:

import matplotlib.pyplot as plt\nfrom jax import config\n\nimport jaxley as jx\nfrom jaxley.channels import HH\n\nconfig.update(\"jax_platform_name\", \"cpu\")  # Or \"gpu\" / \"tpu\".\n\ncell = jx.Cell()  # Define cell.\ncell.insert(HH())  # Insert channels.\n\ncurrent = jx.step_current(i_delay=1.0, i_dur=1.0, i_amp=0.1, delta_t=0.025, t_max=10.0)\ncell.stimulate(current)  # Stimulate with step current.\ncell.record(\"v\")  # Record voltage.\n\nv = jx.integrate(cell)  # Run simulation.\nplt.plot(v.T)  # Plot voltage trace.\n

If you want to learn more, we have tutorials on how to:

  • simulate morphologically detailed neurons
  • simulate networks of such neurons
  • set parameters of cells and networks
  • speed up simulations with GPUs and jit
  • define your own channels and synapses
  • define groups
  • read and handle SWC files
  • compute the gradient and train biophysical models
"},{"location":"#installation","title":"Installation","text":"

Jaxley is available on pypi:

pip install jaxley\n
This will install Jaxley with CPU support. If you want GPU support, follow the instructions on the JAX github repository to install JAX with GPU support (in addition to installing Jaxley). For example, for NVIDIA GPUs, run
pip install -U \"jax[cuda12]\"\n

"},{"location":"#feedback-and-contributions","title":"Feedback and Contributions","text":"

We welcome any feedback on how Jaxley is working for your neuron models and are happy to receive bug reports, pull requests and other feedback (see contribute). We wish to maintain a positive community, please read our Code of Conduct.

"},{"location":"#license","title":"License","text":"

Apache License Version 2.0 (Apache-2.0)

"},{"location":"#citation","title":"Citation","text":"

If you use Jaxley, consider citing the corresponding paper:

@article{deistler2024differentiable,\n  doi = {10.1101/2024.08.21.608979},\n  year = {2024},\n  publisher = {Cold Spring Harbor Laboratory},\n  author = {Deistler, Michael and Kadhim, Kyra L. and Pals, Matthijs and Beck, Jonas and Huang, Ziwei and Gloeckler, Manuel and Lappalainen, Janne K. and Schr{\\\"o}der, Cornelius and Berens, Philipp and Gon{\\c c}alves, Pedro J. and Macke, Jakob H.},\n  title = {Differentiable simulation enables large-scale training of detailed biophysical models of neural dynamics},\n  journal = {bioRxiv}\n}\n
"},{"location":"code_of_conduct/","title":"Contributor Covenant Code of Conduct","text":""},{"location":"code_of_conduct/#our-pledge","title":"Our Pledge","text":"

We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation.

We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.

"},{"location":"code_of_conduct/#our-standards","title":"Our Standards","text":"

Examples of behavior that contributes to a positive environment for our community include:

  • Demonstrating empathy and kindness toward other people
  • Being respectful of differing opinions, viewpoints, and experiences
  • Giving and gracefully accepting constructive feedback
  • Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
  • Focusing on what is best not just for us as individuals, but for the overall community

Examples of unacceptable behavior include:

  • The use of sexualized language or imagery, and sexual attention or advances of any kind
  • Trolling, insulting or derogatory comments, and personal or political attacks
  • Public or private harassment
  • Publishing others\u2019 private information, such as a physical or email address, without their explicit permission
  • Other conduct which could reasonably be considered inappropriate in a professional setting
"},{"location":"code_of_conduct/#enforcement-responsibilities","title":"Enforcement Responsibilities","text":"

Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.

Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.

"},{"location":"code_of_conduct/#scope","title":"Scope","text":"

This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.

"},{"location":"code_of_conduct/#enforcement","title":"Enforcement","text":"

Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting jaxley developer Michael Deistler via email (michael.deistler@uni-tuebingen.de). All complaints will be reviewed and investigated promptly and fairly.

All community leaders are obligated to respect the privacy and security of the reporter of any incident.

"},{"location":"code_of_conduct/#enforcement-guidelines","title":"Enforcement Guidelines","text":"

Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:

"},{"location":"code_of_conduct/#1-correction","title":"1. Correction","text":"

Community Impact: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.

Consequence: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.

"},{"location":"code_of_conduct/#2-warning","title":"2. Warning","text":"

Community Impact: A violation through a single incident or series of actions.

Consequence: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.

"},{"location":"code_of_conduct/#3-temporary-ban","title":"3. Temporary Ban","text":"

Community Impact: A serious violation of community standards, including sustained inappropriate behavior.

Consequence: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.

"},{"location":"code_of_conduct/#4-permanent-ban","title":"4. Permanent Ban","text":"

Community Impact: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.

Consequence: A permanent ban from any sort of public interaction within the community.

"},{"location":"code_of_conduct/#attribution","title":"Attribution","text":"

This Code of Conduct is adapted from the Contributor Covenant, version 2.1, available at https://www.contributor-covenant.org/version/2/1/code_of_conduct.html.

Community Impact Guidelines were inspired by Mozilla\u2019s code of conduct enforcement ladder.

For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.

"},{"location":"contribute/","title":"Guide","text":""},{"location":"contribute/#user-experiences-bugs-and-feature-requests","title":"User experiences, bugs, and feature requests","text":"

To report bugs and suggest features (including better documentation), please head over to issues on GitHub.

"},{"location":"contribute/#code-contributions","title":"Code contributions","text":"

In general, we use pull requests to make changes to Jaxley. So, if you are planning to make a contribution, please fork, create a feature branch and then make a PR from your feature branch to the upstream Jaxley (details).

"},{"location":"contribute/#development-environment","title":"Development environment","text":"

Clone the repo and install via setup.py using pip install -e \".[dev]\" (the dev flag installs development and testing dependencies).

"},{"location":"contribute/#style-conventions","title":"Style conventions","text":"

For docstrings and comments, we use Google Style.

Code needs to pass through the following tools, which are installed alongside Jaxley:

black: Automatic code formatting for Python. You can run black manually from the console using black . in the top directory of the repository, which will format all files.

isort: Used to consistently order imports. You can run isort manually from the console using isort in the top directory.

black and isort are checked as part of our CI actions. If these checks fail please make sure you have installed the latest versions for each of them and run them locally.

"},{"location":"contribute/#online-documentation","title":"Online documentation","text":"

Most of the documentation is written in markdown (basic markdown guide).

You can directly fix mistakes and suggest clearer formulations in markdown files simply by initiating a PR on through GitHub. Click on documentation file and look for the little pencil at top right.

"},{"location":"credits/","title":"Credits","text":"

Jaxley is a collaborative project between the groups of Jakob Macke (Uni T\u00fcbingen), Pedro Gon\u00e7alves (KU Leuven / NERF), and Philipp Berens (Uni T\u00fcbingen).

"},{"location":"credits/#license","title":"License","text":"

Jaxley is licensed under the Apache License Version 2.0 (Apache-2.0) and

Copyright (C) 2024 Michael Deistler, Jakob H. Macke, Pedro J. Goncalves, Philipp Berens.

"},{"location":"credits/#important-dependencies-and-prior-art","title":"Important dependencies and prior art","text":"
  • We greatly benefited from previous toolboxes for simulating multicompartment neurons, in particular NEURON.
"},{"location":"credits/#funding","title":"Funding","text":"

This work was supported by the German Research Foundation (DFG) through Germany\u2019s Excellence Strategy (EXC 2064 \u2013 Project number 390727645) and the CRC 1233 \u201cRobust Vision\u201d, the German Federal Ministry of Education and Research (Tu\u0308bingen AI Center, FKZ: 01IS18039A), the \u2018Certification and Foundations of Safe Machine Learning Systems in Healthcare\u2019 project funded by the Carl Zeiss Foundation, and the European Union (ERC, \u201cDeepCoMechTome\u201d, ref. 101089288, \u201cNextMechMod\u201d, ref. 101039115).

"},{"location":"faq/","title":"Frequently asked questions","text":"
  • What kinds of models can be implemented in Jaxley?
  • What units does Jaxley use?
  • How can I save and load cells and networks?

See also the discussion page and the issue tracker on the Jaxley GitHub repository for recent questions and problems.

"},{"location":"install/","title":"Installation","text":""},{"location":"install/#install-the-most-recent-stable-version","title":"Install the most recent stable version","text":"

Jaxley is available on PyPI:

pip install jaxley\n
This will install Jaxley with CPU support. If you want GPU support, follow the instructions on the JAX github repository to install JAX with GPU support (in addition to installing Jaxley). For example, for NVIDIA GPUs, run
pip install -U \"jax[cuda12]\"\n

"},{"location":"install/#install-from-source","title":"Install from source","text":"

You can also install Jaxley from source:

git clone https://github.com/jaxleyverse/jaxley.git\ncd jaxley\npip install -e .\n

Note that pip>=21.3 is required to install the editable version with pyproject.toml see pip docs.

"},{"location":"faq/question_01/","title":"What units does Jaxley use?","text":"

Jaxley uses the same units as the NEURON simulator, which are listed here.

"},{"location":"faq/question_02/","title":"How can I save and load cells and networks?","text":"

All modules (i.e., compartments, branches, cells, and networks) in Jaxley can be saved and loaded with pickle:

import jaxley as jx\nimport pickle\n\n# ... define network, cell, etc.\nnetwork = jx.Network([cell1, cell2])\n\n# Save.\nwith open(\"path/to/file.pkl\", \"wb\") as handle:\n    pickle.dump(network, handle)\n\n# Load.\nwith open(\"path/to/file.pkl\", \"rb\") as handle:\n    network = pickle.load(handle)\n

"},{"location":"faq/question_03/","title":"What kinds of models can be implemented in Jaxley?","text":"

Jaxley focuses on biophysical, Hodgkin-Huxley-type models. You can think of Jaxley like the NEURON simulator written in JAX.

Jaxley allows to simulate the following types of models, as well as networks thereof:

  • single-compartment (point neuron) Hodgkin-Huxley models
  • multi-compartment Hodgkin-Huxley models
  • rate-based neuron models

For all of these models, Jaxley is flexible and accurate. For example, it can flexibly add new channel models, use different kinds of synapses (conductance-based, tanh, \u2026), and it can insert different kinds of channels in different branches (or compartments) within single cells. Like NEURON, Jaxley implements a backward-Euler solver for stable numerical solution of multi-compartment neurons.

However, Jaxley does not implement the following types of models:

  • leaky-integrate and fire neurons
  • Ishikevich neuron models
  • etc\u2026
"},{"location":"reference/connect/","title":"Connecting Cells","text":""},{"location":"reference/connect/#jaxley.connect.connect","title":"connect(pre, post, synapse_type)","text":"

Connect two compartments with a chemical synapse.

The pre- and postsynaptic compartments must be different compartments of the same network.

Parameters:

Name Type Description Default pre View

View of the presynaptic compartment.

required post View

View of the postsynaptic compartment.

required synapse_type Synapse

The synapse to append

required Source code in jaxley/connect.py
def connect(\n    pre: \"View\",\n    post: \"View\",\n    synapse_type: \"Synapse\",\n):\n    \"\"\"Connect two compartments with a chemical synapse.\n\n    The pre- and postsynaptic compartments must be different compartments of the\n    same network.\n\n    Args:\n        pre: View of the presynaptic compartment.\n        post: View of the postsynaptic compartment.\n        synapse_type: The synapse to append\n    \"\"\"\n    assert is_same_network(\n        pre, post\n    ), \"Pre and post compartments must be part of the same network.\"\n\n    pre.base._append_multiple_synapses(pre.nodes, post.nodes, synapse_type)\n
"},{"location":"reference/connect/#jaxley.connect.connectivity_matrix_connect","title":"connectivity_matrix_connect(pre_cell_view, post_cell_view, synapse_type, connectivity_matrix)","text":"

Appends multiple connections which build a custom connected network.

Connects pre- and postsynaptic cells according to a custom connectivity matrix. Entries > 0 in the matrix indicate a connection between the corresponding cells. Connections are from branch 0 location 0 to a randomly chosen branch and loc.

Parameters:

Name Type Description Default pre_cell_view View

View of the presynaptic cell.

required post_cell_view View

View of the postsynaptic cell.

required synapse_type Synapse

The synapse to append.

required connectivity_matrix ndarray[bool]

A boolean matrix indicating the connections between cells.

required Source code in jaxley/connect.py
def connectivity_matrix_connect(\n    pre_cell_view: \"View\",\n    post_cell_view: \"View\",\n    synapse_type: \"Synapse\",\n    connectivity_matrix: np.ndarray[bool],\n):\n    \"\"\"Appends multiple connections which build a custom connected network.\n\n    Connects pre- and postsynaptic cells according to a custom connectivity matrix.\n    Entries > 0 in the matrix indicate a connection between the corresponding cells.\n    Connections are from branch 0 location 0 to a randomly chosen branch and loc.\n\n    Args:\n        pre_cell_view: View of the presynaptic cell.\n        post_cell_view: View of the postsynaptic cell.\n        synapse_type: The synapse to append.\n        connectivity_matrix: A boolean matrix indicating the connections between cells.\n    \"\"\"\n    # Get pre- and postsynaptic cell indices.\n    pre_cell_inds = pre_cell_view._cells_in_view\n    post_cell_inds = post_cell_view._cells_in_view\n    # setting scope ensure that this works indep of current scope\n    pre_nodes = pre_cell_view.scope(\"local\").branch(0).comp(0).nodes\n    pre_nodes[\"index\"] = pre_nodes.index\n    pre_cell_nodes = pre_nodes.set_index(\"global_cell_index\")\n\n    assert connectivity_matrix.shape == (\n        len(pre_cell_inds),\n        len(post_cell_inds),\n    ), \"Connectivity matrix must have shape (num_pre, num_post).\"\n    assert connectivity_matrix.dtype == bool, \"Connectivity matrix must be boolean.\"\n\n    # get connection pairs from connectivity matrix\n    from_idx, to_idx = np.where(connectivity_matrix)\n    pre_cell_inds = pre_cell_inds[from_idx]\n    post_cell_inds = post_cell_inds[to_idx]\n\n    # Sample random postsynaptic compartments (global comp indices).\n    global_post_indices = np.hstack(\n        [\n            sample_comp(post_cell_view.scope(\"global\").cell(cell_idx))\n            for cell_idx in post_cell_inds\n        ]\n    )\n    post_rows = post_cell_view.nodes.loc[global_post_indices]\n\n    # Pre-synapse is at the zero-eth branch and zero-eth compartment.\n    global_pre_indices = pre_cell_nodes.loc[pre_cell_inds, \"index\"].to_numpy()\n    pre_rows = pre_cell_view.select(nodes=global_pre_indices).nodes\n\n    pre_cell_view.base._append_multiple_synapses(pre_rows, post_rows, synapse_type)\n
"},{"location":"reference/connect/#jaxley.connect.fully_connect","title":"fully_connect(pre_cell_view, post_cell_view, synapse_type)","text":"

Appends multiple connections which build a fully connected layer.

Connections are from branch 0 location 0 to a randomly chosen branch and loc.

Parameters:

Name Type Description Default pre_cell_view View

View of the presynaptic cell.

required post_cell_view View

View of the postsynaptic cell.

required synapse_type Synapse

The synapse to append.

required Source code in jaxley/connect.py
def fully_connect(\n    pre_cell_view: \"View\",\n    post_cell_view: \"View\",\n    synapse_type: \"Synapse\",\n):\n    \"\"\"Appends multiple connections which build a fully connected layer.\n\n    Connections are from branch 0 location 0 to a randomly chosen branch and loc.\n\n    Args:\n        pre_cell_view: View of the presynaptic cell.\n        post_cell_view: View of the postsynaptic cell.\n        synapse_type: The synapse to append.\n    \"\"\"\n    # Get pre- and postsynaptic cell indices.\n    num_pre = len(pre_cell_view._cells_in_view)\n    num_post = len(post_cell_view._cells_in_view)\n\n    # Infer indices of (random) postsynaptic compartments.\n    global_post_indices = (\n        post_cell_view.nodes.groupby(\"global_cell_index\")\n        .sample(num_pre, replace=True)\n        .index.to_numpy()\n    )\n    global_post_indices = global_post_indices.reshape((-1, num_pre), order=\"F\").ravel()\n    post_rows = post_cell_view.nodes.loc[global_post_indices]\n\n    # Pre-synapse is at the zero-eth branch and zero-eth compartment.\n    pre_rows = pre_cell_view.scope(\"local\").branch(0).comp(0).nodes.copy()\n    # Repeat rows `num_post` times. See SO 50788508.\n    pre_rows = pre_rows.loc[pre_rows.index.repeat(num_post)].reset_index(drop=True)\n\n    pre_cell_view.base._append_multiple_synapses(pre_rows, post_rows, synapse_type)\n
"},{"location":"reference/connect/#jaxley.connect.is_same_network","title":"is_same_network(pre, post)","text":"

Check if views are from the same network.

Source code in jaxley/connect.py
def is_same_network(pre: \"View\", post: \"View\") -> bool:\n    \"\"\"Check if views are from the same network.\"\"\"\n    is_in_net = \"network\" in pre.base.__class__.__name__.lower()\n    is_in_same_net = pre.base is post.base\n    return is_in_net and is_in_same_net\n
"},{"location":"reference/connect/#jaxley.connect.sample_comp","title":"sample_comp(cell_view, num=1, replace=True)","text":"

Sample a compartment from a cell.

Returns View with shape (num, num_cols).

Source code in jaxley/connect.py
def sample_comp(cell_view: \"View\", num: int = 1, replace=True) -> \"CompartmentView\":\n    \"\"\"Sample a compartment from a cell.\n\n    Returns View with shape (num, num_cols).\"\"\"\n    return np.random.choice(cell_view._comps_in_view, num, replace=replace)\n
"},{"location":"reference/connect/#jaxley.connect.sparse_connect","title":"sparse_connect(pre_cell_view, post_cell_view, synapse_type, p)","text":"

Appends multiple connections which build a sparse, randomly connected layer.

Connections are from branch 0 location 0 to a randomly chosen branch and loc.

Parameters:

Name Type Description Default pre_cell_view View

View of the presynaptic cell.

required post_cell_view View

View of the postsynaptic cell.

required synapse_type Synapse

The synapse to append.

required p float

Probability of connection.

required Source code in jaxley/connect.py
def sparse_connect(\n    pre_cell_view: \"View\",\n    post_cell_view: \"View\",\n    synapse_type: \"Synapse\",\n    p: float,\n):\n    \"\"\"Appends multiple connections which build a sparse, randomly connected layer.\n\n    Connections are from branch 0 location 0 to a randomly chosen branch and loc.\n\n    Args:\n        pre_cell_view: View of the presynaptic cell.\n        post_cell_view: View of the postsynaptic cell.\n        synapse_type: The synapse to append.\n        p: Probability of connection.\n    \"\"\"\n    # Get pre- and postsynaptic cell indices.\n    pre_cell_inds = pre_cell_view._cells_in_view\n    post_cell_inds = post_cell_view._cells_in_view\n    num_pre = len(pre_cell_inds)\n    num_post = len(post_cell_inds)\n\n    num_connections = np.random.binomial(num_pre * num_post, p)\n    pre_syn_neurons = np.random.choice(pre_cell_inds, size=num_connections)\n    post_syn_neurons = np.random.choice(post_cell_inds, size=num_connections)\n\n    # Sort the synapses only for convenience of inspecting `.edges`.\n    sorting = np.argsort(pre_syn_neurons)\n    pre_syn_neurons = pre_syn_neurons[sorting]\n    post_syn_neurons = post_syn_neurons[sorting]\n\n    # Post-synapse is a randomly chosen branch and compartment.\n    global_post_indices = [\n        sample_comp(post_cell_view.scope(\"global\").cell(cell_idx))\n        for cell_idx in post_syn_neurons\n    ]\n    global_post_indices = (\n        np.hstack(global_post_indices) if len(global_post_indices) > 1 else []\n    )\n    post_rows = post_cell_view.base.nodes.loc[global_post_indices]\n\n    # Pre-synapse is at the zero-eth branch and zero-eth compartment.\n    global_pre_indices = pre_cell_view.base._cumsum_nseg_per_cell[pre_syn_neurons]\n    pre_rows = pre_cell_view.base.nodes.loc[global_pre_indices]\n\n    if len(pre_rows) > 0:\n        pre_cell_view.base._append_multiple_synapses(pre_rows, post_rows, synapse_type)\n
"},{"location":"reference/integration/","title":"Simulation","text":""},{"location":"reference/integration/#jaxley.integrate.add_clamps","title":"add_clamps(externals, external_inds, data_clamps=None)","text":"

Adds clamps to the external inputs.

Parameters:

Name Type Description Default externals Dict

Current external inputs.

required external_inds Dict

Current external indices.

required data_clamps Optional[Tuple[str, ndarray, DataFrame]]

Additional data clamps. Defaults to None.

None

Returns:

Type Description Tuple[Dict, Dict]

Tuple[Dict, Dict]: Updated external inputs and indices.

Source code in jaxley/integrate.py
def add_clamps(\n    externals: Dict,\n    external_inds: Dict,\n    data_clamps: Optional[Tuple[str, jnp.ndarray, pd.DataFrame]] = None,\n) -> Tuple[Dict, Dict]:\n    \"\"\"Adds clamps to the external inputs.\n\n    Args:\n        externals (Dict): Current external inputs.\n        external_inds (Dict): Current external indices.\n        data_clamps (Optional[Tuple[str, jnp.ndarray, pd.DataFrame]], optional): Additional data clamps. Defaults to None.\n\n    Returns:\n        Tuple[Dict, Dict]: Updated external inputs and indices.\n    \"\"\"\n    # If a clamp is inserted, add it to the external inputs.\n    if data_clamps is not None:\n        state_name, clamps, inds = data_clamps\n        if state_name in externals.keys():\n            externals[state_name] = jnp.concatenate([externals[state_name], clamps])\n            external_inds[state_name] = jnp.concatenate(\n                [external_inds[state_name], inds.index.to_numpy()]\n            )\n        else:\n            externals[state_name] = clamps\n            external_inds[state_name] = inds.index.to_numpy()\n\n    return externals, external_inds\n
"},{"location":"reference/integration/#jaxley.integrate.add_stimuli","title":"add_stimuli(externals, external_inds, data_stimuli=None)","text":"

Extends the external inputs with the stimuli.

Parameters:

Name Type Description Default externals Dict

Current external inputs.

required external_inds Dict

Current external indices.

required data_stimuli Optional[Tuple[ndarray, DataFrame]]

Additional data stimuli. Defaults to None.

None

Returns:

Type Description Tuple[Dict, Dict]

Tuple[Dict, Dict]: Updated external inputs and indices.

Source code in jaxley/integrate.py
def add_stimuli(\n    externals: Dict,\n    external_inds: Dict,\n    data_stimuli: Optional[Tuple[jnp.ndarray, pd.DataFrame]] = None,\n) -> Tuple[Dict, Dict]:\n    \"\"\"Extends the external inputs with the stimuli.\n\n    Args:\n        externals (Dict): Current external inputs.\n        external_inds (Dict): Current external indices.\n        data_stimuli (Optional[Tuple[jnp.ndarray, pd.DataFrame]], optional): Additional data stimuli. Defaults to None.\n\n    Returns:\n        Tuple[Dict, Dict]: Updated external inputs and indices.\n    \"\"\"\n    # If stimulus is inserted, add it to the external inputs.\n    if \"i\" in externals.keys() or data_stimuli is not None:\n        if \"i\" in externals.keys():\n            if data_stimuli is not None:\n                externals[\"i\"] = jnp.concatenate([externals[\"i\"], data_stimuli[1]])\n                external_inds[\"i\"] = jnp.concatenate(\n                    [external_inds[\"i\"], data_stimuli[2].index.to_numpy()]\n                )\n        else:\n            externals[\"i\"] = data_stimuli[1]\n            external_inds[\"i\"] = data_stimuli[2].index.to_numpy()\n\n    return externals, external_inds\n
"},{"location":"reference/integration/#jaxley.integrate.build_init_and_step_fn","title":"build_init_and_step_fn(module, voltage_solver='jaxley.stone', solver='bwd_euler')","text":"

This function returns the init_fn and step_fn which initialize the parameters and states of the neuron model and then step through the model

Parameters:

Name Type Description Default module Module

A Module object that e.g. a cell.

required voltage_solver str

Voltage solver used in step. Defaults to \u201cjaxley.stone\u201d.

'jaxley.stone' solver str

ODE solver. Defaults to \u201cbwd_euler\u201d.

'bwd_euler'

Returns:

Type Description Tuple[Callable, Callable]

init_fn, step_fn: Functions that initialize the state and parameters, and perform a single integration step, respectively.

Source code in jaxley/integrate.py
def build_init_and_step_fn(\n    module: Module,\n    voltage_solver: str = \"jaxley.stone\",\n    solver: str = \"bwd_euler\",\n) -> Tuple[Callable, Callable]:\n    \"\"\"This function returns the `init_fn` and `step_fn` which initialize the\n    parameters and states of the neuron model and then step through the model\n\n    Args:\n        module (Module): A `Module` object that e.g. a cell.\n        voltage_solver (str, optional): Voltage solver used in step. Defaults to \"jaxley.stone\".\n        solver (str, optional): ODE solver. Defaults to \"bwd_euler\".\n\n    Returns:\n        init_fn, step_fn: Functions that initialize the state and parameters, and perform\n            a single integration step, respectively.\n    \"\"\"\n    # Initialize the external inputs and their indices.\n    external_inds = module.external_inds.copy()\n\n    def init_fn(\n        params: List[Dict[str, jnp.ndarray]],\n        all_states: Optional[Dict] = None,\n        param_state: Optional[List[Dict]] = None,\n        delta_t: float = 0.025,\n    ) -> Tuple[Dict, Dict]:\n        \"\"\"Initializes the parameters and states of the neuron model.\n\n        Args:\n            params (List[Dict[str, jnp.ndarray]]): List of trainable parameters.\n            all_states (Optional[Dict], optional): State if alread initialized. Defaults to None.\n            param_state (Optional[List[Dict]], optional): Parameters returned by `data_set`.. Defaults to None.\n            delta_t (float, optional): Step size. Defaults to 0.025.\n\n        Returns:\n            Tuple[Dict, Dict]: All states and parameters.\n        \"\"\"\n        # Make the `trainable_params` of the same shape as the `param_state`, such that\n        # they can be processed together by `get_all_parameters`.\n        pstate = params_to_pstate(params, module.indices_set_by_trainables)\n        if param_state is not None:\n            pstate += param_state\n\n        all_params = module.get_all_parameters(pstate, voltage_solver=voltage_solver)\n        all_states = (\n            module.get_all_states(pstate, all_params, delta_t)\n            if all_states is None\n            else all_states\n        )\n        return all_states, all_params\n\n    def step_fn(\n        all_states: Dict,\n        all_params: Dict,\n        externals: Dict,\n        external_inds: Dict = external_inds,\n        delta_t: float = 0.025,\n    ) -> Dict:\n        \"\"\"Performs a single integration step with step size delta_t.\n\n        Args:\n            all_states (Dict): Current state of the neuron model.\n            all_params (Dict): Current parameters of the neuron model.\n            externals (Dict): External inputs.\n            external_inds (Dict, optional): External indices. Defaults to `module.external_inds`.\n            delta_t (float, optional): Time step. Defaults to 0.025.\n\n        Returns:\n            Dict: Updated states.\n        \"\"\"\n        state = all_states\n        state = module.step(\n            state,\n            delta_t,\n            external_inds,\n            externals,\n            params=all_params,\n            solver=solver,\n            voltage_solver=voltage_solver,\n        )\n        return state\n\n    return init_fn, step_fn\n
"},{"location":"reference/integration/#jaxley.integrate.integrate","title":"integrate(module, params=[], *, param_state=None, data_stimuli=None, data_clamps=None, t_max=None, delta_t=0.025, solver='bwd_euler', voltage_solver='jaxley.stone', checkpoint_lengths=None, all_states=None, return_states=False)","text":"

Solves ODE and simulates neuron model.

Parameters:

Name Type Description Default params List[Dict[str, ndarray]]

Trainable parameters returned by get_parameters().

[] param_state Optional[List[Dict]]

Parameters returned by data_set.

None data_stimuli Optional[Tuple[ndarray, DataFrame]]

Outputs of .data_stimulate(), only needed if stimuli change across function calls.

None data_clamps Optional[Tuple[str, ndarray, DataFrame]]

Outputs of .data_clamp(), only needed if clamps change across function calls.

None t_max Optional[float]

Duration of the simulation in milliseconds. If t_max is greater than the length of the stimulus input, the stimulus will be padded at the end with zeros. If t_max is smaller, then the stimulus with be truncated.

None delta_t float

Time step of the solver in milliseconds.

0.025 solver str

Which ODE solver to use. Either of [\u201cfwd_euler\u201d, \u201cbwd_euler\u201d, \u201ccrank_nicolson\u201d].

'bwd_euler' tridiag_solver

Algorithm to solve tridiagonal systems. The different options only affect bwd_euler and crank_nicolson solvers. Either of [\u201cstone\u201d, \u201cthomas\u201d], where stone is much faster on GPU for long branches with many compartments and thomas is slightly faster on CPU (thomas is used in NEURON).

required checkpoint_lengths Optional[List[int]]

Number of timesteps at every level of checkpointing. The prod(checkpoint_lengths) must be larger or equal to the desired number of simulated timesteps. Warning: the simulation is run for prod(checkpoint_lengths) timesteps, and the result is posthoc truncated to the desired simulation length. Therefore, a poor choice of checkpoint_lengths can lead to longer simulation time. If None, no checkpointing is applied.

None all_states Optional[Dict]

An optional initial state that was returned by a previous jx.integrate(..., return_states=True) run. Overrides potentially trainable initial states.

None return_states bool

If True, it returns all states such that the current state of the Module can be set with set_states.

False Source code in jaxley/integrate.py
def integrate(\n    module: Module,\n    params: List[Dict[str, jnp.ndarray]] = [],\n    *,\n    param_state: Optional[List[Dict]] = None,\n    data_stimuli: Optional[Tuple[jnp.ndarray, pd.DataFrame]] = None,\n    data_clamps: Optional[Tuple[str, jnp.ndarray, pd.DataFrame]] = None,\n    t_max: Optional[float] = None,\n    delta_t: float = 0.025,\n    solver: str = \"bwd_euler\",\n    voltage_solver: str = \"jaxley.stone\",\n    checkpoint_lengths: Optional[List[int]] = None,\n    all_states: Optional[Dict] = None,\n    return_states: bool = False,\n) -> jnp.ndarray:\n    \"\"\"\n    Solves ODE and simulates neuron model.\n\n    Args:\n        params: Trainable parameters returned by `get_parameters()`.\n        param_state: Parameters returned by `data_set`.\n        data_stimuli: Outputs of `.data_stimulate()`, only needed if stimuli change\n            across function calls.\n        data_clamps: Outputs of `.data_clamp()`, only needed if clamps change across\n            function calls.\n        t_max: Duration of the simulation in milliseconds. If `t_max` is greater than\n            the length of the stimulus input, the stimulus will be padded at the end\n            with zeros. If `t_max` is smaller, then the stimulus with be truncated.\n        delta_t: Time step of the solver in milliseconds.\n        solver: Which ODE solver to use. Either of [\"fwd_euler\", \"bwd_euler\",\n            \"crank_nicolson\"].\n        tridiag_solver: Algorithm to solve tridiagonal systems. The  different options\n            only affect `bwd_euler` and `crank_nicolson` solvers. Either of [\"stone\",\n            \"thomas\"], where `stone` is much faster on GPU for long branches\n            with many compartments and `thomas` is slightly faster on CPU (`thomas` is\n            used in NEURON).\n        checkpoint_lengths: Number of timesteps at every level of checkpointing. The\n            `prod(checkpoint_lengths)` must be larger or equal to the desired number of\n            simulated timesteps. Warning: the simulation is run for\n            `prod(checkpoint_lengths)` timesteps, and the result is posthoc truncated\n            to the desired simulation length. Therefore, a poor choice of\n            `checkpoint_lengths` can lead to longer simulation time. If `None`, no\n            checkpointing is applied.\n        all_states: An optional initial state that was returned by a previous\n            `jx.integrate(..., return_states=True)` run. Overrides potentially\n            trainable initial states.\n        return_states: If True, it returns all states such that the current state of\n            the `Module` can be set with `set_states`.\n    \"\"\"\n\n    assert module.initialized, \"Module is not initialized, run `._initialize()`.\"\n    module.to_jax()  # Creates `.jaxnodes` from `.nodes` and `.jaxedges` from `.edges`.\n\n    # Initialize the external inputs and their indices.\n    externals = module.externals.copy()\n    external_inds = module.external_inds.copy()\n\n    # If stimulus is inserted, add it to the external inputs.\n    externals, external_inds = add_stimuli(externals, external_inds, data_stimuli)\n\n    # If a clamp is inserted, add it to the external inputs.\n    externals, external_inds = add_clamps(externals, external_inds, data_clamps)\n\n    if not externals.keys():\n        # No stimulus was inserted and no clamp was set.\n        assert (\n            t_max is not None\n        ), \"If no stimulus or clamp are inserted you have to specify the simulation duration at `jx.integrate(..., t_max=)`.\"\n\n    for key in externals.keys():\n        externals[key] = externals[key].T  # Shape `(time, num_stimuli)`.\n\n    if module.recordings.empty:\n        raise ValueError(\"No recordings are set. Please set them.\")\n    rec_inds = module.recordings.rec_index.to_numpy()\n    rec_states = module.recordings.state.to_numpy()\n\n    # Shorten or pad stimulus depending on `t_max`.\n    if t_max is not None:\n        t_max_steps = int(t_max // delta_t + 1)\n\n        # Pad or truncate the stimulus.\n        for key in externals.keys():\n            if t_max_steps > externals[key].shape[0]:\n                if key == \"i\":\n                    pad = jnp.zeros(\n                        (t_max_steps - externals[\"i\"].shape[0], externals[\"i\"].shape[1])\n                    )\n                    externals[\"i\"] = jnp.concatenate((externals[\"i\"], pad))\n                else:\n                    raise NotImplementedError(\n                        \"clamp must be at least as long as simulation.\"\n                    )\n            else:\n                externals[key] = externals[key][:t_max_steps, :]\n\n    init_fn, step_fn = build_init_and_step_fn(\n        module, voltage_solver=voltage_solver, solver=solver\n    )\n    all_states, all_params = init_fn(params, all_states, param_state, delta_t)\n\n    def _body_fun(state, externals):\n        state = step_fn(state, all_params, externals, external_inds, delta_t)\n        recs = jnp.asarray(\n            [\n                state[rec_state][rec_ind]\n                for rec_state, rec_ind in zip(rec_states, rec_inds)\n            ]\n        )\n        return state, recs\n\n    # If necessary, pad the stimulus with zeros in order to simulate sufficiently long.\n    # The total simulation length will be `prod(checkpoint_lengths)`. At the end, we\n    # return only the first `nsteps_to_return` elements (plus the initial state).\n    if externals:\n        example_key = list(externals.keys())[0]\n        nsteps_to_return = len(externals[example_key])\n    else:\n        nsteps_to_return = t_max_steps\n\n    if checkpoint_lengths is None:\n        checkpoint_lengths = [nsteps_to_return]\n        length = nsteps_to_return\n    else:\n        length = prod(checkpoint_lengths)\n        size_difference = length - nsteps_to_return\n        assert (\n            nsteps_to_return <= length\n        ), \"The desired simulation duration is longer than `prod(nested_length)`.\"\n        if externals:\n            dummy_external = jnp.zeros(\n                (size_difference, externals[example_key].shape[1])\n            )\n            for key in externals.keys():\n                externals[key] = jnp.concatenate([externals[key], dummy_external])\n\n    # Record the initial state.\n    init_recs = jnp.asarray(\n        [\n            all_states[rec_state][rec_ind]\n            for rec_state, rec_ind in zip(rec_states, rec_inds)\n        ]\n    )\n    init_recording = jnp.expand_dims(init_recs, axis=0)\n\n    # Run simulation.\n    all_states, recordings = nested_checkpoint_scan(\n        _body_fun,\n        all_states,\n        externals,\n        length=length,\n        nested_lengths=checkpoint_lengths,\n    )\n    recs = jnp.concatenate([init_recording, recordings[:nsteps_to_return]], axis=0).T\n    return (recs, all_states) if return_states else recs\n
"},{"location":"reference/integration/#jaxley.solver_gate.exponential_euler","title":"exponential_euler(x, dt, x_inf, x_tau)","text":"

An exact solver for the linear dynamical system dx = -(x - x_inf) / x_tau.

Source code in jaxley/solver_gate.py
def exponential_euler(\n    x: jnp.ndarray,\n    dt: float,\n    x_inf: jnp.ndarray,\n    x_tau: jnp.ndarray,\n):\n    \"\"\"An exact solver for the linear dynamical system `dx = -(x - x_inf) / x_tau`.\"\"\"\n    exp_term = save_exp(-dt / x_tau)\n    return x * exp_term + x_inf * (1.0 - exp_term)\n
"},{"location":"reference/integration/#jaxley.solver_gate.save_exp","title":"save_exp(x, max_value=20.0)","text":"

Clip the input to a maximum value and return its exponential.

Source code in jaxley/solver_gate.py
def save_exp(x, max_value: float = 20.0):\n    \"\"\"Clip the input to a maximum value and return its exponential.\"\"\"\n    x = jnp.clip(x, a_max=max_value)\n    return jnp.exp(x)\n
"},{"location":"reference/integration/#jaxley.solver_gate.solve_inf_gate_exponential","title":"solve_inf_gate_exponential(x, dt, s_inf, tau_s)","text":"

solves dx/dt = (s_inf - x) / tau_s via exponential Euler

Parameters:

Name Type Description Default x ndarray

gate variable

required dt float

time_delta

required s_inf ndarray

description

required tau_s ndarray

description

required

Returns:

Name Type Description _type_

updated gate

Source code in jaxley/solver_gate.py
def solve_inf_gate_exponential(\n    x: jnp.ndarray,\n    dt: float,\n    s_inf: jnp.ndarray,\n    tau_s: jnp.ndarray,\n):\n    \"\"\"solves dx/dt = (s_inf - x) / tau_s\n    via exponential Euler\n\n    Args:\n        x (jnp.ndarray): gate variable\n        dt (float): time_delta\n        s_inf (jnp.ndarray): _description_\n        tau_s (jnp.ndarray): _description_\n\n    Returns:\n        _type_: updated gate\n    \"\"\"\n    slope = -1.0 / tau_s\n    exp_term = save_exp(slope * dt)\n    return x * exp_term + s_inf * (1.0 - exp_term)\n
"},{"location":"reference/integration/#jaxley.solver_voltage.step_voltage_explicit","title":"step_voltage_explicit(voltages, voltage_terms, constant_terms, axial_conductances, internal_node_inds, sinks, sources, types, nseg_per_branch, par_inds, child_inds, nbranches, solver, delta_t, idx, debug_states)","text":"

Solve one timestep of branched nerve equations with explicit (forward) Euler.

Source code in jaxley/solver_voltage.py
def step_voltage_explicit(\n    voltages: jnp.ndarray,\n    voltage_terms: jnp.ndarray,\n    constant_terms: jnp.ndarray,\n    axial_conductances: jnp.ndarray,\n    internal_node_inds: jnp.ndarray,\n    sinks: jnp.ndarray,\n    sources: jnp.ndarray,\n    types: jnp.ndarray,\n    nseg_per_branch: jnp.ndarray,\n    par_inds: jnp.ndarray,\n    child_inds: jnp.ndarray,\n    nbranches: int,\n    solver: str,\n    delta_t: float,\n    idx: JaxleySolveIndexer,\n    debug_states,\n) -> jnp.ndarray:\n    \"\"\"Solve one timestep of branched nerve equations with explicit (forward) Euler.\"\"\"\n    voltages = jnp.reshape(voltages, (nbranches, -1))\n    voltage_terms = jnp.reshape(voltage_terms, (nbranches, -1))\n    constant_terms = jnp.reshape(constant_terms, (nbranches, -1))\n\n    update = _voltage_vectorfield(\n        voltages,\n        voltage_terms,\n        constant_terms,\n        types,\n        sources,\n        sinks,\n        axial_conductances,\n        par_inds,\n        child_inds,\n        nbranches,\n        solver,\n        delta_t,\n        idx,\n        debug_states,\n    )\n    new_voltates = voltages + delta_t * update\n    return new_voltates.ravel(order=\"C\")\n
"},{"location":"reference/integration/#jaxley.solver_voltage.step_voltage_implicit_with_jaxley_spsolve","title":"step_voltage_implicit_with_jaxley_spsolve(voltages, voltage_terms, constant_terms, axial_conductances, internal_node_inds, sinks, sources, types, nseg_per_branch, par_inds, child_inds, nbranches, solver, delta_t, idx, debug_states)","text":"

Solve one timestep of branched nerve equations with implicit (backward) Euler.

Source code in jaxley/solver_voltage.py
def step_voltage_implicit_with_jaxley_spsolve(\n    voltages: jnp.ndarray,\n    voltage_terms: jnp.ndarray,\n    constant_terms: jnp.ndarray,\n    axial_conductances: jnp.ndarray,\n    internal_node_inds: jnp.ndarray,\n    sinks: jnp.ndarray,\n    sources: jnp.ndarray,\n    types: jnp.ndarray,\n    nseg_per_branch: jnp.ndarray,\n    par_inds: jnp.ndarray,\n    child_inds: jnp.ndarray,\n    nbranches: int,\n    solver: str,\n    delta_t: float,\n    idx: JaxleySolveIndexer,\n    debug_states,\n):\n    \"\"\"Solve one timestep of branched nerve equations with implicit (backward) Euler.\"\"\"\n    # Build diagonals.\n    c2c = np.isin(types, [0, 1, 2])\n    total_ncomp = idx.cumsum_nseg[-1]\n    diags = jnp.ones(total_ncomp)\n\n    # if-case needed because `.at` does not allow empty inputs, but the input is\n    # empty for compartments.\n    if len(sinks[c2c]) > 0:\n        diags = diags.at[idx.mask(sinks[c2c])].add(delta_t * axial_conductances[c2c])\n\n    diags = diags.at[idx.mask(internal_node_inds)].add(delta_t * voltage_terms)\n\n    # Build solves.\n    solves = jnp.zeros(total_ncomp)\n    solves = solves.at[idx.mask(internal_node_inds)].add(\n        voltages + delta_t * constant_terms\n    )\n\n    # Build upper and lower within the branch.\n    c2c = types == 0  # c2c = compartment-to-compartment.\n\n    # Build uppers.\n    uppers = jnp.zeros(total_ncomp)\n    upper_inds = sources[c2c] > sinks[c2c]\n    sinks_upper = sinks[c2c][upper_inds]\n    if len(sinks_upper) > 0:\n        uppers = uppers.at[idx.mask(sinks_upper)].add(\n            -delta_t * axial_conductances[c2c][upper_inds]\n        )\n\n    # Build lowers.\n    lowers = jnp.zeros(total_ncomp)\n    lower_inds = sources[c2c] < sinks[c2c]\n    sinks_lower = sinks[c2c][lower_inds]\n    if len(sinks_lower) > 0:\n        lowers = lowers.at[idx.mask(sinks_lower)].add(\n            -delta_t * axial_conductances[c2c][lower_inds]\n        )\n\n    # Build branchpoint conductances.\n    branchpoint_conds_parents = axial_conductances[types == 1]\n    branchpoint_conds_children = axial_conductances[types == 2]\n    branchpoint_weights_parents = axial_conductances[types == 3]\n    branchpoint_weights_children = axial_conductances[types == 4]\n    all_branchpoint_vals = jnp.concatenate(\n        [branchpoint_weights_parents, branchpoint_weights_children]\n    )\n    # Find unique group identifiers\n    num_branchpoints = len(branchpoint_conds_parents)\n    branchpoint_diags = -group_and_sum(\n        all_branchpoint_vals, idx.branchpoint_group_inds, num_branchpoints\n    )\n    branchpoint_solves = jnp.zeros((num_branchpoints,))\n\n    branchpoint_conds_children = -delta_t * branchpoint_conds_children\n    branchpoint_conds_parents = -delta_t * branchpoint_conds_parents\n\n    # Here, I move all child and parent indices towards a branchpoint into a larger\n    # vector. This is wasteful, but it makes indexing much easier. JIT compiling\n    # makes the speed difference negligible.\n    # Children.\n    bp_conds_children = jnp.zeros(nbranches)\n    bp_weights_children = jnp.zeros(nbranches)\n    # Parents.\n    bp_conds_parents = jnp.zeros(nbranches)\n    bp_weights_parents = jnp.zeros(nbranches)\n\n    # `.at[inds]` requires that `inds` is not empty, so we need an if-case here.\n    # `len(inds) == 0` is the case for branches and compartments.\n    if num_branchpoints > 0:\n        bp_conds_children = bp_conds_children.at[child_inds].set(\n            branchpoint_conds_children\n        )\n        bp_weights_children = bp_weights_children.at[child_inds].set(\n            branchpoint_weights_children\n        )\n        bp_conds_parents = bp_conds_parents.at[par_inds].set(branchpoint_conds_parents)\n        bp_weights_parents = bp_weights_parents.at[par_inds].set(\n            branchpoint_weights_parents\n        )\n\n    # Triangulate the linear system of equations.\n    (\n        diags,\n        lowers,\n        solves,\n        uppers,\n        branchpoint_diags,\n        branchpoint_solves,\n        bp_weights_children,\n        bp_conds_parents,\n    ) = _triang_branched(\n        lowers,\n        diags,\n        uppers,\n        solves,\n        bp_conds_children,\n        bp_conds_parents,\n        bp_weights_children,\n        bp_weights_parents,\n        branchpoint_diags,\n        branchpoint_solves,\n        solver,\n        nseg_per_branch,\n        idx,\n        debug_states,\n    )\n\n    # Backsubstitute the linear system of equations.\n    (\n        solves,\n        lowers,\n        diags,\n        bp_weights_parents,\n        branchpoint_solves,\n        bp_conds_children,\n    ) = _backsub_branched(\n        lowers,\n        diags,\n        uppers,\n        solves,\n        bp_conds_children,\n        bp_conds_parents,\n        bp_weights_children,\n        bp_weights_parents,\n        branchpoint_diags,\n        branchpoint_solves,\n        solver,\n        nseg_per_branch,\n        idx,\n        debug_states,\n    )\n    return solves.ravel(order=\"C\")[idx.mask(internal_node_inds)]\n
"},{"location":"reference/mechanisms/","title":"Channels","text":""},{"location":"reference/mechanisms/#channel","title":"Channel","text":"

Channel base class. All channels inherit from this class.

As in NEURON, a Channel is considered a distributed process, which means that its conductances are to be specified in S/cm2 and its currents are to be specified in uA/cm2.

Source code in jaxley/channels/channel.py
class Channel:\n    \"\"\"Channel base class. All channels inherit from this class.\n\n    As in NEURON, a `Channel` is considered a distributed process, which means that its\n    conductances are to be specified in `S/cm2` and its currents are to be specified in\n    `uA/cm2`.\"\"\"\n\n    _name = None\n    channel_params = None\n    channel_states = None\n    current_name = None\n\n    def __init__(self, name: Optional[str] = None):\n        contact = (\n            \"If you have any questions, please reach out via email to \"\n            \"michael.deistler@uni-tuebingen.de or create an issue on Github: \"\n            \"https://github.com/jaxleyverse/jaxley/issues. Thank you!\"\n        )\n        if (\n            not hasattr(self, \"current_is_in_mA_per_cm2\")\n            or not self.current_is_in_mA_per_cm2\n        ):\n            raise ValueError(\n                \"The channel you are using is deprecated. \"\n                \"In Jaxley version 0.5.0, we changed the unit of the current returned \"\n                \"by `compute_current` of channels from `uA/cm^2` to `mA/cm^2`. Please \"\n                \"update your channel model (by dividing the resulting current by 1000) \"\n                \"and set `self.current_is_in_mA_per_cm2=True` as the first line \"\n                f\"in the `__init__()` method of your channel. {contact}\"\n            )\n\n        self._name = name if name else self.__class__.__name__\n\n    @property\n    def name(self) -> Optional[str]:\n        \"\"\"The name of the channel (by default, this is the class name).\"\"\"\n        return self._name\n\n    def change_name(self, new_name: str):\n        \"\"\"Change the channel name.\n\n        Args:\n            new_name: The new name of the channel.\n\n        Returns:\n            Renamed channel, such that this function is chainable.\n        \"\"\"\n        old_prefix = self._name + \"_\"\n        new_prefix = new_name + \"_\"\n\n        self._name = new_name\n        self.channel_params = {\n            (\n                new_prefix + key[len(old_prefix) :]\n                if key.startswith(old_prefix)\n                else key\n            ): value\n            for key, value in self.channel_params.items()\n        }\n\n        self.channel_states = {\n            (\n                new_prefix + key[len(old_prefix) :]\n                if key.startswith(old_prefix)\n                else key\n            ): value\n            for key, value in self.channel_states.items()\n        }\n        return self\n\n    def update_states(\n        self, states, dt, v, params\n    ) -> Tuple[jnp.ndarray, Tuple[jnp.ndarray, jnp.ndarray]]:\n        \"\"\"Return the updated states.\"\"\"\n        raise NotImplementedError\n\n    def compute_current(\n        self, states: Dict[str, jnp.ndarray], v, params: Dict[str, jnp.ndarray]\n    ):\n        \"\"\"Given channel states and voltage, return the current through the channel.\n\n        Args:\n            states: All states of the compartment.\n            v: Voltage of the compartment in mV.\n            params: Parameters of the channel (conductances in `S/cm2`).\n\n        Returns:\n            Current in `uA/cm2`.\n        \"\"\"\n        raise NotImplementedError\n\n    def init_state(\n        self,\n        states: Dict[str, jnp.ndarray],\n        v: jnp.ndarray,\n        params: Dict[str, jnp.ndarray],\n        delta_t: float,\n    ):\n        \"\"\"Initialize states of channel.\"\"\"\n        return {}\n
"},{"location":"reference/mechanisms/#jaxley.channels.channel.Channel.name","title":"name: Optional[str] property","text":"

The name of the channel (by default, this is the class name).

"},{"location":"reference/mechanisms/#jaxley.channels.channel.Channel.change_name","title":"change_name(new_name)","text":"

Change the channel name.

Parameters:

Name Type Description Default new_name str

The new name of the channel.

required

Returns:

Type Description

Renamed channel, such that this function is chainable.

Source code in jaxley/channels/channel.py
def change_name(self, new_name: str):\n    \"\"\"Change the channel name.\n\n    Args:\n        new_name: The new name of the channel.\n\n    Returns:\n        Renamed channel, such that this function is chainable.\n    \"\"\"\n    old_prefix = self._name + \"_\"\n    new_prefix = new_name + \"_\"\n\n    self._name = new_name\n    self.channel_params = {\n        (\n            new_prefix + key[len(old_prefix) :]\n            if key.startswith(old_prefix)\n            else key\n        ): value\n        for key, value in self.channel_params.items()\n    }\n\n    self.channel_states = {\n        (\n            new_prefix + key[len(old_prefix) :]\n            if key.startswith(old_prefix)\n            else key\n        ): value\n        for key, value in self.channel_states.items()\n    }\n    return self\n
"},{"location":"reference/mechanisms/#jaxley.channels.channel.Channel.compute_current","title":"compute_current(states, v, params)","text":"

Given channel states and voltage, return the current through the channel.

Parameters:

Name Type Description Default states Dict[str, ndarray]

All states of the compartment.

required v

Voltage of the compartment in mV.

required params Dict[str, ndarray]

Parameters of the channel (conductances in S/cm2).

required

Returns:

Type Description

Current in uA/cm2.

Source code in jaxley/channels/channel.py
def compute_current(\n    self, states: Dict[str, jnp.ndarray], v, params: Dict[str, jnp.ndarray]\n):\n    \"\"\"Given channel states and voltage, return the current through the channel.\n\n    Args:\n        states: All states of the compartment.\n        v: Voltage of the compartment in mV.\n        params: Parameters of the channel (conductances in `S/cm2`).\n\n    Returns:\n        Current in `uA/cm2`.\n    \"\"\"\n    raise NotImplementedError\n
"},{"location":"reference/mechanisms/#jaxley.channels.channel.Channel.init_state","title":"init_state(states, v, params, delta_t)","text":"

Initialize states of channel.

Source code in jaxley/channels/channel.py
def init_state(\n    self,\n    states: Dict[str, jnp.ndarray],\n    v: jnp.ndarray,\n    params: Dict[str, jnp.ndarray],\n    delta_t: float,\n):\n    \"\"\"Initialize states of channel.\"\"\"\n    return {}\n
"},{"location":"reference/mechanisms/#jaxley.channels.channel.Channel.update_states","title":"update_states(states, dt, v, params)","text":"

Return the updated states.

Source code in jaxley/channels/channel.py
def update_states(\n    self, states, dt, v, params\n) -> Tuple[jnp.ndarray, Tuple[jnp.ndarray, jnp.ndarray]]:\n    \"\"\"Return the updated states.\"\"\"\n    raise NotImplementedError\n
"},{"location":"reference/mechanisms/#hh","title":"HH","text":"

Bases: Channel

Hodgkin-Huxley channel.

Source code in jaxley/channels/hh.py
class HH(Channel):\n    \"\"\"Hodgkin-Huxley channel.\"\"\"\n\n    def __init__(self, name: Optional[str] = None):\n        self.current_is_in_mA_per_cm2 = True\n\n        super().__init__(name)\n        prefix = self._name\n        self.channel_params = {\n            f\"{prefix}_gNa\": 0.12,\n            f\"{prefix}_gK\": 0.036,\n            f\"{prefix}_gLeak\": 0.0003,\n            f\"{prefix}_eNa\": 50.0,\n            f\"{prefix}_eK\": -77.0,\n            f\"{prefix}_eLeak\": -54.3,\n        }\n        self.channel_states = {\n            f\"{prefix}_m\": 0.2,\n            f\"{prefix}_h\": 0.2,\n            f\"{prefix}_n\": 0.2,\n        }\n        self.current_name = f\"i_HH\"\n\n    def update_states(\n        self,\n        states: Dict[str, jnp.ndarray],\n        dt,\n        v,\n        params: Dict[str, jnp.ndarray],\n    ):\n        \"\"\"Return updated HH channel state.\"\"\"\n        prefix = self._name\n        m, h, n = states[f\"{prefix}_m\"], states[f\"{prefix}_h\"], states[f\"{prefix}_n\"]\n        new_m = solve_gate_exponential(m, dt, *self.m_gate(v))\n        new_h = solve_gate_exponential(h, dt, *self.h_gate(v))\n        new_n = solve_gate_exponential(n, dt, *self.n_gate(v))\n        return {f\"{prefix}_m\": new_m, f\"{prefix}_h\": new_h, f\"{prefix}_n\": new_n}\n\n    def compute_current(\n        self, states: Dict[str, jnp.ndarray], v, params: Dict[str, jnp.ndarray]\n    ):\n        \"\"\"Return current through HH channels.\"\"\"\n        prefix = self._name\n        m, h, n = states[f\"{prefix}_m\"], states[f\"{prefix}_h\"], states[f\"{prefix}_n\"]\n\n        gNa = params[f\"{prefix}_gNa\"] * (m**3) * h  # S/cm^2\n        gK = params[f\"{prefix}_gK\"] * n**4  # S/cm^2\n        gLeak = params[f\"{prefix}_gLeak\"]  # S/cm^2\n\n        return (\n            gNa * (v - params[f\"{prefix}_eNa\"])\n            + gK * (v - params[f\"{prefix}_eK\"])\n            + gLeak * (v - params[f\"{prefix}_eLeak\"])\n        )\n\n    def init_state(self, states, v, params, delta_t):\n        \"\"\"Initialize the state such at fixed point of gate dynamics.\"\"\"\n        prefix = self._name\n        alpha_m, beta_m = self.m_gate(v)\n        alpha_h, beta_h = self.h_gate(v)\n        alpha_n, beta_n = self.n_gate(v)\n        return {\n            f\"{prefix}_m\": alpha_m / (alpha_m + beta_m),\n            f\"{prefix}_h\": alpha_h / (alpha_h + beta_h),\n            f\"{prefix}_n\": alpha_n / (alpha_n + beta_n),\n        }\n\n    @staticmethod\n    def m_gate(v):\n        alpha = 0.1 * _vtrap(-(v + 40), 10)\n        beta = 4.0 * save_exp(-(v + 65) / 18)\n        return alpha, beta\n\n    @staticmethod\n    def h_gate(v):\n        alpha = 0.07 * save_exp(-(v + 65) / 20)\n        beta = 1.0 / (save_exp(-(v + 35) / 10) + 1)\n        return alpha, beta\n\n    @staticmethod\n    def n_gate(v):\n        alpha = 0.01 * _vtrap(-(v + 55), 10)\n        beta = 0.125 * save_exp(-(v + 65) / 80)\n        return alpha, beta\n
"},{"location":"reference/mechanisms/#jaxley.channels.hh.HH.compute_current","title":"compute_current(states, v, params)","text":"

Return current through HH channels.

Source code in jaxley/channels/hh.py
def compute_current(\n    self, states: Dict[str, jnp.ndarray], v, params: Dict[str, jnp.ndarray]\n):\n    \"\"\"Return current through HH channels.\"\"\"\n    prefix = self._name\n    m, h, n = states[f\"{prefix}_m\"], states[f\"{prefix}_h\"], states[f\"{prefix}_n\"]\n\n    gNa = params[f\"{prefix}_gNa\"] * (m**3) * h  # S/cm^2\n    gK = params[f\"{prefix}_gK\"] * n**4  # S/cm^2\n    gLeak = params[f\"{prefix}_gLeak\"]  # S/cm^2\n\n    return (\n        gNa * (v - params[f\"{prefix}_eNa\"])\n        + gK * (v - params[f\"{prefix}_eK\"])\n        + gLeak * (v - params[f\"{prefix}_eLeak\"])\n    )\n
"},{"location":"reference/mechanisms/#jaxley.channels.hh.HH.init_state","title":"init_state(states, v, params, delta_t)","text":"

Initialize the state such at fixed point of gate dynamics.

Source code in jaxley/channels/hh.py
def init_state(self, states, v, params, delta_t):\n    \"\"\"Initialize the state such at fixed point of gate dynamics.\"\"\"\n    prefix = self._name\n    alpha_m, beta_m = self.m_gate(v)\n    alpha_h, beta_h = self.h_gate(v)\n    alpha_n, beta_n = self.n_gate(v)\n    return {\n        f\"{prefix}_m\": alpha_m / (alpha_m + beta_m),\n        f\"{prefix}_h\": alpha_h / (alpha_h + beta_h),\n        f\"{prefix}_n\": alpha_n / (alpha_n + beta_n),\n    }\n
"},{"location":"reference/mechanisms/#jaxley.channels.hh.HH.update_states","title":"update_states(states, dt, v, params)","text":"

Return updated HH channel state.

Source code in jaxley/channels/hh.py
def update_states(\n    self,\n    states: Dict[str, jnp.ndarray],\n    dt,\n    v,\n    params: Dict[str, jnp.ndarray],\n):\n    \"\"\"Return updated HH channel state.\"\"\"\n    prefix = self._name\n    m, h, n = states[f\"{prefix}_m\"], states[f\"{prefix}_h\"], states[f\"{prefix}_n\"]\n    new_m = solve_gate_exponential(m, dt, *self.m_gate(v))\n    new_h = solve_gate_exponential(h, dt, *self.h_gate(v))\n    new_n = solve_gate_exponential(n, dt, *self.n_gate(v))\n    return {f\"{prefix}_m\": new_m, f\"{prefix}_h\": new_h, f\"{prefix}_n\": new_n}\n
"},{"location":"reference/mechanisms/#pospischil","title":"Pospischil","text":"

Bases: Channel

Leak current

Source code in jaxley/channels/pospischil.py
class Leak(Channel):\n    \"\"\"Leak current\"\"\"\n\n    def __init__(self, name: Optional[str] = None):\n        self.current_is_in_mA_per_cm2 = True\n\n        super().__init__(name)\n        prefix = self._name\n        self.channel_params = {\n            f\"{prefix}_gLeak\": 1e-4,\n            f\"{prefix}_eLeak\": -70.0,\n        }\n        self.channel_states = {}\n        self.current_name = f\"i_{prefix}\"\n\n    def update_states(\n        self,\n        states: Dict[str, jnp.ndarray],\n        dt,\n        v,\n        params: Dict[str, jnp.ndarray],\n    ):\n        \"\"\"No state to update.\"\"\"\n        return {}\n\n    def compute_current(\n        self, states: Dict[str, jnp.ndarray], v, params: Dict[str, jnp.ndarray]\n    ):\n        \"\"\"Return current.\"\"\"\n        prefix = self._name\n        gLeak = params[f\"{prefix}_gLeak\"]  # S/cm^2\n        return gLeak * (v - params[f\"{prefix}_eLeak\"])\n\n    def init_state(self, states, v, params, delta_t):\n        return {}\n

Bases: Channel

Sodium channel

Source code in jaxley/channels/pospischil.py
class Na(Channel):\n    \"\"\"Sodium channel\"\"\"\n\n    def __init__(self, name: Optional[str] = None):\n        self.current_is_in_mA_per_cm2 = True\n\n        super().__init__(name)\n        prefix = self._name\n        self.channel_params = {\n            f\"{prefix}_gNa\": 50e-3,\n            \"eNa\": 50.0,\n            \"vt\": -60.0,  # Global parameter, not prefixed with `Na`.\n        }\n        self.channel_states = {f\"{prefix}_m\": 0.2, f\"{prefix}_h\": 0.2}\n        self.current_name = f\"i_Na\"\n\n    def update_states(\n        self,\n        states: Dict[str, jnp.ndarray],\n        dt,\n        v,\n        params: Dict[str, jnp.ndarray],\n    ):\n        \"\"\"Update state.\"\"\"\n        prefix = self._name\n        m, h = states[f\"{prefix}_m\"], states[f\"{prefix}_h\"]\n        new_m = solve_gate_exponential(m, dt, *self.m_gate(v, params[\"vt\"]))\n        new_h = solve_gate_exponential(h, dt, *self.h_gate(v, params[\"vt\"]))\n        return {f\"{prefix}_m\": new_m, f\"{prefix}_h\": new_h}\n\n    def compute_current(\n        self, states: Dict[str, jnp.ndarray], v, params: Dict[str, jnp.ndarray]\n    ):\n        \"\"\"Return current.\"\"\"\n        prefix = self._name\n        m, h = states[f\"{prefix}_m\"], states[f\"{prefix}_h\"]\n\n        gNa = params[f\"{prefix}_gNa\"] * (m**3) * h  # S/cm^2\n\n        current = gNa * (v - params[\"eNa\"])\n        return current\n\n    def init_state(self, states, v, params, delta_t):\n        \"\"\"Initialize the state such at fixed point of gate dynamics.\"\"\"\n        prefix = self._name\n        alpha_m, beta_m = self.m_gate(v, params[\"vt\"])\n        alpha_h, beta_h = self.h_gate(v, params[\"vt\"])\n        return {\n            f\"{prefix}_m\": alpha_m / (alpha_m + beta_m),\n            f\"{prefix}_h\": alpha_h / (alpha_h + beta_h),\n        }\n\n    @staticmethod\n    def m_gate(v, vt):\n        v_alpha = v - vt - 13.0\n        alpha = 0.32 * efun(-0.25 * v_alpha) / 0.25\n\n        v_beta = v - vt - 40.0\n        beta = 0.28 * efun(0.2 * v_beta) / 0.2\n        return alpha, beta\n\n    @staticmethod\n    def h_gate(v, vt):\n        v_alpha = v - vt - 17.0\n        alpha = 0.128 * save_exp(-v_alpha / 18.0)\n\n        v_beta = v - vt - 40.0\n        beta = 4.0 / (save_exp(-v_beta / 5.0) + 1.0)\n        return alpha, beta\n

Bases: Channel

Potassium channel

Source code in jaxley/channels/pospischil.py
class K(Channel):\n    \"\"\"Potassium channel\"\"\"\n\n    def __init__(self, name: Optional[str] = None):\n        self.current_is_in_mA_per_cm2 = True\n\n        super().__init__(name)\n        prefix = self._name\n        self.channel_params = {\n            f\"{prefix}_gK\": 5e-3,\n            \"eK\": -90.0,\n            \"vt\": -60.0,  # Global parameter, not prefixed with `Na`.\n        }\n        self.channel_states = {f\"{prefix}_n\": 0.2}\n        self.current_name = f\"i_K\"\n\n    def update_states(\n        self,\n        states: Dict[str, jnp.ndarray],\n        dt,\n        v,\n        params: Dict[str, jnp.ndarray],\n    ):\n        \"\"\"Update state.\"\"\"\n        prefix = self._name\n        n = states[f\"{prefix}_n\"]\n        new_n = solve_gate_exponential(n, dt, *self.n_gate(v, params[\"vt\"]))\n        return {f\"{prefix}_n\": new_n}\n\n    def compute_current(\n        self, states: Dict[str, jnp.ndarray], v, params: Dict[str, jnp.ndarray]\n    ):\n        \"\"\"Return current.\"\"\"\n        prefix = self._name\n        n = states[f\"{prefix}_n\"]\n\n        gK = params[f\"{prefix}_gK\"] * (n**4)  # S/cm^2\n\n        return gK * (v - params[\"eK\"])\n\n    def init_state(self, states, v, params, delta_t):\n        \"\"\"Initialize the state such at fixed point of gate dynamics.\"\"\"\n        prefix = self._name\n        alpha_n, beta_n = self.n_gate(v, params[\"vt\"])\n        return {f\"{prefix}_n\": alpha_n / (alpha_n + beta_n)}\n\n    @staticmethod\n    def n_gate(v, vt):\n        v_alpha = v - vt - 15.0\n        alpha = 0.032 * efun(-0.2 * v_alpha) / 0.2\n\n        v_beta = v - vt - 10.0\n        beta = 0.5 * save_exp(-v_beta / 40.0)\n        return alpha, beta\n

Bases: Channel

Slow M Potassium channel

Source code in jaxley/channels/pospischil.py
class Km(Channel):\n    \"\"\"Slow M Potassium channel\"\"\"\n\n    def __init__(self, name: Optional[str] = None):\n        self.current_is_in_mA_per_cm2 = True\n\n        super().__init__(name)\n        prefix = self._name\n        self.channel_params = {\n            f\"{prefix}_gKm\": 0.004e-3,\n            f\"{prefix}_taumax\": 4000.0,\n            f\"eK\": -90.0,\n        }\n        self.channel_states = {f\"{prefix}_p\": 0.2}\n        self.current_name = f\"i_K\"\n\n    def update_states(\n        self,\n        states: Dict[str, jnp.ndarray],\n        dt,\n        v,\n        params: Dict[str, jnp.ndarray],\n    ):\n        \"\"\"Update state.\"\"\"\n        prefix = self._name\n        p = states[f\"{prefix}_p\"]\n        new_p = solve_inf_gate_exponential(\n            p, dt, *self.p_gate(v, params[f\"{prefix}_taumax\"])\n        )\n        return {f\"{prefix}_p\": new_p}\n\n    def compute_current(\n        self, states: Dict[str, jnp.ndarray], v, params: Dict[str, jnp.ndarray]\n    ):\n        \"\"\"Return current.\"\"\"\n        prefix = self._name\n        p = states[f\"{prefix}_p\"]\n\n        gKm = params[f\"{prefix}_gKm\"] * p  # S/cm^2\n        return gKm * (v - params[\"eK\"])\n\n    def init_state(self, states, v, params, delta_t):\n        \"\"\"Initialize the state such at fixed point of gate dynamics.\"\"\"\n        prefix = self._name\n        alpha_p, beta_p = self.p_gate(v, params[f\"{prefix}_taumax\"])\n        return {f\"{prefix}_p\": alpha_p / (alpha_p + beta_p)}\n\n    @staticmethod\n    def p_gate(v, taumax):\n        v_p = v + 35.0\n        p_inf = 1.0 / (1.0 + save_exp(-0.1 * v_p))\n\n        tau_p = taumax / (3.3 * save_exp(0.05 * v_p) + save_exp(-0.05 * v_p))\n\n        return p_inf, tau_p\n

Bases: Channel

L-type Calcium channel

Source code in jaxley/channels/pospischil.py
class CaL(Channel):\n    \"\"\"L-type Calcium channel\"\"\"\n\n    def __init__(self, name: Optional[str] = None):\n        self.current_is_in_mA_per_cm2 = True\n\n        super().__init__(name)\n        prefix = self._name\n        self.channel_params = {\n            f\"{prefix}_gCaL\": 0.1e-3,\n            \"eCa\": 120.0,\n        }\n        self.channel_states = {f\"{prefix}_q\": 0.2, f\"{prefix}_r\": 0.2}\n        self.current_name = f\"i_Ca\"\n\n    def update_states(\n        self,\n        states: Dict[str, jnp.ndarray],\n        dt,\n        v,\n        params: Dict[str, jnp.ndarray],\n    ):\n        \"\"\"Update state.\"\"\"\n        prefix = self._name\n        q, r = states[f\"{prefix}_q\"], states[f\"{prefix}_r\"]\n        new_q = solve_gate_exponential(q, dt, *self.q_gate(v))\n        new_r = solve_gate_exponential(r, dt, *self.r_gate(v))\n        return {f\"{prefix}_q\": new_q, f\"{prefix}_r\": new_r}\n\n    def compute_current(\n        self, states: Dict[str, jnp.ndarray], v, params: Dict[str, jnp.ndarray]\n    ):\n        \"\"\"Return current.\"\"\"\n        prefix = self._name\n        q, r = states[f\"{prefix}_q\"], states[f\"{prefix}_r\"]\n        gCaL = params[f\"{prefix}_gCaL\"] * (q**2) * r  # S/cm^2\n\n        return gCaL * (v - params[\"eCa\"])\n\n    def init_state(self, states, v, params, delta_t):\n        \"\"\"Initialize the state such at fixed point of gate dynamics.\"\"\"\n        prefix = self._name\n        alpha_q, beta_q = self.q_gate(v)\n        alpha_r, beta_r = self.r_gate(v)\n        return {\n            f\"{prefix}_q\": alpha_q / (alpha_q + beta_q),\n            f\"{prefix}_r\": alpha_r / (alpha_r + beta_r),\n        }\n\n    @staticmethod\n    def q_gate(v):\n        v_alpha = -v - 27.0\n        alpha = 0.055 * efun(v_alpha / 3.8) * 3.8\n\n        v_beta = -v - 75.0\n        beta = 0.94 * save_exp(v_beta / 17.0)\n        return alpha, beta\n\n    @staticmethod\n    def r_gate(v):\n        v_alpha = -v - 13.0\n        alpha = 0.000457 * save_exp(v_alpha / 50)\n\n        v_beta = -v - 15.0\n        beta = 0.0065 / (save_exp(v_beta / 28.0) + 1)\n        return alpha, beta\n

Bases: Channel

T-type Calcium channel

Source code in jaxley/channels/pospischil.py
class CaT(Channel):\n    \"\"\"T-type Calcium channel\"\"\"\n\n    def __init__(self, name: Optional[str] = None):\n        self.current_is_in_mA_per_cm2 = True\n\n        super().__init__(name)\n        prefix = self._name\n        self.channel_params = {\n            f\"{prefix}_gCaT\": 0.4e-4,\n            f\"{prefix}_vx\": 2.0,\n            \"eCa\": 120.0,  # Global parameter, not prefixed with `CaT`.\n        }\n        self.channel_states = {f\"{prefix}_u\": 0.2}\n        self.current_name = f\"i_Ca\"\n\n    def update_states(\n        self,\n        states: Dict[str, jnp.ndarray],\n        dt,\n        v,\n        params: Dict[str, jnp.ndarray],\n    ):\n        \"\"\"Update state.\"\"\"\n        prefix = self._name\n        u = states[f\"{prefix}_u\"]\n        new_u = solve_inf_gate_exponential(\n            u, dt, *self.u_gate(v, params[f\"{prefix}_vx\"])\n        )\n        return {f\"{prefix}_u\": new_u}\n\n    def compute_current(\n        self, states: Dict[str, jnp.ndarray], v, params: Dict[str, jnp.ndarray]\n    ):\n        \"\"\"Return current.\"\"\"\n        prefix = self._name\n        u = states[f\"{prefix}_u\"]\n        s_inf = 1.0 / (1.0 + save_exp(-(v + params[f\"{prefix}_vx\"] + 57.0) / 6.2))\n\n        gCaT = params[f\"{prefix}_gCaT\"] * (s_inf**2) * u  # S/cm^2\n\n        return gCaT * (v - params[\"eCa\"])\n\n    def init_state(self, states, v, params, delta_t):\n        \"\"\"Initialize the state such at fixed point of gate dynamics.\"\"\"\n        prefix = self._name\n        alpha_u, beta_u = self.u_gate(v, params[f\"{prefix}_vx\"])\n        return {f\"{prefix}_u\": alpha_u / (alpha_u + beta_u)}\n\n    @staticmethod\n    def u_gate(v, vx):\n        v_u1 = v + vx + 81.0\n        u_inf = 1.0 / (1.0 + save_exp(v_u1 / 4))\n\n        tau_u = (30.8 + (211.4 + save_exp((v + vx + 113.2) / 5.0))) / (\n            3.7 * (1 + save_exp((v + vx + 84.0) / 3.2))\n        )\n\n        return u_inf, tau_u\n
"},{"location":"reference/mechanisms/#jaxley.channels.pospischil.Leak.compute_current","title":"compute_current(states, v, params)","text":"

Return current.

Source code in jaxley/channels/pospischil.py
def compute_current(\n    self, states: Dict[str, jnp.ndarray], v, params: Dict[str, jnp.ndarray]\n):\n    \"\"\"Return current.\"\"\"\n    prefix = self._name\n    gLeak = params[f\"{prefix}_gLeak\"]  # S/cm^2\n    return gLeak * (v - params[f\"{prefix}_eLeak\"])\n
"},{"location":"reference/mechanisms/#jaxley.channels.pospischil.Leak.update_states","title":"update_states(states, dt, v, params)","text":"

No state to update.

Source code in jaxley/channels/pospischil.py
def update_states(\n    self,\n    states: Dict[str, jnp.ndarray],\n    dt,\n    v,\n    params: Dict[str, jnp.ndarray],\n):\n    \"\"\"No state to update.\"\"\"\n    return {}\n
"},{"location":"reference/mechanisms/#jaxley.channels.pospischil.Na.compute_current","title":"compute_current(states, v, params)","text":"

Return current.

Source code in jaxley/channels/pospischil.py
def compute_current(\n    self, states: Dict[str, jnp.ndarray], v, params: Dict[str, jnp.ndarray]\n):\n    \"\"\"Return current.\"\"\"\n    prefix = self._name\n    m, h = states[f\"{prefix}_m\"], states[f\"{prefix}_h\"]\n\n    gNa = params[f\"{prefix}_gNa\"] * (m**3) * h  # S/cm^2\n\n    current = gNa * (v - params[\"eNa\"])\n    return current\n
"},{"location":"reference/mechanisms/#jaxley.channels.pospischil.Na.init_state","title":"init_state(states, v, params, delta_t)","text":"

Initialize the state such at fixed point of gate dynamics.

Source code in jaxley/channels/pospischil.py
def init_state(self, states, v, params, delta_t):\n    \"\"\"Initialize the state such at fixed point of gate dynamics.\"\"\"\n    prefix = self._name\n    alpha_m, beta_m = self.m_gate(v, params[\"vt\"])\n    alpha_h, beta_h = self.h_gate(v, params[\"vt\"])\n    return {\n        f\"{prefix}_m\": alpha_m / (alpha_m + beta_m),\n        f\"{prefix}_h\": alpha_h / (alpha_h + beta_h),\n    }\n
"},{"location":"reference/mechanisms/#jaxley.channels.pospischil.Na.update_states","title":"update_states(states, dt, v, params)","text":"

Update state.

Source code in jaxley/channels/pospischil.py
def update_states(\n    self,\n    states: Dict[str, jnp.ndarray],\n    dt,\n    v,\n    params: Dict[str, jnp.ndarray],\n):\n    \"\"\"Update state.\"\"\"\n    prefix = self._name\n    m, h = states[f\"{prefix}_m\"], states[f\"{prefix}_h\"]\n    new_m = solve_gate_exponential(m, dt, *self.m_gate(v, params[\"vt\"]))\n    new_h = solve_gate_exponential(h, dt, *self.h_gate(v, params[\"vt\"]))\n    return {f\"{prefix}_m\": new_m, f\"{prefix}_h\": new_h}\n
"},{"location":"reference/mechanisms/#jaxley.channels.pospischil.K.compute_current","title":"compute_current(states, v, params)","text":"

Return current.

Source code in jaxley/channels/pospischil.py
def compute_current(\n    self, states: Dict[str, jnp.ndarray], v, params: Dict[str, jnp.ndarray]\n):\n    \"\"\"Return current.\"\"\"\n    prefix = self._name\n    n = states[f\"{prefix}_n\"]\n\n    gK = params[f\"{prefix}_gK\"] * (n**4)  # S/cm^2\n\n    return gK * (v - params[\"eK\"])\n
"},{"location":"reference/mechanisms/#jaxley.channels.pospischil.K.init_state","title":"init_state(states, v, params, delta_t)","text":"

Initialize the state such at fixed point of gate dynamics.

Source code in jaxley/channels/pospischil.py
def init_state(self, states, v, params, delta_t):\n    \"\"\"Initialize the state such at fixed point of gate dynamics.\"\"\"\n    prefix = self._name\n    alpha_n, beta_n = self.n_gate(v, params[\"vt\"])\n    return {f\"{prefix}_n\": alpha_n / (alpha_n + beta_n)}\n
"},{"location":"reference/mechanisms/#jaxley.channels.pospischil.K.update_states","title":"update_states(states, dt, v, params)","text":"

Update state.

Source code in jaxley/channels/pospischil.py
def update_states(\n    self,\n    states: Dict[str, jnp.ndarray],\n    dt,\n    v,\n    params: Dict[str, jnp.ndarray],\n):\n    \"\"\"Update state.\"\"\"\n    prefix = self._name\n    n = states[f\"{prefix}_n\"]\n    new_n = solve_gate_exponential(n, dt, *self.n_gate(v, params[\"vt\"]))\n    return {f\"{prefix}_n\": new_n}\n
"},{"location":"reference/mechanisms/#jaxley.channels.pospischil.Km.compute_current","title":"compute_current(states, v, params)","text":"

Return current.

Source code in jaxley/channels/pospischil.py
def compute_current(\n    self, states: Dict[str, jnp.ndarray], v, params: Dict[str, jnp.ndarray]\n):\n    \"\"\"Return current.\"\"\"\n    prefix = self._name\n    p = states[f\"{prefix}_p\"]\n\n    gKm = params[f\"{prefix}_gKm\"] * p  # S/cm^2\n    return gKm * (v - params[\"eK\"])\n
"},{"location":"reference/mechanisms/#jaxley.channels.pospischil.Km.init_state","title":"init_state(states, v, params, delta_t)","text":"

Initialize the state such at fixed point of gate dynamics.

Source code in jaxley/channels/pospischil.py
def init_state(self, states, v, params, delta_t):\n    \"\"\"Initialize the state such at fixed point of gate dynamics.\"\"\"\n    prefix = self._name\n    alpha_p, beta_p = self.p_gate(v, params[f\"{prefix}_taumax\"])\n    return {f\"{prefix}_p\": alpha_p / (alpha_p + beta_p)}\n
"},{"location":"reference/mechanisms/#jaxley.channels.pospischil.Km.update_states","title":"update_states(states, dt, v, params)","text":"

Update state.

Source code in jaxley/channels/pospischil.py
def update_states(\n    self,\n    states: Dict[str, jnp.ndarray],\n    dt,\n    v,\n    params: Dict[str, jnp.ndarray],\n):\n    \"\"\"Update state.\"\"\"\n    prefix = self._name\n    p = states[f\"{prefix}_p\"]\n    new_p = solve_inf_gate_exponential(\n        p, dt, *self.p_gate(v, params[f\"{prefix}_taumax\"])\n    )\n    return {f\"{prefix}_p\": new_p}\n
"},{"location":"reference/mechanisms/#jaxley.channels.pospischil.CaL.compute_current","title":"compute_current(states, v, params)","text":"

Return current.

Source code in jaxley/channels/pospischil.py
def compute_current(\n    self, states: Dict[str, jnp.ndarray], v, params: Dict[str, jnp.ndarray]\n):\n    \"\"\"Return current.\"\"\"\n    prefix = self._name\n    q, r = states[f\"{prefix}_q\"], states[f\"{prefix}_r\"]\n    gCaL = params[f\"{prefix}_gCaL\"] * (q**2) * r  # S/cm^2\n\n    return gCaL * (v - params[\"eCa\"])\n
"},{"location":"reference/mechanisms/#jaxley.channels.pospischil.CaL.init_state","title":"init_state(states, v, params, delta_t)","text":"

Initialize the state such at fixed point of gate dynamics.

Source code in jaxley/channels/pospischil.py
def init_state(self, states, v, params, delta_t):\n    \"\"\"Initialize the state such at fixed point of gate dynamics.\"\"\"\n    prefix = self._name\n    alpha_q, beta_q = self.q_gate(v)\n    alpha_r, beta_r = self.r_gate(v)\n    return {\n        f\"{prefix}_q\": alpha_q / (alpha_q + beta_q),\n        f\"{prefix}_r\": alpha_r / (alpha_r + beta_r),\n    }\n
"},{"location":"reference/mechanisms/#jaxley.channels.pospischil.CaL.update_states","title":"update_states(states, dt, v, params)","text":"

Update state.

Source code in jaxley/channels/pospischil.py
def update_states(\n    self,\n    states: Dict[str, jnp.ndarray],\n    dt,\n    v,\n    params: Dict[str, jnp.ndarray],\n):\n    \"\"\"Update state.\"\"\"\n    prefix = self._name\n    q, r = states[f\"{prefix}_q\"], states[f\"{prefix}_r\"]\n    new_q = solve_gate_exponential(q, dt, *self.q_gate(v))\n    new_r = solve_gate_exponential(r, dt, *self.r_gate(v))\n    return {f\"{prefix}_q\": new_q, f\"{prefix}_r\": new_r}\n
"},{"location":"reference/mechanisms/#jaxley.channels.pospischil.CaT.compute_current","title":"compute_current(states, v, params)","text":"

Return current.

Source code in jaxley/channels/pospischil.py
def compute_current(\n    self, states: Dict[str, jnp.ndarray], v, params: Dict[str, jnp.ndarray]\n):\n    \"\"\"Return current.\"\"\"\n    prefix = self._name\n    u = states[f\"{prefix}_u\"]\n    s_inf = 1.0 / (1.0 + save_exp(-(v + params[f\"{prefix}_vx\"] + 57.0) / 6.2))\n\n    gCaT = params[f\"{prefix}_gCaT\"] * (s_inf**2) * u  # S/cm^2\n\n    return gCaT * (v - params[\"eCa\"])\n
"},{"location":"reference/mechanisms/#jaxley.channels.pospischil.CaT.init_state","title":"init_state(states, v, params, delta_t)","text":"

Initialize the state such at fixed point of gate dynamics.

Source code in jaxley/channels/pospischil.py
def init_state(self, states, v, params, delta_t):\n    \"\"\"Initialize the state such at fixed point of gate dynamics.\"\"\"\n    prefix = self._name\n    alpha_u, beta_u = self.u_gate(v, params[f\"{prefix}_vx\"])\n    return {f\"{prefix}_u\": alpha_u / (alpha_u + beta_u)}\n
"},{"location":"reference/mechanisms/#jaxley.channels.pospischil.CaT.update_states","title":"update_states(states, dt, v, params)","text":"

Update state.

Source code in jaxley/channels/pospischil.py
def update_states(\n    self,\n    states: Dict[str, jnp.ndarray],\n    dt,\n    v,\n    params: Dict[str, jnp.ndarray],\n):\n    \"\"\"Update state.\"\"\"\n    prefix = self._name\n    u = states[f\"{prefix}_u\"]\n    new_u = solve_inf_gate_exponential(\n        u, dt, *self.u_gate(v, params[f\"{prefix}_vx\"])\n    )\n    return {f\"{prefix}_u\": new_u}\n
"},{"location":"reference/mechanisms/#synapses","title":"Synapses","text":""},{"location":"reference/mechanisms/#synapse","title":"Synapse","text":"

Base class for a synapse.

As in NEURON, a Synapse is considered a point process, which means that its conductances are to be specified in uS and its currents are to be specified in nA.

Source code in jaxley/synapses/synapse.py
class Synapse:\n    \"\"\"Base class for a synapse.\n\n    As in NEURON, a `Synapse` is considered a point process, which means that its\n    conductances are to be specified in `uS` and its currents are to be specified in\n    `nA`.\n    \"\"\"\n\n    _name = None\n    synapse_params = None\n    synapse_states = None\n\n    def __init__(self, name: Optional[str] = None):\n        self._name = name if name else self.__class__.__name__\n\n    @property\n    def name(self) -> Optional[str]:\n        return self._name\n\n    def change_name(self, new_name: str):\n        \"\"\"Change the synapse name.\n\n        Args:\n            new_name: The new name of the channel.\n\n        Returns:\n            Renamed channel, such that this function is chainable.\n        \"\"\"\n        old_prefix = self._name + \"_\"\n        new_prefix = new_name + \"_\"\n\n        self._name = new_name\n        self.synapse_params = {\n            (\n                new_prefix + key[len(old_prefix) :]\n                if key.startswith(old_prefix)\n                else key\n            ): value\n            for key, value in self.synapse_params.items()\n        }\n\n        self.synapse_states = {\n            (\n                new_prefix + key[len(old_prefix) :]\n                if key.startswith(old_prefix)\n                else key\n            ): value\n            for key, value in self.synapse_states.items()\n        }\n        return self\n\n    def update_states(\n        states: Dict[str, jnp.ndarray],\n        delta_t: float,\n        pre_voltage: jnp.ndarray,\n        post_voltage: jnp.ndarray,\n        params: Dict[str, jnp.ndarray],\n    ) -> Dict[str, jnp.ndarray]:\n        \"\"\"ODE update step.\n\n        Args:\n            states: States of the synapse.\n            delta_t: Time step in `ms`.\n            pre_voltage: Voltage of the presynaptic compartment, shape `()`.\n            post_voltage: Voltage of the postsynaptic compartment, shape `()`.\n            params: Parameters of the synapse. Conductances in `uS`.\n\n        Returns:\n            Updated states.\"\"\"\n        raise NotImplementedError\n\n    def compute_current(\n        states: Dict[str, jnp.ndarray],\n        pre_voltage: jnp.ndarray,\n        post_voltage: jnp.ndarray,\n        params: Dict[str, jnp.ndarray],\n    ) -> jnp.ndarray:\n        \"\"\"Return current through one synapse in `nA`.\n\n        Internally, we use `jax.vmap` to vectorize this function across many synapses.\n\n        Args:\n            states: States of the synapse.\n            pre_voltage: Voltage of the presynaptic compartment, shape `()`.\n            post_voltage: Voltage of the postsynaptic compartment, shape `()`.\n            params: Parameters of the synapse. Conductances in `uS`.\n\n        Returns:\n            Current through the synapse in `nA`, shape `()`.\n        \"\"\"\n        raise NotImplementedError\n
"},{"location":"reference/mechanisms/#jaxley.synapses.synapse.Synapse.change_name","title":"change_name(new_name)","text":"

Change the synapse name.

Parameters:

Name Type Description Default new_name str

The new name of the channel.

required

Returns:

Type Description

Renamed channel, such that this function is chainable.

Source code in jaxley/synapses/synapse.py
def change_name(self, new_name: str):\n    \"\"\"Change the synapse name.\n\n    Args:\n        new_name: The new name of the channel.\n\n    Returns:\n        Renamed channel, such that this function is chainable.\n    \"\"\"\n    old_prefix = self._name + \"_\"\n    new_prefix = new_name + \"_\"\n\n    self._name = new_name\n    self.synapse_params = {\n        (\n            new_prefix + key[len(old_prefix) :]\n            if key.startswith(old_prefix)\n            else key\n        ): value\n        for key, value in self.synapse_params.items()\n    }\n\n    self.synapse_states = {\n        (\n            new_prefix + key[len(old_prefix) :]\n            if key.startswith(old_prefix)\n            else key\n        ): value\n        for key, value in self.synapse_states.items()\n    }\n    return self\n
"},{"location":"reference/mechanisms/#jaxley.synapses.synapse.Synapse.compute_current","title":"compute_current(states, pre_voltage, post_voltage, params)","text":"

Return current through one synapse in nA.

Internally, we use jax.vmap to vectorize this function across many synapses.

Parameters:

Name Type Description Default states Dict[str, ndarray]

States of the synapse.

required pre_voltage ndarray

Voltage of the presynaptic compartment, shape ().

required post_voltage ndarray

Voltage of the postsynaptic compartment, shape ().

required params Dict[str, ndarray]

Parameters of the synapse. Conductances in uS.

required

Returns:

Type Description ndarray

Current through the synapse in nA, shape ().

Source code in jaxley/synapses/synapse.py
def compute_current(\n    states: Dict[str, jnp.ndarray],\n    pre_voltage: jnp.ndarray,\n    post_voltage: jnp.ndarray,\n    params: Dict[str, jnp.ndarray],\n) -> jnp.ndarray:\n    \"\"\"Return current through one synapse in `nA`.\n\n    Internally, we use `jax.vmap` to vectorize this function across many synapses.\n\n    Args:\n        states: States of the synapse.\n        pre_voltage: Voltage of the presynaptic compartment, shape `()`.\n        post_voltage: Voltage of the postsynaptic compartment, shape `()`.\n        params: Parameters of the synapse. Conductances in `uS`.\n\n    Returns:\n        Current through the synapse in `nA`, shape `()`.\n    \"\"\"\n    raise NotImplementedError\n
"},{"location":"reference/mechanisms/#jaxley.synapses.synapse.Synapse.update_states","title":"update_states(states, delta_t, pre_voltage, post_voltage, params)","text":"

ODE update step.

Parameters:

Name Type Description Default states Dict[str, ndarray]

States of the synapse.

required delta_t float

Time step in ms.

required pre_voltage ndarray

Voltage of the presynaptic compartment, shape ().

required post_voltage ndarray

Voltage of the postsynaptic compartment, shape ().

required params Dict[str, ndarray]

Parameters of the synapse. Conductances in uS.

required

Returns:

Type Description Dict[str, ndarray]

Updated states.

Source code in jaxley/synapses/synapse.py
def update_states(\n    states: Dict[str, jnp.ndarray],\n    delta_t: float,\n    pre_voltage: jnp.ndarray,\n    post_voltage: jnp.ndarray,\n    params: Dict[str, jnp.ndarray],\n) -> Dict[str, jnp.ndarray]:\n    \"\"\"ODE update step.\n\n    Args:\n        states: States of the synapse.\n        delta_t: Time step in `ms`.\n        pre_voltage: Voltage of the presynaptic compartment, shape `()`.\n        post_voltage: Voltage of the postsynaptic compartment, shape `()`.\n        params: Parameters of the synapse. Conductances in `uS`.\n\n    Returns:\n        Updated states.\"\"\"\n    raise NotImplementedError\n
"},{"location":"reference/mechanisms/#ionotropic-synapse","title":"Ionotropic Synapse","text":"

Bases: Synapse

Compute synaptic current and update synapse state for a generic ionotropic synapse.

The synapse state \u201cs\u201d is the probability that a postsynaptic receptor channel is open, and this depends on the amount of neurotransmitter released, which is in turn dependent on the presynaptic voltage.

The synaptic parameters are
  • gS: the maximal conductance across the postsynaptic membrane (uS)
  • e_syn: the reversal potential across the postsynaptic membrane (mV)
  • k_minus: the rate constant of neurotransmitter unbinding from the postsynaptic receptor (s^-1)
Details of this implementation can be found in the following book chapter

L. F. Abbott and E. Marder, \u201cModeling Small Networks,\u201d in Methods in Neuronal Modeling, C. Koch and I. Sergev, Eds. Cambridge: MIT Press, 1998.

Source code in jaxley/synapses/ionotropic.py
class IonotropicSynapse(Synapse):\n    \"\"\"\n    Compute synaptic current and update synapse state for a generic ionotropic synapse.\n\n    The synapse state \"s\" is the probability that a postsynaptic receptor channel is\n    open, and this depends on the amount of neurotransmitter released, which is in turn\n    dependent on the presynaptic voltage.\n\n    The synaptic parameters are:\n        - gS: the maximal conductance across the postsynaptic membrane (uS)\n        - e_syn: the reversal potential across the postsynaptic membrane (mV)\n        - k_minus: the rate constant of neurotransmitter unbinding from the postsynaptic\n            receptor (s^-1)\n\n    Details of this implementation can be found in the following book chapter:\n        L. F. Abbott and E. Marder, \"Modeling Small Networks,\" in Methods in Neuronal\n        Modeling, C. Koch and I. Sergev, Eds. Cambridge: MIT Press, 1998.\n\n    \"\"\"\n\n    def __init__(self, name: Optional[str] = None):\n        super().__init__(name)\n        prefix = self._name\n        self.synapse_params = {\n            f\"{prefix}_gS\": 1e-4,\n            f\"{prefix}_e_syn\": 0.0,\n            f\"{prefix}_k_minus\": 0.025,\n        }\n        self.synapse_states = {f\"{prefix}_s\": 0.2}\n\n    def update_states(\n        self,\n        states: Dict,\n        delta_t: float,\n        pre_voltage: float,\n        post_voltage: float,\n        params: Dict,\n    ) -> Dict:\n        \"\"\"Return updated synapse state and current.\"\"\"\n        prefix = self._name\n        v_th = -35.0  # mV\n        delta = 10.0  # mV\n\n        s_inf = 1.0 / (1.0 + save_exp((v_th - pre_voltage) / delta))\n        tau_s = (1.0 - s_inf) / params[f\"{prefix}_k_minus\"]\n\n        slope = -1.0 / tau_s\n        exp_term = save_exp(slope * delta_t)\n        new_s = states[f\"{prefix}_s\"] * exp_term + s_inf * (1.0 - exp_term)\n        return {f\"{prefix}_s\": new_s}\n\n    def compute_current(\n        self, states: Dict, pre_voltage: float, post_voltage: float, params: Dict\n    ) -> float:\n        prefix = self._name\n        g_syn = params[f\"{prefix}_gS\"] * states[f\"{prefix}_s\"]\n        return g_syn * (post_voltage - params[f\"{prefix}_e_syn\"])\n
"},{"location":"reference/mechanisms/#jaxley.synapses.ionotropic.IonotropicSynapse.update_states","title":"update_states(states, delta_t, pre_voltage, post_voltage, params)","text":"

Return updated synapse state and current.

Source code in jaxley/synapses/ionotropic.py
def update_states(\n    self,\n    states: Dict,\n    delta_t: float,\n    pre_voltage: float,\n    post_voltage: float,\n    params: Dict,\n) -> Dict:\n    \"\"\"Return updated synapse state and current.\"\"\"\n    prefix = self._name\n    v_th = -35.0  # mV\n    delta = 10.0  # mV\n\n    s_inf = 1.0 / (1.0 + save_exp((v_th - pre_voltage) / delta))\n    tau_s = (1.0 - s_inf) / params[f\"{prefix}_k_minus\"]\n\n    slope = -1.0 / tau_s\n    exp_term = save_exp(slope * delta_t)\n    new_s = states[f\"{prefix}_s\"] * exp_term + s_inf * (1.0 - exp_term)\n    return {f\"{prefix}_s\": new_s}\n
"},{"location":"reference/mechanisms/#tanh-rate-synapse","title":"TanH Rate Synapse","text":"

Bases: Synapse

Compute synaptic current for tanh synapse (no state).

Source code in jaxley/synapses/tanh_rate.py
class TanhRateSynapse(Synapse):\n    \"\"\"\n    Compute synaptic current for tanh synapse (no state).\n    \"\"\"\n\n    def __init__(self, name: Optional[str] = None):\n        super().__init__(name)\n        prefix = self._name\n        self.synapse_params = {\n            f\"{prefix}_gS\": 1e-4,\n            f\"{prefix}_x_offset\": -70.0,\n            f\"{prefix}_slope\": 1.0,\n        }\n        self.synapse_states = {}\n\n    def update_states(\n        self,\n        states: Dict,\n        delta_t: float,\n        pre_voltage: float,\n        post_voltage: float,\n        params: Dict,\n    ) -> Dict:\n        \"\"\"Return updated synapse state and current.\"\"\"\n        return {}\n\n    def compute_current(\n        self, states: Dict, pre_voltage: float, post_voltage: float, params: Dict\n    ) -> float:\n        \"\"\"Return updated synapse state and current.\"\"\"\n        prefix = self._name\n        current = (\n            -1\n            * params[f\"{prefix}_gS\"]\n            * jnp.tanh(\n                (pre_voltage - params[f\"{prefix}_x_offset\"]) * params[f\"{prefix}_slope\"]\n            )\n        )\n        return current\n
"},{"location":"reference/mechanisms/#jaxley.synapses.tanh_rate.TanhRateSynapse.compute_current","title":"compute_current(states, pre_voltage, post_voltage, params)","text":"

Return updated synapse state and current.

Source code in jaxley/synapses/tanh_rate.py
def compute_current(\n    self, states: Dict, pre_voltage: float, post_voltage: float, params: Dict\n) -> float:\n    \"\"\"Return updated synapse state and current.\"\"\"\n    prefix = self._name\n    current = (\n        -1\n        * params[f\"{prefix}_gS\"]\n        * jnp.tanh(\n            (pre_voltage - params[f\"{prefix}_x_offset\"]) * params[f\"{prefix}_slope\"]\n        )\n    )\n    return current\n
"},{"location":"reference/mechanisms/#jaxley.synapses.tanh_rate.TanhRateSynapse.update_states","title":"update_states(states, delta_t, pre_voltage, post_voltage, params)","text":"

Return updated synapse state and current.

Source code in jaxley/synapses/tanh_rate.py
def update_states(\n    self,\n    states: Dict,\n    delta_t: float,\n    pre_voltage: float,\n    post_voltage: float,\n    params: Dict,\n) -> Dict:\n    \"\"\"Return updated synapse state and current.\"\"\"\n    return {}\n
"},{"location":"reference/modules/","title":"Modules","text":""},{"location":"reference/modules/#module","title":"Module","text":"

Bases: ABC

Module base class.

Modules are everything that can be passed to jx.integrate, i.e. compartments, branches, cells, and networks.

This base class defines the scaffold for all jaxley modules (compartments, branches, cells, networks).

Modules can be traversed and modified using the at, cell, branch, comp, edge, and loc methods. The scope method can be used to toggle between global and local indices. Traversal of Modules will return a View of itself, that has a modified set of attributes, which only consider the part of the Module that is in view.

For developers: The above has consequences for how to operate on Module and which changes take affect where. The following guidelines should be followed (copied from View):

  1. We consider a Module to have everything in view.
  2. Views can display and keep track of how a module is traversed. But(!), do not support making changes or setting variables. This still has to be done in the base Module, i.e. self.base. In order to enssure that these changes only affects whatever is currently in view self._nodes_in_view, or self._edges_in_view among others have to be used. Operating on nodes currently in view can for example be done with self.base.node.loc[self._nodes_in_view].
  3. Every attribute of Module that changes based on what\u2019s in view, i.e. xyzr, needs to modified when View is instantiated. I.e. xyzr of cell.branch(0), should be [self.base.xyzr[0]] This could be achieved via: [self.base.xyzr[b] for b in self._branches_in_view].

For developers: If you want to add a new method to Module, here is an example of how to make methods of Module compatible with View:

.. code-block:: python

# Use data in view to return something.\ndef count_small_branches(self):\n    # no need to use self.base.attr + viewed indices,\n    # since no change is made to the attr in question (nodes)\n    comp_lens = self.nodes[\"length\"]\n    branch_lens = comp_lens.groupby(\"global_branch_index\").sum()\n    return np.sum(branch_lens < 10)\n\n# Change data in view.\ndef change_attr_in_view(self):\n    # changes to attrs have to be made via self.base.attr + viewed indices\n    a = func1(self.base.attr1[self._cells_in_view])\n    b = func2(self.base.attr2[self._edges_in_view])\n    self.base.attr3[self._branches_in_view] = a + b\n
Source code in jaxley/modules/base.py
class Module(ABC):\n    \"\"\"Module base class.\n\n    Modules are everything that can be passed to `jx.integrate`, i.e. compartments,\n    branches, cells, and networks.\n\n    This base class defines the scaffold for all jaxley modules (compartments,\n    branches, cells, networks).\n\n    Modules can be traversed and modified using the `at`, `cell`, `branch`, `comp`,\n    `edge`, and `loc` methods. The `scope` method can be used to toggle between\n    global and local indices. Traversal of Modules will return a `View` of itself,\n    that has a modified set of attributes, which only consider the part of the Module\n    that is in view.\n\n    For developers: The above has consequences for how to operate on `Module` and which\n    changes take affect where. The following guidelines should be followed (copied from\n    `View`):\n\n    1. We consider a Module to have everything in view.\n    2. Views can display and keep track of how a module is traversed. But(!),\n       do not support making changes or setting variables. This still has to be\n       done in the base Module, i.e. `self.base`. In order to enssure that these\n       changes only affects whatever is currently in view `self._nodes_in_view`,\n       or `self._edges_in_view` among others have to be used. Operating on nodes\n       currently in view can for example be done with\n       `self.base.node.loc[self._nodes_in_view]`.\n    3. Every attribute of Module that changes based on what's in view, i.e. `xyzr`,\n       needs to modified when View is instantiated. I.e. `xyzr` of `cell.branch(0)`,\n       should be `[self.base.xyzr[0]]` This could be achieved via:\n       `[self.base.xyzr[b] for b in self._branches_in_view]`.\n\n    For developers: If you want to add a new method to `Module`, here is an example of\n    how to make methods of Module compatible with View:\n\n    .. code-block:: python\n\n        # Use data in view to return something.\n        def count_small_branches(self):\n            # no need to use self.base.attr + viewed indices,\n            # since no change is made to the attr in question (nodes)\n            comp_lens = self.nodes[\"length\"]\n            branch_lens = comp_lens.groupby(\"global_branch_index\").sum()\n            return np.sum(branch_lens < 10)\n\n        # Change data in view.\n        def change_attr_in_view(self):\n            # changes to attrs have to be made via self.base.attr + viewed indices\n            a = func1(self.base.attr1[self._cells_in_view])\n            b = func2(self.base.attr2[self._edges_in_view])\n            self.base.attr3[self._branches_in_view] = a + b\n    \"\"\"\n\n    def __init__(self):\n        self.nseg: int = None\n        self.total_nbranches: int = 0\n        self.nbranches_per_cell: List[int] = None\n\n        self.groups = {}\n\n        self.nodes: Optional[pd.DataFrame] = None\n        self._scope = \"local\"  # defaults to local scope\n        self._nodes_in_view: np.ndarray = None\n        self._edges_in_view: np.ndarray = None\n\n        self.edges = pd.DataFrame(\n            columns=[\n                \"global_edge_index\",\n                \"pre_global_comp_index\",\n                \"post_global_comp_index\",\n                \"pre_locs\",\n                \"post_locs\",\n                \"type\",\n                \"type_ind\",\n            ]\n        )\n\n        self._cumsum_nbranches: Optional[np.ndarray] = None\n\n        self.comb_parents: jnp.ndarray = jnp.asarray([-1])\n\n        self.initialized_morph: bool = False\n        self.initialized_syns: bool = False\n\n        # List of all types of `jx.Synapse`s.\n        self.synapses: List = []\n        self.synapse_param_names = []\n        self.synapse_state_names = []\n        self.synapse_names = []\n\n        # List of types of all `jx.Channel`s.\n        self.channels: List[Channel] = []\n        self.membrane_current_names: List[str] = []\n\n        # For trainable parameters.\n        self.indices_set_by_trainables: List[jnp.ndarray] = []\n        self.trainable_params: List[Dict[str, jnp.ndarray]] = []\n        self.allow_make_trainable: bool = True\n        self.num_trainable_params: int = 0\n\n        # For recordings.\n        self.recordings: pd.DataFrame = pd.DataFrame().from_dict({})\n\n        # For stimuli or clamps.\n        # E.g. `self.externals = {\"v\": zeros(1000,2), \"i\": ones(1000, 2)}`\n        # for 1000 timesteps and two compartments.\n        self.externals: Dict[str, jnp.ndarray] = {}\n        # E.g. `self.external)inds = {\"v\": jnp.asarray([0,1]), \"i\": jnp.asarray([2,3])}`\n        self.external_inds: Dict[str, jnp.ndarray] = {}\n\n        # x, y, z coordinates and radius.\n        self.xyzr: List[np.ndarray] = []\n        self._radius_generating_fns = None  # Defined by `.read_swc()`.\n\n        # For debugging the solver. Will be empty by default and only filled if\n        # `self._init_morph_for_debugging` is run.\n        self.debug_states = {}\n\n        # needs to be set at the end\n        self.base: Module = self\n\n    def __repr__(self):\n        return f\"{type(self).__name__} with {len(self.channels)} different channels. Use `.nodes` for details.\"\n\n    def __str__(self):\n        return f\"jx.{type(self).__name__}\"\n\n    def __dir__(self):\n        base_dir = object.__dir__(self)\n        return sorted(base_dir + self.synapse_names + list(self.group_nodes.keys()))\n\n    def __getattr__(self, key):\n        # Ensure that hidden methods such as `__deepcopy__` still work.\n        if key.startswith(\"__\"):\n            return super().__getattribute__(key)\n\n        # intercepts calls to groups\n        if key in self.base.groups:\n            view = (\n                self.select(self.groups[key])\n                if key in self.groups\n                else self.select(None)\n            )\n            view._set_controlled_by_param(key)\n            return view\n\n        # intercepts calls to channels\n        if key in [c._name for c in self.base.channels]:\n            channel_names = [c._name for c in self.channels]\n            inds = self.nodes.index[self.nodes[key]].to_numpy()\n            view = self.select(inds) if key in channel_names else self.select(None)\n            view._set_controlled_by_param(key)\n            return view\n\n        # intercepts calls to synapse types\n        if key in self.base.synapse_names:\n            syn_inds = self.edges[self.edges[\"type\"] == key][\n                \"global_edge_index\"\n            ].to_numpy()\n            orig_scope = self._scope\n            view = (\n                self.scope(\"global\").edge(syn_inds).scope(orig_scope)\n                if key in self.synapse_names\n                else self.select(None)\n            )\n            view._set_controlled_by_param(key)  # overwrites param set by edge\n            # Ensure synapse param sharing works with `edge`\n            # `edge` will be removed as part of #463\n            view.edges[\"local_edge_index\"] = np.arange(len(view.edges))\n            return view\n\n    def _childviews(self) -> List[str]:\n        \"\"\"Returns levels that module can be viewed at.\n\n        I.e. for net -> [cell, branch, comp]. For branch -> [comp]\"\"\"\n        levels = [\"network\", \"cell\", \"branch\", \"comp\"]\n        if self._current_view in levels:\n            children = levels[levels.index(self._current_view) + 1 :]\n            return children\n        return []\n\n    def _has_childview(self, key: str) -> bool:\n        child_views = self._childviews()\n        return key in child_views\n\n    def __getitem__(self, index):\n        \"\"\"Lazy indexing of the module.\"\"\"\n        supported_parents = [\"network\", \"cell\", \"branch\"]  # cannot index into comp\n\n        not_group_view = self._current_view not in self.groups\n        assert (\n            self._current_view in supported_parents or not_group_view\n        ), \"Lazy indexing is only supported for `Network`, `Cell`, `Branch` and Views thereof.\"\n        index = index if isinstance(index, tuple) else (index,)\n\n        child_views = self._childviews()\n        assert len(index) <= len(child_views), \"Too many indices.\"\n        view = self\n        for i, child in zip(index, child_views):\n            view = view._at_nodes(child, i)\n        return view\n\n    def _update_local_indices(self) -> pd.DataFrame:\n        \"\"\"Compute local indices from the global indices that are in view.\n        This is recomputed everytime a View is created.\"\"\"\n        rerank = lambda df: df.rank(method=\"dense\").astype(int) - 1\n\n        def reorder_cols(\n            df: pd.DataFrame, cols: List[str], first: bool = True\n        ) -> pd.DataFrame:\n            \"\"\"Move cols to front/back.\n\n            Args:\n                df: DataFrame to reorder.\n                cols: List of columns to place before/after remaining columns.\n                first: If True, cols are placed in front, otherwise at the end.\n\n            Returns:\n                DataFrame with reordered columns.\"\"\"\n            new_cols = [col for col in df.columns if first == (col in cols)]\n            new_cols += [col for col in df.columns if first != (col in cols)]\n            return df[new_cols]\n\n        def reindex_a_by_b(\n            df: pd.DataFrame, a: str, b: Optional[Union[str, List[str]]] = None\n        ) -> pd.DataFrame:\n            \"\"\"Reindex based on a different col or several columns\n            for b=[0,0,1,1,2,2,2] -> a=[0,1,0,1,0,1,2]\"\"\"\n            grouped_df = df.groupby(b) if b is not None else df\n            df.loc[:, a] = rerank(grouped_df[a])\n            return df\n\n        index_names = [\"cell_index\", \"branch_index\", \"comp_index\"]  # order is important\n        global_idx_cols = [f\"global_{name}\" for name in index_names]\n        local_idx_cols = [f\"local_{name}\" for name in index_names]\n        idcs = self.nodes[global_idx_cols]\n\n        # update local indices of nodes\n        idcs = reindex_a_by_b(idcs, global_idx_cols[0])\n        idcs = reindex_a_by_b(idcs, global_idx_cols[1], global_idx_cols[0])\n        idcs = reindex_a_by_b(idcs, global_idx_cols[2], global_idx_cols[:2])\n        idcs.columns = [col.replace(\"global\", \"local\") for col in global_idx_cols]\n        self.nodes[local_idx_cols] = idcs[local_idx_cols].astype(int)\n\n        # move indices to the front of the dataframe; move controlled_by_param to the end\n        # move indices of current scope to the front and the others to the back\n        not_scope = \"global\" if self._scope == \"local\" else \"local\"\n        self.nodes = reorder_cols(\n            self.nodes, [f\"{self._scope}_{name}\" for name in index_names], first=True\n        )\n        self.nodes = reorder_cols(\n            self.nodes, [f\"{not_scope}_{name}\" for name in index_names], first=False\n        )\n\n        self.edges = reorder_cols(self.edges, [\"global_edge_index\"])\n        self.nodes = reorder_cols(self.nodes, [\"controlled_by_param\"], first=False)\n        self.edges = reorder_cols(self.edges, [\"controlled_by_param\"], first=False)\n\n    def _init_view(self):\n        \"\"\"Init attributes critical for View.\n\n        Needs to be called at init of a Module.\"\"\"\n        parent = self.__class__.__name__.lower()\n        self._current_view = \"comp\" if parent == \"compartment\" else parent\n        self._nodes_in_view = self.nodes.index.to_numpy()\n        self._edges_in_view = self.edges.index.to_numpy()\n        self.nodes[\"controlled_by_param\"] = 0\n\n    def _compute_coords_of_comp_centers(self) -> np.ndarray:\n        \"\"\"Compute xyz coordinates of compartment centers.\n\n        Centers are the midpoint between the comparment endpoints on the morphology\n        as defined by xyzr.\n\n        Note: For sake of performance, interpolation is not done for each branch\n        individually, but only once along a concatenated (and padded) array of all branches.\n        This means for nsegs = [2,4] and normalized cum_branch_lens of [[0,1],[0,1]] we would\n        interpolate xyz at the locations comp_ends = [[0,0.5,1], [0,0.25,0.5,0.75,1]],\n        where 0 is the start of the branch and 1 is the end point at the full branch_len.\n        To avoid do this in one go we set comp_ends = [0,0.5,1,2,2.25,2.5,2.75,3], and\n        norm_cum_branch_len = [0,1,2,3] incrememting and also padding them by 1 to\n        avoid overlapping branch_lens i.e. norm_cum_branch_len = [0,1,1,2] for only\n        incrementing.\n        \"\"\"\n        nodes_by_branches = self.nodes.groupby(\"global_branch_index\")\n        nsegs = nodes_by_branches[\"global_comp_index\"].nunique().to_numpy()\n\n        comp_ends = [\n            np.linspace(0, 1, nseg + 1) + 2 * i for i, nseg in enumerate(nsegs)\n        ]\n        comp_ends = np.hstack(comp_ends)\n\n        comp_ends = comp_ends.reshape(-1)\n        cum_branch_lens = []\n        for i, xyzr in enumerate(self.xyzr):\n            branch_len = np.sqrt(np.sum(np.diff(xyzr[:, :3], axis=0) ** 2, axis=1))\n            cum_branch_len = np.cumsum(np.concatenate([np.array([0]), branch_len]))\n            max_len = cum_branch_len.max()\n            # add padding like above\n            cum_branch_len = cum_branch_len / (max_len if max_len > 0 else 1) + 2 * i\n            cum_branch_len[np.isnan(cum_branch_len)] = 0\n            cum_branch_lens.append(cum_branch_len)\n        cum_branch_lens = np.hstack(cum_branch_lens)\n        xyz = np.vstack(self.xyzr)[:, :3]\n        xyz = v_interp(comp_ends, cum_branch_lens, xyz).T\n        centers = (xyz[:-1] + xyz[1:]) / 2  # unaware of inter vs intra comp centers\n        cum_nsegs = np.cumsum(nsegs)\n        # this means centers between comps have to be removed here\n        between_comp_inds = (cum_nsegs + np.arange(len(cum_nsegs)))[:-1]\n        centers = np.delete(centers, between_comp_inds, axis=0)\n        return centers\n\n    def compute_compartment_centers(self):\n        \"\"\"Add compartment centers to nodes dataframe\"\"\"\n        centers = self._compute_coords_of_comp_centers()\n        self.base.nodes.loc[self._nodes_in_view, [\"x\", \"y\", \"z\"]] = centers\n\n    def _reformat_index(self, idx: Any, dtype: type = int) -> np.ndarray:\n        \"\"\"Transforms different types of indices into an array.\n\n        Takes slice, list, array, ints, range and None and transforms\n        it into array of indices. If index == \"all\" it returns \"all\"\n        to be handled downstream.\n\n        Args:\n            idx: index that specifies at which locations to view the module.\n            dtype: defaults to int, but can also reformat float for use in `loc`\n\n        Returns:\n            array of indices of shape (N,)\"\"\"\n        if is_str_all(idx):  # also asserts that the only allowed str == \"all\"\n            return idx\n\n        np_dtype = np.int64 if dtype is int else np.float64\n        idx = np.array([], dtype=dtype) if idx is None else idx\n        idx = np.array([idx]) if isinstance(idx, (dtype, np_dtype)) else idx\n        idx = np.array(idx) if isinstance(idx, (list, range, pd.Index)) else idx\n\n        idx = np.arange(len(self.base.nodes))[idx] if isinstance(idx, slice) else idx\n        if idx.dtype == bool:\n            shape = (*self.shape, len(self.edges))\n            which_idx = len(idx) == np.array(shape)\n            assert np.any(which_idx), \"Index not matching num of cells/branches/comps.\"\n            dim = shape[np.where(which_idx)[0][0]]\n            idx = np.arange(dim)[idx]\n        assert isinstance(idx, np.ndarray), \"Invalid type\"\n        assert idx.dtype in [np_dtype, bool], \"Invalid dtype\"\n        return idx.reshape(-1)\n\n    def _set_controlled_by_param(self, key: str):\n        \"\"\"Determines which parameters are shared in `make_trainable`.\n\n        Adds column to nodes/edges dataframes to read of shared params from.\n\n        Args:\n            key: key specifying group / view that is in control of the params.\"\"\"\n        if key in [\"comp\", \"branch\", \"cell\"]:\n            self.nodes[\"controlled_by_param\"] = self.nodes[f\"global_{key}_index\"]\n            self.edges[\"controlled_by_param\"] = 0\n        elif key == \"edge\":\n            self.edges[\"controlled_by_param\"] = np.arange(len(self.edges))\n        elif key == \"filter\":\n            self.nodes[\"controlled_by_param\"] = np.arange(len(self.nodes))\n            self.edges[\"controlled_by_param\"] = np.arange(len(self.edges))\n        else:\n            self.nodes[\"controlled_by_param\"] = 0\n            self.edges[\"controlled_by_param\"] = 0\n        self._current_view = key\n\n    def select(\n        self, nodes: np.ndarray = None, edges: np.ndarray = None, sorted: bool = False\n    ) -> View:\n        \"\"\"Return View of the module filtered by specific node or edges indices.\n\n        Args:\n            nodes: indices of nodes to view. If None, all nodes are viewed.\n            edges: indices of edges to view. If None, all edges are viewed.\n            sorted: if True, nodes and edges are sorted.\n\n        Returns:\n            View for subset of selected nodes and/or edges.\"\"\"\n\n        nodes = self._reformat_index(nodes) if nodes is not None else None\n        nodes = self._nodes_in_view if is_str_all(nodes) else nodes\n        nodes = np.sort(nodes) if sorted else nodes\n\n        edges = self._reformat_index(edges) if edges is not None else None\n        edges = self._edges_in_view if is_str_all(edges) else edges\n        edges = np.sort(edges) if sorted else edges\n\n        view = View(self, nodes, edges)\n        view._set_controlled_by_param(\"filter\")\n        return view\n\n    def set_scope(self, scope: str):\n        \"\"\"Toggle between \"global\" or \"local\" scope.\n\n        Determines if global or local indices are used for viewing the module.\n\n        Args:\n            scope: either \"global\" or \"local\".\"\"\"\n        assert scope in [\"global\", \"local\"], \"Invalid scope.\"\n        self._scope = scope\n\n    def scope(self, scope: str) -> View:\n        \"\"\"Return a View of the module with the specified scope.\n\n        For example `cell.scope(\"global\").branch(2).scope(\"local\").comp(1)`\n        will return the 1st compartment of branch 2.\n\n        Args:\n            scope: either \"global\" or \"local\".\n\n        Returns:\n            View with the specified scope.\"\"\"\n        view = self.view\n        view.set_scope(scope)\n        return view\n\n    def _at_nodes(self, key: str, idx: Any) -> View:\n        \"\"\"Return a View of the module filtering `nodes` by specified key and index.\n\n        Keys can be `cell`, `branch`, `comp` and determine which index is used to filter.\n        \"\"\"\n        base_name = self.base.__class__.__name__\n        assert self.base._has_childview(key), f\"{base_name} does not support {key}.\"\n        idx = self._reformat_index(idx)\n        idx = self.nodes[self._scope + f\"_{key}_index\"] if is_str_all(idx) else idx\n        where = self.nodes[self._scope + f\"_{key}_index\"].isin(idx)\n        inds = self.nodes.index[where].to_numpy()\n\n        view = View(self, nodes=inds)\n        view._set_controlled_by_param(key)\n        return view\n\n    def _at_edges(self, key: str, idx: Any) -> View:\n        \"\"\"Return a View of the module filtering `edges` by specified key and index.\n\n        Keys can be `pre`, `post`, `edge` and determine which index is used to filter.\n        \"\"\"\n        idx = self._reformat_index(idx)\n        idx = self.edges[self._scope + f\"_{key}_index\"] if is_str_all(idx) else idx\n        where = self.edges[self._scope + f\"_{key}_index\"].isin(idx)\n        inds = self.edges.index[where].to_numpy()\n\n        view = View(self, edges=inds)\n        view._set_controlled_by_param(key)\n        return view\n\n    def cell(self, idx: Any) -> View:\n        \"\"\"Return a View of the module at the selected cell(s).\n\n        Args:\n            idx: index of the cell to view.\n\n        Returns:\n            View of the module at the specified cell index.\"\"\"\n        return self._at_nodes(\"cell\", idx)\n\n    def branch(self, idx: Any) -> View:\n        \"\"\"Return a View of the module at the selected branches(s).\n\n        Args:\n            idx: index of the branch to view.\n\n        Returns:\n            View of the module at the specified branch index.\"\"\"\n        return self._at_nodes(\"branch\", idx)\n\n    def comp(self, idx: Any) -> View:\n        \"\"\"Return a View of the module at the selected compartments(s).\n\n        Args:\n            idx: index of the comp to view.\n\n        Returns:\n            View of the module at the specified compartment index.\"\"\"\n        return self._at_nodes(\"comp\", idx)\n\n    def edge(self, idx: Any) -> View:\n        \"\"\"Return a View of the module at the selected synapse edges(s).\n\n        Args:\n            idx: index of the edge to view.\n\n        Returns:\n            View of the module at the specified edge index.\"\"\"\n        return self._at_edges(\"edge\", idx)\n\n    def loc(self, at: Any) -> View:\n        \"\"\"Return a View of the module at the selected branch location(s).\n\n        Args:\n            at: location along the branch.\n\n        Returns:\n            View of the module at the specified branch location.\"\"\"\n        global_comp_idxs = []\n        for i in self._branches_in_view:\n            nseg = self.base.nseg_per_branch[i]\n            comp_locs = np.linspace(0, 1, nseg)\n            at = comp_locs if is_str_all(at) else self._reformat_index(at, dtype=float)\n            comp_edges = np.linspace(0, 1 + 1e-10, nseg + 1)\n            idx = np.digitize(at, comp_edges) - 1 + self.base.cumsum_nseg[i]\n            global_comp_idxs.append(idx)\n        global_comp_idxs = np.concatenate(global_comp_idxs)\n        orig_scope = self._scope\n        # global scope needed to select correct comps, for i.e. branches w. nseg=[1,2]\n        # loc(0.9)  will correspond to different local branches (0 vs 1).\n        view = self.scope(\"global\").comp(global_comp_idxs).scope(orig_scope)\n        view._current_view = \"loc\"\n        return view\n\n    @property\n    def _comps_in_view(self):\n        \"\"\"Lists the global compartment indices which are currently part of the view.\"\"\"\n        # method also exists in View. this copy forgoes need to instantiate a View\n        return self.nodes[\"global_comp_index\"].unique()\n\n    @property\n    def _branches_in_view(self):\n        \"\"\"Lists the global branch indices which are currently part of the view.\"\"\"\n        # method also exists in View. this copy forgoes need to instantiate a View\n        return self.nodes[\"global_branch_index\"].unique()\n\n    @property\n    def _cells_in_view(self):\n        \"\"\"Lists the global cell indices which are currently part of the view.\"\"\"\n        # method also exists in View. this copy forgoes need to instantiate a View\n        return self.nodes[\"global_cell_index\"].unique()\n\n    def _iter_submodules(self, name: str):\n        \"\"\"Iterate over submoduleslevel.\n\n        Used for `cells`, `branches`, `comps`.\"\"\"\n        col = self._scope + f\"_{name}_index\"\n        idxs = self.nodes[col].unique()\n        for idx in idxs:\n            yield self._at_nodes(name, idx)\n\n    @property\n    def cells(self):\n        \"\"\"Iterate over all cells in the module.\n\n        Returns a generator that yields a View of each cell.\"\"\"\n        yield from self._iter_submodules(\"cell\")\n\n    @property\n    def branches(self):\n        \"\"\"Iterate over all branches in the module.\n\n        Returns a generator that yields a View of each branch.\"\"\"\n        yield from self._iter_submodules(\"branch\")\n\n    @property\n    def comps(self):\n        \"\"\"Iterate over all compartments in the module.\n        Can be called on any module, i.e. `net.comps`, `cell.comps` or\n        `branch.comps`. `__iter__` does not allow for this.\n\n        Returns a generator that yields a View of each compartment.\"\"\"\n        yield from self._iter_submodules(\"comp\")\n\n    def __iter__(self):\n        \"\"\"Iterate over parts of the module.\n\n        Internally calls `cells`, `branches`, `comps` at the appropriate level.\n\n        Example:\n\n        .. code-block:: python\n\n            for cell in network:\n                for branch in cell:\n                    for comp in branch:\n                        print(comp.nodes.shape)\n        \"\"\"\n        next_level = self._childviews()[0]\n        yield from self._iter_submodules(next_level)\n\n    @property\n    def shape(self) -> Tuple[int]:\n        \"\"\"Returns the number of submodules contained in a module.\n\n        .. code-block:: python\n\n            network.shape = (num_cells, num_branches, num_compartments)\n            cell.shape = (num_branches, num_compartments)\n            branch.shape = (num_compartments,)\n        \"\"\"\n        cols = [\"global_cell_index\", \"global_branch_index\", \"global_comp_index\"]\n        raw_shape = self.nodes[cols].nunique().to_list()\n\n        # ensure (net.shape -> dim=3, cell.shape -> dim=2, branch.shape -> dim=1, comp.shape -> dim=0)\n        levels = [\"network\", \"cell\", \"branch\", \"comp\"]\n        module = self.base.__class__.__name__.lower()\n        module = \"comp\" if module == \"compartment\" else module\n        shape = tuple(raw_shape[levels.index(module) :])\n        return shape\n\n    def copy(\n        self, reset_index: bool = False, as_module: bool = False\n    ) -> Union[Module, View]:\n        \"\"\"Extract part of a module and return a copy of its View or a new module.\n\n        This can be used to call `jx.integrate` on part of a Module.\n\n        Args:\n            reset_index: if True, the indices of the new module are reset to start from 0.\n            as_module: if True, a new module is returned instead of a View.\n\n        Returns:\n            A part of the module or a copied view of it.\"\"\"\n        view = deepcopy(self)\n        warnings.warn(\"This method is experimental, use at your own risk.\")\n        # TODO FROM #447: add reset_index, i.e. for parents, nodes, edges etc. such that they\n        # start from 0/-1 and are contiguous\n        if as_module:\n            raise NotImplementedError(\"Not yet implemented.\")\n            # initialize a new module with the same attributes\n        return view\n\n    @property\n    def view(self):\n        \"\"\"Return view of the module.\"\"\"\n        return View(self, self._nodes_in_view, self._edges_in_view)\n\n    @property\n    def _module_type(self):\n        \"\"\"Return type of the module (compartment, branch, cell, network) as string.\n\n        This is used to perform asserts for some modules (e.g. network cannot use\n        `set_ncomp`) without having to import the module in `base.py`.\"\"\"\n        return self.__class__.__name__.lower()\n\n    def _append_params_and_states(self, param_dict: Dict, state_dict: Dict):\n        \"\"\"Insert the default params of the module (e.g. radius, length).\n\n        This is run at `__init__()`. It does not deal with channels.\n        \"\"\"\n        for param_name, param_value in param_dict.items():\n            self.base.nodes[param_name] = param_value\n        for state_name, state_value in state_dict.items():\n            self.base.nodes[state_name] = state_value\n\n    def _gather_channels_from_constituents(self, constituents: List):\n        \"\"\"Modify `self.channels` and `self.nodes` with channel info from constituents.\n\n        This is run at `__init__()`. It takes all branches of constituents (e.g.\n        of all branches when the are assembled into a cell) and adds columns to\n        `.nodes` for the relevant channels.\n        \"\"\"\n        for module in constituents:\n            for channel in module.channels:\n                if channel._name not in [c._name for c in self.channels]:\n                    self.base.channels.append(channel)\n                if channel.current_name not in self.membrane_current_names:\n                    self.base.membrane_current_names.append(channel.current_name)\n        # Setting columns of channel names to `False` instead of `NaN`.\n        for channel in self.base.channels:\n            name = channel._name\n            self.base.nodes.loc[self.nodes[name].isna(), name] = False\n\n    @only_allow_module\n    def to_jax(self):\n        # TODO FROM #447: Make this work for View?\n        \"\"\"Move `.nodes` to `.jaxnodes`.\n\n        Before the actual simulation is run (via `jx.integrate`), all parameters of\n        the `jx.Module` are stored in `.nodes` (a `pd.DataFrame`). However, for\n        simulation, these parameters have to be moved to be `jnp.ndarrays` such that\n        they can be processed on GPU/TPU and such that the simulation can be\n        differentiated. `.to_jax()` copies the `.nodes` to `.jaxnodes`.\n        \"\"\"\n        self.base.jaxnodes = {}\n        for key, value in self.base.nodes.to_dict(orient=\"list\").items():\n            inds = jnp.arange(len(value))\n            self.base.jaxnodes[key] = jnp.asarray(value)[inds]\n\n        # `jaxedges` contains only parameters (no indices).\n        # `jaxedges` contains only non-Nan elements. This is unlike the channels where\n        # we allow parameter sharing.\n        self.base.jaxedges = {}\n        edges = self.base.edges.to_dict(orient=\"list\")\n        for i, synapse in enumerate(self.base.synapses):\n            condition = np.asarray(edges[\"type_ind\"]) == i\n            for key in synapse.synapse_params:\n                self.base.jaxedges[key] = jnp.asarray(np.asarray(edges[key])[condition])\n            for key in synapse.synapse_states:\n                self.base.jaxedges[key] = jnp.asarray(np.asarray(edges[key])[condition])\n\n    def show(\n        self,\n        param_names: Optional[Union[str, List[str]]] = None,\n        *,\n        indices: bool = True,\n        params: bool = True,\n        states: bool = True,\n        channel_names: Optional[List[str]] = None,\n    ) -> pd.DataFrame:\n        \"\"\"Print detailed information about the Module or a view of it.\n\n        Args:\n            param_names: The names of the parameters to show. If `None`, all parameters\n                are shown.\n            indices: Whether to show the indices of the compartments.\n            params: Whether to show the parameters of the compartments.\n            states: Whether to show the states of the compartments.\n            channel_names: The names of the channels to show. If `None`, all channels are\n                shown.\n\n        Returns:\n            A `pd.DataFrame` with the requested information.\n        \"\"\"\n        nodes = self.nodes.copy()  # prevents this from being edited\n\n        cols = []\n        inds = [\"comp_index\", \"branch_index\", \"cell_index\"]\n        scopes = [\"local\", \"global\"]\n        inds = [f\"{s}_{i}\" for i in inds for s in scopes] if indices else []\n        cols += inds\n        cols += [ch._name for ch in self.channels] if channel_names else []\n        cols += (\n            sum([list(ch.channel_params) for ch in self.channels], []) if params else []\n        )\n        cols += (\n            sum([list(ch.channel_states) for ch in self.channels], []) if states else []\n        )\n\n        if not param_names is None:\n            cols = (\n                inds + [c for c in cols if c in param_names]\n                if params\n                else list(param_names)\n            )\n\n        return nodes[cols]\n\n    @only_allow_module\n    def _init_morph(self):\n        \"\"\"Initialize the morphology such that it can be processed by the solvers.\"\"\"\n        self._init_morph_jaxley_spsolve()\n        self._init_morph_jax_spsolve()\n        self.initialized_morph = True\n\n    @abstractmethod\n    def _init_morph_jax_spsolve(self):\n        \"\"\"Initialize the morphology for the JAX sparse solver.\"\"\"\n        raise NotImplementedError\n\n    @abstractmethod\n    def _init_morph_jaxley_spsolve(self):\n        \"\"\"Initialize the morphology for the custom Jaxley solver.\"\"\"\n        raise NotImplementedError\n\n    def _compute_axial_conductances(self, params: Dict[str, jnp.ndarray]):\n        \"\"\"Given radius, length, r_a, compute the axial coupling conductances.\"\"\"\n        return compute_axial_conductances(self._comp_edges, params)\n\n    def set(self, key: str, val: Union[float, jnp.ndarray]):\n        \"\"\"Set parameter of module (or its view) to a new value.\n\n        Note that this function can not be called within `jax.jit` or `jax.grad`.\n        Instead, it should be used set the parameters of the module **before** the\n        simulation. Use `.data_set()` to set parameters during `jax.jit` or\n        `jax.grad`.\n\n        Args:\n            key: The name of the parameter to set.\n            val: The value to set the parameter to. If it is `jnp.ndarray` then it\n                must be of shape `(len(num_compartments))`.\n        \"\"\"\n        if key in self.nodes.columns:\n            not_nan = ~self.nodes[key].isna().to_numpy()\n            self.base.nodes.loc[self._nodes_in_view[not_nan], key] = val\n        elif key in self.edges.columns:\n            not_nan = ~self.edges[key].isna().to_numpy()\n            self.base.edges.loc[self._edges_in_view[not_nan], key] = val\n        else:\n            raise KeyError(f\"Key '{key}' not found in nodes or edges\")\n\n    def data_set(\n        self,\n        key: str,\n        val: Union[float, jnp.ndarray],\n        param_state: Optional[List[Dict]],\n    ):\n        \"\"\"Set parameter of module (or its view) to a new value within `jit`.\n\n        Args:\n            key: The name of the parameter to set.\n            val: The value to set the parameter to. If it is `jnp.ndarray` then it\n                must be of shape `(len(num_compartments))`.\n            param_state: State of the setted parameters, internally used such that this\n                function does not modify global state.\n        \"\"\"\n        # Note: `data_set` does not support arrays for `val`.\n        is_node_param = key in self.nodes.columns\n        data = self.nodes if is_node_param else self.edges\n        viewed_inds = self._nodes_in_view if is_node_param else self._edges_in_view\n        if key in data.columns:\n            not_nan = ~data[key].isna()\n            added_param_state = [\n                {\n                    \"indices\": np.atleast_2d(viewed_inds[not_nan]),\n                    \"key\": key,\n                    \"val\": jnp.atleast_1d(jnp.asarray(val)),\n                }\n            ]\n            if param_state is not None:\n                param_state += added_param_state\n            else:\n                param_state = added_param_state\n        else:\n            raise KeyError(\"Key not recognized.\")\n        return param_state\n\n    def set_ncomp(\n        self,\n        ncomp: int,\n        min_radius: Optional[float] = None,\n    ):\n        \"\"\"Set the number of compartments with which the branch is discretized.\n\n        Args:\n            ncomp: The number of compartments that the branch should be discretized\n                into.\n            min_radius: Only used if the morphology was read from an SWC file. If passed\n                the radius is capped to be at least this value.\n\n        Raises:\n            - When there are stimuli in any compartment in the module.\n            - When there are recordings in any compartment in the module.\n            - When the channels of the compartments are not the same within the branch\n            that is modified.\n            - When the lengths of the compartments are not the same within the branch\n            that is modified.\n            - Unless the morphology was read from an SWC file, when the radiuses of the\n            compartments are not the same within the branch that is modified.\n        \"\"\"\n        assert len(self.base.externals) == 0, \"No stimuli allowed!\"\n        assert len(self.base.recordings) == 0, \"No recordings allowed!\"\n        assert len(self.base.trainable_params) == 0, \"No trainables allowed!\"\n\n        assert self.base._module_type != \"network\", \"This is not allowed for networks.\"\n        assert not (\n            self.base._module_type == \"cell\"\n            and len(self._branches_in_view) == len(self.base._branches_in_view)\n        ), \"This is not allowed for cells.\"\n\n        # Update all attributes that are affected by compartment structure.\n        view = self.nodes.copy()\n        all_nodes = self.base.nodes\n        start_idx = self.nodes[\"global_comp_index\"].to_numpy()[0]\n        nseg_per_branch = self.base.nseg_per_branch\n        channel_names = [c._name for c in self.base.channels]\n        channel_param_names = list(\n            chain(*[c.channel_params for c in self.base.channels])\n        )\n        channel_state_names = list(\n            chain(*[c.channel_states for c in self.base.channels])\n        )\n        radius_generating_fns = self.base._radius_generating_fns\n\n        within_branch_radiuses = view[\"radius\"].to_numpy()\n        compartment_lengths = view[\"length\"].to_numpy()\n        num_previous_ncomp = len(within_branch_radiuses)\n        branch_indices = pd.unique(view[\"global_branch_index\"])\n\n        error_msg = lambda name: (\n            f\"You previously modified the {name} of individual compartments, but \"\n            f\"now you are modifying the number of compartments in this branch. \"\n            f\"This is not allowed. First build the morphology with `set_ncomp()` and \"\n            f\"then modify the radiuses and lengths of compartments.\"\n        )\n\n        if (\n            ~np.all(within_branch_radiuses == within_branch_radiuses[0])\n            and radius_generating_fns is None\n        ):\n            raise ValueError(error_msg(\"radius\"))\n\n        for property_name in [\"length\", \"capacitance\", \"axial_resistivity\"]:\n            compartment_properties = view[property_name].to_numpy()\n            if ~np.all(compartment_properties == compartment_properties[0]):\n                raise ValueError(error_msg(property_name))\n\n        if not (self.nodes[channel_names].var() == 0.0).all():\n            raise ValueError(\n                \"Some channel exists only in some compartments of the branch which you\"\n                \"are trying to modify. This is not allowed. First specify the number\"\n                \"of compartments with `.set_ncomp()` and then insert the channels\"\n                \"accordingly.\"\n            )\n\n        if not (\n            self.nodes[channel_param_names + channel_state_names].var() == 0.0\n        ).all():\n            raise ValueError(\n                \"Some channel has different parameters or states between the \"\n                \"different compartments of the branch which you are trying to modify. \"\n                \"This is not allowed. First specify the number of compartments with \"\n                \"`.set_ncomp()` and then insert the channels accordingly.\"\n            )\n\n        # Add new rows as the average of all rows. Special case for the length is below.\n        average_row = self.nodes.mean(skipna=False)\n        average_row = average_row.to_frame().T\n        view = pd.concat([*[average_row] * ncomp], axis=\"rows\")\n\n        # Set the correct datatype after having performed an average which cast\n        # everything to float.\n        integer_cols = [\"global_cell_index\", \"global_branch_index\", \"global_comp_index\"]\n        view[integer_cols] = view[integer_cols].astype(int)\n\n        # Whether or not a channel exists in a compartment is a boolean.\n        boolean_cols = channel_names\n        view[boolean_cols] = view[boolean_cols].astype(bool)\n\n        # Special treatment for the lengths and radiuses. These are not being set as\n        # the average because we:\n        # 1) Want to maintain the total length of a branch.\n        # 2) Want to use the SWC inferred radius.\n        #\n        # Compute new compartment lengths.\n        comp_lengths = np.sum(compartment_lengths) / ncomp\n        view[\"length\"] = comp_lengths\n\n        # Compute new compartment radiuses.\n        if radius_generating_fns is not None:\n            view[\"radius\"] = build_radiuses_from_xyzr(\n                radius_fns=radius_generating_fns,\n                branch_indices=branch_indices,\n                min_radius=min_radius,\n                nseg=ncomp,\n            )\n        else:\n            view[\"radius\"] = within_branch_radiuses[0] * np.ones(ncomp)\n\n        # Update `.nodes`.\n        # 1) Delete N rows starting from start_idx\n        number_deleted = num_previous_ncomp\n        all_nodes = all_nodes.drop(index=range(start_idx, start_idx + number_deleted))\n\n        # 2) Insert M new rows at the same location\n        df1 = all_nodes.iloc[:start_idx]  # Rows before the insertion point\n        df2 = all_nodes.iloc[start_idx:]  # Rows after the insertion point\n\n        # 3) Combine the parts: before, new rows, and after\n        all_nodes = pd.concat([df1, view, df2]).reset_index(drop=True)\n\n        # Override `comp_index` to just be a consecutive list.\n        all_nodes[\"global_comp_index\"] = np.arange(len(all_nodes))\n\n        # Update compartment structure arguments.\n        nseg_per_branch[branch_indices] = ncomp\n        nseg = int(np.max(nseg_per_branch))\n        cumsum_nseg = cumsum_leading_zero(nseg_per_branch)\n        internal_node_inds = np.arange(cumsum_nseg[-1])\n\n        self.base.nodes = all_nodes\n        self.base.nseg_per_branch = nseg_per_branch\n        self.base.nseg = nseg\n        self.base.cumsum_nseg = cumsum_nseg\n        self.base._internal_node_inds = internal_node_inds\n\n        # Update the morphology indexing (e.g., `.comp_edges`).\n        self.base._initialize()\n        self.base._init_view()\n        self.base._update_local_indices()\n\n    def make_trainable(\n        self,\n        key: str,\n        init_val: Optional[Union[float, list]] = None,\n        verbose: bool = True,\n    ):\n        \"\"\"Make a parameter trainable.\n\n        If a parameter is made trainable, it will be returned by `get_parameters()`\n        and should then be passed to `jx.integrate(..., params=params)`.\n\n        Args:\n            key: Name of the parameter to make trainable.\n            init_val: Initial value of the parameter. If `float`, the same value is\n                used for every created parameter. If `list`, the length of the list has\n                to match the number of created parameters. If `None`, the current\n                parameter value is used and if parameter sharing is performed that the\n                current parameter value is averaged over all shared parameters.\n            verbose: Whether to print the number of parameters that are added and the\n                total number of parameters.\n        \"\"\"\n        assert (\n            self.allow_make_trainable\n        ), \"network.cell('all').make_trainable() is not supported. Use a for-loop over cells.\"\n        nsegs_per_branch = (\n            self.base.nodes[\"global_branch_index\"].value_counts().to_numpy()\n        )\n        assert np.all(\n            nsegs_per_branch == nsegs_per_branch[0]\n        ), \"Parameter sharing is not allowed for modules containing branches with different numbers of compartments.\"\n\n        data = self.nodes if key in self.nodes.columns else None\n        data = self.edges if key in self.edges.columns else data\n\n        assert data is not None, f\"Key '{key}' not found in nodes or edges\"\n        not_nan = ~data[key].isna()\n        data = data.loc[not_nan]\n        assert (\n            len(data) > 0\n        ), \"No settable parameters found in the selected compartments.\"\n\n        grouped_view = data.groupby(\"controlled_by_param\")\n        # Because of this `x.index.values` we cannot support `make_trainable()` on\n        # the module level for synapse parameters (but only for `SynapseView`).\n        inds_of_comps = list(\n            grouped_view.apply(lambda x: x.index.values, include_groups=False)\n        )\n        indices_per_param = jnp.stack(inds_of_comps)\n        # Sorted inds are only used to infer the correct starting values.\n        param_vals = jnp.asarray(\n            [data.loc[inds, key].to_numpy() for inds in inds_of_comps]\n        )\n\n        # Set the value which the trainable parameter should take.\n        num_created_parameters = len(indices_per_param)\n        if init_val is not None:\n            if isinstance(init_val, float):\n                new_params = jnp.asarray([init_val] * num_created_parameters)\n            elif isinstance(init_val, list):\n                assert (\n                    len(init_val) == num_created_parameters\n                ), f\"len(init_val)={len(init_val)}, but trying to create {num_created_parameters} parameters.\"\n                new_params = jnp.asarray(init_val)\n            else:\n                raise ValueError(\n                    f\"init_val must a float, list, or None, but it is a {type(init_val).__name__}.\"\n                )\n        else:\n            new_params = jnp.mean(param_vals, axis=1)\n        self.base.trainable_params.append({key: new_params})\n        self.base.indices_set_by_trainables.append(indices_per_param)\n        self.base.num_trainable_params += num_created_parameters\n        if verbose:\n            print(\n                f\"Number of newly added trainable parameters: {num_created_parameters}. Total number of trainable parameters: {self.base.num_trainable_params}\"\n            )\n\n    def write_trainables(self, trainable_params: List[Dict[str, jnp.ndarray]]):\n        \"\"\"Write the trainables into `.nodes` and `.edges`.\n\n        This allows to, e.g., visualize trained networks with `.vis()`.\n\n        Args:\n            trainable_params: The trainable parameters returned by `get_parameters()`.\n        \"\"\"\n        # We do not support views. Why? `jaxedges` does not have any NaN\n        # elements, whereas edges does. Because of this, we already need special\n        # treatment to make this function work, and it would be an even bigger hassle\n        # if we wanted to support this.\n        assert self.__class__.__name__ in [\n            \"Compartment\",\n            \"Branch\",\n            \"Cell\",\n            \"Network\",\n        ], \"Only supports modules.\"\n\n        # We could also implement this without casting the module to jax.\n        # However, I think it allows us to reuse as much code as possible and it avoids\n        # any kind of issues with indexing or parameter sharing (as this is fully\n        # taken care of by `get_all_parameters()`).\n        self.base.to_jax()\n        pstate = params_to_pstate(trainable_params, self.base.indices_set_by_trainables)\n        all_params = self.base.get_all_parameters(pstate, voltage_solver=\"jaxley.stone\")\n\n        # The value for `delta_t` does not matter here because it is only used to\n        # compute the initial current. However, the initial current cannot be made\n        # trainable and so its value never gets used below.\n        all_states = self.base.get_all_states(pstate, all_params, delta_t=0.025)\n\n        # Loop only over the keys in `pstate` to avoid unnecessary computation.\n        for parameter in pstate:\n            key = parameter[\"key\"]\n            if key in self.base.nodes.columns:\n                vals_to_set = all_params if key in all_params.keys() else all_states\n                self.base.nodes[key] = vals_to_set[key]\n\n        # `jaxedges` contains only non-Nan elements. This is unlike the channels where\n        # we allow parameter sharing.\n        edges = self.base.edges.to_dict(orient=\"list\")\n        for i, synapse in enumerate(self.base.synapses):\n            condition = np.asarray(edges[\"type_ind\"]) == i\n            for key in list(synapse.synapse_params.keys()):\n                self.base.edges.loc[condition, key] = all_params[key]\n            for key in list(synapse.synapse_states.keys()):\n                self.base.edges.loc[condition, key] = all_states[key]\n\n    def distance(self, endpoint: \"View\") -> float:\n        \"\"\"Return the direct distance between two compartments.\n        This does not compute the pathwise distance (which is currently not\n        implemented).\n        Args:\n            endpoint: The compartment to which to compute the distance to.\n        \"\"\"\n        assert len(self.xyzr) == 1 and len(endpoint.xyzr) == 1\n        start_xyz = np.mean(self.xyzr[0][:, :3], axis=0)\n        end_xyz = np.mean(endpoint.xyzr[0][:, :3], axis=0)\n        return np.sqrt(np.sum((start_xyz - end_xyz) ** 2))\n\n    def delete_trainables(self):\n        \"\"\"Removes all trainable parameters from the module.\"\"\"\n\n        if isinstance(self, View):\n            trainables_and_inds = self._filter_trainables(is_viewed=False)\n            self.base.indices_set_by_trainables = trainables_and_inds[0]\n            self.base.trainable_params = trainables_and_inds[1]\n            self.base.num_trainable_params -= self.num_trainable_params\n        else:\n            self.base.indices_set_by_trainables = []\n            self.base.trainable_params = []\n            self.base.num_trainable_params = 0\n        self._update_view()\n\n    def add_to_group(self, group_name: str):\n        \"\"\"Add a view of the module to a group.\n\n        Groups can then be indexed. For example:\n\n        .. code-block:: python\n\n            net.cell(0).add_to_group(\"excitatory\")\n            net.excitatory.set(\"radius\", 0.1)\n\n        Args:\n            group_name: The name of the group.\n        \"\"\"\n        if group_name not in self.base.groups:\n            self.base.groups[group_name] = self._nodes_in_view\n        else:\n            self.base.groups[group_name] = np.unique(\n                np.concatenate([self.base.groups[group_name], self._nodes_in_view])\n            )\n\n    def _get_state_names(self) -> Tuple[List, List]:\n        \"\"\"Collect all recordable / clampable states in the membrane and synapses.\n\n        Returns states seperated by comps and edges.\"\"\"\n        channel_states = [name for c in self.channels for name in c.channel_states]\n        synapse_states = [name for s in self.synapses for name in s.synapse_states]\n        membrane_states = [\"v\", \"i\"] + self.membrane_current_names\n        return channel_states + membrane_states, synapse_states\n\n    def get_parameters(self) -> List[Dict[str, jnp.ndarray]]:\n        \"\"\"Get all trainable parameters.\n\n        The returned parameters should be passed to `jx.integrate(..., params=params).\n\n        Returns:\n            A list of all trainable parameters in the form of\n                [{\"gNa\": jnp.array([0.1, 0.2, 0.3])}, ...].\n        \"\"\"\n        return self.trainable_params\n\n    @only_allow_module\n    def get_all_parameters(\n        self, pstate: List[Dict], voltage_solver: str\n    ) -> Dict[str, jnp.ndarray]:\n        # TODO FROM #447: MAKE THIS WORK FOR VIEW?\n        \"\"\"Return all parameters (and coupling conductances) needed to simulate.\n\n        Runs `_compute_axial_conductances()` and return every parameter that is needed\n        to solve the ODE. This includes conductances, radiuses, lengths,\n        axial_resistivities, but also coupling conductances.\n\n        This is done by first obtaining the current value of every parameter (not only\n        the trainable ones) and then replacing the trainable ones with the value\n        in `trainable_params()`. This function is run within `jx.integrate()`.\n\n        pstate can be obtained by calling `params_to_pstate()`.\n\n        .. code-block:: python\n\n            params = module.get_parameters() # i.e. [0, 1, 2]\n            pstate = params_to_pstate(params, module.indices_set_by_trainables)\n            module.to_jax() # needed for call to module.jaxnodes\n\n        Args:\n            pstate: The state of the trainable parameters. pstate takes the form\n                [{\n                    \"key\": \"gNa\", \"indices\": jnp.array([0, 1, 2]),\n                    \"val\": jnp.array([0.1, 0.2, 0.3])\n                }, ...].\n            voltage_solver: The voltage solver that is used. Since `jax.sparse` and\n                `jaxley.xyz` require different formats of the axial conductances, this\n                function will default to different building methods.\n\n        Returns:\n            A dictionary of all module parameters.\n        \"\"\"\n        params = {}\n        for key in [\"radius\", \"length\", \"axial_resistivity\", \"capacitance\"]:\n            params[key] = self.base.jaxnodes[key]\n\n        for channel in self.base.channels:\n            for channel_params in channel.channel_params:\n                params[channel_params] = self.base.jaxnodes[channel_params]\n\n        for synapse_params in self.base.synapse_param_names:\n            params[synapse_params] = self.base.jaxedges[synapse_params]\n\n        # Override with those parameters set by `.make_trainable()`.\n        for parameter in pstate:\n            key = parameter[\"key\"]\n            inds = parameter[\"indices\"]\n            set_param = parameter[\"val\"]\n\n            # This is needed since SynapseViews worked differently before.\n            # This mimics the old behaviour and tranformes the new indices\n            # to the old indices.\n            # TODO FROM #447: Longterm this should be gotten rid of.\n            # Instead edges should work similar to nodes (would also allow for\n            # param sharing).\n            synapse_inds = self.base.edges.groupby(\"type\").rank()[\"global_edge_index\"]\n            synapse_inds = (synapse_inds.astype(int) - 1).to_numpy()\n            if key in self.base.synapse_param_names:\n                inds = synapse_inds[inds]\n\n            if key in params:  # Only parameters, not initial states.\n                # `inds` is of shape `(num_params, num_comps_per_param)`.\n                # `set_param` is of shape `(num_params,)`\n                # We need to unsqueeze `set_param` to make it `(num_params, 1)` for the\n                # `.set()` to work. This is done with `[:, None]`.\n                params[key] = params[key].at[inds].set(set_param[:, None])\n\n        # Compute conductance params and add them to the params dictionary.\n        params[\"axial_conductances\"] = self.base._compute_axial_conductances(\n            params=params\n        )\n        return params\n\n    @only_allow_module\n    def _get_states_from_nodes_and_edges(self) -> Dict[str, jnp.ndarray]:\n        # TODO FROM #447: MAKE THIS WORK FOR VIEW?\n        \"\"\"Return states as they are set in the `.nodes` and `.edges` tables.\"\"\"\n        self.base.to_jax()  # Create `.jaxnodes` from `.nodes` and `.jaxedges` from `.edges`.\n        states = {\"v\": self.base.jaxnodes[\"v\"]}\n        # Join node and edge states into a single state dictionary.\n        for channel in self.base.channels:\n            for channel_states in channel.channel_states:\n                states[channel_states] = self.base.jaxnodes[channel_states]\n        for synapse_states in self.base.synapse_state_names:\n            states[synapse_states] = self.base.jaxedges[synapse_states]\n        return states\n\n    @only_allow_module\n    def get_all_states(\n        self, pstate: List[Dict], all_params, delta_t: float\n    ) -> Dict[str, jnp.ndarray]:\n        # TODO FROM #447: MAKE THIS WORK FOR VIEW?\n        \"\"\"Get the full initial state of the module from jaxnodes and trainables.\n\n        Args:\n            pstate: The state of the trainable parameters.\n            all_params: All parameters of the module.\n            delta_t: The time step.\n\n        Returns:\n            A dictionary of all states of the module.\n        \"\"\"\n        states = self.base._get_states_from_nodes_and_edges()\n\n        # Override with the initial states set by `.make_trainable()`.\n        for parameter in pstate:\n            key = parameter[\"key\"]\n            inds = parameter[\"indices\"]\n            set_param = parameter[\"val\"]\n            if key in states:  # Only initial states, not parameters.\n                # `inds` is of shape `(num_params, num_comps_per_param)`.\n                # `set_param` is of shape `(num_params,)`\n                # We need to unsqueeze `set_param` to make it `(num_params, 1)` for the\n                # `.set()` to work. This is done with `[:, None]`.\n                states[key] = states[key].at[inds].set(set_param[:, None])\n\n        # Add to the states the initial current through every channel.\n        states, _ = self.base._channel_currents(\n            states, delta_t, self.channels, self.nodes, all_params\n        )\n\n        # Add to the states the initial current through every synapse.\n        states, _ = self.base._synapse_currents(\n            states, self.synapses, all_params, delta_t, self.edges\n        )\n        return states\n\n    @property\n    def initialized(self) -> bool:\n        \"\"\"Whether the `Module` is ready to be solved or not.\"\"\"\n        return self.initialized_morph\n\n    def _initialize(self):\n        \"\"\"Initialize the module.\"\"\"\n        self._init_morph()\n        return self\n\n    @only_allow_module\n    def init_states(self, delta_t: float = 0.025):\n        # TODO FROM #447: MAKE THIS WORK FOR VIEW?\n        \"\"\"Initialize all mechanisms in their steady state.\n\n        This considers the voltages and parameters of each compartment.\n\n        Args:\n            delta_t: Passed on to `channel.init_state()`.\n        \"\"\"\n        # Update states of the channels.\n        channel_nodes = self.base.nodes\n        states = self.base._get_states_from_nodes_and_edges()\n\n        # We do not use any `pstate` for initializing. In principle, we could change\n        # that by allowing an input `params` and `pstate` to this function.\n        # `voltage_solver` could also be `jax.sparse` here, because both of them\n        # build the channel parameters in the same way.\n        params = self.base.get_all_parameters([], voltage_solver=\"jaxley.thomas\")\n\n        for channel in self.base.channels:\n            name = channel._name\n            channel_indices = channel_nodes.loc[channel_nodes[name]][\n                \"global_comp_index\"\n            ].to_numpy()\n            voltages = channel_nodes.loc[channel_indices, \"v\"].to_numpy()\n\n            channel_param_names = list(channel.channel_params.keys())\n            channel_state_names = list(channel.channel_states.keys())\n            channel_states = query_channel_states_and_params(\n                states, channel_state_names, channel_indices\n            )\n            channel_params = query_channel_states_and_params(\n                params, channel_param_names, channel_indices\n            )\n\n            init_state = channel.init_state(\n                channel_states, voltages, channel_params, delta_t\n            )\n\n            # `init_state` might not return all channel states. Only the ones that are\n            # returned are updated here.\n            for key, val in init_state.items():\n                # Note that we are overriding `self.nodes` here, but `self.nodes` is\n                # not used above to actually compute the current states (so there are\n                # no issues with overriding states).\n                self.nodes.loc[channel_indices, key] = val\n\n    def _init_morph_for_debugging(self):\n        \"\"\"Instandiates row and column inds which can be used to solve the voltage eqs.\n\n        This is important only for expert users who try to modify the solver for the\n        voltage equations. By default, this function is never run.\n\n        This is useful for debugging the solver because one can use\n        `scipy.linalg.sparse.spsolve` after every step of the solve.\n\n        Here is the code snippet that can be used for debugging then (to be inserted in\n        `solver_voltage`):\n        ```python\n        from scipy.sparse import csc_matrix\n        from scipy.sparse.linalg import spsolve\n        from jaxley.utils.debug_solver import build_voltage_matrix_elements\n\n        elements, solve, num_entries, start_ind_for_branchpoints = (\n            build_voltage_matrix_elements(\n                uppers,\n                lowers,\n                diags,\n                solves,\n                branchpoint_conds_children[debug_states[\"child_inds\"]],\n                branchpoint_conds_parents[debug_states[\"par_inds\"]],\n                branchpoint_weights_children[debug_states[\"child_inds\"]],\n                branchpoint_weights_parents[debug_states[\"par_inds\"]],\n                branchpoint_diags,\n                branchpoint_solves,\n                debug_states[\"nseg\"],\n                nbranches,\n            )\n        )\n        sparse_matrix = csc_matrix(\n            (elements, (debug_states[\"row_inds\"], debug_states[\"col_inds\"])),\n            shape=(num_entries, num_entries),\n        )\n        solution = spsolve(sparse_matrix, solve)\n        solution = solution[:start_ind_for_branchpoints]  # Delete branchpoint voltages.\n        solves = jnp.reshape(solution, (debug_states[\"nseg\"], nbranches))\n        return solves\n        ```\n        \"\"\"\n        # For scipy and jax.scipy.\n        row_and_col_inds = compute_morphology_indices(\n            len(self.base._par_inds),\n            self.base._child_belongs_to_branchpoint,\n            self.base._par_inds,\n            self.base._child_inds,\n            self.base.nseg,\n            self.base.total_nbranches,\n        )\n\n        num_elements = len(row_and_col_inds[\"row_inds\"])\n        data_inds, indices, indptr = convert_to_csc(\n            num_elements=num_elements,\n            row_ind=row_and_col_inds[\"row_inds\"],\n            col_ind=row_and_col_inds[\"col_inds\"],\n        )\n        self.base.debug_states[\"row_inds\"] = row_and_col_inds[\"row_inds\"]\n        self.base.debug_states[\"col_inds\"] = row_and_col_inds[\"col_inds\"]\n        self.base.debug_states[\"data_inds\"] = data_inds\n        self.base.debug_states[\"indices\"] = indices\n        self.base.debug_states[\"indptr\"] = indptr\n\n        self.base.debug_states[\"nseg\"] = self.base.nseg\n        self.base.debug_states[\"child_inds\"] = self.base._child_inds\n        self.base.debug_states[\"par_inds\"] = self.base._par_inds\n\n    def record(self, state: str = \"v\", verbose=True):\n        comp_states, edge_states = self._get_state_names()\n        if state not in comp_states + edge_states:\n            raise KeyError(f\"{state} is not a recognized state in this module.\")\n        in_view = self._nodes_in_view if state in comp_states else self._edges_in_view\n\n        new_recs = pd.DataFrame(in_view, columns=[\"rec_index\"])\n        new_recs[\"state\"] = state\n        self.base.recordings = pd.concat([self.base.recordings, new_recs])\n        has_duplicates = self.base.recordings.duplicated()\n        self.base.recordings = self.base.recordings.loc[~has_duplicates]\n        if verbose:\n            print(\n                f\"Added {len(in_view)-sum(has_duplicates)} recordings. See `.recordings` for details.\"\n            )\n\n    def _update_view(self):\n        \"\"\"Update the attrs of the view after changes in the base module.\"\"\"\n        if isinstance(self, View):\n            scope = self._scope\n            current_view = self._current_view\n            # copy dict of new View. For some reason doing self = View(self)\n            # did not work.\n            self.__dict__ = View(\n                self.base, self._nodes_in_view, self._edges_in_view\n            ).__dict__\n\n            # retain the scope and current_view of the previous view\n            self._scope = scope\n            self._current_view = current_view\n\n    def delete_recordings(self):\n        \"\"\"Removes all recordings from the module.\"\"\"\n        if isinstance(self, View):\n            base_recs = self.base.recordings\n            self.base.recordings = base_recs[\n                ~base_recs.isin(self.recordings).all(axis=1)\n            ]\n            self._update_view()\n        else:\n            self.base.recordings = pd.DataFrame().from_dict({})\n\n    def stimulate(self, current: Optional[jnp.ndarray] = None, verbose: bool = True):\n        \"\"\"Insert a stimulus into the compartment.\n\n        current must be a 1d array or have batch dimension of size `(num_compartments, )`\n        or `(1, )`. If 1d, the same stimulus is added to all compartments.\n\n        This function cannot be run during `jax.jit` and `jax.grad`. Because of this,\n        it should only be used for static stimuli (i.e., stimuli that do not depend\n        on the data and that should not be learned). For stimuli that depend on data\n        (or that should be learned), please use `data_stimulate()`.\n\n        Args:\n            current: Current in `nA`.\n        \"\"\"\n        self._external_input(\"i\", current, verbose=verbose)\n\n    def clamp(self, state_name: str, state_array: jnp.ndarray, verbose: bool = True):\n        \"\"\"Clamp a state to a given value across specified compartments.\n\n        Args:\n            state_name: The name of the state to clamp.\n            state_array (jnp.nd: Array of values to clamp the state to.\n            verbose : If True, prints details about the clamping.\n\n        This function sets external states for the compartments.\n        \"\"\"\n        self._external_input(state_name, state_array, verbose=verbose)\n\n    def _external_input(\n        self,\n        key: str,\n        values: Optional[jnp.ndarray],\n        verbose: bool = True,\n    ):\n        comp_states, edge_states = self._get_state_names()\n        if key not in comp_states + edge_states:\n            raise KeyError(f\"{key} is not a recognized state in this module.\")\n        values = values if values.ndim == 2 else jnp.expand_dims(values, axis=0)\n        batch_size = values.shape[0]\n        num_inserted = (\n            len(self._nodes_in_view) if key in comp_states else len(self._edges_in_view)\n        )\n        is_multiple = num_inserted == batch_size\n        values = values if is_multiple else jnp.repeat(values, num_inserted, axis=0)\n        assert batch_size in [\n            1,\n            num_inserted,\n        ], \"Number of comps and stimuli do not match.\"\n\n        if key in self.base.externals.keys():\n            self.base.externals[key] = jnp.concatenate(\n                [self.base.externals[key], values]\n            )\n            self.base.external_inds[key] = jnp.concatenate(\n                [self.base.external_inds[key], self._nodes_in_view]\n            )\n        else:\n            if key in comp_states:\n                self.base.externals[key] = values\n                self.base.external_inds[key] = self._nodes_in_view\n            else:\n                self.base.externals[key] = values\n                self.base.external_inds[key] = self._edges_in_view\n        if verbose:\n            print(\n                f\"Added {num_inserted} external_states. See `.externals` for details.\"\n            )\n\n    def data_stimulate(\n        self,\n        current: jnp.ndarray,\n        data_stimuli: Optional[Tuple[jnp.ndarray, pd.DataFrame]] = None,\n        verbose: bool = False,\n    ) -> Tuple[jnp.ndarray, pd.DataFrame]:\n        \"\"\"Insert a stimulus into the module within jit (or grad).\n\n        Args:\n            current: Current in `nA`.\n            verbose: Whether or not to print the number of inserted stimuli. `False`\n                by default because this method is meant to be jitted.\n        \"\"\"\n        return self._data_external_input(\n            \"i\", current, data_stimuli, self.nodes, verbose=verbose\n        )\n\n    def data_clamp(\n        self,\n        state_name: str,\n        state_array: jnp.ndarray,\n        data_clamps: Optional[Tuple[jnp.ndarray, pd.DataFrame]] = None,\n        verbose: bool = False,\n    ):\n        \"\"\"Insert a clamp into the module within jit (or grad).\n\n        Args:\n            state_name: Name of the state variable to set.\n            state_array: Time series of the state variable in the default Jaxley unit.\n                State array should be of shape (num_clamps, simulation_time) or\n                (simulation_time, ) for a single clamp.\n            verbose: Whether or not to print the number of inserted clamps. `False`\n                by default because this method is meant to be jitted.\n        \"\"\"\n        comp_states, edge_states = self._get_state_names()\n        if state_name not in comp_states + edge_states:\n            raise KeyError(f\"{state_name} is not a recognized state in this module.\")\n        data = self.nodes if state_name in comp_states else self.edges\n        return self._data_external_input(\n            state_name, state_array, data_clamps, data, verbose=verbose\n        )\n\n    def _data_external_input(\n        self,\n        state_name: str,\n        state_array: jnp.ndarray,\n        data_external_input: Optional[Tuple[jnp.ndarray, pd.DataFrame]],\n        view: pd.DataFrame,\n        verbose: bool = False,\n    ):\n        comp_states, edge_states = self._get_state_names()\n        state_array = (\n            state_array\n            if state_array.ndim == 2\n            else jnp.expand_dims(state_array, axis=0)\n        )\n        batch_size = state_array.shape[0]\n        num_inserted = (\n            len(self._nodes_in_view)\n            if state_name in comp_states\n            else len(self._edges_in_view)\n        )\n        is_multiple = num_inserted == batch_size\n        state_array = (\n            state_array\n            if is_multiple\n            else jnp.repeat(state_array, num_inserted, axis=0)\n        )\n        assert batch_size in [\n            1,\n            num_inserted,\n        ], \"Number of comps and clamps do not match.\"\n\n        if data_external_input is not None:\n            external_input = data_external_input[1]\n            external_input = jnp.concatenate([external_input, state_array])\n            inds = data_external_input[2]\n        else:\n            external_input = state_array\n            inds = pd.DataFrame().from_dict({})\n\n        inds = pd.concat([inds, view])\n\n        if verbose:\n            if state_name == \"i\":\n                print(f\"Added {len(view)} stimuli.\")\n            else:\n                print(f\"Added {len(view)} clamps.\")\n\n        return (state_name, external_input, inds)\n\n    def delete_stimuli(self):\n        \"\"\"Removes all stimuli from the module.\"\"\"\n        self.delete_clamps(\"i\")\n\n    def delete_clamps(self, state_name: Optional[str] = None):\n        \"\"\"Removes all clamps of the given state from the module.\"\"\"\n        all_externals = list(self.externals.keys())\n        if \"i\" in all_externals:\n            all_externals.remove(\"i\")\n        state_names = all_externals if state_name is None else [state_name]\n        for state_name in state_names:\n            if state_name in self.externals:\n                keep_inds = ~np.isin(\n                    self.base.external_inds[state_name], self._nodes_in_view\n                )\n                base_exts = self.base.externals\n                base_exts_inds = self.base.external_inds\n                if np.all(~keep_inds):\n                    base_exts.pop(state_name, None)\n                    base_exts_inds.pop(state_name, None)\n                else:\n                    base_exts[state_name] = base_exts[state_name][keep_inds]\n                    base_exts_inds[state_name] = base_exts_inds[state_name][keep_inds]\n                self._update_view()\n            else:\n                pass  # does not have to be deleted if not in externals\n\n    def insert(self, channel: Channel):\n        \"\"\"Insert a channel into the module.\n\n        Args:\n            channel: The channel to insert.\"\"\"\n        name = channel._name\n\n        # Channel does not yet exist in the `jx.Module` at all.\n        if name not in [c._name for c in self.base.channels]:\n            self.base.channels.append(channel)\n            self.base.nodes[name] = (\n                False  # Previous columns do not have the new channel.\n            )\n\n        if channel.current_name not in self.base.membrane_current_names:\n            self.base.membrane_current_names.append(channel.current_name)\n\n        # Add a binary column that indicates if a channel is present.\n        self.base.nodes.loc[self._nodes_in_view, name] = True\n\n        # Loop over all new parameters, e.g. gNa, eNa.\n        for key in channel.channel_params:\n            self.base.nodes.loc[self._nodes_in_view, key] = channel.channel_params[key]\n\n        # Loop over all new parameters, e.g. gNa, eNa.\n        for key in channel.channel_states:\n            self.base.nodes.loc[self._nodes_in_view, key] = channel.channel_states[key]\n\n    def delete_channel(self, channel: Channel):\n        \"\"\"Remove a channel from the module.\n\n        Args:\n            channel: The channel to remove.\"\"\"\n        name = channel._name\n        channel_names = [c._name for c in self.channels]\n        all_channel_names = [c._name for c in self.base.channels]\n        if name in channel_names:\n            channel_cols = list(channel.channel_params.keys())\n            channel_cols += list(channel.channel_states.keys())\n            self.base.nodes.loc[self._nodes_in_view, channel_cols] = float(\"nan\")\n            self.base.nodes.loc[self._nodes_in_view, name] = False\n\n            # only delete cols if no other comps in the module have the same channel\n            if np.all(~self.base.nodes[name]):\n                self.base.channels.pop(all_channel_names.index(name))\n                self.base.membrane_current_names.remove(channel.current_name)\n                self.base.nodes.drop(columns=channel_cols + [name], inplace=True)\n        else:\n            raise ValueError(f\"Channel {name} not found in the module.\")\n\n    @only_allow_module\n    def step(\n        self,\n        u: Dict[str, jnp.ndarray],\n        delta_t: float,\n        external_inds: Dict[str, jnp.ndarray],\n        externals: Dict[str, jnp.ndarray],\n        params: Dict[str, jnp.ndarray],\n        solver: str = \"bwd_euler\",\n        voltage_solver: str = \"jaxley.stone\",\n    ) -> Dict[str, jnp.ndarray]:\n        \"\"\"One step of solving the Ordinary Differential Equation.\n\n        This function is called inside of `integrate` and increments the state of the\n        module by one time step. Calls `_step_channels` and `_step_synapse` to update\n        the states of the channels and synapses using fwd_euler.\n\n        Args:\n            u: The state of the module. voltages = u[\"v\"]\n            delta_t: The time step.\n            external_inds: The indices of the external inputs.\n            externals: The external inputs.\n            params: The parameters of the module.\n            solver: The solver to use for the voltages. Either of [\"bwd_euler\",\n                \"fwd_euler\", \"crank_nicolson\"].\n            voltage_solver: The tridiagonal solver used to diagonalize the\n                coefficient matrix of the ODE system. Either of [\"jaxley.thomas\",\n                \"jaxley.stone\"].\n\n        Returns:\n            The updated state of the module.\n        \"\"\"\n\n        # Extract the voltages\n        voltages = u[\"v\"]\n\n        # Extract the external inputs\n        if \"i\" in externals.keys():\n            i_current = externals[\"i\"]\n            i_inds = external_inds[\"i\"]\n            i_ext = self._get_external_input(\n                voltages, i_inds, i_current, params[\"radius\"], params[\"length\"]\n            )\n        else:\n            i_ext = 0.0\n\n        # Step of the channels.\n        u, (v_terms, const_terms) = self._step_channels(\n            u, delta_t, self.channels, self.nodes, params\n        )\n\n        # Step of the synapse.\n        u, (syn_v_terms, syn_const_terms) = self._step_synapse(\n            u,\n            self.synapses,\n            params,\n            delta_t,\n            self.edges,\n        )\n\n        # Clamp for channels and synapses.\n        for key in externals.keys():\n            if key not in [\"i\", \"v\"]:\n                u[key] = u[key].at[external_inds[key]].set(externals[key])\n\n        # Voltage steps.\n        cm = params[\"capacitance\"]  # Abbreviation.\n\n        # Arguments used by all solvers.\n        solver_kwargs = {\n            \"voltages\": voltages,\n            \"voltage_terms\": (v_terms + syn_v_terms) / cm,\n            \"constant_terms\": (const_terms + i_ext + syn_const_terms) / cm,\n            \"axial_conductances\": params[\"axial_conductances\"],\n            \"internal_node_inds\": self._internal_node_inds,\n        }\n\n        # Add solver specific arguments.\n        if voltage_solver == \"jax.sparse\":\n            solver_kwargs.update(\n                {\n                    \"sinks\": np.asarray(self._comp_edges[\"sink\"].to_list()),\n                    \"data_inds\": self._data_inds,\n                    \"indices\": self._indices_jax_spsolve,\n                    \"indptr\": self._indptr_jax_spsolve,\n                    \"n_nodes\": self._n_nodes,\n                }\n            )\n            # Only for `bwd_euler` and `cranck-nicolson`.\n            step_voltage_implicit = step_voltage_implicit_with_jax_spsolve\n        else:\n            # Our custom sparse solver requires a different format of all conductance\n            # values to perform triangulation and backsubstution optimally.\n            #\n            # Currently, the forward Euler solver also uses this format. However,\n            # this is only for historical reasons and we are planning to change this in\n            # the future.\n            solver_kwargs.update(\n                {\n                    \"sinks\": np.asarray(self._comp_edges[\"sink\"].to_list()),\n                    \"sources\": np.asarray(self._comp_edges[\"source\"].to_list()),\n                    \"types\": np.asarray(self._comp_edges[\"type\"].to_list()),\n                    \"nseg_per_branch\": self.nseg_per_branch,\n                    \"par_inds\": self._par_inds,\n                    \"child_inds\": self._child_inds,\n                    \"nbranches\": self.total_nbranches,\n                    \"solver\": voltage_solver,\n                    \"idx\": self._solve_indexer,\n                    \"debug_states\": self.debug_states,\n                }\n            )\n            # Only for `bwd_euler` and `cranck-nicolson`.\n            step_voltage_implicit = step_voltage_implicit_with_jaxley_spsolve\n\n        if solver == \"bwd_euler\":\n            u[\"v\"] = step_voltage_implicit(**solver_kwargs, delta_t=delta_t)\n        elif solver == \"crank_nicolson\":\n            # Crank-Nicolson advances by half a step of backward and half a step of\n            # forward Euler.\n            half_step_delta_t = delta_t / 2\n            half_step_voltages = step_voltage_implicit(\n                **solver_kwargs, delta_t=half_step_delta_t\n            )\n            # The forward Euler step in Crank-Nicolson can be performed easily as\n            # `V_{n+1} = 2 * V_{n+1/2} - V_n`. See also NEURON book Chapter 4.\n            u[\"v\"] = 2 * half_step_voltages - voltages\n        elif solver == \"fwd_euler\":\n            u[\"v\"] = step_voltage_explicit(**solver_kwargs, delta_t=delta_t)\n        else:\n            raise ValueError(\n                f\"You specified `solver={solver}`. The only allowed solvers are \"\n                \"['bwd_euler', 'fwd_euler', 'crank_nicolson'].\"\n            )\n\n        # Clamp for voltages.\n        if \"v\" in externals.keys():\n            u[\"v\"] = u[\"v\"].at[external_inds[\"v\"]].set(externals[\"v\"])\n\n        return u\n\n    def _step_channels(\n        self,\n        states: Dict[str, jnp.ndarray],\n        delta_t: float,\n        channels: List[Channel],\n        channel_nodes: pd.DataFrame,\n        params: Dict[str, jnp.ndarray],\n    ) -> Tuple[Dict[str, jnp.ndarray], Tuple[jnp.ndarray, jnp.ndarray]]:\n        \"\"\"One step of integration of the channels and of computing their current.\"\"\"\n        states = self._step_channels_state(\n            states, delta_t, channels, channel_nodes, params\n        )\n        states, current_terms = self._channel_currents(\n            states, delta_t, channels, channel_nodes, params\n        )\n        return states, current_terms\n\n    def _step_channels_state(\n        self,\n        states,\n        delta_t,\n        channels: List[Channel],\n        channel_nodes: pd.DataFrame,\n        params: Dict[str, jnp.ndarray],\n    ) -> Dict[str, jnp.ndarray]:\n        \"\"\"One integration step of the channels.\"\"\"\n        voltages = states[\"v\"]\n\n        # Update states of the channels.\n        indices = channel_nodes[\"global_comp_index\"].to_numpy()\n        for channel in channels:\n            channel_param_names = list(channel.channel_params)\n            channel_param_names += [\n                \"radius\",\n                \"length\",\n                \"axial_resistivity\",\n                \"capacitance\",\n            ]\n            channel_state_names = list(channel.channel_states)\n            channel_state_names += self.membrane_current_names\n            channel_indices = indices[channel_nodes[channel._name].astype(bool)]\n\n            channel_params = query_channel_states_and_params(\n                params, channel_param_names, channel_indices\n            )\n            channel_states = query_channel_states_and_params(\n                states, channel_state_names, channel_indices\n            )\n\n            states_updated = channel.update_states(\n                channel_states, delta_t, voltages[channel_indices], channel_params\n            )\n            # Rebuild state. This has to be done within the loop over channels to allow\n            # multiple channels which modify the same state.\n            for key, val in states_updated.items():\n                states[key] = states[key].at[channel_indices].set(val)\n\n        return states\n\n    def _channel_currents(\n        self,\n        states: Dict[str, jnp.ndarray],\n        delta_t: float,\n        channels: List[Channel],\n        channel_nodes: pd.DataFrame,\n        params: Dict[str, jnp.ndarray],\n    ) -> Tuple[Dict[str, jnp.ndarray], Tuple[jnp.ndarray, jnp.ndarray]]:\n        \"\"\"Return the current through each channel.\n\n        This is also updates `state` because the `state` also contains the current.\n        \"\"\"\n        voltages = states[\"v\"]\n\n        # Compute current through channels.\n        voltage_terms = jnp.zeros_like(voltages)\n        constant_terms = jnp.zeros_like(voltages)\n        # Run with two different voltages that are `diff` apart to infer the slope and\n        # offset.\n        diff = 1e-3\n\n        current_states = {}\n        for name in self.membrane_current_names:\n            current_states[name] = jnp.zeros_like(voltages)\n\n        for channel in channels:\n            name = channel._name\n            channel_param_names = list(channel.channel_params.keys())\n            channel_state_names = list(channel.channel_states.keys())\n            indices = channel_nodes.loc[channel_nodes[name]][\n                \"global_comp_index\"\n            ].to_numpy()\n\n            channel_params = {}\n            for p in channel_param_names:\n                channel_params[p] = params[p][indices]\n            channel_params[\"radius\"] = params[\"radius\"][indices]\n            channel_params[\"length\"] = params[\"length\"][indices]\n            channel_params[\"axial_resistivity\"] = params[\"axial_resistivity\"][indices]\n\n            channel_states = {}\n            for s in channel_state_names:\n                channel_states[s] = states[s][indices]\n\n            v_and_perturbed = jnp.stack([voltages[indices], voltages[indices] + diff])\n            membrane_currents = vmap(channel.compute_current, in_axes=(None, 0, None))(\n                channel_states, v_and_perturbed, channel_params\n            )\n            voltage_term = (membrane_currents[1] - membrane_currents[0]) / diff\n            constant_term = membrane_currents[0] - voltage_term * voltages[indices]\n\n            # * 1000 to convert from mA/cm^2 to uA/cm^2.\n            voltage_terms = voltage_terms.at[indices].add(voltage_term * 1000.0)\n            constant_terms = constant_terms.at[indices].add(-constant_term * 1000.0)\n\n            # Save the current (for the unperturbed voltage) as a state that will\n            # also be passed to the state update.\n            current_states[channel.current_name] = (\n                current_states[channel.current_name]\n                .at[indices]\n                .add(membrane_currents[0])\n            )\n\n        # Copy the currents into the `state` dictionary such that they can be\n        # recorded and used by `Channel.update_states()`.\n        for name in self.membrane_current_names:\n            states[name] = current_states[name]\n\n        return states, (voltage_terms, constant_terms)\n\n    def _step_synapse(\n        self,\n        u: Dict[str, jnp.ndarray],\n        syn_channels: List[Channel],\n        params: Dict[str, jnp.ndarray],\n        delta_t: float,\n        edges: pd.DataFrame,\n    ) -> Tuple[Dict[str, jnp.ndarray], Tuple[jnp.ndarray, jnp.ndarray]]:\n        \"\"\"One step of integration of the channels.\n\n        `Network` overrides this method (because it actually has synapses), whereas\n        `Compartment`, `Branch`, and `Cell` do not override this.\n        \"\"\"\n        voltages = u[\"v\"]\n        return u, (jnp.zeros_like(voltages), jnp.zeros_like(voltages))\n\n    def _synapse_currents(\n        self, states, syn_channels, params, delta_t, edges: pd.DataFrame\n    ) -> Tuple[Dict[str, jnp.ndarray], Tuple[jnp.ndarray, jnp.ndarray]]:\n        return states, (None, None)\n\n    @staticmethod\n    def _get_external_input(\n        voltages: jnp.ndarray,\n        i_inds: jnp.ndarray,\n        i_stim: jnp.ndarray,\n        radius: float,\n        length_single_compartment: float,\n    ) -> jnp.ndarray:\n        \"\"\"\n        Return external input to each compartment in uA / cm^2.\n\n        Args:\n            voltages: mV.\n            i_stim: nA.\n            radius: um.\n            length_single_compartment: um.\n        \"\"\"\n        zero_vec = jnp.zeros_like(voltages)\n        current = convert_point_process_to_distributed(\n            i_stim, radius[i_inds], length_single_compartment[i_inds]\n        )\n\n        dnums = ScatterDimensionNumbers(\n            update_window_dims=(),\n            inserted_window_dims=(0,),\n            scatter_dims_to_operand_dims=(0,),\n        )\n        stim_at_timestep = scatter_add(zero_vec, i_inds[:, None], current, dnums)\n        return stim_at_timestep\n\n    def vis(\n        self,\n        ax: Optional[Axes] = None,\n        col: str = \"k\",\n        dims: Tuple[int] = (0, 1),\n        type: str = \"line\",\n        morph_plot_kwargs: Dict = {},\n    ) -> Axes:\n        \"\"\"Visualize the module.\n\n        Modules can be visualized on one of the cardinal planes (xy, xz, yz) or\n        even in 3D.\n\n        Several options are available:\n        - `line`: All points from the traced morphology (`xyzr`), are connected\n        with a line plot.\n        - `scatter`: All traced points, are plotted as scatter points.\n        - `comp`: Plots the compartmentalized morphology, including radius\n        and shape. (shows the true compartment lengths per default, but this can\n        be changed via the `morph_plot_kwargs`, for details see\n        `jaxley.utils.plot_utils.plot_comps`).\n        - `morph`: Reconstructs the 3D shape of the traced morphology. For details see\n        `jaxley.utils.plot_utils.plot_morph`. Warning: For 3D plots and morphologies\n        with many traced points this can be very slow.\n\n        Args:\n            ax: An axis into which to plot.\n            col: The color for all branches.\n            dims: Which dimensions to plot. 1=x, 2=y, 3=z coordinate. Must be a tuple of\n                two of them.\n            type: The type of plot. One of [\"line\", \"scatter\", \"comp\", \"morph\"].\n            morph_plot_kwargs: Keyword arguments passed to the plotting function.\n        \"\"\"\n        if \"comp\" in type.lower():\n            return plot_comps(self, dims=dims, ax=ax, col=col, **morph_plot_kwargs)\n        if \"morph\" in type.lower():\n            return plot_morph(self, dims=dims, ax=ax, col=col, **morph_plot_kwargs)\n\n        assert not np.any(\n            [np.isnan(xyzr[:, dims]).all() for xyzr in self.xyzr]\n        ), \"No coordinates available. Use `vis(detail='point')` or run `.compute_xyz()` before running `.vis()`.\"\n\n        ax = plot_graph(\n            self.xyzr,\n            dims=dims,\n            col=col,\n            ax=ax,\n            type=type,\n            morph_plot_kwargs=morph_plot_kwargs,\n        )\n\n        return ax\n\n    def compute_xyz(self):\n        \"\"\"Return xyz coordinates of every branch, based on the branch length.\n\n        This function should not be called if the morphology was read from an `.swc`\n        file. However, for morphologies that were constructed from scratch, this\n        function **must** be called before `.vis()`. The computed `xyz` coordinates\n        are only used for plotting.\n        \"\"\"\n        max_y_multiplier = 5.0\n        min_y_multiplier = 0.5\n\n        parents = self.comb_parents\n        num_children = _compute_num_children(parents)\n        index_of_child = _compute_index_of_child(parents)\n        levels = compute_levels(parents)\n\n        # Extract branch.\n        inds_branch = self.nodes.groupby(\"global_branch_index\")[\n            \"global_comp_index\"\n        ].apply(list)\n        branch_lens = [np.sum(self.nodes[\"length\"][np.asarray(i)]) for i in inds_branch]\n        endpoints = []\n\n        # Different levels will get a different \"angle\" at which the children emerge from\n        # the parents. This angle is defined by the `y_offset_multiplier`. This value\n        # defines the range between y-location of the first and of the last child of a\n        # parent.\n        y_offset_multiplier = np.linspace(\n            max_y_multiplier, min_y_multiplier, np.max(levels) + 1\n        )\n\n        for b in range(self.total_nbranches):\n            # For networks with mixed SWC and from-scatch neurons, only update those\n            # branches that do not have coordingates yet.\n            if np.any(np.isnan(self.xyzr[b])):\n                if parents[b] > -1:\n                    start_point = endpoints[parents[b]]\n                    num_children_of_parent = num_children[parents[b]]\n                    if num_children_of_parent > 1:\n                        y_offset = (\n                            ((index_of_child[b] / (num_children_of_parent - 1))) - 0.5\n                        ) * y_offset_multiplier[levels[b]]\n                    else:\n                        y_offset = 0.0\n                else:\n                    start_point = [0, 0, 0]\n                    y_offset = 0.0\n\n                len_of_path = np.sqrt(y_offset**2 + 1.0)\n\n                end_point = [\n                    start_point[0] + branch_lens[b] / len_of_path * 1.0,\n                    start_point[1] + branch_lens[b] / len_of_path * y_offset,\n                    start_point[2],\n                ]\n                endpoints.append(end_point)\n\n                self.xyzr[b][:, :3] = np.asarray([start_point, end_point])\n            else:\n                # Dummy to keey the index `endpoints[parent[b]]` above working.\n                endpoints.append(np.zeros((2,)))\n\n    def move(\n        self, x: float = 0.0, y: float = 0.0, z: float = 0.0, update_nodes: bool = False\n    ):\n        \"\"\"Move cells or networks by adding to their (x, y, z) coordinates.\n\n        This function is used only for visualization. It does not affect the simulation.\n\n        Args:\n            x: The amount to move in the x direction in um.\n            y: The amount to move in the y direction in um.\n            z: The amount to move in the z direction in um.\n            update_nodes: Whether `.nodes` should be updated or not. Setting this to\n                `False` largely speeds up moving, especially for big networks, but\n                `.nodes` or `.show` will not show the new xyz coordinates.\n        \"\"\"\n        for i in self._branches_in_view:\n            self.base.xyzr[i][:, :3] += np.array([x, y, z])\n        if update_nodes:\n            self.compute_compartment_centers()\n\n    def move_to(\n        self,\n        x: Union[float, np.ndarray] = 0.0,\n        y: Union[float, np.ndarray] = 0.0,\n        z: Union[float, np.ndarray] = 0.0,\n        update_nodes: bool = False,\n    ):\n        \"\"\"Move cells or networks to a location (x, y, z).\n\n        If x, y, and z are floats, then the first compartment of the first branch\n        of the first cell is moved to that float coordinate, and everything else is\n        shifted by the difference between that compartment's previous coordinate and\n        the new float location.\n\n        If x, y, and z are arrays, then they must each have a length equal to the number\n        of cells being moved. Then the first compartment of the first branch of each\n        cell is moved to the specified location.\n\n        Args:\n            update_nodes: Whether `.nodes` should be updated or not. Setting this to\n                `False` largely speeds up moving, especially for big networks, but\n                `.nodes` or `.show` will not show the new xyz coordinates.\n        \"\"\"\n        # Test if any coordinate values are NaN which would greatly affect moving\n        if np.any(np.concatenate(self.xyzr, axis=0)[:, :3] == np.nan):\n            raise ValueError(\n                \"NaN coordinate values detected. Shift amounts cannot be computed. Please run compute_xyzr() or assign initial coordinate values.\"\n            )\n\n        # can only iterate over cells for networks\n        # lambda makes sure that generator can be created multiple times\n        base_is_net = self.base._current_view == \"network\"\n        cells = lambda: (self.cells if base_is_net else [self])\n\n        root_xyz_cells = np.array([c.xyzr[0][0, :3] for c in cells()])\n        root_xyz = root_xyz_cells[0] if isinstance(x, float) else root_xyz_cells\n        move_by = np.array([x, y, z]).T - root_xyz\n\n        if len(move_by.shape) == 1:\n            move_by = np.tile(move_by, (len(self._cells_in_view), 1))\n\n        for cell, offset in zip(cells(), move_by):\n            for idx in cell._branches_in_view:\n                self.base.xyzr[idx][:, :3] += offset\n        if update_nodes:\n            self.compute_compartment_centers()\n\n    def rotate(\n        self, degrees: float, rotation_axis: str = \"xy\", update_nodes: bool = False\n    ):\n        \"\"\"Rotate jaxley modules clockwise. Used only for visualization.\n\n        This function is used only for visualization. It does not affect the simulation.\n\n        Args:\n            degrees: How many degrees to rotate the module by.\n            rotation_axis: Either of {`xy` | `xz` | `yz`}.\n        \"\"\"\n        degrees = degrees / 180 * np.pi\n        if rotation_axis == \"xy\":\n            dims = [0, 1]\n        elif rotation_axis == \"xz\":\n            dims = [0, 2]\n        elif rotation_axis == \"yz\":\n            dims = [1, 2]\n        else:\n            raise ValueError\n\n        rotation_matrix = np.asarray(\n            [[np.cos(degrees), np.sin(degrees)], [-np.sin(degrees), np.cos(degrees)]]\n        )\n        for i in self._branches_in_view:\n            rot = np.dot(rotation_matrix, self.base.xyzr[i][:, dims].T).T\n            self.base.xyzr[i][:, dims] = rot\n        if update_nodes:\n            self.compute_compartment_centers()\n\n    def copy_node_property_to_edges(\n        self,\n        properties_to_import: Union[str, List[str]],\n        pre_or_post: Union[str, List[str]] = [\"pre\", \"post\"],\n    ) -> Module:\n        \"\"\"Copy a property that is in `node` over to `edges`.\n\n        By default, `.edges` does not contain the properties (radius, length, cm,\n        channel properties,...) of the pre- and post-synaptic compartments. This\n        method allows to copy a property of the pre- and/or post-synaptic compartment\n        to the edges. It is then accessible as `module.edges.pre_property_name` or\n        `module.edges.post_property_name`.\n\n        Note that, if you modify the node property _after_ having run\n        `copy_node_property_to_edges`, it will not automatically update the value in\n        `.edges`.\n\n        Note that, if this method is called on a View (e.g.\n        `net.cell(0).copy_node_property_to_edges`), then it will return a View, but\n        it will _not_ modify the module itself.\n\n        Args:\n            properties_to_import: The name of the node properties that should be\n                imported. To list all available properties, look at\n                `module.nodes.columns`.\n            pre_or_post: Whether to import only the pre-synaptic property ('pre'), only\n                the post-synaptic property ('post'), or both (['pre', 'post']).\n\n        Returns:\n            A new module which has the property copied to the `nodes`.\n        \"\"\"\n        # If a string is passed, wrap it as a list.\n        if isinstance(pre_or_post, str):\n            pre_or_post = [pre_or_post]\n        if isinstance(properties_to_import, str):\n            properties_to_import = [properties_to_import]\n\n        for pre_or_post_val in pre_or_post:\n            assert pre_or_post_val in [\"pre\", \"post\"]\n            for property_to_import in properties_to_import:\n                # Delete the column if it already exists. Otherwise it would exist\n                # twice.\n                if f\"{pre_or_post_val}_{property_to_import}\" in self.edges.columns:\n                    self.edges.drop(\n                        columns=f\"{pre_or_post_val}_{property_to_import}\", inplace=True\n                    )\n\n                self.edges = self.edges.join(\n                    self.nodes[[property_to_import, \"global_comp_index\"]].set_index(\n                        \"global_comp_index\"\n                    ),\n                    on=f\"{pre_or_post_val}_global_comp_index\",\n                )\n                self.edges = self.edges.rename(\n                    columns={\n                        property_to_import: f\"{pre_or_post_val}_{property_to_import}\"\n                    }\n                )\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.branches","title":"branches property","text":"

Iterate over all branches in the module.

Returns a generator that yields a View of each branch.

"},{"location":"reference/modules/#jaxley.modules.base.Module.cells","title":"cells property","text":"

Iterate over all cells in the module.

Returns a generator that yields a View of each cell.

"},{"location":"reference/modules/#jaxley.modules.base.Module.comps","title":"comps property","text":"

Iterate over all compartments in the module. Can be called on any module, i.e. net.comps, cell.comps or branch.comps. __iter__ does not allow for this.

Returns a generator that yields a View of each compartment.

"},{"location":"reference/modules/#jaxley.modules.base.Module.initialized","title":"initialized: bool property","text":"

Whether the Module is ready to be solved or not.

"},{"location":"reference/modules/#jaxley.modules.base.Module.shape","title":"shape: Tuple[int] property","text":"

Returns the number of submodules contained in a module.

.. code-block:: python

network.shape = (num_cells, num_branches, num_compartments)\ncell.shape = (num_branches, num_compartments)\nbranch.shape = (num_compartments,)\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.view","title":"view property","text":"

Return view of the module.

"},{"location":"reference/modules/#jaxley.modules.base.Module.__getitem__","title":"__getitem__(index)","text":"

Lazy indexing of the module.

Source code in jaxley/modules/base.py
def __getitem__(self, index):\n    \"\"\"Lazy indexing of the module.\"\"\"\n    supported_parents = [\"network\", \"cell\", \"branch\"]  # cannot index into comp\n\n    not_group_view = self._current_view not in self.groups\n    assert (\n        self._current_view in supported_parents or not_group_view\n    ), \"Lazy indexing is only supported for `Network`, `Cell`, `Branch` and Views thereof.\"\n    index = index if isinstance(index, tuple) else (index,)\n\n    child_views = self._childviews()\n    assert len(index) <= len(child_views), \"Too many indices.\"\n    view = self\n    for i, child in zip(index, child_views):\n        view = view._at_nodes(child, i)\n    return view\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.__iter__","title":"__iter__()","text":"

Iterate over parts of the module.

Internally calls cells, branches, comps at the appropriate level.

Example:

.. code-block:: python

for cell in network:\n    for branch in cell:\n        for comp in branch:\n            print(comp.nodes.shape)\n
Source code in jaxley/modules/base.py
def __iter__(self):\n    \"\"\"Iterate over parts of the module.\n\n    Internally calls `cells`, `branches`, `comps` at the appropriate level.\n\n    Example:\n\n    .. code-block:: python\n\n        for cell in network:\n            for branch in cell:\n                for comp in branch:\n                    print(comp.nodes.shape)\n    \"\"\"\n    next_level = self._childviews()[0]\n    yield from self._iter_submodules(next_level)\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.add_to_group","title":"add_to_group(group_name)","text":"

Add a view of the module to a group.

Groups can then be indexed. For example:

.. code-block:: python

net.cell(0).add_to_group(\"excitatory\")\nnet.excitatory.set(\"radius\", 0.1)\n

Parameters:

Name Type Description Default group_name str

The name of the group.

required Source code in jaxley/modules/base.py
def add_to_group(self, group_name: str):\n    \"\"\"Add a view of the module to a group.\n\n    Groups can then be indexed. For example:\n\n    .. code-block:: python\n\n        net.cell(0).add_to_group(\"excitatory\")\n        net.excitatory.set(\"radius\", 0.1)\n\n    Args:\n        group_name: The name of the group.\n    \"\"\"\n    if group_name not in self.base.groups:\n        self.base.groups[group_name] = self._nodes_in_view\n    else:\n        self.base.groups[group_name] = np.unique(\n            np.concatenate([self.base.groups[group_name], self._nodes_in_view])\n        )\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.branch","title":"branch(idx)","text":"

Return a View of the module at the selected branches(s).

Parameters:

Name Type Description Default idx Any

index of the branch to view.

required

Returns:

Type Description View

View of the module at the specified branch index.

Source code in jaxley/modules/base.py
def branch(self, idx: Any) -> View:\n    \"\"\"Return a View of the module at the selected branches(s).\n\n    Args:\n        idx: index of the branch to view.\n\n    Returns:\n        View of the module at the specified branch index.\"\"\"\n    return self._at_nodes(\"branch\", idx)\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.cell","title":"cell(idx)","text":"

Return a View of the module at the selected cell(s).

Parameters:

Name Type Description Default idx Any

index of the cell to view.

required

Returns:

Type Description View

View of the module at the specified cell index.

Source code in jaxley/modules/base.py
def cell(self, idx: Any) -> View:\n    \"\"\"Return a View of the module at the selected cell(s).\n\n    Args:\n        idx: index of the cell to view.\n\n    Returns:\n        View of the module at the specified cell index.\"\"\"\n    return self._at_nodes(\"cell\", idx)\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.clamp","title":"clamp(state_name, state_array, verbose=True)","text":"

Clamp a state to a given value across specified compartments.

Parameters:

Name Type Description Default state_name str

The name of the state to clamp.

required state_array nd

Array of values to clamp the state to.

required verbose

If True, prints details about the clamping.

True

This function sets external states for the compartments.

Source code in jaxley/modules/base.py
def clamp(self, state_name: str, state_array: jnp.ndarray, verbose: bool = True):\n    \"\"\"Clamp a state to a given value across specified compartments.\n\n    Args:\n        state_name: The name of the state to clamp.\n        state_array (jnp.nd: Array of values to clamp the state to.\n        verbose : If True, prints details about the clamping.\n\n    This function sets external states for the compartments.\n    \"\"\"\n    self._external_input(state_name, state_array, verbose=verbose)\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.comp","title":"comp(idx)","text":"

Return a View of the module at the selected compartments(s).

Parameters:

Name Type Description Default idx Any

index of the comp to view.

required

Returns:

Type Description View

View of the module at the specified compartment index.

Source code in jaxley/modules/base.py
def comp(self, idx: Any) -> View:\n    \"\"\"Return a View of the module at the selected compartments(s).\n\n    Args:\n        idx: index of the comp to view.\n\n    Returns:\n        View of the module at the specified compartment index.\"\"\"\n    return self._at_nodes(\"comp\", idx)\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.compute_compartment_centers","title":"compute_compartment_centers()","text":"

Add compartment centers to nodes dataframe

Source code in jaxley/modules/base.py
def compute_compartment_centers(self):\n    \"\"\"Add compartment centers to nodes dataframe\"\"\"\n    centers = self._compute_coords_of_comp_centers()\n    self.base.nodes.loc[self._nodes_in_view, [\"x\", \"y\", \"z\"]] = centers\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.compute_xyz","title":"compute_xyz()","text":"

Return xyz coordinates of every branch, based on the branch length.

This function should not be called if the morphology was read from an .swc file. However, for morphologies that were constructed from scratch, this function must be called before .vis(). The computed xyz coordinates are only used for plotting.

Source code in jaxley/modules/base.py
def compute_xyz(self):\n    \"\"\"Return xyz coordinates of every branch, based on the branch length.\n\n    This function should not be called if the morphology was read from an `.swc`\n    file. However, for morphologies that were constructed from scratch, this\n    function **must** be called before `.vis()`. The computed `xyz` coordinates\n    are only used for plotting.\n    \"\"\"\n    max_y_multiplier = 5.0\n    min_y_multiplier = 0.5\n\n    parents = self.comb_parents\n    num_children = _compute_num_children(parents)\n    index_of_child = _compute_index_of_child(parents)\n    levels = compute_levels(parents)\n\n    # Extract branch.\n    inds_branch = self.nodes.groupby(\"global_branch_index\")[\n        \"global_comp_index\"\n    ].apply(list)\n    branch_lens = [np.sum(self.nodes[\"length\"][np.asarray(i)]) for i in inds_branch]\n    endpoints = []\n\n    # Different levels will get a different \"angle\" at which the children emerge from\n    # the parents. This angle is defined by the `y_offset_multiplier`. This value\n    # defines the range between y-location of the first and of the last child of a\n    # parent.\n    y_offset_multiplier = np.linspace(\n        max_y_multiplier, min_y_multiplier, np.max(levels) + 1\n    )\n\n    for b in range(self.total_nbranches):\n        # For networks with mixed SWC and from-scatch neurons, only update those\n        # branches that do not have coordingates yet.\n        if np.any(np.isnan(self.xyzr[b])):\n            if parents[b] > -1:\n                start_point = endpoints[parents[b]]\n                num_children_of_parent = num_children[parents[b]]\n                if num_children_of_parent > 1:\n                    y_offset = (\n                        ((index_of_child[b] / (num_children_of_parent - 1))) - 0.5\n                    ) * y_offset_multiplier[levels[b]]\n                else:\n                    y_offset = 0.0\n            else:\n                start_point = [0, 0, 0]\n                y_offset = 0.0\n\n            len_of_path = np.sqrt(y_offset**2 + 1.0)\n\n            end_point = [\n                start_point[0] + branch_lens[b] / len_of_path * 1.0,\n                start_point[1] + branch_lens[b] / len_of_path * y_offset,\n                start_point[2],\n            ]\n            endpoints.append(end_point)\n\n            self.xyzr[b][:, :3] = np.asarray([start_point, end_point])\n        else:\n            # Dummy to keey the index `endpoints[parent[b]]` above working.\n            endpoints.append(np.zeros((2,)))\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.copy","title":"copy(reset_index=False, as_module=False)","text":"

Extract part of a module and return a copy of its View or a new module.

This can be used to call jx.integrate on part of a Module.

Parameters:

Name Type Description Default reset_index bool

if True, the indices of the new module are reset to start from 0.

False as_module bool

if True, a new module is returned instead of a View.

False

Returns:

Type Description Union[Module, View]

A part of the module or a copied view of it.

Source code in jaxley/modules/base.py
def copy(\n    self, reset_index: bool = False, as_module: bool = False\n) -> Union[Module, View]:\n    \"\"\"Extract part of a module and return a copy of its View or a new module.\n\n    This can be used to call `jx.integrate` on part of a Module.\n\n    Args:\n        reset_index: if True, the indices of the new module are reset to start from 0.\n        as_module: if True, a new module is returned instead of a View.\n\n    Returns:\n        A part of the module or a copied view of it.\"\"\"\n    view = deepcopy(self)\n    warnings.warn(\"This method is experimental, use at your own risk.\")\n    # TODO FROM #447: add reset_index, i.e. for parents, nodes, edges etc. such that they\n    # start from 0/-1 and are contiguous\n    if as_module:\n        raise NotImplementedError(\"Not yet implemented.\")\n        # initialize a new module with the same attributes\n    return view\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.copy_node_property_to_edges","title":"copy_node_property_to_edges(properties_to_import, pre_or_post=['pre', 'post'])","text":"

Copy a property that is in node over to edges.

By default, .edges does not contain the properties (radius, length, cm, channel properties,\u2026) of the pre- and post-synaptic compartments. This method allows to copy a property of the pre- and/or post-synaptic compartment to the edges. It is then accessible as module.edges.pre_property_name or module.edges.post_property_name.

Note that, if you modify the node property after having run copy_node_property_to_edges, it will not automatically update the value in .edges.

Note that, if this method is called on a View (e.g. net.cell(0).copy_node_property_to_edges), then it will return a View, but it will not modify the module itself.

Parameters:

Name Type Description Default properties_to_import Union[str, List[str]]

The name of the node properties that should be imported. To list all available properties, look at module.nodes.columns.

required pre_or_post Union[str, List[str]]

Whether to import only the pre-synaptic property (\u2018pre\u2019), only the post-synaptic property (\u2018post\u2019), or both ([\u2018pre\u2019, \u2018post\u2019]).

['pre', 'post']

Returns:

Type Description Module

A new module which has the property copied to the nodes.

Source code in jaxley/modules/base.py
def copy_node_property_to_edges(\n    self,\n    properties_to_import: Union[str, List[str]],\n    pre_or_post: Union[str, List[str]] = [\"pre\", \"post\"],\n) -> Module:\n    \"\"\"Copy a property that is in `node` over to `edges`.\n\n    By default, `.edges` does not contain the properties (radius, length, cm,\n    channel properties,...) of the pre- and post-synaptic compartments. This\n    method allows to copy a property of the pre- and/or post-synaptic compartment\n    to the edges. It is then accessible as `module.edges.pre_property_name` or\n    `module.edges.post_property_name`.\n\n    Note that, if you modify the node property _after_ having run\n    `copy_node_property_to_edges`, it will not automatically update the value in\n    `.edges`.\n\n    Note that, if this method is called on a View (e.g.\n    `net.cell(0).copy_node_property_to_edges`), then it will return a View, but\n    it will _not_ modify the module itself.\n\n    Args:\n        properties_to_import: The name of the node properties that should be\n            imported. To list all available properties, look at\n            `module.nodes.columns`.\n        pre_or_post: Whether to import only the pre-synaptic property ('pre'), only\n            the post-synaptic property ('post'), or both (['pre', 'post']).\n\n    Returns:\n        A new module which has the property copied to the `nodes`.\n    \"\"\"\n    # If a string is passed, wrap it as a list.\n    if isinstance(pre_or_post, str):\n        pre_or_post = [pre_or_post]\n    if isinstance(properties_to_import, str):\n        properties_to_import = [properties_to_import]\n\n    for pre_or_post_val in pre_or_post:\n        assert pre_or_post_val in [\"pre\", \"post\"]\n        for property_to_import in properties_to_import:\n            # Delete the column if it already exists. Otherwise it would exist\n            # twice.\n            if f\"{pre_or_post_val}_{property_to_import}\" in self.edges.columns:\n                self.edges.drop(\n                    columns=f\"{pre_or_post_val}_{property_to_import}\", inplace=True\n                )\n\n            self.edges = self.edges.join(\n                self.nodes[[property_to_import, \"global_comp_index\"]].set_index(\n                    \"global_comp_index\"\n                ),\n                on=f\"{pre_or_post_val}_global_comp_index\",\n            )\n            self.edges = self.edges.rename(\n                columns={\n                    property_to_import: f\"{pre_or_post_val}_{property_to_import}\"\n                }\n            )\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.data_clamp","title":"data_clamp(state_name, state_array, data_clamps=None, verbose=False)","text":"

Insert a clamp into the module within jit (or grad).

Parameters:

Name Type Description Default state_name str

Name of the state variable to set.

required state_array ndarray

Time series of the state variable in the default Jaxley unit. State array should be of shape (num_clamps, simulation_time) or (simulation_time, ) for a single clamp.

required verbose bool

Whether or not to print the number of inserted clamps. False by default because this method is meant to be jitted.

False Source code in jaxley/modules/base.py
def data_clamp(\n    self,\n    state_name: str,\n    state_array: jnp.ndarray,\n    data_clamps: Optional[Tuple[jnp.ndarray, pd.DataFrame]] = None,\n    verbose: bool = False,\n):\n    \"\"\"Insert a clamp into the module within jit (or grad).\n\n    Args:\n        state_name: Name of the state variable to set.\n        state_array: Time series of the state variable in the default Jaxley unit.\n            State array should be of shape (num_clamps, simulation_time) or\n            (simulation_time, ) for a single clamp.\n        verbose: Whether or not to print the number of inserted clamps. `False`\n            by default because this method is meant to be jitted.\n    \"\"\"\n    comp_states, edge_states = self._get_state_names()\n    if state_name not in comp_states + edge_states:\n        raise KeyError(f\"{state_name} is not a recognized state in this module.\")\n    data = self.nodes if state_name in comp_states else self.edges\n    return self._data_external_input(\n        state_name, state_array, data_clamps, data, verbose=verbose\n    )\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.data_set","title":"data_set(key, val, param_state)","text":"

Set parameter of module (or its view) to a new value within jit.

Parameters:

Name Type Description Default key str

The name of the parameter to set.

required val Union[float, ndarray]

The value to set the parameter to. If it is jnp.ndarray then it must be of shape (len(num_compartments)).

required param_state Optional[List[Dict]]

State of the setted parameters, internally used such that this function does not modify global state.

required Source code in jaxley/modules/base.py
def data_set(\n    self,\n    key: str,\n    val: Union[float, jnp.ndarray],\n    param_state: Optional[List[Dict]],\n):\n    \"\"\"Set parameter of module (or its view) to a new value within `jit`.\n\n    Args:\n        key: The name of the parameter to set.\n        val: The value to set the parameter to. If it is `jnp.ndarray` then it\n            must be of shape `(len(num_compartments))`.\n        param_state: State of the setted parameters, internally used such that this\n            function does not modify global state.\n    \"\"\"\n    # Note: `data_set` does not support arrays for `val`.\n    is_node_param = key in self.nodes.columns\n    data = self.nodes if is_node_param else self.edges\n    viewed_inds = self._nodes_in_view if is_node_param else self._edges_in_view\n    if key in data.columns:\n        not_nan = ~data[key].isna()\n        added_param_state = [\n            {\n                \"indices\": np.atleast_2d(viewed_inds[not_nan]),\n                \"key\": key,\n                \"val\": jnp.atleast_1d(jnp.asarray(val)),\n            }\n        ]\n        if param_state is not None:\n            param_state += added_param_state\n        else:\n            param_state = added_param_state\n    else:\n        raise KeyError(\"Key not recognized.\")\n    return param_state\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.data_stimulate","title":"data_stimulate(current, data_stimuli=None, verbose=False)","text":"

Insert a stimulus into the module within jit (or grad).

Parameters:

Name Type Description Default current ndarray

Current in nA.

required verbose bool

Whether or not to print the number of inserted stimuli. False by default because this method is meant to be jitted.

False Source code in jaxley/modules/base.py
def data_stimulate(\n    self,\n    current: jnp.ndarray,\n    data_stimuli: Optional[Tuple[jnp.ndarray, pd.DataFrame]] = None,\n    verbose: bool = False,\n) -> Tuple[jnp.ndarray, pd.DataFrame]:\n    \"\"\"Insert a stimulus into the module within jit (or grad).\n\n    Args:\n        current: Current in `nA`.\n        verbose: Whether or not to print the number of inserted stimuli. `False`\n            by default because this method is meant to be jitted.\n    \"\"\"\n    return self._data_external_input(\n        \"i\", current, data_stimuli, self.nodes, verbose=verbose\n    )\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.delete_channel","title":"delete_channel(channel)","text":"

Remove a channel from the module.

Parameters:

Name Type Description Default channel Channel

The channel to remove.

required Source code in jaxley/modules/base.py
def delete_channel(self, channel: Channel):\n    \"\"\"Remove a channel from the module.\n\n    Args:\n        channel: The channel to remove.\"\"\"\n    name = channel._name\n    channel_names = [c._name for c in self.channels]\n    all_channel_names = [c._name for c in self.base.channels]\n    if name in channel_names:\n        channel_cols = list(channel.channel_params.keys())\n        channel_cols += list(channel.channel_states.keys())\n        self.base.nodes.loc[self._nodes_in_view, channel_cols] = float(\"nan\")\n        self.base.nodes.loc[self._nodes_in_view, name] = False\n\n        # only delete cols if no other comps in the module have the same channel\n        if np.all(~self.base.nodes[name]):\n            self.base.channels.pop(all_channel_names.index(name))\n            self.base.membrane_current_names.remove(channel.current_name)\n            self.base.nodes.drop(columns=channel_cols + [name], inplace=True)\n    else:\n        raise ValueError(f\"Channel {name} not found in the module.\")\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.delete_clamps","title":"delete_clamps(state_name=None)","text":"

Removes all clamps of the given state from the module.

Source code in jaxley/modules/base.py
def delete_clamps(self, state_name: Optional[str] = None):\n    \"\"\"Removes all clamps of the given state from the module.\"\"\"\n    all_externals = list(self.externals.keys())\n    if \"i\" in all_externals:\n        all_externals.remove(\"i\")\n    state_names = all_externals if state_name is None else [state_name]\n    for state_name in state_names:\n        if state_name in self.externals:\n            keep_inds = ~np.isin(\n                self.base.external_inds[state_name], self._nodes_in_view\n            )\n            base_exts = self.base.externals\n            base_exts_inds = self.base.external_inds\n            if np.all(~keep_inds):\n                base_exts.pop(state_name, None)\n                base_exts_inds.pop(state_name, None)\n            else:\n                base_exts[state_name] = base_exts[state_name][keep_inds]\n                base_exts_inds[state_name] = base_exts_inds[state_name][keep_inds]\n            self._update_view()\n        else:\n            pass  # does not have to be deleted if not in externals\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.delete_recordings","title":"delete_recordings()","text":"

Removes all recordings from the module.

Source code in jaxley/modules/base.py
def delete_recordings(self):\n    \"\"\"Removes all recordings from the module.\"\"\"\n    if isinstance(self, View):\n        base_recs = self.base.recordings\n        self.base.recordings = base_recs[\n            ~base_recs.isin(self.recordings).all(axis=1)\n        ]\n        self._update_view()\n    else:\n        self.base.recordings = pd.DataFrame().from_dict({})\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.delete_stimuli","title":"delete_stimuli()","text":"

Removes all stimuli from the module.

Source code in jaxley/modules/base.py
def delete_stimuli(self):\n    \"\"\"Removes all stimuli from the module.\"\"\"\n    self.delete_clamps(\"i\")\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.delete_trainables","title":"delete_trainables()","text":"

Removes all trainable parameters from the module.

Source code in jaxley/modules/base.py
def delete_trainables(self):\n    \"\"\"Removes all trainable parameters from the module.\"\"\"\n\n    if isinstance(self, View):\n        trainables_and_inds = self._filter_trainables(is_viewed=False)\n        self.base.indices_set_by_trainables = trainables_and_inds[0]\n        self.base.trainable_params = trainables_and_inds[1]\n        self.base.num_trainable_params -= self.num_trainable_params\n    else:\n        self.base.indices_set_by_trainables = []\n        self.base.trainable_params = []\n        self.base.num_trainable_params = 0\n    self._update_view()\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.distance","title":"distance(endpoint)","text":"

Return the direct distance between two compartments. This does not compute the pathwise distance (which is currently not implemented). Args: endpoint: The compartment to which to compute the distance to.

Source code in jaxley/modules/base.py
def distance(self, endpoint: \"View\") -> float:\n    \"\"\"Return the direct distance between two compartments.\n    This does not compute the pathwise distance (which is currently not\n    implemented).\n    Args:\n        endpoint: The compartment to which to compute the distance to.\n    \"\"\"\n    assert len(self.xyzr) == 1 and len(endpoint.xyzr) == 1\n    start_xyz = np.mean(self.xyzr[0][:, :3], axis=0)\n    end_xyz = np.mean(endpoint.xyzr[0][:, :3], axis=0)\n    return np.sqrt(np.sum((start_xyz - end_xyz) ** 2))\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.edge","title":"edge(idx)","text":"

Return a View of the module at the selected synapse edges(s).

Parameters:

Name Type Description Default idx Any

index of the edge to view.

required

Returns:

Type Description View

View of the module at the specified edge index.

Source code in jaxley/modules/base.py
def edge(self, idx: Any) -> View:\n    \"\"\"Return a View of the module at the selected synapse edges(s).\n\n    Args:\n        idx: index of the edge to view.\n\n    Returns:\n        View of the module at the specified edge index.\"\"\"\n    return self._at_edges(\"edge\", idx)\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.get_all_parameters","title":"get_all_parameters(pstate, voltage_solver)","text":"

Return all parameters (and coupling conductances) needed to simulate.

Runs _compute_axial_conductances() and return every parameter that is needed to solve the ODE. This includes conductances, radiuses, lengths, axial_resistivities, but also coupling conductances.

This is done by first obtaining the current value of every parameter (not only the trainable ones) and then replacing the trainable ones with the value in trainable_params(). This function is run within jx.integrate().

pstate can be obtained by calling params_to_pstate().

.. code-block:: python

params = module.get_parameters() # i.e. [0, 1, 2]\npstate = params_to_pstate(params, module.indices_set_by_trainables)\nmodule.to_jax() # needed for call to module.jaxnodes\n

Parameters:

Name Type Description Default pstate List[Dict]

The state of the trainable parameters. pstate takes the form [{ \u201ckey\u201d: \u201cgNa\u201d, \u201cindices\u201d: jnp.array([0, 1, 2]), \u201cval\u201d: jnp.array([0.1, 0.2, 0.3]) }, \u2026].

required voltage_solver str

The voltage solver that is used. Since jax.sparse and jaxley.xyz require different formats of the axial conductances, this function will default to different building methods.

required

Returns:

Type Description Dict[str, ndarray]

A dictionary of all module parameters.

Source code in jaxley/modules/base.py
@only_allow_module\ndef get_all_parameters(\n    self, pstate: List[Dict], voltage_solver: str\n) -> Dict[str, jnp.ndarray]:\n    # TODO FROM #447: MAKE THIS WORK FOR VIEW?\n    \"\"\"Return all parameters (and coupling conductances) needed to simulate.\n\n    Runs `_compute_axial_conductances()` and return every parameter that is needed\n    to solve the ODE. This includes conductances, radiuses, lengths,\n    axial_resistivities, but also coupling conductances.\n\n    This is done by first obtaining the current value of every parameter (not only\n    the trainable ones) and then replacing the trainable ones with the value\n    in `trainable_params()`. This function is run within `jx.integrate()`.\n\n    pstate can be obtained by calling `params_to_pstate()`.\n\n    .. code-block:: python\n\n        params = module.get_parameters() # i.e. [0, 1, 2]\n        pstate = params_to_pstate(params, module.indices_set_by_trainables)\n        module.to_jax() # needed for call to module.jaxnodes\n\n    Args:\n        pstate: The state of the trainable parameters. pstate takes the form\n            [{\n                \"key\": \"gNa\", \"indices\": jnp.array([0, 1, 2]),\n                \"val\": jnp.array([0.1, 0.2, 0.3])\n            }, ...].\n        voltage_solver: The voltage solver that is used. Since `jax.sparse` and\n            `jaxley.xyz` require different formats of the axial conductances, this\n            function will default to different building methods.\n\n    Returns:\n        A dictionary of all module parameters.\n    \"\"\"\n    params = {}\n    for key in [\"radius\", \"length\", \"axial_resistivity\", \"capacitance\"]:\n        params[key] = self.base.jaxnodes[key]\n\n    for channel in self.base.channels:\n        for channel_params in channel.channel_params:\n            params[channel_params] = self.base.jaxnodes[channel_params]\n\n    for synapse_params in self.base.synapse_param_names:\n        params[synapse_params] = self.base.jaxedges[synapse_params]\n\n    # Override with those parameters set by `.make_trainable()`.\n    for parameter in pstate:\n        key = parameter[\"key\"]\n        inds = parameter[\"indices\"]\n        set_param = parameter[\"val\"]\n\n        # This is needed since SynapseViews worked differently before.\n        # This mimics the old behaviour and tranformes the new indices\n        # to the old indices.\n        # TODO FROM #447: Longterm this should be gotten rid of.\n        # Instead edges should work similar to nodes (would also allow for\n        # param sharing).\n        synapse_inds = self.base.edges.groupby(\"type\").rank()[\"global_edge_index\"]\n        synapse_inds = (synapse_inds.astype(int) - 1).to_numpy()\n        if key in self.base.synapse_param_names:\n            inds = synapse_inds[inds]\n\n        if key in params:  # Only parameters, not initial states.\n            # `inds` is of shape `(num_params, num_comps_per_param)`.\n            # `set_param` is of shape `(num_params,)`\n            # We need to unsqueeze `set_param` to make it `(num_params, 1)` for the\n            # `.set()` to work. This is done with `[:, None]`.\n            params[key] = params[key].at[inds].set(set_param[:, None])\n\n    # Compute conductance params and add them to the params dictionary.\n    params[\"axial_conductances\"] = self.base._compute_axial_conductances(\n        params=params\n    )\n    return params\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.get_all_states","title":"get_all_states(pstate, all_params, delta_t)","text":"

Get the full initial state of the module from jaxnodes and trainables.

Parameters:

Name Type Description Default pstate List[Dict]

The state of the trainable parameters.

required all_params

All parameters of the module.

required delta_t float

The time step.

required

Returns:

Type Description Dict[str, ndarray]

A dictionary of all states of the module.

Source code in jaxley/modules/base.py
@only_allow_module\ndef get_all_states(\n    self, pstate: List[Dict], all_params, delta_t: float\n) -> Dict[str, jnp.ndarray]:\n    # TODO FROM #447: MAKE THIS WORK FOR VIEW?\n    \"\"\"Get the full initial state of the module from jaxnodes and trainables.\n\n    Args:\n        pstate: The state of the trainable parameters.\n        all_params: All parameters of the module.\n        delta_t: The time step.\n\n    Returns:\n        A dictionary of all states of the module.\n    \"\"\"\n    states = self.base._get_states_from_nodes_and_edges()\n\n    # Override with the initial states set by `.make_trainable()`.\n    for parameter in pstate:\n        key = parameter[\"key\"]\n        inds = parameter[\"indices\"]\n        set_param = parameter[\"val\"]\n        if key in states:  # Only initial states, not parameters.\n            # `inds` is of shape `(num_params, num_comps_per_param)`.\n            # `set_param` is of shape `(num_params,)`\n            # We need to unsqueeze `set_param` to make it `(num_params, 1)` for the\n            # `.set()` to work. This is done with `[:, None]`.\n            states[key] = states[key].at[inds].set(set_param[:, None])\n\n    # Add to the states the initial current through every channel.\n    states, _ = self.base._channel_currents(\n        states, delta_t, self.channels, self.nodes, all_params\n    )\n\n    # Add to the states the initial current through every synapse.\n    states, _ = self.base._synapse_currents(\n        states, self.synapses, all_params, delta_t, self.edges\n    )\n    return states\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.get_parameters","title":"get_parameters()","text":"

Get all trainable parameters.

The returned parameters should be passed to `jx.integrate(\u2026, params=params).

Returns:

Type Description List[Dict[str, ndarray]]

A list of all trainable parameters in the form of [{\u201cgNa\u201d: jnp.array([0.1, 0.2, 0.3])}, \u2026].

Source code in jaxley/modules/base.py
def get_parameters(self) -> List[Dict[str, jnp.ndarray]]:\n    \"\"\"Get all trainable parameters.\n\n    The returned parameters should be passed to `jx.integrate(..., params=params).\n\n    Returns:\n        A list of all trainable parameters in the form of\n            [{\"gNa\": jnp.array([0.1, 0.2, 0.3])}, ...].\n    \"\"\"\n    return self.trainable_params\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.init_states","title":"init_states(delta_t=0.025)","text":"

Initialize all mechanisms in their steady state.

This considers the voltages and parameters of each compartment.

Parameters:

Name Type Description Default delta_t float

Passed on to channel.init_state().

0.025 Source code in jaxley/modules/base.py
@only_allow_module\ndef init_states(self, delta_t: float = 0.025):\n    # TODO FROM #447: MAKE THIS WORK FOR VIEW?\n    \"\"\"Initialize all mechanisms in their steady state.\n\n    This considers the voltages and parameters of each compartment.\n\n    Args:\n        delta_t: Passed on to `channel.init_state()`.\n    \"\"\"\n    # Update states of the channels.\n    channel_nodes = self.base.nodes\n    states = self.base._get_states_from_nodes_and_edges()\n\n    # We do not use any `pstate` for initializing. In principle, we could change\n    # that by allowing an input `params` and `pstate` to this function.\n    # `voltage_solver` could also be `jax.sparse` here, because both of them\n    # build the channel parameters in the same way.\n    params = self.base.get_all_parameters([], voltage_solver=\"jaxley.thomas\")\n\n    for channel in self.base.channels:\n        name = channel._name\n        channel_indices = channel_nodes.loc[channel_nodes[name]][\n            \"global_comp_index\"\n        ].to_numpy()\n        voltages = channel_nodes.loc[channel_indices, \"v\"].to_numpy()\n\n        channel_param_names = list(channel.channel_params.keys())\n        channel_state_names = list(channel.channel_states.keys())\n        channel_states = query_channel_states_and_params(\n            states, channel_state_names, channel_indices\n        )\n        channel_params = query_channel_states_and_params(\n            params, channel_param_names, channel_indices\n        )\n\n        init_state = channel.init_state(\n            channel_states, voltages, channel_params, delta_t\n        )\n\n        # `init_state` might not return all channel states. Only the ones that are\n        # returned are updated here.\n        for key, val in init_state.items():\n            # Note that we are overriding `self.nodes` here, but `self.nodes` is\n            # not used above to actually compute the current states (so there are\n            # no issues with overriding states).\n            self.nodes.loc[channel_indices, key] = val\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.insert","title":"insert(channel)","text":"

Insert a channel into the module.

Parameters:

Name Type Description Default channel Channel

The channel to insert.

required Source code in jaxley/modules/base.py
def insert(self, channel: Channel):\n    \"\"\"Insert a channel into the module.\n\n    Args:\n        channel: The channel to insert.\"\"\"\n    name = channel._name\n\n    # Channel does not yet exist in the `jx.Module` at all.\n    if name not in [c._name for c in self.base.channels]:\n        self.base.channels.append(channel)\n        self.base.nodes[name] = (\n            False  # Previous columns do not have the new channel.\n        )\n\n    if channel.current_name not in self.base.membrane_current_names:\n        self.base.membrane_current_names.append(channel.current_name)\n\n    # Add a binary column that indicates if a channel is present.\n    self.base.nodes.loc[self._nodes_in_view, name] = True\n\n    # Loop over all new parameters, e.g. gNa, eNa.\n    for key in channel.channel_params:\n        self.base.nodes.loc[self._nodes_in_view, key] = channel.channel_params[key]\n\n    # Loop over all new parameters, e.g. gNa, eNa.\n    for key in channel.channel_states:\n        self.base.nodes.loc[self._nodes_in_view, key] = channel.channel_states[key]\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.loc","title":"loc(at)","text":"

Return a View of the module at the selected branch location(s).

Parameters:

Name Type Description Default at Any

location along the branch.

required

Returns:

Type Description View

View of the module at the specified branch location.

Source code in jaxley/modules/base.py
def loc(self, at: Any) -> View:\n    \"\"\"Return a View of the module at the selected branch location(s).\n\n    Args:\n        at: location along the branch.\n\n    Returns:\n        View of the module at the specified branch location.\"\"\"\n    global_comp_idxs = []\n    for i in self._branches_in_view:\n        nseg = self.base.nseg_per_branch[i]\n        comp_locs = np.linspace(0, 1, nseg)\n        at = comp_locs if is_str_all(at) else self._reformat_index(at, dtype=float)\n        comp_edges = np.linspace(0, 1 + 1e-10, nseg + 1)\n        idx = np.digitize(at, comp_edges) - 1 + self.base.cumsum_nseg[i]\n        global_comp_idxs.append(idx)\n    global_comp_idxs = np.concatenate(global_comp_idxs)\n    orig_scope = self._scope\n    # global scope needed to select correct comps, for i.e. branches w. nseg=[1,2]\n    # loc(0.9)  will correspond to different local branches (0 vs 1).\n    view = self.scope(\"global\").comp(global_comp_idxs).scope(orig_scope)\n    view._current_view = \"loc\"\n    return view\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.make_trainable","title":"make_trainable(key, init_val=None, verbose=True)","text":"

Make a parameter trainable.

If a parameter is made trainable, it will be returned by get_parameters() and should then be passed to jx.integrate(..., params=params).

Parameters:

Name Type Description Default key str

Name of the parameter to make trainable.

required init_val Optional[Union[float, list]]

Initial value of the parameter. If float, the same value is used for every created parameter. If list, the length of the list has to match the number of created parameters. If None, the current parameter value is used and if parameter sharing is performed that the current parameter value is averaged over all shared parameters.

None verbose bool

Whether to print the number of parameters that are added and the total number of parameters.

True Source code in jaxley/modules/base.py
def make_trainable(\n    self,\n    key: str,\n    init_val: Optional[Union[float, list]] = None,\n    verbose: bool = True,\n):\n    \"\"\"Make a parameter trainable.\n\n    If a parameter is made trainable, it will be returned by `get_parameters()`\n    and should then be passed to `jx.integrate(..., params=params)`.\n\n    Args:\n        key: Name of the parameter to make trainable.\n        init_val: Initial value of the parameter. If `float`, the same value is\n            used for every created parameter. If `list`, the length of the list has\n            to match the number of created parameters. If `None`, the current\n            parameter value is used and if parameter sharing is performed that the\n            current parameter value is averaged over all shared parameters.\n        verbose: Whether to print the number of parameters that are added and the\n            total number of parameters.\n    \"\"\"\n    assert (\n        self.allow_make_trainable\n    ), \"network.cell('all').make_trainable() is not supported. Use a for-loop over cells.\"\n    nsegs_per_branch = (\n        self.base.nodes[\"global_branch_index\"].value_counts().to_numpy()\n    )\n    assert np.all(\n        nsegs_per_branch == nsegs_per_branch[0]\n    ), \"Parameter sharing is not allowed for modules containing branches with different numbers of compartments.\"\n\n    data = self.nodes if key in self.nodes.columns else None\n    data = self.edges if key in self.edges.columns else data\n\n    assert data is not None, f\"Key '{key}' not found in nodes or edges\"\n    not_nan = ~data[key].isna()\n    data = data.loc[not_nan]\n    assert (\n        len(data) > 0\n    ), \"No settable parameters found in the selected compartments.\"\n\n    grouped_view = data.groupby(\"controlled_by_param\")\n    # Because of this `x.index.values` we cannot support `make_trainable()` on\n    # the module level for synapse parameters (but only for `SynapseView`).\n    inds_of_comps = list(\n        grouped_view.apply(lambda x: x.index.values, include_groups=False)\n    )\n    indices_per_param = jnp.stack(inds_of_comps)\n    # Sorted inds are only used to infer the correct starting values.\n    param_vals = jnp.asarray(\n        [data.loc[inds, key].to_numpy() for inds in inds_of_comps]\n    )\n\n    # Set the value which the trainable parameter should take.\n    num_created_parameters = len(indices_per_param)\n    if init_val is not None:\n        if isinstance(init_val, float):\n            new_params = jnp.asarray([init_val] * num_created_parameters)\n        elif isinstance(init_val, list):\n            assert (\n                len(init_val) == num_created_parameters\n            ), f\"len(init_val)={len(init_val)}, but trying to create {num_created_parameters} parameters.\"\n            new_params = jnp.asarray(init_val)\n        else:\n            raise ValueError(\n                f\"init_val must a float, list, or None, but it is a {type(init_val).__name__}.\"\n            )\n    else:\n        new_params = jnp.mean(param_vals, axis=1)\n    self.base.trainable_params.append({key: new_params})\n    self.base.indices_set_by_trainables.append(indices_per_param)\n    self.base.num_trainable_params += num_created_parameters\n    if verbose:\n        print(\n            f\"Number of newly added trainable parameters: {num_created_parameters}. Total number of trainable parameters: {self.base.num_trainable_params}\"\n        )\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.move","title":"move(x=0.0, y=0.0, z=0.0, update_nodes=False)","text":"

Move cells or networks by adding to their (x, y, z) coordinates.

This function is used only for visualization. It does not affect the simulation.

Parameters:

Name Type Description Default x float

The amount to move in the x direction in um.

0.0 y float

The amount to move in the y direction in um.

0.0 z float

The amount to move in the z direction in um.

0.0 update_nodes bool

Whether .nodes should be updated or not. Setting this to False largely speeds up moving, especially for big networks, but .nodes or .show will not show the new xyz coordinates.

False Source code in jaxley/modules/base.py
def move(\n    self, x: float = 0.0, y: float = 0.0, z: float = 0.0, update_nodes: bool = False\n):\n    \"\"\"Move cells or networks by adding to their (x, y, z) coordinates.\n\n    This function is used only for visualization. It does not affect the simulation.\n\n    Args:\n        x: The amount to move in the x direction in um.\n        y: The amount to move in the y direction in um.\n        z: The amount to move in the z direction in um.\n        update_nodes: Whether `.nodes` should be updated or not. Setting this to\n            `False` largely speeds up moving, especially for big networks, but\n            `.nodes` or `.show` will not show the new xyz coordinates.\n    \"\"\"\n    for i in self._branches_in_view:\n        self.base.xyzr[i][:, :3] += np.array([x, y, z])\n    if update_nodes:\n        self.compute_compartment_centers()\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.move_to","title":"move_to(x=0.0, y=0.0, z=0.0, update_nodes=False)","text":"

Move cells or networks to a location (x, y, z).

If x, y, and z are floats, then the first compartment of the first branch of the first cell is moved to that float coordinate, and everything else is shifted by the difference between that compartment\u2019s previous coordinate and the new float location.

If x, y, and z are arrays, then they must each have a length equal to the number of cells being moved. Then the first compartment of the first branch of each cell is moved to the specified location.

Parameters:

Name Type Description Default update_nodes bool

Whether .nodes should be updated or not. Setting this to False largely speeds up moving, especially for big networks, but .nodes or .show will not show the new xyz coordinates.

False Source code in jaxley/modules/base.py
def move_to(\n    self,\n    x: Union[float, np.ndarray] = 0.0,\n    y: Union[float, np.ndarray] = 0.0,\n    z: Union[float, np.ndarray] = 0.0,\n    update_nodes: bool = False,\n):\n    \"\"\"Move cells or networks to a location (x, y, z).\n\n    If x, y, and z are floats, then the first compartment of the first branch\n    of the first cell is moved to that float coordinate, and everything else is\n    shifted by the difference between that compartment's previous coordinate and\n    the new float location.\n\n    If x, y, and z are arrays, then they must each have a length equal to the number\n    of cells being moved. Then the first compartment of the first branch of each\n    cell is moved to the specified location.\n\n    Args:\n        update_nodes: Whether `.nodes` should be updated or not. Setting this to\n            `False` largely speeds up moving, especially for big networks, but\n            `.nodes` or `.show` will not show the new xyz coordinates.\n    \"\"\"\n    # Test if any coordinate values are NaN which would greatly affect moving\n    if np.any(np.concatenate(self.xyzr, axis=0)[:, :3] == np.nan):\n        raise ValueError(\n            \"NaN coordinate values detected. Shift amounts cannot be computed. Please run compute_xyzr() or assign initial coordinate values.\"\n        )\n\n    # can only iterate over cells for networks\n    # lambda makes sure that generator can be created multiple times\n    base_is_net = self.base._current_view == \"network\"\n    cells = lambda: (self.cells if base_is_net else [self])\n\n    root_xyz_cells = np.array([c.xyzr[0][0, :3] for c in cells()])\n    root_xyz = root_xyz_cells[0] if isinstance(x, float) else root_xyz_cells\n    move_by = np.array([x, y, z]).T - root_xyz\n\n    if len(move_by.shape) == 1:\n        move_by = np.tile(move_by, (len(self._cells_in_view), 1))\n\n    for cell, offset in zip(cells(), move_by):\n        for idx in cell._branches_in_view:\n            self.base.xyzr[idx][:, :3] += offset\n    if update_nodes:\n        self.compute_compartment_centers()\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.rotate","title":"rotate(degrees, rotation_axis='xy', update_nodes=False)","text":"

Rotate jaxley modules clockwise. Used only for visualization.

This function is used only for visualization. It does not affect the simulation.

Parameters:

Name Type Description Default degrees float

How many degrees to rotate the module by.

required rotation_axis str

Either of {xy | xz | yz}.

'xy' Source code in jaxley/modules/base.py
def rotate(\n    self, degrees: float, rotation_axis: str = \"xy\", update_nodes: bool = False\n):\n    \"\"\"Rotate jaxley modules clockwise. Used only for visualization.\n\n    This function is used only for visualization. It does not affect the simulation.\n\n    Args:\n        degrees: How many degrees to rotate the module by.\n        rotation_axis: Either of {`xy` | `xz` | `yz`}.\n    \"\"\"\n    degrees = degrees / 180 * np.pi\n    if rotation_axis == \"xy\":\n        dims = [0, 1]\n    elif rotation_axis == \"xz\":\n        dims = [0, 2]\n    elif rotation_axis == \"yz\":\n        dims = [1, 2]\n    else:\n        raise ValueError\n\n    rotation_matrix = np.asarray(\n        [[np.cos(degrees), np.sin(degrees)], [-np.sin(degrees), np.cos(degrees)]]\n    )\n    for i in self._branches_in_view:\n        rot = np.dot(rotation_matrix, self.base.xyzr[i][:, dims].T).T\n        self.base.xyzr[i][:, dims] = rot\n    if update_nodes:\n        self.compute_compartment_centers()\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.scope","title":"scope(scope)","text":"

Return a View of the module with the specified scope.

For example cell.scope(\"global\").branch(2).scope(\"local\").comp(1) will return the 1st compartment of branch 2.

Parameters:

Name Type Description Default scope str

either \u201cglobal\u201d or \u201clocal\u201d.

required

Returns:

Type Description View

View with the specified scope.

Source code in jaxley/modules/base.py
def scope(self, scope: str) -> View:\n    \"\"\"Return a View of the module with the specified scope.\n\n    For example `cell.scope(\"global\").branch(2).scope(\"local\").comp(1)`\n    will return the 1st compartment of branch 2.\n\n    Args:\n        scope: either \"global\" or \"local\".\n\n    Returns:\n        View with the specified scope.\"\"\"\n    view = self.view\n    view.set_scope(scope)\n    return view\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.select","title":"select(nodes=None, edges=None, sorted=False)","text":"

Return View of the module filtered by specific node or edges indices.

Parameters:

Name Type Description Default nodes ndarray

indices of nodes to view. If None, all nodes are viewed.

None edges ndarray

indices of edges to view. If None, all edges are viewed.

None sorted bool

if True, nodes and edges are sorted.

False

Returns:

Type Description View

View for subset of selected nodes and/or edges.

Source code in jaxley/modules/base.py
def select(\n    self, nodes: np.ndarray = None, edges: np.ndarray = None, sorted: bool = False\n) -> View:\n    \"\"\"Return View of the module filtered by specific node or edges indices.\n\n    Args:\n        nodes: indices of nodes to view. If None, all nodes are viewed.\n        edges: indices of edges to view. If None, all edges are viewed.\n        sorted: if True, nodes and edges are sorted.\n\n    Returns:\n        View for subset of selected nodes and/or edges.\"\"\"\n\n    nodes = self._reformat_index(nodes) if nodes is not None else None\n    nodes = self._nodes_in_view if is_str_all(nodes) else nodes\n    nodes = np.sort(nodes) if sorted else nodes\n\n    edges = self._reformat_index(edges) if edges is not None else None\n    edges = self._edges_in_view if is_str_all(edges) else edges\n    edges = np.sort(edges) if sorted else edges\n\n    view = View(self, nodes, edges)\n    view._set_controlled_by_param(\"filter\")\n    return view\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.set","title":"set(key, val)","text":"

Set parameter of module (or its view) to a new value.

Note that this function can not be called within jax.jit or jax.grad. Instead, it should be used set the parameters of the module before the simulation. Use .data_set() to set parameters during jax.jit or jax.grad.

Parameters:

Name Type Description Default key str

The name of the parameter to set.

required val Union[float, ndarray]

The value to set the parameter to. If it is jnp.ndarray then it must be of shape (len(num_compartments)).

required Source code in jaxley/modules/base.py
def set(self, key: str, val: Union[float, jnp.ndarray]):\n    \"\"\"Set parameter of module (or its view) to a new value.\n\n    Note that this function can not be called within `jax.jit` or `jax.grad`.\n    Instead, it should be used set the parameters of the module **before** the\n    simulation. Use `.data_set()` to set parameters during `jax.jit` or\n    `jax.grad`.\n\n    Args:\n        key: The name of the parameter to set.\n        val: The value to set the parameter to. If it is `jnp.ndarray` then it\n            must be of shape `(len(num_compartments))`.\n    \"\"\"\n    if key in self.nodes.columns:\n        not_nan = ~self.nodes[key].isna().to_numpy()\n        self.base.nodes.loc[self._nodes_in_view[not_nan], key] = val\n    elif key in self.edges.columns:\n        not_nan = ~self.edges[key].isna().to_numpy()\n        self.base.edges.loc[self._edges_in_view[not_nan], key] = val\n    else:\n        raise KeyError(f\"Key '{key}' not found in nodes or edges\")\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.set_ncomp","title":"set_ncomp(ncomp, min_radius=None)","text":"

Set the number of compartments with which the branch is discretized.

Parameters:

Name Type Description Default ncomp int

The number of compartments that the branch should be discretized into.

required min_radius Optional[float]

Only used if the morphology was read from an SWC file. If passed the radius is capped to be at least this value.

None Source code in jaxley/modules/base.py
def set_ncomp(\n    self,\n    ncomp: int,\n    min_radius: Optional[float] = None,\n):\n    \"\"\"Set the number of compartments with which the branch is discretized.\n\n    Args:\n        ncomp: The number of compartments that the branch should be discretized\n            into.\n        min_radius: Only used if the morphology was read from an SWC file. If passed\n            the radius is capped to be at least this value.\n\n    Raises:\n        - When there are stimuli in any compartment in the module.\n        - When there are recordings in any compartment in the module.\n        - When the channels of the compartments are not the same within the branch\n        that is modified.\n        - When the lengths of the compartments are not the same within the branch\n        that is modified.\n        - Unless the morphology was read from an SWC file, when the radiuses of the\n        compartments are not the same within the branch that is modified.\n    \"\"\"\n    assert len(self.base.externals) == 0, \"No stimuli allowed!\"\n    assert len(self.base.recordings) == 0, \"No recordings allowed!\"\n    assert len(self.base.trainable_params) == 0, \"No trainables allowed!\"\n\n    assert self.base._module_type != \"network\", \"This is not allowed for networks.\"\n    assert not (\n        self.base._module_type == \"cell\"\n        and len(self._branches_in_view) == len(self.base._branches_in_view)\n    ), \"This is not allowed for cells.\"\n\n    # Update all attributes that are affected by compartment structure.\n    view = self.nodes.copy()\n    all_nodes = self.base.nodes\n    start_idx = self.nodes[\"global_comp_index\"].to_numpy()[0]\n    nseg_per_branch = self.base.nseg_per_branch\n    channel_names = [c._name for c in self.base.channels]\n    channel_param_names = list(\n        chain(*[c.channel_params for c in self.base.channels])\n    )\n    channel_state_names = list(\n        chain(*[c.channel_states for c in self.base.channels])\n    )\n    radius_generating_fns = self.base._radius_generating_fns\n\n    within_branch_radiuses = view[\"radius\"].to_numpy()\n    compartment_lengths = view[\"length\"].to_numpy()\n    num_previous_ncomp = len(within_branch_radiuses)\n    branch_indices = pd.unique(view[\"global_branch_index\"])\n\n    error_msg = lambda name: (\n        f\"You previously modified the {name} of individual compartments, but \"\n        f\"now you are modifying the number of compartments in this branch. \"\n        f\"This is not allowed. First build the morphology with `set_ncomp()` and \"\n        f\"then modify the radiuses and lengths of compartments.\"\n    )\n\n    if (\n        ~np.all(within_branch_radiuses == within_branch_radiuses[0])\n        and radius_generating_fns is None\n    ):\n        raise ValueError(error_msg(\"radius\"))\n\n    for property_name in [\"length\", \"capacitance\", \"axial_resistivity\"]:\n        compartment_properties = view[property_name].to_numpy()\n        if ~np.all(compartment_properties == compartment_properties[0]):\n            raise ValueError(error_msg(property_name))\n\n    if not (self.nodes[channel_names].var() == 0.0).all():\n        raise ValueError(\n            \"Some channel exists only in some compartments of the branch which you\"\n            \"are trying to modify. This is not allowed. First specify the number\"\n            \"of compartments with `.set_ncomp()` and then insert the channels\"\n            \"accordingly.\"\n        )\n\n    if not (\n        self.nodes[channel_param_names + channel_state_names].var() == 0.0\n    ).all():\n        raise ValueError(\n            \"Some channel has different parameters or states between the \"\n            \"different compartments of the branch which you are trying to modify. \"\n            \"This is not allowed. First specify the number of compartments with \"\n            \"`.set_ncomp()` and then insert the channels accordingly.\"\n        )\n\n    # Add new rows as the average of all rows. Special case for the length is below.\n    average_row = self.nodes.mean(skipna=False)\n    average_row = average_row.to_frame().T\n    view = pd.concat([*[average_row] * ncomp], axis=\"rows\")\n\n    # Set the correct datatype after having performed an average which cast\n    # everything to float.\n    integer_cols = [\"global_cell_index\", \"global_branch_index\", \"global_comp_index\"]\n    view[integer_cols] = view[integer_cols].astype(int)\n\n    # Whether or not a channel exists in a compartment is a boolean.\n    boolean_cols = channel_names\n    view[boolean_cols] = view[boolean_cols].astype(bool)\n\n    # Special treatment for the lengths and radiuses. These are not being set as\n    # the average because we:\n    # 1) Want to maintain the total length of a branch.\n    # 2) Want to use the SWC inferred radius.\n    #\n    # Compute new compartment lengths.\n    comp_lengths = np.sum(compartment_lengths) / ncomp\n    view[\"length\"] = comp_lengths\n\n    # Compute new compartment radiuses.\n    if radius_generating_fns is not None:\n        view[\"radius\"] = build_radiuses_from_xyzr(\n            radius_fns=radius_generating_fns,\n            branch_indices=branch_indices,\n            min_radius=min_radius,\n            nseg=ncomp,\n        )\n    else:\n        view[\"radius\"] = within_branch_radiuses[0] * np.ones(ncomp)\n\n    # Update `.nodes`.\n    # 1) Delete N rows starting from start_idx\n    number_deleted = num_previous_ncomp\n    all_nodes = all_nodes.drop(index=range(start_idx, start_idx + number_deleted))\n\n    # 2) Insert M new rows at the same location\n    df1 = all_nodes.iloc[:start_idx]  # Rows before the insertion point\n    df2 = all_nodes.iloc[start_idx:]  # Rows after the insertion point\n\n    # 3) Combine the parts: before, new rows, and after\n    all_nodes = pd.concat([df1, view, df2]).reset_index(drop=True)\n\n    # Override `comp_index` to just be a consecutive list.\n    all_nodes[\"global_comp_index\"] = np.arange(len(all_nodes))\n\n    # Update compartment structure arguments.\n    nseg_per_branch[branch_indices] = ncomp\n    nseg = int(np.max(nseg_per_branch))\n    cumsum_nseg = cumsum_leading_zero(nseg_per_branch)\n    internal_node_inds = np.arange(cumsum_nseg[-1])\n\n    self.base.nodes = all_nodes\n    self.base.nseg_per_branch = nseg_per_branch\n    self.base.nseg = nseg\n    self.base.cumsum_nseg = cumsum_nseg\n    self.base._internal_node_inds = internal_node_inds\n\n    # Update the morphology indexing (e.g., `.comp_edges`).\n    self.base._initialize()\n    self.base._init_view()\n    self.base._update_local_indices()\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.set_scope","title":"set_scope(scope)","text":"

Toggle between \u201cglobal\u201d or \u201clocal\u201d scope.

Determines if global or local indices are used for viewing the module.

Parameters:

Name Type Description Default scope str

either \u201cglobal\u201d or \u201clocal\u201d.

required Source code in jaxley/modules/base.py
def set_scope(self, scope: str):\n    \"\"\"Toggle between \"global\" or \"local\" scope.\n\n    Determines if global or local indices are used for viewing the module.\n\n    Args:\n        scope: either \"global\" or \"local\".\"\"\"\n    assert scope in [\"global\", \"local\"], \"Invalid scope.\"\n    self._scope = scope\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.show","title":"show(param_names=None, *, indices=True, params=True, states=True, channel_names=None)","text":"

Print detailed information about the Module or a view of it.

Parameters:

Name Type Description Default param_names Optional[Union[str, List[str]]]

The names of the parameters to show. If None, all parameters are shown.

None indices bool

Whether to show the indices of the compartments.

True params bool

Whether to show the parameters of the compartments.

True states bool

Whether to show the states of the compartments.

True channel_names Optional[List[str]]

The names of the channels to show. If None, all channels are shown.

None

Returns:

Type Description DataFrame

A pd.DataFrame with the requested information.

Source code in jaxley/modules/base.py
def show(\n    self,\n    param_names: Optional[Union[str, List[str]]] = None,\n    *,\n    indices: bool = True,\n    params: bool = True,\n    states: bool = True,\n    channel_names: Optional[List[str]] = None,\n) -> pd.DataFrame:\n    \"\"\"Print detailed information about the Module or a view of it.\n\n    Args:\n        param_names: The names of the parameters to show. If `None`, all parameters\n            are shown.\n        indices: Whether to show the indices of the compartments.\n        params: Whether to show the parameters of the compartments.\n        states: Whether to show the states of the compartments.\n        channel_names: The names of the channels to show. If `None`, all channels are\n            shown.\n\n    Returns:\n        A `pd.DataFrame` with the requested information.\n    \"\"\"\n    nodes = self.nodes.copy()  # prevents this from being edited\n\n    cols = []\n    inds = [\"comp_index\", \"branch_index\", \"cell_index\"]\n    scopes = [\"local\", \"global\"]\n    inds = [f\"{s}_{i}\" for i in inds for s in scopes] if indices else []\n    cols += inds\n    cols += [ch._name for ch in self.channels] if channel_names else []\n    cols += (\n        sum([list(ch.channel_params) for ch in self.channels], []) if params else []\n    )\n    cols += (\n        sum([list(ch.channel_states) for ch in self.channels], []) if states else []\n    )\n\n    if not param_names is None:\n        cols = (\n            inds + [c for c in cols if c in param_names]\n            if params\n            else list(param_names)\n        )\n\n    return nodes[cols]\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.step","title":"step(u, delta_t, external_inds, externals, params, solver='bwd_euler', voltage_solver='jaxley.stone')","text":"

One step of solving the Ordinary Differential Equation.

This function is called inside of integrate and increments the state of the module by one time step. Calls _step_channels and _step_synapse to update the states of the channels and synapses using fwd_euler.

Parameters:

Name Type Description Default u Dict[str, ndarray]

The state of the module. voltages = u[\u201cv\u201d]

required delta_t float

The time step.

required external_inds Dict[str, ndarray]

The indices of the external inputs.

required externals Dict[str, ndarray]

The external inputs.

required params Dict[str, ndarray]

The parameters of the module.

required solver str

The solver to use for the voltages. Either of [\u201cbwd_euler\u201d, \u201cfwd_euler\u201d, \u201ccrank_nicolson\u201d].

'bwd_euler' voltage_solver str

The tridiagonal solver used to diagonalize the coefficient matrix of the ODE system. Either of [\u201cjaxley.thomas\u201d, \u201cjaxley.stone\u201d].

'jaxley.stone'

Returns:

Type Description Dict[str, ndarray]

The updated state of the module.

Source code in jaxley/modules/base.py
@only_allow_module\ndef step(\n    self,\n    u: Dict[str, jnp.ndarray],\n    delta_t: float,\n    external_inds: Dict[str, jnp.ndarray],\n    externals: Dict[str, jnp.ndarray],\n    params: Dict[str, jnp.ndarray],\n    solver: str = \"bwd_euler\",\n    voltage_solver: str = \"jaxley.stone\",\n) -> Dict[str, jnp.ndarray]:\n    \"\"\"One step of solving the Ordinary Differential Equation.\n\n    This function is called inside of `integrate` and increments the state of the\n    module by one time step. Calls `_step_channels` and `_step_synapse` to update\n    the states of the channels and synapses using fwd_euler.\n\n    Args:\n        u: The state of the module. voltages = u[\"v\"]\n        delta_t: The time step.\n        external_inds: The indices of the external inputs.\n        externals: The external inputs.\n        params: The parameters of the module.\n        solver: The solver to use for the voltages. Either of [\"bwd_euler\",\n            \"fwd_euler\", \"crank_nicolson\"].\n        voltage_solver: The tridiagonal solver used to diagonalize the\n            coefficient matrix of the ODE system. Either of [\"jaxley.thomas\",\n            \"jaxley.stone\"].\n\n    Returns:\n        The updated state of the module.\n    \"\"\"\n\n    # Extract the voltages\n    voltages = u[\"v\"]\n\n    # Extract the external inputs\n    if \"i\" in externals.keys():\n        i_current = externals[\"i\"]\n        i_inds = external_inds[\"i\"]\n        i_ext = self._get_external_input(\n            voltages, i_inds, i_current, params[\"radius\"], params[\"length\"]\n        )\n    else:\n        i_ext = 0.0\n\n    # Step of the channels.\n    u, (v_terms, const_terms) = self._step_channels(\n        u, delta_t, self.channels, self.nodes, params\n    )\n\n    # Step of the synapse.\n    u, (syn_v_terms, syn_const_terms) = self._step_synapse(\n        u,\n        self.synapses,\n        params,\n        delta_t,\n        self.edges,\n    )\n\n    # Clamp for channels and synapses.\n    for key in externals.keys():\n        if key not in [\"i\", \"v\"]:\n            u[key] = u[key].at[external_inds[key]].set(externals[key])\n\n    # Voltage steps.\n    cm = params[\"capacitance\"]  # Abbreviation.\n\n    # Arguments used by all solvers.\n    solver_kwargs = {\n        \"voltages\": voltages,\n        \"voltage_terms\": (v_terms + syn_v_terms) / cm,\n        \"constant_terms\": (const_terms + i_ext + syn_const_terms) / cm,\n        \"axial_conductances\": params[\"axial_conductances\"],\n        \"internal_node_inds\": self._internal_node_inds,\n    }\n\n    # Add solver specific arguments.\n    if voltage_solver == \"jax.sparse\":\n        solver_kwargs.update(\n            {\n                \"sinks\": np.asarray(self._comp_edges[\"sink\"].to_list()),\n                \"data_inds\": self._data_inds,\n                \"indices\": self._indices_jax_spsolve,\n                \"indptr\": self._indptr_jax_spsolve,\n                \"n_nodes\": self._n_nodes,\n            }\n        )\n        # Only for `bwd_euler` and `cranck-nicolson`.\n        step_voltage_implicit = step_voltage_implicit_with_jax_spsolve\n    else:\n        # Our custom sparse solver requires a different format of all conductance\n        # values to perform triangulation and backsubstution optimally.\n        #\n        # Currently, the forward Euler solver also uses this format. However,\n        # this is only for historical reasons and we are planning to change this in\n        # the future.\n        solver_kwargs.update(\n            {\n                \"sinks\": np.asarray(self._comp_edges[\"sink\"].to_list()),\n                \"sources\": np.asarray(self._comp_edges[\"source\"].to_list()),\n                \"types\": np.asarray(self._comp_edges[\"type\"].to_list()),\n                \"nseg_per_branch\": self.nseg_per_branch,\n                \"par_inds\": self._par_inds,\n                \"child_inds\": self._child_inds,\n                \"nbranches\": self.total_nbranches,\n                \"solver\": voltage_solver,\n                \"idx\": self._solve_indexer,\n                \"debug_states\": self.debug_states,\n            }\n        )\n        # Only for `bwd_euler` and `cranck-nicolson`.\n        step_voltage_implicit = step_voltage_implicit_with_jaxley_spsolve\n\n    if solver == \"bwd_euler\":\n        u[\"v\"] = step_voltage_implicit(**solver_kwargs, delta_t=delta_t)\n    elif solver == \"crank_nicolson\":\n        # Crank-Nicolson advances by half a step of backward and half a step of\n        # forward Euler.\n        half_step_delta_t = delta_t / 2\n        half_step_voltages = step_voltage_implicit(\n            **solver_kwargs, delta_t=half_step_delta_t\n        )\n        # The forward Euler step in Crank-Nicolson can be performed easily as\n        # `V_{n+1} = 2 * V_{n+1/2} - V_n`. See also NEURON book Chapter 4.\n        u[\"v\"] = 2 * half_step_voltages - voltages\n    elif solver == \"fwd_euler\":\n        u[\"v\"] = step_voltage_explicit(**solver_kwargs, delta_t=delta_t)\n    else:\n        raise ValueError(\n            f\"You specified `solver={solver}`. The only allowed solvers are \"\n            \"['bwd_euler', 'fwd_euler', 'crank_nicolson'].\"\n        )\n\n    # Clamp for voltages.\n    if \"v\" in externals.keys():\n        u[\"v\"] = u[\"v\"].at[external_inds[\"v\"]].set(externals[\"v\"])\n\n    return u\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.stimulate","title":"stimulate(current=None, verbose=True)","text":"

Insert a stimulus into the compartment.

current must be a 1d array or have batch dimension of size (num_compartments, ) or (1, ). If 1d, the same stimulus is added to all compartments.

This function cannot be run during jax.jit and jax.grad. Because of this, it should only be used for static stimuli (i.e., stimuli that do not depend on the data and that should not be learned). For stimuli that depend on data (or that should be learned), please use data_stimulate().

Parameters:

Name Type Description Default current Optional[ndarray]

Current in nA.

None Source code in jaxley/modules/base.py
def stimulate(self, current: Optional[jnp.ndarray] = None, verbose: bool = True):\n    \"\"\"Insert a stimulus into the compartment.\n\n    current must be a 1d array or have batch dimension of size `(num_compartments, )`\n    or `(1, )`. If 1d, the same stimulus is added to all compartments.\n\n    This function cannot be run during `jax.jit` and `jax.grad`. Because of this,\n    it should only be used for static stimuli (i.e., stimuli that do not depend\n    on the data and that should not be learned). For stimuli that depend on data\n    (or that should be learned), please use `data_stimulate()`.\n\n    Args:\n        current: Current in `nA`.\n    \"\"\"\n    self._external_input(\"i\", current, verbose=verbose)\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.to_jax","title":"to_jax()","text":"

Move .nodes to .jaxnodes.

Before the actual simulation is run (via jx.integrate), all parameters of the jx.Module are stored in .nodes (a pd.DataFrame). However, for simulation, these parameters have to be moved to be jnp.ndarrays such that they can be processed on GPU/TPU and such that the simulation can be differentiated. .to_jax() copies the .nodes to .jaxnodes.

Source code in jaxley/modules/base.py
@only_allow_module\ndef to_jax(self):\n    # TODO FROM #447: Make this work for View?\n    \"\"\"Move `.nodes` to `.jaxnodes`.\n\n    Before the actual simulation is run (via `jx.integrate`), all parameters of\n    the `jx.Module` are stored in `.nodes` (a `pd.DataFrame`). However, for\n    simulation, these parameters have to be moved to be `jnp.ndarrays` such that\n    they can be processed on GPU/TPU and such that the simulation can be\n    differentiated. `.to_jax()` copies the `.nodes` to `.jaxnodes`.\n    \"\"\"\n    self.base.jaxnodes = {}\n    for key, value in self.base.nodes.to_dict(orient=\"list\").items():\n        inds = jnp.arange(len(value))\n        self.base.jaxnodes[key] = jnp.asarray(value)[inds]\n\n    # `jaxedges` contains only parameters (no indices).\n    # `jaxedges` contains only non-Nan elements. This is unlike the channels where\n    # we allow parameter sharing.\n    self.base.jaxedges = {}\n    edges = self.base.edges.to_dict(orient=\"list\")\n    for i, synapse in enumerate(self.base.synapses):\n        condition = np.asarray(edges[\"type_ind\"]) == i\n        for key in synapse.synapse_params:\n            self.base.jaxedges[key] = jnp.asarray(np.asarray(edges[key])[condition])\n        for key in synapse.synapse_states:\n            self.base.jaxedges[key] = jnp.asarray(np.asarray(edges[key])[condition])\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.vis","title":"vis(ax=None, col='k', dims=(0, 1), type='line', morph_plot_kwargs={})","text":"

Visualize the module.

Modules can be visualized on one of the cardinal planes (xy, xz, yz) or even in 3D.

Several options are available: - line: All points from the traced morphology (xyzr), are connected with a line plot. - scatter: All traced points, are plotted as scatter points. - comp: Plots the compartmentalized morphology, including radius and shape. (shows the true compartment lengths per default, but this can be changed via the morph_plot_kwargs, for details see jaxley.utils.plot_utils.plot_comps). - morph: Reconstructs the 3D shape of the traced morphology. For details see jaxley.utils.plot_utils.plot_morph. Warning: For 3D plots and morphologies with many traced points this can be very slow.

Parameters:

Name Type Description Default ax Optional[Axes]

An axis into which to plot.

None col str

The color for all branches.

'k' dims Tuple[int]

Which dimensions to plot. 1=x, 2=y, 3=z coordinate. Must be a tuple of two of them.

(0, 1) type str

The type of plot. One of [\u201cline\u201d, \u201cscatter\u201d, \u201ccomp\u201d, \u201cmorph\u201d].

'line' morph_plot_kwargs Dict

Keyword arguments passed to the plotting function.

{} Source code in jaxley/modules/base.py
def vis(\n    self,\n    ax: Optional[Axes] = None,\n    col: str = \"k\",\n    dims: Tuple[int] = (0, 1),\n    type: str = \"line\",\n    morph_plot_kwargs: Dict = {},\n) -> Axes:\n    \"\"\"Visualize the module.\n\n    Modules can be visualized on one of the cardinal planes (xy, xz, yz) or\n    even in 3D.\n\n    Several options are available:\n    - `line`: All points from the traced morphology (`xyzr`), are connected\n    with a line plot.\n    - `scatter`: All traced points, are plotted as scatter points.\n    - `comp`: Plots the compartmentalized morphology, including radius\n    and shape. (shows the true compartment lengths per default, but this can\n    be changed via the `morph_plot_kwargs`, for details see\n    `jaxley.utils.plot_utils.plot_comps`).\n    - `morph`: Reconstructs the 3D shape of the traced morphology. For details see\n    `jaxley.utils.plot_utils.plot_morph`. Warning: For 3D plots and morphologies\n    with many traced points this can be very slow.\n\n    Args:\n        ax: An axis into which to plot.\n        col: The color for all branches.\n        dims: Which dimensions to plot. 1=x, 2=y, 3=z coordinate. Must be a tuple of\n            two of them.\n        type: The type of plot. One of [\"line\", \"scatter\", \"comp\", \"morph\"].\n        morph_plot_kwargs: Keyword arguments passed to the plotting function.\n    \"\"\"\n    if \"comp\" in type.lower():\n        return plot_comps(self, dims=dims, ax=ax, col=col, **morph_plot_kwargs)\n    if \"morph\" in type.lower():\n        return plot_morph(self, dims=dims, ax=ax, col=col, **morph_plot_kwargs)\n\n    assert not np.any(\n        [np.isnan(xyzr[:, dims]).all() for xyzr in self.xyzr]\n    ), \"No coordinates available. Use `vis(detail='point')` or run `.compute_xyz()` before running `.vis()`.\"\n\n    ax = plot_graph(\n        self.xyzr,\n        dims=dims,\n        col=col,\n        ax=ax,\n        type=type,\n        morph_plot_kwargs=morph_plot_kwargs,\n    )\n\n    return ax\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.write_trainables","title":"write_trainables(trainable_params)","text":"

Write the trainables into .nodes and .edges.

This allows to, e.g., visualize trained networks with .vis().

Parameters:

Name Type Description Default trainable_params List[Dict[str, ndarray]]

The trainable parameters returned by get_parameters().

required Source code in jaxley/modules/base.py
def write_trainables(self, trainable_params: List[Dict[str, jnp.ndarray]]):\n    \"\"\"Write the trainables into `.nodes` and `.edges`.\n\n    This allows to, e.g., visualize trained networks with `.vis()`.\n\n    Args:\n        trainable_params: The trainable parameters returned by `get_parameters()`.\n    \"\"\"\n    # We do not support views. Why? `jaxedges` does not have any NaN\n    # elements, whereas edges does. Because of this, we already need special\n    # treatment to make this function work, and it would be an even bigger hassle\n    # if we wanted to support this.\n    assert self.__class__.__name__ in [\n        \"Compartment\",\n        \"Branch\",\n        \"Cell\",\n        \"Network\",\n    ], \"Only supports modules.\"\n\n    # We could also implement this without casting the module to jax.\n    # However, I think it allows us to reuse as much code as possible and it avoids\n    # any kind of issues with indexing or parameter sharing (as this is fully\n    # taken care of by `get_all_parameters()`).\n    self.base.to_jax()\n    pstate = params_to_pstate(trainable_params, self.base.indices_set_by_trainables)\n    all_params = self.base.get_all_parameters(pstate, voltage_solver=\"jaxley.stone\")\n\n    # The value for `delta_t` does not matter here because it is only used to\n    # compute the initial current. However, the initial current cannot be made\n    # trainable and so its value never gets used below.\n    all_states = self.base.get_all_states(pstate, all_params, delta_t=0.025)\n\n    # Loop only over the keys in `pstate` to avoid unnecessary computation.\n    for parameter in pstate:\n        key = parameter[\"key\"]\n        if key in self.base.nodes.columns:\n            vals_to_set = all_params if key in all_params.keys() else all_states\n            self.base.nodes[key] = vals_to_set[key]\n\n    # `jaxedges` contains only non-Nan elements. This is unlike the channels where\n    # we allow parameter sharing.\n    edges = self.base.edges.to_dict(orient=\"list\")\n    for i, synapse in enumerate(self.base.synapses):\n        condition = np.asarray(edges[\"type_ind\"]) == i\n        for key in list(synapse.synapse_params.keys()):\n            self.base.edges.loc[condition, key] = all_params[key]\n        for key in list(synapse.synapse_states.keys()):\n            self.base.edges.loc[condition, key] = all_states[key]\n
"},{"location":"reference/modules/#compartment","title":"Compartment","text":"

Bases: Module

Compartment class.

This class defines a single compartment that can be simulated by itself or connected up into branches. It is the basic building block of a neuron model.

Source code in jaxley/modules/compartment.py
class Compartment(Module):\n    \"\"\"Compartment class.\n\n    This class defines a single compartment that can be simulated by itself or\n    connected up into branches. It is the basic building block of a neuron model.\n    \"\"\"\n\n    compartment_params: Dict = {\n        \"length\": 10.0,  # um\n        \"radius\": 1.0,  # um\n        \"axial_resistivity\": 5_000.0,  # ohm cm\n        \"capacitance\": 1.0,  # uF/cm^2\n    }\n    compartment_states: Dict = {\"v\": -70.0}\n\n    def __init__(self):\n        super().__init__()\n\n        self.nseg = 1\n        self.nseg_per_branch = np.asarray([1])\n        self.total_nbranches = 1\n        self.nbranches_per_cell = [1]\n        self._cumsum_nbranches = np.asarray([0, 1])\n        self.cumsum_nseg = cumsum_leading_zero(self.nseg_per_branch)\n\n        # Setting up the `nodes` for indexing.\n        self.nodes = pd.DataFrame(\n            dict(global_cell_index=[0], global_branch_index=[0], global_comp_index=[0])\n        )\n        self._append_params_and_states(self.compartment_params, self.compartment_states)\n        self._update_local_indices()\n        self._init_view()\n\n        # Synapses.\n        self.branch_edges = pd.DataFrame(\n            dict(parent_branch_index=[], child_branch_index=[])\n        )\n\n        # For morphology indexing.\n        self._par_inds, self._child_inds, self._child_belongs_to_branchpoint = (\n            compute_children_and_parents(self.branch_edges)\n        )\n        self._internal_node_inds = jnp.asarray([0])\n\n        # Initialize the module.\n        self._initialize()\n\n        # Coordinates.\n        self.xyzr = [float(\"NaN\") * np.zeros((2, 4))]\n\n    def _init_morph_jaxley_spsolve(self):\n        self._solve_indexer = JaxleySolveIndexer(\n            cumsum_nseg=self.cumsum_nseg,\n            branchpoint_group_inds=np.asarray([]).astype(int),\n            children_in_level=[],\n            parents_in_level=[],\n            root_inds=np.asarray([0]),\n            remapped_node_indices=self._internal_node_inds,\n        )\n\n    def _init_morph_jax_spsolve(self):\n        \"\"\"Initialize morphology for the jax sparse voltage solver.\n\n        Explanation of `self._comp_eges['type']`:\n        `type == 0`: compartment <--> compartment (within branch)\n        `type == 1`: branchpoint --> parent-compartment\n        `type == 2`: branchpoint --> child-compartment\n        `type == 3`: parent-compartment --> branchpoint\n        `type == 4`: child-compartment --> branchpoint\n        \"\"\"\n        self._comp_edges = pd.DataFrame().from_dict(\n            {\"source\": [], \"sink\": [], \"type\": []}\n        )\n        n_nodes, data_inds, indices, indptr = comp_edges_to_indices(self._comp_edges)\n        self._n_nodes = n_nodes\n        self._data_inds = data_inds\n        self._indices_jax_spsolve = indices\n        self._indptr_jax_spsolve = indptr\n
"},{"location":"reference/modules/#branch","title":"Branch","text":"

Bases: Module

Branch class.

This class defines a single branch that can be simulated by itself or connected to build a cell. A branch is linear segment of several compartments and can be connected to no, one or more other branches at each end to build more intricate cell morphologies.

Source code in jaxley/modules/branch.py
class Branch(Module):\n    \"\"\"Branch class.\n\n    This class defines a single branch that can be simulated by itself or\n    connected to build a cell. A branch is linear segment of several compartments\n    and can be connected to no, one or more other branches at each end to build more\n    intricate cell morphologies.\n    \"\"\"\n\n    branch_params: Dict = {}\n    branch_states: Dict = {}\n\n    def __init__(\n        self,\n        compartments: Optional[Union[Compartment, List[Compartment]]] = None,\n        nseg: Optional[int] = None,\n    ):\n        \"\"\"\n        Args:\n            compartments: A single compartment or a list of compartments that make up the\n                branch.\n            nseg: Number of segments to divide the branch into. If `compartments` is an\n                a single compartment, than the compartment is repeated `nseg` times to\n                create the branch.\n        \"\"\"\n        super().__init__()\n        assert (\n            isinstance(compartments, (Compartment, List)) or compartments is None\n        ), \"Only Compartment or List[Compartment] is allowed.\"\n        if isinstance(compartments, Compartment):\n            assert (\n                nseg is not None\n            ), \"If `compartments` is not a list then you have to set `nseg`.\"\n        compartments = Compartment() if compartments is None else compartments\n        nseg = 1 if nseg is None else nseg\n\n        if isinstance(compartments, Compartment):\n            compartment_list = [compartments] * nseg\n        else:\n            compartment_list = compartments\n\n        self.nseg = len(compartment_list)\n        self.nseg_per_branch = np.asarray([self.nseg])\n        self.total_nbranches = 1\n        self.nbranches_per_cell = [1]\n        self._cumsum_nbranches = jnp.asarray([0, 1])\n        self.cumsum_nseg = cumsum_leading_zero(self.nseg_per_branch)\n\n        # Indexing.\n        self.nodes = pd.concat([c.nodes for c in compartment_list], ignore_index=True)\n        self._append_params_and_states(self.branch_params, self.branch_states)\n        self.nodes[\"global_comp_index\"] = np.arange(self.nseg).tolist()\n        self.nodes[\"global_branch_index\"] = [0] * self.nseg\n        self.nodes[\"global_cell_index\"] = [0] * self.nseg\n        self._update_local_indices()\n        self._init_view()\n\n        # Channels.\n        self._gather_channels_from_constituents(compartment_list)\n\n        self.branch_edges = pd.DataFrame(\n            dict(parent_branch_index=[], child_branch_index=[])\n        )\n\n        # For morphology indexing.\n        self._par_inds, self._child_inds, self._child_belongs_to_branchpoint = (\n            compute_children_and_parents(self.branch_edges)\n        )\n        self._internal_node_inds = jnp.arange(self.nseg)\n\n        self._initialize()\n\n        # Coordinates.\n        self.xyzr = [float(\"NaN\") * np.zeros((2, 4))]\n\n    def _init_morph_jaxley_spsolve(self):\n        self._solve_indexer = JaxleySolveIndexer(\n            cumsum_nseg=self.cumsum_nseg,\n            branchpoint_group_inds=np.asarray([]).astype(int),\n            remapped_node_indices=self._internal_node_inds,\n            children_in_level=[],\n            parents_in_level=[],\n            root_inds=np.asarray([0]),\n        )\n\n    def _init_morph_jax_spsolve(self):\n        \"\"\"Initialize morphology for the jax sparse voltage solver.\n\n        Explanation of `self._comp_eges['type']`:\n        `type == 0`: compartment <--> compartment (within branch)\n        `type == 1`: branchpoint --> parent-compartment\n        `type == 2`: branchpoint --> child-compartment\n        `type == 3`: parent-compartment --> branchpoint\n        `type == 4`: child-compartment --> branchpoint\n        \"\"\"\n        self._comp_edges = pd.DataFrame().from_dict(\n            {\n                \"source\": list(range(self.nseg - 1)) + list(range(1, self.nseg)),\n                \"sink\": list(range(1, self.nseg)) + list(range(self.nseg - 1)),\n            }\n        )\n        self._comp_edges[\"type\"] = 0\n        n_nodes, data_inds, indices, indptr = comp_edges_to_indices(self._comp_edges)\n        self._n_nodes = n_nodes\n        self._data_inds = data_inds\n        self._indices_jax_spsolve = indices\n        self._indptr_jax_spsolve = indptr\n\n    def __len__(self) -> int:\n        return self.nseg\n
"},{"location":"reference/modules/#jaxley.modules.branch.Branch.__init__","title":"__init__(compartments=None, nseg=None)","text":"

Parameters:

Name Type Description Default compartments Optional[Union[Compartment, List[Compartment]]]

A single compartment or a list of compartments that make up the branch.

None nseg Optional[int]

Number of segments to divide the branch into. If compartments is an a single compartment, than the compartment is repeated nseg times to create the branch.

None Source code in jaxley/modules/branch.py
def __init__(\n    self,\n    compartments: Optional[Union[Compartment, List[Compartment]]] = None,\n    nseg: Optional[int] = None,\n):\n    \"\"\"\n    Args:\n        compartments: A single compartment or a list of compartments that make up the\n            branch.\n        nseg: Number of segments to divide the branch into. If `compartments` is an\n            a single compartment, than the compartment is repeated `nseg` times to\n            create the branch.\n    \"\"\"\n    super().__init__()\n    assert (\n        isinstance(compartments, (Compartment, List)) or compartments is None\n    ), \"Only Compartment or List[Compartment] is allowed.\"\n    if isinstance(compartments, Compartment):\n        assert (\n            nseg is not None\n        ), \"If `compartments` is not a list then you have to set `nseg`.\"\n    compartments = Compartment() if compartments is None else compartments\n    nseg = 1 if nseg is None else nseg\n\n    if isinstance(compartments, Compartment):\n        compartment_list = [compartments] * nseg\n    else:\n        compartment_list = compartments\n\n    self.nseg = len(compartment_list)\n    self.nseg_per_branch = np.asarray([self.nseg])\n    self.total_nbranches = 1\n    self.nbranches_per_cell = [1]\n    self._cumsum_nbranches = jnp.asarray([0, 1])\n    self.cumsum_nseg = cumsum_leading_zero(self.nseg_per_branch)\n\n    # Indexing.\n    self.nodes = pd.concat([c.nodes for c in compartment_list], ignore_index=True)\n    self._append_params_and_states(self.branch_params, self.branch_states)\n    self.nodes[\"global_comp_index\"] = np.arange(self.nseg).tolist()\n    self.nodes[\"global_branch_index\"] = [0] * self.nseg\n    self.nodes[\"global_cell_index\"] = [0] * self.nseg\n    self._update_local_indices()\n    self._init_view()\n\n    # Channels.\n    self._gather_channels_from_constituents(compartment_list)\n\n    self.branch_edges = pd.DataFrame(\n        dict(parent_branch_index=[], child_branch_index=[])\n    )\n\n    # For morphology indexing.\n    self._par_inds, self._child_inds, self._child_belongs_to_branchpoint = (\n        compute_children_and_parents(self.branch_edges)\n    )\n    self._internal_node_inds = jnp.arange(self.nseg)\n\n    self._initialize()\n\n    # Coordinates.\n    self.xyzr = [float(\"NaN\") * np.zeros((2, 4))]\n
"},{"location":"reference/modules/#cell","title":"Cell","text":"

Bases: Module

Cell class.

This class defines a single cell that can be simulated by itself or connected with synapses to build a network. A cell is made up of several branches and supports intricate cell morphologies.

Source code in jaxley/modules/cell.py
class Cell(Module):\n    \"\"\"Cell class.\n\n    This class defines a single cell that can be simulated by itself or\n    connected with synapses to build a network. A cell is made up of several branches\n    and supports intricate cell morphologies.\n    \"\"\"\n\n    cell_params: Dict = {}\n    cell_states: Dict = {}\n\n    def __init__(\n        self,\n        branches: Optional[Union[Branch, List[Branch]]] = None,\n        parents: Optional[List[int]] = None,\n        xyzr: Optional[List[np.ndarray]] = None,\n    ):\n        \"\"\"Initialize a cell.\n\n        Args:\n            branches: A single branch or a list of branches that make up the cell.\n                If a single branch is provided, then the branch is repeated `len(parents)`\n                times to create the cell.\n            parents: The parent branch index for each branch. The first branch has no\n                parent and is therefore set to -1.\n            xyzr: For every branch, the x, y, and z coordinates and the radius at the\n                traced coordinates. Note that this is the full tracing (from SWC), not\n                the stick representation coordinates.\n        \"\"\"\n        super().__init__()\n        assert (\n            isinstance(branches, (Branch, List)) or branches is None\n        ), \"Only Branch or List[Branch] is allowed.\"\n        if branches is not None:\n            assert (\n                parents is not None\n            ), \"If `branches` is not a list then you have to set `parents`.\"\n        if isinstance(branches, List):\n            assert len(parents) == len(\n                branches\n            ), \"Ensure equally many parents, i.e. len(branches) == len(parents).\"\n\n        branches = Branch() if branches is None else branches\n        parents = [-1] if parents is None else parents\n\n        if isinstance(branches, Branch):\n            branch_list = [branches for _ in range(len(parents))]\n        else:\n            branch_list = branches\n\n        if xyzr is not None:\n            assert len(xyzr) == len(parents)\n            self.xyzr = xyzr\n        else:\n            # For every branch (`len(parents)`), we have a start and end point (`2`) and\n            # a (x,y,z,r) coordinate for each of them (`4`).\n            # Since `xyzr` is only inspected at `.vis()` and because it depends on the\n            # (potentially learned) length of every compartment, we only populate\n            # self.xyzr at `.vis()`.\n            self.xyzr = [float(\"NaN\") * np.zeros((2, 4)) for _ in range(len(parents))]\n\n        self.total_nbranches = len(branch_list)\n        self.nbranches_per_cell = [len(branch_list)]\n        self.comb_parents = jnp.asarray(parents)\n        self.comb_children = compute_children_indices(self.comb_parents)\n        self._cumsum_nbranches = np.asarray([0, len(branch_list)])\n\n        # Compartment structure. These arguments have to be rebuilt when `.set_ncomp()`\n        # is run.\n        self.nseg_per_branch = np.asarray([branch.nseg for branch in branch_list])\n        self.nseg = int(np.max(self.nseg_per_branch))\n        self.cumsum_nseg = cumsum_leading_zero(self.nseg_per_branch)\n        self._internal_node_inds = np.arange(self.cumsum_nseg[-1])\n\n        # Build nodes. Has to be changed when `.set_ncomp()` is run.\n        self.nodes = pd.concat([c.nodes for c in branch_list], ignore_index=True)\n        self.nodes[\"global_comp_index\"] = np.arange(self.cumsum_nseg[-1])\n        self.nodes[\"global_branch_index\"] = np.repeat(\n            np.arange(self.total_nbranches), self.nseg_per_branch\n        ).tolist()\n        self.nodes[\"global_cell_index\"] = np.repeat(0, self.cumsum_nseg[-1]).tolist()\n        self._update_local_indices()\n        self._init_view()\n\n        # Appending general parameters (radius, length, r_a, cm) and channel parameters,\n        # as well as the states (v, and channel states).\n        self._append_params_and_states(self.cell_params, self.cell_states)\n\n        # Channels.\n        self._gather_channels_from_constituents(branch_list)\n\n        self.branch_edges = pd.DataFrame(\n            dict(\n                parent_branch_index=self.comb_parents[1:],\n                child_branch_index=np.arange(1, self.total_nbranches),\n            )\n        )\n\n        # For morphology indexing.\n        self._par_inds, self._child_inds, self._child_belongs_to_branchpoint = (\n            compute_children_and_parents(self.branch_edges)\n        )\n\n        self._initialize()\n\n    def _init_morph_jaxley_spsolve(self):\n        \"\"\"Initialize morphology for the custom sparse solver.\n\n        Running this function is only required for custom Jaxley solvers, i.e., for\n        `voltage_solver={'jaxley.stone', 'jaxley.thomas'}`. However, because at\n        `.__init__()` (when the function is run), we do not yet know which solver the\n        user will use. Therefore, we always run this function at `.__init__()`.\n        \"\"\"\n        children_and_parents = compute_morphology_indices_in_levels(\n            len(self._par_inds),\n            self._child_belongs_to_branchpoint,\n            self._par_inds,\n            self._child_inds,\n        )\n        branchpoint_group_inds = build_branchpoint_group_inds(\n            len(self._par_inds),\n            self._child_belongs_to_branchpoint,\n            self.cumsum_nseg[-1],\n        )\n        parents = self.comb_parents\n        children_inds = children_and_parents[\"children\"]\n        parents_inds = children_and_parents[\"parents\"]\n\n        levels = compute_levels(parents)\n        children_in_level = compute_children_in_level(levels, children_inds)\n        parents_in_level = compute_parents_in_level(\n            levels, self._par_inds, parents_inds\n        )\n        levels_and_nseg = pd.DataFrame().from_dict(\n            {\n                \"levels\": levels,\n                \"nsegs\": self.nseg_per_branch,\n            }\n        )\n        levels_and_nseg[\"max_nseg_in_level\"] = levels_and_nseg.groupby(\"levels\")[\n            \"nsegs\"\n        ].transform(\"max\")\n        padded_cumsum_nseg = cumsum_leading_zero(\n            levels_and_nseg[\"max_nseg_in_level\"].to_numpy()\n        )\n\n        # Generate mapping to deal with the masking which allows using the custom\n        # sparse solver to deal with different nseg per branch.\n        remapped_node_indices = remap_index_to_masked(\n            self._internal_node_inds,\n            self.nodes,\n            padded_cumsum_nseg,\n            self.nseg_per_branch,\n        )\n        self._solve_indexer = JaxleySolveIndexer(\n            cumsum_nseg=padded_cumsum_nseg,\n            branchpoint_group_inds=branchpoint_group_inds,\n            children_in_level=children_in_level,\n            parents_in_level=parents_in_level,\n            root_inds=np.asarray([0]),\n            remapped_node_indices=remapped_node_indices,\n        )\n\n    def _init_morph_jax_spsolve(self):\n        \"\"\"For morphology indexing with the `jax.sparse` voltage volver.\n\n        Explanation of `self._comp_eges['type']`:\n        `type == 0`: compartment <--> compartment (within branch)\n        `type == 1`: branchpoint --> parent-compartment\n        `type == 2`: branchpoint --> child-compartment\n        `type == 3`: parent-compartment --> branchpoint\n        `type == 4`: child-compartment --> branchpoint\n\n        Running this function is only required for generic sparse solvers, i.e., for\n        `voltage_solver='jax.sparse'`.\n        \"\"\"\n\n        # Edges between compartments within the branches.\n        self._comp_edges = pd.concat(\n            [\n                pd.DataFrame()\n                .from_dict(\n                    {\n                        \"source\": list(range(cumsum_nseg, nseg - 1 + cumsum_nseg))\n                        + list(range(1 + cumsum_nseg, nseg + cumsum_nseg)),\n                        \"sink\": list(range(1 + cumsum_nseg, nseg + cumsum_nseg))\n                        + list(range(cumsum_nseg, nseg - 1 + cumsum_nseg)),\n                    }\n                )\n                .astype(int)\n                for nseg, cumsum_nseg in zip(self.nseg_per_branch, self.cumsum_nseg)\n            ]\n        )\n        self._comp_edges[\"type\"] = 0\n\n        # Edges from branchpoints to compartments.\n        branchpoint_to_parent_edges = pd.DataFrame().from_dict(\n            {\n                \"source\": np.arange(len(self._par_inds)) + self.cumsum_nseg[-1],\n                \"sink\": self.cumsum_nseg[self._par_inds + 1] - 1,\n                \"type\": 1,\n            }\n        )\n        branchpoint_to_child_edges = pd.DataFrame().from_dict(\n            {\n                \"source\": self._child_belongs_to_branchpoint + self.cumsum_nseg[-1],\n                \"sink\": self.cumsum_nseg[self._child_inds],\n                \"type\": 2,\n            }\n        )\n        self._comp_edges = pd.concat(\n            [\n                self._comp_edges,\n                branchpoint_to_parent_edges,\n                branchpoint_to_child_edges,\n            ],\n            ignore_index=True,\n        )\n\n        # Edges from compartments to branchpoints.\n        parent_to_branchpoint_edges = branchpoint_to_parent_edges.rename(\n            columns={\"sink\": \"source\", \"source\": \"sink\"}\n        )\n        parent_to_branchpoint_edges[\"type\"] = 3\n        child_to_branchpoint_edges = branchpoint_to_child_edges.rename(\n            columns={\"sink\": \"source\", \"source\": \"sink\"}\n        )\n        child_to_branchpoint_edges[\"type\"] = 4\n\n        self._comp_edges = pd.concat(\n            [\n                self._comp_edges,\n                parent_to_branchpoint_edges,\n                child_to_branchpoint_edges,\n            ],\n            ignore_index=True,\n        )\n\n        n_nodes, data_inds, indices, indptr = comp_edges_to_indices(self._comp_edges)\n        self._n_nodes = n_nodes\n        self._data_inds = data_inds\n        self._indices_jax_spsolve = indices\n        self._indptr_jax_spsolve = indptr\n
"},{"location":"reference/modules/#jaxley.modules.cell.Cell.__init__","title":"__init__(branches=None, parents=None, xyzr=None)","text":"

Initialize a cell.

Parameters:

Name Type Description Default branches Optional[Union[Branch, List[Branch]]]

A single branch or a list of branches that make up the cell. If a single branch is provided, then the branch is repeated len(parents) times to create the cell.

None parents Optional[List[int]]

The parent branch index for each branch. The first branch has no parent and is therefore set to -1.

None xyzr Optional[List[ndarray]]

For every branch, the x, y, and z coordinates and the radius at the traced coordinates. Note that this is the full tracing (from SWC), not the stick representation coordinates.

None Source code in jaxley/modules/cell.py
def __init__(\n    self,\n    branches: Optional[Union[Branch, List[Branch]]] = None,\n    parents: Optional[List[int]] = None,\n    xyzr: Optional[List[np.ndarray]] = None,\n):\n    \"\"\"Initialize a cell.\n\n    Args:\n        branches: A single branch or a list of branches that make up the cell.\n            If a single branch is provided, then the branch is repeated `len(parents)`\n            times to create the cell.\n        parents: The parent branch index for each branch. The first branch has no\n            parent and is therefore set to -1.\n        xyzr: For every branch, the x, y, and z coordinates and the radius at the\n            traced coordinates. Note that this is the full tracing (from SWC), not\n            the stick representation coordinates.\n    \"\"\"\n    super().__init__()\n    assert (\n        isinstance(branches, (Branch, List)) or branches is None\n    ), \"Only Branch or List[Branch] is allowed.\"\n    if branches is not None:\n        assert (\n            parents is not None\n        ), \"If `branches` is not a list then you have to set `parents`.\"\n    if isinstance(branches, List):\n        assert len(parents) == len(\n            branches\n        ), \"Ensure equally many parents, i.e. len(branches) == len(parents).\"\n\n    branches = Branch() if branches is None else branches\n    parents = [-1] if parents is None else parents\n\n    if isinstance(branches, Branch):\n        branch_list = [branches for _ in range(len(parents))]\n    else:\n        branch_list = branches\n\n    if xyzr is not None:\n        assert len(xyzr) == len(parents)\n        self.xyzr = xyzr\n    else:\n        # For every branch (`len(parents)`), we have a start and end point (`2`) and\n        # a (x,y,z,r) coordinate for each of them (`4`).\n        # Since `xyzr` is only inspected at `.vis()` and because it depends on the\n        # (potentially learned) length of every compartment, we only populate\n        # self.xyzr at `.vis()`.\n        self.xyzr = [float(\"NaN\") * np.zeros((2, 4)) for _ in range(len(parents))]\n\n    self.total_nbranches = len(branch_list)\n    self.nbranches_per_cell = [len(branch_list)]\n    self.comb_parents = jnp.asarray(parents)\n    self.comb_children = compute_children_indices(self.comb_parents)\n    self._cumsum_nbranches = np.asarray([0, len(branch_list)])\n\n    # Compartment structure. These arguments have to be rebuilt when `.set_ncomp()`\n    # is run.\n    self.nseg_per_branch = np.asarray([branch.nseg for branch in branch_list])\n    self.nseg = int(np.max(self.nseg_per_branch))\n    self.cumsum_nseg = cumsum_leading_zero(self.nseg_per_branch)\n    self._internal_node_inds = np.arange(self.cumsum_nseg[-1])\n\n    # Build nodes. Has to be changed when `.set_ncomp()` is run.\n    self.nodes = pd.concat([c.nodes for c in branch_list], ignore_index=True)\n    self.nodes[\"global_comp_index\"] = np.arange(self.cumsum_nseg[-1])\n    self.nodes[\"global_branch_index\"] = np.repeat(\n        np.arange(self.total_nbranches), self.nseg_per_branch\n    ).tolist()\n    self.nodes[\"global_cell_index\"] = np.repeat(0, self.cumsum_nseg[-1]).tolist()\n    self._update_local_indices()\n    self._init_view()\n\n    # Appending general parameters (radius, length, r_a, cm) and channel parameters,\n    # as well as the states (v, and channel states).\n    self._append_params_and_states(self.cell_params, self.cell_states)\n\n    # Channels.\n    self._gather_channels_from_constituents(branch_list)\n\n    self.branch_edges = pd.DataFrame(\n        dict(\n            parent_branch_index=self.comb_parents[1:],\n            child_branch_index=np.arange(1, self.total_nbranches),\n        )\n    )\n\n    # For morphology indexing.\n    self._par_inds, self._child_inds, self._child_belongs_to_branchpoint = (\n        compute_children_and_parents(self.branch_edges)\n    )\n\n    self._initialize()\n
"},{"location":"reference/modules/#network","title":"Network","text":"

Bases: Module

Network class.

This class defines a network of cells that can be connected with synapses.

Source code in jaxley/modules/network.py
class Network(Module):\n    \"\"\"Network class.\n\n    This class defines a network of cells that can be connected with synapses.\n    \"\"\"\n\n    network_params: Dict = {}\n    network_states: Dict = {}\n\n    def __init__(\n        self,\n        cells: List[Cell],\n    ):\n        \"\"\"Initialize network of cells and synapses.\n\n        Args:\n            cells: A list of cells that make up the network.\n        \"\"\"\n        super().__init__()\n        for cell in cells:\n            self.xyzr += deepcopy(cell.xyzr)\n\n        self._cells_list = cells\n        self.nseg_per_branch = np.concatenate([cell.nseg_per_branch for cell in cells])\n        self.nseg = int(np.max(self.nseg_per_branch))\n        self.cumsum_nseg = cumsum_leading_zero(self.nseg_per_branch)\n        self._internal_node_inds = np.arange(self.cumsum_nseg[-1])\n        self._append_params_and_states(self.network_params, self.network_states)\n\n        self.nbranches_per_cell = [cell.total_nbranches for cell in cells]\n        self.total_nbranches = sum(self.nbranches_per_cell)\n        self._cumsum_nbranches = cumsum_leading_zero(self.nbranches_per_cell)\n\n        self.nodes = pd.concat([c.nodes for c in cells], ignore_index=True)\n        self.nodes[\"global_comp_index\"] = np.arange(self.cumsum_nseg[-1])\n        self.nodes[\"global_branch_index\"] = np.repeat(\n            np.arange(self.total_nbranches), self.nseg_per_branch\n        ).tolist()\n        self.nodes[\"global_cell_index\"] = list(\n            itertools.chain(\n                *[[i] * int(cell.cumsum_nseg[-1]) for i, cell in enumerate(cells)]\n            )\n        )\n        self._update_local_indices()\n        self._init_view()\n\n        parents = [cell.comb_parents for cell in cells]\n        self.comb_parents = jnp.concatenate(\n            [p.at[1:].add(self._cumsum_nbranches[i]) for i, p in enumerate(parents)]\n        )\n\n        # Two columns: `parent_branch_index` and `child_branch_index`. One row per\n        # branch, apart from those branches which do not have a parent (i.e.\n        # -1 in parents). For every branch, tracks the global index of that branch\n        # (`child_branch_index`) and the global index of its parent\n        # (`parent_branch_index`).\n        self.branch_edges = pd.DataFrame(\n            dict(\n                parent_branch_index=self.comb_parents[self.comb_parents != -1],\n                child_branch_index=np.where(self.comb_parents != -1)[0],\n            )\n        )\n\n        # For morphology indexing of both `jax.sparse` and the custom `jaxley` solvers.\n        self._par_inds, self._child_inds, self._child_belongs_to_branchpoint = (\n            compute_children_and_parents(self.branch_edges)\n        )\n\n        # `nbranchpoints` in each cell == cell._par_inds (because `par_inds` are unique).\n        nbranchpoints = jnp.asarray([len(cell._par_inds) for cell in cells])\n        self._cumsum_nbranchpoints_per_cell = cumsum_leading_zero(nbranchpoints)\n\n        # Channels.\n        self._gather_channels_from_constituents(cells)\n\n        self._initialize()\n        del self._cells_list\n\n    def __repr__(self):\n        return f\"{type(self).__name__} with {len(self.channels)} different channels and {len(self.synapses)} synapses. Use `.nodes` or `.edges` for details.\"\n\n    def _init_morph_jaxley_spsolve(self):\n        branchpoint_group_inds = build_branchpoint_group_inds(\n            len(self._par_inds),\n            self._child_belongs_to_branchpoint,\n            self.cumsum_nseg[-1],\n        )\n        children_in_level = merge_cells(\n            self._cumsum_nbranches,\n            self._cumsum_nbranchpoints_per_cell,\n            [cell._solve_indexer.children_in_level for cell in self._cells_list],\n            exclude_first=False,\n        )\n        parents_in_level = merge_cells(\n            self._cumsum_nbranches,\n            self._cumsum_nbranchpoints_per_cell,\n            [cell._solve_indexer.parents_in_level for cell in self._cells_list],\n            exclude_first=False,\n        )\n        padded_cumsum_nseg = cumsum_leading_zero(\n            np.concatenate(\n                [np.diff(cell._solve_indexer.cumsum_nseg) for cell in self._cells_list]\n            )\n        )\n\n        # Generate mapping to dealing with the masking which allows using the custom\n        # sparse solver to deal with different nseg per branch.\n        remapped_node_indices = remap_index_to_masked(\n            self._internal_node_inds,\n            self.nodes,\n            padded_cumsum_nseg,\n            self.nseg_per_branch,\n        )\n        self._solve_indexer = JaxleySolveIndexer(\n            cumsum_nseg=padded_cumsum_nseg,\n            branchpoint_group_inds=branchpoint_group_inds,\n            children_in_level=children_in_level,\n            parents_in_level=parents_in_level,\n            root_inds=self._cumsum_nbranches[:-1],\n            remapped_node_indices=remapped_node_indices,\n        )\n\n    def _init_morph_jax_spsolve(self):\n        \"\"\"Initialize the morphology for networks.\n\n        The reason that this function is a bit involved for a `Network` is that Jaxley\n        considers branchpoint nodes to be at the very end of __all__ nodes (i.e. the\n        branchpoints of the first cell are even after the compartments of the second\n        cell. The reason for this is that, otherwise, `cumsum_nseg` becomes tricky).\n\n        To achieve this, we first loop over all compartments and append them, and then\n        loop over all branchpoints and append those. The code for building the indices\n        from the `comp_edges` is identical to `jx.Cell`.\n\n        Explanation of `self._comp_eges['type']`:\n        `type == 0`: compartment <--> compartment (within branch)\n        `type == 1`: branchpoint --> parent-compartment\n        `type == 2`: branchpoint --> child-compartment\n        `type == 3`: parent-compartment --> branchpoint\n        `type == 4`: child-compartment --> branchpoint\n        \"\"\"\n        self._cumsum_nseg_per_cell = cumsum_leading_zero(\n            jnp.asarray([cell.cumsum_nseg[-1] for cell in self.cells])\n        )\n        self._comp_edges = pd.DataFrame()\n\n        # Add all the internal nodes.\n        for offset, cell in zip(self._cumsum_nseg_per_cell, self._cells_list):\n            condition = cell._comp_edges[\"type\"].to_numpy() == 0\n            rows = cell._comp_edges[condition]\n            self._comp_edges = pd.concat(\n                [self._comp_edges, [offset, offset, 0] + rows], ignore_index=True\n            )\n\n        # All branchpoint-to-compartment nodes.\n        start_branchpoints = self.cumsum_nseg[-1]  # Index of the first branchpoint.\n        for offset, offset_branchpoints, cell in zip(\n            self._cumsum_nseg_per_cell,\n            self._cumsum_nbranchpoints_per_cell,\n            self._cells_list,\n        ):\n            offset_within_cell = cell.cumsum_nseg[-1]\n            condition = cell._comp_edges[\"type\"].isin([1, 2])\n            rows = cell._comp_edges[condition]\n            self._comp_edges = pd.concat(\n                [\n                    self._comp_edges,\n                    [\n                        start_branchpoints - offset_within_cell + offset_branchpoints,\n                        offset,\n                        0,\n                    ]\n                    + rows,\n                ],\n                ignore_index=True,\n            )\n\n        # All compartment-to-branchpoint nodes.\n        for offset, offset_branchpoints, cell in zip(\n            self._cumsum_nseg_per_cell,\n            self._cumsum_nbranchpoints_per_cell,\n            self._cells_list,\n        ):\n            offset_within_cell = cell.cumsum_nseg[-1]\n            condition = cell._comp_edges[\"type\"].isin([3, 4])\n            rows = cell._comp_edges[condition]\n            self._comp_edges = pd.concat(\n                [\n                    self._comp_edges,\n                    [\n                        offset,\n                        start_branchpoints - offset_within_cell + offset_branchpoints,\n                        0,\n                    ]\n                    + rows,\n                ],\n                ignore_index=True,\n            )\n\n        # Convert comp_edges to the index format required for `jax.sparse` solvers.\n        n_nodes, data_inds, indices, indptr = comp_edges_to_indices(self._comp_edges)\n        self._n_nodes = n_nodes\n        self._data_inds = data_inds\n        self._indices_jax_spsolve = indices\n        self._indptr_jax_spsolve = indptr\n\n    def _step_synapse(\n        self,\n        states: Dict,\n        syn_channels: List,\n        params: Dict,\n        delta_t: float,\n        edges: pd.DataFrame,\n    ) -> Tuple[Dict, Tuple[jnp.ndarray, jnp.ndarray]]:\n        \"\"\"Perform one step of the synapses and obtain their currents.\"\"\"\n        states = self._step_synapse_state(states, syn_channels, params, delta_t, edges)\n        states, current_terms = self._synapse_currents(\n            states, syn_channels, params, delta_t, edges\n        )\n        return states, current_terms\n\n    def _step_synapse_state(\n        self,\n        states: Dict,\n        syn_channels: List,\n        params: Dict,\n        delta_t: float,\n        edges: pd.DataFrame,\n    ) -> Dict:\n        voltages = states[\"v\"]\n\n        grouped_syns = edges.groupby(\"type\", sort=False, group_keys=False)\n        pre_syn_inds = grouped_syns[\"pre_global_comp_index\"].apply(list)\n        post_syn_inds = grouped_syns[\"post_global_comp_index\"].apply(list)\n        synapse_names = list(grouped_syns.indices.keys())\n\n        for i, synapse_type in enumerate(syn_channels):\n            assert (\n                synapse_names[i] == synapse_type._name\n            ), \"Mixup in the ordering of synapses. Please create an issue on Github.\"\n            synapse_param_names = list(synapse_type.synapse_params.keys())\n            synapse_state_names = list(synapse_type.synapse_states.keys())\n\n            synapse_params = {}\n            for p in synapse_param_names:\n                synapse_params[p] = params[p]\n            synapse_states = {}\n            for s in synapse_state_names:\n                synapse_states[s] = states[s]\n\n            pre_inds = np.asarray(pre_syn_inds[synapse_names[i]])\n            post_inds = np.asarray(post_syn_inds[synapse_names[i]])\n\n            # State updates.\n            states_updated = synapse_type.update_states(\n                synapse_states,\n                delta_t,\n                voltages[pre_inds],\n                voltages[post_inds],\n                synapse_params,\n            )\n\n            # Rebuild state.\n            for key, val in states_updated.items():\n                states[key] = val\n\n        return states\n\n    def _synapse_currents(\n        self,\n        states: Dict,\n        syn_channels: List,\n        params: Dict,\n        delta_t: float,\n        edges: pd.DataFrame,\n    ) -> Tuple[Dict, Tuple[jnp.ndarray, jnp.ndarray]]:\n        voltages = states[\"v\"]\n\n        grouped_syns = edges.groupby(\"type\", sort=False, group_keys=False)\n        pre_syn_inds = grouped_syns[\"pre_global_comp_index\"].apply(list)\n        post_syn_inds = grouped_syns[\"post_global_comp_index\"].apply(list)\n        synapse_names = list(grouped_syns.indices.keys())\n\n        syn_voltage_terms = jnp.zeros_like(voltages)\n        syn_constant_terms = jnp.zeros_like(voltages)\n        # Run with two different voltages that are `diff` apart to infer the slope and\n        # offset.\n        diff = 1e-3\n        for i, synapse_type in enumerate(syn_channels):\n            assert (\n                synapse_names[i] == synapse_type._name\n            ), \"Mixup in the ordering of synapses. Please create an issue on Github.\"\n            synapse_param_names = list(synapse_type.synapse_params.keys())\n            synapse_state_names = list(synapse_type.synapse_states.keys())\n\n            synapse_params = {}\n            for p in synapse_param_names:\n                synapse_params[p] = params[p]\n            synapse_states = {}\n            for s in synapse_state_names:\n                synapse_states[s] = states[s]\n\n            # Get pre and post indexes of the current synapse type.\n            pre_inds = np.asarray(pre_syn_inds[synapse_names[i]])\n            post_inds = np.asarray(post_syn_inds[synapse_names[i]])\n\n            # Compute slope and offset of the current through every synapse.\n            pre_v_and_perturbed = jnp.stack(\n                [voltages[pre_inds], voltages[pre_inds] + diff]\n            )\n            post_v_and_perturbed = jnp.stack(\n                [voltages[post_inds], voltages[post_inds] + diff]\n            )\n            synapse_currents = vmap(\n                synapse_type.compute_current, in_axes=(None, 0, 0, None)\n            )(\n                synapse_states,\n                pre_v_and_perturbed,\n                post_v_and_perturbed,\n                synapse_params,\n            )\n            synapse_currents_dist = convert_point_process_to_distributed(\n                synapse_currents,\n                params[\"radius\"][post_inds],\n                params[\"length\"][post_inds],\n            )\n\n            # Split into voltage and constant terms.\n            voltage_term = (synapse_currents_dist[1] - synapse_currents_dist[0]) / diff\n            constant_term = (\n                synapse_currents_dist[0] - voltage_term * voltages[post_inds]\n            )\n\n            # Gather slope and offset for every postsynaptic compartment.\n            gathered_syn_currents = gather_synapes(\n                len(voltages),\n                post_inds,\n                voltage_term,\n                constant_term,\n            )\n            syn_voltage_terms += gathered_syn_currents[0]\n            syn_constant_terms -= gathered_syn_currents[1]\n\n            # Add the synaptic currents through every compartment as state.\n            # `post_syn_currents` is a `jnp.ndarray` of as many elements as there are\n            # compartments in the network.\n            # `[0]` because we only use the non-perturbed voltage.\n            states[f\"{synapse_type._name}_current\"] = synapse_currents[0]\n\n        return states, (syn_voltage_terms, syn_constant_terms)\n\n    def vis(\n        self,\n        detail: str = \"full\",\n        ax: Optional[Axes] = None,\n        col: str = \"k\",\n        synapse_col: str = \"b\",\n        dims: Tuple[int] = (0, 1),\n        type: str = \"line\",\n        layers: Optional[List] = None,\n        morph_plot_kwargs: Dict = {},\n        synapse_plot_kwargs: Dict = {},\n        synapse_scatter_kwargs: Dict = {},\n        networkx_options: Dict = {},\n        layer_kwargs: Dict = {},\n    ) -> Axes:\n        \"\"\"Visualize the module.\n\n        Args:\n            detail: Either of [point, full]. `point` visualizes every neuron in the\n                network as a dot (and it uses `networkx` to obtain cell positions).\n                `full` plots the full morphology of every neuron. It requires that\n                `compute_xyz()` has been run and allows for indivual neurons to be\n                moved with `.move()`.\n            col: The color in which cells are plotted. Only takes effect if\n                `detail='full'`.\n            type: Either `line` or `scatter`. Only takes effect if `detail='full'`.\n            synapse_col: The color in which synapses are plotted. Only takes effect if\n                `detail='full'`.\n            dims: Which dimensions to plot. 1=x, 2=y, 3=z coordinate. Must be a tuple of\n                two of them.\n            layers: Allows to plot the network in layers. Should provide the number of\n                neurons in each layer, e.g., [5, 10, 1] would be a network with 5 input\n                neurons, 10 hidden layer neurons, and 1 output neuron.\n            morph_plot_kwargs: Keyword arguments passed to the plotting function for\n                cell morphologies. Only takes effect for `detail='full'`.\n            synapse_plot_kwargs: Keyword arguments passed to the plotting function for\n                syanpses. Only takes effect for `detail='full'`.\n            synapse_scatter_kwargs: Keyword arguments passed to the scatter function\n                for the end point of synapses. Only takes effect for `detail='full'`.\n            networkx_options: Options passed to `networkx.draw()`. Only takes effect if\n                `detail='point'`.\n            layer_kwargs: Only used if `layers` is specified and if `detail='full'`.\n                Can have the following entries: `within_layer_offset` (float),\n                `between_layer_offset` (float), `vertical_layers` (bool).\n        \"\"\"\n        if detail == \"point\":\n            graph = self._build_graph(layers)\n\n            if layers is not None:\n                pos = nx.multipartite_layout(graph, subset_key=\"layer\")\n                nx.draw(graph, pos, with_labels=True, **networkx_options)\n            else:\n                nx.draw(graph, with_labels=True, **networkx_options)\n        elif detail == \"full\":\n            if layers is not None:\n                # Assemble cells in the network into layers.\n                global_counter = 0\n                layers_config = {\n                    \"within_layer_offset\": 500.0,\n                    \"between_layer_offset\": 1500.0,\n                    \"vertical_layers\": False,\n                }\n                layers_config.update(layer_kwargs)\n                for layer_ind, num_in_layer in enumerate(layers):\n                    for ind_within_layer in range(num_in_layer):\n                        if layers_config[\"vertical_layers\"]:\n                            x_offset = (\n                                ind_within_layer - (num_in_layer - 1) / 2\n                            ) * layers_config[\"within_layer_offset\"]\n                            y_offset = (len(layers) - 1 - layer_ind) * layers_config[\n                                \"between_layer_offset\"\n                            ]\n                        else:\n                            x_offset = layer_ind * layers_config[\"between_layer_offset\"]\n                            y_offset = (\n                                ind_within_layer - (num_in_layer - 1) / 2\n                            ) * layers_config[\"within_layer_offset\"]\n\n                        self.cell(global_counter).move_to(x=x_offset, y=y_offset, z=0)\n                        global_counter += 1\n            ax = super().vis(\n                dims=dims,\n                col=col,\n                ax=ax,\n                type=type,\n                morph_plot_kwargs=morph_plot_kwargs,\n            )\n\n            pre_locs = self.edges[\"pre_locs\"].to_numpy()\n            post_locs = self.edges[\"post_locs\"].to_numpy()\n            pre_comp = self.edges[\"pre_global_comp_index\"].to_numpy()\n            nodes = self.nodes.set_index(\"global_comp_index\")\n            pre_branch = nodes.loc[pre_comp, \"global_branch_index\"].to_numpy()\n            post_comp = self.edges[\"post_global_comp_index\"].to_numpy()\n            post_branch = nodes.loc[post_comp, \"global_branch_index\"].to_numpy()\n\n            dims_np = np.asarray(dims)\n\n            for pre_loc, post_loc, pre_b, post_b in zip(\n                pre_locs, post_locs, pre_branch, post_branch\n            ):\n                pre_coord = self.xyzr[pre_b]\n                if len(pre_coord) == 2:\n                    # If only start and end point of a branch are traced, perform a\n                    # linear interpolation to get the synpase location.\n                    pre_coord = pre_coord[0] + (pre_coord[1] - pre_coord[0]) * pre_loc\n                else:\n                    # If densely traced, use intermediate trace values for synapse loc.\n                    middle_ind = int((len(pre_coord) - 1) * pre_loc)\n                    pre_coord = pre_coord[middle_ind]\n\n                post_coord = self.xyzr[post_b]\n                if len(post_coord) == 2:\n                    # If only start and end point of a branch are traced, perform a\n                    # linear interpolation to get the synpase location.\n                    post_coord = (\n                        post_coord[0] + (post_coord[1] - post_coord[0]) * post_loc\n                    )\n                else:\n                    # If densely traced, use intermediate trace values for synapse loc.\n                    middle_ind = int((len(post_coord) - 1) * post_loc)\n                    post_coord = post_coord[middle_ind]\n\n                coords = np.stack([pre_coord[dims_np], post_coord[dims_np]]).T\n                ax.plot(\n                    coords[0],\n                    coords[1],\n                    c=synapse_col,\n                    **synapse_plot_kwargs,\n                )\n                ax.scatter(\n                    post_coord[dims_np[0]],\n                    post_coord[dims_np[1]],\n                    c=synapse_col,\n                    **synapse_scatter_kwargs,\n                )\n        else:\n            raise ValueError(\"detail must be in {full, point}.\")\n\n        return ax\n\n    def _build_graph(self, layers: Optional[List] = None, **options):\n        graph = nx.DiGraph()\n\n        def build_extents(*subset_sizes):\n            return nx.utils.pairwise(itertools.accumulate((0,) + subset_sizes))\n\n        if layers is not None:\n            extents = build_extents(*layers)\n            layers = [range(start, end) for start, end in extents]\n            for i, layer in enumerate(layers):\n                graph.add_nodes_from(layer, layer=i)\n        else:\n            graph.add_nodes_from(range(len(self._cells_in_view)))\n\n        pre_comp = self.edges[\"pre_global_comp_index\"].to_numpy()\n        nodes = self.nodes.set_index(\"global_comp_index\")\n        pre_cell = nodes.loc[pre_comp, \"global_cell_index\"].to_numpy()\n        post_comp = self.edges[\"post_global_comp_index\"].to_numpy()\n        post_cell = nodes.loc[post_comp, \"global_cell_index\"].to_numpy()\n\n        inds = np.stack([pre_cell, post_cell]).T\n        graph.add_edges_from(inds)\n\n        return graph\n\n    def _infer_synapse_type_ind(self, synapse_name):\n        syn_names = self.base.synapse_names\n        is_new_type = False if synapse_name in syn_names else True\n        type_ind = len(syn_names) if is_new_type else syn_names.index(synapse_name)\n        return type_ind, is_new_type\n\n    def _update_synapse_state_names(self, synapse_type):\n        # (Potentially) update variables that track meta information about synapses.\n        self.base.synapse_names.append(synapse_type._name)\n        self.base.synapse_param_names += list(synapse_type.synapse_params.keys())\n        self.base.synapse_state_names += list(synapse_type.synapse_states.keys())\n        self.base.synapses.append(synapse_type)\n\n    def _append_multiple_synapses(self, pre_nodes, post_nodes, synapse_type):\n        # Add synapse types to the module and infer their unique identifier.\n        synapse_name = synapse_type._name\n        type_ind, is_new = self._infer_synapse_type_ind(synapse_name)\n        if is_new:  # synapse is not known\n            self._update_synapse_state_names(synapse_type)\n\n        index = len(self.base.edges)\n        indices = [idx for idx in range(index, index + len(pre_nodes))]\n        global_edge_index = pd.DataFrame({\"global_edge_index\": indices})\n        post_loc = loc_of_index(\n            post_nodes[\"global_comp_index\"].to_numpy(),\n            post_nodes[\"global_branch_index\"].to_numpy(),\n            self.nseg_per_branch,\n        )\n        pre_loc = loc_of_index(\n            pre_nodes[\"global_comp_index\"].to_numpy(),\n            pre_nodes[\"global_branch_index\"].to_numpy(),\n            self.nseg_per_branch,\n        )\n\n        # Define new synapses. Each row is one synapse.\n        pre_nodes = pre_nodes[[\"global_comp_index\"]]\n        pre_nodes.columns = [\"pre_global_comp_index\"]\n        post_nodes = post_nodes[[\"global_comp_index\"]]\n        post_nodes.columns = [\"post_global_comp_index\"]\n        new_rows = pd.concat(\n            [\n                global_edge_index,\n                pre_nodes.reset_index(drop=True),\n                post_nodes.reset_index(drop=True),\n            ],\n            axis=1,\n        )\n        new_rows[\"type\"] = synapse_name\n        new_rows[\"type_ind\"] = type_ind\n        new_rows[\"pre_locs\"] = pre_loc\n        new_rows[\"post_locs\"] = post_loc\n        self.base.edges = concat_and_ignore_empty(\n            [self.base.edges, new_rows], ignore_index=True, axis=0\n        )\n        self._add_params_to_edges(synapse_type, indices)\n        self.base.edges[\"controlled_by_param\"] = 0\n        self._edges_in_view = self.edges.index.to_numpy()\n\n    def _add_params_to_edges(self, synapse_type, indices):\n        # Add parameters and states to the `.edges` table.\n        for key, param_val in synapse_type.synapse_params.items():\n            self.base.edges.loc[indices, key] = param_val\n\n        # Update synaptic state array.\n        for key, state_val in synapse_type.synapse_states.items():\n            self.base.edges.loc[indices, key] = state_val\n
"},{"location":"reference/modules/#jaxley.modules.network.Network.__init__","title":"__init__(cells)","text":"

Initialize network of cells and synapses.

Parameters:

Name Type Description Default cells List[Cell]

A list of cells that make up the network.

required Source code in jaxley/modules/network.py
def __init__(\n    self,\n    cells: List[Cell],\n):\n    \"\"\"Initialize network of cells and synapses.\n\n    Args:\n        cells: A list of cells that make up the network.\n    \"\"\"\n    super().__init__()\n    for cell in cells:\n        self.xyzr += deepcopy(cell.xyzr)\n\n    self._cells_list = cells\n    self.nseg_per_branch = np.concatenate([cell.nseg_per_branch for cell in cells])\n    self.nseg = int(np.max(self.nseg_per_branch))\n    self.cumsum_nseg = cumsum_leading_zero(self.nseg_per_branch)\n    self._internal_node_inds = np.arange(self.cumsum_nseg[-1])\n    self._append_params_and_states(self.network_params, self.network_states)\n\n    self.nbranches_per_cell = [cell.total_nbranches for cell in cells]\n    self.total_nbranches = sum(self.nbranches_per_cell)\n    self._cumsum_nbranches = cumsum_leading_zero(self.nbranches_per_cell)\n\n    self.nodes = pd.concat([c.nodes for c in cells], ignore_index=True)\n    self.nodes[\"global_comp_index\"] = np.arange(self.cumsum_nseg[-1])\n    self.nodes[\"global_branch_index\"] = np.repeat(\n        np.arange(self.total_nbranches), self.nseg_per_branch\n    ).tolist()\n    self.nodes[\"global_cell_index\"] = list(\n        itertools.chain(\n            *[[i] * int(cell.cumsum_nseg[-1]) for i, cell in enumerate(cells)]\n        )\n    )\n    self._update_local_indices()\n    self._init_view()\n\n    parents = [cell.comb_parents for cell in cells]\n    self.comb_parents = jnp.concatenate(\n        [p.at[1:].add(self._cumsum_nbranches[i]) for i, p in enumerate(parents)]\n    )\n\n    # Two columns: `parent_branch_index` and `child_branch_index`. One row per\n    # branch, apart from those branches which do not have a parent (i.e.\n    # -1 in parents). For every branch, tracks the global index of that branch\n    # (`child_branch_index`) and the global index of its parent\n    # (`parent_branch_index`).\n    self.branch_edges = pd.DataFrame(\n        dict(\n            parent_branch_index=self.comb_parents[self.comb_parents != -1],\n            child_branch_index=np.where(self.comb_parents != -1)[0],\n        )\n    )\n\n    # For morphology indexing of both `jax.sparse` and the custom `jaxley` solvers.\n    self._par_inds, self._child_inds, self._child_belongs_to_branchpoint = (\n        compute_children_and_parents(self.branch_edges)\n    )\n\n    # `nbranchpoints` in each cell == cell._par_inds (because `par_inds` are unique).\n    nbranchpoints = jnp.asarray([len(cell._par_inds) for cell in cells])\n    self._cumsum_nbranchpoints_per_cell = cumsum_leading_zero(nbranchpoints)\n\n    # Channels.\n    self._gather_channels_from_constituents(cells)\n\n    self._initialize()\n    del self._cells_list\n
"},{"location":"reference/modules/#jaxley.modules.network.Network.vis","title":"vis(detail='full', ax=None, col='k', synapse_col='b', dims=(0, 1), type='line', layers=None, morph_plot_kwargs={}, synapse_plot_kwargs={}, synapse_scatter_kwargs={}, networkx_options={}, layer_kwargs={})","text":"

Visualize the module.

Parameters:

Name Type Description Default detail str

Either of [point, full]. point visualizes every neuron in the network as a dot (and it uses networkx to obtain cell positions). full plots the full morphology of every neuron. It requires that compute_xyz() has been run and allows for indivual neurons to be moved with .move().

'full' col str

The color in which cells are plotted. Only takes effect if detail='full'.

'k' type str

Either line or scatter. Only takes effect if detail='full'.

'line' synapse_col str

The color in which synapses are plotted. Only takes effect if detail='full'.

'b' dims Tuple[int]

Which dimensions to plot. 1=x, 2=y, 3=z coordinate. Must be a tuple of two of them.

(0, 1) layers Optional[List]

Allows to plot the network in layers. Should provide the number of neurons in each layer, e.g., [5, 10, 1] would be a network with 5 input neurons, 10 hidden layer neurons, and 1 output neuron.

None morph_plot_kwargs Dict

Keyword arguments passed to the plotting function for cell morphologies. Only takes effect for detail='full'.

{} synapse_plot_kwargs Dict

Keyword arguments passed to the plotting function for syanpses. Only takes effect for detail='full'.

{} synapse_scatter_kwargs Dict

Keyword arguments passed to the scatter function for the end point of synapses. Only takes effect for detail='full'.

{} networkx_options Dict

Options passed to networkx.draw(). Only takes effect if detail='point'.

{} layer_kwargs Dict

Only used if layers is specified and if detail='full'. Can have the following entries: within_layer_offset (float), between_layer_offset (float), vertical_layers (bool).

{} Source code in jaxley/modules/network.py
def vis(\n    self,\n    detail: str = \"full\",\n    ax: Optional[Axes] = None,\n    col: str = \"k\",\n    synapse_col: str = \"b\",\n    dims: Tuple[int] = (0, 1),\n    type: str = \"line\",\n    layers: Optional[List] = None,\n    morph_plot_kwargs: Dict = {},\n    synapse_plot_kwargs: Dict = {},\n    synapse_scatter_kwargs: Dict = {},\n    networkx_options: Dict = {},\n    layer_kwargs: Dict = {},\n) -> Axes:\n    \"\"\"Visualize the module.\n\n    Args:\n        detail: Either of [point, full]. `point` visualizes every neuron in the\n            network as a dot (and it uses `networkx` to obtain cell positions).\n            `full` plots the full morphology of every neuron. It requires that\n            `compute_xyz()` has been run and allows for indivual neurons to be\n            moved with `.move()`.\n        col: The color in which cells are plotted. Only takes effect if\n            `detail='full'`.\n        type: Either `line` or `scatter`. Only takes effect if `detail='full'`.\n        synapse_col: The color in which synapses are plotted. Only takes effect if\n            `detail='full'`.\n        dims: Which dimensions to plot. 1=x, 2=y, 3=z coordinate. Must be a tuple of\n            two of them.\n        layers: Allows to plot the network in layers. Should provide the number of\n            neurons in each layer, e.g., [5, 10, 1] would be a network with 5 input\n            neurons, 10 hidden layer neurons, and 1 output neuron.\n        morph_plot_kwargs: Keyword arguments passed to the plotting function for\n            cell morphologies. Only takes effect for `detail='full'`.\n        synapse_plot_kwargs: Keyword arguments passed to the plotting function for\n            syanpses. Only takes effect for `detail='full'`.\n        synapse_scatter_kwargs: Keyword arguments passed to the scatter function\n            for the end point of synapses. Only takes effect for `detail='full'`.\n        networkx_options: Options passed to `networkx.draw()`. Only takes effect if\n            `detail='point'`.\n        layer_kwargs: Only used if `layers` is specified and if `detail='full'`.\n            Can have the following entries: `within_layer_offset` (float),\n            `between_layer_offset` (float), `vertical_layers` (bool).\n    \"\"\"\n    if detail == \"point\":\n        graph = self._build_graph(layers)\n\n        if layers is not None:\n            pos = nx.multipartite_layout(graph, subset_key=\"layer\")\n            nx.draw(graph, pos, with_labels=True, **networkx_options)\n        else:\n            nx.draw(graph, with_labels=True, **networkx_options)\n    elif detail == \"full\":\n        if layers is not None:\n            # Assemble cells in the network into layers.\n            global_counter = 0\n            layers_config = {\n                \"within_layer_offset\": 500.0,\n                \"between_layer_offset\": 1500.0,\n                \"vertical_layers\": False,\n            }\n            layers_config.update(layer_kwargs)\n            for layer_ind, num_in_layer in enumerate(layers):\n                for ind_within_layer in range(num_in_layer):\n                    if layers_config[\"vertical_layers\"]:\n                        x_offset = (\n                            ind_within_layer - (num_in_layer - 1) / 2\n                        ) * layers_config[\"within_layer_offset\"]\n                        y_offset = (len(layers) - 1 - layer_ind) * layers_config[\n                            \"between_layer_offset\"\n                        ]\n                    else:\n                        x_offset = layer_ind * layers_config[\"between_layer_offset\"]\n                        y_offset = (\n                            ind_within_layer - (num_in_layer - 1) / 2\n                        ) * layers_config[\"within_layer_offset\"]\n\n                    self.cell(global_counter).move_to(x=x_offset, y=y_offset, z=0)\n                    global_counter += 1\n        ax = super().vis(\n            dims=dims,\n            col=col,\n            ax=ax,\n            type=type,\n            morph_plot_kwargs=morph_plot_kwargs,\n        )\n\n        pre_locs = self.edges[\"pre_locs\"].to_numpy()\n        post_locs = self.edges[\"post_locs\"].to_numpy()\n        pre_comp = self.edges[\"pre_global_comp_index\"].to_numpy()\n        nodes = self.nodes.set_index(\"global_comp_index\")\n        pre_branch = nodes.loc[pre_comp, \"global_branch_index\"].to_numpy()\n        post_comp = self.edges[\"post_global_comp_index\"].to_numpy()\n        post_branch = nodes.loc[post_comp, \"global_branch_index\"].to_numpy()\n\n        dims_np = np.asarray(dims)\n\n        for pre_loc, post_loc, pre_b, post_b in zip(\n            pre_locs, post_locs, pre_branch, post_branch\n        ):\n            pre_coord = self.xyzr[pre_b]\n            if len(pre_coord) == 2:\n                # If only start and end point of a branch are traced, perform a\n                # linear interpolation to get the synpase location.\n                pre_coord = pre_coord[0] + (pre_coord[1] - pre_coord[0]) * pre_loc\n            else:\n                # If densely traced, use intermediate trace values for synapse loc.\n                middle_ind = int((len(pre_coord) - 1) * pre_loc)\n                pre_coord = pre_coord[middle_ind]\n\n            post_coord = self.xyzr[post_b]\n            if len(post_coord) == 2:\n                # If only start and end point of a branch are traced, perform a\n                # linear interpolation to get the synpase location.\n                post_coord = (\n                    post_coord[0] + (post_coord[1] - post_coord[0]) * post_loc\n                )\n            else:\n                # If densely traced, use intermediate trace values for synapse loc.\n                middle_ind = int((len(post_coord) - 1) * post_loc)\n                post_coord = post_coord[middle_ind]\n\n            coords = np.stack([pre_coord[dims_np], post_coord[dims_np]]).T\n            ax.plot(\n                coords[0],\n                coords[1],\n                c=synapse_col,\n                **synapse_plot_kwargs,\n            )\n            ax.scatter(\n                post_coord[dims_np[0]],\n                post_coord[dims_np[1]],\n                c=synapse_col,\n                **synapse_scatter_kwargs,\n            )\n    else:\n        raise ValueError(\"detail must be in {full, point}.\")\n\n    return ax\n
"},{"location":"reference/optimize/","title":"Optimization","text":""},{"location":"reference/optimize/#jaxley.optimize.optimizer.TypeOptimizer","title":"TypeOptimizer","text":"

optax wrapper which allows different argument values for different params.

Source code in jaxley/optimize/optimizer.py
class TypeOptimizer:\n    \"\"\"`optax` wrapper which allows different argument values for different params.\"\"\"\n\n    def __init__(\n        self,\n        optimizer: Callable,\n        optimizer_args: Dict[str, Any],\n        opt_params: List[Dict[str, jnp.ndarray]],\n    ):\n        \"\"\"Create the optimizers.\n\n        This requires access to `opt_params` in order to know how many optimizers\n        should be created. It creates `len(opt_params)` optimizers.\n\n        Example usage:\n        ```\n        lrs = {\"HH_gNa\": 0.01, \"radius\": 1.0}\n        optimizer = TypeOptimizer(lambda lr: optax.adam(lr), lrs, opt_params)\n        opt_state = optimizer.init(opt_params)\n        ```\n\n        ```\n        optimizer_args = {\"HH_gNa\": [0.01, 0.4], \"radius\": [1.0, 0.8]}\n        optimizer = TypeOptimizer(\n            lambda args: optax.sgd(args[0], momentum=args[1]),\n            optimizer_args,\n            opt_params\n        )\n        opt_state = optimizer.init(opt_params)\n        ```\n\n        Args:\n            optimizer: A Callable that takes the learning rate and returns the\n                `optax.optimizer` which should be used.\n            optimizer_args: The arguments for different kinds of parameters.\n                Each item of the dictionary will be passed to the `Callable` passed to\n                `optimizer`.\n            opt_params: The parameters to be optimized. The exact values are not used,\n                only the number of elements in the list and the key of each dict.\n        \"\"\"\n        self.base_optimizer = optimizer\n\n        self.optimizers = []\n        for params in opt_params:\n            names = list(params.keys())\n            assert len(names) == 1, \"Multiple parameters were added at once.\"\n            name = names[0]\n            optimizer = self.base_optimizer(optimizer_args[name])\n            self.optimizers.append({name: optimizer})\n\n    def init(self, opt_params: List[Dict[str, jnp.ndarray]]) -> List:\n        \"\"\"Initialize the optimizers. Equivalent to `optax.optimizers.init()`.\"\"\"\n        opt_states = []\n        for params, optimizer in zip(opt_params, self.optimizers):\n            name = list(optimizer.keys())[0]\n            opt_state = optimizer[name].init(params)\n            opt_states.append(opt_state)\n        return opt_states\n\n    def update(self, gradient: jnp.ndarray, opt_state: List) -> Tuple[List, List]:\n        \"\"\"Update the optimizers. Equivalent to `optax.optimizers.update()`.\"\"\"\n        all_updates = []\n        new_opt_states = []\n        for grad, state, opt in zip(gradient, opt_state, self.optimizers):\n            name = list(opt.keys())[0]\n            updates, new_opt_state = opt[name].update(grad, state)\n            all_updates.append(updates)\n            new_opt_states.append(new_opt_state)\n        return all_updates, new_opt_states\n
"},{"location":"reference/optimize/#jaxley.optimize.optimizer.TypeOptimizer.__init__","title":"__init__(optimizer, optimizer_args, opt_params)","text":"

Create the optimizers.

This requires access to opt_params in order to know how many optimizers should be created. It creates len(opt_params) optimizers.

Example usage:

lrs = {\"HH_gNa\": 0.01, \"radius\": 1.0}\noptimizer = TypeOptimizer(lambda lr: optax.adam(lr), lrs, opt_params)\nopt_state = optimizer.init(opt_params)\n

optimizer_args = {\"HH_gNa\": [0.01, 0.4], \"radius\": [1.0, 0.8]}\noptimizer = TypeOptimizer(\n    lambda args: optax.sgd(args[0], momentum=args[1]),\n    optimizer_args,\n    opt_params\n)\nopt_state = optimizer.init(opt_params)\n

Parameters:

Name Type Description Default optimizer Callable

A Callable that takes the learning rate and returns the optax.optimizer which should be used.

required optimizer_args Dict[str, Any]

The arguments for different kinds of parameters. Each item of the dictionary will be passed to the Callable passed to optimizer.

required opt_params List[Dict[str, ndarray]]

The parameters to be optimized. The exact values are not used, only the number of elements in the list and the key of each dict.

required Source code in jaxley/optimize/optimizer.py
def __init__(\n    self,\n    optimizer: Callable,\n    optimizer_args: Dict[str, Any],\n    opt_params: List[Dict[str, jnp.ndarray]],\n):\n    \"\"\"Create the optimizers.\n\n    This requires access to `opt_params` in order to know how many optimizers\n    should be created. It creates `len(opt_params)` optimizers.\n\n    Example usage:\n    ```\n    lrs = {\"HH_gNa\": 0.01, \"radius\": 1.0}\n    optimizer = TypeOptimizer(lambda lr: optax.adam(lr), lrs, opt_params)\n    opt_state = optimizer.init(opt_params)\n    ```\n\n    ```\n    optimizer_args = {\"HH_gNa\": [0.01, 0.4], \"radius\": [1.0, 0.8]}\n    optimizer = TypeOptimizer(\n        lambda args: optax.sgd(args[0], momentum=args[1]),\n        optimizer_args,\n        opt_params\n    )\n    opt_state = optimizer.init(opt_params)\n    ```\n\n    Args:\n        optimizer: A Callable that takes the learning rate and returns the\n            `optax.optimizer` which should be used.\n        optimizer_args: The arguments for different kinds of parameters.\n            Each item of the dictionary will be passed to the `Callable` passed to\n            `optimizer`.\n        opt_params: The parameters to be optimized. The exact values are not used,\n            only the number of elements in the list and the key of each dict.\n    \"\"\"\n    self.base_optimizer = optimizer\n\n    self.optimizers = []\n    for params in opt_params:\n        names = list(params.keys())\n        assert len(names) == 1, \"Multiple parameters were added at once.\"\n        name = names[0]\n        optimizer = self.base_optimizer(optimizer_args[name])\n        self.optimizers.append({name: optimizer})\n
"},{"location":"reference/optimize/#jaxley.optimize.optimizer.TypeOptimizer.init","title":"init(opt_params)","text":"

Initialize the optimizers. Equivalent to optax.optimizers.init().

Source code in jaxley/optimize/optimizer.py
def init(self, opt_params: List[Dict[str, jnp.ndarray]]) -> List:\n    \"\"\"Initialize the optimizers. Equivalent to `optax.optimizers.init()`.\"\"\"\n    opt_states = []\n    for params, optimizer in zip(opt_params, self.optimizers):\n        name = list(optimizer.keys())[0]\n        opt_state = optimizer[name].init(params)\n        opt_states.append(opt_state)\n    return opt_states\n
"},{"location":"reference/optimize/#jaxley.optimize.optimizer.TypeOptimizer.update","title":"update(gradient, opt_state)","text":"

Update the optimizers. Equivalent to optax.optimizers.update().

Source code in jaxley/optimize/optimizer.py
def update(self, gradient: jnp.ndarray, opt_state: List) -> Tuple[List, List]:\n    \"\"\"Update the optimizers. Equivalent to `optax.optimizers.update()`.\"\"\"\n    all_updates = []\n    new_opt_states = []\n    for grad, state, opt in zip(gradient, opt_state, self.optimizers):\n        name = list(opt.keys())[0]\n        updates, new_opt_state = opt[name].update(grad, state)\n        all_updates.append(updates)\n        new_opt_states.append(new_opt_state)\n    return all_updates, new_opt_states\n
"},{"location":"reference/optimize/#jaxley.optimize.transforms.AffineTransform","title":"AffineTransform","text":"

Bases: Transform

Source code in jaxley/optimize/transforms.py
class AffineTransform(Transform):\n    def __init__(self, scale: ArrayLike, shift: ArrayLike):\n        \"\"\"This transform rescales and shifts the input.\n\n        Args:\n            scale (ArrayLike): Scaling factor.\n            shift (ArrayLike): Additive shift.\n\n        Raises:\n            ValueError: Scale needs to be larger than 0\n        \"\"\"\n        if jnp.allclose(scale, 0):\n            raise ValueError(\"a cannot be zero, must be invertible\")\n        self.a = scale\n        self.b = shift\n\n    def forward(self, x: ArrayLike) -> Array:\n        return self.a * x + self.b\n\n    def inverse(self, x: ArrayLike) -> Array:\n        return (x - self.b) / self.a\n
"},{"location":"reference/optimize/#jaxley.optimize.transforms.AffineTransform.__init__","title":"__init__(scale, shift)","text":"

This transform rescales and shifts the input.

Parameters:

Name Type Description Default scale ArrayLike

Scaling factor.

required shift ArrayLike

Additive shift.

required

Raises:

Type Description ValueError

Scale needs to be larger than 0

Source code in jaxley/optimize/transforms.py
def __init__(self, scale: ArrayLike, shift: ArrayLike):\n    \"\"\"This transform rescales and shifts the input.\n\n    Args:\n        scale (ArrayLike): Scaling factor.\n        shift (ArrayLike): Additive shift.\n\n    Raises:\n        ValueError: Scale needs to be larger than 0\n    \"\"\"\n    if jnp.allclose(scale, 0):\n        raise ValueError(\"a cannot be zero, must be invertible\")\n    self.a = scale\n    self.b = shift\n
"},{"location":"reference/optimize/#jaxley.optimize.transforms.ChainTransform","title":"ChainTransform","text":"

Bases: Transform

Chaining together multiple transformations

Source code in jaxley/optimize/transforms.py
class ChainTransform(Transform):\n    \"\"\"Chaining together multiple transformations\"\"\"\n\n    def __init__(self, transforms: Sequence[Transform]) -> None:\n        \"\"\"A chain of transformations\n\n        Args:\n            transforms (Sequence[Transform]): Transforms to apply\n        \"\"\"\n        super().__init__()\n        self.transforms = transforms\n\n    def forward(self, x: ArrayLike) -> Array:\n        for transform in self.transforms:\n            x = transform(x)\n        return x\n\n    def inverse(self, y: ArrayLike) -> Array:\n        for transform in reversed(self.transforms):\n            y = transform.inverse(y)\n        return y\n
"},{"location":"reference/optimize/#jaxley.optimize.transforms.ChainTransform.__init__","title":"__init__(transforms)","text":"

A chain of transformations

Parameters:

Name Type Description Default transforms Sequence[Transform]

Transforms to apply

required Source code in jaxley/optimize/transforms.py
def __init__(self, transforms: Sequence[Transform]) -> None:\n    \"\"\"A chain of transformations\n\n    Args:\n        transforms (Sequence[Transform]): Transforms to apply\n    \"\"\"\n    super().__init__()\n    self.transforms = transforms\n
"},{"location":"reference/optimize/#jaxley.optimize.transforms.CustomTransform","title":"CustomTransform","text":"

Bases: Transform

Custom transformation

Source code in jaxley/optimize/transforms.py
class CustomTransform(Transform):\n    \"\"\"Custom transformation\"\"\"\n\n    def __init__(self, forward_fn: Callable, inverse_fn: Callable) -> None:\n        \"\"\"A custom transformation using a user-defined froward and\n        inverse function\n\n        Args:\n            forward_fn (Callable): Forward transformation\n            inverse_fn (Callable): Inverse transformation\n        \"\"\"\n        super().__init__()\n        self.forward_fn = forward_fn\n        self.inverse_fn = inverse_fn\n\n    def forward(self, x: ArrayLike) -> Array:\n        return self.forward_fn(x)\n\n    def inverse(self, y: ArrayLike) -> Array:\n        return self.inverse_fn(y)\n
"},{"location":"reference/optimize/#jaxley.optimize.transforms.CustomTransform.__init__","title":"__init__(forward_fn, inverse_fn)","text":"

A custom transformation using a user-defined froward and inverse function

Parameters:

Name Type Description Default forward_fn Callable

Forward transformation

required inverse_fn Callable

Inverse transformation

required Source code in jaxley/optimize/transforms.py
def __init__(self, forward_fn: Callable, inverse_fn: Callable) -> None:\n    \"\"\"A custom transformation using a user-defined froward and\n    inverse function\n\n    Args:\n        forward_fn (Callable): Forward transformation\n        inverse_fn (Callable): Inverse transformation\n    \"\"\"\n    super().__init__()\n    self.forward_fn = forward_fn\n    self.inverse_fn = inverse_fn\n
"},{"location":"reference/optimize/#jaxley.optimize.transforms.MaskedTransform","title":"MaskedTransform","text":"

Bases: Transform

Source code in jaxley/optimize/transforms.py
class MaskedTransform(Transform):\n    def __init__(self, mask: ArrayLike, transform: Transform) -> None:\n        \"\"\"A masked transformation\n\n        Args:\n            mask (ArrayLike): Which elements to transform\n            transform (Transform): Transformation to apply\n        \"\"\"\n        super().__init__()\n        self.mask = mask\n        self.transform = transform\n\n    def forward(self, x: ArrayLike) -> Array:\n        return jnp.where(self.mask, self.transform.forward(x), x)\n\n    def inverse(self, y: ArrayLike) -> Array:\n        return jnp.where(self.mask, self.transform.inverse(y), y)\n
"},{"location":"reference/optimize/#jaxley.optimize.transforms.MaskedTransform.__init__","title":"__init__(mask, transform)","text":"

A masked transformation

Parameters:

Name Type Description Default mask ArrayLike

Which elements to transform

required transform Transform

Transformation to apply

required Source code in jaxley/optimize/transforms.py
def __init__(self, mask: ArrayLike, transform: Transform) -> None:\n    \"\"\"A masked transformation\n\n    Args:\n        mask (ArrayLike): Which elements to transform\n        transform (Transform): Transformation to apply\n    \"\"\"\n    super().__init__()\n    self.mask = mask\n    self.transform = transform\n
"},{"location":"reference/optimize/#jaxley.optimize.transforms.NegSoftplusTransform","title":"NegSoftplusTransform","text":"

Bases: SoftplusTransform

Negative softplus transformation.

Source code in jaxley/optimize/transforms.py
class NegSoftplusTransform(SoftplusTransform):\n    \"\"\"Negative softplus transformation.\"\"\"\n\n    def __init__(self, upper: ArrayLike) -> None:\n        \"\"\"This transform maps any value bijectively to the interval (-inf, upper].\n\n        Args:\n            upper (ArrayLike): Upper bound of the interval.\n        \"\"\"\n        super().__init__(upper)\n\n    def forward(self, x: ArrayLike) -> Array:\n        return -super().forward(-x)\n\n    def inverse(self, y: ArrayLike) -> Array:\n        return -super().inverse(-y)\n
"},{"location":"reference/optimize/#jaxley.optimize.transforms.NegSoftplusTransform.__init__","title":"__init__(upper)","text":"

This transform maps any value bijectively to the interval (-inf, upper].

Parameters:

Name Type Description Default upper ArrayLike

Upper bound of the interval.

required Source code in jaxley/optimize/transforms.py
def __init__(self, upper: ArrayLike) -> None:\n    \"\"\"This transform maps any value bijectively to the interval (-inf, upper].\n\n    Args:\n        upper (ArrayLike): Upper bound of the interval.\n    \"\"\"\n    super().__init__(upper)\n
"},{"location":"reference/optimize/#jaxley.optimize.transforms.ParamTransform","title":"ParamTransform","text":"

Parameter transformation utility.

This class is used to transform parameters usually from an unconstrained space to a constrained space and back (bacause most biophysical parameter are bounded). The user can specify a PyTree of transforms that are applied to the parameters.

Attributes:

Name Type Description tf_dict

A PyTree of transforms for each parameter.

Source code in jaxley/optimize/transforms.py
class ParamTransform:\n    \"\"\"Parameter transformation utility.\n\n    This class is used to transform parameters usually from an unconstrained space to a constrained space\n    and back (bacause most biophysical parameter are bounded). The user can specify a PyTree of transforms\n    that are applied to the parameters.\n\n    Attributes:\n        tf_dict: A PyTree of transforms for each parameter.\n\n    \"\"\"\n\n    def __init__(self, tf_dict: List[Dict[str, Transform]] | Transform) -> None:\n        \"\"\"Creates a new ParamTransform object.\n\n        Args:\n            tf_dict: A PyTree of transforms for each parameter.\n        \"\"\"\n\n        self.tf_dict = tf_dict\n\n    def forward(\n        self, params: List[Dict[str, ArrayLike]] | ArrayLike\n    ) -> Dict[str, Array]:\n        \"\"\"Pushes unconstrained parameters through a tf such that they fit the interval.\n\n        Args:\n            params: A list of dictionaries (or any PyTree) with unconstrained parameters.\n\n        Returns:\n            A list of dictionaries (or any PyTree) with transformed parameters.\n\n        \"\"\"\n\n        return jax.tree_util.tree_map(lambda x, tf: tf.forward(x), params, self.tf_dict)\n\n    def inverse(\n        self, params: List[Dict[str, ArrayLike]] | ArrayLike\n    ) -> Dict[str, Array]:\n        \"\"\"Takes parameters from within the interval and makes them unconstrained.\n\n        Args:\n            params: A list of dictionaries (or any PyTree) with transformed parameters.\n\n        Returns:\n            A list of dictionaries (or any PyTree) with unconstrained parameters.\n        \"\"\"\n\n        return jax.tree_util.tree_map(lambda x, tf: tf.inverse(x), params, self.tf_dict)\n
"},{"location":"reference/optimize/#jaxley.optimize.transforms.ParamTransform.__init__","title":"__init__(tf_dict)","text":"

Creates a new ParamTransform object.

Parameters:

Name Type Description Default tf_dict List[Dict[str, Transform]] | Transform

A PyTree of transforms for each parameter.

required Source code in jaxley/optimize/transforms.py
def __init__(self, tf_dict: List[Dict[str, Transform]] | Transform) -> None:\n    \"\"\"Creates a new ParamTransform object.\n\n    Args:\n        tf_dict: A PyTree of transforms for each parameter.\n    \"\"\"\n\n    self.tf_dict = tf_dict\n
"},{"location":"reference/optimize/#jaxley.optimize.transforms.ParamTransform.forward","title":"forward(params)","text":"

Pushes unconstrained parameters through a tf such that they fit the interval.

Parameters:

Name Type Description Default params List[Dict[str, ArrayLike]] | ArrayLike

A list of dictionaries (or any PyTree) with unconstrained parameters.

required

Returns:

Type Description Dict[str, Array]

A list of dictionaries (or any PyTree) with transformed parameters.

Source code in jaxley/optimize/transforms.py
def forward(\n    self, params: List[Dict[str, ArrayLike]] | ArrayLike\n) -> Dict[str, Array]:\n    \"\"\"Pushes unconstrained parameters through a tf such that they fit the interval.\n\n    Args:\n        params: A list of dictionaries (or any PyTree) with unconstrained parameters.\n\n    Returns:\n        A list of dictionaries (or any PyTree) with transformed parameters.\n\n    \"\"\"\n\n    return jax.tree_util.tree_map(lambda x, tf: tf.forward(x), params, self.tf_dict)\n
"},{"location":"reference/optimize/#jaxley.optimize.transforms.ParamTransform.inverse","title":"inverse(params)","text":"

Takes parameters from within the interval and makes them unconstrained.

Parameters:

Name Type Description Default params List[Dict[str, ArrayLike]] | ArrayLike

A list of dictionaries (or any PyTree) with transformed parameters.

required

Returns:

Type Description Dict[str, Array]

A list of dictionaries (or any PyTree) with unconstrained parameters.

Source code in jaxley/optimize/transforms.py
def inverse(\n    self, params: List[Dict[str, ArrayLike]] | ArrayLike\n) -> Dict[str, Array]:\n    \"\"\"Takes parameters from within the interval and makes them unconstrained.\n\n    Args:\n        params: A list of dictionaries (or any PyTree) with transformed parameters.\n\n    Returns:\n        A list of dictionaries (or any PyTree) with unconstrained parameters.\n    \"\"\"\n\n    return jax.tree_util.tree_map(lambda x, tf: tf.inverse(x), params, self.tf_dict)\n
"},{"location":"reference/optimize/#jaxley.optimize.transforms.SigmoidTransform","title":"SigmoidTransform","text":"

Bases: Transform

Sigmoid transformation.

Source code in jaxley/optimize/transforms.py
class SigmoidTransform(Transform):\n    \"\"\"Sigmoid transformation.\"\"\"\n\n    def __init__(self, lower: ArrayLike, upper: ArrayLike) -> None:\n        \"\"\"This transform maps any value bijectively to the interval [lower, upper].\n\n        Args:\n            lower (ArrayLike): Lower bound of the interval.\n            upper (ArrayLike): Upper bound of the interval.\n        \"\"\"\n        super().__init__()\n        self.lower = lower\n        self.width = upper - lower\n\n    def forward(self, x: ArrayLike) -> Array:\n        y = 1.0 / (1.0 + save_exp(-x))\n        return self.lower + self.width * y\n\n    def inverse(self, y: ArrayLike) -> Array:\n        x = (y - self.lower) / self.width\n        x = -jnp.log((1.0 / x) - 1.0)\n        return x\n
"},{"location":"reference/optimize/#jaxley.optimize.transforms.SigmoidTransform.__init__","title":"__init__(lower, upper)","text":"

This transform maps any value bijectively to the interval [lower, upper].

Parameters:

Name Type Description Default lower ArrayLike

Lower bound of the interval.

required upper ArrayLike

Upper bound of the interval.

required Source code in jaxley/optimize/transforms.py
def __init__(self, lower: ArrayLike, upper: ArrayLike) -> None:\n    \"\"\"This transform maps any value bijectively to the interval [lower, upper].\n\n    Args:\n        lower (ArrayLike): Lower bound of the interval.\n        upper (ArrayLike): Upper bound of the interval.\n    \"\"\"\n    super().__init__()\n    self.lower = lower\n    self.width = upper - lower\n
"},{"location":"reference/optimize/#jaxley.optimize.transforms.SoftplusTransform","title":"SoftplusTransform","text":"

Bases: Transform

Softplus transformation.

Source code in jaxley/optimize/transforms.py
class SoftplusTransform(Transform):\n    \"\"\"Softplus transformation.\"\"\"\n\n    def __init__(self, lower: ArrayLike) -> None:\n        \"\"\"This transform maps any value bijectively to the interval [lower, inf).\n\n        Args:\n            lower (ArrayLike): Lower bound of the interval.\n        \"\"\"\n        super().__init__()\n        self.lower = lower\n\n    def forward(self, x: ArrayLike) -> Array:\n        return jnp.log1p(save_exp(x)) + self.lower\n\n    def inverse(self, y: ArrayLike) -> Array:\n        return jnp.log(save_exp(y - self.lower) - 1.0)\n
"},{"location":"reference/optimize/#jaxley.optimize.transforms.SoftplusTransform.__init__","title":"__init__(lower)","text":"

This transform maps any value bijectively to the interval [lower, inf).

Parameters:

Name Type Description Default lower ArrayLike

Lower bound of the interval.

required Source code in jaxley/optimize/transforms.py
def __init__(self, lower: ArrayLike) -> None:\n    \"\"\"This transform maps any value bijectively to the interval [lower, inf).\n\n    Args:\n        lower (ArrayLike): Lower bound of the interval.\n    \"\"\"\n    super().__init__()\n    self.lower = lower\n
"},{"location":"reference/utils/","title":"Utils","text":""},{"location":"reference/utils/#jaxley.utils.cell_utils.build_radiuses_from_xyzr","title":"build_radiuses_from_xyzr(radius_fns, branch_indices, min_radius, nseg)","text":"

Return the radiuses of branches given SWC file xyzr.

Returns an array of shape (num_branches, nseg).

Parameters:

Name Type Description Default radius_fns List[Callable]

Functions which, given compartment locations return the radius.

required branch_indices List[int]

The indices of the branches for which to return the radiuses.

required min_radius Optional[float]

If passed, the radiuses are clipped to be at least as large.

required nseg int

The number of compartments that every branch is discretized into.

required Source code in jaxley/utils/cell_utils.py
def build_radiuses_from_xyzr(\n    radius_fns: List[Callable],\n    branch_indices: List[int],\n    min_radius: Optional[float],\n    nseg: int,\n) -> jnp.ndarray:\n    \"\"\"Return the radiuses of branches given SWC file xyzr.\n\n    Returns an array of shape `(num_branches, nseg)`.\n\n    Args:\n        radius_fns: Functions which, given compartment locations return the radius.\n        branch_indices: The indices of the branches for which to return the radiuses.\n        min_radius: If passed, the radiuses are clipped to be at least as large.\n        nseg: The number of compartments that every branch is discretized into.\n    \"\"\"\n    # Compartment locations are at the center of the internal nodes.\n    non_split = 1 / nseg\n    range_ = np.linspace(non_split / 2, 1 - non_split / 2, nseg)\n\n    # Build radiuses.\n    radiuses = np.asarray([radius_fns[b](range_) for b in branch_indices])\n    radiuses_each = radiuses.ravel(order=\"C\")\n    if min_radius is None:\n        assert np.all(\n            radiuses_each > 0.0\n        ), \"Radius 0.0 in SWC file. Set `read_swc(..., min_radius=...)`.\"\n    else:\n        radiuses_each[radiuses_each < min_radius] = min_radius\n\n    return radiuses_each\n
"},{"location":"reference/utils/#jaxley.utils.cell_utils.compute_axial_conductances","title":"compute_axial_conductances(comp_edges, params)","text":"

Given comp_edges, radius, length, r_a, cm, compute the axial conductances.

Note that the resulting axial conductances will already by divided by the capacitance cm.

Source code in jaxley/utils/cell_utils.py
def compute_axial_conductances(\n    comp_edges: pd.DataFrame, params: Dict[str, jnp.ndarray]\n) -> jnp.ndarray:\n    \"\"\"Given `comp_edges`, radius, length, r_a, cm, compute the axial conductances.\n\n    Note that the resulting axial conductances will already by divided by the\n    capacitance `cm`.\n    \"\"\"\n    # `Compartment-to-compartment` (c2c) axial coupling conductances.\n    condition = comp_edges[\"type\"].to_numpy() == 0\n    source_comp_inds = np.asarray(comp_edges[condition][\"source\"].to_list())\n    sink_comp_inds = np.asarray(comp_edges[condition][\"sink\"].to_list())\n\n    if len(sink_comp_inds) > 0:\n        conds_c2c = (\n            vmap(compute_coupling_cond, in_axes=(0, 0, 0, 0, 0, 0))(\n                params[\"radius\"][sink_comp_inds],\n                params[\"radius\"][source_comp_inds],\n                params[\"axial_resistivity\"][sink_comp_inds],\n                params[\"axial_resistivity\"][source_comp_inds],\n                params[\"length\"][sink_comp_inds],\n                params[\"length\"][source_comp_inds],\n            )\n            / params[\"capacitance\"][sink_comp_inds]\n        )\n    else:\n        conds_c2c = jnp.asarray([])\n\n    # `branchpoint-to-compartment` (bp2c) axial coupling conductances.\n    condition = comp_edges[\"type\"].isin([1, 2])\n    sink_comp_inds = np.asarray(comp_edges[condition][\"sink\"].to_list())\n\n    if len(sink_comp_inds) > 0:\n        conds_bp2c = (\n            vmap(compute_coupling_cond_branchpoint, in_axes=(0, 0, 0))(\n                params[\"radius\"][sink_comp_inds],\n                params[\"axial_resistivity\"][sink_comp_inds],\n                params[\"length\"][sink_comp_inds],\n            )\n            / params[\"capacitance\"][sink_comp_inds]\n        )\n    else:\n        conds_bp2c = jnp.asarray([])\n\n    # `compartment-to-branchpoint` (c2bp) axial coupling conductances.\n    condition = comp_edges[\"type\"].isin([3, 4])\n    source_comp_inds = np.asarray(comp_edges[condition][\"source\"].to_list())\n\n    if len(source_comp_inds) > 0:\n        conds_c2bp = vmap(compute_impact_on_node, in_axes=(0, 0, 0))(\n            params[\"radius\"][source_comp_inds],\n            params[\"axial_resistivity\"][source_comp_inds],\n            params[\"length\"][source_comp_inds],\n        )\n        # For numerical stability. These values are very small, but their scale\n        # does not matter.\n        conds_c2bp *= 1_000\n    else:\n        conds_c2bp = jnp.asarray([])\n\n    # All axial coupling conductances.\n    return jnp.concatenate([conds_c2c, conds_bp2c, conds_c2bp])\n
"},{"location":"reference/utils/#jaxley.utils.cell_utils.compute_children_and_parents","title":"compute_children_and_parents(branch_edges)","text":"

Build indices used during `._init_morph_custom_spsolve().

Source code in jaxley/utils/cell_utils.py
def compute_children_and_parents(\n    branch_edges: pd.DataFrame,\n) -> Tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray, int]:\n    \"\"\"Build indices used during `._init_morph_custom_spsolve().\"\"\"\n    par_inds = branch_edges[\"parent_branch_index\"].to_numpy()\n    child_inds = branch_edges[\"child_branch_index\"].to_numpy()\n    child_belongs_to_branchpoint = remap_to_consecutive(par_inds)\n    par_inds = np.unique(par_inds)\n    return par_inds, child_inds, child_belongs_to_branchpoint\n
"},{"location":"reference/utils/#jaxley.utils.cell_utils.compute_children_indices","title":"compute_children_indices(parents)","text":"

Return all children indices of every branch.

Example:

parents = [-1, 0, 0]\ncompute_children_indices(parents) -> [[1, 2], [], []]\n

Source code in jaxley/utils/cell_utils.py
def compute_children_indices(parents) -> List[jnp.ndarray]:\n    \"\"\"Return all children indices of every branch.\n\n    Example:\n    ```\n    parents = [-1, 0, 0]\n    compute_children_indices(parents) -> [[1, 2], [], []]\n    ```\n    \"\"\"\n    num_branches = len(parents)\n    child_indices = []\n    for b in range(num_branches):\n        child_indices.append(np.where(parents == b)[0])\n    return child_indices\n
"},{"location":"reference/utils/#jaxley.utils.cell_utils.compute_coupling_cond","title":"compute_coupling_cond(rad1, rad2, r_a1, r_a2, l1, l2)","text":"

Return the coupling conductance between two compartments.

Equations taken from https://en.wikipedia.org/wiki/Compartmental_neuron_models.

radius: um r_a: ohm cm length_single_compartment: um coupling_conds: S * um / cm / um^2 = S / cm / um -> *10**7 -> mS / cm^2

Source code in jaxley/utils/cell_utils.py
def compute_coupling_cond(rad1, rad2, r_a1, r_a2, l1, l2):\n    \"\"\"Return the coupling conductance between two compartments.\n\n    Equations taken from `https://en.wikipedia.org/wiki/Compartmental_neuron_models`.\n\n    `radius`: um\n    `r_a`: ohm cm\n    `length_single_compartment`: um\n    `coupling_conds`: S * um / cm / um^2 = S / cm / um -> *10**7 -> mS / cm^2\n    \"\"\"\n    # Multiply by 10**7 to convert (S / cm / um) -> (mS / cm^2).\n    return rad1 * rad2**2 / (r_a1 * rad2**2 * l1 + r_a2 * rad1**2 * l2) / l1 * 10**7\n
"},{"location":"reference/utils/#jaxley.utils.cell_utils.compute_coupling_cond_branchpoint","title":"compute_coupling_cond_branchpoint(rad, r_a, l)","text":"

Return the coupling conductance between one compartment and a comp with l=0.

From https://en.wikipedia.org/wiki/Compartmental_neuron_models

If one compartment has l=0.0 then the equations simplify.

R_long = \\sum_i r_a * L_i/2 / crosssection_i

with crosssection = pi * r**2

For a single compartment with L>0, this turns into: R_long = r_a * L/2 / crosssection

Then, g_long = crosssection * 2 / L / r_a

Then, the effective conductance is g_long / zylinder_area. So: g = pi * r**2 * 2 / L / r_a / 2 / pi / r / L g = r / r_a / L**2

Source code in jaxley/utils/cell_utils.py
def compute_coupling_cond_branchpoint(rad, r_a, l):\n    r\"\"\"Return the coupling conductance between one compartment and a comp with l=0.\n\n    From https://en.wikipedia.org/wiki/Compartmental_neuron_models\n\n    If one compartment has l=0.0 then the equations simplify.\n\n    R_long = \\sum_i r_a * L_i/2 / crosssection_i\n\n    with crosssection = pi * r**2\n\n    For a single compartment with L>0, this turns into:\n    R_long = r_a * L/2 / crosssection\n\n    Then, g_long = crosssection * 2 / L / r_a\n\n    Then, the effective conductance is g_long / zylinder_area. So:\n    g = pi * r**2 * 2 / L / r_a / 2 / pi / r / L\n    g = r / r_a / L**2\n    \"\"\"\n    return rad / r_a / l**2 * 10**7  # Convert (S / cm / um) -> (mS / cm^2)\n
"},{"location":"reference/utils/#jaxley.utils.cell_utils.compute_impact_on_node","title":"compute_impact_on_node(rad, r_a, l)","text":"

Compute the weight with which a compartment influences its node.

In order to satisfy Kirchhoffs current law, the current at a branch point must be proportional to the crosssection of the compartment. We only require proportionality here because the branch point equation reads: g_1 * (V_1 - V_b) + g_2 * (V_2 - V_b) = 0.0

Because R_long = r_a * L/2 / crosssection, we get g_long = crosssection * 2 / L / r_a \\propto rad**2 / L / r_a

This equation can be multiplied by any constant.

Source code in jaxley/utils/cell_utils.py
def compute_impact_on_node(rad, r_a, l):\n    r\"\"\"Compute the weight with which a compartment influences its node.\n\n    In order to satisfy Kirchhoffs current law, the current at a branch point must be\n    proportional to the crosssection of the compartment. We only require proportionality\n    here because the branch point equation reads:\n    `g_1 * (V_1 - V_b) + g_2 * (V_2 - V_b) = 0.0`\n\n    Because R_long = r_a * L/2 / crosssection, we get\n    g_long = crosssection * 2 / L / r_a \\propto rad**2 / L / r_a\n\n    This equation can be multiplied by any constant.\"\"\"\n    return rad**2 / r_a / l\n
"},{"location":"reference/utils/#jaxley.utils.cell_utils.compute_morphology_indices_in_levels","title":"compute_morphology_indices_in_levels(num_branchpoints, child_belongs_to_branchpoint, par_inds, child_inds)","text":"

Return (row, col) to build the sparse matrix defining the voltage eqs.

This is run at init, not during runtime.

Source code in jaxley/utils/cell_utils.py
def compute_morphology_indices_in_levels(\n    num_branchpoints,\n    child_belongs_to_branchpoint,\n    par_inds,\n    child_inds,\n):\n    \"\"\"Return (row, col) to build the sparse matrix defining the voltage eqs.\n\n    This is run at `init`, not during runtime.\n    \"\"\"\n    branchpoint_inds_parents = jnp.arange(num_branchpoints)\n    branchpoint_inds_children = child_belongs_to_branchpoint\n    branch_inds_parents = par_inds\n    branch_inds_children = child_inds\n\n    children = jnp.stack([branch_inds_children, branchpoint_inds_children])\n    parents = jnp.stack([branch_inds_parents, branchpoint_inds_parents])\n\n    return {\"children\": children.T, \"parents\": parents.T}\n
"},{"location":"reference/utils/#jaxley.utils.cell_utils.convert_point_process_to_distributed","title":"convert_point_process_to_distributed(current, radius, length)","text":"

Convert current point process (nA) to distributed current (uA/cm2).

This function gets called for synapses and for external stimuli.

Parameters:

Name Type Description Default current ndarray

Current in nA.

required radius ndarray

Compartment radius in um.

required length ndarray

Compartment length in um.

required Return

Current in uA/cm2.

Source code in jaxley/utils/cell_utils.py
def convert_point_process_to_distributed(\n    current: jnp.ndarray, radius: jnp.ndarray, length: jnp.ndarray\n) -> jnp.ndarray:\n    \"\"\"Convert current point process (nA) to distributed current (uA/cm2).\n\n    This function gets called for synapses and for external stimuli.\n\n    Args:\n        current: Current in `nA`.\n        radius: Compartment radius in `um`.\n        length: Compartment length in `um`.\n\n    Return:\n        Current in `uA/cm2`.\n    \"\"\"\n    area = 2 * pi * radius * length\n    current /= area  # nA / um^2\n    return current * 100_000  # Convert (nA / um^2) to (uA / cm^2)\n
"},{"location":"reference/utils/#jaxley.utils.cell_utils.equal_segments","title":"equal_segments(branch_property, nseg_per_branch)","text":"

Generates segments where some property is the same in each segment.

Parameters:

Name Type Description Default branch_property list

List of values of the property in each branch. Should have len(branch_property) == num_branches.

required Source code in jaxley/utils/cell_utils.py
def equal_segments(branch_property: list, nseg_per_branch: int):\n    \"\"\"Generates segments where some property is the same in each segment.\n\n    Args:\n        branch_property: List of values of the property in each branch. Should have\n            `len(branch_property) == num_branches`.\n    \"\"\"\n    assert isinstance(branch_property, list), \"branch_property must be a list.\"\n    return jnp.asarray([branch_property] * nseg_per_branch).T\n
"},{"location":"reference/utils/#jaxley.utils.cell_utils.get_num_neighbours","title":"get_num_neighbours(num_children, nseg_per_branch, num_branches)","text":"

Number of neighbours of each compartment.

Source code in jaxley/utils/cell_utils.py
def get_num_neighbours(\n    num_children: jnp.ndarray,\n    nseg_per_branch: int,\n    num_branches: int,\n):\n    \"\"\"\n    Number of neighbours of each compartment.\n    \"\"\"\n    num_neighbours = 2 * jnp.ones((num_branches * nseg_per_branch))\n    num_neighbours = num_neighbours.at[nseg_per_branch - 1].set(1.0)\n    num_neighbours = num_neighbours.at[jnp.arange(num_branches) * nseg_per_branch].set(\n        num_children + 1.0\n    )\n    return num_neighbours\n
"},{"location":"reference/utils/#jaxley.utils.cell_utils.group_and_sum","title":"group_and_sum(values_to_sum, inds_to_group_by, num_branchpoints)","text":"

Group values by whether they have the same integer and sum values within group.

This is used to construct the last diagonals at the branch points.

Written by ChatGPT.

Source code in jaxley/utils/cell_utils.py
def group_and_sum(\n    values_to_sum: jnp.ndarray, inds_to_group_by: jnp.ndarray, num_branchpoints: int\n) -> jnp.ndarray:\n    \"\"\"Group values by whether they have the same integer and sum values within group.\n\n    This is used to construct the last diagonals at the branch points.\n\n    Written by ChatGPT.\n    \"\"\"\n    # Initialize an array to hold the sum of each group\n    group_sums = jnp.zeros(num_branchpoints)\n\n    # `.at[inds]` requires that `inds` is not empty, so we need an if-case here.\n    # `len(inds) == 0` is the case for branches and compartments.\n    if num_branchpoints > 0:\n        group_sums = group_sums.at[inds_to_group_by].add(values_to_sum)\n\n    return group_sums\n
"},{"location":"reference/utils/#jaxley.utils.cell_utils.interpolate_xyzr","title":"interpolate_xyzr(loc, coords)","text":"

Perform a linear interpolation between xyz-coordinates.

Parameters:

Name Type Description Default loc float

The location in [0,1] along the branch.

required coords ndarray

Array containing the reconstructed xyzr points of the branch.

required Return

Interpolated xyz coordinate at loc, shape `(3,).

Source code in jaxley/utils/cell_utils.py
def interpolate_xyzr(loc: float, coords: np.ndarray):\n    \"\"\"Perform a linear interpolation between xyz-coordinates.\n\n    Args:\n        loc: The location in [0,1] along the branch.\n        coords: Array containing the reconstructed xyzr points of the branch.\n\n    Return:\n        Interpolated xyz coordinate at `loc`, shape `(3,).\n    \"\"\"\n    dl = np.sqrt(np.sum(np.diff(coords[:, :3], axis=0) ** 2, axis=1))\n    pathlens = np.insert(np.cumsum(dl), 0, 0)  # cummulative length of sections\n    norm_pathlens = pathlens / np.maximum(1e-8, pathlens[-1])  # norm lengths to [0,1].\n\n    return v_interp(loc, norm_pathlens, coords)\n
"},{"location":"reference/utils/#jaxley.utils.cell_utils.linear_segments","title":"linear_segments(initial_val, endpoint_vals, parents, nseg_per_branch)","text":"

Generates segments where some property is linearly interpolated.

Parameters:

Name Type Description Default initial_val float

The value at the tip of the soma.

required endpoint_vals list

The value at the endpoints of each branch.

required Source code in jaxley/utils/cell_utils.py
def linear_segments(\n    initial_val: float, endpoint_vals: list, parents: jnp.ndarray, nseg_per_branch: int\n):\n    \"\"\"Generates segments where some property is linearly interpolated.\n\n    Args:\n        initial_val: The value at the tip of the soma.\n        endpoint_vals: The value at the endpoints of each branch.\n    \"\"\"\n    branch_property = endpoint_vals + [initial_val]\n    num_branches = len(parents)\n    # Compute radiuses by linear interpolation.\n    endpoint_radiuses = jnp.asarray(branch_property)\n\n    def compute_rad(branch_ind, loc):\n        start = endpoint_radiuses[parents[branch_ind]]\n        end = endpoint_radiuses[branch_ind]\n        return (end - start) * loc + start\n\n    branch_inds_of_each_comp = jnp.tile(jnp.arange(num_branches), nseg_per_branch)\n    locs_of_each_comp = jnp.linspace(1, 0, nseg_per_branch).repeat(num_branches)\n    rad_of_each_comp = compute_rad(branch_inds_of_each_comp, locs_of_each_comp)\n\n    return jnp.reshape(rad_of_each_comp, (nseg_per_branch, num_branches)).T\n
"},{"location":"reference/utils/#jaxley.utils.cell_utils.loc_of_index","title":"loc_of_index(global_comp_index, global_branch_index, nseg_per_branch)","text":"

Return location corresponding to global compartment index.

Source code in jaxley/utils/cell_utils.py
def loc_of_index(global_comp_index, global_branch_index, nseg_per_branch):\n    \"\"\"Return location corresponding to global compartment index.\"\"\"\n    cumsum_nseg = cumsum_leading_zero(nseg_per_branch)\n    index = global_comp_index - cumsum_nseg[global_branch_index]\n    nseg = nseg_per_branch[global_branch_index]\n    return (0.5 + index) / nseg\n
"},{"location":"reference/utils/#jaxley.utils.cell_utils.local_index_of_loc","title":"local_index_of_loc(loc, global_branch_ind, nseg_per_branch)","text":"

Returns the local index of a comp given a loc [0, 1] and the index of a branch.

This is used because we specify locations such as synapses as a value between 0 and 1. We have to convert this onto a discrete segment here.

Parameters:

Name Type Description Default branch_ind

Index of the branch.

required loc float

Location (in [0, 1]) along that branch.

required nseg_per_branch int

Number of segments of each branch.

required

Returns:

Type Description int

The local index of the compartment.

Source code in jaxley/utils/cell_utils.py
def local_index_of_loc(loc: float, global_branch_ind: int, nseg_per_branch: int) -> int:\n    \"\"\"Returns the local index of a comp given a loc [0, 1] and the index of a branch.\n\n    This is used because we specify locations such as synapses as a value between 0 and\n    1. We have to convert this onto a discrete segment here.\n\n    Args:\n        branch_ind: Index of the branch.\n        loc: Location (in [0, 1]) along that branch.\n        nseg_per_branch: Number of segments of each branch.\n\n    Returns:\n        The local index of the compartment.\n    \"\"\"\n    nseg = nseg_per_branch[global_branch_ind]  # only for convenience.\n    possible_locs = np.linspace(0.5 / nseg, 1 - 0.5 / nseg, nseg)\n    ind_along_branch = np.argmin(np.abs(possible_locs - loc))\n    return ind_along_branch\n
"},{"location":"reference/utils/#jaxley.utils.cell_utils.merge_cells","title":"merge_cells(cumsum_num_branches, cumsum_num_branchpoints, arrs, exclude_first=True)","text":"

Build full list of which branches are solved in which iteration.

From the branching pattern of single cells, this \u201cmerges\u201d them into a single ordering of branches.

Parameters:

Name Type Description Default cumsum_num_branches List[int]

cumulative number of branches. E.g., for three cells with 10, 15, and 5 branches respectively, this will should be a list containing [0, 10, 25, 30].

required arrs List[List[ndarray]]

A list of a list of arrays that should be merged.

required exclude_first bool

If True, the first element of each list in arrs will remain unchanged. Useful if a -1 (which indicates \u201cno parent\u201d) entry should not be changed.

True

Returns:

Type Description ndarray

A list of arrays which contain the branch indices that are computed at each

ndarray

level (i.e., iteration).

Source code in jaxley/utils/cell_utils.py
def merge_cells(\n    cumsum_num_branches: List[int],\n    cumsum_num_branchpoints: List[int],\n    arrs: List[List[np.ndarray]],\n    exclude_first: bool = True,\n) -> np.ndarray:\n    \"\"\"\n    Build full list of which branches are solved in which iteration.\n\n    From the branching pattern of single cells, this \"merges\" them into a single\n    ordering of branches.\n\n    Args:\n        cumsum_num_branches: cumulative number of branches. E.g., for three cells with\n            10, 15, and 5 branches respectively, this will should be a list containing\n            `[0, 10, 25, 30]`.\n        arrs: A list of a list of arrays that should be merged.\n        exclude_first: If `True`, the first element of each list in `arrs` will remain\n            unchanged. Useful if a `-1` (which indicates \"no parent\") entry should not\n            be changed.\n\n    Returns:\n        A list of arrays which contain the branch indices that are computed at each\n        level (i.e., iteration).\n    \"\"\"\n    ps = []\n    for i, att in enumerate(arrs):\n        p = att\n        if exclude_first:\n            raise NotImplementedError\n            p = [p[0]] + [p_in_level + cumsum_num_branches[i] for p_in_level in p[1:]]\n        else:\n            p = [\n                p_in_level\n                + np.asarray([cumsum_num_branches[i], cumsum_num_branchpoints[i]])\n                for p_in_level in p\n            ]\n        ps.append(p)\n\n    max_len = max([len(att) for att in arrs])\n    combined_parents_in_level = []\n    for i in range(max_len):\n        current_ps = []\n        for p in ps:\n            if len(p) > i:\n                current_ps.append(p[i])\n        combined_parents_in_level.append(np.concatenate(current_ps))\n\n    return combined_parents_in_level\n
"},{"location":"reference/utils/#jaxley.utils.cell_utils.params_to_pstate","title":"params_to_pstate(params, indices_set_by_trainables)","text":"

Make outputs get_parameters() conform with outputs of .data_set().

make_trainable() followed by params=get_parameters() does not return indices because these indices would also be differentiated by jax.grad (as soon as the params are passed to def simulate(params). Therefore, in jx.integrate, we run the function to add indices to the dict. The outputs of params_to_pstate are of the same shape as the outputs of .data_set().

Source code in jaxley/utils/cell_utils.py
def params_to_pstate(\n    params: List[Dict[str, jnp.ndarray]],\n    indices_set_by_trainables: List[jnp.ndarray],\n):\n    \"\"\"Make outputs `get_parameters()` conform with outputs of `.data_set()`.\n\n    `make_trainable()` followed by `params=get_parameters()` does not return indices\n    because these indices would also be differentiated by `jax.grad` (as soon as\n    the `params` are passed to `def simulate(params)`. Therefore, in `jx.integrate`,\n    we run the function to add indices to the dict. The outputs of `params_to_pstate`\n    are of the same shape as the outputs of `.data_set()`.\"\"\"\n    return [\n        {\"key\": list(p.keys())[0], \"val\": list(p.values())[0], \"indices\": i}\n        for p, i in zip(params, indices_set_by_trainables)\n    ]\n
"},{"location":"reference/utils/#jaxley.utils.cell_utils.query_channel_states_and_params","title":"query_channel_states_and_params(d, keys, idcs)","text":"

Get dict with subset of keys and values from d.

This is used to restrict a dict where every item contains all states to only the ones that are relevant for the channel. E.g.

states = {'eCa': Array([ 0., 0., nan]}

will be states = {'eCa': Array([ 0., 0.]}

Only loops over necessary keys, as opposed to looping over d.items().

Source code in jaxley/utils/cell_utils.py
def query_channel_states_and_params(d, keys, idcs):\n    \"\"\"Get dict with subset of keys and values from d.\n\n    This is used to restrict a dict where every item contains __all__ states to only\n    the ones that are relevant for the channel. E.g.\n\n    ```states = {'eCa': Array([ 0.,  0., nan]}```\n\n    will be\n    ```states = {'eCa': Array([ 0.,  0.]}```\n\n    Only loops over necessary keys, as opposed to looping over `d.items()`.\"\"\"\n    return dict(zip(keys, (v[idcs] for v in map(d.get, keys))))\n
"},{"location":"reference/utils/#jaxley.utils.cell_utils.remap_to_consecutive","title":"remap_to_consecutive(arr)","text":"

Maps an array of integers to an array of consecutive integers.

E.g. [0, 0, 1, 4, 4, 6, 6] -> [0, 0, 1, 2, 2, 3, 3]

Source code in jaxley/utils/cell_utils.py
def remap_to_consecutive(arr):\n    \"\"\"Maps an array of integers to an array of consecutive integers.\n\n    E.g. `[0, 0, 1, 4, 4, 6, 6] -> [0, 0, 1, 2, 2, 3, 3]`\n    \"\"\"\n    _, inverse_indices = jnp.unique(arr, return_inverse=True)\n    return inverse_indices\n
"},{"location":"reference/utils/#jaxley.utils.plot_utils.compute_rotation_matrix","title":"compute_rotation_matrix(axis, angle)","text":"

Return the rotation matrix associated with counterclockwise rotation about the given axis by the given angle.

Can be used to rotate a coordinate vector by multiplying it with the rotation matrix.

Parameters:

Name Type Description Default axis ndarray

The axis of rotation.

required angle float

The angle of rotation in radians.

required

Returns:

Type Description ndarray

A 3x3 rotation matrix.

Source code in jaxley/utils/plot_utils.py
def compute_rotation_matrix(axis: ndarray, angle: float) -> ndarray:\n    \"\"\"\n    Return the rotation matrix associated with counterclockwise rotation about\n    the given axis by the given angle.\n\n    Can be used to rotate a coordinate vector by multiplying it with the rotation\n    matrix.\n\n    Args:\n        axis: The axis of rotation.\n        angle: The angle of rotation in radians.\n\n    Returns:\n        A 3x3 rotation matrix.\n    \"\"\"\n    axis = axis / np.sqrt(np.dot(axis, axis))\n    a = np.cos(angle / 2.0)\n    b, c, d = -axis * np.sin(angle / 2.0)\n    aa, bb, cc, dd = a * a, b * b, c * c, d * d\n    bc, ad, ac, ab, bd, cd = b * c, a * d, a * c, a * b, b * d, c * d\n    return np.array(\n        [\n            [aa + bb - cc - dd, 2 * (bc + ad), 2 * (bd - ac)],\n            [2 * (bc - ad), aa + cc - bb - dd, 2 * (cd + ab)],\n            [2 * (bd + ac), 2 * (cd - ab), aa + dd - bb - cc],\n        ]\n    )\n
"},{"location":"reference/utils/#jaxley.utils.plot_utils.create_cone_frustum_mesh","title":"create_cone_frustum_mesh(length, radius_bottom, radius_top, bottom_dome=False, top_dome=False, resolution=100)","text":"

Generates mesh points for a cone frustum, with optional domes at either end.

This is used to render the traced morphology in 3D (and to project it to 2D) as part of plot_morph. Sections between two traced coordinates with two different radii can be represented by a cone frustum. Additionally, the ends of the frustum can be capped with hemispheres to ensure that two neighbouring frustums are connected smoothly (like ball joints).

Parameters:

Name Type Description Default length float

The length of the frustum.

required radius_bottom float

The radius of the bottom of the frustum.

required radius_top float

The radius of the top of the frustum.

required bottom_dome bool

If True, a dome is added to the bottom of the frustum. The dome is a hemisphere with radius radius_bottom.

False top_dome bool

If True, a dome is added to the top of the frustum. The dome is a hemisphere with radius radius_top.

False resolution int

defines the resolution of the mesh. If too low (typically <10), can result in errors. Useful too have a simpler mesh for plotting.

100

Returns:

Type Description ndarray

An array of mesh points.

Source code in jaxley/utils/plot_utils.py
def create_cone_frustum_mesh(\n    length: float,\n    radius_bottom: float,\n    radius_top: float,\n    bottom_dome: bool = False,\n    top_dome: bool = False,\n    resolution: int = 100,\n) -> ndarray:\n    \"\"\"Generates mesh points for a cone frustum, with optional domes at either end.\n\n    This is used to render the traced morphology in 3D (and to project it to 2D)\n    as part of `plot_morph`. Sections between two traced coordinates with two\n    different radii can be represented by a cone frustum. Additionally, the ends\n    of the frustum can be capped with hemispheres to ensure that two neighbouring\n    frustums are connected smoothly (like ball joints).\n\n    Args:\n        length: The length of the frustum.\n        radius_bottom: The radius of the bottom of the frustum.\n        radius_top: The radius of the top of the frustum.\n        bottom_dome: If True, a dome is added to the bottom of the frustum.\n            The dome is a hemisphere with radius `radius_bottom`.\n        top_dome: If True, a dome is added to the top of the frustum.\n            The dome is a hemisphere with radius `radius_top`.\n        resolution: defines the resolution of the mesh.\n            If too low (typically <10), can result in errors.\n            Useful too have a simpler mesh for plotting.\n\n    Returns:\n        An array of mesh points.\n    \"\"\"\n\n    t = np.linspace(0, 2 * np.pi, resolution)\n\n    # Determine the total height including domes\n    total_height = length\n    total_height += radius_bottom if bottom_dome else 0\n    total_height += radius_top if top_dome else 0\n\n    z = np.linspace(0, total_height, resolution)\n    t_grid, z_coords = np.meshgrid(t, z)\n\n    # Initialize arrays\n    x_coords = np.zeros_like(t_grid)\n    y_coords = np.zeros_like(t_grid)\n    r_coords = np.zeros_like(t_grid)\n\n    # Bottom hemisphere\n    if bottom_dome:\n        dome_mask = z_coords < radius_bottom\n        arg = 1 - z_coords[dome_mask] / radius_bottom\n        arg[np.isclose(arg, 1, atol=1e-6, rtol=1e-6)] = 1\n        arg[np.isclose(arg, -1, atol=1e-6, rtol=1e-6)] = -1\n        phi = np.arccos(1 - z_coords[dome_mask] / radius_bottom)\n        r_coords[dome_mask] = radius_bottom * np.sin(phi)\n        z_coords[dome_mask] = z_coords[dome_mask]\n\n    # Frustum\n    frustum_start = radius_bottom if bottom_dome else 0\n    frustum_end = total_height - (radius_top if top_dome else 0)\n    frustum_mask = (z_coords >= frustum_start) & (z_coords <= frustum_end)\n    z_frustum = z_coords[frustum_mask] - frustum_start\n    r_coords[frustum_mask] = radius_bottom + (radius_top - radius_bottom) * (\n        z_frustum / length\n    )\n\n    # Top hemisphere\n    if top_dome:\n        dome_mask = z_coords > (total_height - radius_top)\n        arg = (z_coords[dome_mask] - (total_height - radius_top)) / radius_top\n        arg[np.isclose(arg, 1, atol=1e-6, rtol=1e-6)] = 1\n        arg[np.isclose(arg, -1, atol=1e-6, rtol=1e-6)] = -1\n        phi = np.arccos(arg)\n        r_coords[dome_mask] = radius_top * np.sin(phi)\n\n    x_coords = r_coords * np.cos(t_grid)\n    y_coords = r_coords * np.sin(t_grid)\n\n    return np.stack([x_coords, y_coords, z_coords])\n
"},{"location":"reference/utils/#jaxley.utils.plot_utils.create_cylinder_mesh","title":"create_cylinder_mesh(length, radius, resolution=100)","text":"

Generates mesh points for a cylinder.

This is used to render cylindrical compartments in 3D (and to project it to 2D) as part of plot_comps.

Parameters:

Name Type Description Default length float

The length of the cylinder.

required radius float

The radius of the cylinder.

required resolution int

defines the resolution of the mesh. If too low (typically <10), can result in errors. Useful too have a simpler mesh for plotting.

100

Returns:

Type Description ndarray

An array of mesh points.

Source code in jaxley/utils/plot_utils.py
def create_cylinder_mesh(\n    length: float, radius: float, resolution: int = 100\n) -> ndarray:\n    \"\"\"Generates mesh points for a cylinder.\n\n    This is used to render cylindrical compartments in 3D (and to project it to 2D)\n    as part of `plot_comps`.\n\n    Args:\n        length: The length of the cylinder.\n        radius: The radius of the cylinder.\n        resolution: defines the resolution of the mesh.\n            If too low (typically <10), can result in errors.\n            Useful too have a simpler mesh for plotting.\n\n    Returns:\n        An array of mesh points.\n    \"\"\"\n    # Define cylinder\n    t = np.linspace(0, 2 * np.pi, resolution)\n    z_coords = np.linspace(-length / 2, length / 2, resolution)\n    t_grid, z_coords = np.meshgrid(t, z_coords)\n\n    x_coords = radius * np.cos(t_grid)\n    y_coords = radius * np.sin(t_grid)\n    return np.stack([x_coords, y_coords, z_coords])\n
"},{"location":"reference/utils/#jaxley.utils.plot_utils.create_sphere_mesh","title":"create_sphere_mesh(radius, resolution=100)","text":"

Generates mesh points for a sphere.

This is used to render spherical compartments in 3D (and to project it to 2D) as part of plot_comps.

Parameters:

Name Type Description Default radius float

The radius of the sphere.

required resolution int

defines the resolution of the mesh. If too low (typically <10), can result in errors. Useful too have a simpler mesh for plotting.

100

Returns:

Type Description ndarray

An array of mesh points.

Source code in jaxley/utils/plot_utils.py
def create_sphere_mesh(radius: float, resolution: int = 100) -> np.ndarray:\n    \"\"\"Generates mesh points for a sphere.\n\n    This is used to render spherical compartments in 3D (and to project it to 2D)\n    as part of `plot_comps`.\n\n    Args:\n        radius: The radius of the sphere.\n        resolution: defines the resolution of the mesh.\n            If too low (typically <10), can result in errors.\n            Useful too have a simpler mesh for plotting.\n\n    Returns:\n        An array of mesh points.\n    \"\"\"\n    phi = np.linspace(0, np.pi, resolution)\n    theta = np.linspace(0, 2 * np.pi, resolution)\n\n    # Create a 2D meshgrid for phi and theta\n    phi_coords, theta_coords = np.meshgrid(phi, theta)\n\n    # Convert spherical coordinates to Cartesian coordinates\n    x_coords = radius * np.sin(phi_coords) * np.cos(theta_coords)\n    y_coords = radius * np.sin(phi_coords) * np.sin(theta_coords)\n    z_coords = radius * np.cos(phi_coords)\n\n    return np.stack([x_coords, y_coords, z_coords])\n
"},{"location":"reference/utils/#jaxley.utils.plot_utils.extract_outline","title":"extract_outline(points)","text":"

Get the outline of a 2D/3D shape.

Extracts the subset of points which form the convex hull, i.e. the outline of the input points.

Parameters:

Name Type Description Default points ndarray

An array of points / corrdinates.

required

Returns:

Type Description ndarray

An array of points which form the convex hull.

Source code in jaxley/utils/plot_utils.py
def extract_outline(points: ndarray) -> ndarray:\n    \"\"\"Get the outline of a 2D/3D shape.\n\n    Extracts the subset of points which form the convex hull, i.e. the outline of\n    the input points.\n\n    Args:\n        points: An array of points / corrdinates.\n\n    Returns:\n        An array of points which form the convex hull.\n    \"\"\"\n    hull = ConvexHull(points)\n    hull_points = points[hull.vertices]\n    return hull_points\n
"},{"location":"reference/utils/#jaxley.utils.plot_utils.plot_comps","title":"plot_comps(module_or_view, dims=(0, 1), col='k', ax=None, comp_plot_kwargs={}, true_comp_length=True, resolution=100)","text":"

Plot compartmentalized neural morphology.

Plots the projection of the cylindrical compartments.

Parameters:

Name Type Description Default module_or_view Union[Module, View]

The module or view to plot.

required dims Tuple[int]

The dimensions to plot / to project the cylinder onto, i.e. [0,1] xy-plane or [0,1,2] for 3D.

(0, 1) col str

The color for all compartments

'k' ax Optional[Axes]

The matplotlib axis to plot on.

None comp_plot_kwargs Dict

The plot kwargs for plt.fill.

{} true_comp_length bool

If True, the length of the compartment is used, i.e. the length of the traced neurite. This means for zig-zagging neurites the cylinders will be longer than the straight-line distance between the start and end point of the neurite. This can lead to overlapping and miss-aligned cylinders. Setting this False will use the straight-line distance instead for nicer plots.

True resolution int

defines the resolution of the mesh. If too low (typically <10), can result in errors. Useful too have a simpler mesh for plotting.

100

Returns:

Type Description Axes

Plot of the compartmentalized morphology.

Source code in jaxley/utils/plot_utils.py
def plot_comps(\n    module_or_view: Union[\"jx.Module\", \"jx.View\"],\n    dims: Tuple[int] = (0, 1),\n    col: str = \"k\",\n    ax: Optional[Axes] = None,\n    comp_plot_kwargs: Dict = {},\n    true_comp_length: bool = True,\n    resolution: int = 100,\n) -> Axes:\n    \"\"\"Plot compartmentalized neural morphology.\n\n    Plots the projection of the cylindrical compartments.\n\n    Args:\n        module_or_view: The module or view to plot.\n        dims: The dimensions to plot / to project the cylinder onto,\n            i.e. [0,1] xy-plane or [0,1,2] for 3D.\n        col: The color for all compartments\n        ax: The matplotlib axis to plot on.\n        comp_plot_kwargs: The plot kwargs for plt.fill.\n        true_comp_length: If True, the length of the compartment is used, i.e. the\n            length of the traced neurite. This means for zig-zagging neurites the\n            cylinders will be longer than the straight-line distance between the\n            start and end point of the neurite. This can lead to overlapping and\n            miss-aligned cylinders. Setting this False will use the straight-line\n            distance instead for nicer plots.\n        resolution: defines the resolution of the mesh.\n            If too low (typically <10), can result in errors.\n            Useful too have a simpler mesh for plotting.\n\n    Returns:\n        Plot of the compartmentalized morphology.\n    \"\"\"\n    if ax is None:\n        fig = plt.figure(figsize=(3, 3))\n        ax = fig.add_subplot(111) if len(dims) < 3 else plt.axes(projection=\"3d\")\n\n    assert not np.any(\n        np.isnan(module_or_view.xyzr[0][:, :3])\n    ), \"missing xyz coordinates.\"\n    if \"x\" not in module_or_view.nodes.columns:\n        module_or_view.compute_compartment_centers()\n\n    for idx, xyzr in zip(module_or_view._branches_in_view, module_or_view.xyzr):\n        locs = xyzr[:, :3]\n        if locs.shape[0] == 1:  # assume spherical comp\n            radius = xyzr[:, -1]\n            center = xyzr[0, :3]\n            if len(dims) == 3:\n                xyz = create_sphere_mesh(radius, resolution)\n                ax = plot_mesh(\n                    xyz,\n                    np.array([0, 0, 1]),\n                    center,\n                    np.array(dims),\n                    ax,\n                    color=col,\n                    **comp_plot_kwargs,\n                )\n            else:\n                ax.add_artist(plt.Circle(locs[0, dims], radius, color=col))\n        else:\n            lens = np.sqrt(np.nansum(np.diff(locs, axis=0) ** 2, axis=1))\n            lens = np.cumsum([0] + lens.tolist())\n            comp_ends = v_interp(\n                np.linspace(0, lens[-1], module_or_view.nseg + 1), lens, locs\n            ).T\n            axes = np.diff(comp_ends, axis=0)\n            cylinder_lens = np.sqrt(np.sum(axes**2, axis=1))\n\n            branch_df = module_or_view.nodes[\n                module_or_view.nodes[\"global_branch_index\"] == idx\n            ]\n            for l, axis, (i, comp) in zip(cylinder_lens, axes, branch_df.iterrows()):\n                center = comp[[\"x\", \"y\", \"z\"]]\n                radius = comp[\"radius\"]\n                length = comp[\"length\"] if true_comp_length else l\n                xyz = create_cylinder_mesh(length, radius, resolution)\n                ax = plot_mesh(\n                    xyz,\n                    axis,\n                    center,\n                    np.array(dims),\n                    ax,\n                    color=col,\n                    **comp_plot_kwargs,\n                )\n    return ax\n
"},{"location":"reference/utils/#jaxley.utils.plot_utils.plot_graph","title":"plot_graph(xyzr, dims=(0, 1), col='k', ax=None, type='line', morph_plot_kwargs={})","text":"

Plot morphology.

Parameters:

Name Type Description Default xyzr ndarray

The coordinates of the morphology.

required dims Tuple[int]

Which dimensions to plot. 1=x, 2=y, 3=z coordinate. Must be a tuple of two or three of them.

(0, 1) col str

The color for all branches.

'k' ax Optional[Axes]

The matplotlib axis to plot on.

None type str

Either line or scatter.

'line' morph_plot_kwargs Dict

The plot kwargs for plt.plot or plt.scatter.

{} Source code in jaxley/utils/plot_utils.py
def plot_graph(\n    xyzr: ndarray,\n    dims: Tuple[int] = (0, 1),\n    col: str = \"k\",\n    ax: Optional[Axes] = None,\n    type: str = \"line\",\n    morph_plot_kwargs: Dict = {},\n) -> Axes:\n    \"\"\"Plot morphology.\n\n    Args:\n        xyzr: The coordinates of the morphology.\n        dims: Which dimensions to plot. 1=x, 2=y, 3=z coordinate. Must be a tuple of\n            two or three of them.\n        col: The color for all branches.\n        ax: The matplotlib axis to plot on.\n        type: Either `line` or `scatter`.\n        morph_plot_kwargs: The plot kwargs for plt.plot or plt.scatter.\n    \"\"\"\n\n    if ax is None:\n        fig = plt.figure(figsize=(3, 3))\n        ax = fig.add_subplot(111) if len(dims) < 3 else plt.axes(projection=\"3d\")\n\n    for coords_of_branch in xyzr:\n        points = coords_of_branch[:, dims].T\n\n        if \"line\" in type.lower():\n            _ = ax.plot(*points, color=col, **morph_plot_kwargs)\n        elif \"scatter\" in type.lower():\n            _ = ax.scatter(*points, color=col, **morph_plot_kwargs)\n        else:\n            raise NotImplementedError\n\n    return ax\n
"},{"location":"reference/utils/#jaxley.utils.plot_utils.plot_mesh","title":"plot_mesh(mesh_points, orientation, center, dims, ax=None, **kwargs)","text":"

Plot the 2D projection of a volume mesh on a cardinal plane.

Project the projection of a cylinder that is oriented in 3D space. - Create cylinder mesh - rotate cylinder mesh to orient it lengthwise along a given orientation vector. - move its center - project onto plane - compute outline of projected mesh. - fill area inside the outline

Parameters:

Name Type Description Default mesh_points ndarray

coordinates of the xyz mesh that define the volume

required orientation ndarray

orientation vector. The cylinder will be oriented along this vector.

required center ndarray

The x,y,z coordinates of the center of the cylinder.

required dims Tuple[int]

The dimensions to plot / to project the cylinder onto,

required ax Axes

The matplotlib axis to plot on.

None

Returns:

Type Description Axes

Plot of the cylinder projection.

Source code in jaxley/utils/plot_utils.py
def plot_mesh(\n    mesh_points: ndarray,\n    orientation: ndarray,\n    center: ndarray,\n    dims: Tuple[int],\n    ax: Axes = None,\n    **kwargs,\n) -> Axes:\n    \"\"\"Plot the 2D projection of a volume mesh on a cardinal plane.\n\n    Project the projection of a cylinder that is oriented in 3D space.\n    - Create cylinder mesh\n    - rotate cylinder mesh to orient it lengthwise along a given orientation vector.\n    - move its center\n    - project onto plane\n    - compute outline of projected mesh.\n    - fill area inside the outline\n\n    Args:\n        mesh_points: coordinates of the xyz mesh that define the volume\n        orientation: orientation vector. The cylinder will be oriented along this vector.\n        center: The x,y,z coordinates of the center of the cylinder.\n        dims: The dimensions to plot / to project the cylinder onto,\n        i.e. [0,1] xy-plane or [0,1,2] for 3D.\n        ax: The matplotlib axis to plot on.\n\n    Returns:\n        Plot of the cylinder projection.\n    \"\"\"\n    if ax is None:\n        fig = plt.figure(figsize=(3, 3))\n        ax = fig.add_subplot(111) if len(dims) < 3 else plt.axes(projection=\"3d\")\n\n    # Normalize axis vector\n    orientation = np.array(orientation)\n    orientation = orientation / np.linalg.norm(orientation)\n\n    # Create a rotation matrix to align the cylinder with the given axis\n    z_axis = np.array([0, 0, 1])\n    rotation_axis = np.cross(z_axis, orientation)\n    rotation_angle = np.arccos(np.dot(z_axis, orientation))\n\n    if np.allclose(rotation_axis, 0):\n        rotation_matrix = np.eye(3)\n    else:\n        rotation_matrix = compute_rotation_matrix(rotation_axis, rotation_angle)\n\n    # Rotate mesh\n    x_mesh, y_mesh, z_mesh = mesh_points\n    rotated_mesh_points = np.dot(\n        rotation_matrix,\n        np.array([x_mesh.flatten(), y_mesh.flatten(), z_mesh.flatten()]),\n    )\n    rotated_mesh_points = rotated_mesh_points.reshape(3, -1)\n\n    # project onto plane and move\n    rotated_mesh_points = rotated_mesh_points[dims]\n    rotated_mesh_points += np.array(center)[dims, np.newaxis]\n\n    if len(dims) < 3:\n        # get outline of cylinder mesh\n        mesh_outline = extract_outline(rotated_mesh_points.T).T\n        ax.fill(*mesh_outline.reshape(mesh_outline.shape[0], -1), **kwargs)\n    else:\n        # plot 3d mesh\n        ax.plot_surface(*rotated_mesh_points.reshape(*mesh_points.shape), **kwargs)\n    return ax\n
"},{"location":"reference/utils/#jaxley.utils.plot_utils.plot_morph","title":"plot_morph(module_or_view, dims=(0, 1), col='k', ax=None, resolution=100, morph_plot_kwargs={})","text":"

Plot the detailed morphology.

Plots the traced morphology it was traced. That means at every point that was traced a disc of radius r is plotted. The outline of the discs are then connected to form the morphology. This means every trace segement can be represented by a cone frustum. To prevent breaks in the morphology, each segement is connected with a ball joint.

Parameters:

Name Type Description Default module_or_view Union[Module, View]

The module or view to plot.

required dims Tuple[int]

The dimensions to plot / to project the cylinder onto, i.e. [0,1] xy-plane or [0,1,2] for 3D.

(0, 1) col str

The color for all branches

'k' ax Optional[Axes]

The matplotlib axis to plot on.

None morph_plot_kwargs Dict

The plot kwargs for plt.fill.

{} resolution int

defines the resolution of the mesh. If too low (typically <10), can result in errors. Useful too have a simpler mesh for plotting.

100

Returns:

Type Description Axes

Plot of the detailed morphology.

Source code in jaxley/utils/plot_utils.py
def plot_morph(\n    module_or_view: Union[\"jx.Module\", \"jx.View\"],\n    dims: Tuple[int] = (0, 1),\n    col: str = \"k\",\n    ax: Optional[Axes] = None,\n    resolution: int = 100,\n    morph_plot_kwargs: Dict = {},\n) -> Axes:\n    \"\"\"Plot the detailed morphology.\n\n    Plots the traced morphology it was traced. That means at every point that was\n    traced a disc of radius `r` is plotted. The outline of the discs are then\n    connected to form the morphology. This means every trace segement can be\n    represented by a cone frustum. To prevent breaks in the morphology, each\n    segement is connected with a ball joint.\n\n    Args:\n        module_or_view: The module or view to plot.\n        dims: The dimensions to plot / to project the cylinder onto,\n            i.e. [0,1] xy-plane or [0,1,2] for 3D.\n        col: The color for all branches\n        ax: The matplotlib axis to plot on.\n        morph_plot_kwargs: The plot kwargs for plt.fill.\n\n        resolution: defines the resolution of the mesh.\n            If too low (typically <10), can result in errors.\n            Useful too have a simpler mesh for plotting.\n\n    Returns:\n        Plot of the detailed morphology.\"\"\"\n    if ax is None:\n        fig = plt.figure(figsize=(3, 3))\n        ax = fig.add_subplot(111) if len(dims) < 3 else plt.axes(projection=\"3d\")\n    if len(dims) == 3:\n        warn(\n            \"rendering large morphologies in 3D can take a while. Consider projecting to 2D instead.\"\n        )\n\n    assert not np.any(\n        np.isnan(module_or_view.xyzr[0][:, :3])\n    ), \"missing xyz coordinates.\"\n\n    for xyzr in module_or_view.xyzr:\n        if len(xyzr) > 1:\n            for xyzr1, xyzr2 in zip(xyzr[1:, :], xyzr[:-1, :]):\n                dxyz = xyzr2[:3] - xyzr1[:3]\n                length = np.sqrt(np.sum(dxyz**2))\n                points = create_cone_frustum_mesh(\n                    length,\n                    xyzr1[-1],\n                    xyzr2[-1],\n                    bottom_dome=True,\n                    top_dome=True,\n                    resolution=resolution,\n                )\n                plot_mesh(\n                    points,\n                    dxyz,\n                    xyzr1[:3],\n                    np.array(dims),\n                    color=col,\n                    ax=ax,\n                    **morph_plot_kwargs,\n                )\n        else:\n            points = create_cone_frustum_mesh(\n                0,\n                xyzr[:, -1],\n                xyzr[:, -1],\n                bottom_dome=True,\n                top_dome=True,\n                resolution=resolution,\n            )\n            plot_mesh(\n                points,\n                np.ones(3),\n                xyzr[0, :3],\n                dims=np.array(dims),\n                color=col,\n                ax=ax,\n                **morph_plot_kwargs,\n            )\n\n    return ax\n
"},{"location":"reference/utils/#jaxley.utils.jax_utils.nested_checkpoint_scan","title":"nested_checkpoint_scan(f, init, xs, length=None, *, nested_lengths, scan_fn=jax.lax.scan, checkpoint_fn=jax.checkpoint)","text":"

A version of lax.scan that supports recursive gradient checkpointing.

Code taken from: https://github.com/google/jax/issues/2139

The interface of nested_checkpoint_scan exactly matches lax.scan, except for the required nested_lengths argument.

The key feature of nested_checkpoint_scan is that gradient calculations require O(max(nested_lengths)) memory, vs O(prod(nested_lengths)) for unnested scans, which it achieves by re-evaluating the forward pass len(nested_lengths) - 1 times.

nested_checkpoint_scan reduces to lax.scan when nested_lengths has a single element.

Parameters:

Name Type Description Default f Callable[[Carry, Dict[str, ndarray]], Tuple[Carry, Output]]

function to scan over.

required init Carry

initial value.

required xs Dict[str, ndarray]

scanned over values.

required length Optional[int]

leading length of all dimensions

None nested_lengths Sequence[int]

required list of lengths to scan over for each level of checkpointing. The product of nested_lengths must match length (if provided) and the size of the leading axis for all arrays in xs.

required scan_fn

function matching the API of lax.scan

scan checkpoint_fn Callable[[Func], Func]

function matching the API of jax.checkpoint.

checkpoint Source code in jaxley/utils/jax_utils.py
def nested_checkpoint_scan(\n    f: Callable[[Carry, Dict[str, jnp.ndarray]], Tuple[Carry, Output]],\n    init: Carry,\n    xs: Dict[str, jnp.ndarray],\n    length: Optional[int] = None,\n    *,\n    nested_lengths: Sequence[int],\n    scan_fn=jax.lax.scan,\n    checkpoint_fn: Callable[[Func], Func] = jax.checkpoint,\n):\n    \"\"\"A version of lax.scan that supports recursive gradient checkpointing.\n\n    Code taken from: https://github.com/google/jax/issues/2139\n\n    The interface of `nested_checkpoint_scan` exactly matches lax.scan, except for\n    the required `nested_lengths` argument.\n\n    The key feature of `nested_checkpoint_scan` is that gradient calculations\n    require O(max(nested_lengths)) memory, vs O(prod(nested_lengths)) for unnested\n    scans, which it achieves by re-evaluating the forward pass\n    `len(nested_lengths) - 1` times.\n\n    `nested_checkpoint_scan` reduces to `lax.scan` when `nested_lengths` has a\n    single element.\n\n    Args:\n        f: function to scan over.\n        init: initial value.\n        xs: scanned over values.\n        length: leading length of all dimensions\n        nested_lengths: required list of lengths to scan over for each level of\n            checkpointing. The product of nested_lengths must match length (if\n            provided) and the size of the leading axis for all arrays in ``xs``.\n        scan_fn: function matching the API of lax.scan\n        checkpoint_fn: function matching the API of jax.checkpoint.\n    \"\"\"\n    if length is not None and length != math.prod(nested_lengths):\n        raise ValueError(f\"inconsistent {length=} and {nested_lengths=}\")\n\n    def nested_reshape(x):\n        x = jnp.asarray(x)\n        new_shape = tuple(nested_lengths) + x.shape[1:]\n        return x.reshape(new_shape)\n\n    sub_xs = jax.tree_util.tree_map(nested_reshape, xs)\n    return _inner_nested_scan(f, init, sub_xs, nested_lengths, scan_fn, checkpoint_fn)\n
"},{"location":"reference/utils/#jaxley.utils.syn_utils.gather_synapes","title":"gather_synapes(number_of_compartments, post_syn_comp_inds, current_each_synapse_voltage_term, current_each_synapse_constant_term)","text":"

Compute current at the post synapse.

All this does it that it sums the synaptic currents that come into a particular compartment. It returns an array of as many elements as there are compartments.

Source code in jaxley/utils/syn_utils.py
def gather_synapes(\n    number_of_compartments: jnp.ndarray,\n    post_syn_comp_inds: np.ndarray,\n    current_each_synapse_voltage_term: jnp.ndarray,\n    current_each_synapse_constant_term: jnp.ndarray,\n) -> Tuple[jnp.ndarray, jnp.ndarray]:\n    \"\"\"Compute current at the post synapse.\n\n    All this does it that it sums the synaptic currents that come into a particular\n    compartment. It returns an array of as many elements as there are compartments.\n    \"\"\"\n    incoming_currents_voltages = jnp.zeros((number_of_compartments,))\n    incoming_currents_contant = jnp.zeros((number_of_compartments,))\n\n    dnums = ScatterDimensionNumbers(\n        update_window_dims=(),\n        inserted_window_dims=(0,),\n        scatter_dims_to_operand_dims=(0,),\n    )\n    incoming_currents_voltages = scatter_add(\n        incoming_currents_voltages,\n        post_syn_comp_inds[:, None],\n        current_each_synapse_voltage_term,\n        dnums,\n    )\n    incoming_currents_contant = scatter_add(\n        incoming_currents_contant,\n        post_syn_comp_inds[:, None],\n        current_each_synapse_constant_term,\n        dnums,\n    )\n    return incoming_currents_voltages, incoming_currents_contant\n
"},{"location":"tutorial/00_jaxley_api/","title":"Key concepts in Jaxley","text":"

In this tutorial, we will introduce you to the basic concepts of Jaxley. You will learn about:

  • Modules (e.g., Cell, Network,\u2026)
    • nodes
    • edges
  • Views
    • Groups
  • Channels
  • Synapses

Here is a code snippet which you will learn to understand in this tutorial:

import jaxley as jx\nfrom jaxley.channels import Na, K, Leak\nfrom jaxley.synapses import IonotropicSynapse\nfrom jaxley.connect import connect\nimport matplotlib.pyplot as plt\nimport numpy as np\n\n\n# Assembling different Modules into a Network\ncomp = jx.Compartment()\nbranch = jx.Branch(comp, nseg=1)\ncell = jx.Cell(branch, parents=[-1, 0, 0])\nnet = jx.Network([cell]*3)\n\n# Navigating and inspecting the Modules using Views\ncell0 = net.cell(0)\ncell0.nodes\n\n# How to group together parts of Modules\nnet.cell(1).add_to_group(\"cell1\")\n\n# inserting channels in the membrane\nwith net.cell(0) as cell0:\n    cell0.insert(Na())\n    cell0.insert(K())\n\n# connecting two cells using a Synapse\npre_comp = cell0.branch(1).comp(0)\npost_comp = net.cell1.branch(0).comp(0)\n\nconnect(pre_comp, post_comp)\n

First, we import the relevant libraries:

from jax import config\nconfig.update(\"jax_enable_x64\", True)\nconfig.update(\"jax_platform_name\", \"cpu\")\n\nimport jaxley as jx\nfrom jaxley.channels import Na, K, Leak\nfrom jaxley.synapses import IonotropicSynapse\nfrom jaxley.connect import connect\nimport matplotlib.pyplot as plt\nimport numpy as np\n
"},{"location":"tutorial/00_jaxley_api/#modules","title":"Modules","text":"

In Jaxley, we heavily rely on the concept of Modules to build biophyiscal models of neural systems at various scales. Jaxley implements four types of Modules: - Compartment - Branch - Cell - Network

Modules can be connected together to build increasingly detailed and complex models. Compartment -> Branch -> Cell -> Network.

Compartments are the atoms of biophysical models in Jaxley. All mechanisms and synaptic connections live on the level of Compartments and can already be simulated using jx.integrate on their own. Everything you do in Jaxley starts with a Compartment.

comp = jx.Compartment() # single compartment model.\n

Mutliple Compartments can be connected together to form longer, linear segments / cables, which we call Branches and are equivalent to sections in NEURON.

nseg = 4\nbranch = jx.Branch([comp] * nseg)\n

In order to construct cell morphologies in Jaxley, multiple Branches can to be connected together as a Cell:

# -1 indicates that the first branch has no parent branch.\n# The other two branches both have the 0-eth branch as their parent.\nparents = [-1, 0, 0]\ncell = jx.Cell([branch] * len(parents), parents)\n

Finally, several Cells can be grouped together to form a Network, which can than be connected together using Synpases.

ncells = 2\nnet = jx.Network([cell]*ncells)\n\nnet.shape # shows you the num_cells, num_branches, num_comps\n
(2, 6, 24)\n

Every module tracks information about its current state and parameters in two Dataframes called nodes and edges. nodes contains all the information that we associate with compartments in the model (each row corresponds to one compartment) and edges tracks all the information relevant to synapses.

This means that you can easily keep track of the current state of your Module and how it changes at all times.

net.nodes\n
local_cell_index local_branch_index local_comp_index length radius axial_resistivity capacitance v x y ... Na_m Na_h K K_gK eK K_n global_cell_index global_branch_index global_comp_index controlled_by_param 0 0 0 0 10.0 0.020703 5000.0 1.0 -70.0 NaN NaN ... NaN NaN False NaN NaN NaN 0 0 0 0 1 0 0 1 10.0 0.020703 5000.0 1.0 -70.0 NaN NaN ... NaN NaN False NaN NaN NaN 0 0 1 0 2 0 0 2 10.0 0.020703 5000.0 1.0 -70.0 NaN NaN ... NaN NaN False NaN NaN NaN 0 0 2 0 3 0 0 3 10.0 0.020703 5000.0 1.0 -70.0 NaN NaN ... NaN NaN False NaN NaN NaN 0 0 3 0 4 0 1 0 10.0 0.020703 5000.0 1.0 -70.0 NaN NaN ... NaN NaN False NaN NaN NaN 0 1 4 0 5 0 1 1 10.0 0.020703 5000.0 1.0 -70.0 NaN NaN ... NaN NaN False NaN NaN NaN 0 1 5 0 6 0 1 2 10.0 0.020703 5000.0 1.0 -70.0 NaN NaN ... NaN NaN False NaN NaN NaN 0 1 6 0 7 0 1 3 10.0 0.020703 5000.0 1.0 -70.0 NaN NaN ... NaN NaN False NaN NaN NaN 0 1 7 0 8 0 2 0 10.0 0.020703 5000.0 1.0 -70.0 NaN NaN ... NaN NaN False NaN NaN NaN 0 2 8 0 9 0 2 1 10.0 0.020703 5000.0 1.0 -70.0 NaN NaN ... NaN NaN False NaN NaN NaN 0 2 9 0 10 0 2 2 10.0 0.020703 5000.0 1.0 -70.0 NaN NaN ... NaN NaN False NaN NaN NaN 0 2 10 0 11 0 2 3 10.0 0.020703 5000.0 1.0 -70.0 NaN NaN ... NaN NaN False NaN NaN NaN 0 2 11 0 12 1 0 0 10.0 0.020703 5000.0 1.0 -70.0 NaN NaN ... NaN NaN False NaN NaN NaN 1 3 12 0 13 1 0 1 10.0 0.020703 5000.0 1.0 -70.0 NaN NaN ... NaN NaN False NaN NaN NaN 1 3 13 0 14 1 0 2 10.0 0.020703 5000.0 1.0 -70.0 NaN NaN ... NaN NaN False NaN NaN NaN 1 3 14 0 15 1 0 3 10.0 0.020703 5000.0 1.0 -70.0 NaN NaN ... NaN NaN False NaN NaN NaN 1 3 15 0 16 1 1 0 10.0 0.020703 5000.0 1.0 -70.0 NaN NaN ... NaN NaN False NaN NaN NaN 1 4 16 0 17 1 1 1 10.0 0.020703 5000.0 1.0 -70.0 NaN NaN ... NaN NaN False NaN NaN NaN 1 4 17 0 18 1 1 2 10.0 0.020703 5000.0 1.0 -70.0 NaN NaN ... NaN NaN False NaN NaN NaN 1 4 18 0 19 1 1 3 10.0 0.020703 5000.0 1.0 -70.0 NaN NaN ... NaN NaN False NaN NaN NaN 1 4 19 0 20 1 2 0 10.0 0.020703 5000.0 1.0 -70.0 NaN NaN ... NaN NaN False NaN NaN NaN 1 5 20 0 21 1 2 1 10.0 0.020703 5000.0 1.0 -70.0 NaN NaN ... NaN NaN False NaN NaN NaN 1 5 21 0 22 1 2 2 10.0 0.020703 5000.0 1.0 -70.0 NaN NaN ... NaN NaN False NaN NaN NaN 1 5 22 0 23 1 2 3 10.0 0.020703 5000.0 1.0 -70.0 NaN NaN ... NaN NaN False NaN NaN NaN 1 5 23 0

24 rows \u00d7 28 columns

net.edges.head() # this is currently empty since we have not made any connections yet\n
global_edge_index global_pre_comp_index global_post_comp_index pre_locs post_locs type type_ind"},{"location":"tutorial/00_jaxley_api/#views","title":"Views","text":"

Since these Modules can become very complex, Jaxley utilizes so called Views to make working with Modules easy and intuitive.

The simplest way to navigate Modules is by navigating them via the hierachy that we introduced above. A View is what you get when you index into the module. For example, for a Network:

net.cell(0)\n
View with 1 different channels. Use `.nodes` for details.\n

Views behave very similarly to Modules, i.e. the cell(0) (the 0th cell of the network) behaves like the cell we instantiated earlier. As such, cell(0) also has a nodes attribute, which keeps track of it\u2019s part of the network:

net.cell(0).nodes\n
local_cell_index local_branch_index local_comp_index length radius axial_resistivity capacitance v x y ... Na_m Na_h K K_gK eK K_n global_cell_index global_branch_index global_comp_index controlled_by_param 0 0 0 0 10.0 0.020703 5000.0 1.0 -70.0 NaN NaN ... NaN NaN False NaN NaN NaN 0 0 0 0 1 0 0 1 10.0 0.020703 5000.0 1.0 -70.0 NaN NaN ... NaN NaN False NaN NaN NaN 0 0 1 0 2 0 0 2 10.0 0.020703 5000.0 1.0 -70.0 NaN NaN ... NaN NaN False NaN NaN NaN 0 0 2 0 3 0 0 3 10.0 0.020703 5000.0 1.0 -70.0 NaN NaN ... NaN NaN False NaN NaN NaN 0 0 3 0 4 0 1 0 10.0 0.020703 5000.0 1.0 -70.0 NaN NaN ... NaN NaN False NaN NaN NaN 0 1 4 0 5 0 1 1 10.0 0.020703 5000.0 1.0 -70.0 NaN NaN ... NaN NaN False NaN NaN NaN 0 1 5 0 6 0 1 2 10.0 0.020703 5000.0 1.0 -70.0 NaN NaN ... NaN NaN False NaN NaN NaN 0 1 6 0 7 0 1 3 10.0 0.020703 5000.0 1.0 -70.0 NaN NaN ... NaN NaN False NaN NaN NaN 0 1 7 0 8 0 2 0 10.0 0.020703 5000.0 1.0 -70.0 NaN NaN ... NaN NaN False NaN NaN NaN 0 2 8 0 9 0 2 1 10.0 0.020703 5000.0 1.0 -70.0 NaN NaN ... NaN NaN False NaN NaN NaN 0 2 9 0 10 0 2 2 10.0 0.020703 5000.0 1.0 -70.0 NaN NaN ... NaN NaN False NaN NaN NaN 0 2 10 0 11 0 2 3 10.0 0.020703 5000.0 1.0 -70.0 NaN NaN ... NaN NaN False NaN NaN NaN 0 2 11 0

12 rows \u00d7 28 columns

Let\u2019s use Views to visualize only parts of the Network. Before we do that, we create x, y, and z coordinates for the Network:

# Compute xyz coordinates of the cells.\nnet.compute_xyz()\n\n# Move cells (since they are placed on top of each other by default).\nnet.cell(0).move(y=30)\n

We can now visualize the entire net (i.e., the entire Module) with the .vis() method\u2026

# We can use the vis function to visualize Modules.\nfig, ax = plt.subplots(1,1, figsize=(3,3))\nnet.vis(ax=ax)\n
<Axes: >\n

\u2026but we can also create a View to visualize only parts of the net:

# ... and Views\nfig, ax = plt.subplots(1,1, figsize=(3,3))\nnet.cell(0).vis(ax=ax, col=\"blue\") # View of the 0th cell of the network\nnet.cell(1).vis(ax=ax, col=\"red\") # View of the 1st cell of the network\n\nnet.cell(0).branch(0).vis(ax=ax, col=\"green\") # View of the 1st branch of the 0th cell of the network\nnet.cell(1).branch(1).comp(1).vis(ax=ax, col=\"black\", type=\"scatter\") # View of the 0th comp of the 1st branch of the 0th cell of the network\n
<Axes: >\n

"},{"location":"tutorial/00_jaxley_api/#how-to-create-views","title":"How to create Views","text":"

Above, we used net.cell(0) to generate a View of the 0-eth cell. Jaxley supports many ways of performing such indexing:

# several types of indices are supported (lists, ranges, ...)\nnet.cell([0,1]).branch(\"all\").comp(0)  # View of all 0th comps of all branches of cell 0 and 1\n\nbranch.loc(0.1)  # Equivalent to `NEURON`s `loc`. Assumes branches are continous from 0-1.\n\nnet[0,0,0]  # Modules/Views can also be lazily indexed\n\ncell0 = net.cell(0)  # Views can be assigned to variables and only track the parts of the Module they belong to\ncell0.branch(1).comp(0)  # Views can be continuely indexed\n
View with 1 different channels. Use `.nodes` for details.\n
cell0.nodes\n
local_cell_index local_branch_index local_comp_index length radius axial_resistivity capacitance v x y ... Na_m Na_h K K_gK eK K_n global_cell_index global_branch_index global_comp_index controlled_by_param 0 0 0 0 10.0 0.020703 5000.0 1.0 -70.0 5.000000 30.000000 ... NaN NaN False NaN NaN NaN 0 0 0 0 1 0 0 1 10.0 0.020703 5000.0 1.0 -70.0 15.000000 30.000000 ... NaN NaN False NaN NaN NaN 0 0 1 0 2 0 0 2 10.0 0.020703 5000.0 1.0 -70.0 25.000000 30.000000 ... NaN NaN False NaN NaN NaN 0 0 2 0 3 0 0 3 10.0 0.020703 5000.0 1.0 -70.0 35.000000 30.000000 ... NaN NaN False NaN NaN NaN 0 0 3 0 4 0 1 0 10.0 0.020703 5000.0 1.0 -70.0 44.850713 28.787322 ... NaN NaN False NaN NaN NaN 0 1 4 0 5 0 1 1 10.0 0.020703 5000.0 1.0 -70.0 54.552138 26.361966 ... NaN NaN False NaN NaN NaN 0 1 5 0 6 0 1 2 10.0 0.020703 5000.0 1.0 -70.0 64.253563 23.936609 ... NaN NaN False NaN NaN NaN 0 1 6 0 7 0 1 3 10.0 0.020703 5000.0 1.0 -70.0 73.954988 21.511253 ... NaN NaN False NaN NaN NaN 0 1 7 0 8 0 2 0 10.0 0.020703 5000.0 1.0 -70.0 44.850713 31.212678 ... NaN NaN False NaN NaN NaN 0 2 8 0 9 0 2 1 10.0 0.020703 5000.0 1.0 -70.0 54.552138 33.638034 ... NaN NaN False NaN NaN NaN 0 2 9 0 10 0 2 2 10.0 0.020703 5000.0 1.0 -70.0 64.253563 36.063391 ... NaN NaN False NaN NaN NaN 0 2 10 0 11 0 2 3 10.0 0.020703 5000.0 1.0 -70.0 73.954988 38.488747 ... NaN NaN False NaN NaN NaN 0 2 11 0

12 rows \u00d7 28 columns

net.shape\n
(2, 6, 24)\n

Note: In case you need even more flexibility in how you select parts of a Module, Jaxley provides a select method, to give full control over the exact parts of the nodes and edges that are part of a View. On examples of how this can be used, see the tutorial on advanced indexing.

You can also iterate over networks, cells, and branches:

# We set the radiuses to random values...\nradiuses = np.random.rand((24))\nnet.set(\"radius\", radiuses)\n\n# ...and then we set the length to 100.0 um if the radius is >0.5.\nfor cell in net:\n    for branch in cell:\n        for comp in branch:\n            if comp.nodes.iloc[0][\"radius\"] > 0.5:\n                comp.set(\"length\", 100.0)\n\n# Show the first five compartments:\nnet.nodes[[\"radius\", \"length\"]][:5]\n
radius length 0 0.988127 100.0 1 0.568548 100.0 2 0.064304 2.5 3 0.859943 100.0 4 0.879433 100.0

Finally, you can also use Views in a context manager:

with net.cell(0).branch(0) as branch0:\n    branch0.set(\"radius\", 2.0)\n    branch0.set(\"length\", 2.5)\n\n# Show the first five compartments.\nnet.nodes[[\"radius\", \"length\"]][:5]\n
radius length 0 2.000000 2.5 1 2.000000 2.5 2 2.000000 2.5 3 2.000000 2.5 4 0.879433 100.0"},{"location":"tutorial/00_jaxley_api/#channels","title":"Channels","text":"

The Modules that we have created above will not do anything interesting, since by default Jaxley initializes them without any mechanisms in the membrane. To change this, we have to insert channels into the membrane. For this purpose Jaxley implements Channels that can be inserted into any compartment using the insert method of a Module or a View:

# insert a Leak channel into all compartments in the Module.\nnet.insert(Leak())\nnet.nodes.head() # Channel parameters are now also added to `nodes`.\n
local_cell_index local_branch_index local_comp_index length radius axial_resistivity capacitance v x y z global_cell_index global_branch_index global_comp_index controlled_by_param Leak Leak_gLeak Leak_eLeak 0 0 0 0 100.0 0.924252 5000.0 1.0 -70.0 5.000000 30.000000 0.0 0 0 0 0 True 0.0001 -70.0 1 0 0 1 100.0 0.566347 5000.0 1.0 -70.0 15.000000 30.000000 0.0 0 0 1 0 True 0.0001 -70.0 2 0 0 2 10.0 0.208471 5000.0 1.0 -70.0 25.000000 30.000000 0.0 0 0 2 0 True 0.0001 -70.0 3 0 0 3 100.0 0.596002 5000.0 1.0 -70.0 35.000000 30.000000 0.0 0 0 3 0 True 0.0001 -70.0 4 0 1 0 10.0 0.027419 5000.0 1.0 -70.0 44.850713 28.787322 0.0 0 1 4 0 True 0.0001 -70.0

This is also were Views come in handy, as it allows to easily target the insertion of channels to specific compartments.

# inserting several channels into parts of the network\nwith net.cell(0) as cell0:\n    cell0.insert(Na())\n    cell0.insert(K())\n\n# # The above is equivalent to:\n# net.cell(0).insert(Na())\n# net.cell(0).insert(K())\n\n# K and Na channels were only insert into cell 0\nnet.cell(\"all\").branch(0).comp(0).nodes[[\"global_cell_index\", \"Na\", \"K\", \"Leak\"]]\n
global_cell_index Na K Leak 0 0 True True True 12 1 False False True"},{"location":"tutorial/00_jaxley_api/#synapses","title":"Synapses","text":"

To connect different cells together, Jaxley implements a connect method, that can be used to couple 2 compartments together using a Synapse. Synapses in Jaxley work only on the compartment level, that means to be able to connect two cells, you need to specify the exact compartments on a given cell to make the connections between. Below is an example of this:

# connecting two cells using a Synapse\npre_comp = cell0.branch(1).comp(0)\npost_comp = net.cell(1).branch(0).comp(0)\n\nconnect(pre_comp, post_comp, IonotropicSynapse())\n\nnet.edges\n
global_edge_index global_pre_comp_index global_post_comp_index type type_ind pre_locs post_locs IonotropicSynapse_gS IonotropicSynapse_e_syn IonotropicSynapse_k_minus IonotropicSynapse_s controlled_by_param 0 0 4 12 IonotropicSynapse 0 0.125 0.125 0.0001 0.0 0.025 0.2 0

As you can see above, now the edges dataframe is also updated with the information of the newly added synapse.

Congrats! You should now have an intuitive understand of how to use Jaxley\u2019s API to construct, navigate and manipulate neuron models.

"},{"location":"tutorial/01_morph_neurons/","title":"Basics of Jaxley","text":"

In this tutorial, you will learn how to:

  • build your first morphologically detailed cell or read it from SWC
  • stimulate the cell
  • record from the cell
  • visualize cells
  • run your first simulation

Here is a code snippet which you will learn to understand in this tutorial:

import jaxley as jx\nfrom jaxley.channels import Na, K, Leak\nimport matplotlib.pyplot as plt\n\n\n# Build the cell.\ncomp = jx.Compartment()\nbranch = jx.Branch(comp, nseg=2)\ncell = jx.Cell(branch, parents=[-1, 0, 0, 1, 1])\n\n# Insert channels.\ncell.insert(Leak())\ncell.branch(0).insert(Na())\ncell.branch(0).insert(K())\n\n# Change parameters.\ncell.set(\"axial_resistivity\", 200.0)\n\n# Visualize the morphology.\ncell.compute_xyz()\nfig, ax = plt.subplots(1, 1, figsize=(4, 4))\ncell.vis(ax=ax)\n\n# Stimulate.\ncurrent = jx.step_current(i_delay=1.0, i_dur=1.0, i_amp=0.1, delta_t=0.025, t_max=10.0)\ncell.branch(0).loc(0.0).stimulate(current)\n\n# Record.\ncell.branch(0).loc(0.0).record(\"v\")\n\n# Simulate and plot.\nv = jx.integrate(cell, delta_t=0.025)\nplt.plot(v.T)\n

First, we import the relevant libraries:

from jax import config\nconfig.update(\"jax_enable_x64\", True)\nconfig.update(\"jax_platform_name\", \"cpu\")\n\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport jax.numpy as jnp\nfrom jax import jit\n\nimport jaxley as jx\nfrom jaxley.channels import Na, K, Leak\nfrom jaxley.synapses import IonotropicSynapse\nfrom jaxley.connect import fully_connect\n

We will now build our first cell in Jaxley. You have two options to do this: you can either build a cell bottom-up by defining the morphology yourselve, or you can load cells from SWC files.

"},{"location":"tutorial/01_morph_neurons/#define-the-cell-from-scratch","title":"Define the cell from scratch","text":"

To define a cell from scratch you first have to define a single compartment and then assemble those compartments into a branch:

comp = jx.Compartment()\nbranch = jx.Branch(comp, nseg=2)\n

Next, we can assemble branches into a cell. To do so, we have to define for each branch what its parent branch is. A -1 entry means that this branch does not have a parent.

parents = jnp.asarray([-1, 0, 0, 1, 1])\ncell = jx.Cell(branch, parents=parents)\n

To learn more about Compartments, Branches, and Cells, see this tutorial.

"},{"location":"tutorial/01_morph_neurons/#read-the-cell-from-an-swc-file","title":"Read the cell from an SWC file","text":"

Alternatively, you could also load cells from SWC with

cell = jx.read_swc(fname, nseg=4)

Details on handling SWC files can be found in this tutorial.

"},{"location":"tutorial/01_morph_neurons/#visualize-the-cells","title":"Visualize the cells","text":"

Cells can be visualized as follows:

cell.compute_xyz()  # Only needed for visualization.\n\nfig, ax = plt.subplots(1, 1, figsize=(4, 2))\n_ = cell.vis(ax=ax, col=\"k\")\n

"},{"location":"tutorial/01_morph_neurons/#insert-mechanisms","title":"Insert mechanisms","text":"

Currently, the cell does not contain any kind of ion channel (not even a leak). We can fix this by inserting a leak channel into the entire cell, and by inserting sodium and potassium into the zero-eth branch.

cell.insert(Leak())\ncell.branch(0).insert(Na())\ncell.branch(0).insert(K())\n

Once the cell is created, we can inspect its .nodes attribute which lists all properties of the cell:

cell.nodes\n
local_cell_index local_branch_index local_comp_index length radius axial_resistivity capacitance v global_cell_index global_branch_index ... Na Na_gNa eNa vt Na_m Na_h K K_gK eK K_n 0 0 0 0 10.0 1.0 5000.0 1.0 -70.0 0 0 ... True 0.05 50.0 -60.0 0.2 0.2 True 0.005 -90.0 0.2 1 0 0 1 10.0 1.0 5000.0 1.0 -70.0 0 0 ... True 0.05 50.0 -60.0 0.2 0.2 True 0.005 -90.0 0.2 2 0 1 0 10.0 1.0 5000.0 1.0 -70.0 0 1 ... False NaN NaN NaN NaN NaN False NaN NaN NaN 3 0 1 1 10.0 1.0 5000.0 1.0 -70.0 0 1 ... False NaN NaN NaN NaN NaN False NaN NaN NaN 4 0 2 0 10.0 1.0 5000.0 1.0 -70.0 0 2 ... False NaN NaN NaN NaN NaN False NaN NaN NaN 5 0 2 1 10.0 1.0 5000.0 1.0 -70.0 0 2 ... False NaN NaN NaN NaN NaN False NaN NaN NaN 6 0 3 0 10.0 1.0 5000.0 1.0 -70.0 0 3 ... False NaN NaN NaN NaN NaN False NaN NaN NaN 7 0 3 1 10.0 1.0 5000.0 1.0 -70.0 0 3 ... False NaN NaN NaN NaN NaN False NaN NaN NaN 8 0 4 0 10.0 1.0 5000.0 1.0 -70.0 0 4 ... False NaN NaN NaN NaN NaN False NaN NaN NaN 9 0 4 1 10.0 1.0 5000.0 1.0 -70.0 0 4 ... False NaN NaN NaN NaN NaN False NaN NaN NaN

10 rows \u00d7 25 columns

Note that Jaxley uses the same units as the NEURON simulator, which are listed here.

You can also inspect just parts of the cell, for example its 1st branch:

cell.branch(1).nodes\n
local_cell_index local_branch_index local_comp_index length radius axial_resistivity capacitance v Leak Leak_gLeak ... Na_m Na_h K K_gK eK K_n global_cell_index global_branch_index global_comp_index controlled_by_param 2 0 0 0 10.0 1.0 5000.0 1.0 -70.0 True 0.0001 ... NaN NaN False NaN NaN NaN 0 1 2 1 3 0 0 1 10.0 1.0 5000.0 1.0 -70.0 True 0.0001 ... NaN NaN False NaN NaN NaN 0 1 3 1

2 rows \u00d7 25 columns

The easiest way to know which branch is the 1st branch (or, e.g., the zero-eth compartment of the 1st branch) is to plot it in a different color:

fig, ax = plt.subplots(1, 1, figsize=(4, 2))\n_ = cell.vis(ax=ax, col=\"k\")\n_ = cell.branch(1).vis(ax=ax, col=\"r\")\n_ = cell.branch(1).comp(1).vis(ax=ax, col=\"b\", type=\"scatter\")\n

More background and features on indexing as cell.branch(0) is in this tutorial.

"},{"location":"tutorial/01_morph_neurons/#change-parameters-of-the-cell","title":"Change parameters of the cell","text":"

You can change properties of the cell with the .set() method:

cell.branch(1).set(\"axial_resistivity\", 200.0)\n

And we can again inspect the .nodes to make sure that the axial resistivity indeed changed:

cell.branch(1).nodes\n
local_cell_index local_branch_index local_comp_index length radius axial_resistivity capacitance v Leak Leak_gLeak ... Na_m Na_h K K_gK eK K_n global_cell_index global_branch_index global_comp_index controlled_by_param 2 0 0 0 10.0 1.0 200.0 1.0 -70.0 True 0.0001 ... NaN NaN False NaN NaN NaN 0 1 2 1 3 0 0 1 10.0 1.0 200.0 1.0 -70.0 True 0.0001 ... NaN NaN False NaN NaN NaN 0 1 3 1

2 rows \u00d7 25 columns

In a similar way, you can modify channel properties or initial states (units are again here):

cell.branch(0).set(\"K_gK\", 0.01)  # modify potassium conductance.\ncell.set(\"v\", -65.0)  # modify initial voltage.\n
"},{"location":"tutorial/01_morph_neurons/#stimulate-the-cell","title":"Stimulate the cell","text":"

We next stimulate one of the compartments with a step current. For this, we first define the step current (units are again here):

dt = 0.025\nt_max = 10.0\ntime_vec = np.arange(0, t_max+dt, dt)\ncurrent = jx.step_current(i_delay=1.0, i_dur=2.0, i_amp=0.08, delta_t=dt, t_max=t_max)\n\nfig, ax = plt.subplots(1, 1, figsize=(4, 2))\n_ = plt.plot(time_vec, current)\n

We then stimulate one of the compartments of the cell with this step current:

cell.delete_stimuli()\ncell.branch(0).loc(0.0).stimulate(current)\n
Added 1 external_states. See `.externals` for details.\n
"},{"location":"tutorial/01_morph_neurons/#define-recordings","title":"Define recordings","text":"

Next, you have to define where to record the voltage. In this case, we will record the voltage at two locations:

cell.delete_recordings()\ncell.branch(0).loc(0.0).record(\"v\")\ncell.branch(3).loc(1.0).record(\"v\")\n
Added 1 recordings. See `.recordings` for details.\nAdded 1 recordings. See `.recordings` for details.\n

We can again visualize these locations to understand where we inserted recordings:

fig, ax = plt.subplots(1, 1, figsize=(4, 2))\n_ = cell.vis(ax=ax)\n_ = cell.branch(0).loc(0.0).vis(ax=ax, col=\"b\", type=\"scatter\")\n_ = cell.branch(3).loc(1.0).vis(ax=ax, col=\"g\", type=\"scatter\")\n

"},{"location":"tutorial/01_morph_neurons/#simulate-the-cell-response","title":"Simulate the cell response","text":"

Having set up the cell, inserted stimuli and recordings, we are now ready to run a simulation with jx.integrate:

voltages = jx.integrate(cell, delta_t=dt)\nprint(\"voltages.shape\", voltages.shape)\n
voltages.shape (2, 402)\n

The jx.integrate function returns an array of shape (num_recordings, num_timepoints). In our case, we inserted 2 recordings and we simulated for 10ms at a 0.025 time step, which leads to 402 time steps.

We can now visualize the voltage response:

fig, ax = plt.subplots(1, 1, figsize=(4, 2))\n_ = ax.plot(voltages[0], c=\"b\")\n_ = ax.plot(voltages[1], c=\"orange\")\n

At the location of the first recording (in blue) the cell spiked, whereas at the second recording, it did not. This makes sense because we only inserted sodium and potassium channels into the first branch, but not in the entire cell.

Congrats! You have just run your first morphologically detailed neuron simulation in Jaxley. We suggest to continue by learning how to build networks. If you are only interested in single cell simulations, you can directly jump to learning how to speed up simulations. If you want to simulate detailed morphologies from SWC files, checkout our tutorial on working with detailed morphologies.

"},{"location":"tutorial/02_small_network/","title":"Network simulations in Jaxley","text":"

In this tutorial, you will learn how to:

  • connect neurons into a network
  • visualize networks
  • use the .edges attribute to inspect and change synaptic parameters

Here is a code snippet which you will learn to understand in this tutorial:

import jaxley as jx\nfrom jaxley.synapses import IonotropicSynapse\nfrom jaxley.connect import connect\n\n\n# Define a network. `cell` is defined as in previous tutorial.\nnet = jx.Network([cell for _ in range(11)])\n\n# Define synapses.\nfully_connect(\n    net.cell(range(10)),\n    net.cell(10),\n    IonotropicSynapse(),\n)\n\n# Change synaptic parameters.\nnet.select(edges=[0, 1]).set(\"IonotropicSynapse_gS\", 0.1)  # nS\n\n# Visualize the network.\nnet.compute_xyz()\nfig, ax = plt.subplots(1, 1, figsize=(4, 4))\nnet.vis(ax=ax, detail=\"full\", layers=[10, 1])  # or `detail=\"point\"`.\n

In the previous tutorial, you learned how to build single cells with morphological detail, how to insert stimuli and recordings, and how to run a first simulation. In this tutorial, we will define networks of multiple cells and connect them with synapses. Let\u2019s get started:

from jax import config\nconfig.update(\"jax_enable_x64\", True)\nconfig.update(\"jax_platform_name\", \"cpu\")\n\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport jax.numpy as jnp\nfrom jax import jit\n\nimport jaxley as jx\nfrom jaxley.channels import Na, K, Leak\nfrom jaxley.synapses import IonotropicSynapse\nfrom jaxley.connect import fully_connect, connect\n
"},{"location":"tutorial/02_small_network/#define-the-network","title":"Define the network","text":"

First, we define a cell as you saw in the previous tutorial.

comp = jx.Compartment()\nbranch = jx.Branch(comp, nseg=4)\ncell = jx.Cell(branch, parents=[-1, 0, 0, 1, 1, 2, 2])\n

We can assemble multiple cells into a network by using jx.Network, which takes a list of jx.Cells. Here, we assemble 11 cells into a network:

num_cells = 11\nnet = jx.Network([cell for _ in range(num_cells)])\n

At this point, we can already visualize this network:

net.compute_xyz()\nnet.rotate(180)\nfig, ax = plt.subplots(1, 1, figsize=(3, 6))\n_ = net.vis(ax=ax, detail=\"full\", layers=[10, 1], layer_kwargs={\"within_layer_offset\": 150, \"between_layer_offset\": 200})\n

Note: you can use move_to to have more control over the location of cells, e.g.: network.cell(i).move_to(x=0, y=200).

As you can see, the neurons are not connected yet. Let\u2019s fix this by connecting neurons with synapses. We will build a network consisting of two layers: 10 neurons in the input layer and 1 neuron in the output layer.

We can use Jaxley\u2019s fully_connect method to connect these layers:

pre = net.cell(range(10))\npost = net.cell(10)\nfully_connect(pre, post, IonotropicSynapse())\n

Let\u2019s visualize this again:

fig, ax = plt.subplots(1, 1, figsize=(3, 6))\n_ = net.vis(ax=ax, detail=\"full\", layers=[10, 1], layer_kwargs={\"within_layer_offset\": 150, \"between_layer_offset\": 200})\n

As you can see, the full_connect method inserted one synapse (in blue) from every neuron in the first layer to the output neuron. The fully_connect method builds this synapse from the zero-eth compartment and zero-eth branch of the presynaptic neuron onto a random branch of the postsynaptic neuron. If you want more control over the pre- and post-synaptic branches, you can use the connect method:

pre = net.cell(0).branch(5).loc(1.0)\npost = net.cell(10).branch(0).loc(0.0)\nconnect(pre, post, IonotropicSynapse())\n
fig, ax = plt.subplots(1, 1, figsize=(3, 6))\n_ = net.vis(ax=ax, detail=\"full\", layers=[10, 1], layer_kwargs={\"within_layer_offset\": 150, \"between_layer_offset\": 200})\n

"},{"location":"tutorial/02_small_network/#inspecting-and-changing-synaptic-parameters","title":"Inspecting and changing synaptic parameters","text":"

You can inspect synaptic parameters via the .edges attribute:

net.edges\n
global_edge_index global_pre_comp_index global_post_comp_index type type_ind pre_locs post_locs IonotropicSynapse_gS IonotropicSynapse_e_syn IonotropicSynapse_k_minus IonotropicSynapse_s controlled_by_param 0 0 0 307 IonotropicSynapse 0 0.125 0.875 0.0001 0.0 0.025 0.2 0 1 1 28 303 IonotropicSynapse 0 0.125 0.875 0.0001 0.0 0.025 0.2 0 2 2 56 280 IonotropicSynapse 0 0.125 0.125 0.0001 0.0 0.025 0.2 0 3 3 84 281 IonotropicSynapse 0 0.125 0.375 0.0001 0.0 0.025 0.2 0 4 4 112 306 IonotropicSynapse 0 0.125 0.625 0.0001 0.0 0.025 0.2 0 5 5 140 298 IonotropicSynapse 0 0.125 0.625 0.0001 0.0 0.025 0.2 0 6 6 168 301 IonotropicSynapse 0 0.125 0.375 0.0001 0.0 0.025 0.2 0 7 7 196 293 IonotropicSynapse 0 0.125 0.375 0.0001 0.0 0.025 0.2 0 8 8 224 300 IonotropicSynapse 0 0.125 0.125 0.0001 0.0 0.025 0.2 0 9 9 252 303 IonotropicSynapse 0 0.125 0.875 0.0001 0.0 0.025 0.2 0 10 10 23 280 IonotropicSynapse 0 0.875 0.125 0.0001 0.0 0.025 0.2 0

To modify a parameter of all synapses you can again use .set():

net.set(\"IonotropicSynapse_gS\", 0.0003)  # nS\n

To modify individual syanptic parameters, use the .select() method. Below, we change the values of the first two synapses:

net.select(edges=[0, 1]).set(\"IonotropicSynapse_gS\", 0.0004)  # nS\n

For more details on how to flexibly set synaptic parameters (e.g., by cell type, or by pre-synaptic cell index,\u2026), see this tutorial.

"},{"location":"tutorial/02_small_network/#stimulating-recording-and-simulating-the-network","title":"Stimulating, recording, and simulating the network","text":"

We will now set up a simulation of the network. This works exactly as it does for single neurons:

# Stimulus.\ni_delay = 3.0  # ms\ni_amp = 0.05  # nA\ni_dur = 2.0  # ms\n\n# Duration and step size.\ndt = 0.025  # ms\nt_max = 50.0  # ms\n
time_vec = jnp.arange(0.0, t_max + dt, dt)\n

As a simple example, we insert sodium, potassium, and leak into every compartment of every cell of the network.

net.insert(Na())\nnet.insert(K())\nnet.insert(Leak())\n

We stimulate every neuron in the input layer and record the voltage from the output neuron:

current = jx.step_current(i_delay, i_dur, i_amp, dt, t_max)\nnet.delete_stimuli()\nfor stim_ind in range(10):\n    net.cell(stim_ind).branch(0).loc(0.0).stimulate(current)\n\nnet.delete_recordings()\nnet.cell(10).branch(0).loc(0.0).record()\n
Added 1 external_states. See `.externals` for details.\nAdded 1 external_states. See `.externals` for details.\nAdded 1 external_states. See `.externals` for details.\nAdded 1 external_states. See `.externals` for details.\nAdded 1 external_states. See `.externals` for details.\nAdded 1 external_states. See `.externals` for details.\nAdded 1 external_states. See `.externals` for details.\nAdded 1 external_states. See `.externals` for details.\nAdded 1 external_states. See `.externals` for details.\nAdded 1 external_states. See `.externals` for details.\nAdded 1 recordings. See `.recordings` for details.\n

Finally, we can again run the network simulation and plot the result:

s = jx.integrate(net, delta_t=dt)\n
fig, ax = plt.subplots(1, 1, figsize=(4, 2))\n_ = ax.plot(s.T)\n

That\u2019s it! You now know how to simulate networks of morphologically detailed neurons. We recommend that you now have a look at how you can speed up your simulation. To learn more about handling synaptic parameters, we recommend to check out this tutorial.

"},{"location":"tutorial/04_jit_and_vmap/","title":"Speeding up simulations","text":"

In this tutorial, you will learn how to:

  • make parameter sweeps in Jaxley
  • use jit to compile your simulations and make them faster
  • use vmap to parallelize simulations on GPUs

Here is a code snippet which you will learn to understand in this tutorial:

from jax import jit, vmap\n\n\ncell = ...  # See tutorial on Basics of Jaxley.\n\ndef simulate(params):\n    param_state = None\n    param_state = cell.data_set(\"Na_gNa\", params[0], param_state)\n    param_state = cell.data_set(\"K_gK\", params[1], param_state)\n    return jx.integrate(cell, param_state=param_state, delta_t=0.025)\n\n# Define 100 sets of sodium and potassium conductances.\nall_params = jnp.asarray(np.random.rand(100, 2))\n\n# Fast for-loops with jit compilation.\njitted_simulate = jit(simulate)\nvoltages = [jitted_simulate(params) for params in all_params]\n\n# Using vmap for parallelization.\nvmapped_simulate = vmap(jitted_simulate, in_axes=(0,))\nvoltages = vmapped_simulate(all_params)\n

In the previous tutorials, you learned how to build single cells or networks and how to change their parameters. In this tutorial, you will learn how to speed up such simulations by many orders of magnitude. This can be achieved in to ways:

  • by using JIT compilation
  • by using GPU parallelization

Let\u2019s get started!

"},{"location":"tutorial/04_jit_and_vmap/#using-gpu-or-cpu","title":"Using GPU or CPU","text":"

In Jaxley you can set whether you want to use gpu or cpu with the following lines at the beginning of your script:

from jax import config\nconfig.update(\"jax_platform_name\", \"cpu\")\n

JAX (and Jaxley) also allow to choose between float32 and float64. Especially on GPUs, float32 will be faster, but we have experienced stability issues when simulating morphologically detailed neurons with float32.

config.update(\"jax_enable_x64\", True)  # Set to false to use `float32`.\n

Next, we will import relevant libraries:

import matplotlib.pyplot as plt\nimport numpy as np\nimport jax.numpy as jnp\nfrom jax import jit, vmap\n\nimport jaxley as jx\nfrom jaxley.channels import Na, K, Leak\n
"},{"location":"tutorial/04_jit_and_vmap/#building-the-cell-or-network","title":"Building the cell or network","text":"

We first build a cell (or network) in the same way as we showed in the previous tutorials:

dt = 0.025\nt_max = 10.0\n\ncomp = jx.Compartment()\nbranch = jx.Branch(comp, nseg=4)\ncell = jx.Cell(branch, parents=[-1, 0, 0, 1, 1, 2, 2])\n\ncell.insert(Na())\ncell.insert(K())\ncell.insert(Leak())\n\ncell.delete_stimuli()\ncurrent = jx.step_current(i_delay=1.0, i_dur=1.0, i_amp=0.1, delta_t=dt, t_max=t_max)\ncell.branch(0).loc(0.0).stimulate(current)\n\ncell.delete_recordings()\ncell.branch(0).loc(0.0).record()\n
Added 1 external_states. See `.externals` for details.\nAdded 1 recordings. See `.recordings` for details.\n
"},{"location":"tutorial/04_jit_and_vmap/#parameter-sweeps","title":"Parameter sweeps","text":"

Assume you want to run the same cell with many different values for the sodium and potassium conductance, for example for genetic algorithms or for parameter sweeps. To do this efficiently in Jaxley, you have to use the data_set() method (in combination with jit and vmap, as shown later):

def simulate(params):\n    param_state = None\n    param_state = cell.data_set(\"Na_gNa\", params[0], param_state)\n    param_state = cell.data_set(\"K_gK\", params[1], param_state)\n    return jx.integrate(cell, param_state=param_state, delta_t=dt)\n

The .data_set() method takes three arguments:

1) the name of the parameter you want to set. Jaxley allows to set the following parameters: \u201cradius\u201d, \u201clength\u201d, \u201caxial_resistivity\u201d, as well as all parameters of channels and synapses. 2) the value of the parameter. 3) a param_state which is initialized as None and is modified by .data_set(). This has to be passed to jx.integrate().

Having done this, the simplest (but least efficient) way to perform the parameter sweep is to run a for-loop over many parameter sets:

# Define 5 sets of sodium and potassium conductances.\nall_params = jnp.asarray(np.random.rand(5, 2))\n\nvoltages = jnp.asarray([simulate(params) for params in all_params])\nprint(\"voltages.shape\", voltages.shape)\n
voltages.shape (5, 1, 402)\n

The resulting voltages have shape (num_simulations, num_recordings, num_timesteps).

"},{"location":"tutorial/04_jit_and_vmap/#stimulus-sweeps","title":"Stimulus sweeps","text":"

In addition to running sweeps across multiple parameters, you can also run sweeeps across multiple stimuli (e.g. step current stimuli of different amplitudes. You can achieve this with the data_stimulate() method:

def simulate(i_amp):\n    current = jx.step_current(1.0, 1.0, i_amp, 0.025, 10.0)\n\n    data_stimuli = None\n    data_stimuli = cell.branch(0).comp(0).data_stimulate(current, data_stimuli)\n    return jx.integrate(cell, data_stimuli=data_stimuli)\n

"},{"location":"tutorial/04_jit_and_vmap/#speeding-up-for-loops-via-jit-compilation","title":"Speeding up for loops via jit compilation","text":"

We can speed up such parameter sweeps (or stimulus sweeps) with jit compilation. jit compilation will compile the simulation when it is run for the first time, such that every other simulation will be must faster. This can be achieved by defining a new function which uses JAX\u2019s jit():

jitted_simulate = jit(simulate)\n
# First run, will be slow.\nvoltages = jitted_simulate(all_params[0])\n
# More runs, will be much faster.\nvoltages = jnp.asarray([jitted_simulate(params) for params in all_params])\nprint(\"voltages.shape\", voltages.shape)\n
voltages.shape (5, 1, 402)\n

jit compilation can be up to 10k times faster, especially for small simulations with few compartments. For very large models, the gain obtained with jit will be much smaller (jit may even provide no speed up at all).

"},{"location":"tutorial/04_jit_and_vmap/#speeding-up-with-gpu-parallelization-via-vmap","title":"Speeding up with GPU parallelization via vmap","text":"

Another way to speed up parameter sweeps is with GPU parallelization. Parallelization in Jaxley can be achieved by using vmap of JAX. To do this, we first create a new function that handles multiple parameter sets directly:

# Using vmap for parallelization.\nvmapped_simulate = vmap(jitted_simulate)\n

We can then run this method on all parameter sets (all_params.shape == (100, 2)), and Jaxley will automatically parallelize across them. Of course, you will only get a speed-up if you have a GPU available and you specified gpu as device in the beginning of this tutorial.

voltages = vmapped_simulate(all_params)\n

GPU parallelization with vmap can give a large speed-up, which can easily be 2-3 orders of magnitude.

"},{"location":"tutorial/04_jit_and_vmap/#combining-jit-and-vmap","title":"Combining jit and vmap","text":"

Finally, you can also combine using jit and vmap. For example, you can run multiple batches of many parallel simulations. Each batch can be parallelized with vmap and simulating each batch can be compiled with jit:

jitted_vmapped_simulate = jit(vmap(simulate))\n
for batch in range(10):\n    all_params = jnp.asarray(np.random.rand(5, 2))\n    voltages_batch = jitted_vmapped_simulate(all_params)\n

That\u2019s all you have to know about jit and vmap! If you have worked through this and the previous tutorials, you should be ready to set up your first network simulations.

"},{"location":"tutorial/04_jit_and_vmap/#next-steps","title":"Next steps","text":"

If you want to learn more, we recommend you to read the tutorial on building channel and synapse models.

Alternatively, you can also directly jump ahead to the tutorial on training biophysical networks which will teach you how you can optimize parameters of biophysical models with gradient descent.

Finally, if you want to learn more about JAX, check out their tutorial on jit or their tutorial on vmap.

"},{"location":"tutorial/05_channel_and_synapse_models/","title":"Building ion channel models","text":"

In this tutorial, you will learn how to:

  • define your own ion channel models beyond the preconfigured channels in Jaxley

This tutorial assumes that you have already learned how to build basic simulations.

from jax import config\nconfig.update(\"jax_enable_x64\", True)\nconfig.update(\"jax_platform_name\", \"cpu\")\n\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport jax\nimport jax.numpy as jnp\nfrom jax import jit, value_and_grad\n\nimport jaxley as jx\n

First, we define a cell as you saw in the previous tutorial:

comp = jx.Compartment()\nbranch = jx.Branch(comp, nseg=4)\ncell = jx.Cell(branch, parents=[-1, 0, 0, 1, 1, 2, 2])\n

You have also already learned how to insert preconfigured channels into Jaxley models:

cell.insert(Na())\ncell.insert(K())\ncell.insert(Leak())\n

In this tutorial, we will show you how to build your own channel and synapse models.

"},{"location":"tutorial/05_channel_and_synapse_models/#your-own-channel","title":"Your own channel","text":"

Below is how you can define your own channel. We will go into detail about individual parts of the code in the next couple of cells.

import jax.numpy as jnp\nfrom jaxley.channels import Channel\nfrom jaxley.solver_gate import solve_gate_exponential\n\n\ndef exp_update_alpha(x, y):\n    return x / (jnp.exp(x / y) - 1.0)\n\nclass Potassium(Channel):\n    \"\"\"Potassium channel.\"\"\"\n\n    def __init__(self, name = None):\n        self.current_is_in_mA_per_cm2 = True\n        super().__init__(name)\n        self.channel_params = {\"gK_new\": 1e-4}\n        self.channel_states = {\"n_new\": 0.0}\n        self.current_name = \"i_K\"\n\n    def update_states(self, states, dt, v, params):\n        \"\"\"Update state.\"\"\"\n        ns = states[\"n_new\"]\n        alpha = 0.01 * exp_update_alpha(-(v + 55), 10)\n        beta = 0.125 * jnp.exp(-(v + 65) / 80)\n        new_n = solve_gate_exponential(ns, dt, alpha, beta)\n        return {\"n_new\": new_n}\n\n    def compute_current(self, states, v, params):\n        \"\"\"Return current.\"\"\"\n        ns = states[\"n_new\"]\n        kd_conds = params[\"gK_new\"] * ns**4  # S/cm^2\n\n        e_kd = -77.0        \n        return kd_conds * (v - e_kd)\n\n    def init_state(self, states, v, params, delta_t):\n        alpha = 0.01 * exp_update_alpha(-(v + 55), 10)\n        beta = 0.125 * jnp.exp(-(v + 65) / 80)\n        return {\"n_new\": alpha / (alpha + beta)}\n

Let\u2019s look at each part of this in detail.

The below is simply a helper function for the solver of the gate variables:

def exp_update_alpha(x, y):\n    return x / (jnp.exp(x / y) - 1.0)\n

Next, we define our channel as a class. It should inherit from the Channel class and define channel_params, channel_states, and current_name. You also need to set self.current_is_in_mA_per_cm2=True as the first line on your __init__() method. This is to acknowledge that your current is returned in mA/cm2 (not in uA/cm2, as would have been required in Jaxley versions 0.4.0 or older).

class Potassium(Channel):\n    \"\"\"Potassium channel.\"\"\"\n\n    def __init__(self, name=None):\n        self.current_is_in_mA_per_cm2 = True\n        super().__init__(name)\n        self.channel_params = {\"gK_new\": 1e-4}\n        self.channel_states = {\"n_new\": 0.0}\n        self.current_name = \"i_K\"\n

Next, we have the update_states() method, which updates the gating variables:

    def update_states(self, states, dt, v, params):\n

Every channel you define must have an update_states() method which takes exactly these five arguments (self, states, dt, v, params). The inputs states to the update_states method is a dictionary which contains all states that are updated (including states of other channels). v is a jnp.ndarray which contains the voltage of a single compartment (shape ()). Let\u2019s get the state of the potassium channel which we are building here:

ns = states[\"n_new\"]\n

Next, we update the state of the channel. In this example, we do this with exponential Euler, but you can implement any solver yourself:

alpha = 0.01 * exp_update_alpha(-(v + 55), 10)\nbeta = 0.125 * jnp.exp(-(v + 65) / 80)\nnew_n = solve_gate_exponential(ns, dt, alpha, beta)\nreturn {\"n_new\": new_n}\n

A channel also needs a compute_current() method which returns the current throught the channel:

    def compute_current(self, states, v, params):\n        ns = states[\"n_new\"]\n\n        # Multiply with 1000 to convert Siemens to milli Siemens.\n        kd_conds = params[\"gK_new\"] * ns**4  # S/cm^2\n\n        e_kd = -77.0        \n        current = kd_conds * (v - e_kd)\n        return current\n

Finally, the init_state() method can be implemented optionally. It can be used to automatically compute the initial state based on the voltage when cell.init_states() is run.

Alright, done! We can now insert this channel into any jx.Module such as our cell:

cell.insert(Potassium())\n
cell.delete_stimuli()\ncurrent = jx.step_current(1.0, 1.0, 0.1, 0.025, 10.0)\ncell.branch(0).comp(0).stimulate(current)\n\ncell.delete_recordings()\ncell.branch(0).comp(0).record()\n
Added 1 external_states. See `.externals` for details.\nAdded 1 recordings. See `.recordings` for details.\n
s = jx.integrate(cell)\n
fig, ax = plt.subplots(1, 1, figsize=(4, 2))\n_ = ax.plot(s.T[:-1])\n_ = ax.set_ylim([-80, 50])\n_ = ax.set_xlabel(\"Time (ms)\")\n_ = ax.set_ylabel(\"Voltage (mV)\")\n

"},{"location":"tutorial/05_channel_and_synapse_models/#your-own-synapse","title":"Your own synapse","text":"

The parts below assume that you have already learned how to build network simulations in Jaxley.

Note that again, a synapse needs to have the two functions update_states and compute_current with all input arguments shown below.

The below is an example of how to define your own synapse model in Jaxley:

import jax.numpy as jnp\nfrom jaxley.synapses.synapse import Synapse\n\n\nclass TestSynapse(Synapse):\n    \"\"\"\n    Compute syanptic current and update syanpse state.\n    \"\"\"\n    def __init__(self, name = None):\n        super().__init__(name)\n        self.synapse_params = {\"gChol\": 0.001, \"eChol\": 0.0}\n        self.synapse_states = {\"s_chol\": 0.1}\n\n    def update_states(self, states, delta_t, pre_voltage, post_voltage, params):\n        \"\"\"Return updated synapse state and current.\"\"\"\n        s_inf = 1.0 / (1.0 + jnp.exp((-35.0 - pre_voltage) / 10.0))\n        exp_term = jnp.exp(-delta_t)\n        new_s = states[\"s_chol\"] * exp_term + s_inf * (1.0 - exp_term)\n        return {\"s_chol\": new_s}\n\n    def compute_current(self, states, pre_voltage, post_voltage, params):\n        g_syn = params[\"gChol\"] * states[\"s_chol\"]\n        return g_syn * (post_voltage - params[\"eChol\"])\n

As you can see above, synapses follow closely how channels are defined. The main difference is that the compute_current method takes two voltages: the pre-synaptic voltage (a jnp.ndarray of shape ()) and the post-synaptic voltage (a jnp.ndarray of shape ()).

net = jx.Network([cell for _ in range(3)])\n
from jaxley.connect import connect\n\npre = net.cell(0).branch(0).loc(0.0)\npost = net.cell(1).branch(0).loc(0.0)\nconnect(pre, post, TestSynapse())\n
net.cell(0).branch(0).loc(0.0).stimulate(jx.step_current(1.0, 2.0, 0.1, 0.025, 10.0))\nfor i in range(3):\n    net.cell(i).branch(0).loc(0.0).record()\n
Added 1 external_states. See `.externals` for details.\nAdded 1 recordings. See `.recordings` for details.\nAdded 1 recordings. See `.recordings` for details.\nAdded 1 recordings. See `.recordings` for details.\n
s = jx.integrate(net)\n
fig, ax = plt.subplots(1, 1, figsize=(4, 2))\n_ = ax.plot(s.T[:-1])\n_ = ax.set_ylim([-80, 50])\n_ = ax.set_xlabel(\"Time (ms)\")\n_ = ax.set_ylabel(\"Voltage (mV)\")\n

That\u2019s it! You are now ready to build your own custom simulations and equip them with channel and synapse models!

This tutorial does not have an immediate follow-up tutorial. If you have not done so already, you can check out our tutorial on training biophysical networks which will teach you how you can optimize parameters of biophysical models with gradient descent.

"},{"location":"tutorial/06_groups/","title":"Defining groups","text":"

In this tutorial, you will learn how to:

  • define groups (aka sectionlists) to simplify iteractions with Jaxley

Here is a code snippet which you will learn to understand in this tutorial:

from jax import jit, vmap\n\n\nnet = ...  # See tutorial on Basics of Jaxley.\n\nnet.cell(0).add_to_group(\"fast_spiking\")\nnet.cell(1).add_to_group(\"slow_spiking\")\n\ndef simulate(params):\n    param_state = None\n    param_state = net.fast_spiking.data_set(\"HH_gNa\", params[0], param_state)\n    param_state = net.slow_spiking.data_set(\"HH_gNa\", params[1], param_state)\n    return jx.integrate(net, param_state=param_state)\n\n# Define sodium for fast and slow spiking neurons.\nparams = jnp.asarray([1.0, 0.1])\n\n# Run simulation.\nvoltages = simulate(params)\n

In many cases, you might want to group several compartments (or branches, or cells) and assign a unique parameter or mechanism to this group. For example, you might want to define a couple of branches as basal and then assign a Hodgkin-Huxley mechanism only to those branches. Or you might define a couple of cells as fast spiking and assign them a high value for the sodium conductance. We describe how you can do this in this tutorial.

from jax import config\nconfig.update(\"jax_enable_x64\", True)\nconfig.update(\"jax_platform_name\", \"cpu\")\n\nimport time\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport jax\nimport jax.numpy as jnp\nfrom jax import jit, value_and_grad\n\nimport jaxley as jx\nfrom jaxley.channels import Na, K, Leak\nfrom jaxley.synapses import IonotropicSynapse\nfrom jaxley.connect import fully_connect\n

First, we define a network as you saw in the previous tutorial:

comp = jx.Compartment()\nbranch = jx.Branch(comp, nseg=2)\ncell = jx.Cell(branch, parents=[-1, 0, 0, 1])\nnetwork = jx.Network([cell for _ in range(3)])\n\npre = network.cell([0, 1])\npost = network.cell([2])\nfully_connect(pre, post, IonotropicSynapse())\n\nnetwork.insert(Na())\nnetwork.insert(K())\nnetwork.insert(Leak())\n
/Users/michaeldeistler/Documents/phd/jaxley/jaxley/modules/base.py:1533: FutureWarning: The behavior of DataFrame concatenation with empty or all-NA entries is deprecated. In a future version, this will no longer exclude empty or all-NA columns when determining the result dtypes. To retain the old behavior, exclude the relevant entries before the concat operation.\n  self.pointer.edges = pd.concat(\n
"},{"location":"tutorial/06_groups/#group-apical-dendrites","title":"Group: apical dendrites","text":"

Assume that, in each of the five neurons in this network, the second and forth branch are apical dendrites. We can define this as:

for cell_ind in range(3):\n    network.cell(cell_ind).branch(1).add_to_group(\"apical\")\n    network.cell(cell_ind).branch(3).add_to_group(\"apical\")\n

After this, we can access network.apical as we previously accesses anything else:

network.apical.set(\"radius\", 0.3)\n
network.apical.view\n
comp_index branch_index cell_index length radius axial_resistivity capacitance v Na Na_gNa ... K_gK eK K_n Leak Leak_gLeak Leak_eLeak global_comp_index global_branch_index global_cell_index controlled_by_param 2 2 1 0 10.0 0.3 5000.0 1.0 -70.0 True 0.05 ... 0.005 -90.0 0.2 True 0.0001 -70.0 2 1 0 0 3 3 1 0 10.0 0.3 5000.0 1.0 -70.0 True 0.05 ... 0.005 -90.0 0.2 True 0.0001 -70.0 3 1 0 0 6 6 3 0 10.0 0.3 5000.0 1.0 -70.0 True 0.05 ... 0.005 -90.0 0.2 True 0.0001 -70.0 6 3 0 0 7 7 3 0 10.0 0.3 5000.0 1.0 -70.0 True 0.05 ... 0.005 -90.0 0.2 True 0.0001 -70.0 7 3 0 0 10 10 5 1 10.0 0.3 5000.0 1.0 -70.0 True 0.05 ... 0.005 -90.0 0.2 True 0.0001 -70.0 10 5 1 0 11 11 5 1 10.0 0.3 5000.0 1.0 -70.0 True 0.05 ... 0.005 -90.0 0.2 True 0.0001 -70.0 11 5 1 0 14 14 7 1 10.0 0.3 5000.0 1.0 -70.0 True 0.05 ... 0.005 -90.0 0.2 True 0.0001 -70.0 14 7 1 0 15 15 7 1 10.0 0.3 5000.0 1.0 -70.0 True 0.05 ... 0.005 -90.0 0.2 True 0.0001 -70.0 15 7 1 0 18 18 9 2 10.0 0.3 5000.0 1.0 -70.0 True 0.05 ... 0.005 -90.0 0.2 True 0.0001 -70.0 18 9 2 0 19 19 9 2 10.0 0.3 5000.0 1.0 -70.0 True 0.05 ... 0.005 -90.0 0.2 True 0.0001 -70.0 19 9 2 0 22 22 11 2 10.0 0.3 5000.0 1.0 -70.0 True 0.05 ... 0.005 -90.0 0.2 True 0.0001 -70.0 22 11 2 0 23 23 11 2 10.0 0.3 5000.0 1.0 -70.0 True 0.05 ... 0.005 -90.0 0.2 True 0.0001 -70.0 23 11 2 0

12 rows \u00d7 25 columns

"},{"location":"tutorial/06_groups/#group-fast-spiking","title":"Group: fast spiking","text":"

Similarly, you could define a group of fast-spiking cells. Assume that the first and second cell are fast-spiking:

network.cell(0).add_to_group(\"fast_spiking\")\nnetwork.cell(1).add_to_group(\"fast_spiking\")\n
network.fast_spiking.set(\"Na_gNa\", 0.4)\n
network.fast_spiking.view\n
comp_index branch_index cell_index length radius axial_resistivity capacitance v Na Na_gNa ... K_gK eK K_n Leak Leak_gLeak Leak_eLeak global_comp_index global_branch_index global_cell_index controlled_by_param 0 0 0 0 10.0 1.0 5000.0 1.0 -70.0 True 0.4 ... 0.005 -90.0 0.2 True 0.0001 -70.0 0 0 0 0 1 1 0 0 10.0 1.0 5000.0 1.0 -70.0 True 0.4 ... 0.005 -90.0 0.2 True 0.0001 -70.0 1 0 0 0 2 2 1 0 10.0 0.3 5000.0 1.0 -70.0 True 0.4 ... 0.005 -90.0 0.2 True 0.0001 -70.0 2 1 0 0 3 3 1 0 10.0 0.3 5000.0 1.0 -70.0 True 0.4 ... 0.005 -90.0 0.2 True 0.0001 -70.0 3 1 0 0 4 4 2 0 10.0 1.0 5000.0 1.0 -70.0 True 0.4 ... 0.005 -90.0 0.2 True 0.0001 -70.0 4 2 0 0 5 5 2 0 10.0 1.0 5000.0 1.0 -70.0 True 0.4 ... 0.005 -90.0 0.2 True 0.0001 -70.0 5 2 0 0 6 6 3 0 10.0 0.3 5000.0 1.0 -70.0 True 0.4 ... 0.005 -90.0 0.2 True 0.0001 -70.0 6 3 0 0 7 7 3 0 10.0 0.3 5000.0 1.0 -70.0 True 0.4 ... 0.005 -90.0 0.2 True 0.0001 -70.0 7 3 0 0 8 8 4 1 10.0 1.0 5000.0 1.0 -70.0 True 0.4 ... 0.005 -90.0 0.2 True 0.0001 -70.0 8 4 1 0 9 9 4 1 10.0 1.0 5000.0 1.0 -70.0 True 0.4 ... 0.005 -90.0 0.2 True 0.0001 -70.0 9 4 1 0 10 10 5 1 10.0 0.3 5000.0 1.0 -70.0 True 0.4 ... 0.005 -90.0 0.2 True 0.0001 -70.0 10 5 1 0 11 11 5 1 10.0 0.3 5000.0 1.0 -70.0 True 0.4 ... 0.005 -90.0 0.2 True 0.0001 -70.0 11 5 1 0 12 12 6 1 10.0 1.0 5000.0 1.0 -70.0 True 0.4 ... 0.005 -90.0 0.2 True 0.0001 -70.0 12 6 1 0 13 13 6 1 10.0 1.0 5000.0 1.0 -70.0 True 0.4 ... 0.005 -90.0 0.2 True 0.0001 -70.0 13 6 1 0 14 14 7 1 10.0 0.3 5000.0 1.0 -70.0 True 0.4 ... 0.005 -90.0 0.2 True 0.0001 -70.0 14 7 1 0 15 15 7 1 10.0 0.3 5000.0 1.0 -70.0 True 0.4 ... 0.005 -90.0 0.2 True 0.0001 -70.0 15 7 1 0

16 rows \u00d7 25 columns

"},{"location":"tutorial/06_groups/#groups-from-swc-files","title":"Groups from SWC files","text":"

If you are reading .swc morphologigies, you can automatically assign groups with

jx.read_swc(file_name, nseg=n, assign_groups=True).\n
After that, you can directly use cell.soma, cell.apical, cell.basal, or cell.axon.

"},{"location":"tutorial/06_groups/#how-groups-are-interpreted-by-make_trainable","title":"How groups are interpreted by .make_trainable()","text":"

If you make a parameter of a group trainable, then it will be treated as a single shared parameter for a given property:

network.fast_spiking.make_trainable(\"Na_gNa\")\n
Number of newly added trainable parameters: 1. Total number of trainable parameters: 1\n

As such, get_parameters() returns only a single trainable parameter, which will be the sodium conductance for every compartment of every fast-spiking neuron:

network.get_parameters()\n
[{'Na_gNa': Array([0.4], dtype=float64)}]\n

If, instead, you would want a separate parameter for every fast-spiking cell, you should not use the group, but instead do the following (remember that fast-spiking neurons had indices [0,1]):

network.cell([0,1]).make_trainable(\"axial_resistivity\")\n
Number of newly added trainable parameters: 2. Total number of trainable parameters: 3\n
network.get_parameters()\n
[{'Na_gNa': Array([0.4], dtype=float64)},\n {'axial_resistivity': Array([5000., 5000.], dtype=float64)}]\n

This generated two parameters for the axial resistivitiy, each corresponding to one cell.

"},{"location":"tutorial/06_groups/#summary","title":"Summary","text":"

Groups allow you to organize your simulation in a more intuitive way, and they allow to perform parameter sharing with make_trainable().

"},{"location":"tutorial/07_gradient_descent/","title":"Training biophysical models","text":"

In this tutorial, you will learn how to train biophysical models in Jaxley. This includes the following:

  • compute the gradient with respect to parameters
  • use parameter transformations
  • use multi-level checkpointing
  • define optimizers
  • write dataloaders and parallelize across data

Here is a code snippet which you will learn to understand in this tutorial:

from jax import jit, vmap, value_and_grad\nimport jaxley as jx\nimport jaxley.optimize.transforms as jt\n\nnet = ...  # See tutorial on the basics of `Jaxley`.\n\n# Define which parameters to train.\nnet.cell(\"all\").make_trainable(\"HH_gNa\")\nnet.IonotropicSynapse.make_trainable(\"IonotropicSynapse_gS\")\nparameters = net.get_parameters()\n\n# Define parameter transform and apply it to the parameters.\ntransform = jx.ParamTransform([\n    {\"IonotropicSynapse_gS\": jt.SigmoidTransform(0.0, 1.0)},\n    {\"HH_gNa\":jt.SigmoidTransform(0.0, 1, 0)}\n])\n\nopt_params = transform.inverse(parameters)\n\n# Define simulation and batch it across stimuli.\ndef simulate(params, datapoint):\n    current = jx.datapoint_to_step_currents(i_delay=1.0, i_dur=1.0, i_amps=datapoint, dt=0.025, t_max=5.0)\n    data_stimuli = net.cell(0).branch(0).comp(0).data_stimulate(current, None)\n    return jx.integrate(net, params=params, data_stimuli=data_stimuli, checkpoint_inds=[20, 20], delta_t=0.025)\n\nbatch_simulate = vmap(simulate, in_axes=(None, 0))\n\n# Define loss function and its gradient.\ndef loss_fn(opt_params, datapoints, label):\n    params = transform.forward(opt_params)\n    voltages = batch_simulate(params, datapoints)\n    return jnp.abs(jnp.mean(voltages) - label)\n\ngrad_fn = jit(value_and_grad(loss_fn, argnums=0))\n\n# Define data and dataloader.\ndata = jnp.asarray(np.random.randn(100, 3))\ndataloader = Dataset.from_tensor_slices((inputs, labels))\ndataloader = dataloader.shuffle(dataloader.cardinality()).batch(4)\n\n# Define the optimizer.\noptimizer = optax.Adam(lr=0.01)\nopt_state = optimizer.init_state(opt_params)\n\nfor epoch in range(10):\n    for batch in dataloader:\n        stimuli = batch[0].numpy()\n        labels = batch[1].numpy()\n        loss, gradient = grad_fn(opt_params, stimuli, labels)\n\n        # Optimizer step.\n        updates, opt_state = optimizer.update(gradient, opt_state)\n        opt_params = optax.apply_updates(opt_params, updates)\n

from jax import config\nconfig.update(\"jax_enable_x64\", True)\nconfig.update(\"jax_platform_name\", \"cpu\")\n\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport jax\nimport jax.numpy as jnp\nfrom jax import jit, vmap, value_and_grad\n\nimport jaxley as jx\nfrom jaxley.channels import Leak\nfrom jaxley.synapses import TanhRateSynapse\nfrom jaxley.connect import fully_connect\n

First, we define a network as you saw in the previous tutorial:

_ = np.random.seed(0)  # For synaptic locations.\n\ncomp = jx.Compartment()\nbranch = jx.Branch(comp, nseg=2)\ncell = jx.Cell(branch, parents=[-1, 0, 0])\nnet = jx.Network([cell for _ in range(3)])\n\npre = net.cell([0, 1])\npost = net.cell([2])\nfully_connect(pre, post, TanhRateSynapse())\n\n# Change some default values of the tanh synapse.\nnet.TanhRateSynapse.set(\"TanhRateSynapse_x_offset\", -60.0)\nnet.TanhRateSynapse.set(\"TanhRateSynapse_gS\", 1e-3)\nnet.TanhRateSynapse.set(\"TanhRateSynapse_slope\", 0.1)\n\nnet.insert(Leak())\n

This network consists of three neurons arranged in two layers:

net.compute_xyz()\nnet.rotate(180)\nfig, ax = plt.subplots(1, 1, figsize=(3, 2))\n_ = net.vis(ax=ax, detail=\"full\", layers=[2, 1], layer_kwargs={\"within_layer_offset\": 100.0, \"between_layer_offset\": 100.0}) \n

We consider the last neuron as the output neuron and record the voltage from there:

net.delete_recordings()\nnet.cell(0).branch(0).loc(0.0).record()\nnet.cell(1).branch(0).loc(0.0).record()\nnet.cell(2).branch(0).loc(0.0).record()\n
Added 1 recordings. See `.recordings` for details.\nAdded 1 recordings. See `.recordings` for details.\nAdded 1 recordings. See `.recordings` for details.\n
"},{"location":"tutorial/07_gradient_descent/#defining-a-dataset","title":"Defining a dataset","text":"

We will train this biophysical network on a classification task. The inputs will be values and the label is binary:

inputs = jnp.asarray(np.random.rand(100, 2))\nlabels = jnp.asarray((inputs[:, 0] + inputs[:, 1]) > 1.0)\n
fig, ax = plt.subplots(1, 1, figsize=(3, 2))\n_ = ax.scatter(inputs[labels, 0], inputs[labels, 1])\n_ = ax.scatter(inputs[~labels, 0], inputs[~labels, 1])\n

labels = labels.astype(float)\n
"},{"location":"tutorial/07_gradient_descent/#defining-trainable-parameters","title":"Defining trainable parameters","text":"
net.delete_trainables()\n

This follows the same API as .set() seen in the previous tutorial. If you want to use a single parameter for all radiuses in the entire network, do:

net.make_trainable(\"radius\")\n
Number of newly added trainable parameters: 1. Total number of trainable parameters: 1\n

We can also define parameters for individual compartments. To do this, use the \"all\" key. The following defines a separate parameter the sodium conductance for every compartment in the entire network:

net.cell(\"all\").branch(\"all\").loc(\"all\").make_trainable(\"Leak_gLeak\")\n
Number of newly added trainable parameters: 18. Total number of trainable parameters: 19\n
"},{"location":"tutorial/07_gradient_descent/#making-synaptic-parameters-trainable","title":"Making synaptic parameters trainable","text":"

Synaptic parameters can be made trainable in the exact same way. To use a single parameter for all syanptic conductances in the entire network, do

net.TanhRateSynapse.make_trainable(\"TanhRateSynapse_gS\")\n

Here, we use a different syanptic conductance for all syanpses. This can be done as follows:

net.TanhRateSynapse.edge(\"all\").make_trainable(\"TanhRateSynapse_gS\")\n
Number of newly added trainable parameters: 2. Total number of trainable parameters: 21\n
"},{"location":"tutorial/07_gradient_descent/#running-the-simulation","title":"Running the simulation","text":"

Once all parameters are defined, you have to use .get_parameters() to obtain all trainable parameters. This is also the time to check how many trainable parameters your network has:

params = net.get_parameters()\n

You can now run the simulation with the trainable parameters by passing them to the jx.integrate function.

s = jx.integrate(net, params=params, t_max=10.0)\n
"},{"location":"tutorial/07_gradient_descent/#stimulating-the-network","title":"Stimulating the network","text":"

The network above does not yet get any stimuli. We will use the 2D inputs from the dataset to stimulate the two input neurons. The amplitude of the step current corresponds to the input value. Below is the simulator that defines this:

def simulate(params, inputs):\n    currents = jx.datapoint_to_step_currents(i_delay=1.0, i_dur=1.0, i_amp=inputs / 10, delta_t=0.025, t_max=10.0)\n\n    data_stimuli = None\n    data_stimuli = net.cell(0).branch(2).loc(1.0).data_stimulate(currents[0], data_stimuli=data_stimuli)\n    data_stimuli = net.cell(1).branch(2).loc(1.0).data_stimulate(currents[1], data_stimuli=data_stimuli)\n\n    return jx.integrate(net, params=params, data_stimuli=data_stimuli, delta_t=0.025)\n\nbatched_simulate = vmap(simulate, in_axes=(None, 0))\n

We can also inspect some traces:

traces = batched_simulate(params, inputs[:4])\n
fig, ax = plt.subplots(1, 1, figsize=(4, 2))\n_ = ax.plot(traces[:, 2, :].T)\n

"},{"location":"tutorial/07_gradient_descent/#defining-a-loss-function","title":"Defining a loss function","text":"

Let us define a loss function to be optimized:

def loss(params, inputs, labels):\n    traces = batched_simulate(params, inputs)  # Shape `(batchsize, num_recordings, timepoints)`.\n    prediction = jnp.mean(traces[:, 2], axis=1)  # Use the average over time of the output neuron (2) as prediction.\n    prediction = (prediction + 72.0) / 5  # Such that the prediction is roughly in [0, 1].\n    losses = jnp.abs(prediction - labels)  # Mean absolute error loss.\n    return jnp.mean(losses)  # Average across the batch.\n

And we can use JAX\u2019s inbuilt functions to take the gradient through the entire ODE:

jitted_grad = jit(value_and_grad(loss, argnums=0))\n
value, gradient = jitted_grad(params, inputs[:4], labels[:4])\n
"},{"location":"tutorial/07_gradient_descent/#defining-parameter-transformations","title":"Defining parameter transformations","text":"

Before training, however, we will enforce for all parameters to be within a prespecified range (such that, e.g., conductances can not become negative)

import jaxley.optimize.transforms as jt\n
# Define a function to create appropriate transforms for each parameter\ndef create_transform(name):\n    if name == \"axial_resistivity\":\n        # Must be positive; apply Softplus and scale to match initialization\n        return jt.ChainTransform([jt.SoftplusTransform(0), jt.AffineTransform(5000, 0)])\n    elif name == \"length\":\n        # Apply Softplus and affine transform for the 'length' parameter\n        return jt.ChainTransform([jt.SoftplusTransform(0), jt.AffineTransform(10, 0)])\n    else:\n        # Default to a Softplus transform for other parameters\n        return jt.SoftplusTransform(0)\n\n# Apply the transforms to the parameters\ntransforms = [{k: create_transform(k) for k in param} for param in params]\ntf = jt.ParamTransform(transforms)\n
transform = jx.ParamTransform([{\"radius\": jt.SigmoidTransform(0.1, 5.0)},\n                               {\"Leak_gLeak\":jt.SigmoidTransform(1e-5, 1e-3)},\n                               {\"TanhRateSynapse_gS\" : jt.SigmoidTransform(1e-5, 1e-2)}])\n

With these modify the loss function acocrdingly:

def loss(opt_params, inputs, labels):\n    transform.forward(opt_params)\n\n    traces = batched_simulate(params, inputs)  # Shape `(batchsize, num_recordings, timepoints)`.\n    prediction = jnp.mean(traces[:, 2], axis=1)  # Use the average over time of the output neuron (2) as prediction.\n    prediction = (prediction + 72.0)  # Such that the prediction is around 0.\n    losses = jnp.abs(prediction - labels)  # Mean absolute error loss.\n    return jnp.mean(losses)  # Average across the batch.\n
"},{"location":"tutorial/07_gradient_descent/#using-checkpointing","title":"Using checkpointing","text":"

Checkpointing allows to vastly reduce the memory requirements of training biophysical models (see also JAX\u2019s full tutorial on checkpointing).

t_max = 5.0\ndt = 0.025\n\nlevels = 2\ntime_points = t_max // dt + 2\ncheckpoints = [int(np.ceil(time_points**(1/levels))) for _ in range(levels)]\n

To enable checkpointing, we have to modify the simulate function appropriately and use

jx.integrate(..., checkpoint_inds=checkpoints)\n
as done below:

def simulate(params, inputs):\n    currents = jx.datapoint_to_step_currents(i_delay=1.0, i_dur=1.0, i_amp=inputs / 10.0, delta_t=dt, t_max=t_max)\n\n    data_stimuli = None\n    data_stimuli = net.cell(0).branch(2).loc(1.0).data_stimulate(currents[0], data_stimuli=data_stimuli)\n    data_stimuli = net.cell(1).branch(2).loc(1.0).data_stimulate(currents[1], data_stimuli=data_stimuli)\n\n    return jx.integrate(net, params=params, data_stimuli=data_stimuli, checkpoint_lengths=checkpoints)\n\nbatched_simulate = vmap(simulate, in_axes=(None, 0))\n\n\ndef predict(params, inputs):\n    traces = simulate(params, inputs)  # Shape `(batchsize, num_recordings, timepoints)`.\n    prediction = jnp.mean(traces[2])  # Use the average over time of the output neuron (2) as prediction.\n    return prediction + 72.0  # Such that the prediction is around 0.\n\nbatched_predict = vmap(predict, in_axes=(None, 0))\n\n\ndef loss(opt_params, inputs, labels):\n    params = transform.forward(opt_params)\n\n    predictions = batched_predict(params, inputs)\n    losses = jnp.abs(predictions - labels)  # Mean absolute error loss.\n    return jnp.mean(losses)  # Average across the batch.\n\njitted_grad = jit(value_and_grad(loss, argnums=0))\n
"},{"location":"tutorial/07_gradient_descent/#training","title":"Training","text":"

We will use the ADAM optimizer from the optax library to optimize the free parameters (you have to install the package with pip install optax first):

import optax\n
opt_params = transform.inverse(params)\noptimizer = optax.adam(learning_rate=0.01)\nopt_state = optimizer.init(opt_params)\n
"},{"location":"tutorial/07_gradient_descent/#writing-a-dataloader","title":"Writing a dataloader","text":"

Below, we just write our own (very simple) dataloader. Alternatively, you could use the dataloader from any deep learning library such as pytorch or tensorflow:

class Dataset:\n    def __init__(self, inputs: np.ndarray, labels: np.ndarray):\n        \"\"\"Simple Dataloader.\n\n        Args:\n            inputs: Array of shape (num_samples, num_dim)\n            labels: Array of shape (num_samples,)\n        \"\"\"\n        assert len(inputs) == len(labels), \"Inputs and labels must have same length\"\n        self.inputs = inputs\n        self.labels = labels\n        self.num_samples = len(inputs)\n        self._rng_state = None\n        self.batch_size = 1\n\n    def shuffle(self, seed=None):\n        \"\"\"Shuffle the dataset in-place\"\"\"\n        self._rng_state = np.random.get_state()[1][0] if seed is None else seed\n        np.random.seed(self._rng_state)\n        indices = np.random.permutation(self.num_samples)\n        self.inputs = self.inputs[indices]\n        self.labels = self.labels[indices]\n        return self\n\n    def batch(self, batch_size):\n        \"\"\"Create batches of the data\"\"\"\n        self.batch_size = batch_size\n        return self\n\n    def __iter__(self):\n        self.shuffle(seed=self._rng_state)\n        for start in range(0, self.num_samples, self.batch_size):\n            end = min(start + self.batch_size, self.num_samples)\n            yield self.inputs[start:end], self.labels[start:end]\n        self._rng_state += 1\n
"},{"location":"tutorial/07_gradient_descent/#training-loop","title":"Training loop","text":"
batch_size = 4\ndataloader = Dataset(inputs, labels)\ndataloader = dataloader.shuffle(seed=0).batch(batch_size)\n\nfor epoch in range(10):\n    epoch_loss = 0.0\n\n    for batch_ind, batch in enumerate(dataloader):\n        current_batch, label_batch = batch\n        loss_val, gradient = jitted_grad(opt_params, current_batch, label_batch)\n        updates, opt_state = optimizer.update(gradient, opt_state)\n        opt_params = optax.apply_updates(opt_params, updates)\n        epoch_loss += loss_val\n\n    print(f\"epoch {epoch}, loss {epoch_loss}\")\n\nfinal_params = transform.forward(opt_params)\n
epoch 0, loss 25.033223182772293\nepoch 1, loss 21.00894915349165\nepoch 2, loss 15.092242959956026\nepoch 3, loss 9.061544660383163\nepoch 4, loss 6.925509860325612\nepoch 5, loss 6.273630037897756\nepoch 6, loss 6.1757316054693145\nepoch 7, loss 6.135132525725265\nepoch 8, loss 6.145608619185389\nepoch 9, loss 6.135660902068834\n
ntest = 32\npredictions = batched_predict(final_params, inputs[:ntest])\n
fig, ax = plt.subplots(1, 1, figsize=(3, 2))\n_ = ax.scatter(labels[:ntest], predictions)\n_ = ax.set_xlabel(\"Label\")\n_ = ax.set_ylabel(\"Prediction\")\n

Indeed, the loss goes down and the network successfully classifies the patterns.

"},{"location":"tutorial/07_gradient_descent/#summary","title":"Summary","text":"

Puh, this was a pretty dense tutorial with a lot of material. You should have learned how to:

  • compute the gradient with respect to parameters
  • use parameter transformations
  • use multi-level checkpointing
  • define optimizers
  • write dataloaders and parallelize across data

This was the last \u201cbasic\u201d tutorial of the Jaxley toolbox. If you want to learn more, check out our Advanced Tutorials. If anything is still unclear please create a discussion. If you find any bugs, please open an issue. Happy coding!

"},{"location":"tutorial/08_importing_morphologies/","title":"Working with morphologies","text":"

In this tutorial, you will learn how to:

  • Load morphologies and make them compatible with Jaxley
  • Use the visualization features
  • Assemble a small network of morphologically accurate cells.

Here is a code snippet which you will learn to understand in this tutorial:

import jaxley as jx\n\ncell = jx.read_swc(\"my_cell.swc\", nseg=4, assign_groups=True)\n

To work with more complicated morphologies, Jaxley supports importing morphological reconstructions via .swc files. .swc is currently the only supported format. Other formats like .asc need to be converted to .swc first, for example using the BlueBrain\u2019s morph-tool. For more information on the exact specifications of .swc see here.

import jaxley as jx\nfrom jaxley.synapses import IonotropicSynapse\nimport matplotlib.pyplot as plt\n

To work with .swc files, Jaxley implements a custom .swc reader. The reader traces the morphology and identifies all uninterrupted sections. These are then partitioned into branches, each of which will be approximated by a number of equally many compartments that can be simulated fully in parallel.

To demonstrate this, let\u2019s import an example morphology of a Layer 5 pyramidal cell and visualize it.

# import swc file into jx.Cell object\nfname = \"data/morph.swc\"\ncell = jx.read_swc(fname, nseg=8, max_branch_len=2000.0, assign_groups=True)\n\n# print shape (num_branches, num_comps)\nprint(cell.shape)\n\ncell.show()\n
(157, 1256)\n
local_comp_index global_comp_index local_branch_index global_branch_index local_cell_index global_cell_index 0 0 0 0 0 0 0 1 1 1 0 0 0 0 2 2 2 0 0 0 0 3 3 3 0 0 0 0 4 4 4 0 0 0 0 ... ... ... ... ... ... ... 1251 3 1251 156 156 0 0 1252 4 1252 156 156 0 0 1253 5 1253 156 156 0 0 1254 6 1254 156 156 0 0 1255 7 1255 156 156 0 0

1256 rows \u00d7 6 columns

As we can see, this yields a morphology that is approximated by 1256 compartments. Depending on the amount of detail that you need, you can also change the number of compartments in each branch:

cell = jx.read_swc(fname, nseg=2, max_branch_len=2000.0, assign_groups=True)\n\n# print shape (num_branches, num_comps)\nprint(cell.shape)\n\ncell.show()\n
(157, 314)\n
local_comp_index global_comp_index local_branch_index global_branch_index local_cell_index global_cell_index 0 0 0 0 0 0 0 1 1 1 0 0 0 0 2 0 2 1 1 0 0 3 1 3 1 1 0 0 4 0 4 2 2 0 0 ... ... ... ... ... ... ... 309 1 309 154 154 0 0 310 0 310 155 155 0 0 311 1 311 155 155 0 0 312 0 312 156 156 0 0 313 1 313 156 156 0 0

314 rows \u00d7 6 columns

Once imported the compartmentalized morphology can be viewed using vis.

# visualize the cell\ncell.vis()\nplt.axis(\"off\")\nplt.title(\"L5PC\")\nplt.show()\n

vis can be called on any jx.Module and every View of the module. This means we can also for example use vis to highlight each branch. This can be done by iterating over each branch index and calling cell.branch(i).vis(). Within the loop.

fig, ax = plt.subplots(1, 1, figsize=(5, 5))\n# define colorwheel with 10 colors\ncolors = plt.cm.tab10.colors\nfor i, branch in enumerate(cell.branches):\n    branch.vis(ax=ax, col=colors[i % 10])\nplt.axis(\"off\")\nplt.title(\"Branches\")\nplt.show()\n

While we only use two compartments to approximate each branch in this example, we can see the morphology is still plotted in great detail. This is because we always plot the full .swc reconstruction irrespective of the number of compartments used. The morphology lives seperately in the cell.xyzr attribute in a per branch fashion.

In addition to plotting the full morphology of the cell using points vis(type=\"scatter\") or lines vis(type=\"line\"), Jaxley also supports plotting a detailed morphological vis(type=\"morph\") or approximate compartmental reconstruction vis(type=\"comp\") that correctly considers the thickness of the neurite. Note that \"comp\" plots the lengths of each compartment which is equal to the length of the traced neurite. While neurites can be zigzaggy, the compartments that approximate them are straight lines. This can lead to miss-aligment of the compartment ends. For details see the documentation of vis.

The morphologies can either be projected onto 2D or also rendered in 3D.

# visualize the cell\nfig, ax = plt.subplots(1, 4, figsize=(10, 3), layout=\"constrained\", sharex=True, sharey=True)\ncell.vis(ax=ax[0], type=\"morph\", dims=[0,1])\ncell.vis(ax=ax[1], type=\"comp\", dims=[0,1])\ncell.vis(ax=ax[2], type=\"scatter\", dims=[0,1], morph_plot_kwargs={\"s\": 1})\ncell.vis(ax=ax[3], type=\"line\", dims=[0,1])\nfig.suptitle(\"Comparison of plot types\")\nplt.show()\n

# set to interactive mode\n# %matplotlib notebook\n
# plot in 3D\nfig = plt.figure()\nax = fig.add_subplot(111, projection='3d')\ncell.vis(ax=ax, type=\"line\", dims=[2,0,1])\nax.view_init(elev=20, azim=5)\nplt.show()\n

Since Jaxley supports grouping different branches or compartments together, we can also use the id labels provided by the .swc file to assign group labels to the jx.Cell object.

print(list(cell.groups.keys()))\n\nfig, ax = plt.subplots(1, 1, figsize=(5, 5))\ncolors = plt.cm.tab10.colors\ncell.basal.vis(ax=ax, col=colors[2])\ncell.soma.vis(ax=ax, col=colors[1])\ncell.apical.vis(ax=ax, col=colors[0])\nplt.axis(\"off\")\nplt.title(\"Groups\")\nplt.show()\n
['soma', 'basal', 'apical', 'custom']\n

To build a network of morphologically detailed cells, we can now connect several reconstructed cells together and also visualize the network. However, since all cells are going to have the same center, Jaxley will naively plot all of them on top of each other. To seperate out the cells, we therefore have to move them to a new location first.

net = jx.Network([cell]*5)\njx.connect(net[0,0,0], net[2,0,0], IonotropicSynapse())\njx.connect(net[0,0,0], net[3,0,0], IonotropicSynapse())\njx.connect(net[0,0,0], net[4,0,0], IonotropicSynapse())\n\njx.connect(net[1,0,0], net[2,0,0], IonotropicSynapse())\njx.connect(net[1,0,0], net[3,0,0], IonotropicSynapse())\njx.connect(net[1,0,0], net[4,0,0], IonotropicSynapse())\n\nnet.rotate(-90)\n\nnet.cell(0).move(0, 300)\nnet.cell(1).move(0, 500)\n\nnet.cell(2).move(900, 200)\nnet.cell(3).move(900, 400)\nnet.cell(4).move(900, 600)\n\nnet.vis()\nplt.axis(\"off\")\nplt.show()\n

Congrats! You have now learned how to vizualize and build networks out of very complex morphologies. To simulate this network, you can follow the steps in the tutorial on how to build a network.

"},{"location":"tutorial/09_advanced_indexing/","title":"Customizing synaptic parameters","text":"

In this tutorial, you will learn how to:

  • use the select() method to fully customize network simulations with Jaxley.
  • use the copy_node_property_to_edges() method to flexibly modify synapses.

Here is a code snippet which you will learn to understand in this tutorial:

net = ...  # See tutorial on Basics of Jaxley.\n\n# Set synaptic conductance of the synapse with index 0 and 1.\nnet.select(edges=[0, 1]).set(\"Ionotropic_gS\", 0.1)\n\n# Set synaptic conductance of all synapses that have cells 3 or 4 as presynaptic neuron.\nnet.copy_node_property_to_edges(\"global_cell_index\")\ndf = net.edges\ndf = df.query(\"pre_global_cell_index in [3, 4]\")\nnet.select(edges=df.index).set(\"Ionotropic_gS\", 0.2)\n\n# Set synaptic conductance of all synapses that\n# 1) have cells 2 or 3 as presynaptic neuron and\n# 2) has cell 5 as postsynaptic neuron\ndf = net.edges\ndf = df.query(\"pre_global_cell_index in [2, 3]\")\ndf = df.query(\"post_global_cell_index == 5\")\nnet.select(edges=df.index).set(\"Ionotropic_gS\", 0.3)\n

In a previous tutorial you learned how to set parameters of a jx.Network. In that tutorial, we briefly mentioned the select() method which allowed to set individual synapses to particular values. In this tutorial, we will go into detail in how you can fully customize your Jaxley simulation.

Let\u2019s go!

import jaxley as jx\nfrom jaxley.channels import Na, K, Leak\nfrom jaxley.connect import fully_connect\nfrom jaxley.synapses import IonotropicSynapse\n
"},{"location":"tutorial/09_advanced_indexing/#preface-building-the-network","title":"Preface: Building the network","text":"

We first build a network consisting of six neurons, in the same way as we showed in the previous tutorials:

dt = 0.025\nt_max = 10.0\n\ncomp = jx.Compartment()\nbranch = jx.Branch(comp, nseg=2)\ncell = jx.Cell(branch, parents=[-1, 0])\nnet = jx.Network([cell for _ in range(6)])\nfully_connect(net.cell([0, 1, 2]), net.cell([3, 4, 5]), IonotropicSynapse())\n
"},{"location":"tutorial/09_advanced_indexing/#setting-individual-synapse-parameters","title":"Setting individual synapse parameters","text":"

As always, you can use the .edges table to inspect synaptic parameters of the network:

net.edges\n
global_edge_index pre_global_comp_index post_global_comp_index type type_ind pre_locs post_locs IonotropicSynapse_gS IonotropicSynapse_e_syn IonotropicSynapse_k_minus IonotropicSynapse_s controlled_by_param 0 0 0 13 IonotropicSynapse 0 0.25 0.75 0.0001 0.0 0.025 0.2 0 1 1 0 19 IonotropicSynapse 0 0.25 0.75 0.0001 0.0 0.025 0.2 0 2 2 0 20 IonotropicSynapse 0 0.25 0.25 0.0001 0.0 0.025 0.2 0 3 3 4 12 IonotropicSynapse 0 0.25 0.25 0.0001 0.0 0.025 0.2 0 4 4 4 16 IonotropicSynapse 0 0.25 0.25 0.0001 0.0 0.025 0.2 0 5 5 4 21 IonotropicSynapse 0 0.25 0.75 0.0001 0.0 0.025 0.2 0 6 6 8 13 IonotropicSynapse 0 0.25 0.75 0.0001 0.0 0.025 0.2 0 7 7 8 17 IonotropicSynapse 0 0.25 0.75 0.0001 0.0 0.025 0.2 0 8 8 8 21 IonotropicSynapse 0 0.25 0.75 0.0001 0.0 0.025 0.2 0

This table has nine rows, each corresponding to one synapse. This makes sense because we fully connected three neurons (0, 1, 2) to three other neurons (3, 4, 5), giving a total of 3x3=9 synapses.

You can modify parameters of individual synapses as follows:

net.select(edges=[3, 4, 5]).set(\"IonotropicSynapse_gS\", 0.2)\n

Above, we are modifying the synapses with indices [3, 4, 5] (i.e., the indices of the net.edges DataFrame). The resulting values are indeed changed:

net.edges.IonotropicSynapse_gS\n
0    0.0001\n1    0.0001\n2    0.0001\n3    0.2000\n4    0.2000\n5    0.2000\n6    0.0001\n7    0.0001\n8    0.0001\nName: IonotropicSynapse_gS, dtype: float64\n
"},{"location":"tutorial/09_advanced_indexing/#example-1-setting-synaptic-parameters-which-connect-particular-neurons","title":"Example 1: Setting synaptic parameters which connect particular neurons","text":"

This is great, but setting synaptic parameters just by their index can be exhausting, in particular in very large networks. Instead, we would want to, for example, set the maximal conductance of all synapses that connect from cell 0 or 1 to any other neuron.

In Jaxley, such customization can be achieved by filtering the .edges dataframe accordingly, as shown below:

net = jx.Network([cell for _ in range(6)])\nfully_connect(net.cell([0, 1, 2]), net.cell([3, 4, 5]), IonotropicSynapse())\n\nnet.copy_node_property_to_edges(\"global_cell_index\")\ndf = net.edges\ndf = df.query(\"pre_global_cell_index in [0, 1]\")\nnet.select(edges=df.index).set(\"IonotropicSynapse_gS\", 0.23)\n
net.edges.IonotropicSynapse_gS\n
0    0.2300\n1    0.2300\n2    0.2300\n3    0.2300\n4    0.2300\n5    0.2300\n6    0.0001\n7    0.0001\n8    0.0001\nName: IonotropicSynapse_gS, dtype: float64\n

Indeed, the first six synapses now have the value 0.23! Let\u2019s look at the individual lines to understand how this worked:

We want to set parameter by cell index. However, by default, the pre- or post-synaptic cell-indices are not listed in net.edges. We can add the cell index to the .edges dataframe by calling .copy_node_property_to_edges():

net.copy_node_property_to_edges(\"global_cell_index\")\n

After this, the pre- and post-synaptic cell indices are listed in net.edges as pre_global_cell_index and post_global_cell_index.

Next, we take .edges, which is a pandas DataFrame:

df = net.edges\n

We then modify this DataFrame to only contain those rows where the global cell index is in 0 or 1:

df = df.query(\"pre_global_cell_index in [0, 1]\")\n

For the above step, you use any column of the DataFrame to filter it (you can see all columns with df.columns). Note that, while we used .query() here, you can really filter the pandas DataFrame however you want. For example, the query above is identical to df = df[df[\"pre_global_cell_index\"].isin([0, 1])].

Finally, we use the .select() method, which returns a subset of the Network at the specified indices. This subset of the network can be modified with .set():

net.select(edges=df.index).set(\"IonotropicSynapse_gS\", 0.23)\n

"},{"location":"tutorial/09_advanced_indexing/#example-2-setting-parameters-given-pre-and-post-synaptic-cell-indices","title":"Example 2: Setting parameters given pre- and post-synaptic cell indices","text":"

Say you want to select all synapses that have cells 1 or 2 as presynaptic neuron and cell 4 or 5 as postsynaptic neuron.

net = jx.Network([cell for _ in range(6)])\nfully_connect(net.cell([0, 1, 2]), net.cell([3, 4, 5]), IonotropicSynapse())\n

Just like before, we can simply use .query() as already shown above. However, this time, call .query() to twice to filter by pre- and post-synaptic cell indices:

net.copy_node_property_to_edges(\"global_cell_index\")\n\ndf = net.edges\ndf = df.query(\"pre_global_cell_index in [1, 2]\")\ndf = df.query(\"post_global_cell_index in [4, 5]\")\nnet.select(edges=df.index).set(\"IonotropicSynapse_gS\", 0.3)\n
net.edges.IonotropicSynapse_gS\n
0    0.0001\n1    0.0001\n2    0.0001\n3    0.0001\n4    0.3000\n5    0.3000\n6    0.0001\n7    0.3000\n8    0.3000\nName: IonotropicSynapse_gS, dtype: float64\n
"},{"location":"tutorial/09_advanced_indexing/#example-3-applying-this-strategy-to-cell-level-parameters","title":"Example 3: Applying this strategy to cell level parameters","text":"

You had previously seen that you can modify parameters with, e.g., net.cell(0).set(...). However, if you need more flexibility than this, you can also use the above strategy to modify cell-level parameters:

net = jx.Network([cell for _ in range(6)])\nfully_connect(net.cell([0, 1, 2]), net.cell([3, 4, 5]), IonotropicSynapse())\n\ndf = net.nodes\ndf = df.query(\"global_cell_index in [0, 1]\")\nnet.select(nodes=df.index).set(\"radius\", 0.1)\n
"},{"location":"tutorial/09_advanced_indexing/#example-4-flexibly-setting-parameters-based-on-their-groups","title":"Example 4: Flexibly setting parameters based on their groups","text":"

If you are using groups, as shown in this tutorial, then you can also use this for querying synapses. To demonstrate this, let\u2019s create a group of excitatory neurons (e.g., cells 0, 3, 5):

# Redefine network.\nnet = jx.Network([cell for _ in range(6)])\nfully_connect(net.cell([0, 1, 2]), net.cell([3, 4, 5]), IonotropicSynapse())\n\nnet.cell([0, 3, 5]).add_to_group(\"exc\")\n

Now, say we want all synapses that start from these excitatory neurons. You can do this as follows:

# First, we have to identify which cells are in the `exc` group.\nindices_of_excitatory_cells = net.exc.nodes[\"global_cell_index\"].unique().tolist()  # [0, 3, 5]\n\n# Then we can proceed as before:\nnet.copy_node_property_to_edges(\"global_cell_index\")\ndf = net.edges\ndf = df.query(f\"pre_global_cell_index in {indices_of_excitatory_cells}\")\nnet.select(edges=df.index).set(\"IonotropicSynapse_gS\", 0.4)\n
"},{"location":"tutorial/09_advanced_indexing/#example-5-setting-synaptic-parameters-based-on-properties-of-the-presynaptic-cell","title":"Example 5: Setting synaptic parameters based on properties of the presynaptic cell","text":"

Let\u2019s discuss one more example: Imagine we only want to modify those synapses whose presynaptic compartment has a sodium channel. Let\u2019s first add a sodium channel to some of the cells:

net = jx.Network([cell for _ in range(6)])\nfully_connect(net.cell([0, 1, 2]), net.cell([3, 4, 5]), IonotropicSynapse())\n\nnet.cell(0).branch(0).comp(0).insert(Na())\nnet.cell(2).branch(1).comp(1).insert(Na())\n

Now, let us query which cells have the desired synapses:

df = net.nodes\ndf = df.query(\"Na\")\nindices_of_sodium_compartments = df[\"global_comp_index\"].unique().tolist()\n

indices_of_sodium_compartments lists all compartments which contained sodium:

print(indices_of_sodium_compartments)\n
[0, 11]\n

Then, we can proceed as always and filter for the global pre-synaptic compartment index:

df = net.edges\ndf = df.query(f\"pre_global_comp_index in {indices_of_sodium_compartments}\")\nnet.select(edges=df.index).set(\"IonotropicSynapse_gS\", 0.6)\n
net.edges.IonotropicSynapse_gS\n
0    0.6000\n1    0.6000\n2    0.6000\n3    0.0001\n4    0.0001\n5    0.0001\n6    0.0001\n7    0.0001\n8    0.0001\nName: IonotropicSynapse_gS, dtype: float64\n

Indeed, only synapses coming from the first neuron were modified (as its presynaptic compartment contained sodium), in contrast to synapses from neuron 2 (whose presynaptic compartment did not).

"},{"location":"tutorial/09_advanced_indexing/#summary","title":"Summary","text":"

In this tutorial, you learned how to fully customize your Jaxley simulation. This works by querying rows from the .edges DataFrame.

"},{"location":"tutorial/10_advanced_parameter_sharing/","title":"Synaptic parameter sharing","text":"

In this tutorial, you will learn how to:

  • flexibly share parameters of synapses

Here is a code snippet which you will learn to understand in this tutorial:

net = ...  # See tutorial on Basics of Jaxley.\n\n# The same parameter for all synapses\nnet.make_trainable(\"Ionotropic_gS\")\n\n# An individual parameter for every synapse.\nnet.select(edges=\"all\").make_trainable(\"Ionotropic_gS\")\n\n# Share synaptic conductances emerging from the same neurons.\nnet.copy_node_property_to_edges(\"cell_index\")\nsub_net = net.select(edges=[0, 1, 2])\nsub_net.edges[\"controlled_by_param\"] = sub_net.edges[\"pre_global_cell_index\"]\nsub_net.make_trainable(\"Ionotropic_gS\")\n

In a previous tutorial about training networks, we briefly touched on parameter sharing. In this tutorial, we will show you how you can flexibly share parameters within a network.

import jaxley as jx\nfrom jaxley.channels import Na, K, Leak\nfrom jaxley.connect import fully_connect\nfrom jaxley.synapses import IonotropicSynapse\n
"},{"location":"tutorial/10_advanced_parameter_sharing/#preface-building-the-network","title":"Preface: Building the network","text":"

We first build a network consisting of six neurons, in the same way as we showed in the previous tutorials:

dt = 0.025\nt_max = 10.0\n\ncomp = jx.Compartment()\nbranch = jx.Branch(comp, nseg=2)\ncell = jx.Cell(branch, parents=[-1, 0])\nnet = jx.Network([cell for _ in range(6)])\nfully_connect(net.cell([0, 1, 2]), net.cell([3, 4, 5]), IonotropicSynapse())\n
"},{"location":"tutorial/10_advanced_parameter_sharing/#sharing-parameters-by-modifying-controlled_by_param","title":"Sharing parameters by modifying controlled_by_param","text":"
net.copy_node_property_to_edges(\"global_cell_index\")\n\ndf = net.edges\ndf = df.query(\"pre_global_cell_index in [0, 1, 2]\")\nsubnetwork = net.select(edges=df.index)\n\ndf = subnetwork.edges\ndf[\"controlled_by_param\"] = df[\"pre_global_cell_index\"]\nsubnetwork.make_trainable(\"IonotropicSynapse_gS\")\n
Number of newly added trainable parameters: 3. Total number of trainable parameters: 3\n

Let\u2019s look at this line by line. First, we exactly follow the previous tutorial in selecting the synapses which we are interested in training (i.e., the ones whose presynaptic neuron has index 0, 1, 2):

df = net.edges\ndf = df.query(\"pre_global_cell_index in [0, 1, 2]\")\nsubnetwork = net.select(edges=df.index)\n

As second step, we enable parameter sharing. This is done by setting the controlled_by_param. Synapses that have the same value in controlled_by_param will be shared. Let\u2019s inspect controlled_by_param before we modify it:

subnetwork.edges[[\"pre_global_cell_index\", \"controlled_by_param\"]]\n
pre_global_cell_index controlled_by_param 0 0 0 1 0 1 2 0 2 3 1 3 4 1 4 5 1 5 6 2 6 7 2 7 8 2 8

Every synapse has a different value. Because of this, no synaptic parameters will be shared. To enable parameter sharing we override the controlled_by_param column with the presynaptic cell index:

df = subnetwork.edges\ndf[\"controlled_by_param\"] = df[\"pre_global_cell_index\"]\n
df[[\"pre_global_cell_index\", \"controlled_by_param\"]]\n
pre_global_cell_index controlled_by_param 0 0 0 1 0 0 2 0 0 3 1 1 4 1 1 5 1 1 6 2 2 7 2 2 8 2 2

Now, all we have to do is to make these synaptic parameters trainable with the make_trainable() method:

subnetwork.make_trainable(\"IonotropicSynapse_gS\")\n
Number of newly added trainable parameters: 3. Total number of trainable parameters: 6\n

It correctly says that we added three parameters (because we have three cells, and we share individual synaptic parameters). We now have 6 trainable parameters in total (because we already added 3 trainable parameters above).

"},{"location":"tutorial/10_advanced_parameter_sharing/#a-more-involved-example-sharing-by-pre-and-post-synaptic-cell-type","title":"A more involved example: sharing by pre- and post-synaptic cell type","text":"

As an example, consider the following: We have a fully connected network of six cells. Each cell falls into one of three cell types:

from typing import Union, List\n
net = jx.Network([cell for _ in range(6)])\nfully_connect(net.cell(\"all\"), net.cell(\"all\"), IonotropicSynapse())\n\nnet.cell([0, 1]).add_to_group(\"exc\")\nnet.cell([2, 3]).add_to_group(\"inh\")\nnet.cell([4, 5]).add_to_group(\"unknown\")\n

We want to make all synapses that start from excitatory or inhibitory neurons trainable. In addition, we want to use the same parameter for synapses if they have the same pre- and post-synaptic cell type.

To achieve this, we will first want a column in net.nodes which indicates the cell type.

for group, inds in net.groups.items():\n    net.nodes.loc[inds, \"cell_type\"] = group\n
net.nodes[\"cell_type\"]\n
0         exc\n1         exc\n2         exc\n3         exc\n4         exc\n5         exc\n6         exc\n7         exc\n8         inh\n9         inh\n10        inh\n11        inh\n12        inh\n13        inh\n14        inh\n15        inh\n16    unknown\n17    unknown\n18    unknown\n19    unknown\n20    unknown\n21    unknown\n22    unknown\n23    unknown\nName: cell_type, dtype: object\n

The cell_type is now part of the net.nodes. However, we would like to do parameter sharing of synapses based on the pre- and post-synaptic node values. To do so, we import the cell_type column into net.edges. To do this, we use the .copy_node_property_to_edges() which the name of the property you are copying from nodes:

net.copy_node_property_to_edges(\"cell_type\")\n

After this, you have columns in the .edges which indicate the pre- and post-synaptic cell type:

net.edges[[\"pre_cell_type\", \"post_cell_type\"]]\n
pre_cell_type post_cell_type 0 exc exc 1 exc exc 2 exc inh 3 exc inh 4 exc unknown 5 exc unknown 6 exc exc 7 exc exc 8 exc inh 9 exc inh 10 exc unknown 11 exc unknown 12 inh exc 13 inh exc 14 inh inh 15 inh inh 16 inh unknown 17 inh unknown 18 inh exc 19 inh exc 20 inh inh 21 inh inh 22 inh unknown 23 inh unknown 24 unknown exc 25 unknown exc 26 unknown inh 27 unknown inh 28 unknown unknown 29 unknown unknown 30 unknown exc 31 unknown exc 32 unknown inh 33 unknown inh 34 unknown unknown 35 unknown unknown

Next, we specify which parts of the network we actually want to change (in this case, all synapses which have excitatory or inhibitory presynaptic neurons):

df = net.edges\ndf = df.query(f\"pre_cell_type in ['exc', 'inh']\")\nprint(f\"There are {len(df)} synapses to be changed.\")\n\nsubnetwork = net.select(edges=df.index)\n
There are 24 synapses to be changed.\n

As the last step, we again have to specify parameter sharing by setting controlled_by_param. In this case, we want to share parameters that have the same pre- and post-synaptic neuron. We achieve this by grouping the synpases by their pre- and post-synaptic cell type (see pd.DataFrame.groupby for details):

# Step 6: use groupby to specify parameter sharing and make the parameters trainable.\nsubnetwork.edges[\"controlled_by_param\"] = subnetwork.edges.groupby([\"pre_cell_type\", \"post_cell_type\"]).ngroup()\nsubnetwork.make_trainable(\"IonotropicSynapse_gS\")\n
Number of newly added trainable parameters: 6. Total number of trainable parameters: 6\n

This created six trainable parameters, which makes sense as we have two types of pre-synaptic neurons (excitatory and inhibitory) and each has three options for the postsynaptic neuron (pre, post, unknown).

"},{"location":"tutorial/10_advanced_parameter_sharing/#summary","title":"Summary","text":"

In this tutorial, you learned how you can flexibly share synaptic parameters. This works by first using select() to identify which synapses to make trainable, and by then modifying controlled_by_param to customize parameter sharing.

"}]} \ No newline at end of file +{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Home","text":"

Jaxley is a differentiable simulator for biophysical neuron models in JAX. Its key features are:

  • automatic differentiation, allowing gradient-based optimization of thousands of parameters
  • support for CPU, GPU, or TPU without any changes to the code
  • jit-compilation, making it as fast as other packages while being fully written in python
  • backward-Euler solver for stable numerical solution of multicompartment neurons
  • elegant mechanisms for parameter sharing
"},{"location":"#getting-started","title":"Getting started","text":"

Jaxley allows to simulate biophysical neuron models on CPU, GPU, or TPU:

import matplotlib.pyplot as plt\nfrom jax import config\n\nimport jaxley as jx\nfrom jaxley.channels import HH\n\nconfig.update(\"jax_platform_name\", \"cpu\")  # Or \"gpu\" / \"tpu\".\n\ncell = jx.Cell()  # Define cell.\ncell.insert(HH())  # Insert channels.\n\ncurrent = jx.step_current(i_delay=1.0, i_dur=1.0, i_amp=0.1, delta_t=0.025, t_max=10.0)\ncell.stimulate(current)  # Stimulate with step current.\ncell.record(\"v\")  # Record voltage.\n\nv = jx.integrate(cell)  # Run simulation.\nplt.plot(v.T)  # Plot voltage trace.\n

If you want to learn more, we have tutorials on how to:

  • simulate morphologically detailed neurons
  • simulate networks of such neurons
  • set parameters of cells and networks
  • speed up simulations with GPUs and jit
  • define your own channels and synapses
  • define groups
  • read and handle SWC files
  • compute the gradient and train biophysical models
"},{"location":"#installation","title":"Installation","text":"

Jaxley is available on pypi:

pip install jaxley\n
This will install Jaxley with CPU support. If you want GPU support, follow the instructions on the JAX github repository to install JAX with GPU support (in addition to installing Jaxley). For example, for NVIDIA GPUs, run
pip install -U \"jax[cuda12]\"\n

"},{"location":"#feedback-and-contributions","title":"Feedback and Contributions","text":"

We welcome any feedback on how Jaxley is working for your neuron models and are happy to receive bug reports, pull requests and other feedback (see contribute). We wish to maintain a positive community, please read our Code of Conduct.

"},{"location":"#license","title":"License","text":"

Apache License Version 2.0 (Apache-2.0)

"},{"location":"#citation","title":"Citation","text":"

If you use Jaxley, consider citing the corresponding paper:

@article{deistler2024differentiable,\n  doi = {10.1101/2024.08.21.608979},\n  year = {2024},\n  publisher = {Cold Spring Harbor Laboratory},\n  author = {Deistler, Michael and Kadhim, Kyra L. and Pals, Matthijs and Beck, Jonas and Huang, Ziwei and Gloeckler, Manuel and Lappalainen, Janne K. and Schr{\\\"o}der, Cornelius and Berens, Philipp and Gon{\\c c}alves, Pedro J. and Macke, Jakob H.},\n  title = {Differentiable simulation enables large-scale training of detailed biophysical models of neural dynamics},\n  journal = {bioRxiv}\n}\n
"},{"location":"code_of_conduct/","title":"Contributor Covenant Code of Conduct","text":""},{"location":"code_of_conduct/#our-pledge","title":"Our Pledge","text":"

We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation.

We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.

"},{"location":"code_of_conduct/#our-standards","title":"Our Standards","text":"

Examples of behavior that contributes to a positive environment for our community include:

  • Demonstrating empathy and kindness toward other people
  • Being respectful of differing opinions, viewpoints, and experiences
  • Giving and gracefully accepting constructive feedback
  • Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
  • Focusing on what is best not just for us as individuals, but for the overall community

Examples of unacceptable behavior include:

  • The use of sexualized language or imagery, and sexual attention or advances of any kind
  • Trolling, insulting or derogatory comments, and personal or political attacks
  • Public or private harassment
  • Publishing others\u2019 private information, such as a physical or email address, without their explicit permission
  • Other conduct which could reasonably be considered inappropriate in a professional setting
"},{"location":"code_of_conduct/#enforcement-responsibilities","title":"Enforcement Responsibilities","text":"

Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.

Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.

"},{"location":"code_of_conduct/#scope","title":"Scope","text":"

This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.

"},{"location":"code_of_conduct/#enforcement","title":"Enforcement","text":"

Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting jaxley developer Michael Deistler via email (michael.deistler@uni-tuebingen.de). All complaints will be reviewed and investigated promptly and fairly.

All community leaders are obligated to respect the privacy and security of the reporter of any incident.

"},{"location":"code_of_conduct/#enforcement-guidelines","title":"Enforcement Guidelines","text":"

Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:

"},{"location":"code_of_conduct/#1-correction","title":"1. Correction","text":"

Community Impact: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.

Consequence: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.

"},{"location":"code_of_conduct/#2-warning","title":"2. Warning","text":"

Community Impact: A violation through a single incident or series of actions.

Consequence: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.

"},{"location":"code_of_conduct/#3-temporary-ban","title":"3. Temporary Ban","text":"

Community Impact: A serious violation of community standards, including sustained inappropriate behavior.

Consequence: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.

"},{"location":"code_of_conduct/#4-permanent-ban","title":"4. Permanent Ban","text":"

Community Impact: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.

Consequence: A permanent ban from any sort of public interaction within the community.

"},{"location":"code_of_conduct/#attribution","title":"Attribution","text":"

This Code of Conduct is adapted from the Contributor Covenant, version 2.1, available at https://www.contributor-covenant.org/version/2/1/code_of_conduct.html.

Community Impact Guidelines were inspired by Mozilla\u2019s code of conduct enforcement ladder.

For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.

"},{"location":"contribute/","title":"Guide","text":""},{"location":"contribute/#user-experiences-bugs-and-feature-requests","title":"User experiences, bugs, and feature requests","text":"

To report bugs and suggest features (including better documentation), please head over to issues on GitHub.

"},{"location":"contribute/#code-contributions","title":"Code contributions","text":"

In general, we use pull requests to make changes to Jaxley. So, if you are planning to make a contribution, please fork, create a feature branch and then make a PR from your feature branch to the upstream Jaxley (details).

"},{"location":"contribute/#development-environment","title":"Development environment","text":"

Clone the repo and install via setup.py using pip install -e \".[dev]\" (the dev flag installs development and testing dependencies).

"},{"location":"contribute/#style-conventions","title":"Style conventions","text":"

For docstrings and comments, we use Google Style.

Code needs to pass through the following tools, which are installed alongside Jaxley:

black: Automatic code formatting for Python. You can run black manually from the console using black . in the top directory of the repository, which will format all files.

isort: Used to consistently order imports. You can run isort manually from the console using isort in the top directory.

black and isort are checked as part of our CI actions. If these checks fail please make sure you have installed the latest versions for each of them and run them locally.

"},{"location":"contribute/#online-documentation","title":"Online documentation","text":"

Most of the documentation is written in markdown (basic markdown guide).

You can directly fix mistakes and suggest clearer formulations in markdown files simply by initiating a PR on through GitHub. Click on documentation file and look for the little pencil at top right.

"},{"location":"credits/","title":"Credits","text":"

Jaxley is a collaborative project between the groups of Jakob Macke (Uni T\u00fcbingen), Pedro Gon\u00e7alves (KU Leuven / NERF), and Philipp Berens (Uni T\u00fcbingen).

"},{"location":"credits/#license","title":"License","text":"

Jaxley is licensed under the Apache License Version 2.0 (Apache-2.0) and

Copyright (C) 2024 Michael Deistler, Jakob H. Macke, Pedro J. Goncalves, Philipp Berens.

"},{"location":"credits/#important-dependencies-and-prior-art","title":"Important dependencies and prior art","text":"
  • We greatly benefited from previous toolboxes for simulating multicompartment neurons, in particular NEURON.
"},{"location":"credits/#funding","title":"Funding","text":"

This work was supported by the German Research Foundation (DFG) through Germany\u2019s Excellence Strategy (EXC 2064 \u2013 Project number 390727645) and the CRC 1233 \u201cRobust Vision\u201d, the German Federal Ministry of Education and Research (Tu\u0308bingen AI Center, FKZ: 01IS18039A), the \u2018Certification and Foundations of Safe Machine Learning Systems in Healthcare\u2019 project funded by the Carl Zeiss Foundation, and the European Union (ERC, \u201cDeepCoMechTome\u201d, ref. 101089288, \u201cNextMechMod\u201d, ref. 101039115).

"},{"location":"faq/","title":"Frequently asked questions","text":"
  • What kinds of models can be implemented in Jaxley?
  • What units does Jaxley use?
  • How can I save and load cells and networks?

See also the discussion page and the issue tracker on the Jaxley GitHub repository for recent questions and problems.

"},{"location":"install/","title":"Installation","text":""},{"location":"install/#install-the-most-recent-stable-version","title":"Install the most recent stable version","text":"

Jaxley is available on PyPI:

pip install jaxley\n
This will install Jaxley with CPU support. If you want GPU support, follow the instructions on the JAX github repository to install JAX with GPU support (in addition to installing Jaxley). For example, for NVIDIA GPUs, run
pip install -U \"jax[cuda12]\"\n

"},{"location":"install/#install-from-source","title":"Install from source","text":"

You can also install Jaxley from source:

git clone https://github.com/jaxleyverse/jaxley.git\ncd jaxley\npip install -e .\n

Note that pip>=21.3 is required to install the editable version with pyproject.toml see pip docs.

"},{"location":"faq/question_01/","title":"What units does Jaxley use?","text":"

Jaxley uses the same units as the NEURON simulator, which are listed here.

"},{"location":"faq/question_02/","title":"How can I save and load cells and networks?","text":"

All modules (i.e., compartments, branches, cells, and networks) in Jaxley can be saved and loaded with pickle:

import jaxley as jx\nimport pickle\n\n# ... define network, cell, etc.\nnetwork = jx.Network([cell1, cell2])\n\n# Save.\nwith open(\"path/to/file.pkl\", \"wb\") as handle:\n    pickle.dump(network, handle)\n\n# Load.\nwith open(\"path/to/file.pkl\", \"rb\") as handle:\n    network = pickle.load(handle)\n

"},{"location":"faq/question_03/","title":"What kinds of models can be implemented in Jaxley?","text":"

Jaxley focuses on biophysical, Hodgkin-Huxley-type models. You can think of Jaxley like the NEURON simulator written in JAX.

Jaxley allows to simulate the following types of models, as well as networks thereof:

  • single-compartment (point neuron) Hodgkin-Huxley models
  • multi-compartment Hodgkin-Huxley models
  • rate-based neuron models

For all of these models, Jaxley is flexible and accurate. For example, it can flexibly add new channel models, use different kinds of synapses (conductance-based, tanh, \u2026), and it can insert different kinds of channels in different branches (or compartments) within single cells. Like NEURON, Jaxley implements a backward-Euler solver for stable numerical solution of multi-compartment neurons.

However, Jaxley does not implement the following types of models:

  • leaky-integrate and fire neurons
  • Ishikevich neuron models
  • etc\u2026
"},{"location":"reference/connect/","title":"Connecting Cells","text":""},{"location":"reference/connect/#jaxley.connect.connect","title":"connect(pre, post, synapse_type)","text":"

Connect two compartments with a chemical synapse.

The pre- and postsynaptic compartments must be different compartments of the same network.

Parameters:

Name Type Description Default pre View

View of the presynaptic compartment.

required post View

View of the postsynaptic compartment.

required synapse_type Synapse

The synapse to append

required Source code in jaxley/connect.py
def connect(\n    pre: \"View\",\n    post: \"View\",\n    synapse_type: \"Synapse\",\n):\n    \"\"\"Connect two compartments with a chemical synapse.\n\n    The pre- and postsynaptic compartments must be different compartments of the\n    same network.\n\n    Args:\n        pre: View of the presynaptic compartment.\n        post: View of the postsynaptic compartment.\n        synapse_type: The synapse to append\n    \"\"\"\n    assert is_same_network(\n        pre, post\n    ), \"Pre and post compartments must be part of the same network.\"\n\n    pre.base._append_multiple_synapses(pre.nodes, post.nodes, synapse_type)\n
"},{"location":"reference/connect/#jaxley.connect.connectivity_matrix_connect","title":"connectivity_matrix_connect(pre_cell_view, post_cell_view, synapse_type, connectivity_matrix)","text":"

Appends multiple connections which build a custom connected network.

Connects pre- and postsynaptic cells according to a custom connectivity matrix. Entries > 0 in the matrix indicate a connection between the corresponding cells. Connections are from branch 0 location 0 to a randomly chosen branch and loc.

Parameters:

Name Type Description Default pre_cell_view View

View of the presynaptic cell.

required post_cell_view View

View of the postsynaptic cell.

required synapse_type Synapse

The synapse to append.

required connectivity_matrix ndarray[bool]

A boolean matrix indicating the connections between cells.

required Source code in jaxley/connect.py
def connectivity_matrix_connect(\n    pre_cell_view: \"View\",\n    post_cell_view: \"View\",\n    synapse_type: \"Synapse\",\n    connectivity_matrix: np.ndarray[bool],\n):\n    \"\"\"Appends multiple connections which build a custom connected network.\n\n    Connects pre- and postsynaptic cells according to a custom connectivity matrix.\n    Entries > 0 in the matrix indicate a connection between the corresponding cells.\n    Connections are from branch 0 location 0 to a randomly chosen branch and loc.\n\n    Args:\n        pre_cell_view: View of the presynaptic cell.\n        post_cell_view: View of the postsynaptic cell.\n        synapse_type: The synapse to append.\n        connectivity_matrix: A boolean matrix indicating the connections between cells.\n    \"\"\"\n    # Get pre- and postsynaptic cell indices.\n    pre_cell_inds = pre_cell_view._cells_in_view\n    post_cell_inds = post_cell_view._cells_in_view\n    # setting scope ensure that this works indep of current scope\n    pre_nodes = pre_cell_view.scope(\"local\").branch(0).comp(0).nodes\n    pre_nodes[\"index\"] = pre_nodes.index\n    pre_cell_nodes = pre_nodes.set_index(\"global_cell_index\")\n\n    assert connectivity_matrix.shape == (\n        len(pre_cell_inds),\n        len(post_cell_inds),\n    ), \"Connectivity matrix must have shape (num_pre, num_post).\"\n    assert connectivity_matrix.dtype == bool, \"Connectivity matrix must be boolean.\"\n\n    # get connection pairs from connectivity matrix\n    from_idx, to_idx = np.where(connectivity_matrix)\n    pre_cell_inds = pre_cell_inds[from_idx]\n    post_cell_inds = post_cell_inds[to_idx]\n\n    # Sample random postsynaptic compartments (global comp indices).\n    global_post_indices = np.hstack(\n        [\n            sample_comp(post_cell_view.scope(\"global\").cell(cell_idx))\n            for cell_idx in post_cell_inds\n        ]\n    )\n    post_rows = post_cell_view.nodes.loc[global_post_indices]\n\n    # Pre-synapse is at the zero-eth branch and zero-eth compartment.\n    global_pre_indices = pre_cell_nodes.loc[pre_cell_inds, \"index\"].to_numpy()\n    pre_rows = pre_cell_view.select(nodes=global_pre_indices).nodes\n\n    pre_cell_view.base._append_multiple_synapses(pre_rows, post_rows, synapse_type)\n
"},{"location":"reference/connect/#jaxley.connect.fully_connect","title":"fully_connect(pre_cell_view, post_cell_view, synapse_type)","text":"

Appends multiple connections which build a fully connected layer.

Connections are from branch 0 location 0 to a randomly chosen branch and loc.

Parameters:

Name Type Description Default pre_cell_view View

View of the presynaptic cell.

required post_cell_view View

View of the postsynaptic cell.

required synapse_type Synapse

The synapse to append.

required Source code in jaxley/connect.py
def fully_connect(\n    pre_cell_view: \"View\",\n    post_cell_view: \"View\",\n    synapse_type: \"Synapse\",\n):\n    \"\"\"Appends multiple connections which build a fully connected layer.\n\n    Connections are from branch 0 location 0 to a randomly chosen branch and loc.\n\n    Args:\n        pre_cell_view: View of the presynaptic cell.\n        post_cell_view: View of the postsynaptic cell.\n        synapse_type: The synapse to append.\n    \"\"\"\n    # Get pre- and postsynaptic cell indices.\n    num_pre = len(pre_cell_view._cells_in_view)\n    num_post = len(post_cell_view._cells_in_view)\n\n    # Infer indices of (random) postsynaptic compartments.\n    global_post_indices = (\n        post_cell_view.nodes.groupby(\"global_cell_index\")\n        .sample(num_pre, replace=True)\n        .index.to_numpy()\n    )\n    global_post_indices = global_post_indices.reshape((-1, num_pre), order=\"F\").ravel()\n    post_rows = post_cell_view.nodes.loc[global_post_indices]\n\n    # Pre-synapse is at the zero-eth branch and zero-eth compartment.\n    pre_rows = pre_cell_view.scope(\"local\").branch(0).comp(0).nodes.copy()\n    # Repeat rows `num_post` times. See SO 50788508.\n    pre_rows = pre_rows.loc[pre_rows.index.repeat(num_post)].reset_index(drop=True)\n\n    pre_cell_view.base._append_multiple_synapses(pre_rows, post_rows, synapse_type)\n
"},{"location":"reference/connect/#jaxley.connect.is_same_network","title":"is_same_network(pre, post)","text":"

Check if views are from the same network.

Source code in jaxley/connect.py
def is_same_network(pre: \"View\", post: \"View\") -> bool:\n    \"\"\"Check if views are from the same network.\"\"\"\n    is_in_net = \"network\" in pre.base.__class__.__name__.lower()\n    is_in_same_net = pre.base is post.base\n    return is_in_net and is_in_same_net\n
"},{"location":"reference/connect/#jaxley.connect.sample_comp","title":"sample_comp(cell_view, num=1, replace=True)","text":"

Sample a compartment from a cell.

Returns View with shape (num, num_cols).

Source code in jaxley/connect.py
def sample_comp(cell_view: \"View\", num: int = 1, replace=True) -> \"CompartmentView\":\n    \"\"\"Sample a compartment from a cell.\n\n    Returns View with shape (num, num_cols).\"\"\"\n    return np.random.choice(cell_view._comps_in_view, num, replace=replace)\n
"},{"location":"reference/connect/#jaxley.connect.sparse_connect","title":"sparse_connect(pre_cell_view, post_cell_view, synapse_type, p)","text":"

Appends multiple connections which build a sparse, randomly connected layer.

Connections are from branch 0 location 0 to a randomly chosen branch and loc.

Parameters:

Name Type Description Default pre_cell_view View

View of the presynaptic cell.

required post_cell_view View

View of the postsynaptic cell.

required synapse_type Synapse

The synapse to append.

required p float

Probability of connection.

required Source code in jaxley/connect.py
def sparse_connect(\n    pre_cell_view: \"View\",\n    post_cell_view: \"View\",\n    synapse_type: \"Synapse\",\n    p: float,\n):\n    \"\"\"Appends multiple connections which build a sparse, randomly connected layer.\n\n    Connections are from branch 0 location 0 to a randomly chosen branch and loc.\n\n    Args:\n        pre_cell_view: View of the presynaptic cell.\n        post_cell_view: View of the postsynaptic cell.\n        synapse_type: The synapse to append.\n        p: Probability of connection.\n    \"\"\"\n    # Get pre- and postsynaptic cell indices.\n    pre_cell_inds = pre_cell_view._cells_in_view\n    post_cell_inds = post_cell_view._cells_in_view\n    num_pre = len(pre_cell_inds)\n    num_post = len(post_cell_inds)\n\n    num_connections = np.random.binomial(num_pre * num_post, p)\n    pre_syn_neurons = np.random.choice(pre_cell_inds, size=num_connections)\n    post_syn_neurons = np.random.choice(post_cell_inds, size=num_connections)\n\n    # Sort the synapses only for convenience of inspecting `.edges`.\n    sorting = np.argsort(pre_syn_neurons)\n    pre_syn_neurons = pre_syn_neurons[sorting]\n    post_syn_neurons = post_syn_neurons[sorting]\n\n    # Post-synapse is a randomly chosen branch and compartment.\n    global_post_indices = [\n        sample_comp(post_cell_view.scope(\"global\").cell(cell_idx))\n        for cell_idx in post_syn_neurons\n    ]\n    global_post_indices = (\n        np.hstack(global_post_indices) if len(global_post_indices) > 1 else []\n    )\n    post_rows = post_cell_view.base.nodes.loc[global_post_indices]\n\n    # Pre-synapse is at the zero-eth branch and zero-eth compartment.\n    global_pre_indices = pre_cell_view.base._cumsum_ncomp_per_cell[pre_syn_neurons]\n    pre_rows = pre_cell_view.base.nodes.loc[global_pre_indices]\n\n    if len(pre_rows) > 0:\n        pre_cell_view.base._append_multiple_synapses(pre_rows, post_rows, synapse_type)\n
"},{"location":"reference/integration/","title":"Simulation","text":""},{"location":"reference/integration/#jaxley.integrate.add_clamps","title":"add_clamps(externals, external_inds, data_clamps=None)","text":"

Adds clamps to the external inputs.

Parameters:

Name Type Description Default externals Dict

Current external inputs.

required external_inds Dict

Current external indices.

required data_clamps Optional[Tuple[str, ndarray, DataFrame]]

Additional data clamps. Defaults to None.

None

Returns:

Type Description Tuple[Dict, Dict]

Tuple[Dict, Dict]: Updated external inputs and indices.

Source code in jaxley/integrate.py
def add_clamps(\n    externals: Dict,\n    external_inds: Dict,\n    data_clamps: Optional[Tuple[str, jnp.ndarray, pd.DataFrame]] = None,\n) -> Tuple[Dict, Dict]:\n    \"\"\"Adds clamps to the external inputs.\n\n    Args:\n        externals (Dict): Current external inputs.\n        external_inds (Dict): Current external indices.\n        data_clamps (Optional[Tuple[str, jnp.ndarray, pd.DataFrame]], optional): Additional data clamps. Defaults to None.\n\n    Returns:\n        Tuple[Dict, Dict]: Updated external inputs and indices.\n    \"\"\"\n    # If a clamp is inserted, add it to the external inputs.\n    if data_clamps is not None:\n        state_name, clamps, inds = data_clamps\n        if state_name in externals.keys():\n            externals[state_name] = jnp.concatenate([externals[state_name], clamps])\n            external_inds[state_name] = jnp.concatenate(\n                [external_inds[state_name], inds.index.to_numpy()]\n            )\n        else:\n            externals[state_name] = clamps\n            external_inds[state_name] = inds.index.to_numpy()\n\n    return externals, external_inds\n
"},{"location":"reference/integration/#jaxley.integrate.add_stimuli","title":"add_stimuli(externals, external_inds, data_stimuli=None)","text":"

Extends the external inputs with the stimuli.

Parameters:

Name Type Description Default externals Dict

Current external inputs.

required external_inds Dict

Current external indices.

required data_stimuli Optional[Tuple[ndarray, DataFrame]]

Additional data stimuli. Defaults to None.

None

Returns:

Type Description Tuple[Dict, Dict]

Tuple[Dict, Dict]: Updated external inputs and indices.

Source code in jaxley/integrate.py
def add_stimuli(\n    externals: Dict,\n    external_inds: Dict,\n    data_stimuli: Optional[Tuple[jnp.ndarray, pd.DataFrame]] = None,\n) -> Tuple[Dict, Dict]:\n    \"\"\"Extends the external inputs with the stimuli.\n\n    Args:\n        externals (Dict): Current external inputs.\n        external_inds (Dict): Current external indices.\n        data_stimuli (Optional[Tuple[jnp.ndarray, pd.DataFrame]], optional): Additional data stimuli. Defaults to None.\n\n    Returns:\n        Tuple[Dict, Dict]: Updated external inputs and indices.\n    \"\"\"\n    # If stimulus is inserted, add it to the external inputs.\n    if \"i\" in externals.keys() or data_stimuli is not None:\n        if \"i\" in externals.keys():\n            if data_stimuli is not None:\n                externals[\"i\"] = jnp.concatenate([externals[\"i\"], data_stimuli[1]])\n                external_inds[\"i\"] = jnp.concatenate(\n                    [external_inds[\"i\"], data_stimuli[2].index.to_numpy()]\n                )\n        else:\n            externals[\"i\"] = data_stimuli[1]\n            external_inds[\"i\"] = data_stimuli[2].index.to_numpy()\n\n    return externals, external_inds\n
"},{"location":"reference/integration/#jaxley.integrate.build_init_and_step_fn","title":"build_init_and_step_fn(module, voltage_solver='jaxley.stone', solver='bwd_euler')","text":"

This function returns the init_fn and step_fn which initialize the parameters and states of the neuron model and then step through the model

Parameters:

Name Type Description Default module Module

A Module object that e.g. a cell.

required voltage_solver str

Voltage solver used in step. Defaults to \u201cjaxley.stone\u201d.

'jaxley.stone' solver str

ODE solver. Defaults to \u201cbwd_euler\u201d.

'bwd_euler'

Returns:

Type Description Tuple[Callable, Callable]

init_fn, step_fn: Functions that initialize the state and parameters, and perform a single integration step, respectively.

Source code in jaxley/integrate.py
def build_init_and_step_fn(\n    module: Module,\n    voltage_solver: str = \"jaxley.stone\",\n    solver: str = \"bwd_euler\",\n) -> Tuple[Callable, Callable]:\n    \"\"\"This function returns the `init_fn` and `step_fn` which initialize the\n    parameters and states of the neuron model and then step through the model\n\n    Args:\n        module (Module): A `Module` object that e.g. a cell.\n        voltage_solver (str, optional): Voltage solver used in step. Defaults to \"jaxley.stone\".\n        solver (str, optional): ODE solver. Defaults to \"bwd_euler\".\n\n    Returns:\n        init_fn, step_fn: Functions that initialize the state and parameters, and perform\n            a single integration step, respectively.\n    \"\"\"\n    # Initialize the external inputs and their indices.\n    external_inds = module.external_inds.copy()\n\n    def init_fn(\n        params: List[Dict[str, jnp.ndarray]],\n        all_states: Optional[Dict] = None,\n        param_state: Optional[List[Dict]] = None,\n        delta_t: float = 0.025,\n    ) -> Tuple[Dict, Dict]:\n        \"\"\"Initializes the parameters and states of the neuron model.\n\n        Args:\n            params (List[Dict[str, jnp.ndarray]]): List of trainable parameters.\n            all_states (Optional[Dict], optional): State if alread initialized. Defaults to None.\n            param_state (Optional[List[Dict]], optional): Parameters returned by `data_set`.. Defaults to None.\n            delta_t (float, optional): Step size. Defaults to 0.025.\n\n        Returns:\n            Tuple[Dict, Dict]: All states and parameters.\n        \"\"\"\n        # Make the `trainable_params` of the same shape as the `param_state`, such that\n        # they can be processed together by `get_all_parameters`.\n        pstate = params_to_pstate(params, module.indices_set_by_trainables)\n        if param_state is not None:\n            pstate += param_state\n\n        all_params = module.get_all_parameters(pstate, voltage_solver=voltage_solver)\n        all_states = (\n            module.get_all_states(pstate, all_params, delta_t)\n            if all_states is None\n            else all_states\n        )\n        return all_states, all_params\n\n    def step_fn(\n        all_states: Dict,\n        all_params: Dict,\n        externals: Dict,\n        external_inds: Dict = external_inds,\n        delta_t: float = 0.025,\n    ) -> Dict:\n        \"\"\"Performs a single integration step with step size delta_t.\n\n        Args:\n            all_states (Dict): Current state of the neuron model.\n            all_params (Dict): Current parameters of the neuron model.\n            externals (Dict): External inputs.\n            external_inds (Dict, optional): External indices. Defaults to `module.external_inds`.\n            delta_t (float, optional): Time step. Defaults to 0.025.\n\n        Returns:\n            Dict: Updated states.\n        \"\"\"\n        state = all_states\n        state = module.step(\n            state,\n            delta_t,\n            external_inds,\n            externals,\n            params=all_params,\n            solver=solver,\n            voltage_solver=voltage_solver,\n        )\n        return state\n\n    return init_fn, step_fn\n
"},{"location":"reference/integration/#jaxley.integrate.integrate","title":"integrate(module, params=[], *, param_state=None, data_stimuli=None, data_clamps=None, t_max=None, delta_t=0.025, solver='bwd_euler', voltage_solver='jaxley.stone', checkpoint_lengths=None, all_states=None, return_states=False)","text":"

Solves ODE and simulates neuron model.

Parameters:

Name Type Description Default params List[Dict[str, ndarray]]

Trainable parameters returned by get_parameters().

[] param_state Optional[List[Dict]]

Parameters returned by data_set.

None data_stimuli Optional[Tuple[ndarray, DataFrame]]

Outputs of .data_stimulate(), only needed if stimuli change across function calls.

None data_clamps Optional[Tuple[str, ndarray, DataFrame]]

Outputs of .data_clamp(), only needed if clamps change across function calls.

None t_max Optional[float]

Duration of the simulation in milliseconds. If t_max is greater than the length of the stimulus input, the stimulus will be padded at the end with zeros. If t_max is smaller, then the stimulus with be truncated.

None delta_t float

Time step of the solver in milliseconds.

0.025 solver str

Which ODE solver to use. Either of [\u201cfwd_euler\u201d, \u201cbwd_euler\u201d, \u201ccrank_nicolson\u201d].

'bwd_euler' tridiag_solver

Algorithm to solve tridiagonal systems. The different options only affect bwd_euler and crank_nicolson solvers. Either of [\u201cstone\u201d, \u201cthomas\u201d], where stone is much faster on GPU for long branches with many compartments and thomas is slightly faster on CPU (thomas is used in NEURON).

required checkpoint_lengths Optional[List[int]]

Number of timesteps at every level of checkpointing. The prod(checkpoint_lengths) must be larger or equal to the desired number of simulated timesteps. Warning: the simulation is run for prod(checkpoint_lengths) timesteps, and the result is posthoc truncated to the desired simulation length. Therefore, a poor choice of checkpoint_lengths can lead to longer simulation time. If None, no checkpointing is applied.

None all_states Optional[Dict]

An optional initial state that was returned by a previous jx.integrate(..., return_states=True) run. Overrides potentially trainable initial states.

None return_states bool

If True, it returns all states such that the current state of the Module can be set with set_states.

False Source code in jaxley/integrate.py
def integrate(\n    module: Module,\n    params: List[Dict[str, jnp.ndarray]] = [],\n    *,\n    param_state: Optional[List[Dict]] = None,\n    data_stimuli: Optional[Tuple[jnp.ndarray, pd.DataFrame]] = None,\n    data_clamps: Optional[Tuple[str, jnp.ndarray, pd.DataFrame]] = None,\n    t_max: Optional[float] = None,\n    delta_t: float = 0.025,\n    solver: str = \"bwd_euler\",\n    voltage_solver: str = \"jaxley.stone\",\n    checkpoint_lengths: Optional[List[int]] = None,\n    all_states: Optional[Dict] = None,\n    return_states: bool = False,\n) -> jnp.ndarray:\n    \"\"\"\n    Solves ODE and simulates neuron model.\n\n    Args:\n        params: Trainable parameters returned by `get_parameters()`.\n        param_state: Parameters returned by `data_set`.\n        data_stimuli: Outputs of `.data_stimulate()`, only needed if stimuli change\n            across function calls.\n        data_clamps: Outputs of `.data_clamp()`, only needed if clamps change across\n            function calls.\n        t_max: Duration of the simulation in milliseconds. If `t_max` is greater than\n            the length of the stimulus input, the stimulus will be padded at the end\n            with zeros. If `t_max` is smaller, then the stimulus with be truncated.\n        delta_t: Time step of the solver in milliseconds.\n        solver: Which ODE solver to use. Either of [\"fwd_euler\", \"bwd_euler\",\n            \"crank_nicolson\"].\n        tridiag_solver: Algorithm to solve tridiagonal systems. The  different options\n            only affect `bwd_euler` and `crank_nicolson` solvers. Either of [\"stone\",\n            \"thomas\"], where `stone` is much faster on GPU for long branches\n            with many compartments and `thomas` is slightly faster on CPU (`thomas` is\n            used in NEURON).\n        checkpoint_lengths: Number of timesteps at every level of checkpointing. The\n            `prod(checkpoint_lengths)` must be larger or equal to the desired number of\n            simulated timesteps. Warning: the simulation is run for\n            `prod(checkpoint_lengths)` timesteps, and the result is posthoc truncated\n            to the desired simulation length. Therefore, a poor choice of\n            `checkpoint_lengths` can lead to longer simulation time. If `None`, no\n            checkpointing is applied.\n        all_states: An optional initial state that was returned by a previous\n            `jx.integrate(..., return_states=True)` run. Overrides potentially\n            trainable initial states.\n        return_states: If True, it returns all states such that the current state of\n            the `Module` can be set with `set_states`.\n    \"\"\"\n\n    assert module.initialized, \"Module is not initialized, run `._initialize()`.\"\n    module.to_jax()  # Creates `.jaxnodes` from `.nodes` and `.jaxedges` from `.edges`.\n\n    # Initialize the external inputs and their indices.\n    externals = module.externals.copy()\n    external_inds = module.external_inds.copy()\n\n    # If stimulus is inserted, add it to the external inputs.\n    externals, external_inds = add_stimuli(externals, external_inds, data_stimuli)\n\n    # If a clamp is inserted, add it to the external inputs.\n    externals, external_inds = add_clamps(externals, external_inds, data_clamps)\n\n    if not externals.keys():\n        # No stimulus was inserted and no clamp was set.\n        assert (\n            t_max is not None\n        ), \"If no stimulus or clamp are inserted you have to specify the simulation duration at `jx.integrate(..., t_max=)`.\"\n\n    for key in externals.keys():\n        externals[key] = externals[key].T  # Shape `(time, num_stimuli)`.\n\n    if module.recordings.empty:\n        raise ValueError(\"No recordings are set. Please set them.\")\n    rec_inds = module.recordings.rec_index.to_numpy()\n    rec_states = module.recordings.state.to_numpy()\n\n    # Shorten or pad stimulus depending on `t_max`.\n    if t_max is not None:\n        t_max_steps = int(t_max // delta_t + 1)\n\n        # Pad or truncate the stimulus.\n        for key in externals.keys():\n            if t_max_steps > externals[key].shape[0]:\n                if key == \"i\":\n                    pad = jnp.zeros(\n                        (t_max_steps - externals[\"i\"].shape[0], externals[\"i\"].shape[1])\n                    )\n                    externals[\"i\"] = jnp.concatenate((externals[\"i\"], pad))\n                else:\n                    raise NotImplementedError(\n                        \"clamp must be at least as long as simulation.\"\n                    )\n            else:\n                externals[key] = externals[key][:t_max_steps, :]\n\n    init_fn, step_fn = build_init_and_step_fn(\n        module, voltage_solver=voltage_solver, solver=solver\n    )\n    all_states, all_params = init_fn(params, all_states, param_state, delta_t)\n\n    def _body_fun(state, externals):\n        state = step_fn(state, all_params, externals, external_inds, delta_t)\n        recs = jnp.asarray(\n            [\n                state[rec_state][rec_ind]\n                for rec_state, rec_ind in zip(rec_states, rec_inds)\n            ]\n        )\n        return state, recs\n\n    # If necessary, pad the stimulus with zeros in order to simulate sufficiently long.\n    # The total simulation length will be `prod(checkpoint_lengths)`. At the end, we\n    # return only the first `nsteps_to_return` elements (plus the initial state).\n    if externals:\n        example_key = list(externals.keys())[0]\n        nsteps_to_return = len(externals[example_key])\n    else:\n        nsteps_to_return = t_max_steps\n\n    if checkpoint_lengths is None:\n        checkpoint_lengths = [nsteps_to_return]\n        length = nsteps_to_return\n    else:\n        length = prod(checkpoint_lengths)\n        size_difference = length - nsteps_to_return\n        assert (\n            nsteps_to_return <= length\n        ), \"The desired simulation duration is longer than `prod(nested_length)`.\"\n        if externals:\n            dummy_external = jnp.zeros(\n                (size_difference, externals[example_key].shape[1])\n            )\n            for key in externals.keys():\n                externals[key] = jnp.concatenate([externals[key], dummy_external])\n\n    # Record the initial state.\n    init_recs = jnp.asarray(\n        [\n            all_states[rec_state][rec_ind]\n            for rec_state, rec_ind in zip(rec_states, rec_inds)\n        ]\n    )\n    init_recording = jnp.expand_dims(init_recs, axis=0)\n\n    # Run simulation.\n    all_states, recordings = nested_checkpoint_scan(\n        _body_fun,\n        all_states,\n        externals,\n        length=length,\n        nested_lengths=checkpoint_lengths,\n    )\n    recs = jnp.concatenate([init_recording, recordings[:nsteps_to_return]], axis=0).T\n    return (recs, all_states) if return_states else recs\n
"},{"location":"reference/integration/#jaxley.solver_gate.exponential_euler","title":"exponential_euler(x, dt, x_inf, x_tau)","text":"

An exact solver for the linear dynamical system dx = -(x - x_inf) / x_tau.

Source code in jaxley/solver_gate.py
def exponential_euler(\n    x: jnp.ndarray,\n    dt: float,\n    x_inf: jnp.ndarray,\n    x_tau: jnp.ndarray,\n):\n    \"\"\"An exact solver for the linear dynamical system `dx = -(x - x_inf) / x_tau`.\"\"\"\n    exp_term = save_exp(-dt / x_tau)\n    return x * exp_term + x_inf * (1.0 - exp_term)\n
"},{"location":"reference/integration/#jaxley.solver_gate.save_exp","title":"save_exp(x, max_value=20.0)","text":"

Clip the input to a maximum value and return its exponential.

Source code in jaxley/solver_gate.py
def save_exp(x, max_value: float = 20.0):\n    \"\"\"Clip the input to a maximum value and return its exponential.\"\"\"\n    x = jnp.clip(x, a_max=max_value)\n    return jnp.exp(x)\n
"},{"location":"reference/integration/#jaxley.solver_gate.solve_inf_gate_exponential","title":"solve_inf_gate_exponential(x, dt, s_inf, tau_s)","text":"

solves dx/dt = (s_inf - x) / tau_s via exponential Euler

Parameters:

Name Type Description Default x ndarray

gate variable

required dt float

time_delta

required s_inf ndarray

description

required tau_s ndarray

description

required

Returns:

Name Type Description _type_

updated gate

Source code in jaxley/solver_gate.py
def solve_inf_gate_exponential(\n    x: jnp.ndarray,\n    dt: float,\n    s_inf: jnp.ndarray,\n    tau_s: jnp.ndarray,\n):\n    \"\"\"solves dx/dt = (s_inf - x) / tau_s\n    via exponential Euler\n\n    Args:\n        x (jnp.ndarray): gate variable\n        dt (float): time_delta\n        s_inf (jnp.ndarray): _description_\n        tau_s (jnp.ndarray): _description_\n\n    Returns:\n        _type_: updated gate\n    \"\"\"\n    slope = -1.0 / tau_s\n    exp_term = save_exp(slope * dt)\n    return x * exp_term + s_inf * (1.0 - exp_term)\n
"},{"location":"reference/integration/#jaxley.solver_voltage.step_voltage_explicit","title":"step_voltage_explicit(voltages, voltage_terms, constant_terms, axial_conductances, internal_node_inds, sinks, sources, types, ncomp_per_branch, par_inds, child_inds, nbranches, solver, delta_t, idx, debug_states)","text":"

Solve one timestep of branched nerve equations with explicit (forward) Euler.

Source code in jaxley/solver_voltage.py
def step_voltage_explicit(\n    voltages: jnp.ndarray,\n    voltage_terms: jnp.ndarray,\n    constant_terms: jnp.ndarray,\n    axial_conductances: jnp.ndarray,\n    internal_node_inds: jnp.ndarray,\n    sinks: jnp.ndarray,\n    sources: jnp.ndarray,\n    types: jnp.ndarray,\n    ncomp_per_branch: jnp.ndarray,\n    par_inds: jnp.ndarray,\n    child_inds: jnp.ndarray,\n    nbranches: int,\n    solver: str,\n    delta_t: float,\n    idx: JaxleySolveIndexer,\n    debug_states,\n) -> jnp.ndarray:\n    \"\"\"Solve one timestep of branched nerve equations with explicit (forward) Euler.\"\"\"\n    voltages = jnp.reshape(voltages, (nbranches, -1))\n    voltage_terms = jnp.reshape(voltage_terms, (nbranches, -1))\n    constant_terms = jnp.reshape(constant_terms, (nbranches, -1))\n\n    update = _voltage_vectorfield(\n        voltages,\n        voltage_terms,\n        constant_terms,\n        types,\n        sources,\n        sinks,\n        axial_conductances,\n        par_inds,\n        child_inds,\n        nbranches,\n        solver,\n        delta_t,\n        idx,\n        debug_states,\n    )\n    new_voltates = voltages + delta_t * update\n    return new_voltates.ravel(order=\"C\")\n
"},{"location":"reference/integration/#jaxley.solver_voltage.step_voltage_implicit_with_jaxley_spsolve","title":"step_voltage_implicit_with_jaxley_spsolve(voltages, voltage_terms, constant_terms, axial_conductances, internal_node_inds, sinks, sources, types, ncomp_per_branch, par_inds, child_inds, nbranches, solver, delta_t, idx, debug_states)","text":"

Solve one timestep of branched nerve equations with implicit (backward) Euler.

Source code in jaxley/solver_voltage.py
def step_voltage_implicit_with_jaxley_spsolve(\n    voltages: jnp.ndarray,\n    voltage_terms: jnp.ndarray,\n    constant_terms: jnp.ndarray,\n    axial_conductances: jnp.ndarray,\n    internal_node_inds: jnp.ndarray,\n    sinks: jnp.ndarray,\n    sources: jnp.ndarray,\n    types: jnp.ndarray,\n    ncomp_per_branch: jnp.ndarray,\n    par_inds: jnp.ndarray,\n    child_inds: jnp.ndarray,\n    nbranches: int,\n    solver: str,\n    delta_t: float,\n    idx: JaxleySolveIndexer,\n    debug_states,\n):\n    \"\"\"Solve one timestep of branched nerve equations with implicit (backward) Euler.\"\"\"\n    # Build diagonals.\n    c2c = np.isin(types, [0, 1, 2])\n    total_ncomp = idx.cumsum_ncomp[-1]\n    diags = jnp.ones(total_ncomp)\n\n    # if-case needed because `.at` does not allow empty inputs, but the input is\n    # empty for compartments.\n    if len(sinks[c2c]) > 0:\n        diags = diags.at[idx.mask(sinks[c2c])].add(delta_t * axial_conductances[c2c])\n\n    diags = diags.at[idx.mask(internal_node_inds)].add(delta_t * voltage_terms)\n\n    # Build solves.\n    solves = jnp.zeros(total_ncomp)\n    solves = solves.at[idx.mask(internal_node_inds)].add(\n        voltages + delta_t * constant_terms\n    )\n\n    # Build upper and lower within the branch.\n    c2c = types == 0  # c2c = compartment-to-compartment.\n\n    # Build uppers.\n    uppers = jnp.zeros(total_ncomp)\n    upper_inds = sources[c2c] > sinks[c2c]\n    sinks_upper = sinks[c2c][upper_inds]\n    if len(sinks_upper) > 0:\n        uppers = uppers.at[idx.mask(sinks_upper)].add(\n            -delta_t * axial_conductances[c2c][upper_inds]\n        )\n\n    # Build lowers.\n    lowers = jnp.zeros(total_ncomp)\n    lower_inds = sources[c2c] < sinks[c2c]\n    sinks_lower = sinks[c2c][lower_inds]\n    if len(sinks_lower) > 0:\n        lowers = lowers.at[idx.mask(sinks_lower)].add(\n            -delta_t * axial_conductances[c2c][lower_inds]\n        )\n\n    # Build branchpoint conductances.\n    branchpoint_conds_parents = axial_conductances[types == 1]\n    branchpoint_conds_children = axial_conductances[types == 2]\n    branchpoint_weights_parents = axial_conductances[types == 3]\n    branchpoint_weights_children = axial_conductances[types == 4]\n    all_branchpoint_vals = jnp.concatenate(\n        [branchpoint_weights_parents, branchpoint_weights_children]\n    )\n    # Find unique group identifiers\n    num_branchpoints = len(branchpoint_conds_parents)\n    branchpoint_diags = -group_and_sum(\n        all_branchpoint_vals, idx.branchpoint_group_inds, num_branchpoints\n    )\n    branchpoint_solves = jnp.zeros((num_branchpoints,))\n\n    branchpoint_conds_children = -delta_t * branchpoint_conds_children\n    branchpoint_conds_parents = -delta_t * branchpoint_conds_parents\n\n    # Here, I move all child and parent indices towards a branchpoint into a larger\n    # vector. This is wasteful, but it makes indexing much easier. JIT compiling\n    # makes the speed difference negligible.\n    # Children.\n    bp_conds_children = jnp.zeros(nbranches)\n    bp_weights_children = jnp.zeros(nbranches)\n    # Parents.\n    bp_conds_parents = jnp.zeros(nbranches)\n    bp_weights_parents = jnp.zeros(nbranches)\n\n    # `.at[inds]` requires that `inds` is not empty, so we need an if-case here.\n    # `len(inds) == 0` is the case for branches and compartments.\n    if num_branchpoints > 0:\n        bp_conds_children = bp_conds_children.at[child_inds].set(\n            branchpoint_conds_children\n        )\n        bp_weights_children = bp_weights_children.at[child_inds].set(\n            branchpoint_weights_children\n        )\n        bp_conds_parents = bp_conds_parents.at[par_inds].set(branchpoint_conds_parents)\n        bp_weights_parents = bp_weights_parents.at[par_inds].set(\n            branchpoint_weights_parents\n        )\n\n    # Triangulate the linear system of equations.\n    (\n        diags,\n        lowers,\n        solves,\n        uppers,\n        branchpoint_diags,\n        branchpoint_solves,\n        bp_weights_children,\n        bp_conds_parents,\n    ) = _triang_branched(\n        lowers,\n        diags,\n        uppers,\n        solves,\n        bp_conds_children,\n        bp_conds_parents,\n        bp_weights_children,\n        bp_weights_parents,\n        branchpoint_diags,\n        branchpoint_solves,\n        solver,\n        ncomp_per_branch,\n        idx,\n        debug_states,\n    )\n\n    # Backsubstitute the linear system of equations.\n    (\n        solves,\n        lowers,\n        diags,\n        bp_weights_parents,\n        branchpoint_solves,\n        bp_conds_children,\n    ) = _backsub_branched(\n        lowers,\n        diags,\n        uppers,\n        solves,\n        bp_conds_children,\n        bp_conds_parents,\n        bp_weights_children,\n        bp_weights_parents,\n        branchpoint_diags,\n        branchpoint_solves,\n        solver,\n        ncomp_per_branch,\n        idx,\n        debug_states,\n    )\n    return solves.ravel(order=\"C\")[idx.mask(internal_node_inds)]\n
"},{"location":"reference/mechanisms/","title":"Channels","text":""},{"location":"reference/mechanisms/#channel","title":"Channel","text":"

Channel base class. All channels inherit from this class.

As in NEURON, a Channel is considered a distributed process, which means that its conductances are to be specified in S/cm2 and its currents are to be specified in uA/cm2.

Source code in jaxley/channels/channel.py
class Channel:\n    \"\"\"Channel base class. All channels inherit from this class.\n\n    As in NEURON, a `Channel` is considered a distributed process, which means that its\n    conductances are to be specified in `S/cm2` and its currents are to be specified in\n    `uA/cm2`.\"\"\"\n\n    _name = None\n    channel_params = None\n    channel_states = None\n    current_name = None\n\n    def __init__(self, name: Optional[str] = None):\n        contact = (\n            \"If you have any questions, please reach out via email to \"\n            \"michael.deistler@uni-tuebingen.de or create an issue on Github: \"\n            \"https://github.com/jaxleyverse/jaxley/issues. Thank you!\"\n        )\n        if (\n            not hasattr(self, \"current_is_in_mA_per_cm2\")\n            or not self.current_is_in_mA_per_cm2\n        ):\n            raise ValueError(\n                \"The channel you are using is deprecated. \"\n                \"In Jaxley version 0.5.0, we changed the unit of the current returned \"\n                \"by `compute_current` of channels from `uA/cm^2` to `mA/cm^2`. Please \"\n                \"update your channel model (by dividing the resulting current by 1000) \"\n                \"and set `self.current_is_in_mA_per_cm2=True` as the first line \"\n                f\"in the `__init__()` method of your channel. {contact}\"\n            )\n\n        self._name = name if name else self.__class__.__name__\n\n    @property\n    def name(self) -> Optional[str]:\n        \"\"\"The name of the channel (by default, this is the class name).\"\"\"\n        return self._name\n\n    def change_name(self, new_name: str):\n        \"\"\"Change the channel name.\n\n        Args:\n            new_name: The new name of the channel.\n\n        Returns:\n            Renamed channel, such that this function is chainable.\n        \"\"\"\n        old_prefix = self._name + \"_\"\n        new_prefix = new_name + \"_\"\n\n        self._name = new_name\n        self.channel_params = {\n            (\n                new_prefix + key[len(old_prefix) :]\n                if key.startswith(old_prefix)\n                else key\n            ): value\n            for key, value in self.channel_params.items()\n        }\n\n        self.channel_states = {\n            (\n                new_prefix + key[len(old_prefix) :]\n                if key.startswith(old_prefix)\n                else key\n            ): value\n            for key, value in self.channel_states.items()\n        }\n        return self\n\n    def update_states(\n        self, states, dt, v, params\n    ) -> Tuple[jnp.ndarray, Tuple[jnp.ndarray, jnp.ndarray]]:\n        \"\"\"Return the updated states.\"\"\"\n        raise NotImplementedError\n\n    def compute_current(\n        self, states: Dict[str, jnp.ndarray], v, params: Dict[str, jnp.ndarray]\n    ):\n        \"\"\"Given channel states and voltage, return the current through the channel.\n\n        Args:\n            states: All states of the compartment.\n            v: Voltage of the compartment in mV.\n            params: Parameters of the channel (conductances in `S/cm2`).\n\n        Returns:\n            Current in `uA/cm2`.\n        \"\"\"\n        raise NotImplementedError\n\n    def init_state(\n        self,\n        states: Dict[str, jnp.ndarray],\n        v: jnp.ndarray,\n        params: Dict[str, jnp.ndarray],\n        delta_t: float,\n    ):\n        \"\"\"Initialize states of channel.\"\"\"\n        return {}\n
"},{"location":"reference/mechanisms/#jaxley.channels.channel.Channel.name","title":"name: Optional[str] property","text":"

The name of the channel (by default, this is the class name).

"},{"location":"reference/mechanisms/#jaxley.channels.channel.Channel.change_name","title":"change_name(new_name)","text":"

Change the channel name.

Parameters:

Name Type Description Default new_name str

The new name of the channel.

required

Returns:

Type Description

Renamed channel, such that this function is chainable.

Source code in jaxley/channels/channel.py
def change_name(self, new_name: str):\n    \"\"\"Change the channel name.\n\n    Args:\n        new_name: The new name of the channel.\n\n    Returns:\n        Renamed channel, such that this function is chainable.\n    \"\"\"\n    old_prefix = self._name + \"_\"\n    new_prefix = new_name + \"_\"\n\n    self._name = new_name\n    self.channel_params = {\n        (\n            new_prefix + key[len(old_prefix) :]\n            if key.startswith(old_prefix)\n            else key\n        ): value\n        for key, value in self.channel_params.items()\n    }\n\n    self.channel_states = {\n        (\n            new_prefix + key[len(old_prefix) :]\n            if key.startswith(old_prefix)\n            else key\n        ): value\n        for key, value in self.channel_states.items()\n    }\n    return self\n
"},{"location":"reference/mechanisms/#jaxley.channels.channel.Channel.compute_current","title":"compute_current(states, v, params)","text":"

Given channel states and voltage, return the current through the channel.

Parameters:

Name Type Description Default states Dict[str, ndarray]

All states of the compartment.

required v

Voltage of the compartment in mV.

required params Dict[str, ndarray]

Parameters of the channel (conductances in S/cm2).

required

Returns:

Type Description

Current in uA/cm2.

Source code in jaxley/channels/channel.py
def compute_current(\n    self, states: Dict[str, jnp.ndarray], v, params: Dict[str, jnp.ndarray]\n):\n    \"\"\"Given channel states and voltage, return the current through the channel.\n\n    Args:\n        states: All states of the compartment.\n        v: Voltage of the compartment in mV.\n        params: Parameters of the channel (conductances in `S/cm2`).\n\n    Returns:\n        Current in `uA/cm2`.\n    \"\"\"\n    raise NotImplementedError\n
"},{"location":"reference/mechanisms/#jaxley.channels.channel.Channel.init_state","title":"init_state(states, v, params, delta_t)","text":"

Initialize states of channel.

Source code in jaxley/channels/channel.py
def init_state(\n    self,\n    states: Dict[str, jnp.ndarray],\n    v: jnp.ndarray,\n    params: Dict[str, jnp.ndarray],\n    delta_t: float,\n):\n    \"\"\"Initialize states of channel.\"\"\"\n    return {}\n
"},{"location":"reference/mechanisms/#jaxley.channels.channel.Channel.update_states","title":"update_states(states, dt, v, params)","text":"

Return the updated states.

Source code in jaxley/channels/channel.py
def update_states(\n    self, states, dt, v, params\n) -> Tuple[jnp.ndarray, Tuple[jnp.ndarray, jnp.ndarray]]:\n    \"\"\"Return the updated states.\"\"\"\n    raise NotImplementedError\n
"},{"location":"reference/mechanisms/#hh","title":"HH","text":"

Bases: Channel

Hodgkin-Huxley channel.

Source code in jaxley/channels/hh.py
class HH(Channel):\n    \"\"\"Hodgkin-Huxley channel.\"\"\"\n\n    def __init__(self, name: Optional[str] = None):\n        self.current_is_in_mA_per_cm2 = True\n\n        super().__init__(name)\n        prefix = self._name\n        self.channel_params = {\n            f\"{prefix}_gNa\": 0.12,\n            f\"{prefix}_gK\": 0.036,\n            f\"{prefix}_gLeak\": 0.0003,\n            f\"{prefix}_eNa\": 50.0,\n            f\"{prefix}_eK\": -77.0,\n            f\"{prefix}_eLeak\": -54.3,\n        }\n        self.channel_states = {\n            f\"{prefix}_m\": 0.2,\n            f\"{prefix}_h\": 0.2,\n            f\"{prefix}_n\": 0.2,\n        }\n        self.current_name = f\"i_HH\"\n\n    def update_states(\n        self,\n        states: Dict[str, jnp.ndarray],\n        dt,\n        v,\n        params: Dict[str, jnp.ndarray],\n    ):\n        \"\"\"Return updated HH channel state.\"\"\"\n        prefix = self._name\n        m, h, n = states[f\"{prefix}_m\"], states[f\"{prefix}_h\"], states[f\"{prefix}_n\"]\n        new_m = solve_gate_exponential(m, dt, *self.m_gate(v))\n        new_h = solve_gate_exponential(h, dt, *self.h_gate(v))\n        new_n = solve_gate_exponential(n, dt, *self.n_gate(v))\n        return {f\"{prefix}_m\": new_m, f\"{prefix}_h\": new_h, f\"{prefix}_n\": new_n}\n\n    def compute_current(\n        self, states: Dict[str, jnp.ndarray], v, params: Dict[str, jnp.ndarray]\n    ):\n        \"\"\"Return current through HH channels.\"\"\"\n        prefix = self._name\n        m, h, n = states[f\"{prefix}_m\"], states[f\"{prefix}_h\"], states[f\"{prefix}_n\"]\n\n        gNa = params[f\"{prefix}_gNa\"] * (m**3) * h  # S/cm^2\n        gK = params[f\"{prefix}_gK\"] * n**4  # S/cm^2\n        gLeak = params[f\"{prefix}_gLeak\"]  # S/cm^2\n\n        return (\n            gNa * (v - params[f\"{prefix}_eNa\"])\n            + gK * (v - params[f\"{prefix}_eK\"])\n            + gLeak * (v - params[f\"{prefix}_eLeak\"])\n        )\n\n    def init_state(self, states, v, params, delta_t):\n        \"\"\"Initialize the state such at fixed point of gate dynamics.\"\"\"\n        prefix = self._name\n        alpha_m, beta_m = self.m_gate(v)\n        alpha_h, beta_h = self.h_gate(v)\n        alpha_n, beta_n = self.n_gate(v)\n        return {\n            f\"{prefix}_m\": alpha_m / (alpha_m + beta_m),\n            f\"{prefix}_h\": alpha_h / (alpha_h + beta_h),\n            f\"{prefix}_n\": alpha_n / (alpha_n + beta_n),\n        }\n\n    @staticmethod\n    def m_gate(v):\n        alpha = 0.1 * _vtrap(-(v + 40), 10)\n        beta = 4.0 * save_exp(-(v + 65) / 18)\n        return alpha, beta\n\n    @staticmethod\n    def h_gate(v):\n        alpha = 0.07 * save_exp(-(v + 65) / 20)\n        beta = 1.0 / (save_exp(-(v + 35) / 10) + 1)\n        return alpha, beta\n\n    @staticmethod\n    def n_gate(v):\n        alpha = 0.01 * _vtrap(-(v + 55), 10)\n        beta = 0.125 * save_exp(-(v + 65) / 80)\n        return alpha, beta\n
"},{"location":"reference/mechanisms/#jaxley.channels.hh.HH.compute_current","title":"compute_current(states, v, params)","text":"

Return current through HH channels.

Source code in jaxley/channels/hh.py
def compute_current(\n    self, states: Dict[str, jnp.ndarray], v, params: Dict[str, jnp.ndarray]\n):\n    \"\"\"Return current through HH channels.\"\"\"\n    prefix = self._name\n    m, h, n = states[f\"{prefix}_m\"], states[f\"{prefix}_h\"], states[f\"{prefix}_n\"]\n\n    gNa = params[f\"{prefix}_gNa\"] * (m**3) * h  # S/cm^2\n    gK = params[f\"{prefix}_gK\"] * n**4  # S/cm^2\n    gLeak = params[f\"{prefix}_gLeak\"]  # S/cm^2\n\n    return (\n        gNa * (v - params[f\"{prefix}_eNa\"])\n        + gK * (v - params[f\"{prefix}_eK\"])\n        + gLeak * (v - params[f\"{prefix}_eLeak\"])\n    )\n
"},{"location":"reference/mechanisms/#jaxley.channels.hh.HH.init_state","title":"init_state(states, v, params, delta_t)","text":"

Initialize the state such at fixed point of gate dynamics.

Source code in jaxley/channels/hh.py
def init_state(self, states, v, params, delta_t):\n    \"\"\"Initialize the state such at fixed point of gate dynamics.\"\"\"\n    prefix = self._name\n    alpha_m, beta_m = self.m_gate(v)\n    alpha_h, beta_h = self.h_gate(v)\n    alpha_n, beta_n = self.n_gate(v)\n    return {\n        f\"{prefix}_m\": alpha_m / (alpha_m + beta_m),\n        f\"{prefix}_h\": alpha_h / (alpha_h + beta_h),\n        f\"{prefix}_n\": alpha_n / (alpha_n + beta_n),\n    }\n
"},{"location":"reference/mechanisms/#jaxley.channels.hh.HH.update_states","title":"update_states(states, dt, v, params)","text":"

Return updated HH channel state.

Source code in jaxley/channels/hh.py
def update_states(\n    self,\n    states: Dict[str, jnp.ndarray],\n    dt,\n    v,\n    params: Dict[str, jnp.ndarray],\n):\n    \"\"\"Return updated HH channel state.\"\"\"\n    prefix = self._name\n    m, h, n = states[f\"{prefix}_m\"], states[f\"{prefix}_h\"], states[f\"{prefix}_n\"]\n    new_m = solve_gate_exponential(m, dt, *self.m_gate(v))\n    new_h = solve_gate_exponential(h, dt, *self.h_gate(v))\n    new_n = solve_gate_exponential(n, dt, *self.n_gate(v))\n    return {f\"{prefix}_m\": new_m, f\"{prefix}_h\": new_h, f\"{prefix}_n\": new_n}\n
"},{"location":"reference/mechanisms/#pospischil","title":"Pospischil","text":"

Bases: Channel

Leak current

Source code in jaxley/channels/pospischil.py
class Leak(Channel):\n    \"\"\"Leak current\"\"\"\n\n    def __init__(self, name: Optional[str] = None):\n        self.current_is_in_mA_per_cm2 = True\n\n        super().__init__(name)\n        prefix = self._name\n        self.channel_params = {\n            f\"{prefix}_gLeak\": 1e-4,\n            f\"{prefix}_eLeak\": -70.0,\n        }\n        self.channel_states = {}\n        self.current_name = f\"i_{prefix}\"\n\n    def update_states(\n        self,\n        states: Dict[str, jnp.ndarray],\n        dt,\n        v,\n        params: Dict[str, jnp.ndarray],\n    ):\n        \"\"\"No state to update.\"\"\"\n        return {}\n\n    def compute_current(\n        self, states: Dict[str, jnp.ndarray], v, params: Dict[str, jnp.ndarray]\n    ):\n        \"\"\"Return current.\"\"\"\n        prefix = self._name\n        gLeak = params[f\"{prefix}_gLeak\"]  # S/cm^2\n        return gLeak * (v - params[f\"{prefix}_eLeak\"])\n\n    def init_state(self, states, v, params, delta_t):\n        return {}\n

Bases: Channel

Sodium channel

Source code in jaxley/channels/pospischil.py
class Na(Channel):\n    \"\"\"Sodium channel\"\"\"\n\n    def __init__(self, name: Optional[str] = None):\n        self.current_is_in_mA_per_cm2 = True\n\n        super().__init__(name)\n        prefix = self._name\n        self.channel_params = {\n            f\"{prefix}_gNa\": 50e-3,\n            \"eNa\": 50.0,\n            \"vt\": -60.0,  # Global parameter, not prefixed with `Na`.\n        }\n        self.channel_states = {f\"{prefix}_m\": 0.2, f\"{prefix}_h\": 0.2}\n        self.current_name = f\"i_Na\"\n\n    def update_states(\n        self,\n        states: Dict[str, jnp.ndarray],\n        dt,\n        v,\n        params: Dict[str, jnp.ndarray],\n    ):\n        \"\"\"Update state.\"\"\"\n        prefix = self._name\n        m, h = states[f\"{prefix}_m\"], states[f\"{prefix}_h\"]\n        new_m = solve_gate_exponential(m, dt, *self.m_gate(v, params[\"vt\"]))\n        new_h = solve_gate_exponential(h, dt, *self.h_gate(v, params[\"vt\"]))\n        return {f\"{prefix}_m\": new_m, f\"{prefix}_h\": new_h}\n\n    def compute_current(\n        self, states: Dict[str, jnp.ndarray], v, params: Dict[str, jnp.ndarray]\n    ):\n        \"\"\"Return current.\"\"\"\n        prefix = self._name\n        m, h = states[f\"{prefix}_m\"], states[f\"{prefix}_h\"]\n\n        gNa = params[f\"{prefix}_gNa\"] * (m**3) * h  # S/cm^2\n\n        current = gNa * (v - params[\"eNa\"])\n        return current\n\n    def init_state(self, states, v, params, delta_t):\n        \"\"\"Initialize the state such at fixed point of gate dynamics.\"\"\"\n        prefix = self._name\n        alpha_m, beta_m = self.m_gate(v, params[\"vt\"])\n        alpha_h, beta_h = self.h_gate(v, params[\"vt\"])\n        return {\n            f\"{prefix}_m\": alpha_m / (alpha_m + beta_m),\n            f\"{prefix}_h\": alpha_h / (alpha_h + beta_h),\n        }\n\n    @staticmethod\n    def m_gate(v, vt):\n        v_alpha = v - vt - 13.0\n        alpha = 0.32 * efun(-0.25 * v_alpha) / 0.25\n\n        v_beta = v - vt - 40.0\n        beta = 0.28 * efun(0.2 * v_beta) / 0.2\n        return alpha, beta\n\n    @staticmethod\n    def h_gate(v, vt):\n        v_alpha = v - vt - 17.0\n        alpha = 0.128 * save_exp(-v_alpha / 18.0)\n\n        v_beta = v - vt - 40.0\n        beta = 4.0 / (save_exp(-v_beta / 5.0) + 1.0)\n        return alpha, beta\n

Bases: Channel

Potassium channel

Source code in jaxley/channels/pospischil.py
class K(Channel):\n    \"\"\"Potassium channel\"\"\"\n\n    def __init__(self, name: Optional[str] = None):\n        self.current_is_in_mA_per_cm2 = True\n\n        super().__init__(name)\n        prefix = self._name\n        self.channel_params = {\n            f\"{prefix}_gK\": 5e-3,\n            \"eK\": -90.0,\n            \"vt\": -60.0,  # Global parameter, not prefixed with `Na`.\n        }\n        self.channel_states = {f\"{prefix}_n\": 0.2}\n        self.current_name = f\"i_K\"\n\n    def update_states(\n        self,\n        states: Dict[str, jnp.ndarray],\n        dt,\n        v,\n        params: Dict[str, jnp.ndarray],\n    ):\n        \"\"\"Update state.\"\"\"\n        prefix = self._name\n        n = states[f\"{prefix}_n\"]\n        new_n = solve_gate_exponential(n, dt, *self.n_gate(v, params[\"vt\"]))\n        return {f\"{prefix}_n\": new_n}\n\n    def compute_current(\n        self, states: Dict[str, jnp.ndarray], v, params: Dict[str, jnp.ndarray]\n    ):\n        \"\"\"Return current.\"\"\"\n        prefix = self._name\n        n = states[f\"{prefix}_n\"]\n\n        gK = params[f\"{prefix}_gK\"] * (n**4)  # S/cm^2\n\n        return gK * (v - params[\"eK\"])\n\n    def init_state(self, states, v, params, delta_t):\n        \"\"\"Initialize the state such at fixed point of gate dynamics.\"\"\"\n        prefix = self._name\n        alpha_n, beta_n = self.n_gate(v, params[\"vt\"])\n        return {f\"{prefix}_n\": alpha_n / (alpha_n + beta_n)}\n\n    @staticmethod\n    def n_gate(v, vt):\n        v_alpha = v - vt - 15.0\n        alpha = 0.032 * efun(-0.2 * v_alpha) / 0.2\n\n        v_beta = v - vt - 10.0\n        beta = 0.5 * save_exp(-v_beta / 40.0)\n        return alpha, beta\n

Bases: Channel

Slow M Potassium channel

Source code in jaxley/channels/pospischil.py
class Km(Channel):\n    \"\"\"Slow M Potassium channel\"\"\"\n\n    def __init__(self, name: Optional[str] = None):\n        self.current_is_in_mA_per_cm2 = True\n\n        super().__init__(name)\n        prefix = self._name\n        self.channel_params = {\n            f\"{prefix}_gKm\": 0.004e-3,\n            f\"{prefix}_taumax\": 4000.0,\n            f\"eK\": -90.0,\n        }\n        self.channel_states = {f\"{prefix}_p\": 0.2}\n        self.current_name = f\"i_K\"\n\n    def update_states(\n        self,\n        states: Dict[str, jnp.ndarray],\n        dt,\n        v,\n        params: Dict[str, jnp.ndarray],\n    ):\n        \"\"\"Update state.\"\"\"\n        prefix = self._name\n        p = states[f\"{prefix}_p\"]\n        new_p = solve_inf_gate_exponential(\n            p, dt, *self.p_gate(v, params[f\"{prefix}_taumax\"])\n        )\n        return {f\"{prefix}_p\": new_p}\n\n    def compute_current(\n        self, states: Dict[str, jnp.ndarray], v, params: Dict[str, jnp.ndarray]\n    ):\n        \"\"\"Return current.\"\"\"\n        prefix = self._name\n        p = states[f\"{prefix}_p\"]\n\n        gKm = params[f\"{prefix}_gKm\"] * p  # S/cm^2\n        return gKm * (v - params[\"eK\"])\n\n    def init_state(self, states, v, params, delta_t):\n        \"\"\"Initialize the state such at fixed point of gate dynamics.\"\"\"\n        prefix = self._name\n        alpha_p, beta_p = self.p_gate(v, params[f\"{prefix}_taumax\"])\n        return {f\"{prefix}_p\": alpha_p / (alpha_p + beta_p)}\n\n    @staticmethod\n    def p_gate(v, taumax):\n        v_p = v + 35.0\n        p_inf = 1.0 / (1.0 + save_exp(-0.1 * v_p))\n\n        tau_p = taumax / (3.3 * save_exp(0.05 * v_p) + save_exp(-0.05 * v_p))\n\n        return p_inf, tau_p\n

Bases: Channel

L-type Calcium channel

Source code in jaxley/channels/pospischil.py
class CaL(Channel):\n    \"\"\"L-type Calcium channel\"\"\"\n\n    def __init__(self, name: Optional[str] = None):\n        self.current_is_in_mA_per_cm2 = True\n\n        super().__init__(name)\n        prefix = self._name\n        self.channel_params = {\n            f\"{prefix}_gCaL\": 0.1e-3,\n            \"eCa\": 120.0,\n        }\n        self.channel_states = {f\"{prefix}_q\": 0.2, f\"{prefix}_r\": 0.2}\n        self.current_name = f\"i_Ca\"\n\n    def update_states(\n        self,\n        states: Dict[str, jnp.ndarray],\n        dt,\n        v,\n        params: Dict[str, jnp.ndarray],\n    ):\n        \"\"\"Update state.\"\"\"\n        prefix = self._name\n        q, r = states[f\"{prefix}_q\"], states[f\"{prefix}_r\"]\n        new_q = solve_gate_exponential(q, dt, *self.q_gate(v))\n        new_r = solve_gate_exponential(r, dt, *self.r_gate(v))\n        return {f\"{prefix}_q\": new_q, f\"{prefix}_r\": new_r}\n\n    def compute_current(\n        self, states: Dict[str, jnp.ndarray], v, params: Dict[str, jnp.ndarray]\n    ):\n        \"\"\"Return current.\"\"\"\n        prefix = self._name\n        q, r = states[f\"{prefix}_q\"], states[f\"{prefix}_r\"]\n        gCaL = params[f\"{prefix}_gCaL\"] * (q**2) * r  # S/cm^2\n\n        return gCaL * (v - params[\"eCa\"])\n\n    def init_state(self, states, v, params, delta_t):\n        \"\"\"Initialize the state such at fixed point of gate dynamics.\"\"\"\n        prefix = self._name\n        alpha_q, beta_q = self.q_gate(v)\n        alpha_r, beta_r = self.r_gate(v)\n        return {\n            f\"{prefix}_q\": alpha_q / (alpha_q + beta_q),\n            f\"{prefix}_r\": alpha_r / (alpha_r + beta_r),\n        }\n\n    @staticmethod\n    def q_gate(v):\n        v_alpha = -v - 27.0\n        alpha = 0.055 * efun(v_alpha / 3.8) * 3.8\n\n        v_beta = -v - 75.0\n        beta = 0.94 * save_exp(v_beta / 17.0)\n        return alpha, beta\n\n    @staticmethod\n    def r_gate(v):\n        v_alpha = -v - 13.0\n        alpha = 0.000457 * save_exp(v_alpha / 50)\n\n        v_beta = -v - 15.0\n        beta = 0.0065 / (save_exp(v_beta / 28.0) + 1)\n        return alpha, beta\n

Bases: Channel

T-type Calcium channel

Source code in jaxley/channels/pospischil.py
class CaT(Channel):\n    \"\"\"T-type Calcium channel\"\"\"\n\n    def __init__(self, name: Optional[str] = None):\n        self.current_is_in_mA_per_cm2 = True\n\n        super().__init__(name)\n        prefix = self._name\n        self.channel_params = {\n            f\"{prefix}_gCaT\": 0.4e-4,\n            f\"{prefix}_vx\": 2.0,\n            \"eCa\": 120.0,  # Global parameter, not prefixed with `CaT`.\n        }\n        self.channel_states = {f\"{prefix}_u\": 0.2}\n        self.current_name = f\"i_Ca\"\n\n    def update_states(\n        self,\n        states: Dict[str, jnp.ndarray],\n        dt,\n        v,\n        params: Dict[str, jnp.ndarray],\n    ):\n        \"\"\"Update state.\"\"\"\n        prefix = self._name\n        u = states[f\"{prefix}_u\"]\n        new_u = solve_inf_gate_exponential(\n            u, dt, *self.u_gate(v, params[f\"{prefix}_vx\"])\n        )\n        return {f\"{prefix}_u\": new_u}\n\n    def compute_current(\n        self, states: Dict[str, jnp.ndarray], v, params: Dict[str, jnp.ndarray]\n    ):\n        \"\"\"Return current.\"\"\"\n        prefix = self._name\n        u = states[f\"{prefix}_u\"]\n        s_inf = 1.0 / (1.0 + save_exp(-(v + params[f\"{prefix}_vx\"] + 57.0) / 6.2))\n\n        gCaT = params[f\"{prefix}_gCaT\"] * (s_inf**2) * u  # S/cm^2\n\n        return gCaT * (v - params[\"eCa\"])\n\n    def init_state(self, states, v, params, delta_t):\n        \"\"\"Initialize the state such at fixed point of gate dynamics.\"\"\"\n        prefix = self._name\n        alpha_u, beta_u = self.u_gate(v, params[f\"{prefix}_vx\"])\n        return {f\"{prefix}_u\": alpha_u / (alpha_u + beta_u)}\n\n    @staticmethod\n    def u_gate(v, vx):\n        v_u1 = v + vx + 81.0\n        u_inf = 1.0 / (1.0 + save_exp(v_u1 / 4))\n\n        tau_u = (30.8 + (211.4 + save_exp((v + vx + 113.2) / 5.0))) / (\n            3.7 * (1 + save_exp((v + vx + 84.0) / 3.2))\n        )\n\n        return u_inf, tau_u\n
"},{"location":"reference/mechanisms/#jaxley.channels.pospischil.Leak.compute_current","title":"compute_current(states, v, params)","text":"

Return current.

Source code in jaxley/channels/pospischil.py
def compute_current(\n    self, states: Dict[str, jnp.ndarray], v, params: Dict[str, jnp.ndarray]\n):\n    \"\"\"Return current.\"\"\"\n    prefix = self._name\n    gLeak = params[f\"{prefix}_gLeak\"]  # S/cm^2\n    return gLeak * (v - params[f\"{prefix}_eLeak\"])\n
"},{"location":"reference/mechanisms/#jaxley.channels.pospischil.Leak.update_states","title":"update_states(states, dt, v, params)","text":"

No state to update.

Source code in jaxley/channels/pospischil.py
def update_states(\n    self,\n    states: Dict[str, jnp.ndarray],\n    dt,\n    v,\n    params: Dict[str, jnp.ndarray],\n):\n    \"\"\"No state to update.\"\"\"\n    return {}\n
"},{"location":"reference/mechanisms/#jaxley.channels.pospischil.Na.compute_current","title":"compute_current(states, v, params)","text":"

Return current.

Source code in jaxley/channels/pospischil.py
def compute_current(\n    self, states: Dict[str, jnp.ndarray], v, params: Dict[str, jnp.ndarray]\n):\n    \"\"\"Return current.\"\"\"\n    prefix = self._name\n    m, h = states[f\"{prefix}_m\"], states[f\"{prefix}_h\"]\n\n    gNa = params[f\"{prefix}_gNa\"] * (m**3) * h  # S/cm^2\n\n    current = gNa * (v - params[\"eNa\"])\n    return current\n
"},{"location":"reference/mechanisms/#jaxley.channels.pospischil.Na.init_state","title":"init_state(states, v, params, delta_t)","text":"

Initialize the state such at fixed point of gate dynamics.

Source code in jaxley/channels/pospischil.py
def init_state(self, states, v, params, delta_t):\n    \"\"\"Initialize the state such at fixed point of gate dynamics.\"\"\"\n    prefix = self._name\n    alpha_m, beta_m = self.m_gate(v, params[\"vt\"])\n    alpha_h, beta_h = self.h_gate(v, params[\"vt\"])\n    return {\n        f\"{prefix}_m\": alpha_m / (alpha_m + beta_m),\n        f\"{prefix}_h\": alpha_h / (alpha_h + beta_h),\n    }\n
"},{"location":"reference/mechanisms/#jaxley.channels.pospischil.Na.update_states","title":"update_states(states, dt, v, params)","text":"

Update state.

Source code in jaxley/channels/pospischil.py
def update_states(\n    self,\n    states: Dict[str, jnp.ndarray],\n    dt,\n    v,\n    params: Dict[str, jnp.ndarray],\n):\n    \"\"\"Update state.\"\"\"\n    prefix = self._name\n    m, h = states[f\"{prefix}_m\"], states[f\"{prefix}_h\"]\n    new_m = solve_gate_exponential(m, dt, *self.m_gate(v, params[\"vt\"]))\n    new_h = solve_gate_exponential(h, dt, *self.h_gate(v, params[\"vt\"]))\n    return {f\"{prefix}_m\": new_m, f\"{prefix}_h\": new_h}\n
"},{"location":"reference/mechanisms/#jaxley.channels.pospischil.K.compute_current","title":"compute_current(states, v, params)","text":"

Return current.

Source code in jaxley/channels/pospischil.py
def compute_current(\n    self, states: Dict[str, jnp.ndarray], v, params: Dict[str, jnp.ndarray]\n):\n    \"\"\"Return current.\"\"\"\n    prefix = self._name\n    n = states[f\"{prefix}_n\"]\n\n    gK = params[f\"{prefix}_gK\"] * (n**4)  # S/cm^2\n\n    return gK * (v - params[\"eK\"])\n
"},{"location":"reference/mechanisms/#jaxley.channels.pospischil.K.init_state","title":"init_state(states, v, params, delta_t)","text":"

Initialize the state such at fixed point of gate dynamics.

Source code in jaxley/channels/pospischil.py
def init_state(self, states, v, params, delta_t):\n    \"\"\"Initialize the state such at fixed point of gate dynamics.\"\"\"\n    prefix = self._name\n    alpha_n, beta_n = self.n_gate(v, params[\"vt\"])\n    return {f\"{prefix}_n\": alpha_n / (alpha_n + beta_n)}\n
"},{"location":"reference/mechanisms/#jaxley.channels.pospischil.K.update_states","title":"update_states(states, dt, v, params)","text":"

Update state.

Source code in jaxley/channels/pospischil.py
def update_states(\n    self,\n    states: Dict[str, jnp.ndarray],\n    dt,\n    v,\n    params: Dict[str, jnp.ndarray],\n):\n    \"\"\"Update state.\"\"\"\n    prefix = self._name\n    n = states[f\"{prefix}_n\"]\n    new_n = solve_gate_exponential(n, dt, *self.n_gate(v, params[\"vt\"]))\n    return {f\"{prefix}_n\": new_n}\n
"},{"location":"reference/mechanisms/#jaxley.channels.pospischil.Km.compute_current","title":"compute_current(states, v, params)","text":"

Return current.

Source code in jaxley/channels/pospischil.py
def compute_current(\n    self, states: Dict[str, jnp.ndarray], v, params: Dict[str, jnp.ndarray]\n):\n    \"\"\"Return current.\"\"\"\n    prefix = self._name\n    p = states[f\"{prefix}_p\"]\n\n    gKm = params[f\"{prefix}_gKm\"] * p  # S/cm^2\n    return gKm * (v - params[\"eK\"])\n
"},{"location":"reference/mechanisms/#jaxley.channels.pospischil.Km.init_state","title":"init_state(states, v, params, delta_t)","text":"

Initialize the state such at fixed point of gate dynamics.

Source code in jaxley/channels/pospischil.py
def init_state(self, states, v, params, delta_t):\n    \"\"\"Initialize the state such at fixed point of gate dynamics.\"\"\"\n    prefix = self._name\n    alpha_p, beta_p = self.p_gate(v, params[f\"{prefix}_taumax\"])\n    return {f\"{prefix}_p\": alpha_p / (alpha_p + beta_p)}\n
"},{"location":"reference/mechanisms/#jaxley.channels.pospischil.Km.update_states","title":"update_states(states, dt, v, params)","text":"

Update state.

Source code in jaxley/channels/pospischil.py
def update_states(\n    self,\n    states: Dict[str, jnp.ndarray],\n    dt,\n    v,\n    params: Dict[str, jnp.ndarray],\n):\n    \"\"\"Update state.\"\"\"\n    prefix = self._name\n    p = states[f\"{prefix}_p\"]\n    new_p = solve_inf_gate_exponential(\n        p, dt, *self.p_gate(v, params[f\"{prefix}_taumax\"])\n    )\n    return {f\"{prefix}_p\": new_p}\n
"},{"location":"reference/mechanisms/#jaxley.channels.pospischil.CaL.compute_current","title":"compute_current(states, v, params)","text":"

Return current.

Source code in jaxley/channels/pospischil.py
def compute_current(\n    self, states: Dict[str, jnp.ndarray], v, params: Dict[str, jnp.ndarray]\n):\n    \"\"\"Return current.\"\"\"\n    prefix = self._name\n    q, r = states[f\"{prefix}_q\"], states[f\"{prefix}_r\"]\n    gCaL = params[f\"{prefix}_gCaL\"] * (q**2) * r  # S/cm^2\n\n    return gCaL * (v - params[\"eCa\"])\n
"},{"location":"reference/mechanisms/#jaxley.channels.pospischil.CaL.init_state","title":"init_state(states, v, params, delta_t)","text":"

Initialize the state such at fixed point of gate dynamics.

Source code in jaxley/channels/pospischil.py
def init_state(self, states, v, params, delta_t):\n    \"\"\"Initialize the state such at fixed point of gate dynamics.\"\"\"\n    prefix = self._name\n    alpha_q, beta_q = self.q_gate(v)\n    alpha_r, beta_r = self.r_gate(v)\n    return {\n        f\"{prefix}_q\": alpha_q / (alpha_q + beta_q),\n        f\"{prefix}_r\": alpha_r / (alpha_r + beta_r),\n    }\n
"},{"location":"reference/mechanisms/#jaxley.channels.pospischil.CaL.update_states","title":"update_states(states, dt, v, params)","text":"

Update state.

Source code in jaxley/channels/pospischil.py
def update_states(\n    self,\n    states: Dict[str, jnp.ndarray],\n    dt,\n    v,\n    params: Dict[str, jnp.ndarray],\n):\n    \"\"\"Update state.\"\"\"\n    prefix = self._name\n    q, r = states[f\"{prefix}_q\"], states[f\"{prefix}_r\"]\n    new_q = solve_gate_exponential(q, dt, *self.q_gate(v))\n    new_r = solve_gate_exponential(r, dt, *self.r_gate(v))\n    return {f\"{prefix}_q\": new_q, f\"{prefix}_r\": new_r}\n
"},{"location":"reference/mechanisms/#jaxley.channels.pospischil.CaT.compute_current","title":"compute_current(states, v, params)","text":"

Return current.

Source code in jaxley/channels/pospischil.py
def compute_current(\n    self, states: Dict[str, jnp.ndarray], v, params: Dict[str, jnp.ndarray]\n):\n    \"\"\"Return current.\"\"\"\n    prefix = self._name\n    u = states[f\"{prefix}_u\"]\n    s_inf = 1.0 / (1.0 + save_exp(-(v + params[f\"{prefix}_vx\"] + 57.0) / 6.2))\n\n    gCaT = params[f\"{prefix}_gCaT\"] * (s_inf**2) * u  # S/cm^2\n\n    return gCaT * (v - params[\"eCa\"])\n
"},{"location":"reference/mechanisms/#jaxley.channels.pospischil.CaT.init_state","title":"init_state(states, v, params, delta_t)","text":"

Initialize the state such at fixed point of gate dynamics.

Source code in jaxley/channels/pospischil.py
def init_state(self, states, v, params, delta_t):\n    \"\"\"Initialize the state such at fixed point of gate dynamics.\"\"\"\n    prefix = self._name\n    alpha_u, beta_u = self.u_gate(v, params[f\"{prefix}_vx\"])\n    return {f\"{prefix}_u\": alpha_u / (alpha_u + beta_u)}\n
"},{"location":"reference/mechanisms/#jaxley.channels.pospischil.CaT.update_states","title":"update_states(states, dt, v, params)","text":"

Update state.

Source code in jaxley/channels/pospischil.py
def update_states(\n    self,\n    states: Dict[str, jnp.ndarray],\n    dt,\n    v,\n    params: Dict[str, jnp.ndarray],\n):\n    \"\"\"Update state.\"\"\"\n    prefix = self._name\n    u = states[f\"{prefix}_u\"]\n    new_u = solve_inf_gate_exponential(\n        u, dt, *self.u_gate(v, params[f\"{prefix}_vx\"])\n    )\n    return {f\"{prefix}_u\": new_u}\n
"},{"location":"reference/mechanisms/#synapses","title":"Synapses","text":""},{"location":"reference/mechanisms/#synapse","title":"Synapse","text":"

Base class for a synapse.

As in NEURON, a Synapse is considered a point process, which means that its conductances are to be specified in uS and its currents are to be specified in nA.

Source code in jaxley/synapses/synapse.py
class Synapse:\n    \"\"\"Base class for a synapse.\n\n    As in NEURON, a `Synapse` is considered a point process, which means that its\n    conductances are to be specified in `uS` and its currents are to be specified in\n    `nA`.\n    \"\"\"\n\n    _name = None\n    synapse_params = None\n    synapse_states = None\n\n    def __init__(self, name: Optional[str] = None):\n        self._name = name if name else self.__class__.__name__\n\n    @property\n    def name(self) -> Optional[str]:\n        return self._name\n\n    def change_name(self, new_name: str):\n        \"\"\"Change the synapse name.\n\n        Args:\n            new_name: The new name of the channel.\n\n        Returns:\n            Renamed channel, such that this function is chainable.\n        \"\"\"\n        old_prefix = self._name + \"_\"\n        new_prefix = new_name + \"_\"\n\n        self._name = new_name\n        self.synapse_params = {\n            (\n                new_prefix + key[len(old_prefix) :]\n                if key.startswith(old_prefix)\n                else key\n            ): value\n            for key, value in self.synapse_params.items()\n        }\n\n        self.synapse_states = {\n            (\n                new_prefix + key[len(old_prefix) :]\n                if key.startswith(old_prefix)\n                else key\n            ): value\n            for key, value in self.synapse_states.items()\n        }\n        return self\n\n    def update_states(\n        states: Dict[str, jnp.ndarray],\n        delta_t: float,\n        pre_voltage: jnp.ndarray,\n        post_voltage: jnp.ndarray,\n        params: Dict[str, jnp.ndarray],\n    ) -> Dict[str, jnp.ndarray]:\n        \"\"\"ODE update step.\n\n        Args:\n            states: States of the synapse.\n            delta_t: Time step in `ms`.\n            pre_voltage: Voltage of the presynaptic compartment, shape `()`.\n            post_voltage: Voltage of the postsynaptic compartment, shape `()`.\n            params: Parameters of the synapse. Conductances in `uS`.\n\n        Returns:\n            Updated states.\"\"\"\n        raise NotImplementedError\n\n    def compute_current(\n        states: Dict[str, jnp.ndarray],\n        pre_voltage: jnp.ndarray,\n        post_voltage: jnp.ndarray,\n        params: Dict[str, jnp.ndarray],\n    ) -> jnp.ndarray:\n        \"\"\"Return current through one synapse in `nA`.\n\n        Internally, we use `jax.vmap` to vectorize this function across many synapses.\n\n        Args:\n            states: States of the synapse.\n            pre_voltage: Voltage of the presynaptic compartment, shape `()`.\n            post_voltage: Voltage of the postsynaptic compartment, shape `()`.\n            params: Parameters of the synapse. Conductances in `uS`.\n\n        Returns:\n            Current through the synapse in `nA`, shape `()`.\n        \"\"\"\n        raise NotImplementedError\n
"},{"location":"reference/mechanisms/#jaxley.synapses.synapse.Synapse.change_name","title":"change_name(new_name)","text":"

Change the synapse name.

Parameters:

Name Type Description Default new_name str

The new name of the channel.

required

Returns:

Type Description

Renamed channel, such that this function is chainable.

Source code in jaxley/synapses/synapse.py
def change_name(self, new_name: str):\n    \"\"\"Change the synapse name.\n\n    Args:\n        new_name: The new name of the channel.\n\n    Returns:\n        Renamed channel, such that this function is chainable.\n    \"\"\"\n    old_prefix = self._name + \"_\"\n    new_prefix = new_name + \"_\"\n\n    self._name = new_name\n    self.synapse_params = {\n        (\n            new_prefix + key[len(old_prefix) :]\n            if key.startswith(old_prefix)\n            else key\n        ): value\n        for key, value in self.synapse_params.items()\n    }\n\n    self.synapse_states = {\n        (\n            new_prefix + key[len(old_prefix) :]\n            if key.startswith(old_prefix)\n            else key\n        ): value\n        for key, value in self.synapse_states.items()\n    }\n    return self\n
"},{"location":"reference/mechanisms/#jaxley.synapses.synapse.Synapse.compute_current","title":"compute_current(states, pre_voltage, post_voltage, params)","text":"

Return current through one synapse in nA.

Internally, we use jax.vmap to vectorize this function across many synapses.

Parameters:

Name Type Description Default states Dict[str, ndarray]

States of the synapse.

required pre_voltage ndarray

Voltage of the presynaptic compartment, shape ().

required post_voltage ndarray

Voltage of the postsynaptic compartment, shape ().

required params Dict[str, ndarray]

Parameters of the synapse. Conductances in uS.

required

Returns:

Type Description ndarray

Current through the synapse in nA, shape ().

Source code in jaxley/synapses/synapse.py
def compute_current(\n    states: Dict[str, jnp.ndarray],\n    pre_voltage: jnp.ndarray,\n    post_voltage: jnp.ndarray,\n    params: Dict[str, jnp.ndarray],\n) -> jnp.ndarray:\n    \"\"\"Return current through one synapse in `nA`.\n\n    Internally, we use `jax.vmap` to vectorize this function across many synapses.\n\n    Args:\n        states: States of the synapse.\n        pre_voltage: Voltage of the presynaptic compartment, shape `()`.\n        post_voltage: Voltage of the postsynaptic compartment, shape `()`.\n        params: Parameters of the synapse. Conductances in `uS`.\n\n    Returns:\n        Current through the synapse in `nA`, shape `()`.\n    \"\"\"\n    raise NotImplementedError\n
"},{"location":"reference/mechanisms/#jaxley.synapses.synapse.Synapse.update_states","title":"update_states(states, delta_t, pre_voltage, post_voltage, params)","text":"

ODE update step.

Parameters:

Name Type Description Default states Dict[str, ndarray]

States of the synapse.

required delta_t float

Time step in ms.

required pre_voltage ndarray

Voltage of the presynaptic compartment, shape ().

required post_voltage ndarray

Voltage of the postsynaptic compartment, shape ().

required params Dict[str, ndarray]

Parameters of the synapse. Conductances in uS.

required

Returns:

Type Description Dict[str, ndarray]

Updated states.

Source code in jaxley/synapses/synapse.py
def update_states(\n    states: Dict[str, jnp.ndarray],\n    delta_t: float,\n    pre_voltage: jnp.ndarray,\n    post_voltage: jnp.ndarray,\n    params: Dict[str, jnp.ndarray],\n) -> Dict[str, jnp.ndarray]:\n    \"\"\"ODE update step.\n\n    Args:\n        states: States of the synapse.\n        delta_t: Time step in `ms`.\n        pre_voltage: Voltage of the presynaptic compartment, shape `()`.\n        post_voltage: Voltage of the postsynaptic compartment, shape `()`.\n        params: Parameters of the synapse. Conductances in `uS`.\n\n    Returns:\n        Updated states.\"\"\"\n    raise NotImplementedError\n
"},{"location":"reference/mechanisms/#ionotropic-synapse","title":"Ionotropic Synapse","text":"

Bases: Synapse

Compute synaptic current and update synapse state for a generic ionotropic synapse.

The synapse state \u201cs\u201d is the probability that a postsynaptic receptor channel is open, and this depends on the amount of neurotransmitter released, which is in turn dependent on the presynaptic voltage.

The synaptic parameters are
  • gS: the maximal conductance across the postsynaptic membrane (uS)
  • e_syn: the reversal potential across the postsynaptic membrane (mV)
  • k_minus: the rate constant of neurotransmitter unbinding from the postsynaptic receptor (s^-1)
Details of this implementation can be found in the following book chapter

L. F. Abbott and E. Marder, \u201cModeling Small Networks,\u201d in Methods in Neuronal Modeling, C. Koch and I. Sergev, Eds. Cambridge: MIT Press, 1998.

Source code in jaxley/synapses/ionotropic.py
class IonotropicSynapse(Synapse):\n    \"\"\"\n    Compute synaptic current and update synapse state for a generic ionotropic synapse.\n\n    The synapse state \"s\" is the probability that a postsynaptic receptor channel is\n    open, and this depends on the amount of neurotransmitter released, which is in turn\n    dependent on the presynaptic voltage.\n\n    The synaptic parameters are:\n        - gS: the maximal conductance across the postsynaptic membrane (uS)\n        - e_syn: the reversal potential across the postsynaptic membrane (mV)\n        - k_minus: the rate constant of neurotransmitter unbinding from the postsynaptic\n            receptor (s^-1)\n\n    Details of this implementation can be found in the following book chapter:\n        L. F. Abbott and E. Marder, \"Modeling Small Networks,\" in Methods in Neuronal\n        Modeling, C. Koch and I. Sergev, Eds. Cambridge: MIT Press, 1998.\n\n    \"\"\"\n\n    def __init__(self, name: Optional[str] = None):\n        super().__init__(name)\n        prefix = self._name\n        self.synapse_params = {\n            f\"{prefix}_gS\": 1e-4,\n            f\"{prefix}_e_syn\": 0.0,\n            f\"{prefix}_k_minus\": 0.025,\n        }\n        self.synapse_states = {f\"{prefix}_s\": 0.2}\n\n    def update_states(\n        self,\n        states: Dict,\n        delta_t: float,\n        pre_voltage: float,\n        post_voltage: float,\n        params: Dict,\n    ) -> Dict:\n        \"\"\"Return updated synapse state and current.\"\"\"\n        prefix = self._name\n        v_th = -35.0  # mV\n        delta = 10.0  # mV\n\n        s_inf = 1.0 / (1.0 + save_exp((v_th - pre_voltage) / delta))\n        tau_s = (1.0 - s_inf) / params[f\"{prefix}_k_minus\"]\n\n        slope = -1.0 / tau_s\n        exp_term = save_exp(slope * delta_t)\n        new_s = states[f\"{prefix}_s\"] * exp_term + s_inf * (1.0 - exp_term)\n        return {f\"{prefix}_s\": new_s}\n\n    def compute_current(\n        self, states: Dict, pre_voltage: float, post_voltage: float, params: Dict\n    ) -> float:\n        prefix = self._name\n        g_syn = params[f\"{prefix}_gS\"] * states[f\"{prefix}_s\"]\n        return g_syn * (post_voltage - params[f\"{prefix}_e_syn\"])\n
"},{"location":"reference/mechanisms/#jaxley.synapses.ionotropic.IonotropicSynapse.update_states","title":"update_states(states, delta_t, pre_voltage, post_voltage, params)","text":"

Return updated synapse state and current.

Source code in jaxley/synapses/ionotropic.py
def update_states(\n    self,\n    states: Dict,\n    delta_t: float,\n    pre_voltage: float,\n    post_voltage: float,\n    params: Dict,\n) -> Dict:\n    \"\"\"Return updated synapse state and current.\"\"\"\n    prefix = self._name\n    v_th = -35.0  # mV\n    delta = 10.0  # mV\n\n    s_inf = 1.0 / (1.0 + save_exp((v_th - pre_voltage) / delta))\n    tau_s = (1.0 - s_inf) / params[f\"{prefix}_k_minus\"]\n\n    slope = -1.0 / tau_s\n    exp_term = save_exp(slope * delta_t)\n    new_s = states[f\"{prefix}_s\"] * exp_term + s_inf * (1.0 - exp_term)\n    return {f\"{prefix}_s\": new_s}\n
"},{"location":"reference/mechanisms/#tanh-rate-synapse","title":"TanH Rate Synapse","text":"

Bases: Synapse

Compute synaptic current for tanh synapse (no state).

Source code in jaxley/synapses/tanh_rate.py
class TanhRateSynapse(Synapse):\n    \"\"\"\n    Compute synaptic current for tanh synapse (no state).\n    \"\"\"\n\n    def __init__(self, name: Optional[str] = None):\n        super().__init__(name)\n        prefix = self._name\n        self.synapse_params = {\n            f\"{prefix}_gS\": 1e-4,\n            f\"{prefix}_x_offset\": -70.0,\n            f\"{prefix}_slope\": 1.0,\n        }\n        self.synapse_states = {}\n\n    def update_states(\n        self,\n        states: Dict,\n        delta_t: float,\n        pre_voltage: float,\n        post_voltage: float,\n        params: Dict,\n    ) -> Dict:\n        \"\"\"Return updated synapse state and current.\"\"\"\n        return {}\n\n    def compute_current(\n        self, states: Dict, pre_voltage: float, post_voltage: float, params: Dict\n    ) -> float:\n        \"\"\"Return updated synapse state and current.\"\"\"\n        prefix = self._name\n        current = (\n            -1\n            * params[f\"{prefix}_gS\"]\n            * jnp.tanh(\n                (pre_voltage - params[f\"{prefix}_x_offset\"]) * params[f\"{prefix}_slope\"]\n            )\n        )\n        return current\n
"},{"location":"reference/mechanisms/#jaxley.synapses.tanh_rate.TanhRateSynapse.compute_current","title":"compute_current(states, pre_voltage, post_voltage, params)","text":"

Return updated synapse state and current.

Source code in jaxley/synapses/tanh_rate.py
def compute_current(\n    self, states: Dict, pre_voltage: float, post_voltage: float, params: Dict\n) -> float:\n    \"\"\"Return updated synapse state and current.\"\"\"\n    prefix = self._name\n    current = (\n        -1\n        * params[f\"{prefix}_gS\"]\n        * jnp.tanh(\n            (pre_voltage - params[f\"{prefix}_x_offset\"]) * params[f\"{prefix}_slope\"]\n        )\n    )\n    return current\n
"},{"location":"reference/mechanisms/#jaxley.synapses.tanh_rate.TanhRateSynapse.update_states","title":"update_states(states, delta_t, pre_voltage, post_voltage, params)","text":"

Return updated synapse state and current.

Source code in jaxley/synapses/tanh_rate.py
def update_states(\n    self,\n    states: Dict,\n    delta_t: float,\n    pre_voltage: float,\n    post_voltage: float,\n    params: Dict,\n) -> Dict:\n    \"\"\"Return updated synapse state and current.\"\"\"\n    return {}\n
"},{"location":"reference/modules/","title":"Modules","text":""},{"location":"reference/modules/#module","title":"Module","text":"

Bases: ABC

Module base class.

Modules are everything that can be passed to jx.integrate, i.e. compartments, branches, cells, and networks.

This base class defines the scaffold for all jaxley modules (compartments, branches, cells, networks).

Modules can be traversed and modified using the at, cell, branch, comp, edge, and loc methods. The scope method can be used to toggle between global and local indices. Traversal of Modules will return a View of itself, that has a modified set of attributes, which only consider the part of the Module that is in view.

For developers: The above has consequences for how to operate on Module and which changes take affect where. The following guidelines should be followed (copied from View):

  1. We consider a Module to have everything in view.
  2. Views can display and keep track of how a module is traversed. But(!), do not support making changes or setting variables. This still has to be done in the base Module, i.e. self.base. In order to enssure that these changes only affects whatever is currently in view self._nodes_in_view, or self._edges_in_view among others have to be used. Operating on nodes currently in view can for example be done with self.base.node.loc[self._nodes_in_view].
  3. Every attribute of Module that changes based on what\u2019s in view, i.e. xyzr, needs to modified when View is instantiated. I.e. xyzr of cell.branch(0), should be [self.base.xyzr[0]] This could be achieved via: [self.base.xyzr[b] for b in self._branches_in_view].

For developers: If you want to add a new method to Module, here is an example of how to make methods of Module compatible with View:

.. code-block:: python

# Use data in view to return something.\ndef count_small_branches(self):\n    # no need to use self.base.attr + viewed indices,\n    # since no change is made to the attr in question (nodes)\n    comp_lens = self.nodes[\"length\"]\n    branch_lens = comp_lens.groupby(\"global_branch_index\").sum()\n    return np.sum(branch_lens < 10)\n\n# Change data in view.\ndef change_attr_in_view(self):\n    # changes to attrs have to be made via self.base.attr + viewed indices\n    a = func1(self.base.attr1[self._cells_in_view])\n    b = func2(self.base.attr2[self._edges_in_view])\n    self.base.attr3[self._branches_in_view] = a + b\n
Source code in jaxley/modules/base.py
class Module(ABC):\n    \"\"\"Module base class.\n\n    Modules are everything that can be passed to `jx.integrate`, i.e. compartments,\n    branches, cells, and networks.\n\n    This base class defines the scaffold for all jaxley modules (compartments,\n    branches, cells, networks).\n\n    Modules can be traversed and modified using the `at`, `cell`, `branch`, `comp`,\n    `edge`, and `loc` methods. The `scope` method can be used to toggle between\n    global and local indices. Traversal of Modules will return a `View` of itself,\n    that has a modified set of attributes, which only consider the part of the Module\n    that is in view.\n\n    For developers: The above has consequences for how to operate on `Module` and which\n    changes take affect where. The following guidelines should be followed (copied from\n    `View`):\n\n    1. We consider a Module to have everything in view.\n    2. Views can display and keep track of how a module is traversed. But(!),\n       do not support making changes or setting variables. This still has to be\n       done in the base Module, i.e. `self.base`. In order to enssure that these\n       changes only affects whatever is currently in view `self._nodes_in_view`,\n       or `self._edges_in_view` among others have to be used. Operating on nodes\n       currently in view can for example be done with\n       `self.base.node.loc[self._nodes_in_view]`.\n    3. Every attribute of Module that changes based on what's in view, i.e. `xyzr`,\n       needs to modified when View is instantiated. I.e. `xyzr` of `cell.branch(0)`,\n       should be `[self.base.xyzr[0]]` This could be achieved via:\n       `[self.base.xyzr[b] for b in self._branches_in_view]`.\n\n    For developers: If you want to add a new method to `Module`, here is an example of\n    how to make methods of Module compatible with View:\n\n    .. code-block:: python\n\n        # Use data in view to return something.\n        def count_small_branches(self):\n            # no need to use self.base.attr + viewed indices,\n            # since no change is made to the attr in question (nodes)\n            comp_lens = self.nodes[\"length\"]\n            branch_lens = comp_lens.groupby(\"global_branch_index\").sum()\n            return np.sum(branch_lens < 10)\n\n        # Change data in view.\n        def change_attr_in_view(self):\n            # changes to attrs have to be made via self.base.attr + viewed indices\n            a = func1(self.base.attr1[self._cells_in_view])\n            b = func2(self.base.attr2[self._edges_in_view])\n            self.base.attr3[self._branches_in_view] = a + b\n    \"\"\"\n\n    def __init__(self):\n        self.ncomp: int = None\n        self.total_nbranches: int = 0\n        self.nbranches_per_cell: List[int] = None\n\n        self.groups = {}\n\n        self.nodes: Optional[pd.DataFrame] = None\n        self._scope = \"local\"  # defaults to local scope\n        self._nodes_in_view: np.ndarray = None\n        self._edges_in_view: np.ndarray = None\n\n        self.edges = pd.DataFrame(\n            columns=[\n                \"global_edge_index\",\n                \"pre_global_comp_index\",\n                \"post_global_comp_index\",\n                \"pre_locs\",\n                \"post_locs\",\n                \"type\",\n                \"type_ind\",\n            ]\n        )\n\n        self._cumsum_nbranches: Optional[np.ndarray] = None\n\n        self.comb_parents: jnp.ndarray = jnp.asarray([-1])\n\n        self.initialized_morph: bool = False\n        self.initialized_syns: bool = False\n\n        # List of all types of `jx.Synapse`s.\n        self.synapses: List = []\n        self.synapse_param_names = []\n        self.synapse_state_names = []\n        self.synapse_names = []\n\n        # List of types of all `jx.Channel`s.\n        self.channels: List[Channel] = []\n        self.membrane_current_names: List[str] = []\n\n        # For trainable parameters.\n        self.indices_set_by_trainables: List[jnp.ndarray] = []\n        self.trainable_params: List[Dict[str, jnp.ndarray]] = []\n        self.allow_make_trainable: bool = True\n        self.num_trainable_params: int = 0\n\n        # For recordings.\n        self.recordings: pd.DataFrame = pd.DataFrame().from_dict({})\n\n        # For stimuli or clamps.\n        # E.g. `self.externals = {\"v\": zeros(1000,2), \"i\": ones(1000, 2)}`\n        # for 1000 timesteps and two compartments.\n        self.externals: Dict[str, jnp.ndarray] = {}\n        # E.g. `self.external)inds = {\"v\": jnp.asarray([0,1]), \"i\": jnp.asarray([2,3])}`\n        self.external_inds: Dict[str, jnp.ndarray] = {}\n\n        # x, y, z coordinates and radius.\n        self.xyzr: List[np.ndarray] = []\n        self._radius_generating_fns = None  # Defined by `.read_swc()`.\n\n        # For debugging the solver. Will be empty by default and only filled if\n        # `self._init_morph_for_debugging` is run.\n        self.debug_states = {}\n\n        # needs to be set at the end\n        self.base: Module = self\n\n    def __repr__(self):\n        return f\"{type(self).__name__} with {len(self.channels)} different channels. Use `.nodes` for details.\"\n\n    def __str__(self):\n        return f\"jx.{type(self).__name__}\"\n\n    def __dir__(self):\n        base_dir = object.__dir__(self)\n        return sorted(base_dir + self.synapse_names + list(self.group_nodes.keys()))\n\n    def __getattr__(self, key):\n        # Ensure that hidden methods such as `__deepcopy__` still work.\n        if key.startswith(\"__\"):\n            return super().__getattribute__(key)\n\n        # intercepts calls to groups\n        if key in self.base.groups:\n            view = (\n                self.select(self.groups[key])\n                if key in self.groups\n                else self.select(None)\n            )\n            view._set_controlled_by_param(key)\n            return view\n\n        # intercepts calls to channels\n        if key in [c._name for c in self.base.channels]:\n            channel_names = [c._name for c in self.channels]\n            inds = self.nodes.index[self.nodes[key]].to_numpy()\n            view = self.select(inds) if key in channel_names else self.select(None)\n            view._set_controlled_by_param(key)\n            return view\n\n        # intercepts calls to synapse types\n        if key in self.base.synapse_names:\n            syn_inds = self.edges[self.edges[\"type\"] == key][\n                \"global_edge_index\"\n            ].to_numpy()\n            orig_scope = self._scope\n            view = (\n                self.scope(\"global\").edge(syn_inds).scope(orig_scope)\n                if key in self.synapse_names\n                else self.select(None)\n            )\n            view._set_controlled_by_param(key)  # overwrites param set by edge\n            # Ensure synapse param sharing works with `edge`\n            # `edge` will be removed as part of #463\n            view.edges[\"local_edge_index\"] = np.arange(len(view.edges))\n            return view\n\n    def _childviews(self) -> List[str]:\n        \"\"\"Returns levels that module can be viewed at.\n\n        I.e. for net -> [cell, branch, comp]. For branch -> [comp]\"\"\"\n        levels = [\"network\", \"cell\", \"branch\", \"comp\"]\n        if self._current_view in levels:\n            children = levels[levels.index(self._current_view) + 1 :]\n            return children\n        return []\n\n    def _has_childview(self, key: str) -> bool:\n        child_views = self._childviews()\n        return key in child_views\n\n    def __getitem__(self, index):\n        \"\"\"Lazy indexing of the module.\"\"\"\n        supported_parents = [\"network\", \"cell\", \"branch\"]  # cannot index into comp\n\n        not_group_view = self._current_view not in self.groups\n        assert (\n            self._current_view in supported_parents or not_group_view\n        ), \"Lazy indexing is only supported for `Network`, `Cell`, `Branch` and Views thereof.\"\n        index = index if isinstance(index, tuple) else (index,)\n\n        child_views = self._childviews()\n        assert len(index) <= len(child_views), \"Too many indices.\"\n        view = self\n        for i, child in zip(index, child_views):\n            view = view._at_nodes(child, i)\n        return view\n\n    def _update_local_indices(self) -> pd.DataFrame:\n        \"\"\"Compute local indices from the global indices that are in view.\n        This is recomputed everytime a View is created.\"\"\"\n        rerank = lambda df: df.rank(method=\"dense\").astype(int) - 1\n\n        def reorder_cols(\n            df: pd.DataFrame, cols: List[str], first: bool = True\n        ) -> pd.DataFrame:\n            \"\"\"Move cols to front/back.\n\n            Args:\n                df: DataFrame to reorder.\n                cols: List of columns to place before/after remaining columns.\n                first: If True, cols are placed in front, otherwise at the end.\n\n            Returns:\n                DataFrame with reordered columns.\"\"\"\n            new_cols = [col for col in df.columns if first == (col in cols)]\n            new_cols += [col for col in df.columns if first != (col in cols)]\n            return df[new_cols]\n\n        def reindex_a_by_b(\n            df: pd.DataFrame, a: str, b: Optional[Union[str, List[str]]] = None\n        ) -> pd.DataFrame:\n            \"\"\"Reindex based on a different col or several columns\n            for b=[0,0,1,1,2,2,2] -> a=[0,1,0,1,0,1,2]\"\"\"\n            grouped_df = df.groupby(b) if b is not None else df\n            df.loc[:, a] = rerank(grouped_df[a])\n            return df\n\n        index_names = [\"cell_index\", \"branch_index\", \"comp_index\"]  # order is important\n        global_idx_cols = [f\"global_{name}\" for name in index_names]\n        local_idx_cols = [f\"local_{name}\" for name in index_names]\n        idcs = self.nodes[global_idx_cols]\n\n        # update local indices of nodes\n        idcs = reindex_a_by_b(idcs, global_idx_cols[0])\n        idcs = reindex_a_by_b(idcs, global_idx_cols[1], global_idx_cols[0])\n        idcs = reindex_a_by_b(idcs, global_idx_cols[2], global_idx_cols[:2])\n        idcs.columns = [col.replace(\"global\", \"local\") for col in global_idx_cols]\n        self.nodes[local_idx_cols] = idcs[local_idx_cols].astype(int)\n\n        # move indices to the front of the dataframe; move controlled_by_param to the end\n        # move indices of current scope to the front and the others to the back\n        not_scope = \"global\" if self._scope == \"local\" else \"local\"\n        self.nodes = reorder_cols(\n            self.nodes, [f\"{self._scope}_{name}\" for name in index_names], first=True\n        )\n        self.nodes = reorder_cols(\n            self.nodes, [f\"{not_scope}_{name}\" for name in index_names], first=False\n        )\n\n        self.edges = reorder_cols(self.edges, [\"global_edge_index\"])\n        self.nodes = reorder_cols(self.nodes, [\"controlled_by_param\"], first=False)\n        self.edges = reorder_cols(self.edges, [\"controlled_by_param\"], first=False)\n\n    def _init_view(self):\n        \"\"\"Init attributes critical for View.\n\n        Needs to be called at init of a Module.\"\"\"\n        parent = self.__class__.__name__.lower()\n        self._current_view = \"comp\" if parent == \"compartment\" else parent\n        self._nodes_in_view = self.nodes.index.to_numpy()\n        self._edges_in_view = self.edges.index.to_numpy()\n        self.nodes[\"controlled_by_param\"] = 0\n\n    def _compute_coords_of_comp_centers(self) -> np.ndarray:\n        \"\"\"Compute xyz coordinates of compartment centers.\n\n        Centers are the midpoint between the comparment endpoints on the morphology\n        as defined by xyzr.\n\n        Note: For sake of performance, interpolation is not done for each branch\n        individually, but only once along a concatenated (and padded) array of all branches.\n        This means for ncomps = [2,4] and normalized cum_branch_lens of [[0,1],[0,1]] we would\n        interpolate xyz at the locations comp_ends = [[0,0.5,1], [0,0.25,0.5,0.75,1]],\n        where 0 is the start of the branch and 1 is the end point at the full branch_len.\n        To avoid do this in one go we set comp_ends = [0,0.5,1,2,2.25,2.5,2.75,3], and\n        norm_cum_branch_len = [0,1,2,3] incrememting and also padding them by 1 to\n        avoid overlapping branch_lens i.e. norm_cum_branch_len = [0,1,1,2] for only\n        incrementing.\n        \"\"\"\n        nodes_by_branches = self.nodes.groupby(\"global_branch_index\")\n        ncomps = nodes_by_branches[\"global_comp_index\"].nunique().to_numpy()\n\n        comp_ends = [\n            np.linspace(0, 1, ncomp + 1) + 2 * i for i, ncomp in enumerate(ncomps)\n        ]\n        comp_ends = np.hstack(comp_ends)\n\n        comp_ends = comp_ends.reshape(-1)\n        cum_branch_lens = []\n        for i, xyzr in enumerate(self.xyzr):\n            branch_len = np.sqrt(np.sum(np.diff(xyzr[:, :3], axis=0) ** 2, axis=1))\n            cum_branch_len = np.cumsum(np.concatenate([np.array([0]), branch_len]))\n            max_len = cum_branch_len.max()\n            # add padding like above\n            cum_branch_len = cum_branch_len / (max_len if max_len > 0 else 1) + 2 * i\n            cum_branch_len[np.isnan(cum_branch_len)] = 0\n            cum_branch_lens.append(cum_branch_len)\n        cum_branch_lens = np.hstack(cum_branch_lens)\n        xyz = np.vstack(self.xyzr)[:, :3]\n        xyz = v_interp(comp_ends, cum_branch_lens, xyz).T\n        centers = (xyz[:-1] + xyz[1:]) / 2  # unaware of inter vs intra comp centers\n        cum_ncomps = np.cumsum(ncomps)\n        # this means centers between comps have to be removed here\n        between_comp_inds = (cum_ncomps + np.arange(len(cum_ncomps)))[:-1]\n        centers = np.delete(centers, between_comp_inds, axis=0)\n        return centers\n\n    def compute_compartment_centers(self):\n        \"\"\"Add compartment centers to nodes dataframe\"\"\"\n        centers = self._compute_coords_of_comp_centers()\n        self.base.nodes.loc[self._nodes_in_view, [\"x\", \"y\", \"z\"]] = centers\n\n    def _reformat_index(self, idx: Any, dtype: type = int) -> np.ndarray:\n        \"\"\"Transforms different types of indices into an array.\n\n        Takes slice, list, array, ints, range and None and transforms\n        it into array of indices. If index == \"all\" it returns \"all\"\n        to be handled downstream.\n\n        Args:\n            idx: index that specifies at which locations to view the module.\n            dtype: defaults to int, but can also reformat float for use in `loc`\n\n        Returns:\n            array of indices of shape (N,)\"\"\"\n        if is_str_all(idx):  # also asserts that the only allowed str == \"all\"\n            return idx\n\n        np_dtype = np.int64 if dtype is int else np.float64\n        idx = np.array([], dtype=dtype) if idx is None else idx\n        idx = np.array([idx]) if isinstance(idx, (dtype, np_dtype)) else idx\n        idx = np.array(idx) if isinstance(idx, (list, range, pd.Index)) else idx\n\n        idx = np.arange(len(self.base.nodes))[idx] if isinstance(idx, slice) else idx\n        if idx.dtype == bool:\n            shape = (*self.shape, len(self.edges))\n            which_idx = len(idx) == np.array(shape)\n            assert np.any(which_idx), \"Index not matching num of cells/branches/comps.\"\n            dim = shape[np.where(which_idx)[0][0]]\n            idx = np.arange(dim)[idx]\n        assert isinstance(idx, np.ndarray), \"Invalid type\"\n        assert idx.dtype in [np_dtype, bool], \"Invalid dtype\"\n        return idx.reshape(-1)\n\n    def _set_controlled_by_param(self, key: str):\n        \"\"\"Determines which parameters are shared in `make_trainable`.\n\n        Adds column to nodes/edges dataframes to read of shared params from.\n\n        Args:\n            key: key specifying group / view that is in control of the params.\"\"\"\n        if key in [\"comp\", \"branch\", \"cell\"]:\n            self.nodes[\"controlled_by_param\"] = self.nodes[f\"global_{key}_index\"]\n            self.edges[\"controlled_by_param\"] = 0\n        elif key == \"edge\":\n            self.edges[\"controlled_by_param\"] = np.arange(len(self.edges))\n        elif key == \"filter\":\n            self.nodes[\"controlled_by_param\"] = np.arange(len(self.nodes))\n            self.edges[\"controlled_by_param\"] = np.arange(len(self.edges))\n        else:\n            self.nodes[\"controlled_by_param\"] = 0\n            self.edges[\"controlled_by_param\"] = 0\n        self._current_view = key\n\n    def select(\n        self, nodes: np.ndarray = None, edges: np.ndarray = None, sorted: bool = False\n    ) -> View:\n        \"\"\"Return View of the module filtered by specific node or edges indices.\n\n        Args:\n            nodes: indices of nodes to view. If None, all nodes are viewed.\n            edges: indices of edges to view. If None, all edges are viewed.\n            sorted: if True, nodes and edges are sorted.\n\n        Returns:\n            View for subset of selected nodes and/or edges.\"\"\"\n\n        nodes = self._reformat_index(nodes) if nodes is not None else None\n        nodes = self._nodes_in_view if is_str_all(nodes) else nodes\n        nodes = np.sort(nodes) if sorted else nodes\n\n        edges = self._reformat_index(edges) if edges is not None else None\n        edges = self._edges_in_view if is_str_all(edges) else edges\n        edges = np.sort(edges) if sorted else edges\n\n        view = View(self, nodes, edges)\n        view._set_controlled_by_param(\"filter\")\n        return view\n\n    def set_scope(self, scope: str):\n        \"\"\"Toggle between \"global\" or \"local\" scope.\n\n        Determines if global or local indices are used for viewing the module.\n\n        Args:\n            scope: either \"global\" or \"local\".\"\"\"\n        assert scope in [\"global\", \"local\"], \"Invalid scope.\"\n        self._scope = scope\n\n    def scope(self, scope: str) -> View:\n        \"\"\"Return a View of the module with the specified scope.\n\n        For example `cell.scope(\"global\").branch(2).scope(\"local\").comp(1)`\n        will return the 1st compartment of branch 2.\n\n        Args:\n            scope: either \"global\" or \"local\".\n\n        Returns:\n            View with the specified scope.\"\"\"\n        view = self.view\n        view.set_scope(scope)\n        return view\n\n    def _at_nodes(self, key: str, idx: Any) -> View:\n        \"\"\"Return a View of the module filtering `nodes` by specified key and index.\n\n        Keys can be `cell`, `branch`, `comp` and determine which index is used to filter.\n        \"\"\"\n        base_name = self.base.__class__.__name__\n        assert self.base._has_childview(key), f\"{base_name} does not support {key}.\"\n        idx = self._reformat_index(idx)\n        idx = self.nodes[self._scope + f\"_{key}_index\"] if is_str_all(idx) else idx\n        where = self.nodes[self._scope + f\"_{key}_index\"].isin(idx)\n        inds = self.nodes.index[where].to_numpy()\n\n        view = View(self, nodes=inds)\n        view._set_controlled_by_param(key)\n        return view\n\n    def _at_edges(self, key: str, idx: Any) -> View:\n        \"\"\"Return a View of the module filtering `edges` by specified key and index.\n\n        Keys can be `pre`, `post`, `edge` and determine which index is used to filter.\n        \"\"\"\n        idx = self._reformat_index(idx)\n        idx = self.edges[self._scope + f\"_{key}_index\"] if is_str_all(idx) else idx\n        where = self.edges[self._scope + f\"_{key}_index\"].isin(idx)\n        inds = self.edges.index[where].to_numpy()\n\n        view = View(self, edges=inds)\n        view._set_controlled_by_param(key)\n        return view\n\n    def cell(self, idx: Any) -> View:\n        \"\"\"Return a View of the module at the selected cell(s).\n\n        Args:\n            idx: index of the cell to view.\n\n        Returns:\n            View of the module at the specified cell index.\"\"\"\n        return self._at_nodes(\"cell\", idx)\n\n    def branch(self, idx: Any) -> View:\n        \"\"\"Return a View of the module at the selected branches(s).\n\n        Args:\n            idx: index of the branch to view.\n\n        Returns:\n            View of the module at the specified branch index.\"\"\"\n        return self._at_nodes(\"branch\", idx)\n\n    def comp(self, idx: Any) -> View:\n        \"\"\"Return a View of the module at the selected compartments(s).\n\n        Args:\n            idx: index of the comp to view.\n\n        Returns:\n            View of the module at the specified compartment index.\"\"\"\n        return self._at_nodes(\"comp\", idx)\n\n    def edge(self, idx: Any) -> View:\n        \"\"\"Return a View of the module at the selected synapse edges(s).\n\n        Args:\n            idx: index of the edge to view.\n\n        Returns:\n            View of the module at the specified edge index.\"\"\"\n        return self._at_edges(\"edge\", idx)\n\n    def loc(self, at: Any) -> View:\n        \"\"\"Return a View of the module at the selected branch location(s).\n\n        Args:\n            at: location along the branch.\n\n        Returns:\n            View of the module at the specified branch location.\"\"\"\n        global_comp_idxs = []\n        for i in self._branches_in_view:\n            ncomp = self.base.ncomp_per_branch[i]\n            comp_locs = np.linspace(0, 1, ncomp)\n            at = comp_locs if is_str_all(at) else self._reformat_index(at, dtype=float)\n            comp_edges = np.linspace(0, 1 + 1e-10, ncomp + 1)\n            idx = np.digitize(at, comp_edges) - 1 + self.base.cumsum_ncomp[i]\n            global_comp_idxs.append(idx)\n        global_comp_idxs = np.concatenate(global_comp_idxs)\n        orig_scope = self._scope\n        # global scope needed to select correct comps, for i.e. branches w. ncomp=[1,2]\n        # loc(0.9)  will correspond to different local branches (0 vs 1).\n        view = self.scope(\"global\").comp(global_comp_idxs).scope(orig_scope)\n        view._current_view = \"loc\"\n        return view\n\n    @property\n    def _comps_in_view(self):\n        \"\"\"Lists the global compartment indices which are currently part of the view.\"\"\"\n        # method also exists in View. this copy forgoes need to instantiate a View\n        return self.nodes[\"global_comp_index\"].unique()\n\n    @property\n    def _branches_in_view(self):\n        \"\"\"Lists the global branch indices which are currently part of the view.\"\"\"\n        # method also exists in View. this copy forgoes need to instantiate a View\n        return self.nodes[\"global_branch_index\"].unique()\n\n    @property\n    def _cells_in_view(self):\n        \"\"\"Lists the global cell indices which are currently part of the view.\"\"\"\n        # method also exists in View. this copy forgoes need to instantiate a View\n        return self.nodes[\"global_cell_index\"].unique()\n\n    def _iter_submodules(self, name: str):\n        \"\"\"Iterate over submoduleslevel.\n\n        Used for `cells`, `branches`, `comps`.\"\"\"\n        col = self._scope + f\"_{name}_index\"\n        idxs = self.nodes[col].unique()\n        for idx in idxs:\n            yield self._at_nodes(name, idx)\n\n    @property\n    def cells(self):\n        \"\"\"Iterate over all cells in the module.\n\n        Returns a generator that yields a View of each cell.\"\"\"\n        yield from self._iter_submodules(\"cell\")\n\n    @property\n    def branches(self):\n        \"\"\"Iterate over all branches in the module.\n\n        Returns a generator that yields a View of each branch.\"\"\"\n        yield from self._iter_submodules(\"branch\")\n\n    @property\n    def comps(self):\n        \"\"\"Iterate over all compartments in the module.\n        Can be called on any module, i.e. `net.comps`, `cell.comps` or\n        `branch.comps`. `__iter__` does not allow for this.\n\n        Returns a generator that yields a View of each compartment.\"\"\"\n        yield from self._iter_submodules(\"comp\")\n\n    def __iter__(self):\n        \"\"\"Iterate over parts of the module.\n\n        Internally calls `cells`, `branches`, `comps` at the appropriate level.\n\n        Example:\n\n        .. code-block:: python\n\n            for cell in network:\n                for branch in cell:\n                    for comp in branch:\n                        print(comp.nodes.shape)\n        \"\"\"\n        next_level = self._childviews()[0]\n        yield from self._iter_submodules(next_level)\n\n    @property\n    def shape(self) -> Tuple[int]:\n        \"\"\"Returns the number of submodules contained in a module.\n\n        .. code-block:: python\n\n            network.shape = (num_cells, num_branches, num_compartments)\n            cell.shape = (num_branches, num_compartments)\n            branch.shape = (num_compartments,)\n        \"\"\"\n        cols = [\"global_cell_index\", \"global_branch_index\", \"global_comp_index\"]\n        raw_shape = self.nodes[cols].nunique().to_list()\n\n        # ensure (net.shape -> dim=3, cell.shape -> dim=2, branch.shape -> dim=1, comp.shape -> dim=0)\n        levels = [\"network\", \"cell\", \"branch\", \"comp\"]\n        module = self.base.__class__.__name__.lower()\n        module = \"comp\" if module == \"compartment\" else module\n        shape = tuple(raw_shape[levels.index(module) :])\n        return shape\n\n    def copy(\n        self, reset_index: bool = False, as_module: bool = False\n    ) -> Union[Module, View]:\n        \"\"\"Extract part of a module and return a copy of its View or a new module.\n\n        This can be used to call `jx.integrate` on part of a Module.\n\n        Args:\n            reset_index: if True, the indices of the new module are reset to start from 0.\n            as_module: if True, a new module is returned instead of a View.\n\n        Returns:\n            A part of the module or a copied view of it.\"\"\"\n        view = deepcopy(self)\n        warnings.warn(\"This method is experimental, use at your own risk.\")\n        # TODO FROM #447: add reset_index, i.e. for parents, nodes, edges etc. such that they\n        # start from 0/-1 and are contiguous\n        if as_module:\n            raise NotImplementedError(\"Not yet implemented.\")\n            # initialize a new module with the same attributes\n        return view\n\n    @property\n    def view(self):\n        \"\"\"Return view of the module.\"\"\"\n        return View(self, self._nodes_in_view, self._edges_in_view)\n\n    @property\n    def _module_type(self):\n        \"\"\"Return type of the module (compartment, branch, cell, network) as string.\n\n        This is used to perform asserts for some modules (e.g. network cannot use\n        `set_ncomp`) without having to import the module in `base.py`.\"\"\"\n        return self.__class__.__name__.lower()\n\n    def _append_params_and_states(self, param_dict: Dict, state_dict: Dict):\n        \"\"\"Insert the default params of the module (e.g. radius, length).\n\n        This is run at `__init__()`. It does not deal with channels.\n        \"\"\"\n        for param_name, param_value in param_dict.items():\n            self.base.nodes[param_name] = param_value\n        for state_name, state_value in state_dict.items():\n            self.base.nodes[state_name] = state_value\n\n    def _gather_channels_from_constituents(self, constituents: List):\n        \"\"\"Modify `self.channels` and `self.nodes` with channel info from constituents.\n\n        This is run at `__init__()`. It takes all branches of constituents (e.g.\n        of all branches when the are assembled into a cell) and adds columns to\n        `.nodes` for the relevant channels.\n        \"\"\"\n        for module in constituents:\n            for channel in module.channels:\n                if channel._name not in [c._name for c in self.channels]:\n                    self.base.channels.append(channel)\n                if channel.current_name not in self.membrane_current_names:\n                    self.base.membrane_current_names.append(channel.current_name)\n        # Setting columns of channel names to `False` instead of `NaN`.\n        for channel in self.base.channels:\n            name = channel._name\n            self.base.nodes.loc[self.nodes[name].isna(), name] = False\n\n    @only_allow_module\n    def to_jax(self):\n        # TODO FROM #447: Make this work for View?\n        \"\"\"Move `.nodes` to `.jaxnodes`.\n\n        Before the actual simulation is run (via `jx.integrate`), all parameters of\n        the `jx.Module` are stored in `.nodes` (a `pd.DataFrame`). However, for\n        simulation, these parameters have to be moved to be `jnp.ndarrays` such that\n        they can be processed on GPU/TPU and such that the simulation can be\n        differentiated. `.to_jax()` copies the `.nodes` to `.jaxnodes`.\n        \"\"\"\n        self.base.jaxnodes = {}\n        for key, value in self.base.nodes.to_dict(orient=\"list\").items():\n            inds = jnp.arange(len(value))\n            self.base.jaxnodes[key] = jnp.asarray(value)[inds]\n\n        # `jaxedges` contains only parameters (no indices).\n        # `jaxedges` contains only non-Nan elements. This is unlike the channels where\n        # we allow parameter sharing.\n        self.base.jaxedges = {}\n        edges = self.base.edges.to_dict(orient=\"list\")\n        for i, synapse in enumerate(self.base.synapses):\n            condition = np.asarray(edges[\"type_ind\"]) == i\n            for key in synapse.synapse_params:\n                self.base.jaxedges[key] = jnp.asarray(np.asarray(edges[key])[condition])\n            for key in synapse.synapse_states:\n                self.base.jaxedges[key] = jnp.asarray(np.asarray(edges[key])[condition])\n\n    def show(\n        self,\n        param_names: Optional[Union[str, List[str]]] = None,\n        *,\n        indices: bool = True,\n        params: bool = True,\n        states: bool = True,\n        channel_names: Optional[List[str]] = None,\n    ) -> pd.DataFrame:\n        \"\"\"Print detailed information about the Module or a view of it.\n\n        Args:\n            param_names: The names of the parameters to show. If `None`, all parameters\n                are shown.\n            indices: Whether to show the indices of the compartments.\n            params: Whether to show the parameters of the compartments.\n            states: Whether to show the states of the compartments.\n            channel_names: The names of the channels to show. If `None`, all channels are\n                shown.\n\n        Returns:\n            A `pd.DataFrame` with the requested information.\n        \"\"\"\n        nodes = self.nodes.copy()  # prevents this from being edited\n\n        cols = []\n        inds = [\"comp_index\", \"branch_index\", \"cell_index\"]\n        scopes = [\"local\", \"global\"]\n        inds = [f\"{s}_{i}\" for i in inds for s in scopes] if indices else []\n        cols += inds\n        cols += [ch._name for ch in self.channels] if channel_names else []\n        cols += (\n            sum([list(ch.channel_params) for ch in self.channels], []) if params else []\n        )\n        cols += (\n            sum([list(ch.channel_states) for ch in self.channels], []) if states else []\n        )\n\n        if not param_names is None:\n            cols = (\n                inds + [c for c in cols if c in param_names]\n                if params\n                else list(param_names)\n            )\n\n        return nodes[cols]\n\n    @only_allow_module\n    def _init_morph(self):\n        \"\"\"Initialize the morphology such that it can be processed by the solvers.\"\"\"\n        self._init_morph_jaxley_spsolve()\n        self._init_morph_jax_spsolve()\n        self.initialized_morph = True\n\n    @abstractmethod\n    def _init_morph_jax_spsolve(self):\n        \"\"\"Initialize the morphology for the JAX sparse solver.\"\"\"\n        raise NotImplementedError\n\n    @abstractmethod\n    def _init_morph_jaxley_spsolve(self):\n        \"\"\"Initialize the morphology for the custom Jaxley solver.\"\"\"\n        raise NotImplementedError\n\n    def _compute_axial_conductances(self, params: Dict[str, jnp.ndarray]):\n        \"\"\"Given radius, length, r_a, compute the axial coupling conductances.\"\"\"\n        return compute_axial_conductances(self._comp_edges, params)\n\n    def set(self, key: str, val: Union[float, jnp.ndarray]):\n        \"\"\"Set parameter of module (or its view) to a new value.\n\n        Note that this function can not be called within `jax.jit` or `jax.grad`.\n        Instead, it should be used set the parameters of the module **before** the\n        simulation. Use `.data_set()` to set parameters during `jax.jit` or\n        `jax.grad`.\n\n        Args:\n            key: The name of the parameter to set.\n            val: The value to set the parameter to. If it is `jnp.ndarray` then it\n                must be of shape `(len(num_compartments))`.\n        \"\"\"\n        if key in self.nodes.columns:\n            not_nan = ~self.nodes[key].isna().to_numpy()\n            self.base.nodes.loc[self._nodes_in_view[not_nan], key] = val\n        elif key in self.edges.columns:\n            not_nan = ~self.edges[key].isna().to_numpy()\n            self.base.edges.loc[self._edges_in_view[not_nan], key] = val\n        else:\n            raise KeyError(f\"Key '{key}' not found in nodes or edges\")\n\n    def data_set(\n        self,\n        key: str,\n        val: Union[float, jnp.ndarray],\n        param_state: Optional[List[Dict]],\n    ):\n        \"\"\"Set parameter of module (or its view) to a new value within `jit`.\n\n        Args:\n            key: The name of the parameter to set.\n            val: The value to set the parameter to. If it is `jnp.ndarray` then it\n                must be of shape `(len(num_compartments))`.\n            param_state: State of the setted parameters, internally used such that this\n                function does not modify global state.\n        \"\"\"\n        # Note: `data_set` does not support arrays for `val`.\n        is_node_param = key in self.nodes.columns\n        data = self.nodes if is_node_param else self.edges\n        viewed_inds = self._nodes_in_view if is_node_param else self._edges_in_view\n        if key in data.columns:\n            not_nan = ~data[key].isna()\n            added_param_state = [\n                {\n                    \"indices\": np.atleast_2d(viewed_inds[not_nan]),\n                    \"key\": key,\n                    \"val\": jnp.atleast_1d(jnp.asarray(val)),\n                }\n            ]\n            if param_state is not None:\n                param_state += added_param_state\n            else:\n                param_state = added_param_state\n        else:\n            raise KeyError(\"Key not recognized.\")\n        return param_state\n\n    def set_ncomp(\n        self,\n        ncomp: int,\n        min_radius: Optional[float] = None,\n    ):\n        \"\"\"Set the number of compartments with which the branch is discretized.\n\n        Args:\n            ncomp: The number of compartments that the branch should be discretized\n                into.\n            min_radius: Only used if the morphology was read from an SWC file. If passed\n                the radius is capped to be at least this value.\n\n        Raises:\n            - When there are stimuli in any compartment in the module.\n            - When there are recordings in any compartment in the module.\n            - When the channels of the compartments are not the same within the branch\n            that is modified.\n            - When the lengths of the compartments are not the same within the branch\n            that is modified.\n            - Unless the morphology was read from an SWC file, when the radiuses of the\n            compartments are not the same within the branch that is modified.\n        \"\"\"\n        assert len(self.base.externals) == 0, \"No stimuli allowed!\"\n        assert len(self.base.recordings) == 0, \"No recordings allowed!\"\n        assert len(self.base.trainable_params) == 0, \"No trainables allowed!\"\n\n        assert self.base._module_type != \"network\", \"This is not allowed for networks.\"\n        assert not (\n            self.base._module_type == \"cell\"\n            and len(self._branches_in_view) == len(self.base._branches_in_view)\n        ), \"This is not allowed for cells.\"\n\n        # Update all attributes that are affected by compartment structure.\n        view = self.nodes.copy()\n        all_nodes = self.base.nodes\n        start_idx = self.nodes[\"global_comp_index\"].to_numpy()[0]\n        ncomp_per_branch = self.base.ncomp_per_branch\n        channel_names = [c._name for c in self.base.channels]\n        channel_param_names = list(\n            chain(*[c.channel_params for c in self.base.channels])\n        )\n        channel_state_names = list(\n            chain(*[c.channel_states for c in self.base.channels])\n        )\n        radius_generating_fns = self.base._radius_generating_fns\n\n        within_branch_radiuses = view[\"radius\"].to_numpy()\n        compartment_lengths = view[\"length\"].to_numpy()\n        num_previous_ncomp = len(within_branch_radiuses)\n        branch_indices = pd.unique(view[\"global_branch_index\"])\n\n        error_msg = lambda name: (\n            f\"You previously modified the {name} of individual compartments, but \"\n            f\"now you are modifying the number of compartments in this branch. \"\n            f\"This is not allowed. First build the morphology with `set_ncomp()` and \"\n            f\"then modify the radiuses and lengths of compartments.\"\n        )\n\n        if (\n            ~np.all(within_branch_radiuses == within_branch_radiuses[0])\n            and radius_generating_fns is None\n        ):\n            raise ValueError(error_msg(\"radius\"))\n\n        for property_name in [\"length\", \"capacitance\", \"axial_resistivity\"]:\n            compartment_properties = view[property_name].to_numpy()\n            if ~np.all(compartment_properties == compartment_properties[0]):\n                raise ValueError(error_msg(property_name))\n\n        if not (self.nodes[channel_names].var() == 0.0).all():\n            raise ValueError(\n                \"Some channel exists only in some compartments of the branch which you\"\n                \"are trying to modify. This is not allowed. First specify the number\"\n                \"of compartments with `.set_ncomp()` and then insert the channels\"\n                \"accordingly.\"\n            )\n\n        if not (\n            self.nodes[channel_param_names + channel_state_names].var() == 0.0\n        ).all():\n            raise ValueError(\n                \"Some channel has different parameters or states between the \"\n                \"different compartments of the branch which you are trying to modify. \"\n                \"This is not allowed. First specify the number of compartments with \"\n                \"`.set_ncomp()` and then insert the channels accordingly.\"\n            )\n\n        # Add new rows as the average of all rows. Special case for the length is below.\n        average_row = self.nodes.mean(skipna=False)\n        average_row = average_row.to_frame().T\n        view = pd.concat([*[average_row] * ncomp], axis=\"rows\")\n\n        # Set the correct datatype after having performed an average which cast\n        # everything to float.\n        integer_cols = [\"global_cell_index\", \"global_branch_index\", \"global_comp_index\"]\n        view[integer_cols] = view[integer_cols].astype(int)\n\n        # Whether or not a channel exists in a compartment is a boolean.\n        boolean_cols = channel_names\n        view[boolean_cols] = view[boolean_cols].astype(bool)\n\n        # Special treatment for the lengths and radiuses. These are not being set as\n        # the average because we:\n        # 1) Want to maintain the total length of a branch.\n        # 2) Want to use the SWC inferred radius.\n        #\n        # Compute new compartment lengths.\n        comp_lengths = np.sum(compartment_lengths) / ncomp\n        view[\"length\"] = comp_lengths\n\n        # Compute new compartment radiuses.\n        if radius_generating_fns is not None:\n            view[\"radius\"] = build_radiuses_from_xyzr(\n                radius_fns=radius_generating_fns,\n                branch_indices=branch_indices,\n                min_radius=min_radius,\n                ncomp=ncomp,\n            )\n        else:\n            view[\"radius\"] = within_branch_radiuses[0] * np.ones(ncomp)\n\n        # Update `.nodes`.\n        # 1) Delete N rows starting from start_idx\n        number_deleted = num_previous_ncomp\n        all_nodes = all_nodes.drop(index=range(start_idx, start_idx + number_deleted))\n\n        # 2) Insert M new rows at the same location\n        df1 = all_nodes.iloc[:start_idx]  # Rows before the insertion point\n        df2 = all_nodes.iloc[start_idx:]  # Rows after the insertion point\n\n        # 3) Combine the parts: before, new rows, and after\n        all_nodes = pd.concat([df1, view, df2]).reset_index(drop=True)\n\n        # Override `comp_index` to just be a consecutive list.\n        all_nodes[\"global_comp_index\"] = np.arange(len(all_nodes))\n\n        # Update compartment structure arguments.\n        ncomp_per_branch[branch_indices] = ncomp\n        ncomp = int(np.max(ncomp_per_branch))\n        cumsum_ncomp = cumsum_leading_zero(ncomp_per_branch)\n        internal_node_inds = np.arange(cumsum_ncomp[-1])\n\n        self.base.nodes = all_nodes\n        self.base.ncomp_per_branch = ncomp_per_branch\n        self.base.ncomp = ncomp\n        self.base.cumsum_ncomp = cumsum_ncomp\n        self.base._internal_node_inds = internal_node_inds\n\n        # Update the morphology indexing (e.g., `.comp_edges`).\n        self.base._initialize()\n        self.base._init_view()\n        self.base._update_local_indices()\n\n    def make_trainable(\n        self,\n        key: str,\n        init_val: Optional[Union[float, list]] = None,\n        verbose: bool = True,\n    ):\n        \"\"\"Make a parameter trainable.\n\n        If a parameter is made trainable, it will be returned by `get_parameters()`\n        and should then be passed to `jx.integrate(..., params=params)`.\n\n        Args:\n            key: Name of the parameter to make trainable.\n            init_val: Initial value of the parameter. If `float`, the same value is\n                used for every created parameter. If `list`, the length of the list has\n                to match the number of created parameters. If `None`, the current\n                parameter value is used and if parameter sharing is performed that the\n                current parameter value is averaged over all shared parameters.\n            verbose: Whether to print the number of parameters that are added and the\n                total number of parameters.\n        \"\"\"\n        assert (\n            self.allow_make_trainable\n        ), \"network.cell('all').make_trainable() is not supported. Use a for-loop over cells.\"\n        ncomps_per_branch = (\n            self.base.nodes[\"global_branch_index\"].value_counts().to_numpy()\n        )\n        assert np.all(\n            ncomps_per_branch == ncomps_per_branch[0]\n        ), \"Parameter sharing is not allowed for modules containing branches with different numbers of compartments.\"\n\n        data = self.nodes if key in self.nodes.columns else None\n        data = self.edges if key in self.edges.columns else data\n\n        assert data is not None, f\"Key '{key}' not found in nodes or edges\"\n        not_nan = ~data[key].isna()\n        data = data.loc[not_nan]\n        assert (\n            len(data) > 0\n        ), \"No settable parameters found in the selected compartments.\"\n\n        grouped_view = data.groupby(\"controlled_by_param\")\n        # Because of this `x.index.values` we cannot support `make_trainable()` on\n        # the module level for synapse parameters (but only for `SynapseView`).\n        inds_of_comps = list(\n            grouped_view.apply(lambda x: x.index.values, include_groups=False)\n        )\n        indices_per_param = jnp.stack(inds_of_comps)\n        # Sorted inds are only used to infer the correct starting values.\n        param_vals = jnp.asarray(\n            [data.loc[inds, key].to_numpy() for inds in inds_of_comps]\n        )\n\n        # Set the value which the trainable parameter should take.\n        num_created_parameters = len(indices_per_param)\n        if init_val is not None:\n            if isinstance(init_val, float):\n                new_params = jnp.asarray([init_val] * num_created_parameters)\n            elif isinstance(init_val, list):\n                assert (\n                    len(init_val) == num_created_parameters\n                ), f\"len(init_val)={len(init_val)}, but trying to create {num_created_parameters} parameters.\"\n                new_params = jnp.asarray(init_val)\n            else:\n                raise ValueError(\n                    f\"init_val must a float, list, or None, but it is a {type(init_val).__name__}.\"\n                )\n        else:\n            new_params = jnp.mean(param_vals, axis=1)\n        self.base.trainable_params.append({key: new_params})\n        self.base.indices_set_by_trainables.append(indices_per_param)\n        self.base.num_trainable_params += num_created_parameters\n        if verbose:\n            print(\n                f\"Number of newly added trainable parameters: {num_created_parameters}. Total number of trainable parameters: {self.base.num_trainable_params}\"\n            )\n\n    def write_trainables(self, trainable_params: List[Dict[str, jnp.ndarray]]):\n        \"\"\"Write the trainables into `.nodes` and `.edges`.\n\n        This allows to, e.g., visualize trained networks with `.vis()`.\n\n        Args:\n            trainable_params: The trainable parameters returned by `get_parameters()`.\n        \"\"\"\n        # We do not support views. Why? `jaxedges` does not have any NaN\n        # elements, whereas edges does. Because of this, we already need special\n        # treatment to make this function work, and it would be an even bigger hassle\n        # if we wanted to support this.\n        assert self.__class__.__name__ in [\n            \"Compartment\",\n            \"Branch\",\n            \"Cell\",\n            \"Network\",\n        ], \"Only supports modules.\"\n\n        # We could also implement this without casting the module to jax.\n        # However, I think it allows us to reuse as much code as possible and it avoids\n        # any kind of issues with indexing or parameter sharing (as this is fully\n        # taken care of by `get_all_parameters()`).\n        self.base.to_jax()\n        pstate = params_to_pstate(trainable_params, self.base.indices_set_by_trainables)\n        all_params = self.base.get_all_parameters(pstate, voltage_solver=\"jaxley.stone\")\n\n        # The value for `delta_t` does not matter here because it is only used to\n        # compute the initial current. However, the initial current cannot be made\n        # trainable and so its value never gets used below.\n        all_states = self.base.get_all_states(pstate, all_params, delta_t=0.025)\n\n        # Loop only over the keys in `pstate` to avoid unnecessary computation.\n        for parameter in pstate:\n            key = parameter[\"key\"]\n            if key in self.base.nodes.columns:\n                vals_to_set = all_params if key in all_params.keys() else all_states\n                self.base.nodes[key] = vals_to_set[key]\n\n        # `jaxedges` contains only non-Nan elements. This is unlike the channels where\n        # we allow parameter sharing.\n        edges = self.base.edges.to_dict(orient=\"list\")\n        for i, synapse in enumerate(self.base.synapses):\n            condition = np.asarray(edges[\"type_ind\"]) == i\n            for key in list(synapse.synapse_params.keys()):\n                self.base.edges.loc[condition, key] = all_params[key]\n            for key in list(synapse.synapse_states.keys()):\n                self.base.edges.loc[condition, key] = all_states[key]\n\n    def distance(self, endpoint: \"View\") -> float:\n        \"\"\"Return the direct distance between two compartments.\n        This does not compute the pathwise distance (which is currently not\n        implemented).\n        Args:\n            endpoint: The compartment to which to compute the distance to.\n        \"\"\"\n        assert len(self.xyzr) == 1 and len(endpoint.xyzr) == 1\n        start_xyz = np.mean(self.xyzr[0][:, :3], axis=0)\n        end_xyz = np.mean(endpoint.xyzr[0][:, :3], axis=0)\n        return np.sqrt(np.sum((start_xyz - end_xyz) ** 2))\n\n    def delete_trainables(self):\n        \"\"\"Removes all trainable parameters from the module.\"\"\"\n\n        if isinstance(self, View):\n            trainables_and_inds = self._filter_trainables(is_viewed=False)\n            self.base.indices_set_by_trainables = trainables_and_inds[0]\n            self.base.trainable_params = trainables_and_inds[1]\n            self.base.num_trainable_params -= self.num_trainable_params\n        else:\n            self.base.indices_set_by_trainables = []\n            self.base.trainable_params = []\n            self.base.num_trainable_params = 0\n        self._update_view()\n\n    def add_to_group(self, group_name: str):\n        \"\"\"Add a view of the module to a group.\n\n        Groups can then be indexed. For example:\n\n        .. code-block:: python\n\n            net.cell(0).add_to_group(\"excitatory\")\n            net.excitatory.set(\"radius\", 0.1)\n\n        Args:\n            group_name: The name of the group.\n        \"\"\"\n        if group_name not in self.base.groups:\n            self.base.groups[group_name] = self._nodes_in_view\n        else:\n            self.base.groups[group_name] = np.unique(\n                np.concatenate([self.base.groups[group_name], self._nodes_in_view])\n            )\n\n    def _get_state_names(self) -> Tuple[List, List]:\n        \"\"\"Collect all recordable / clampable states in the membrane and synapses.\n\n        Returns states seperated by comps and edges.\"\"\"\n        channel_states = [name for c in self.channels for name in c.channel_states]\n        synapse_states = [name for s in self.synapses for name in s.synapse_states]\n        membrane_states = [\"v\", \"i\"] + self.membrane_current_names\n        return channel_states + membrane_states, synapse_states\n\n    def get_parameters(self) -> List[Dict[str, jnp.ndarray]]:\n        \"\"\"Get all trainable parameters.\n\n        The returned parameters should be passed to `jx.integrate(..., params=params).\n\n        Returns:\n            A list of all trainable parameters in the form of\n                [{\"gNa\": jnp.array([0.1, 0.2, 0.3])}, ...].\n        \"\"\"\n        return self.trainable_params\n\n    @only_allow_module\n    def get_all_parameters(\n        self, pstate: List[Dict], voltage_solver: str\n    ) -> Dict[str, jnp.ndarray]:\n        # TODO FROM #447: MAKE THIS WORK FOR VIEW?\n        \"\"\"Return all parameters (and coupling conductances) needed to simulate.\n\n        Runs `_compute_axial_conductances()` and return every parameter that is needed\n        to solve the ODE. This includes conductances, radiuses, lengths,\n        axial_resistivities, but also coupling conductances.\n\n        This is done by first obtaining the current value of every parameter (not only\n        the trainable ones) and then replacing the trainable ones with the value\n        in `trainable_params()`. This function is run within `jx.integrate()`.\n\n        pstate can be obtained by calling `params_to_pstate()`.\n\n        .. code-block:: python\n\n            params = module.get_parameters() # i.e. [0, 1, 2]\n            pstate = params_to_pstate(params, module.indices_set_by_trainables)\n            module.to_jax() # needed for call to module.jaxnodes\n\n        Args:\n            pstate: The state of the trainable parameters. pstate takes the form\n                [{\n                    \"key\": \"gNa\", \"indices\": jnp.array([0, 1, 2]),\n                    \"val\": jnp.array([0.1, 0.2, 0.3])\n                }, ...].\n            voltage_solver: The voltage solver that is used. Since `jax.sparse` and\n                `jaxley.xyz` require different formats of the axial conductances, this\n                function will default to different building methods.\n\n        Returns:\n            A dictionary of all module parameters.\n        \"\"\"\n        params = {}\n        for key in [\"radius\", \"length\", \"axial_resistivity\", \"capacitance\"]:\n            params[key] = self.base.jaxnodes[key]\n\n        for channel in self.base.channels:\n            for channel_params in channel.channel_params:\n                params[channel_params] = self.base.jaxnodes[channel_params]\n\n        for synapse_params in self.base.synapse_param_names:\n            params[synapse_params] = self.base.jaxedges[synapse_params]\n\n        # Override with those parameters set by `.make_trainable()`.\n        for parameter in pstate:\n            key = parameter[\"key\"]\n            inds = parameter[\"indices\"]\n            set_param = parameter[\"val\"]\n\n            # This is needed since SynapseViews worked differently before.\n            # This mimics the old behaviour and tranformes the new indices\n            # to the old indices.\n            # TODO FROM #447: Longterm this should be gotten rid of.\n            # Instead edges should work similar to nodes (would also allow for\n            # param sharing).\n            synapse_inds = self.base.edges.groupby(\"type\").rank()[\"global_edge_index\"]\n            synapse_inds = (synapse_inds.astype(int) - 1).to_numpy()\n            if key in self.base.synapse_param_names:\n                inds = synapse_inds[inds]\n\n            if key in params:  # Only parameters, not initial states.\n                # `inds` is of shape `(num_params, num_comps_per_param)`.\n                # `set_param` is of shape `(num_params,)`\n                # We need to unsqueeze `set_param` to make it `(num_params, 1)` for the\n                # `.set()` to work. This is done with `[:, None]`.\n                params[key] = params[key].at[inds].set(set_param[:, None])\n\n        # Compute conductance params and add them to the params dictionary.\n        params[\"axial_conductances\"] = self.base._compute_axial_conductances(\n            params=params\n        )\n        return params\n\n    @only_allow_module\n    def _get_states_from_nodes_and_edges(self) -> Dict[str, jnp.ndarray]:\n        # TODO FROM #447: MAKE THIS WORK FOR VIEW?\n        \"\"\"Return states as they are set in the `.nodes` and `.edges` tables.\"\"\"\n        self.base.to_jax()  # Create `.jaxnodes` from `.nodes` and `.jaxedges` from `.edges`.\n        states = {\"v\": self.base.jaxnodes[\"v\"]}\n        # Join node and edge states into a single state dictionary.\n        for channel in self.base.channels:\n            for channel_states in channel.channel_states:\n                states[channel_states] = self.base.jaxnodes[channel_states]\n        for synapse_states in self.base.synapse_state_names:\n            states[synapse_states] = self.base.jaxedges[synapse_states]\n        return states\n\n    @only_allow_module\n    def get_all_states(\n        self, pstate: List[Dict], all_params, delta_t: float\n    ) -> Dict[str, jnp.ndarray]:\n        # TODO FROM #447: MAKE THIS WORK FOR VIEW?\n        \"\"\"Get the full initial state of the module from jaxnodes and trainables.\n\n        Args:\n            pstate: The state of the trainable parameters.\n            all_params: All parameters of the module.\n            delta_t: The time step.\n\n        Returns:\n            A dictionary of all states of the module.\n        \"\"\"\n        states = self.base._get_states_from_nodes_and_edges()\n\n        # Override with the initial states set by `.make_trainable()`.\n        for parameter in pstate:\n            key = parameter[\"key\"]\n            inds = parameter[\"indices\"]\n            set_param = parameter[\"val\"]\n            if key in states:  # Only initial states, not parameters.\n                # `inds` is of shape `(num_params, num_comps_per_param)`.\n                # `set_param` is of shape `(num_params,)`\n                # We need to unsqueeze `set_param` to make it `(num_params, 1)` for the\n                # `.set()` to work. This is done with `[:, None]`.\n                states[key] = states[key].at[inds].set(set_param[:, None])\n\n        # Add to the states the initial current through every channel.\n        states, _ = self.base._channel_currents(\n            states, delta_t, self.channels, self.nodes, all_params\n        )\n\n        # Add to the states the initial current through every synapse.\n        states, _ = self.base._synapse_currents(\n            states, self.synapses, all_params, delta_t, self.edges\n        )\n        return states\n\n    @property\n    def initialized(self) -> bool:\n        \"\"\"Whether the `Module` is ready to be solved or not.\"\"\"\n        return self.initialized_morph\n\n    def _initialize(self):\n        \"\"\"Initialize the module.\"\"\"\n        self._init_morph()\n        return self\n\n    @only_allow_module\n    def init_states(self, delta_t: float = 0.025):\n        # TODO FROM #447: MAKE THIS WORK FOR VIEW?\n        \"\"\"Initialize all mechanisms in their steady state.\n\n        This considers the voltages and parameters of each compartment.\n\n        Args:\n            delta_t: Passed on to `channel.init_state()`.\n        \"\"\"\n        # Update states of the channels.\n        channel_nodes = self.base.nodes\n        states = self.base._get_states_from_nodes_and_edges()\n\n        # We do not use any `pstate` for initializing. In principle, we could change\n        # that by allowing an input `params` and `pstate` to this function.\n        # `voltage_solver` could also be `jax.sparse` here, because both of them\n        # build the channel parameters in the same way.\n        params = self.base.get_all_parameters([], voltage_solver=\"jaxley.thomas\")\n\n        for channel in self.base.channels:\n            name = channel._name\n            channel_indices = channel_nodes.loc[channel_nodes[name]][\n                \"global_comp_index\"\n            ].to_numpy()\n            voltages = channel_nodes.loc[channel_indices, \"v\"].to_numpy()\n\n            channel_param_names = list(channel.channel_params.keys())\n            channel_state_names = list(channel.channel_states.keys())\n            channel_states = query_channel_states_and_params(\n                states, channel_state_names, channel_indices\n            )\n            channel_params = query_channel_states_and_params(\n                params, channel_param_names, channel_indices\n            )\n\n            init_state = channel.init_state(\n                channel_states, voltages, channel_params, delta_t\n            )\n\n            # `init_state` might not return all channel states. Only the ones that are\n            # returned are updated here.\n            for key, val in init_state.items():\n                # Note that we are overriding `self.nodes` here, but `self.nodes` is\n                # not used above to actually compute the current states (so there are\n                # no issues with overriding states).\n                self.nodes.loc[channel_indices, key] = val\n\n    def _init_morph_for_debugging(self):\n        \"\"\"Instandiates row and column inds which can be used to solve the voltage eqs.\n\n        This is important only for expert users who try to modify the solver for the\n        voltage equations. By default, this function is never run.\n\n        This is useful for debugging the solver because one can use\n        `scipy.linalg.sparse.spsolve` after every step of the solve.\n\n        Here is the code snippet that can be used for debugging then (to be inserted in\n        `solver_voltage`):\n        ```python\n        from scipy.sparse import csc_matrix\n        from scipy.sparse.linalg import spsolve\n        from jaxley.utils.debug_solver import build_voltage_matrix_elements\n\n        elements, solve, num_entries, start_ind_for_branchpoints = (\n            build_voltage_matrix_elements(\n                uppers,\n                lowers,\n                diags,\n                solves,\n                branchpoint_conds_children[debug_states[\"child_inds\"]],\n                branchpoint_conds_parents[debug_states[\"par_inds\"]],\n                branchpoint_weights_children[debug_states[\"child_inds\"]],\n                branchpoint_weights_parents[debug_states[\"par_inds\"]],\n                branchpoint_diags,\n                branchpoint_solves,\n                debug_states[\"ncomp\"],\n                nbranches,\n            )\n        )\n        sparse_matrix = csc_matrix(\n            (elements, (debug_states[\"row_inds\"], debug_states[\"col_inds\"])),\n            shape=(num_entries, num_entries),\n        )\n        solution = spsolve(sparse_matrix, solve)\n        solution = solution[:start_ind_for_branchpoints]  # Delete branchpoint voltages.\n        solves = jnp.reshape(solution, (debug_states[\"ncomp\"], nbranches))\n        return solves\n        ```\n        \"\"\"\n        # For scipy and jax.scipy.\n        row_and_col_inds = compute_morphology_indices(\n            len(self.base._par_inds),\n            self.base._child_belongs_to_branchpoint,\n            self.base._par_inds,\n            self.base._child_inds,\n            self.base.ncomp,\n            self.base.total_nbranches,\n        )\n\n        num_elements = len(row_and_col_inds[\"row_inds\"])\n        data_inds, indices, indptr = convert_to_csc(\n            num_elements=num_elements,\n            row_ind=row_and_col_inds[\"row_inds\"],\n            col_ind=row_and_col_inds[\"col_inds\"],\n        )\n        self.base.debug_states[\"row_inds\"] = row_and_col_inds[\"row_inds\"]\n        self.base.debug_states[\"col_inds\"] = row_and_col_inds[\"col_inds\"]\n        self.base.debug_states[\"data_inds\"] = data_inds\n        self.base.debug_states[\"indices\"] = indices\n        self.base.debug_states[\"indptr\"] = indptr\n\n        self.base.debug_states[\"ncomp\"] = self.base.ncomp\n        self.base.debug_states[\"child_inds\"] = self.base._child_inds\n        self.base.debug_states[\"par_inds\"] = self.base._par_inds\n\n    def record(self, state: str = \"v\", verbose=True):\n        comp_states, edge_states = self._get_state_names()\n        if state not in comp_states + edge_states:\n            raise KeyError(f\"{state} is not a recognized state in this module.\")\n        in_view = self._nodes_in_view if state in comp_states else self._edges_in_view\n\n        new_recs = pd.DataFrame(in_view, columns=[\"rec_index\"])\n        new_recs[\"state\"] = state\n        self.base.recordings = pd.concat([self.base.recordings, new_recs])\n        has_duplicates = self.base.recordings.duplicated()\n        self.base.recordings = self.base.recordings.loc[~has_duplicates]\n        if verbose:\n            print(\n                f\"Added {len(in_view)-sum(has_duplicates)} recordings. See `.recordings` for details.\"\n            )\n\n    def _update_view(self):\n        \"\"\"Update the attrs of the view after changes in the base module.\"\"\"\n        if isinstance(self, View):\n            scope = self._scope\n            current_view = self._current_view\n            # copy dict of new View. For some reason doing self = View(self)\n            # did not work.\n            self.__dict__ = View(\n                self.base, self._nodes_in_view, self._edges_in_view\n            ).__dict__\n\n            # retain the scope and current_view of the previous view\n            self._scope = scope\n            self._current_view = current_view\n\n    def delete_recordings(self):\n        \"\"\"Removes all recordings from the module.\"\"\"\n        if isinstance(self, View):\n            base_recs = self.base.recordings\n            self.base.recordings = base_recs[\n                ~base_recs.isin(self.recordings).all(axis=1)\n            ]\n            self._update_view()\n        else:\n            self.base.recordings = pd.DataFrame().from_dict({})\n\n    def stimulate(self, current: Optional[jnp.ndarray] = None, verbose: bool = True):\n        \"\"\"Insert a stimulus into the compartment.\n\n        current must be a 1d array or have batch dimension of size `(num_compartments, )`\n        or `(1, )`. If 1d, the same stimulus is added to all compartments.\n\n        This function cannot be run during `jax.jit` and `jax.grad`. Because of this,\n        it should only be used for static stimuli (i.e., stimuli that do not depend\n        on the data and that should not be learned). For stimuli that depend on data\n        (or that should be learned), please use `data_stimulate()`.\n\n        Args:\n            current: Current in `nA`.\n        \"\"\"\n        self._external_input(\"i\", current, verbose=verbose)\n\n    def clamp(self, state_name: str, state_array: jnp.ndarray, verbose: bool = True):\n        \"\"\"Clamp a state to a given value across specified compartments.\n\n        Args:\n            state_name: The name of the state to clamp.\n            state_array (jnp.nd: Array of values to clamp the state to.\n            verbose : If True, prints details about the clamping.\n\n        This function sets external states for the compartments.\n        \"\"\"\n        self._external_input(state_name, state_array, verbose=verbose)\n\n    def _external_input(\n        self,\n        key: str,\n        values: Optional[jnp.ndarray],\n        verbose: bool = True,\n    ):\n        comp_states, edge_states = self._get_state_names()\n        if key not in comp_states + edge_states:\n            raise KeyError(f\"{key} is not a recognized state in this module.\")\n        values = values if values.ndim == 2 else jnp.expand_dims(values, axis=0)\n        batch_size = values.shape[0]\n        num_inserted = (\n            len(self._nodes_in_view) if key in comp_states else len(self._edges_in_view)\n        )\n        is_multiple = num_inserted == batch_size\n        values = values if is_multiple else jnp.repeat(values, num_inserted, axis=0)\n        assert batch_size in [\n            1,\n            num_inserted,\n        ], \"Number of comps and stimuli do not match.\"\n\n        if key in self.base.externals.keys():\n            self.base.externals[key] = jnp.concatenate(\n                [self.base.externals[key], values]\n            )\n            self.base.external_inds[key] = jnp.concatenate(\n                [self.base.external_inds[key], self._nodes_in_view]\n            )\n        else:\n            if key in comp_states:\n                self.base.externals[key] = values\n                self.base.external_inds[key] = self._nodes_in_view\n            else:\n                self.base.externals[key] = values\n                self.base.external_inds[key] = self._edges_in_view\n        if verbose:\n            print(\n                f\"Added {num_inserted} external_states. See `.externals` for details.\"\n            )\n\n    def data_stimulate(\n        self,\n        current: jnp.ndarray,\n        data_stimuli: Optional[Tuple[jnp.ndarray, pd.DataFrame]] = None,\n        verbose: bool = False,\n    ) -> Tuple[jnp.ndarray, pd.DataFrame]:\n        \"\"\"Insert a stimulus into the module within jit (or grad).\n\n        Args:\n            current: Current in `nA`.\n            verbose: Whether or not to print the number of inserted stimuli. `False`\n                by default because this method is meant to be jitted.\n        \"\"\"\n        return self._data_external_input(\n            \"i\", current, data_stimuli, self.nodes, verbose=verbose\n        )\n\n    def data_clamp(\n        self,\n        state_name: str,\n        state_array: jnp.ndarray,\n        data_clamps: Optional[Tuple[jnp.ndarray, pd.DataFrame]] = None,\n        verbose: bool = False,\n    ):\n        \"\"\"Insert a clamp into the module within jit (or grad).\n\n        Args:\n            state_name: Name of the state variable to set.\n            state_array: Time series of the state variable in the default Jaxley unit.\n                State array should be of shape (num_clamps, simulation_time) or\n                (simulation_time, ) for a single clamp.\n            verbose: Whether or not to print the number of inserted clamps. `False`\n                by default because this method is meant to be jitted.\n        \"\"\"\n        comp_states, edge_states = self._get_state_names()\n        if state_name not in comp_states + edge_states:\n            raise KeyError(f\"{state_name} is not a recognized state in this module.\")\n        data = self.nodes if state_name in comp_states else self.edges\n        return self._data_external_input(\n            state_name, state_array, data_clamps, data, verbose=verbose\n        )\n\n    def _data_external_input(\n        self,\n        state_name: str,\n        state_array: jnp.ndarray,\n        data_external_input: Optional[Tuple[jnp.ndarray, pd.DataFrame]],\n        view: pd.DataFrame,\n        verbose: bool = False,\n    ):\n        comp_states, edge_states = self._get_state_names()\n        state_array = (\n            state_array\n            if state_array.ndim == 2\n            else jnp.expand_dims(state_array, axis=0)\n        )\n        batch_size = state_array.shape[0]\n        num_inserted = (\n            len(self._nodes_in_view)\n            if state_name in comp_states\n            else len(self._edges_in_view)\n        )\n        is_multiple = num_inserted == batch_size\n        state_array = (\n            state_array\n            if is_multiple\n            else jnp.repeat(state_array, num_inserted, axis=0)\n        )\n        assert batch_size in [\n            1,\n            num_inserted,\n        ], \"Number of comps and clamps do not match.\"\n\n        if data_external_input is not None:\n            external_input = data_external_input[1]\n            external_input = jnp.concatenate([external_input, state_array])\n            inds = data_external_input[2]\n        else:\n            external_input = state_array\n            inds = pd.DataFrame().from_dict({})\n\n        inds = pd.concat([inds, view])\n\n        if verbose:\n            if state_name == \"i\":\n                print(f\"Added {len(view)} stimuli.\")\n            else:\n                print(f\"Added {len(view)} clamps.\")\n\n        return (state_name, external_input, inds)\n\n    def delete_stimuli(self):\n        \"\"\"Removes all stimuli from the module.\"\"\"\n        self.delete_clamps(\"i\")\n\n    def delete_clamps(self, state_name: Optional[str] = None):\n        \"\"\"Removes all clamps of the given state from the module.\"\"\"\n        all_externals = list(self.externals.keys())\n        if \"i\" in all_externals:\n            all_externals.remove(\"i\")\n        state_names = all_externals if state_name is None else [state_name]\n        for state_name in state_names:\n            if state_name in self.externals:\n                keep_inds = ~np.isin(\n                    self.base.external_inds[state_name], self._nodes_in_view\n                )\n                base_exts = self.base.externals\n                base_exts_inds = self.base.external_inds\n                if np.all(~keep_inds):\n                    base_exts.pop(state_name, None)\n                    base_exts_inds.pop(state_name, None)\n                else:\n                    base_exts[state_name] = base_exts[state_name][keep_inds]\n                    base_exts_inds[state_name] = base_exts_inds[state_name][keep_inds]\n                self._update_view()\n            else:\n                pass  # does not have to be deleted if not in externals\n\n    def insert(self, channel: Channel):\n        \"\"\"Insert a channel into the module.\n\n        Args:\n            channel: The channel to insert.\"\"\"\n        name = channel._name\n\n        # Channel does not yet exist in the `jx.Module` at all.\n        if name not in [c._name for c in self.base.channels]:\n            self.base.channels.append(channel)\n            self.base.nodes[name] = (\n                False  # Previous columns do not have the new channel.\n            )\n\n        if channel.current_name not in self.base.membrane_current_names:\n            self.base.membrane_current_names.append(channel.current_name)\n\n        # Add a binary column that indicates if a channel is present.\n        self.base.nodes.loc[self._nodes_in_view, name] = True\n\n        # Loop over all new parameters, e.g. gNa, eNa.\n        for key in channel.channel_params:\n            self.base.nodes.loc[self._nodes_in_view, key] = channel.channel_params[key]\n\n        # Loop over all new parameters, e.g. gNa, eNa.\n        for key in channel.channel_states:\n            self.base.nodes.loc[self._nodes_in_view, key] = channel.channel_states[key]\n\n    def delete_channel(self, channel: Channel):\n        \"\"\"Remove a channel from the module.\n\n        Args:\n            channel: The channel to remove.\"\"\"\n        name = channel._name\n        channel_names = [c._name for c in self.channels]\n        all_channel_names = [c._name for c in self.base.channels]\n        if name in channel_names:\n            channel_cols = list(channel.channel_params.keys())\n            channel_cols += list(channel.channel_states.keys())\n            self.base.nodes.loc[self._nodes_in_view, channel_cols] = float(\"nan\")\n            self.base.nodes.loc[self._nodes_in_view, name] = False\n\n            # only delete cols if no other comps in the module have the same channel\n            if np.all(~self.base.nodes[name]):\n                self.base.channels.pop(all_channel_names.index(name))\n                self.base.membrane_current_names.remove(channel.current_name)\n                self.base.nodes.drop(columns=channel_cols + [name], inplace=True)\n        else:\n            raise ValueError(f\"Channel {name} not found in the module.\")\n\n    @only_allow_module\n    def step(\n        self,\n        u: Dict[str, jnp.ndarray],\n        delta_t: float,\n        external_inds: Dict[str, jnp.ndarray],\n        externals: Dict[str, jnp.ndarray],\n        params: Dict[str, jnp.ndarray],\n        solver: str = \"bwd_euler\",\n        voltage_solver: str = \"jaxley.stone\",\n    ) -> Dict[str, jnp.ndarray]:\n        \"\"\"One step of solving the Ordinary Differential Equation.\n\n        This function is called inside of `integrate` and increments the state of the\n        module by one time step. Calls `_step_channels` and `_step_synapse` to update\n        the states of the channels and synapses using fwd_euler.\n\n        Args:\n            u: The state of the module. voltages = u[\"v\"]\n            delta_t: The time step.\n            external_inds: The indices of the external inputs.\n            externals: The external inputs.\n            params: The parameters of the module.\n            solver: The solver to use for the voltages. Either of [\"bwd_euler\",\n                \"fwd_euler\", \"crank_nicolson\"].\n            voltage_solver: The tridiagonal solver used to diagonalize the\n                coefficient matrix of the ODE system. Either of [\"jaxley.thomas\",\n                \"jaxley.stone\"].\n\n        Returns:\n            The updated state of the module.\n        \"\"\"\n\n        # Extract the voltages\n        voltages = u[\"v\"]\n\n        # Extract the external inputs\n        if \"i\" in externals.keys():\n            i_current = externals[\"i\"]\n            i_inds = external_inds[\"i\"]\n            i_ext = self._get_external_input(\n                voltages, i_inds, i_current, params[\"radius\"], params[\"length\"]\n            )\n        else:\n            i_ext = 0.0\n\n        # Step of the channels.\n        u, (v_terms, const_terms) = self._step_channels(\n            u, delta_t, self.channels, self.nodes, params\n        )\n\n        # Step of the synapse.\n        u, (syn_v_terms, syn_const_terms) = self._step_synapse(\n            u,\n            self.synapses,\n            params,\n            delta_t,\n            self.edges,\n        )\n\n        # Clamp for channels and synapses.\n        for key in externals.keys():\n            if key not in [\"i\", \"v\"]:\n                u[key] = u[key].at[external_inds[key]].set(externals[key])\n\n        # Voltage steps.\n        cm = params[\"capacitance\"]  # Abbreviation.\n\n        # Arguments used by all solvers.\n        solver_kwargs = {\n            \"voltages\": voltages,\n            \"voltage_terms\": (v_terms + syn_v_terms) / cm,\n            \"constant_terms\": (const_terms + i_ext + syn_const_terms) / cm,\n            \"axial_conductances\": params[\"axial_conductances\"],\n            \"internal_node_inds\": self._internal_node_inds,\n        }\n\n        # Add solver specific arguments.\n        if voltage_solver == \"jax.sparse\":\n            solver_kwargs.update(\n                {\n                    \"sinks\": np.asarray(self._comp_edges[\"sink\"].to_list()),\n                    \"data_inds\": self._data_inds,\n                    \"indices\": self._indices_jax_spsolve,\n                    \"indptr\": self._indptr_jax_spsolve,\n                    \"n_nodes\": self._n_nodes,\n                }\n            )\n            # Only for `bwd_euler` and `cranck-nicolson`.\n            step_voltage_implicit = step_voltage_implicit_with_jax_spsolve\n        else:\n            # Our custom sparse solver requires a different format of all conductance\n            # values to perform triangulation and backsubstution optimally.\n            #\n            # Currently, the forward Euler solver also uses this format. However,\n            # this is only for historical reasons and we are planning to change this in\n            # the future.\n            solver_kwargs.update(\n                {\n                    \"sinks\": np.asarray(self._comp_edges[\"sink\"].to_list()),\n                    \"sources\": np.asarray(self._comp_edges[\"source\"].to_list()),\n                    \"types\": np.asarray(self._comp_edges[\"type\"].to_list()),\n                    \"ncomp_per_branch\": self.ncomp_per_branch,\n                    \"par_inds\": self._par_inds,\n                    \"child_inds\": self._child_inds,\n                    \"nbranches\": self.total_nbranches,\n                    \"solver\": voltage_solver,\n                    \"idx\": self._solve_indexer,\n                    \"debug_states\": self.debug_states,\n                }\n            )\n            # Only for `bwd_euler` and `cranck-nicolson`.\n            step_voltage_implicit = step_voltage_implicit_with_jaxley_spsolve\n\n        if solver == \"bwd_euler\":\n            u[\"v\"] = step_voltage_implicit(**solver_kwargs, delta_t=delta_t)\n        elif solver == \"crank_nicolson\":\n            # Crank-Nicolson advances by half a step of backward and half a step of\n            # forward Euler.\n            half_step_delta_t = delta_t / 2\n            half_step_voltages = step_voltage_implicit(\n                **solver_kwargs, delta_t=half_step_delta_t\n            )\n            # The forward Euler step in Crank-Nicolson can be performed easily as\n            # `V_{n+1} = 2 * V_{n+1/2} - V_n`. See also NEURON book Chapter 4.\n            u[\"v\"] = 2 * half_step_voltages - voltages\n        elif solver == \"fwd_euler\":\n            u[\"v\"] = step_voltage_explicit(**solver_kwargs, delta_t=delta_t)\n        else:\n            raise ValueError(\n                f\"You specified `solver={solver}`. The only allowed solvers are \"\n                \"['bwd_euler', 'fwd_euler', 'crank_nicolson'].\"\n            )\n\n        # Clamp for voltages.\n        if \"v\" in externals.keys():\n            u[\"v\"] = u[\"v\"].at[external_inds[\"v\"]].set(externals[\"v\"])\n\n        return u\n\n    def _step_channels(\n        self,\n        states: Dict[str, jnp.ndarray],\n        delta_t: float,\n        channels: List[Channel],\n        channel_nodes: pd.DataFrame,\n        params: Dict[str, jnp.ndarray],\n    ) -> Tuple[Dict[str, jnp.ndarray], Tuple[jnp.ndarray, jnp.ndarray]]:\n        \"\"\"One step of integration of the channels and of computing their current.\"\"\"\n        states = self._step_channels_state(\n            states, delta_t, channels, channel_nodes, params\n        )\n        states, current_terms = self._channel_currents(\n            states, delta_t, channels, channel_nodes, params\n        )\n        return states, current_terms\n\n    def _step_channels_state(\n        self,\n        states,\n        delta_t,\n        channels: List[Channel],\n        channel_nodes: pd.DataFrame,\n        params: Dict[str, jnp.ndarray],\n    ) -> Dict[str, jnp.ndarray]:\n        \"\"\"One integration step of the channels.\"\"\"\n        voltages = states[\"v\"]\n\n        # Update states of the channels.\n        indices = channel_nodes[\"global_comp_index\"].to_numpy()\n        for channel in channels:\n            channel_param_names = list(channel.channel_params)\n            channel_param_names += [\n                \"radius\",\n                \"length\",\n                \"axial_resistivity\",\n                \"capacitance\",\n            ]\n            channel_state_names = list(channel.channel_states)\n            channel_state_names += self.membrane_current_names\n            channel_indices = indices[channel_nodes[channel._name].astype(bool)]\n\n            channel_params = query_channel_states_and_params(\n                params, channel_param_names, channel_indices\n            )\n            channel_states = query_channel_states_and_params(\n                states, channel_state_names, channel_indices\n            )\n\n            states_updated = channel.update_states(\n                channel_states, delta_t, voltages[channel_indices], channel_params\n            )\n            # Rebuild state. This has to be done within the loop over channels to allow\n            # multiple channels which modify the same state.\n            for key, val in states_updated.items():\n                states[key] = states[key].at[channel_indices].set(val)\n\n        return states\n\n    def _channel_currents(\n        self,\n        states: Dict[str, jnp.ndarray],\n        delta_t: float,\n        channels: List[Channel],\n        channel_nodes: pd.DataFrame,\n        params: Dict[str, jnp.ndarray],\n    ) -> Tuple[Dict[str, jnp.ndarray], Tuple[jnp.ndarray, jnp.ndarray]]:\n        \"\"\"Return the current through each channel.\n\n        This is also updates `state` because the `state` also contains the current.\n        \"\"\"\n        voltages = states[\"v\"]\n\n        # Compute current through channels.\n        voltage_terms = jnp.zeros_like(voltages)\n        constant_terms = jnp.zeros_like(voltages)\n        # Run with two different voltages that are `diff` apart to infer the slope and\n        # offset.\n        diff = 1e-3\n\n        current_states = {}\n        for name in self.membrane_current_names:\n            current_states[name] = jnp.zeros_like(voltages)\n\n        for channel in channels:\n            name = channel._name\n            channel_param_names = list(channel.channel_params.keys())\n            channel_state_names = list(channel.channel_states.keys())\n            indices = channel_nodes.loc[channel_nodes[name]][\n                \"global_comp_index\"\n            ].to_numpy()\n\n            channel_params = {}\n            for p in channel_param_names:\n                channel_params[p] = params[p][indices]\n            channel_params[\"radius\"] = params[\"radius\"][indices]\n            channel_params[\"length\"] = params[\"length\"][indices]\n            channel_params[\"axial_resistivity\"] = params[\"axial_resistivity\"][indices]\n\n            channel_states = {}\n            for s in channel_state_names:\n                channel_states[s] = states[s][indices]\n\n            v_and_perturbed = jnp.stack([voltages[indices], voltages[indices] + diff])\n            membrane_currents = vmap(channel.compute_current, in_axes=(None, 0, None))(\n                channel_states, v_and_perturbed, channel_params\n            )\n            voltage_term = (membrane_currents[1] - membrane_currents[0]) / diff\n            constant_term = membrane_currents[0] - voltage_term * voltages[indices]\n\n            # * 1000 to convert from mA/cm^2 to uA/cm^2.\n            voltage_terms = voltage_terms.at[indices].add(voltage_term * 1000.0)\n            constant_terms = constant_terms.at[indices].add(-constant_term * 1000.0)\n\n            # Save the current (for the unperturbed voltage) as a state that will\n            # also be passed to the state update.\n            current_states[channel.current_name] = (\n                current_states[channel.current_name]\n                .at[indices]\n                .add(membrane_currents[0])\n            )\n\n        # Copy the currents into the `state` dictionary such that they can be\n        # recorded and used by `Channel.update_states()`.\n        for name in self.membrane_current_names:\n            states[name] = current_states[name]\n\n        return states, (voltage_terms, constant_terms)\n\n    def _step_synapse(\n        self,\n        u: Dict[str, jnp.ndarray],\n        syn_channels: List[Channel],\n        params: Dict[str, jnp.ndarray],\n        delta_t: float,\n        edges: pd.DataFrame,\n    ) -> Tuple[Dict[str, jnp.ndarray], Tuple[jnp.ndarray, jnp.ndarray]]:\n        \"\"\"One step of integration of the channels.\n\n        `Network` overrides this method (because it actually has synapses), whereas\n        `Compartment`, `Branch`, and `Cell` do not override this.\n        \"\"\"\n        voltages = u[\"v\"]\n        return u, (jnp.zeros_like(voltages), jnp.zeros_like(voltages))\n\n    def _synapse_currents(\n        self, states, syn_channels, params, delta_t, edges: pd.DataFrame\n    ) -> Tuple[Dict[str, jnp.ndarray], Tuple[jnp.ndarray, jnp.ndarray]]:\n        return states, (None, None)\n\n    @staticmethod\n    def _get_external_input(\n        voltages: jnp.ndarray,\n        i_inds: jnp.ndarray,\n        i_stim: jnp.ndarray,\n        radius: float,\n        length_single_compartment: float,\n    ) -> jnp.ndarray:\n        \"\"\"\n        Return external input to each compartment in uA / cm^2.\n\n        Args:\n            voltages: mV.\n            i_stim: nA.\n            radius: um.\n            length_single_compartment: um.\n        \"\"\"\n        zero_vec = jnp.zeros_like(voltages)\n        current = convert_point_process_to_distributed(\n            i_stim, radius[i_inds], length_single_compartment[i_inds]\n        )\n\n        dnums = ScatterDimensionNumbers(\n            update_window_dims=(),\n            inserted_window_dims=(0,),\n            scatter_dims_to_operand_dims=(0,),\n        )\n        stim_at_timestep = scatter_add(zero_vec, i_inds[:, None], current, dnums)\n        return stim_at_timestep\n\n    def vis(\n        self,\n        ax: Optional[Axes] = None,\n        col: str = \"k\",\n        dims: Tuple[int] = (0, 1),\n        type: str = \"line\",\n        morph_plot_kwargs: Dict = {},\n    ) -> Axes:\n        \"\"\"Visualize the module.\n\n        Modules can be visualized on one of the cardinal planes (xy, xz, yz) or\n        even in 3D.\n\n        Several options are available:\n        - `line`: All points from the traced morphology (`xyzr`), are connected\n        with a line plot.\n        - `scatter`: All traced points, are plotted as scatter points.\n        - `comp`: Plots the compartmentalized morphology, including radius\n        and shape. (shows the true compartment lengths per default, but this can\n        be changed via the `morph_plot_kwargs`, for details see\n        `jaxley.utils.plot_utils.plot_comps`).\n        - `morph`: Reconstructs the 3D shape of the traced morphology. For details see\n        `jaxley.utils.plot_utils.plot_morph`. Warning: For 3D plots and morphologies\n        with many traced points this can be very slow.\n\n        Args:\n            ax: An axis into which to plot.\n            col: The color for all branches.\n            dims: Which dimensions to plot. 1=x, 2=y, 3=z coordinate. Must be a tuple of\n                two of them.\n            type: The type of plot. One of [\"line\", \"scatter\", \"comp\", \"morph\"].\n            morph_plot_kwargs: Keyword arguments passed to the plotting function.\n        \"\"\"\n        if \"comp\" in type.lower():\n            return plot_comps(self, dims=dims, ax=ax, col=col, **morph_plot_kwargs)\n        if \"morph\" in type.lower():\n            return plot_morph(self, dims=dims, ax=ax, col=col, **morph_plot_kwargs)\n\n        assert not np.any(\n            [np.isnan(xyzr[:, dims]).all() for xyzr in self.xyzr]\n        ), \"No coordinates available. Use `vis(detail='point')` or run `.compute_xyz()` before running `.vis()`.\"\n\n        ax = plot_graph(\n            self.xyzr,\n            dims=dims,\n            col=col,\n            ax=ax,\n            type=type,\n            morph_plot_kwargs=morph_plot_kwargs,\n        )\n\n        return ax\n\n    def compute_xyz(self):\n        \"\"\"Return xyz coordinates of every branch, based on the branch length.\n\n        This function should not be called if the morphology was read from an `.swc`\n        file. However, for morphologies that were constructed from scratch, this\n        function **must** be called before `.vis()`. The computed `xyz` coordinates\n        are only used for plotting.\n        \"\"\"\n        max_y_multiplier = 5.0\n        min_y_multiplier = 0.5\n\n        parents = self.comb_parents\n        num_children = _compute_num_children(parents)\n        index_of_child = _compute_index_of_child(parents)\n        levels = compute_levels(parents)\n\n        # Extract branch.\n        inds_branch = self.nodes.groupby(\"global_branch_index\")[\n            \"global_comp_index\"\n        ].apply(list)\n        branch_lens = [np.sum(self.nodes[\"length\"][np.asarray(i)]) for i in inds_branch]\n        endpoints = []\n\n        # Different levels will get a different \"angle\" at which the children emerge from\n        # the parents. This angle is defined by the `y_offset_multiplier`. This value\n        # defines the range between y-location of the first and of the last child of a\n        # parent.\n        y_offset_multiplier = np.linspace(\n            max_y_multiplier, min_y_multiplier, np.max(levels) + 1\n        )\n\n        for b in range(self.total_nbranches):\n            # For networks with mixed SWC and from-scatch neurons, only update those\n            # branches that do not have coordingates yet.\n            if np.any(np.isnan(self.xyzr[b])):\n                if parents[b] > -1:\n                    start_point = endpoints[parents[b]]\n                    num_children_of_parent = num_children[parents[b]]\n                    if num_children_of_parent > 1:\n                        y_offset = (\n                            ((index_of_child[b] / (num_children_of_parent - 1))) - 0.5\n                        ) * y_offset_multiplier[levels[b]]\n                    else:\n                        y_offset = 0.0\n                else:\n                    start_point = [0, 0, 0]\n                    y_offset = 0.0\n\n                len_of_path = np.sqrt(y_offset**2 + 1.0)\n\n                end_point = [\n                    start_point[0] + branch_lens[b] / len_of_path * 1.0,\n                    start_point[1] + branch_lens[b] / len_of_path * y_offset,\n                    start_point[2],\n                ]\n                endpoints.append(end_point)\n\n                self.xyzr[b][:, :3] = np.asarray([start_point, end_point])\n            else:\n                # Dummy to keey the index `endpoints[parent[b]]` above working.\n                endpoints.append(np.zeros((2,)))\n\n    def move(\n        self, x: float = 0.0, y: float = 0.0, z: float = 0.0, update_nodes: bool = False\n    ):\n        \"\"\"Move cells or networks by adding to their (x, y, z) coordinates.\n\n        This function is used only for visualization. It does not affect the simulation.\n\n        Args:\n            x: The amount to move in the x direction in um.\n            y: The amount to move in the y direction in um.\n            z: The amount to move in the z direction in um.\n            update_nodes: Whether `.nodes` should be updated or not. Setting this to\n                `False` largely speeds up moving, especially for big networks, but\n                `.nodes` or `.show` will not show the new xyz coordinates.\n        \"\"\"\n        for i in self._branches_in_view:\n            self.base.xyzr[i][:, :3] += np.array([x, y, z])\n        if update_nodes:\n            self.compute_compartment_centers()\n\n    def move_to(\n        self,\n        x: Union[float, np.ndarray] = 0.0,\n        y: Union[float, np.ndarray] = 0.0,\n        z: Union[float, np.ndarray] = 0.0,\n        update_nodes: bool = False,\n    ):\n        \"\"\"Move cells or networks to a location (x, y, z).\n\n        If x, y, and z are floats, then the first compartment of the first branch\n        of the first cell is moved to that float coordinate, and everything else is\n        shifted by the difference between that compartment's previous coordinate and\n        the new float location.\n\n        If x, y, and z are arrays, then they must each have a length equal to the number\n        of cells being moved. Then the first compartment of the first branch of each\n        cell is moved to the specified location.\n\n        Args:\n            update_nodes: Whether `.nodes` should be updated or not. Setting this to\n                `False` largely speeds up moving, especially for big networks, but\n                `.nodes` or `.show` will not show the new xyz coordinates.\n        \"\"\"\n        # Test if any coordinate values are NaN which would greatly affect moving\n        if np.any(np.concatenate(self.xyzr, axis=0)[:, :3] == np.nan):\n            raise ValueError(\n                \"NaN coordinate values detected. Shift amounts cannot be computed. Please run compute_xyzr() or assign initial coordinate values.\"\n            )\n\n        # can only iterate over cells for networks\n        # lambda makes sure that generator can be created multiple times\n        base_is_net = self.base._current_view == \"network\"\n        cells = lambda: (self.cells if base_is_net else [self])\n\n        root_xyz_cells = np.array([c.xyzr[0][0, :3] for c in cells()])\n        root_xyz = root_xyz_cells[0] if isinstance(x, float) else root_xyz_cells\n        move_by = np.array([x, y, z]).T - root_xyz\n\n        if len(move_by.shape) == 1:\n            move_by = np.tile(move_by, (len(self._cells_in_view), 1))\n\n        for cell, offset in zip(cells(), move_by):\n            for idx in cell._branches_in_view:\n                self.base.xyzr[idx][:, :3] += offset\n        if update_nodes:\n            self.compute_compartment_centers()\n\n    def rotate(\n        self, degrees: float, rotation_axis: str = \"xy\", update_nodes: bool = False\n    ):\n        \"\"\"Rotate jaxley modules clockwise. Used only for visualization.\n\n        This function is used only for visualization. It does not affect the simulation.\n\n        Args:\n            degrees: How many degrees to rotate the module by.\n            rotation_axis: Either of {`xy` | `xz` | `yz`}.\n        \"\"\"\n        degrees = degrees / 180 * np.pi\n        if rotation_axis == \"xy\":\n            dims = [0, 1]\n        elif rotation_axis == \"xz\":\n            dims = [0, 2]\n        elif rotation_axis == \"yz\":\n            dims = [1, 2]\n        else:\n            raise ValueError\n\n        rotation_matrix = np.asarray(\n            [[np.cos(degrees), np.sin(degrees)], [-np.sin(degrees), np.cos(degrees)]]\n        )\n        for i in self._branches_in_view:\n            rot = np.dot(rotation_matrix, self.base.xyzr[i][:, dims].T).T\n            self.base.xyzr[i][:, dims] = rot\n        if update_nodes:\n            self.compute_compartment_centers()\n\n    def copy_node_property_to_edges(\n        self,\n        properties_to_import: Union[str, List[str]],\n        pre_or_post: Union[str, List[str]] = [\"pre\", \"post\"],\n    ) -> Module:\n        \"\"\"Copy a property that is in `node` over to `edges`.\n\n        By default, `.edges` does not contain the properties (radius, length, cm,\n        channel properties,...) of the pre- and post-synaptic compartments. This\n        method allows to copy a property of the pre- and/or post-synaptic compartment\n        to the edges. It is then accessible as `module.edges.pre_property_name` or\n        `module.edges.post_property_name`.\n\n        Note that, if you modify the node property _after_ having run\n        `copy_node_property_to_edges`, it will not automatically update the value in\n        `.edges`.\n\n        Note that, if this method is called on a View (e.g.\n        `net.cell(0).copy_node_property_to_edges`), then it will return a View, but\n        it will _not_ modify the module itself.\n\n        Args:\n            properties_to_import: The name of the node properties that should be\n                imported. To list all available properties, look at\n                `module.nodes.columns`.\n            pre_or_post: Whether to import only the pre-synaptic property ('pre'), only\n                the post-synaptic property ('post'), or both (['pre', 'post']).\n\n        Returns:\n            A new module which has the property copied to the `nodes`.\n        \"\"\"\n        # If a string is passed, wrap it as a list.\n        if isinstance(pre_or_post, str):\n            pre_or_post = [pre_or_post]\n        if isinstance(properties_to_import, str):\n            properties_to_import = [properties_to_import]\n\n        for pre_or_post_val in pre_or_post:\n            assert pre_or_post_val in [\"pre\", \"post\"]\n            for property_to_import in properties_to_import:\n                # Delete the column if it already exists. Otherwise it would exist\n                # twice.\n                if f\"{pre_or_post_val}_{property_to_import}\" in self.edges.columns:\n                    self.edges.drop(\n                        columns=f\"{pre_or_post_val}_{property_to_import}\", inplace=True\n                    )\n\n                self.edges = self.edges.join(\n                    self.nodes[[property_to_import, \"global_comp_index\"]].set_index(\n                        \"global_comp_index\"\n                    ),\n                    on=f\"{pre_or_post_val}_global_comp_index\",\n                )\n                self.edges = self.edges.rename(\n                    columns={\n                        property_to_import: f\"{pre_or_post_val}_{property_to_import}\"\n                    }\n                )\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.branches","title":"branches property","text":"

Iterate over all branches in the module.

Returns a generator that yields a View of each branch.

"},{"location":"reference/modules/#jaxley.modules.base.Module.cells","title":"cells property","text":"

Iterate over all cells in the module.

Returns a generator that yields a View of each cell.

"},{"location":"reference/modules/#jaxley.modules.base.Module.comps","title":"comps property","text":"

Iterate over all compartments in the module. Can be called on any module, i.e. net.comps, cell.comps or branch.comps. __iter__ does not allow for this.

Returns a generator that yields a View of each compartment.

"},{"location":"reference/modules/#jaxley.modules.base.Module.initialized","title":"initialized: bool property","text":"

Whether the Module is ready to be solved or not.

"},{"location":"reference/modules/#jaxley.modules.base.Module.shape","title":"shape: Tuple[int] property","text":"

Returns the number of submodules contained in a module.

.. code-block:: python

network.shape = (num_cells, num_branches, num_compartments)\ncell.shape = (num_branches, num_compartments)\nbranch.shape = (num_compartments,)\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.view","title":"view property","text":"

Return view of the module.

"},{"location":"reference/modules/#jaxley.modules.base.Module.__getitem__","title":"__getitem__(index)","text":"

Lazy indexing of the module.

Source code in jaxley/modules/base.py
def __getitem__(self, index):\n    \"\"\"Lazy indexing of the module.\"\"\"\n    supported_parents = [\"network\", \"cell\", \"branch\"]  # cannot index into comp\n\n    not_group_view = self._current_view not in self.groups\n    assert (\n        self._current_view in supported_parents or not_group_view\n    ), \"Lazy indexing is only supported for `Network`, `Cell`, `Branch` and Views thereof.\"\n    index = index if isinstance(index, tuple) else (index,)\n\n    child_views = self._childviews()\n    assert len(index) <= len(child_views), \"Too many indices.\"\n    view = self\n    for i, child in zip(index, child_views):\n        view = view._at_nodes(child, i)\n    return view\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.__iter__","title":"__iter__()","text":"

Iterate over parts of the module.

Internally calls cells, branches, comps at the appropriate level.

Example:

.. code-block:: python

for cell in network:\n    for branch in cell:\n        for comp in branch:\n            print(comp.nodes.shape)\n
Source code in jaxley/modules/base.py
def __iter__(self):\n    \"\"\"Iterate over parts of the module.\n\n    Internally calls `cells`, `branches`, `comps` at the appropriate level.\n\n    Example:\n\n    .. code-block:: python\n\n        for cell in network:\n            for branch in cell:\n                for comp in branch:\n                    print(comp.nodes.shape)\n    \"\"\"\n    next_level = self._childviews()[0]\n    yield from self._iter_submodules(next_level)\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.add_to_group","title":"add_to_group(group_name)","text":"

Add a view of the module to a group.

Groups can then be indexed. For example:

.. code-block:: python

net.cell(0).add_to_group(\"excitatory\")\nnet.excitatory.set(\"radius\", 0.1)\n

Parameters:

Name Type Description Default group_name str

The name of the group.

required Source code in jaxley/modules/base.py
def add_to_group(self, group_name: str):\n    \"\"\"Add a view of the module to a group.\n\n    Groups can then be indexed. For example:\n\n    .. code-block:: python\n\n        net.cell(0).add_to_group(\"excitatory\")\n        net.excitatory.set(\"radius\", 0.1)\n\n    Args:\n        group_name: The name of the group.\n    \"\"\"\n    if group_name not in self.base.groups:\n        self.base.groups[group_name] = self._nodes_in_view\n    else:\n        self.base.groups[group_name] = np.unique(\n            np.concatenate([self.base.groups[group_name], self._nodes_in_view])\n        )\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.branch","title":"branch(idx)","text":"

Return a View of the module at the selected branches(s).

Parameters:

Name Type Description Default idx Any

index of the branch to view.

required

Returns:

Type Description View

View of the module at the specified branch index.

Source code in jaxley/modules/base.py
def branch(self, idx: Any) -> View:\n    \"\"\"Return a View of the module at the selected branches(s).\n\n    Args:\n        idx: index of the branch to view.\n\n    Returns:\n        View of the module at the specified branch index.\"\"\"\n    return self._at_nodes(\"branch\", idx)\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.cell","title":"cell(idx)","text":"

Return a View of the module at the selected cell(s).

Parameters:

Name Type Description Default idx Any

index of the cell to view.

required

Returns:

Type Description View

View of the module at the specified cell index.

Source code in jaxley/modules/base.py
def cell(self, idx: Any) -> View:\n    \"\"\"Return a View of the module at the selected cell(s).\n\n    Args:\n        idx: index of the cell to view.\n\n    Returns:\n        View of the module at the specified cell index.\"\"\"\n    return self._at_nodes(\"cell\", idx)\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.clamp","title":"clamp(state_name, state_array, verbose=True)","text":"

Clamp a state to a given value across specified compartments.

Parameters:

Name Type Description Default state_name str

The name of the state to clamp.

required state_array nd

Array of values to clamp the state to.

required verbose

If True, prints details about the clamping.

True

This function sets external states for the compartments.

Source code in jaxley/modules/base.py
def clamp(self, state_name: str, state_array: jnp.ndarray, verbose: bool = True):\n    \"\"\"Clamp a state to a given value across specified compartments.\n\n    Args:\n        state_name: The name of the state to clamp.\n        state_array (jnp.nd: Array of values to clamp the state to.\n        verbose : If True, prints details about the clamping.\n\n    This function sets external states for the compartments.\n    \"\"\"\n    self._external_input(state_name, state_array, verbose=verbose)\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.comp","title":"comp(idx)","text":"

Return a View of the module at the selected compartments(s).

Parameters:

Name Type Description Default idx Any

index of the comp to view.

required

Returns:

Type Description View

View of the module at the specified compartment index.

Source code in jaxley/modules/base.py
def comp(self, idx: Any) -> View:\n    \"\"\"Return a View of the module at the selected compartments(s).\n\n    Args:\n        idx: index of the comp to view.\n\n    Returns:\n        View of the module at the specified compartment index.\"\"\"\n    return self._at_nodes(\"comp\", idx)\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.compute_compartment_centers","title":"compute_compartment_centers()","text":"

Add compartment centers to nodes dataframe

Source code in jaxley/modules/base.py
def compute_compartment_centers(self):\n    \"\"\"Add compartment centers to nodes dataframe\"\"\"\n    centers = self._compute_coords_of_comp_centers()\n    self.base.nodes.loc[self._nodes_in_view, [\"x\", \"y\", \"z\"]] = centers\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.compute_xyz","title":"compute_xyz()","text":"

Return xyz coordinates of every branch, based on the branch length.

This function should not be called if the morphology was read from an .swc file. However, for morphologies that were constructed from scratch, this function must be called before .vis(). The computed xyz coordinates are only used for plotting.

Source code in jaxley/modules/base.py
def compute_xyz(self):\n    \"\"\"Return xyz coordinates of every branch, based on the branch length.\n\n    This function should not be called if the morphology was read from an `.swc`\n    file. However, for morphologies that were constructed from scratch, this\n    function **must** be called before `.vis()`. The computed `xyz` coordinates\n    are only used for plotting.\n    \"\"\"\n    max_y_multiplier = 5.0\n    min_y_multiplier = 0.5\n\n    parents = self.comb_parents\n    num_children = _compute_num_children(parents)\n    index_of_child = _compute_index_of_child(parents)\n    levels = compute_levels(parents)\n\n    # Extract branch.\n    inds_branch = self.nodes.groupby(\"global_branch_index\")[\n        \"global_comp_index\"\n    ].apply(list)\n    branch_lens = [np.sum(self.nodes[\"length\"][np.asarray(i)]) for i in inds_branch]\n    endpoints = []\n\n    # Different levels will get a different \"angle\" at which the children emerge from\n    # the parents. This angle is defined by the `y_offset_multiplier`. This value\n    # defines the range between y-location of the first and of the last child of a\n    # parent.\n    y_offset_multiplier = np.linspace(\n        max_y_multiplier, min_y_multiplier, np.max(levels) + 1\n    )\n\n    for b in range(self.total_nbranches):\n        # For networks with mixed SWC and from-scatch neurons, only update those\n        # branches that do not have coordingates yet.\n        if np.any(np.isnan(self.xyzr[b])):\n            if parents[b] > -1:\n                start_point = endpoints[parents[b]]\n                num_children_of_parent = num_children[parents[b]]\n                if num_children_of_parent > 1:\n                    y_offset = (\n                        ((index_of_child[b] / (num_children_of_parent - 1))) - 0.5\n                    ) * y_offset_multiplier[levels[b]]\n                else:\n                    y_offset = 0.0\n            else:\n                start_point = [0, 0, 0]\n                y_offset = 0.0\n\n            len_of_path = np.sqrt(y_offset**2 + 1.0)\n\n            end_point = [\n                start_point[0] + branch_lens[b] / len_of_path * 1.0,\n                start_point[1] + branch_lens[b] / len_of_path * y_offset,\n                start_point[2],\n            ]\n            endpoints.append(end_point)\n\n            self.xyzr[b][:, :3] = np.asarray([start_point, end_point])\n        else:\n            # Dummy to keey the index `endpoints[parent[b]]` above working.\n            endpoints.append(np.zeros((2,)))\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.copy","title":"copy(reset_index=False, as_module=False)","text":"

Extract part of a module and return a copy of its View or a new module.

This can be used to call jx.integrate on part of a Module.

Parameters:

Name Type Description Default reset_index bool

if True, the indices of the new module are reset to start from 0.

False as_module bool

if True, a new module is returned instead of a View.

False

Returns:

Type Description Union[Module, View]

A part of the module or a copied view of it.

Source code in jaxley/modules/base.py
def copy(\n    self, reset_index: bool = False, as_module: bool = False\n) -> Union[Module, View]:\n    \"\"\"Extract part of a module and return a copy of its View or a new module.\n\n    This can be used to call `jx.integrate` on part of a Module.\n\n    Args:\n        reset_index: if True, the indices of the new module are reset to start from 0.\n        as_module: if True, a new module is returned instead of a View.\n\n    Returns:\n        A part of the module or a copied view of it.\"\"\"\n    view = deepcopy(self)\n    warnings.warn(\"This method is experimental, use at your own risk.\")\n    # TODO FROM #447: add reset_index, i.e. for parents, nodes, edges etc. such that they\n    # start from 0/-1 and are contiguous\n    if as_module:\n        raise NotImplementedError(\"Not yet implemented.\")\n        # initialize a new module with the same attributes\n    return view\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.copy_node_property_to_edges","title":"copy_node_property_to_edges(properties_to_import, pre_or_post=['pre', 'post'])","text":"

Copy a property that is in node over to edges.

By default, .edges does not contain the properties (radius, length, cm, channel properties,\u2026) of the pre- and post-synaptic compartments. This method allows to copy a property of the pre- and/or post-synaptic compartment to the edges. It is then accessible as module.edges.pre_property_name or module.edges.post_property_name.

Note that, if you modify the node property after having run copy_node_property_to_edges, it will not automatically update the value in .edges.

Note that, if this method is called on a View (e.g. net.cell(0).copy_node_property_to_edges), then it will return a View, but it will not modify the module itself.

Parameters:

Name Type Description Default properties_to_import Union[str, List[str]]

The name of the node properties that should be imported. To list all available properties, look at module.nodes.columns.

required pre_or_post Union[str, List[str]]

Whether to import only the pre-synaptic property (\u2018pre\u2019), only the post-synaptic property (\u2018post\u2019), or both ([\u2018pre\u2019, \u2018post\u2019]).

['pre', 'post']

Returns:

Type Description Module

A new module which has the property copied to the nodes.

Source code in jaxley/modules/base.py
def copy_node_property_to_edges(\n    self,\n    properties_to_import: Union[str, List[str]],\n    pre_or_post: Union[str, List[str]] = [\"pre\", \"post\"],\n) -> Module:\n    \"\"\"Copy a property that is in `node` over to `edges`.\n\n    By default, `.edges` does not contain the properties (radius, length, cm,\n    channel properties,...) of the pre- and post-synaptic compartments. This\n    method allows to copy a property of the pre- and/or post-synaptic compartment\n    to the edges. It is then accessible as `module.edges.pre_property_name` or\n    `module.edges.post_property_name`.\n\n    Note that, if you modify the node property _after_ having run\n    `copy_node_property_to_edges`, it will not automatically update the value in\n    `.edges`.\n\n    Note that, if this method is called on a View (e.g.\n    `net.cell(0).copy_node_property_to_edges`), then it will return a View, but\n    it will _not_ modify the module itself.\n\n    Args:\n        properties_to_import: The name of the node properties that should be\n            imported. To list all available properties, look at\n            `module.nodes.columns`.\n        pre_or_post: Whether to import only the pre-synaptic property ('pre'), only\n            the post-synaptic property ('post'), or both (['pre', 'post']).\n\n    Returns:\n        A new module which has the property copied to the `nodes`.\n    \"\"\"\n    # If a string is passed, wrap it as a list.\n    if isinstance(pre_or_post, str):\n        pre_or_post = [pre_or_post]\n    if isinstance(properties_to_import, str):\n        properties_to_import = [properties_to_import]\n\n    for pre_or_post_val in pre_or_post:\n        assert pre_or_post_val in [\"pre\", \"post\"]\n        for property_to_import in properties_to_import:\n            # Delete the column if it already exists. Otherwise it would exist\n            # twice.\n            if f\"{pre_or_post_val}_{property_to_import}\" in self.edges.columns:\n                self.edges.drop(\n                    columns=f\"{pre_or_post_val}_{property_to_import}\", inplace=True\n                )\n\n            self.edges = self.edges.join(\n                self.nodes[[property_to_import, \"global_comp_index\"]].set_index(\n                    \"global_comp_index\"\n                ),\n                on=f\"{pre_or_post_val}_global_comp_index\",\n            )\n            self.edges = self.edges.rename(\n                columns={\n                    property_to_import: f\"{pre_or_post_val}_{property_to_import}\"\n                }\n            )\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.data_clamp","title":"data_clamp(state_name, state_array, data_clamps=None, verbose=False)","text":"

Insert a clamp into the module within jit (or grad).

Parameters:

Name Type Description Default state_name str

Name of the state variable to set.

required state_array ndarray

Time series of the state variable in the default Jaxley unit. State array should be of shape (num_clamps, simulation_time) or (simulation_time, ) for a single clamp.

required verbose bool

Whether or not to print the number of inserted clamps. False by default because this method is meant to be jitted.

False Source code in jaxley/modules/base.py
def data_clamp(\n    self,\n    state_name: str,\n    state_array: jnp.ndarray,\n    data_clamps: Optional[Tuple[jnp.ndarray, pd.DataFrame]] = None,\n    verbose: bool = False,\n):\n    \"\"\"Insert a clamp into the module within jit (or grad).\n\n    Args:\n        state_name: Name of the state variable to set.\n        state_array: Time series of the state variable in the default Jaxley unit.\n            State array should be of shape (num_clamps, simulation_time) or\n            (simulation_time, ) for a single clamp.\n        verbose: Whether or not to print the number of inserted clamps. `False`\n            by default because this method is meant to be jitted.\n    \"\"\"\n    comp_states, edge_states = self._get_state_names()\n    if state_name not in comp_states + edge_states:\n        raise KeyError(f\"{state_name} is not a recognized state in this module.\")\n    data = self.nodes if state_name in comp_states else self.edges\n    return self._data_external_input(\n        state_name, state_array, data_clamps, data, verbose=verbose\n    )\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.data_set","title":"data_set(key, val, param_state)","text":"

Set parameter of module (or its view) to a new value within jit.

Parameters:

Name Type Description Default key str

The name of the parameter to set.

required val Union[float, ndarray]

The value to set the parameter to. If it is jnp.ndarray then it must be of shape (len(num_compartments)).

required param_state Optional[List[Dict]]

State of the setted parameters, internally used such that this function does not modify global state.

required Source code in jaxley/modules/base.py
def data_set(\n    self,\n    key: str,\n    val: Union[float, jnp.ndarray],\n    param_state: Optional[List[Dict]],\n):\n    \"\"\"Set parameter of module (or its view) to a new value within `jit`.\n\n    Args:\n        key: The name of the parameter to set.\n        val: The value to set the parameter to. If it is `jnp.ndarray` then it\n            must be of shape `(len(num_compartments))`.\n        param_state: State of the setted parameters, internally used such that this\n            function does not modify global state.\n    \"\"\"\n    # Note: `data_set` does not support arrays for `val`.\n    is_node_param = key in self.nodes.columns\n    data = self.nodes if is_node_param else self.edges\n    viewed_inds = self._nodes_in_view if is_node_param else self._edges_in_view\n    if key in data.columns:\n        not_nan = ~data[key].isna()\n        added_param_state = [\n            {\n                \"indices\": np.atleast_2d(viewed_inds[not_nan]),\n                \"key\": key,\n                \"val\": jnp.atleast_1d(jnp.asarray(val)),\n            }\n        ]\n        if param_state is not None:\n            param_state += added_param_state\n        else:\n            param_state = added_param_state\n    else:\n        raise KeyError(\"Key not recognized.\")\n    return param_state\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.data_stimulate","title":"data_stimulate(current, data_stimuli=None, verbose=False)","text":"

Insert a stimulus into the module within jit (or grad).

Parameters:

Name Type Description Default current ndarray

Current in nA.

required verbose bool

Whether or not to print the number of inserted stimuli. False by default because this method is meant to be jitted.

False Source code in jaxley/modules/base.py
def data_stimulate(\n    self,\n    current: jnp.ndarray,\n    data_stimuli: Optional[Tuple[jnp.ndarray, pd.DataFrame]] = None,\n    verbose: bool = False,\n) -> Tuple[jnp.ndarray, pd.DataFrame]:\n    \"\"\"Insert a stimulus into the module within jit (or grad).\n\n    Args:\n        current: Current in `nA`.\n        verbose: Whether or not to print the number of inserted stimuli. `False`\n            by default because this method is meant to be jitted.\n    \"\"\"\n    return self._data_external_input(\n        \"i\", current, data_stimuli, self.nodes, verbose=verbose\n    )\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.delete_channel","title":"delete_channel(channel)","text":"

Remove a channel from the module.

Parameters:

Name Type Description Default channel Channel

The channel to remove.

required Source code in jaxley/modules/base.py
def delete_channel(self, channel: Channel):\n    \"\"\"Remove a channel from the module.\n\n    Args:\n        channel: The channel to remove.\"\"\"\n    name = channel._name\n    channel_names = [c._name for c in self.channels]\n    all_channel_names = [c._name for c in self.base.channels]\n    if name in channel_names:\n        channel_cols = list(channel.channel_params.keys())\n        channel_cols += list(channel.channel_states.keys())\n        self.base.nodes.loc[self._nodes_in_view, channel_cols] = float(\"nan\")\n        self.base.nodes.loc[self._nodes_in_view, name] = False\n\n        # only delete cols if no other comps in the module have the same channel\n        if np.all(~self.base.nodes[name]):\n            self.base.channels.pop(all_channel_names.index(name))\n            self.base.membrane_current_names.remove(channel.current_name)\n            self.base.nodes.drop(columns=channel_cols + [name], inplace=True)\n    else:\n        raise ValueError(f\"Channel {name} not found in the module.\")\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.delete_clamps","title":"delete_clamps(state_name=None)","text":"

Removes all clamps of the given state from the module.

Source code in jaxley/modules/base.py
def delete_clamps(self, state_name: Optional[str] = None):\n    \"\"\"Removes all clamps of the given state from the module.\"\"\"\n    all_externals = list(self.externals.keys())\n    if \"i\" in all_externals:\n        all_externals.remove(\"i\")\n    state_names = all_externals if state_name is None else [state_name]\n    for state_name in state_names:\n        if state_name in self.externals:\n            keep_inds = ~np.isin(\n                self.base.external_inds[state_name], self._nodes_in_view\n            )\n            base_exts = self.base.externals\n            base_exts_inds = self.base.external_inds\n            if np.all(~keep_inds):\n                base_exts.pop(state_name, None)\n                base_exts_inds.pop(state_name, None)\n            else:\n                base_exts[state_name] = base_exts[state_name][keep_inds]\n                base_exts_inds[state_name] = base_exts_inds[state_name][keep_inds]\n            self._update_view()\n        else:\n            pass  # does not have to be deleted if not in externals\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.delete_recordings","title":"delete_recordings()","text":"

Removes all recordings from the module.

Source code in jaxley/modules/base.py
def delete_recordings(self):\n    \"\"\"Removes all recordings from the module.\"\"\"\n    if isinstance(self, View):\n        base_recs = self.base.recordings\n        self.base.recordings = base_recs[\n            ~base_recs.isin(self.recordings).all(axis=1)\n        ]\n        self._update_view()\n    else:\n        self.base.recordings = pd.DataFrame().from_dict({})\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.delete_stimuli","title":"delete_stimuli()","text":"

Removes all stimuli from the module.

Source code in jaxley/modules/base.py
def delete_stimuli(self):\n    \"\"\"Removes all stimuli from the module.\"\"\"\n    self.delete_clamps(\"i\")\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.delete_trainables","title":"delete_trainables()","text":"

Removes all trainable parameters from the module.

Source code in jaxley/modules/base.py
def delete_trainables(self):\n    \"\"\"Removes all trainable parameters from the module.\"\"\"\n\n    if isinstance(self, View):\n        trainables_and_inds = self._filter_trainables(is_viewed=False)\n        self.base.indices_set_by_trainables = trainables_and_inds[0]\n        self.base.trainable_params = trainables_and_inds[1]\n        self.base.num_trainable_params -= self.num_trainable_params\n    else:\n        self.base.indices_set_by_trainables = []\n        self.base.trainable_params = []\n        self.base.num_trainable_params = 0\n    self._update_view()\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.distance","title":"distance(endpoint)","text":"

Return the direct distance between two compartments. This does not compute the pathwise distance (which is currently not implemented). Args: endpoint: The compartment to which to compute the distance to.

Source code in jaxley/modules/base.py
def distance(self, endpoint: \"View\") -> float:\n    \"\"\"Return the direct distance between two compartments.\n    This does not compute the pathwise distance (which is currently not\n    implemented).\n    Args:\n        endpoint: The compartment to which to compute the distance to.\n    \"\"\"\n    assert len(self.xyzr) == 1 and len(endpoint.xyzr) == 1\n    start_xyz = np.mean(self.xyzr[0][:, :3], axis=0)\n    end_xyz = np.mean(endpoint.xyzr[0][:, :3], axis=0)\n    return np.sqrt(np.sum((start_xyz - end_xyz) ** 2))\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.edge","title":"edge(idx)","text":"

Return a View of the module at the selected synapse edges(s).

Parameters:

Name Type Description Default idx Any

index of the edge to view.

required

Returns:

Type Description View

View of the module at the specified edge index.

Source code in jaxley/modules/base.py
def edge(self, idx: Any) -> View:\n    \"\"\"Return a View of the module at the selected synapse edges(s).\n\n    Args:\n        idx: index of the edge to view.\n\n    Returns:\n        View of the module at the specified edge index.\"\"\"\n    return self._at_edges(\"edge\", idx)\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.get_all_parameters","title":"get_all_parameters(pstate, voltage_solver)","text":"

Return all parameters (and coupling conductances) needed to simulate.

Runs _compute_axial_conductances() and return every parameter that is needed to solve the ODE. This includes conductances, radiuses, lengths, axial_resistivities, but also coupling conductances.

This is done by first obtaining the current value of every parameter (not only the trainable ones) and then replacing the trainable ones with the value in trainable_params(). This function is run within jx.integrate().

pstate can be obtained by calling params_to_pstate().

.. code-block:: python

params = module.get_parameters() # i.e. [0, 1, 2]\npstate = params_to_pstate(params, module.indices_set_by_trainables)\nmodule.to_jax() # needed for call to module.jaxnodes\n

Parameters:

Name Type Description Default pstate List[Dict]

The state of the trainable parameters. pstate takes the form [{ \u201ckey\u201d: \u201cgNa\u201d, \u201cindices\u201d: jnp.array([0, 1, 2]), \u201cval\u201d: jnp.array([0.1, 0.2, 0.3]) }, \u2026].

required voltage_solver str

The voltage solver that is used. Since jax.sparse and jaxley.xyz require different formats of the axial conductances, this function will default to different building methods.

required

Returns:

Type Description Dict[str, ndarray]

A dictionary of all module parameters.

Source code in jaxley/modules/base.py
@only_allow_module\ndef get_all_parameters(\n    self, pstate: List[Dict], voltage_solver: str\n) -> Dict[str, jnp.ndarray]:\n    # TODO FROM #447: MAKE THIS WORK FOR VIEW?\n    \"\"\"Return all parameters (and coupling conductances) needed to simulate.\n\n    Runs `_compute_axial_conductances()` and return every parameter that is needed\n    to solve the ODE. This includes conductances, radiuses, lengths,\n    axial_resistivities, but also coupling conductances.\n\n    This is done by first obtaining the current value of every parameter (not only\n    the trainable ones) and then replacing the trainable ones with the value\n    in `trainable_params()`. This function is run within `jx.integrate()`.\n\n    pstate can be obtained by calling `params_to_pstate()`.\n\n    .. code-block:: python\n\n        params = module.get_parameters() # i.e. [0, 1, 2]\n        pstate = params_to_pstate(params, module.indices_set_by_trainables)\n        module.to_jax() # needed for call to module.jaxnodes\n\n    Args:\n        pstate: The state of the trainable parameters. pstate takes the form\n            [{\n                \"key\": \"gNa\", \"indices\": jnp.array([0, 1, 2]),\n                \"val\": jnp.array([0.1, 0.2, 0.3])\n            }, ...].\n        voltage_solver: The voltage solver that is used. Since `jax.sparse` and\n            `jaxley.xyz` require different formats of the axial conductances, this\n            function will default to different building methods.\n\n    Returns:\n        A dictionary of all module parameters.\n    \"\"\"\n    params = {}\n    for key in [\"radius\", \"length\", \"axial_resistivity\", \"capacitance\"]:\n        params[key] = self.base.jaxnodes[key]\n\n    for channel in self.base.channels:\n        for channel_params in channel.channel_params:\n            params[channel_params] = self.base.jaxnodes[channel_params]\n\n    for synapse_params in self.base.synapse_param_names:\n        params[synapse_params] = self.base.jaxedges[synapse_params]\n\n    # Override with those parameters set by `.make_trainable()`.\n    for parameter in pstate:\n        key = parameter[\"key\"]\n        inds = parameter[\"indices\"]\n        set_param = parameter[\"val\"]\n\n        # This is needed since SynapseViews worked differently before.\n        # This mimics the old behaviour and tranformes the new indices\n        # to the old indices.\n        # TODO FROM #447: Longterm this should be gotten rid of.\n        # Instead edges should work similar to nodes (would also allow for\n        # param sharing).\n        synapse_inds = self.base.edges.groupby(\"type\").rank()[\"global_edge_index\"]\n        synapse_inds = (synapse_inds.astype(int) - 1).to_numpy()\n        if key in self.base.synapse_param_names:\n            inds = synapse_inds[inds]\n\n        if key in params:  # Only parameters, not initial states.\n            # `inds` is of shape `(num_params, num_comps_per_param)`.\n            # `set_param` is of shape `(num_params,)`\n            # We need to unsqueeze `set_param` to make it `(num_params, 1)` for the\n            # `.set()` to work. This is done with `[:, None]`.\n            params[key] = params[key].at[inds].set(set_param[:, None])\n\n    # Compute conductance params and add them to the params dictionary.\n    params[\"axial_conductances\"] = self.base._compute_axial_conductances(\n        params=params\n    )\n    return params\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.get_all_states","title":"get_all_states(pstate, all_params, delta_t)","text":"

Get the full initial state of the module from jaxnodes and trainables.

Parameters:

Name Type Description Default pstate List[Dict]

The state of the trainable parameters.

required all_params

All parameters of the module.

required delta_t float

The time step.

required

Returns:

Type Description Dict[str, ndarray]

A dictionary of all states of the module.

Source code in jaxley/modules/base.py
@only_allow_module\ndef get_all_states(\n    self, pstate: List[Dict], all_params, delta_t: float\n) -> Dict[str, jnp.ndarray]:\n    # TODO FROM #447: MAKE THIS WORK FOR VIEW?\n    \"\"\"Get the full initial state of the module from jaxnodes and trainables.\n\n    Args:\n        pstate: The state of the trainable parameters.\n        all_params: All parameters of the module.\n        delta_t: The time step.\n\n    Returns:\n        A dictionary of all states of the module.\n    \"\"\"\n    states = self.base._get_states_from_nodes_and_edges()\n\n    # Override with the initial states set by `.make_trainable()`.\n    for parameter in pstate:\n        key = parameter[\"key\"]\n        inds = parameter[\"indices\"]\n        set_param = parameter[\"val\"]\n        if key in states:  # Only initial states, not parameters.\n            # `inds` is of shape `(num_params, num_comps_per_param)`.\n            # `set_param` is of shape `(num_params,)`\n            # We need to unsqueeze `set_param` to make it `(num_params, 1)` for the\n            # `.set()` to work. This is done with `[:, None]`.\n            states[key] = states[key].at[inds].set(set_param[:, None])\n\n    # Add to the states the initial current through every channel.\n    states, _ = self.base._channel_currents(\n        states, delta_t, self.channels, self.nodes, all_params\n    )\n\n    # Add to the states the initial current through every synapse.\n    states, _ = self.base._synapse_currents(\n        states, self.synapses, all_params, delta_t, self.edges\n    )\n    return states\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.get_parameters","title":"get_parameters()","text":"

Get all trainable parameters.

The returned parameters should be passed to `jx.integrate(\u2026, params=params).

Returns:

Type Description List[Dict[str, ndarray]]

A list of all trainable parameters in the form of [{\u201cgNa\u201d: jnp.array([0.1, 0.2, 0.3])}, \u2026].

Source code in jaxley/modules/base.py
def get_parameters(self) -> List[Dict[str, jnp.ndarray]]:\n    \"\"\"Get all trainable parameters.\n\n    The returned parameters should be passed to `jx.integrate(..., params=params).\n\n    Returns:\n        A list of all trainable parameters in the form of\n            [{\"gNa\": jnp.array([0.1, 0.2, 0.3])}, ...].\n    \"\"\"\n    return self.trainable_params\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.init_states","title":"init_states(delta_t=0.025)","text":"

Initialize all mechanisms in their steady state.

This considers the voltages and parameters of each compartment.

Parameters:

Name Type Description Default delta_t float

Passed on to channel.init_state().

0.025 Source code in jaxley/modules/base.py
@only_allow_module\ndef init_states(self, delta_t: float = 0.025):\n    # TODO FROM #447: MAKE THIS WORK FOR VIEW?\n    \"\"\"Initialize all mechanisms in their steady state.\n\n    This considers the voltages and parameters of each compartment.\n\n    Args:\n        delta_t: Passed on to `channel.init_state()`.\n    \"\"\"\n    # Update states of the channels.\n    channel_nodes = self.base.nodes\n    states = self.base._get_states_from_nodes_and_edges()\n\n    # We do not use any `pstate` for initializing. In principle, we could change\n    # that by allowing an input `params` and `pstate` to this function.\n    # `voltage_solver` could also be `jax.sparse` here, because both of them\n    # build the channel parameters in the same way.\n    params = self.base.get_all_parameters([], voltage_solver=\"jaxley.thomas\")\n\n    for channel in self.base.channels:\n        name = channel._name\n        channel_indices = channel_nodes.loc[channel_nodes[name]][\n            \"global_comp_index\"\n        ].to_numpy()\n        voltages = channel_nodes.loc[channel_indices, \"v\"].to_numpy()\n\n        channel_param_names = list(channel.channel_params.keys())\n        channel_state_names = list(channel.channel_states.keys())\n        channel_states = query_channel_states_and_params(\n            states, channel_state_names, channel_indices\n        )\n        channel_params = query_channel_states_and_params(\n            params, channel_param_names, channel_indices\n        )\n\n        init_state = channel.init_state(\n            channel_states, voltages, channel_params, delta_t\n        )\n\n        # `init_state` might not return all channel states. Only the ones that are\n        # returned are updated here.\n        for key, val in init_state.items():\n            # Note that we are overriding `self.nodes` here, but `self.nodes` is\n            # not used above to actually compute the current states (so there are\n            # no issues with overriding states).\n            self.nodes.loc[channel_indices, key] = val\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.insert","title":"insert(channel)","text":"

Insert a channel into the module.

Parameters:

Name Type Description Default channel Channel

The channel to insert.

required Source code in jaxley/modules/base.py
def insert(self, channel: Channel):\n    \"\"\"Insert a channel into the module.\n\n    Args:\n        channel: The channel to insert.\"\"\"\n    name = channel._name\n\n    # Channel does not yet exist in the `jx.Module` at all.\n    if name not in [c._name for c in self.base.channels]:\n        self.base.channels.append(channel)\n        self.base.nodes[name] = (\n            False  # Previous columns do not have the new channel.\n        )\n\n    if channel.current_name not in self.base.membrane_current_names:\n        self.base.membrane_current_names.append(channel.current_name)\n\n    # Add a binary column that indicates if a channel is present.\n    self.base.nodes.loc[self._nodes_in_view, name] = True\n\n    # Loop over all new parameters, e.g. gNa, eNa.\n    for key in channel.channel_params:\n        self.base.nodes.loc[self._nodes_in_view, key] = channel.channel_params[key]\n\n    # Loop over all new parameters, e.g. gNa, eNa.\n    for key in channel.channel_states:\n        self.base.nodes.loc[self._nodes_in_view, key] = channel.channel_states[key]\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.loc","title":"loc(at)","text":"

Return a View of the module at the selected branch location(s).

Parameters:

Name Type Description Default at Any

location along the branch.

required

Returns:

Type Description View

View of the module at the specified branch location.

Source code in jaxley/modules/base.py
def loc(self, at: Any) -> View:\n    \"\"\"Return a View of the module at the selected branch location(s).\n\n    Args:\n        at: location along the branch.\n\n    Returns:\n        View of the module at the specified branch location.\"\"\"\n    global_comp_idxs = []\n    for i in self._branches_in_view:\n        ncomp = self.base.ncomp_per_branch[i]\n        comp_locs = np.linspace(0, 1, ncomp)\n        at = comp_locs if is_str_all(at) else self._reformat_index(at, dtype=float)\n        comp_edges = np.linspace(0, 1 + 1e-10, ncomp + 1)\n        idx = np.digitize(at, comp_edges) - 1 + self.base.cumsum_ncomp[i]\n        global_comp_idxs.append(idx)\n    global_comp_idxs = np.concatenate(global_comp_idxs)\n    orig_scope = self._scope\n    # global scope needed to select correct comps, for i.e. branches w. ncomp=[1,2]\n    # loc(0.9)  will correspond to different local branches (0 vs 1).\n    view = self.scope(\"global\").comp(global_comp_idxs).scope(orig_scope)\n    view._current_view = \"loc\"\n    return view\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.make_trainable","title":"make_trainable(key, init_val=None, verbose=True)","text":"

Make a parameter trainable.

If a parameter is made trainable, it will be returned by get_parameters() and should then be passed to jx.integrate(..., params=params).

Parameters:

Name Type Description Default key str

Name of the parameter to make trainable.

required init_val Optional[Union[float, list]]

Initial value of the parameter. If float, the same value is used for every created parameter. If list, the length of the list has to match the number of created parameters. If None, the current parameter value is used and if parameter sharing is performed that the current parameter value is averaged over all shared parameters.

None verbose bool

Whether to print the number of parameters that are added and the total number of parameters.

True Source code in jaxley/modules/base.py
def make_trainable(\n    self,\n    key: str,\n    init_val: Optional[Union[float, list]] = None,\n    verbose: bool = True,\n):\n    \"\"\"Make a parameter trainable.\n\n    If a parameter is made trainable, it will be returned by `get_parameters()`\n    and should then be passed to `jx.integrate(..., params=params)`.\n\n    Args:\n        key: Name of the parameter to make trainable.\n        init_val: Initial value of the parameter. If `float`, the same value is\n            used for every created parameter. If `list`, the length of the list has\n            to match the number of created parameters. If `None`, the current\n            parameter value is used and if parameter sharing is performed that the\n            current parameter value is averaged over all shared parameters.\n        verbose: Whether to print the number of parameters that are added and the\n            total number of parameters.\n    \"\"\"\n    assert (\n        self.allow_make_trainable\n    ), \"network.cell('all').make_trainable() is not supported. Use a for-loop over cells.\"\n    ncomps_per_branch = (\n        self.base.nodes[\"global_branch_index\"].value_counts().to_numpy()\n    )\n    assert np.all(\n        ncomps_per_branch == ncomps_per_branch[0]\n    ), \"Parameter sharing is not allowed for modules containing branches with different numbers of compartments.\"\n\n    data = self.nodes if key in self.nodes.columns else None\n    data = self.edges if key in self.edges.columns else data\n\n    assert data is not None, f\"Key '{key}' not found in nodes or edges\"\n    not_nan = ~data[key].isna()\n    data = data.loc[not_nan]\n    assert (\n        len(data) > 0\n    ), \"No settable parameters found in the selected compartments.\"\n\n    grouped_view = data.groupby(\"controlled_by_param\")\n    # Because of this `x.index.values` we cannot support `make_trainable()` on\n    # the module level for synapse parameters (but only for `SynapseView`).\n    inds_of_comps = list(\n        grouped_view.apply(lambda x: x.index.values, include_groups=False)\n    )\n    indices_per_param = jnp.stack(inds_of_comps)\n    # Sorted inds are only used to infer the correct starting values.\n    param_vals = jnp.asarray(\n        [data.loc[inds, key].to_numpy() for inds in inds_of_comps]\n    )\n\n    # Set the value which the trainable parameter should take.\n    num_created_parameters = len(indices_per_param)\n    if init_val is not None:\n        if isinstance(init_val, float):\n            new_params = jnp.asarray([init_val] * num_created_parameters)\n        elif isinstance(init_val, list):\n            assert (\n                len(init_val) == num_created_parameters\n            ), f\"len(init_val)={len(init_val)}, but trying to create {num_created_parameters} parameters.\"\n            new_params = jnp.asarray(init_val)\n        else:\n            raise ValueError(\n                f\"init_val must a float, list, or None, but it is a {type(init_val).__name__}.\"\n            )\n    else:\n        new_params = jnp.mean(param_vals, axis=1)\n    self.base.trainable_params.append({key: new_params})\n    self.base.indices_set_by_trainables.append(indices_per_param)\n    self.base.num_trainable_params += num_created_parameters\n    if verbose:\n        print(\n            f\"Number of newly added trainable parameters: {num_created_parameters}. Total number of trainable parameters: {self.base.num_trainable_params}\"\n        )\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.move","title":"move(x=0.0, y=0.0, z=0.0, update_nodes=False)","text":"

Move cells or networks by adding to their (x, y, z) coordinates.

This function is used only for visualization. It does not affect the simulation.

Parameters:

Name Type Description Default x float

The amount to move in the x direction in um.

0.0 y float

The amount to move in the y direction in um.

0.0 z float

The amount to move in the z direction in um.

0.0 update_nodes bool

Whether .nodes should be updated or not. Setting this to False largely speeds up moving, especially for big networks, but .nodes or .show will not show the new xyz coordinates.

False Source code in jaxley/modules/base.py
def move(\n    self, x: float = 0.0, y: float = 0.0, z: float = 0.0, update_nodes: bool = False\n):\n    \"\"\"Move cells or networks by adding to their (x, y, z) coordinates.\n\n    This function is used only for visualization. It does not affect the simulation.\n\n    Args:\n        x: The amount to move in the x direction in um.\n        y: The amount to move in the y direction in um.\n        z: The amount to move in the z direction in um.\n        update_nodes: Whether `.nodes` should be updated or not. Setting this to\n            `False` largely speeds up moving, especially for big networks, but\n            `.nodes` or `.show` will not show the new xyz coordinates.\n    \"\"\"\n    for i in self._branches_in_view:\n        self.base.xyzr[i][:, :3] += np.array([x, y, z])\n    if update_nodes:\n        self.compute_compartment_centers()\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.move_to","title":"move_to(x=0.0, y=0.0, z=0.0, update_nodes=False)","text":"

Move cells or networks to a location (x, y, z).

If x, y, and z are floats, then the first compartment of the first branch of the first cell is moved to that float coordinate, and everything else is shifted by the difference between that compartment\u2019s previous coordinate and the new float location.

If x, y, and z are arrays, then they must each have a length equal to the number of cells being moved. Then the first compartment of the first branch of each cell is moved to the specified location.

Parameters:

Name Type Description Default update_nodes bool

Whether .nodes should be updated or not. Setting this to False largely speeds up moving, especially for big networks, but .nodes or .show will not show the new xyz coordinates.

False Source code in jaxley/modules/base.py
def move_to(\n    self,\n    x: Union[float, np.ndarray] = 0.0,\n    y: Union[float, np.ndarray] = 0.0,\n    z: Union[float, np.ndarray] = 0.0,\n    update_nodes: bool = False,\n):\n    \"\"\"Move cells or networks to a location (x, y, z).\n\n    If x, y, and z are floats, then the first compartment of the first branch\n    of the first cell is moved to that float coordinate, and everything else is\n    shifted by the difference between that compartment's previous coordinate and\n    the new float location.\n\n    If x, y, and z are arrays, then they must each have a length equal to the number\n    of cells being moved. Then the first compartment of the first branch of each\n    cell is moved to the specified location.\n\n    Args:\n        update_nodes: Whether `.nodes` should be updated or not. Setting this to\n            `False` largely speeds up moving, especially for big networks, but\n            `.nodes` or `.show` will not show the new xyz coordinates.\n    \"\"\"\n    # Test if any coordinate values are NaN which would greatly affect moving\n    if np.any(np.concatenate(self.xyzr, axis=0)[:, :3] == np.nan):\n        raise ValueError(\n            \"NaN coordinate values detected. Shift amounts cannot be computed. Please run compute_xyzr() or assign initial coordinate values.\"\n        )\n\n    # can only iterate over cells for networks\n    # lambda makes sure that generator can be created multiple times\n    base_is_net = self.base._current_view == \"network\"\n    cells = lambda: (self.cells if base_is_net else [self])\n\n    root_xyz_cells = np.array([c.xyzr[0][0, :3] for c in cells()])\n    root_xyz = root_xyz_cells[0] if isinstance(x, float) else root_xyz_cells\n    move_by = np.array([x, y, z]).T - root_xyz\n\n    if len(move_by.shape) == 1:\n        move_by = np.tile(move_by, (len(self._cells_in_view), 1))\n\n    for cell, offset in zip(cells(), move_by):\n        for idx in cell._branches_in_view:\n            self.base.xyzr[idx][:, :3] += offset\n    if update_nodes:\n        self.compute_compartment_centers()\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.rotate","title":"rotate(degrees, rotation_axis='xy', update_nodes=False)","text":"

Rotate jaxley modules clockwise. Used only for visualization.

This function is used only for visualization. It does not affect the simulation.

Parameters:

Name Type Description Default degrees float

How many degrees to rotate the module by.

required rotation_axis str

Either of {xy | xz | yz}.

'xy' Source code in jaxley/modules/base.py
def rotate(\n    self, degrees: float, rotation_axis: str = \"xy\", update_nodes: bool = False\n):\n    \"\"\"Rotate jaxley modules clockwise. Used only for visualization.\n\n    This function is used only for visualization. It does not affect the simulation.\n\n    Args:\n        degrees: How many degrees to rotate the module by.\n        rotation_axis: Either of {`xy` | `xz` | `yz`}.\n    \"\"\"\n    degrees = degrees / 180 * np.pi\n    if rotation_axis == \"xy\":\n        dims = [0, 1]\n    elif rotation_axis == \"xz\":\n        dims = [0, 2]\n    elif rotation_axis == \"yz\":\n        dims = [1, 2]\n    else:\n        raise ValueError\n\n    rotation_matrix = np.asarray(\n        [[np.cos(degrees), np.sin(degrees)], [-np.sin(degrees), np.cos(degrees)]]\n    )\n    for i in self._branches_in_view:\n        rot = np.dot(rotation_matrix, self.base.xyzr[i][:, dims].T).T\n        self.base.xyzr[i][:, dims] = rot\n    if update_nodes:\n        self.compute_compartment_centers()\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.scope","title":"scope(scope)","text":"

Return a View of the module with the specified scope.

For example cell.scope(\"global\").branch(2).scope(\"local\").comp(1) will return the 1st compartment of branch 2.

Parameters:

Name Type Description Default scope str

either \u201cglobal\u201d or \u201clocal\u201d.

required

Returns:

Type Description View

View with the specified scope.

Source code in jaxley/modules/base.py
def scope(self, scope: str) -> View:\n    \"\"\"Return a View of the module with the specified scope.\n\n    For example `cell.scope(\"global\").branch(2).scope(\"local\").comp(1)`\n    will return the 1st compartment of branch 2.\n\n    Args:\n        scope: either \"global\" or \"local\".\n\n    Returns:\n        View with the specified scope.\"\"\"\n    view = self.view\n    view.set_scope(scope)\n    return view\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.select","title":"select(nodes=None, edges=None, sorted=False)","text":"

Return View of the module filtered by specific node or edges indices.

Parameters:

Name Type Description Default nodes ndarray

indices of nodes to view. If None, all nodes are viewed.

None edges ndarray

indices of edges to view. If None, all edges are viewed.

None sorted bool

if True, nodes and edges are sorted.

False

Returns:

Type Description View

View for subset of selected nodes and/or edges.

Source code in jaxley/modules/base.py
def select(\n    self, nodes: np.ndarray = None, edges: np.ndarray = None, sorted: bool = False\n) -> View:\n    \"\"\"Return View of the module filtered by specific node or edges indices.\n\n    Args:\n        nodes: indices of nodes to view. If None, all nodes are viewed.\n        edges: indices of edges to view. If None, all edges are viewed.\n        sorted: if True, nodes and edges are sorted.\n\n    Returns:\n        View for subset of selected nodes and/or edges.\"\"\"\n\n    nodes = self._reformat_index(nodes) if nodes is not None else None\n    nodes = self._nodes_in_view if is_str_all(nodes) else nodes\n    nodes = np.sort(nodes) if sorted else nodes\n\n    edges = self._reformat_index(edges) if edges is not None else None\n    edges = self._edges_in_view if is_str_all(edges) else edges\n    edges = np.sort(edges) if sorted else edges\n\n    view = View(self, nodes, edges)\n    view._set_controlled_by_param(\"filter\")\n    return view\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.set","title":"set(key, val)","text":"

Set parameter of module (or its view) to a new value.

Note that this function can not be called within jax.jit or jax.grad. Instead, it should be used set the parameters of the module before the simulation. Use .data_set() to set parameters during jax.jit or jax.grad.

Parameters:

Name Type Description Default key str

The name of the parameter to set.

required val Union[float, ndarray]

The value to set the parameter to. If it is jnp.ndarray then it must be of shape (len(num_compartments)).

required Source code in jaxley/modules/base.py
def set(self, key: str, val: Union[float, jnp.ndarray]):\n    \"\"\"Set parameter of module (or its view) to a new value.\n\n    Note that this function can not be called within `jax.jit` or `jax.grad`.\n    Instead, it should be used set the parameters of the module **before** the\n    simulation. Use `.data_set()` to set parameters during `jax.jit` or\n    `jax.grad`.\n\n    Args:\n        key: The name of the parameter to set.\n        val: The value to set the parameter to. If it is `jnp.ndarray` then it\n            must be of shape `(len(num_compartments))`.\n    \"\"\"\n    if key in self.nodes.columns:\n        not_nan = ~self.nodes[key].isna().to_numpy()\n        self.base.nodes.loc[self._nodes_in_view[not_nan], key] = val\n    elif key in self.edges.columns:\n        not_nan = ~self.edges[key].isna().to_numpy()\n        self.base.edges.loc[self._edges_in_view[not_nan], key] = val\n    else:\n        raise KeyError(f\"Key '{key}' not found in nodes or edges\")\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.set_ncomp","title":"set_ncomp(ncomp, min_radius=None)","text":"

Set the number of compartments with which the branch is discretized.

Parameters:

Name Type Description Default ncomp int

The number of compartments that the branch should be discretized into.

required min_radius Optional[float]

Only used if the morphology was read from an SWC file. If passed the radius is capped to be at least this value.

None Source code in jaxley/modules/base.py
def set_ncomp(\n    self,\n    ncomp: int,\n    min_radius: Optional[float] = None,\n):\n    \"\"\"Set the number of compartments with which the branch is discretized.\n\n    Args:\n        ncomp: The number of compartments that the branch should be discretized\n            into.\n        min_radius: Only used if the morphology was read from an SWC file. If passed\n            the radius is capped to be at least this value.\n\n    Raises:\n        - When there are stimuli in any compartment in the module.\n        - When there are recordings in any compartment in the module.\n        - When the channels of the compartments are not the same within the branch\n        that is modified.\n        - When the lengths of the compartments are not the same within the branch\n        that is modified.\n        - Unless the morphology was read from an SWC file, when the radiuses of the\n        compartments are not the same within the branch that is modified.\n    \"\"\"\n    assert len(self.base.externals) == 0, \"No stimuli allowed!\"\n    assert len(self.base.recordings) == 0, \"No recordings allowed!\"\n    assert len(self.base.trainable_params) == 0, \"No trainables allowed!\"\n\n    assert self.base._module_type != \"network\", \"This is not allowed for networks.\"\n    assert not (\n        self.base._module_type == \"cell\"\n        and len(self._branches_in_view) == len(self.base._branches_in_view)\n    ), \"This is not allowed for cells.\"\n\n    # Update all attributes that are affected by compartment structure.\n    view = self.nodes.copy()\n    all_nodes = self.base.nodes\n    start_idx = self.nodes[\"global_comp_index\"].to_numpy()[0]\n    ncomp_per_branch = self.base.ncomp_per_branch\n    channel_names = [c._name for c in self.base.channels]\n    channel_param_names = list(\n        chain(*[c.channel_params for c in self.base.channels])\n    )\n    channel_state_names = list(\n        chain(*[c.channel_states for c in self.base.channels])\n    )\n    radius_generating_fns = self.base._radius_generating_fns\n\n    within_branch_radiuses = view[\"radius\"].to_numpy()\n    compartment_lengths = view[\"length\"].to_numpy()\n    num_previous_ncomp = len(within_branch_radiuses)\n    branch_indices = pd.unique(view[\"global_branch_index\"])\n\n    error_msg = lambda name: (\n        f\"You previously modified the {name} of individual compartments, but \"\n        f\"now you are modifying the number of compartments in this branch. \"\n        f\"This is not allowed. First build the morphology with `set_ncomp()` and \"\n        f\"then modify the radiuses and lengths of compartments.\"\n    )\n\n    if (\n        ~np.all(within_branch_radiuses == within_branch_radiuses[0])\n        and radius_generating_fns is None\n    ):\n        raise ValueError(error_msg(\"radius\"))\n\n    for property_name in [\"length\", \"capacitance\", \"axial_resistivity\"]:\n        compartment_properties = view[property_name].to_numpy()\n        if ~np.all(compartment_properties == compartment_properties[0]):\n            raise ValueError(error_msg(property_name))\n\n    if not (self.nodes[channel_names].var() == 0.0).all():\n        raise ValueError(\n            \"Some channel exists only in some compartments of the branch which you\"\n            \"are trying to modify. This is not allowed. First specify the number\"\n            \"of compartments with `.set_ncomp()` and then insert the channels\"\n            \"accordingly.\"\n        )\n\n    if not (\n        self.nodes[channel_param_names + channel_state_names].var() == 0.0\n    ).all():\n        raise ValueError(\n            \"Some channel has different parameters or states between the \"\n            \"different compartments of the branch which you are trying to modify. \"\n            \"This is not allowed. First specify the number of compartments with \"\n            \"`.set_ncomp()` and then insert the channels accordingly.\"\n        )\n\n    # Add new rows as the average of all rows. Special case for the length is below.\n    average_row = self.nodes.mean(skipna=False)\n    average_row = average_row.to_frame().T\n    view = pd.concat([*[average_row] * ncomp], axis=\"rows\")\n\n    # Set the correct datatype after having performed an average which cast\n    # everything to float.\n    integer_cols = [\"global_cell_index\", \"global_branch_index\", \"global_comp_index\"]\n    view[integer_cols] = view[integer_cols].astype(int)\n\n    # Whether or not a channel exists in a compartment is a boolean.\n    boolean_cols = channel_names\n    view[boolean_cols] = view[boolean_cols].astype(bool)\n\n    # Special treatment for the lengths and radiuses. These are not being set as\n    # the average because we:\n    # 1) Want to maintain the total length of a branch.\n    # 2) Want to use the SWC inferred radius.\n    #\n    # Compute new compartment lengths.\n    comp_lengths = np.sum(compartment_lengths) / ncomp\n    view[\"length\"] = comp_lengths\n\n    # Compute new compartment radiuses.\n    if radius_generating_fns is not None:\n        view[\"radius\"] = build_radiuses_from_xyzr(\n            radius_fns=radius_generating_fns,\n            branch_indices=branch_indices,\n            min_radius=min_radius,\n            ncomp=ncomp,\n        )\n    else:\n        view[\"radius\"] = within_branch_radiuses[0] * np.ones(ncomp)\n\n    # Update `.nodes`.\n    # 1) Delete N rows starting from start_idx\n    number_deleted = num_previous_ncomp\n    all_nodes = all_nodes.drop(index=range(start_idx, start_idx + number_deleted))\n\n    # 2) Insert M new rows at the same location\n    df1 = all_nodes.iloc[:start_idx]  # Rows before the insertion point\n    df2 = all_nodes.iloc[start_idx:]  # Rows after the insertion point\n\n    # 3) Combine the parts: before, new rows, and after\n    all_nodes = pd.concat([df1, view, df2]).reset_index(drop=True)\n\n    # Override `comp_index` to just be a consecutive list.\n    all_nodes[\"global_comp_index\"] = np.arange(len(all_nodes))\n\n    # Update compartment structure arguments.\n    ncomp_per_branch[branch_indices] = ncomp\n    ncomp = int(np.max(ncomp_per_branch))\n    cumsum_ncomp = cumsum_leading_zero(ncomp_per_branch)\n    internal_node_inds = np.arange(cumsum_ncomp[-1])\n\n    self.base.nodes = all_nodes\n    self.base.ncomp_per_branch = ncomp_per_branch\n    self.base.ncomp = ncomp\n    self.base.cumsum_ncomp = cumsum_ncomp\n    self.base._internal_node_inds = internal_node_inds\n\n    # Update the morphology indexing (e.g., `.comp_edges`).\n    self.base._initialize()\n    self.base._init_view()\n    self.base._update_local_indices()\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.set_scope","title":"set_scope(scope)","text":"

Toggle between \u201cglobal\u201d or \u201clocal\u201d scope.

Determines if global or local indices are used for viewing the module.

Parameters:

Name Type Description Default scope str

either \u201cglobal\u201d or \u201clocal\u201d.

required Source code in jaxley/modules/base.py
def set_scope(self, scope: str):\n    \"\"\"Toggle between \"global\" or \"local\" scope.\n\n    Determines if global or local indices are used for viewing the module.\n\n    Args:\n        scope: either \"global\" or \"local\".\"\"\"\n    assert scope in [\"global\", \"local\"], \"Invalid scope.\"\n    self._scope = scope\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.show","title":"show(param_names=None, *, indices=True, params=True, states=True, channel_names=None)","text":"

Print detailed information about the Module or a view of it.

Parameters:

Name Type Description Default param_names Optional[Union[str, List[str]]]

The names of the parameters to show. If None, all parameters are shown.

None indices bool

Whether to show the indices of the compartments.

True params bool

Whether to show the parameters of the compartments.

True states bool

Whether to show the states of the compartments.

True channel_names Optional[List[str]]

The names of the channels to show. If None, all channels are shown.

None

Returns:

Type Description DataFrame

A pd.DataFrame with the requested information.

Source code in jaxley/modules/base.py
def show(\n    self,\n    param_names: Optional[Union[str, List[str]]] = None,\n    *,\n    indices: bool = True,\n    params: bool = True,\n    states: bool = True,\n    channel_names: Optional[List[str]] = None,\n) -> pd.DataFrame:\n    \"\"\"Print detailed information about the Module or a view of it.\n\n    Args:\n        param_names: The names of the parameters to show. If `None`, all parameters\n            are shown.\n        indices: Whether to show the indices of the compartments.\n        params: Whether to show the parameters of the compartments.\n        states: Whether to show the states of the compartments.\n        channel_names: The names of the channels to show. If `None`, all channels are\n            shown.\n\n    Returns:\n        A `pd.DataFrame` with the requested information.\n    \"\"\"\n    nodes = self.nodes.copy()  # prevents this from being edited\n\n    cols = []\n    inds = [\"comp_index\", \"branch_index\", \"cell_index\"]\n    scopes = [\"local\", \"global\"]\n    inds = [f\"{s}_{i}\" for i in inds for s in scopes] if indices else []\n    cols += inds\n    cols += [ch._name for ch in self.channels] if channel_names else []\n    cols += (\n        sum([list(ch.channel_params) for ch in self.channels], []) if params else []\n    )\n    cols += (\n        sum([list(ch.channel_states) for ch in self.channels], []) if states else []\n    )\n\n    if not param_names is None:\n        cols = (\n            inds + [c for c in cols if c in param_names]\n            if params\n            else list(param_names)\n        )\n\n    return nodes[cols]\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.step","title":"step(u, delta_t, external_inds, externals, params, solver='bwd_euler', voltage_solver='jaxley.stone')","text":"

One step of solving the Ordinary Differential Equation.

This function is called inside of integrate and increments the state of the module by one time step. Calls _step_channels and _step_synapse to update the states of the channels and synapses using fwd_euler.

Parameters:

Name Type Description Default u Dict[str, ndarray]

The state of the module. voltages = u[\u201cv\u201d]

required delta_t float

The time step.

required external_inds Dict[str, ndarray]

The indices of the external inputs.

required externals Dict[str, ndarray]

The external inputs.

required params Dict[str, ndarray]

The parameters of the module.

required solver str

The solver to use for the voltages. Either of [\u201cbwd_euler\u201d, \u201cfwd_euler\u201d, \u201ccrank_nicolson\u201d].

'bwd_euler' voltage_solver str

The tridiagonal solver used to diagonalize the coefficient matrix of the ODE system. Either of [\u201cjaxley.thomas\u201d, \u201cjaxley.stone\u201d].

'jaxley.stone'

Returns:

Type Description Dict[str, ndarray]

The updated state of the module.

Source code in jaxley/modules/base.py
@only_allow_module\ndef step(\n    self,\n    u: Dict[str, jnp.ndarray],\n    delta_t: float,\n    external_inds: Dict[str, jnp.ndarray],\n    externals: Dict[str, jnp.ndarray],\n    params: Dict[str, jnp.ndarray],\n    solver: str = \"bwd_euler\",\n    voltage_solver: str = \"jaxley.stone\",\n) -> Dict[str, jnp.ndarray]:\n    \"\"\"One step of solving the Ordinary Differential Equation.\n\n    This function is called inside of `integrate` and increments the state of the\n    module by one time step. Calls `_step_channels` and `_step_synapse` to update\n    the states of the channels and synapses using fwd_euler.\n\n    Args:\n        u: The state of the module. voltages = u[\"v\"]\n        delta_t: The time step.\n        external_inds: The indices of the external inputs.\n        externals: The external inputs.\n        params: The parameters of the module.\n        solver: The solver to use for the voltages. Either of [\"bwd_euler\",\n            \"fwd_euler\", \"crank_nicolson\"].\n        voltage_solver: The tridiagonal solver used to diagonalize the\n            coefficient matrix of the ODE system. Either of [\"jaxley.thomas\",\n            \"jaxley.stone\"].\n\n    Returns:\n        The updated state of the module.\n    \"\"\"\n\n    # Extract the voltages\n    voltages = u[\"v\"]\n\n    # Extract the external inputs\n    if \"i\" in externals.keys():\n        i_current = externals[\"i\"]\n        i_inds = external_inds[\"i\"]\n        i_ext = self._get_external_input(\n            voltages, i_inds, i_current, params[\"radius\"], params[\"length\"]\n        )\n    else:\n        i_ext = 0.0\n\n    # Step of the channels.\n    u, (v_terms, const_terms) = self._step_channels(\n        u, delta_t, self.channels, self.nodes, params\n    )\n\n    # Step of the synapse.\n    u, (syn_v_terms, syn_const_terms) = self._step_synapse(\n        u,\n        self.synapses,\n        params,\n        delta_t,\n        self.edges,\n    )\n\n    # Clamp for channels and synapses.\n    for key in externals.keys():\n        if key not in [\"i\", \"v\"]:\n            u[key] = u[key].at[external_inds[key]].set(externals[key])\n\n    # Voltage steps.\n    cm = params[\"capacitance\"]  # Abbreviation.\n\n    # Arguments used by all solvers.\n    solver_kwargs = {\n        \"voltages\": voltages,\n        \"voltage_terms\": (v_terms + syn_v_terms) / cm,\n        \"constant_terms\": (const_terms + i_ext + syn_const_terms) / cm,\n        \"axial_conductances\": params[\"axial_conductances\"],\n        \"internal_node_inds\": self._internal_node_inds,\n    }\n\n    # Add solver specific arguments.\n    if voltage_solver == \"jax.sparse\":\n        solver_kwargs.update(\n            {\n                \"sinks\": np.asarray(self._comp_edges[\"sink\"].to_list()),\n                \"data_inds\": self._data_inds,\n                \"indices\": self._indices_jax_spsolve,\n                \"indptr\": self._indptr_jax_spsolve,\n                \"n_nodes\": self._n_nodes,\n            }\n        )\n        # Only for `bwd_euler` and `cranck-nicolson`.\n        step_voltage_implicit = step_voltage_implicit_with_jax_spsolve\n    else:\n        # Our custom sparse solver requires a different format of all conductance\n        # values to perform triangulation and backsubstution optimally.\n        #\n        # Currently, the forward Euler solver also uses this format. However,\n        # this is only for historical reasons and we are planning to change this in\n        # the future.\n        solver_kwargs.update(\n            {\n                \"sinks\": np.asarray(self._comp_edges[\"sink\"].to_list()),\n                \"sources\": np.asarray(self._comp_edges[\"source\"].to_list()),\n                \"types\": np.asarray(self._comp_edges[\"type\"].to_list()),\n                \"ncomp_per_branch\": self.ncomp_per_branch,\n                \"par_inds\": self._par_inds,\n                \"child_inds\": self._child_inds,\n                \"nbranches\": self.total_nbranches,\n                \"solver\": voltage_solver,\n                \"idx\": self._solve_indexer,\n                \"debug_states\": self.debug_states,\n            }\n        )\n        # Only for `bwd_euler` and `cranck-nicolson`.\n        step_voltage_implicit = step_voltage_implicit_with_jaxley_spsolve\n\n    if solver == \"bwd_euler\":\n        u[\"v\"] = step_voltage_implicit(**solver_kwargs, delta_t=delta_t)\n    elif solver == \"crank_nicolson\":\n        # Crank-Nicolson advances by half a step of backward and half a step of\n        # forward Euler.\n        half_step_delta_t = delta_t / 2\n        half_step_voltages = step_voltage_implicit(\n            **solver_kwargs, delta_t=half_step_delta_t\n        )\n        # The forward Euler step in Crank-Nicolson can be performed easily as\n        # `V_{n+1} = 2 * V_{n+1/2} - V_n`. See also NEURON book Chapter 4.\n        u[\"v\"] = 2 * half_step_voltages - voltages\n    elif solver == \"fwd_euler\":\n        u[\"v\"] = step_voltage_explicit(**solver_kwargs, delta_t=delta_t)\n    else:\n        raise ValueError(\n            f\"You specified `solver={solver}`. The only allowed solvers are \"\n            \"['bwd_euler', 'fwd_euler', 'crank_nicolson'].\"\n        )\n\n    # Clamp for voltages.\n    if \"v\" in externals.keys():\n        u[\"v\"] = u[\"v\"].at[external_inds[\"v\"]].set(externals[\"v\"])\n\n    return u\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.stimulate","title":"stimulate(current=None, verbose=True)","text":"

Insert a stimulus into the compartment.

current must be a 1d array or have batch dimension of size (num_compartments, ) or (1, ). If 1d, the same stimulus is added to all compartments.

This function cannot be run during jax.jit and jax.grad. Because of this, it should only be used for static stimuli (i.e., stimuli that do not depend on the data and that should not be learned). For stimuli that depend on data (or that should be learned), please use data_stimulate().

Parameters:

Name Type Description Default current Optional[ndarray]

Current in nA.

None Source code in jaxley/modules/base.py
def stimulate(self, current: Optional[jnp.ndarray] = None, verbose: bool = True):\n    \"\"\"Insert a stimulus into the compartment.\n\n    current must be a 1d array or have batch dimension of size `(num_compartments, )`\n    or `(1, )`. If 1d, the same stimulus is added to all compartments.\n\n    This function cannot be run during `jax.jit` and `jax.grad`. Because of this,\n    it should only be used for static stimuli (i.e., stimuli that do not depend\n    on the data and that should not be learned). For stimuli that depend on data\n    (or that should be learned), please use `data_stimulate()`.\n\n    Args:\n        current: Current in `nA`.\n    \"\"\"\n    self._external_input(\"i\", current, verbose=verbose)\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.to_jax","title":"to_jax()","text":"

Move .nodes to .jaxnodes.

Before the actual simulation is run (via jx.integrate), all parameters of the jx.Module are stored in .nodes (a pd.DataFrame). However, for simulation, these parameters have to be moved to be jnp.ndarrays such that they can be processed on GPU/TPU and such that the simulation can be differentiated. .to_jax() copies the .nodes to .jaxnodes.

Source code in jaxley/modules/base.py
@only_allow_module\ndef to_jax(self):\n    # TODO FROM #447: Make this work for View?\n    \"\"\"Move `.nodes` to `.jaxnodes`.\n\n    Before the actual simulation is run (via `jx.integrate`), all parameters of\n    the `jx.Module` are stored in `.nodes` (a `pd.DataFrame`). However, for\n    simulation, these parameters have to be moved to be `jnp.ndarrays` such that\n    they can be processed on GPU/TPU and such that the simulation can be\n    differentiated. `.to_jax()` copies the `.nodes` to `.jaxnodes`.\n    \"\"\"\n    self.base.jaxnodes = {}\n    for key, value in self.base.nodes.to_dict(orient=\"list\").items():\n        inds = jnp.arange(len(value))\n        self.base.jaxnodes[key] = jnp.asarray(value)[inds]\n\n    # `jaxedges` contains only parameters (no indices).\n    # `jaxedges` contains only non-Nan elements. This is unlike the channels where\n    # we allow parameter sharing.\n    self.base.jaxedges = {}\n    edges = self.base.edges.to_dict(orient=\"list\")\n    for i, synapse in enumerate(self.base.synapses):\n        condition = np.asarray(edges[\"type_ind\"]) == i\n        for key in synapse.synapse_params:\n            self.base.jaxedges[key] = jnp.asarray(np.asarray(edges[key])[condition])\n        for key in synapse.synapse_states:\n            self.base.jaxedges[key] = jnp.asarray(np.asarray(edges[key])[condition])\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.vis","title":"vis(ax=None, col='k', dims=(0, 1), type='line', morph_plot_kwargs={})","text":"

Visualize the module.

Modules can be visualized on one of the cardinal planes (xy, xz, yz) or even in 3D.

Several options are available: - line: All points from the traced morphology (xyzr), are connected with a line plot. - scatter: All traced points, are plotted as scatter points. - comp: Plots the compartmentalized morphology, including radius and shape. (shows the true compartment lengths per default, but this can be changed via the morph_plot_kwargs, for details see jaxley.utils.plot_utils.plot_comps). - morph: Reconstructs the 3D shape of the traced morphology. For details see jaxley.utils.plot_utils.plot_morph. Warning: For 3D plots and morphologies with many traced points this can be very slow.

Parameters:

Name Type Description Default ax Optional[Axes]

An axis into which to plot.

None col str

The color for all branches.

'k' dims Tuple[int]

Which dimensions to plot. 1=x, 2=y, 3=z coordinate. Must be a tuple of two of them.

(0, 1) type str

The type of plot. One of [\u201cline\u201d, \u201cscatter\u201d, \u201ccomp\u201d, \u201cmorph\u201d].

'line' morph_plot_kwargs Dict

Keyword arguments passed to the plotting function.

{} Source code in jaxley/modules/base.py
def vis(\n    self,\n    ax: Optional[Axes] = None,\n    col: str = \"k\",\n    dims: Tuple[int] = (0, 1),\n    type: str = \"line\",\n    morph_plot_kwargs: Dict = {},\n) -> Axes:\n    \"\"\"Visualize the module.\n\n    Modules can be visualized on one of the cardinal planes (xy, xz, yz) or\n    even in 3D.\n\n    Several options are available:\n    - `line`: All points from the traced morphology (`xyzr`), are connected\n    with a line plot.\n    - `scatter`: All traced points, are plotted as scatter points.\n    - `comp`: Plots the compartmentalized morphology, including radius\n    and shape. (shows the true compartment lengths per default, but this can\n    be changed via the `morph_plot_kwargs`, for details see\n    `jaxley.utils.plot_utils.plot_comps`).\n    - `morph`: Reconstructs the 3D shape of the traced morphology. For details see\n    `jaxley.utils.plot_utils.plot_morph`. Warning: For 3D plots and morphologies\n    with many traced points this can be very slow.\n\n    Args:\n        ax: An axis into which to plot.\n        col: The color for all branches.\n        dims: Which dimensions to plot. 1=x, 2=y, 3=z coordinate. Must be a tuple of\n            two of them.\n        type: The type of plot. One of [\"line\", \"scatter\", \"comp\", \"morph\"].\n        morph_plot_kwargs: Keyword arguments passed to the plotting function.\n    \"\"\"\n    if \"comp\" in type.lower():\n        return plot_comps(self, dims=dims, ax=ax, col=col, **morph_plot_kwargs)\n    if \"morph\" in type.lower():\n        return plot_morph(self, dims=dims, ax=ax, col=col, **morph_plot_kwargs)\n\n    assert not np.any(\n        [np.isnan(xyzr[:, dims]).all() for xyzr in self.xyzr]\n    ), \"No coordinates available. Use `vis(detail='point')` or run `.compute_xyz()` before running `.vis()`.\"\n\n    ax = plot_graph(\n        self.xyzr,\n        dims=dims,\n        col=col,\n        ax=ax,\n        type=type,\n        morph_plot_kwargs=morph_plot_kwargs,\n    )\n\n    return ax\n
"},{"location":"reference/modules/#jaxley.modules.base.Module.write_trainables","title":"write_trainables(trainable_params)","text":"

Write the trainables into .nodes and .edges.

This allows to, e.g., visualize trained networks with .vis().

Parameters:

Name Type Description Default trainable_params List[Dict[str, ndarray]]

The trainable parameters returned by get_parameters().

required Source code in jaxley/modules/base.py
def write_trainables(self, trainable_params: List[Dict[str, jnp.ndarray]]):\n    \"\"\"Write the trainables into `.nodes` and `.edges`.\n\n    This allows to, e.g., visualize trained networks with `.vis()`.\n\n    Args:\n        trainable_params: The trainable parameters returned by `get_parameters()`.\n    \"\"\"\n    # We do not support views. Why? `jaxedges` does not have any NaN\n    # elements, whereas edges does. Because of this, we already need special\n    # treatment to make this function work, and it would be an even bigger hassle\n    # if we wanted to support this.\n    assert self.__class__.__name__ in [\n        \"Compartment\",\n        \"Branch\",\n        \"Cell\",\n        \"Network\",\n    ], \"Only supports modules.\"\n\n    # We could also implement this without casting the module to jax.\n    # However, I think it allows us to reuse as much code as possible and it avoids\n    # any kind of issues with indexing or parameter sharing (as this is fully\n    # taken care of by `get_all_parameters()`).\n    self.base.to_jax()\n    pstate = params_to_pstate(trainable_params, self.base.indices_set_by_trainables)\n    all_params = self.base.get_all_parameters(pstate, voltage_solver=\"jaxley.stone\")\n\n    # The value for `delta_t` does not matter here because it is only used to\n    # compute the initial current. However, the initial current cannot be made\n    # trainable and so its value never gets used below.\n    all_states = self.base.get_all_states(pstate, all_params, delta_t=0.025)\n\n    # Loop only over the keys in `pstate` to avoid unnecessary computation.\n    for parameter in pstate:\n        key = parameter[\"key\"]\n        if key in self.base.nodes.columns:\n            vals_to_set = all_params if key in all_params.keys() else all_states\n            self.base.nodes[key] = vals_to_set[key]\n\n    # `jaxedges` contains only non-Nan elements. This is unlike the channels where\n    # we allow parameter sharing.\n    edges = self.base.edges.to_dict(orient=\"list\")\n    for i, synapse in enumerate(self.base.synapses):\n        condition = np.asarray(edges[\"type_ind\"]) == i\n        for key in list(synapse.synapse_params.keys()):\n            self.base.edges.loc[condition, key] = all_params[key]\n        for key in list(synapse.synapse_states.keys()):\n            self.base.edges.loc[condition, key] = all_states[key]\n
"},{"location":"reference/modules/#compartment","title":"Compartment","text":"

Bases: Module

Compartment class.

This class defines a single compartment that can be simulated by itself or connected up into branches. It is the basic building block of a neuron model.

Source code in jaxley/modules/compartment.py
class Compartment(Module):\n    \"\"\"Compartment class.\n\n    This class defines a single compartment that can be simulated by itself or\n    connected up into branches. It is the basic building block of a neuron model.\n    \"\"\"\n\n    compartment_params: Dict = {\n        \"length\": 10.0,  # um\n        \"radius\": 1.0,  # um\n        \"axial_resistivity\": 5_000.0,  # ohm cm\n        \"capacitance\": 1.0,  # uF/cm^2\n    }\n    compartment_states: Dict = {\"v\": -70.0}\n\n    def __init__(self):\n        super().__init__()\n\n        self.ncomp = 1\n        self.ncomp_per_branch = np.asarray([1])\n        self.total_nbranches = 1\n        self.nbranches_per_cell = [1]\n        self._cumsum_nbranches = np.asarray([0, 1])\n        self.cumsum_ncomp = cumsum_leading_zero(self.ncomp_per_branch)\n\n        # Setting up the `nodes` for indexing.\n        self.nodes = pd.DataFrame(\n            dict(global_cell_index=[0], global_branch_index=[0], global_comp_index=[0])\n        )\n        self._append_params_and_states(self.compartment_params, self.compartment_states)\n        self._update_local_indices()\n        self._init_view()\n\n        # Synapses.\n        self.branch_edges = pd.DataFrame(\n            dict(parent_branch_index=[], child_branch_index=[])\n        )\n\n        # For morphology indexing.\n        self._par_inds, self._child_inds, self._child_belongs_to_branchpoint = (\n            compute_children_and_parents(self.branch_edges)\n        )\n        self._internal_node_inds = jnp.asarray([0])\n\n        # Initialize the module.\n        self._initialize()\n\n        # Coordinates.\n        self.xyzr = [float(\"NaN\") * np.zeros((2, 4))]\n\n    def _init_morph_jaxley_spsolve(self):\n        self._solve_indexer = JaxleySolveIndexer(\n            cumsum_ncomp=self.cumsum_ncomp,\n            branchpoint_group_inds=np.asarray([]).astype(int),\n            children_in_level=[],\n            parents_in_level=[],\n            root_inds=np.asarray([0]),\n            remapped_node_indices=self._internal_node_inds,\n        )\n\n    def _init_morph_jax_spsolve(self):\n        \"\"\"Initialize morphology for the jax sparse voltage solver.\n\n        Explanation of `self._comp_eges['type']`:\n        `type == 0`: compartment <--> compartment (within branch)\n        `type == 1`: branchpoint --> parent-compartment\n        `type == 2`: branchpoint --> child-compartment\n        `type == 3`: parent-compartment --> branchpoint\n        `type == 4`: child-compartment --> branchpoint\n        \"\"\"\n        self._comp_edges = pd.DataFrame().from_dict(\n            {\"source\": [], \"sink\": [], \"type\": []}\n        )\n        n_nodes, data_inds, indices, indptr = comp_edges_to_indices(self._comp_edges)\n        self._n_nodes = n_nodes\n        self._data_inds = data_inds\n        self._indices_jax_spsolve = indices\n        self._indptr_jax_spsolve = indptr\n
"},{"location":"reference/modules/#branch","title":"Branch","text":"

Bases: Module

Branch class.

This class defines a single branch that can be simulated by itself or connected to build a cell. A branch is linear segment of several compartments and can be connected to no, one or more other branches at each end to build more intricate cell morphologies.

Source code in jaxley/modules/branch.py
class Branch(Module):\n    \"\"\"Branch class.\n\n    This class defines a single branch that can be simulated by itself or\n    connected to build a cell. A branch is linear segment of several compartments\n    and can be connected to no, one or more other branches at each end to build more\n    intricate cell morphologies.\n    \"\"\"\n\n    branch_params: Dict = {}\n    branch_states: Dict = {}\n\n    @deprecated_kwargs(\"0.6.0\", [\"nseg\"])\n    def __init__(\n        self,\n        compartments: Optional[Union[Compartment, List[Compartment]]] = None,\n        ncomp: Optional[int] = None,\n        nseg: Optional[int] = None,\n    ):\n        \"\"\"\n        Args:\n            compartments: A single compartment or a list of compartments that make up the\n                branch.\n            ncomp: Number of segments to divide the branch into. If `compartments` is an\n                a single compartment, than the compartment is repeated `ncomp` times to\n                create the branch.\n        \"\"\"\n        # Warnings and errors that deal with the change from `nseg` to `ncomp` change\n        # in Jaxley v0.5.0.\n        if ncomp is not None and nseg is not None:\n            raise ValueError(\"You passed `ncomp` and `nseg`. Please pass only `ncomp`.\")\n        if ncomp is None and nseg is not None:\n            ncomp = nseg\n\n        super().__init__()\n        assert (\n            isinstance(compartments, (Compartment, List)) or compartments is None\n        ), \"Only Compartment or List[Compartment] is allowed.\"\n        if isinstance(compartments, Compartment):\n            assert (\n                ncomp is not None\n            ), \"If `compartments` is not a list then you have to set `ncomp`.\"\n        compartments = Compartment() if compartments is None else compartments\n        ncomp = 1 if ncomp is None else ncomp\n\n        if isinstance(compartments, Compartment):\n            compartment_list = [compartments] * ncomp\n        else:\n            compartment_list = compartments\n\n        self.ncomp = len(compartment_list)\n        self.ncomp_per_branch = np.asarray([self.ncomp])\n        self.total_nbranches = 1\n        self.nbranches_per_cell = [1]\n        self._cumsum_nbranches = jnp.asarray([0, 1])\n        self.cumsum_ncomp = cumsum_leading_zero(self.ncomp_per_branch)\n\n        # Indexing.\n        self.nodes = pd.concat([c.nodes for c in compartment_list], ignore_index=True)\n        self._append_params_and_states(self.branch_params, self.branch_states)\n        self.nodes[\"global_comp_index\"] = np.arange(self.ncomp).tolist()\n        self.nodes[\"global_branch_index\"] = [0] * self.ncomp\n        self.nodes[\"global_cell_index\"] = [0] * self.ncomp\n        self._update_local_indices()\n        self._init_view()\n\n        # Channels.\n        self._gather_channels_from_constituents(compartment_list)\n\n        self.branch_edges = pd.DataFrame(\n            dict(parent_branch_index=[], child_branch_index=[])\n        )\n\n        # For morphology indexing.\n        self._par_inds, self._child_inds, self._child_belongs_to_branchpoint = (\n            compute_children_and_parents(self.branch_edges)\n        )\n        self._internal_node_inds = jnp.arange(self.ncomp)\n\n        self._initialize()\n\n        # Coordinates.\n        self.xyzr = [float(\"NaN\") * np.zeros((2, 4))]\n\n    def _init_morph_jaxley_spsolve(self):\n        self._solve_indexer = JaxleySolveIndexer(\n            cumsum_ncomp=self.cumsum_ncomp,\n            branchpoint_group_inds=np.asarray([]).astype(int),\n            remapped_node_indices=self._internal_node_inds,\n            children_in_level=[],\n            parents_in_level=[],\n            root_inds=np.asarray([0]),\n        )\n\n    def _init_morph_jax_spsolve(self):\n        \"\"\"Initialize morphology for the jax sparse voltage solver.\n\n        Explanation of `self._comp_eges['type']`:\n        `type == 0`: compartment <--> compartment (within branch)\n        `type == 1`: branchpoint --> parent-compartment\n        `type == 2`: branchpoint --> child-compartment\n        `type == 3`: parent-compartment --> branchpoint\n        `type == 4`: child-compartment --> branchpoint\n        \"\"\"\n        self._comp_edges = pd.DataFrame().from_dict(\n            {\n                \"source\": list(range(self.ncomp - 1)) + list(range(1, self.ncomp)),\n                \"sink\": list(range(1, self.ncomp)) + list(range(self.ncomp - 1)),\n            }\n        )\n        self._comp_edges[\"type\"] = 0\n        n_nodes, data_inds, indices, indptr = comp_edges_to_indices(self._comp_edges)\n        self._n_nodes = n_nodes\n        self._data_inds = data_inds\n        self._indices_jax_spsolve = indices\n        self._indptr_jax_spsolve = indptr\n\n    def __len__(self) -> int:\n        return self.ncomp\n
"},{"location":"reference/modules/#jaxley.modules.branch.Branch.__init__","title":"__init__(compartments=None, ncomp=None, nseg=None)","text":"

Parameters:

Name Type Description Default compartments Optional[Union[Compartment, List[Compartment]]]

A single compartment or a list of compartments that make up the branch.

None ncomp Optional[int]

Number of segments to divide the branch into. If compartments is an a single compartment, than the compartment is repeated ncomp times to create the branch.

None Source code in jaxley/modules/branch.py
@deprecated_kwargs(\"0.6.0\", [\"nseg\"])\ndef __init__(\n    self,\n    compartments: Optional[Union[Compartment, List[Compartment]]] = None,\n    ncomp: Optional[int] = None,\n    nseg: Optional[int] = None,\n):\n    \"\"\"\n    Args:\n        compartments: A single compartment or a list of compartments that make up the\n            branch.\n        ncomp: Number of segments to divide the branch into. If `compartments` is an\n            a single compartment, than the compartment is repeated `ncomp` times to\n            create the branch.\n    \"\"\"\n    # Warnings and errors that deal with the change from `nseg` to `ncomp` change\n    # in Jaxley v0.5.0.\n    if ncomp is not None and nseg is not None:\n        raise ValueError(\"You passed `ncomp` and `nseg`. Please pass only `ncomp`.\")\n    if ncomp is None and nseg is not None:\n        ncomp = nseg\n\n    super().__init__()\n    assert (\n        isinstance(compartments, (Compartment, List)) or compartments is None\n    ), \"Only Compartment or List[Compartment] is allowed.\"\n    if isinstance(compartments, Compartment):\n        assert (\n            ncomp is not None\n        ), \"If `compartments` is not a list then you have to set `ncomp`.\"\n    compartments = Compartment() if compartments is None else compartments\n    ncomp = 1 if ncomp is None else ncomp\n\n    if isinstance(compartments, Compartment):\n        compartment_list = [compartments] * ncomp\n    else:\n        compartment_list = compartments\n\n    self.ncomp = len(compartment_list)\n    self.ncomp_per_branch = np.asarray([self.ncomp])\n    self.total_nbranches = 1\n    self.nbranches_per_cell = [1]\n    self._cumsum_nbranches = jnp.asarray([0, 1])\n    self.cumsum_ncomp = cumsum_leading_zero(self.ncomp_per_branch)\n\n    # Indexing.\n    self.nodes = pd.concat([c.nodes for c in compartment_list], ignore_index=True)\n    self._append_params_and_states(self.branch_params, self.branch_states)\n    self.nodes[\"global_comp_index\"] = np.arange(self.ncomp).tolist()\n    self.nodes[\"global_branch_index\"] = [0] * self.ncomp\n    self.nodes[\"global_cell_index\"] = [0] * self.ncomp\n    self._update_local_indices()\n    self._init_view()\n\n    # Channels.\n    self._gather_channels_from_constituents(compartment_list)\n\n    self.branch_edges = pd.DataFrame(\n        dict(parent_branch_index=[], child_branch_index=[])\n    )\n\n    # For morphology indexing.\n    self._par_inds, self._child_inds, self._child_belongs_to_branchpoint = (\n        compute_children_and_parents(self.branch_edges)\n    )\n    self._internal_node_inds = jnp.arange(self.ncomp)\n\n    self._initialize()\n\n    # Coordinates.\n    self.xyzr = [float(\"NaN\") * np.zeros((2, 4))]\n
"},{"location":"reference/modules/#cell","title":"Cell","text":"

Bases: Module

Cell class.

This class defines a single cell that can be simulated by itself or connected with synapses to build a network. A cell is made up of several branches and supports intricate cell morphologies.

Source code in jaxley/modules/cell.py
class Cell(Module):\n    \"\"\"Cell class.\n\n    This class defines a single cell that can be simulated by itself or\n    connected with synapses to build a network. A cell is made up of several branches\n    and supports intricate cell morphologies.\n    \"\"\"\n\n    cell_params: Dict = {}\n    cell_states: Dict = {}\n\n    def __init__(\n        self,\n        branches: Optional[Union[Branch, List[Branch]]] = None,\n        parents: Optional[List[int]] = None,\n        xyzr: Optional[List[np.ndarray]] = None,\n    ):\n        \"\"\"Initialize a cell.\n\n        Args:\n            branches: A single branch or a list of branches that make up the cell.\n                If a single branch is provided, then the branch is repeated `len(parents)`\n                times to create the cell.\n            parents: The parent branch index for each branch. The first branch has no\n                parent and is therefore set to -1.\n            xyzr: For every branch, the x, y, and z coordinates and the radius at the\n                traced coordinates. Note that this is the full tracing (from SWC), not\n                the stick representation coordinates.\n        \"\"\"\n        super().__init__()\n        assert (\n            isinstance(branches, (Branch, List)) or branches is None\n        ), \"Only Branch or List[Branch] is allowed.\"\n        if branches is not None:\n            assert (\n                parents is not None\n            ), \"If `branches` is not a list then you have to set `parents`.\"\n        if isinstance(branches, List):\n            assert len(parents) == len(\n                branches\n            ), \"Ensure equally many parents, i.e. len(branches) == len(parents).\"\n\n        branches = Branch() if branches is None else branches\n        parents = [-1] if parents is None else parents\n\n        if isinstance(branches, Branch):\n            branch_list = [branches for _ in range(len(parents))]\n        else:\n            branch_list = branches\n\n        if xyzr is not None:\n            assert len(xyzr) == len(parents)\n            self.xyzr = xyzr\n        else:\n            # For every branch (`len(parents)`), we have a start and end point (`2`) and\n            # a (x,y,z,r) coordinate for each of them (`4`).\n            # Since `xyzr` is only inspected at `.vis()` and because it depends on the\n            # (potentially learned) length of every compartment, we only populate\n            # self.xyzr at `.vis()`.\n            self.xyzr = [float(\"NaN\") * np.zeros((2, 4)) for _ in range(len(parents))]\n\n        self.total_nbranches = len(branch_list)\n        self.nbranches_per_cell = [len(branch_list)]\n        self.comb_parents = jnp.asarray(parents)\n        self.comb_children = compute_children_indices(self.comb_parents)\n        self._cumsum_nbranches = np.asarray([0, len(branch_list)])\n\n        # Compartment structure. These arguments have to be rebuilt when `.set_ncomp()`\n        # is run.\n        self.ncomp_per_branch = np.asarray([branch.ncomp for branch in branch_list])\n        self.ncomp = int(np.max(self.ncomp_per_branch))\n        self.cumsum_ncomp = cumsum_leading_zero(self.ncomp_per_branch)\n        self._internal_node_inds = np.arange(self.cumsum_ncomp[-1])\n\n        # Build nodes. Has to be changed when `.set_ncomp()` is run.\n        self.nodes = pd.concat([c.nodes for c in branch_list], ignore_index=True)\n        self.nodes[\"global_comp_index\"] = np.arange(self.cumsum_ncomp[-1])\n        self.nodes[\"global_branch_index\"] = np.repeat(\n            np.arange(self.total_nbranches), self.ncomp_per_branch\n        ).tolist()\n        self.nodes[\"global_cell_index\"] = np.repeat(0, self.cumsum_ncomp[-1]).tolist()\n        self._update_local_indices()\n        self._init_view()\n\n        # Appending general parameters (radius, length, r_a, cm) and channel parameters,\n        # as well as the states (v, and channel states).\n        self._append_params_and_states(self.cell_params, self.cell_states)\n\n        # Channels.\n        self._gather_channels_from_constituents(branch_list)\n\n        self.branch_edges = pd.DataFrame(\n            dict(\n                parent_branch_index=self.comb_parents[1:],\n                child_branch_index=np.arange(1, self.total_nbranches),\n            )\n        )\n\n        # For morphology indexing.\n        self._par_inds, self._child_inds, self._child_belongs_to_branchpoint = (\n            compute_children_and_parents(self.branch_edges)\n        )\n\n        self._initialize()\n\n    def _init_morph_jaxley_spsolve(self):\n        \"\"\"Initialize morphology for the custom sparse solver.\n\n        Running this function is only required for custom Jaxley solvers, i.e., for\n        `voltage_solver={'jaxley.stone', 'jaxley.thomas'}`. However, because at\n        `.__init__()` (when the function is run), we do not yet know which solver the\n        user will use. Therefore, we always run this function at `.__init__()`.\n        \"\"\"\n        children_and_parents = compute_morphology_indices_in_levels(\n            len(self._par_inds),\n            self._child_belongs_to_branchpoint,\n            self._par_inds,\n            self._child_inds,\n        )\n        branchpoint_group_inds = build_branchpoint_group_inds(\n            len(self._par_inds),\n            self._child_belongs_to_branchpoint,\n            self.cumsum_ncomp[-1],\n        )\n        parents = self.comb_parents\n        children_inds = children_and_parents[\"children\"]\n        parents_inds = children_and_parents[\"parents\"]\n\n        levels = compute_levels(parents)\n        children_in_level = compute_children_in_level(levels, children_inds)\n        parents_in_level = compute_parents_in_level(\n            levels, self._par_inds, parents_inds\n        )\n        levels_and_ncomp = pd.DataFrame().from_dict(\n            {\n                \"levels\": levels,\n                \"ncomps\": self.ncomp_per_branch,\n            }\n        )\n        levels_and_ncomp[\"max_ncomp_in_level\"] = levels_and_ncomp.groupby(\"levels\")[\n            \"ncomps\"\n        ].transform(\"max\")\n        padded_cumsum_ncomp = cumsum_leading_zero(\n            levels_and_ncomp[\"max_ncomp_in_level\"].to_numpy()\n        )\n\n        # Generate mapping to deal with the masking which allows using the custom\n        # sparse solver to deal with different ncomp per branch.\n        remapped_node_indices = remap_index_to_masked(\n            self._internal_node_inds,\n            self.nodes,\n            padded_cumsum_ncomp,\n            self.ncomp_per_branch,\n        )\n        self._solve_indexer = JaxleySolveIndexer(\n            cumsum_ncomp=padded_cumsum_ncomp,\n            branchpoint_group_inds=branchpoint_group_inds,\n            children_in_level=children_in_level,\n            parents_in_level=parents_in_level,\n            root_inds=np.asarray([0]),\n            remapped_node_indices=remapped_node_indices,\n        )\n\n    def _init_morph_jax_spsolve(self):\n        \"\"\"For morphology indexing with the `jax.sparse` voltage volver.\n\n        Explanation of `self._comp_eges['type']`:\n        `type == 0`: compartment <--> compartment (within branch)\n        `type == 1`: branchpoint --> parent-compartment\n        `type == 2`: branchpoint --> child-compartment\n        `type == 3`: parent-compartment --> branchpoint\n        `type == 4`: child-compartment --> branchpoint\n\n        Running this function is only required for generic sparse solvers, i.e., for\n        `voltage_solver='jax.sparse'`.\n        \"\"\"\n\n        # Edges between compartments within the branches.\n        self._comp_edges = pd.concat(\n            [\n                pd.DataFrame()\n                .from_dict(\n                    {\n                        \"source\": list(range(cumsum_ncomp, ncomp - 1 + cumsum_ncomp))\n                        + list(range(1 + cumsum_ncomp, ncomp + cumsum_ncomp)),\n                        \"sink\": list(range(1 + cumsum_ncomp, ncomp + cumsum_ncomp))\n                        + list(range(cumsum_ncomp, ncomp - 1 + cumsum_ncomp)),\n                    }\n                )\n                .astype(int)\n                for ncomp, cumsum_ncomp in zip(self.ncomp_per_branch, self.cumsum_ncomp)\n            ]\n        )\n        self._comp_edges[\"type\"] = 0\n\n        # Edges from branchpoints to compartments.\n        branchpoint_to_parent_edges = pd.DataFrame().from_dict(\n            {\n                \"source\": np.arange(len(self._par_inds)) + self.cumsum_ncomp[-1],\n                \"sink\": self.cumsum_ncomp[self._par_inds + 1] - 1,\n                \"type\": 1,\n            }\n        )\n        branchpoint_to_child_edges = pd.DataFrame().from_dict(\n            {\n                \"source\": self._child_belongs_to_branchpoint + self.cumsum_ncomp[-1],\n                \"sink\": self.cumsum_ncomp[self._child_inds],\n                \"type\": 2,\n            }\n        )\n        self._comp_edges = pd.concat(\n            [\n                self._comp_edges,\n                branchpoint_to_parent_edges,\n                branchpoint_to_child_edges,\n            ],\n            ignore_index=True,\n        )\n\n        # Edges from compartments to branchpoints.\n        parent_to_branchpoint_edges = branchpoint_to_parent_edges.rename(\n            columns={\"sink\": \"source\", \"source\": \"sink\"}\n        )\n        parent_to_branchpoint_edges[\"type\"] = 3\n        child_to_branchpoint_edges = branchpoint_to_child_edges.rename(\n            columns={\"sink\": \"source\", \"source\": \"sink\"}\n        )\n        child_to_branchpoint_edges[\"type\"] = 4\n\n        self._comp_edges = pd.concat(\n            [\n                self._comp_edges,\n                parent_to_branchpoint_edges,\n                child_to_branchpoint_edges,\n            ],\n            ignore_index=True,\n        )\n\n        n_nodes, data_inds, indices, indptr = comp_edges_to_indices(self._comp_edges)\n        self._n_nodes = n_nodes\n        self._data_inds = data_inds\n        self._indices_jax_spsolve = indices\n        self._indptr_jax_spsolve = indptr\n
"},{"location":"reference/modules/#jaxley.modules.cell.Cell.__init__","title":"__init__(branches=None, parents=None, xyzr=None)","text":"

Initialize a cell.

Parameters:

Name Type Description Default branches Optional[Union[Branch, List[Branch]]]

A single branch or a list of branches that make up the cell. If a single branch is provided, then the branch is repeated len(parents) times to create the cell.

None parents Optional[List[int]]

The parent branch index for each branch. The first branch has no parent and is therefore set to -1.

None xyzr Optional[List[ndarray]]

For every branch, the x, y, and z coordinates and the radius at the traced coordinates. Note that this is the full tracing (from SWC), not the stick representation coordinates.

None Source code in jaxley/modules/cell.py
def __init__(\n    self,\n    branches: Optional[Union[Branch, List[Branch]]] = None,\n    parents: Optional[List[int]] = None,\n    xyzr: Optional[List[np.ndarray]] = None,\n):\n    \"\"\"Initialize a cell.\n\n    Args:\n        branches: A single branch or a list of branches that make up the cell.\n            If a single branch is provided, then the branch is repeated `len(parents)`\n            times to create the cell.\n        parents: The parent branch index for each branch. The first branch has no\n            parent and is therefore set to -1.\n        xyzr: For every branch, the x, y, and z coordinates and the radius at the\n            traced coordinates. Note that this is the full tracing (from SWC), not\n            the stick representation coordinates.\n    \"\"\"\n    super().__init__()\n    assert (\n        isinstance(branches, (Branch, List)) or branches is None\n    ), \"Only Branch or List[Branch] is allowed.\"\n    if branches is not None:\n        assert (\n            parents is not None\n        ), \"If `branches` is not a list then you have to set `parents`.\"\n    if isinstance(branches, List):\n        assert len(parents) == len(\n            branches\n        ), \"Ensure equally many parents, i.e. len(branches) == len(parents).\"\n\n    branches = Branch() if branches is None else branches\n    parents = [-1] if parents is None else parents\n\n    if isinstance(branches, Branch):\n        branch_list = [branches for _ in range(len(parents))]\n    else:\n        branch_list = branches\n\n    if xyzr is not None:\n        assert len(xyzr) == len(parents)\n        self.xyzr = xyzr\n    else:\n        # For every branch (`len(parents)`), we have a start and end point (`2`) and\n        # a (x,y,z,r) coordinate for each of them (`4`).\n        # Since `xyzr` is only inspected at `.vis()` and because it depends on the\n        # (potentially learned) length of every compartment, we only populate\n        # self.xyzr at `.vis()`.\n        self.xyzr = [float(\"NaN\") * np.zeros((2, 4)) for _ in range(len(parents))]\n\n    self.total_nbranches = len(branch_list)\n    self.nbranches_per_cell = [len(branch_list)]\n    self.comb_parents = jnp.asarray(parents)\n    self.comb_children = compute_children_indices(self.comb_parents)\n    self._cumsum_nbranches = np.asarray([0, len(branch_list)])\n\n    # Compartment structure. These arguments have to be rebuilt when `.set_ncomp()`\n    # is run.\n    self.ncomp_per_branch = np.asarray([branch.ncomp for branch in branch_list])\n    self.ncomp = int(np.max(self.ncomp_per_branch))\n    self.cumsum_ncomp = cumsum_leading_zero(self.ncomp_per_branch)\n    self._internal_node_inds = np.arange(self.cumsum_ncomp[-1])\n\n    # Build nodes. Has to be changed when `.set_ncomp()` is run.\n    self.nodes = pd.concat([c.nodes for c in branch_list], ignore_index=True)\n    self.nodes[\"global_comp_index\"] = np.arange(self.cumsum_ncomp[-1])\n    self.nodes[\"global_branch_index\"] = np.repeat(\n        np.arange(self.total_nbranches), self.ncomp_per_branch\n    ).tolist()\n    self.nodes[\"global_cell_index\"] = np.repeat(0, self.cumsum_ncomp[-1]).tolist()\n    self._update_local_indices()\n    self._init_view()\n\n    # Appending general parameters (radius, length, r_a, cm) and channel parameters,\n    # as well as the states (v, and channel states).\n    self._append_params_and_states(self.cell_params, self.cell_states)\n\n    # Channels.\n    self._gather_channels_from_constituents(branch_list)\n\n    self.branch_edges = pd.DataFrame(\n        dict(\n            parent_branch_index=self.comb_parents[1:],\n            child_branch_index=np.arange(1, self.total_nbranches),\n        )\n    )\n\n    # For morphology indexing.\n    self._par_inds, self._child_inds, self._child_belongs_to_branchpoint = (\n        compute_children_and_parents(self.branch_edges)\n    )\n\n    self._initialize()\n
"},{"location":"reference/modules/#network","title":"Network","text":"

Bases: Module

Network class.

This class defines a network of cells that can be connected with synapses.

Source code in jaxley/modules/network.py
class Network(Module):\n    \"\"\"Network class.\n\n    This class defines a network of cells that can be connected with synapses.\n    \"\"\"\n\n    network_params: Dict = {}\n    network_states: Dict = {}\n\n    def __init__(\n        self,\n        cells: List[Cell],\n    ):\n        \"\"\"Initialize network of cells and synapses.\n\n        Args:\n            cells: A list of cells that make up the network.\n        \"\"\"\n        super().__init__()\n        for cell in cells:\n            self.xyzr += deepcopy(cell.xyzr)\n\n        self._cells_list = cells\n        self.ncomp_per_branch = np.concatenate(\n            [cell.ncomp_per_branch for cell in cells]\n        )\n        self.ncomp = int(np.max(self.ncomp_per_branch))\n        self.cumsum_ncomp = cumsum_leading_zero(self.ncomp_per_branch)\n        self._internal_node_inds = np.arange(self.cumsum_ncomp[-1])\n        self._append_params_and_states(self.network_params, self.network_states)\n\n        self.nbranches_per_cell = [cell.total_nbranches for cell in cells]\n        self.total_nbranches = sum(self.nbranches_per_cell)\n        self._cumsum_nbranches = cumsum_leading_zero(self.nbranches_per_cell)\n\n        self.nodes = pd.concat([c.nodes for c in cells], ignore_index=True)\n        self.nodes[\"global_comp_index\"] = np.arange(self.cumsum_ncomp[-1])\n        self.nodes[\"global_branch_index\"] = np.repeat(\n            np.arange(self.total_nbranches), self.ncomp_per_branch\n        ).tolist()\n        self.nodes[\"global_cell_index\"] = list(\n            itertools.chain(\n                *[[i] * int(cell.cumsum_ncomp[-1]) for i, cell in enumerate(cells)]\n            )\n        )\n        self._update_local_indices()\n        self._init_view()\n\n        parents = [cell.comb_parents for cell in cells]\n        self.comb_parents = jnp.concatenate(\n            [p.at[1:].add(self._cumsum_nbranches[i]) for i, p in enumerate(parents)]\n        )\n\n        # Two columns: `parent_branch_index` and `child_branch_index`. One row per\n        # branch, apart from those branches which do not have a parent (i.e.\n        # -1 in parents). For every branch, tracks the global index of that branch\n        # (`child_branch_index`) and the global index of its parent\n        # (`parent_branch_index`).\n        self.branch_edges = pd.DataFrame(\n            dict(\n                parent_branch_index=self.comb_parents[self.comb_parents != -1],\n                child_branch_index=np.where(self.comb_parents != -1)[0],\n            )\n        )\n\n        # For morphology indexing of both `jax.sparse` and the custom `jaxley` solvers.\n        self._par_inds, self._child_inds, self._child_belongs_to_branchpoint = (\n            compute_children_and_parents(self.branch_edges)\n        )\n\n        # `nbranchpoints` in each cell == cell._par_inds (because `par_inds` are unique).\n        nbranchpoints = jnp.asarray([len(cell._par_inds) for cell in cells])\n        self._cumsum_nbranchpoints_per_cell = cumsum_leading_zero(nbranchpoints)\n\n        # Channels.\n        self._gather_channels_from_constituents(cells)\n\n        self._initialize()\n        del self._cells_list\n\n    def __repr__(self):\n        return f\"{type(self).__name__} with {len(self.channels)} different channels and {len(self.synapses)} synapses. Use `.nodes` or `.edges` for details.\"\n\n    def _init_morph_jaxley_spsolve(self):\n        branchpoint_group_inds = build_branchpoint_group_inds(\n            len(self._par_inds),\n            self._child_belongs_to_branchpoint,\n            self.cumsum_ncomp[-1],\n        )\n        children_in_level = merge_cells(\n            self._cumsum_nbranches,\n            self._cumsum_nbranchpoints_per_cell,\n            [cell._solve_indexer.children_in_level for cell in self._cells_list],\n            exclude_first=False,\n        )\n        parents_in_level = merge_cells(\n            self._cumsum_nbranches,\n            self._cumsum_nbranchpoints_per_cell,\n            [cell._solve_indexer.parents_in_level for cell in self._cells_list],\n            exclude_first=False,\n        )\n        padded_cumsum_ncomp = cumsum_leading_zero(\n            np.concatenate(\n                [np.diff(cell._solve_indexer.cumsum_ncomp) for cell in self._cells_list]\n            )\n        )\n\n        # Generate mapping to dealing with the masking which allows using the custom\n        # sparse solver to deal with different ncomp per branch.\n        remapped_node_indices = remap_index_to_masked(\n            self._internal_node_inds,\n            self.nodes,\n            padded_cumsum_ncomp,\n            self.ncomp_per_branch,\n        )\n        self._solve_indexer = JaxleySolveIndexer(\n            cumsum_ncomp=padded_cumsum_ncomp,\n            branchpoint_group_inds=branchpoint_group_inds,\n            children_in_level=children_in_level,\n            parents_in_level=parents_in_level,\n            root_inds=self._cumsum_nbranches[:-1],\n            remapped_node_indices=remapped_node_indices,\n        )\n\n    def _init_morph_jax_spsolve(self):\n        \"\"\"Initialize the morphology for networks.\n\n        The reason that this function is a bit involved for a `Network` is that Jaxley\n        considers branchpoint nodes to be at the very end of __all__ nodes (i.e. the\n        branchpoints of the first cell are even after the compartments of the second\n        cell. The reason for this is that, otherwise, `cumsum_ncomp` becomes tricky).\n\n        To achieve this, we first loop over all compartments and append them, and then\n        loop over all branchpoints and append those. The code for building the indices\n        from the `comp_edges` is identical to `jx.Cell`.\n\n        Explanation of `self._comp_eges['type']`:\n        `type == 0`: compartment <--> compartment (within branch)\n        `type == 1`: branchpoint --> parent-compartment\n        `type == 2`: branchpoint --> child-compartment\n        `type == 3`: parent-compartment --> branchpoint\n        `type == 4`: child-compartment --> branchpoint\n        \"\"\"\n        self._cumsum_ncomp_per_cell = cumsum_leading_zero(\n            jnp.asarray([cell.cumsum_ncomp[-1] for cell in self.cells])\n        )\n        self._comp_edges = pd.DataFrame()\n\n        # Add all the internal nodes.\n        for offset, cell in zip(self._cumsum_ncomp_per_cell, self._cells_list):\n            condition = cell._comp_edges[\"type\"].to_numpy() == 0\n            rows = cell._comp_edges[condition]\n            self._comp_edges = pd.concat(\n                [self._comp_edges, [offset, offset, 0] + rows], ignore_index=True\n            )\n\n        # All branchpoint-to-compartment nodes.\n        start_branchpoints = self.cumsum_ncomp[-1]  # Index of the first branchpoint.\n        for offset, offset_branchpoints, cell in zip(\n            self._cumsum_ncomp_per_cell,\n            self._cumsum_nbranchpoints_per_cell,\n            self._cells_list,\n        ):\n            offset_within_cell = cell.cumsum_ncomp[-1]\n            condition = cell._comp_edges[\"type\"].isin([1, 2])\n            rows = cell._comp_edges[condition]\n            self._comp_edges = pd.concat(\n                [\n                    self._comp_edges,\n                    [\n                        start_branchpoints - offset_within_cell + offset_branchpoints,\n                        offset,\n                        0,\n                    ]\n                    + rows,\n                ],\n                ignore_index=True,\n            )\n\n        # All compartment-to-branchpoint nodes.\n        for offset, offset_branchpoints, cell in zip(\n            self._cumsum_ncomp_per_cell,\n            self._cumsum_nbranchpoints_per_cell,\n            self._cells_list,\n        ):\n            offset_within_cell = cell.cumsum_ncomp[-1]\n            condition = cell._comp_edges[\"type\"].isin([3, 4])\n            rows = cell._comp_edges[condition]\n            self._comp_edges = pd.concat(\n                [\n                    self._comp_edges,\n                    [\n                        offset,\n                        start_branchpoints - offset_within_cell + offset_branchpoints,\n                        0,\n                    ]\n                    + rows,\n                ],\n                ignore_index=True,\n            )\n\n        # Convert comp_edges to the index format required for `jax.sparse` solvers.\n        n_nodes, data_inds, indices, indptr = comp_edges_to_indices(self._comp_edges)\n        self._n_nodes = n_nodes\n        self._data_inds = data_inds\n        self._indices_jax_spsolve = indices\n        self._indptr_jax_spsolve = indptr\n\n    def _step_synapse(\n        self,\n        states: Dict,\n        syn_channels: List,\n        params: Dict,\n        delta_t: float,\n        edges: pd.DataFrame,\n    ) -> Tuple[Dict, Tuple[jnp.ndarray, jnp.ndarray]]:\n        \"\"\"Perform one step of the synapses and obtain their currents.\"\"\"\n        states = self._step_synapse_state(states, syn_channels, params, delta_t, edges)\n        states, current_terms = self._synapse_currents(\n            states, syn_channels, params, delta_t, edges\n        )\n        return states, current_terms\n\n    def _step_synapse_state(\n        self,\n        states: Dict,\n        syn_channels: List,\n        params: Dict,\n        delta_t: float,\n        edges: pd.DataFrame,\n    ) -> Dict:\n        voltages = states[\"v\"]\n\n        grouped_syns = edges.groupby(\"type\", sort=False, group_keys=False)\n        pre_syn_inds = grouped_syns[\"pre_global_comp_index\"].apply(list)\n        post_syn_inds = grouped_syns[\"post_global_comp_index\"].apply(list)\n        synapse_names = list(grouped_syns.indices.keys())\n\n        for i, synapse_type in enumerate(syn_channels):\n            assert (\n                synapse_names[i] == synapse_type._name\n            ), \"Mixup in the ordering of synapses. Please create an issue on Github.\"\n            synapse_param_names = list(synapse_type.synapse_params.keys())\n            synapse_state_names = list(synapse_type.synapse_states.keys())\n\n            synapse_params = {}\n            for p in synapse_param_names:\n                synapse_params[p] = params[p]\n            synapse_states = {}\n            for s in synapse_state_names:\n                synapse_states[s] = states[s]\n\n            pre_inds = np.asarray(pre_syn_inds[synapse_names[i]])\n            post_inds = np.asarray(post_syn_inds[synapse_names[i]])\n\n            # State updates.\n            states_updated = synapse_type.update_states(\n                synapse_states,\n                delta_t,\n                voltages[pre_inds],\n                voltages[post_inds],\n                synapse_params,\n            )\n\n            # Rebuild state.\n            for key, val in states_updated.items():\n                states[key] = val\n\n        return states\n\n    def _synapse_currents(\n        self,\n        states: Dict,\n        syn_channels: List,\n        params: Dict,\n        delta_t: float,\n        edges: pd.DataFrame,\n    ) -> Tuple[Dict, Tuple[jnp.ndarray, jnp.ndarray]]:\n        voltages = states[\"v\"]\n\n        grouped_syns = edges.groupby(\"type\", sort=False, group_keys=False)\n        pre_syn_inds = grouped_syns[\"pre_global_comp_index\"].apply(list)\n        post_syn_inds = grouped_syns[\"post_global_comp_index\"].apply(list)\n        synapse_names = list(grouped_syns.indices.keys())\n\n        syn_voltage_terms = jnp.zeros_like(voltages)\n        syn_constant_terms = jnp.zeros_like(voltages)\n        # Run with two different voltages that are `diff` apart to infer the slope and\n        # offset.\n        diff = 1e-3\n        for i, synapse_type in enumerate(syn_channels):\n            assert (\n                synapse_names[i] == synapse_type._name\n            ), \"Mixup in the ordering of synapses. Please create an issue on Github.\"\n            synapse_param_names = list(synapse_type.synapse_params.keys())\n            synapse_state_names = list(synapse_type.synapse_states.keys())\n\n            synapse_params = {}\n            for p in synapse_param_names:\n                synapse_params[p] = params[p]\n            synapse_states = {}\n            for s in synapse_state_names:\n                synapse_states[s] = states[s]\n\n            # Get pre and post indexes of the current synapse type.\n            pre_inds = np.asarray(pre_syn_inds[synapse_names[i]])\n            post_inds = np.asarray(post_syn_inds[synapse_names[i]])\n\n            # Compute slope and offset of the current through every synapse.\n            pre_v_and_perturbed = jnp.stack(\n                [voltages[pre_inds], voltages[pre_inds] + diff]\n            )\n            post_v_and_perturbed = jnp.stack(\n                [voltages[post_inds], voltages[post_inds] + diff]\n            )\n            synapse_currents = vmap(\n                synapse_type.compute_current, in_axes=(None, 0, 0, None)\n            )(\n                synapse_states,\n                pre_v_and_perturbed,\n                post_v_and_perturbed,\n                synapse_params,\n            )\n            synapse_currents_dist = convert_point_process_to_distributed(\n                synapse_currents,\n                params[\"radius\"][post_inds],\n                params[\"length\"][post_inds],\n            )\n\n            # Split into voltage and constant terms.\n            voltage_term = (synapse_currents_dist[1] - synapse_currents_dist[0]) / diff\n            constant_term = (\n                synapse_currents_dist[0] - voltage_term * voltages[post_inds]\n            )\n\n            # Gather slope and offset for every postsynaptic compartment.\n            gathered_syn_currents = gather_synapes(\n                len(voltages),\n                post_inds,\n                voltage_term,\n                constant_term,\n            )\n            syn_voltage_terms += gathered_syn_currents[0]\n            syn_constant_terms -= gathered_syn_currents[1]\n\n            # Add the synaptic currents through every compartment as state.\n            # `post_syn_currents` is a `jnp.ndarray` of as many elements as there are\n            # compartments in the network.\n            # `[0]` because we only use the non-perturbed voltage.\n            states[f\"{synapse_type._name}_current\"] = synapse_currents[0]\n\n        return states, (syn_voltage_terms, syn_constant_terms)\n\n    def vis(\n        self,\n        detail: str = \"full\",\n        ax: Optional[Axes] = None,\n        col: str = \"k\",\n        synapse_col: str = \"b\",\n        dims: Tuple[int] = (0, 1),\n        type: str = \"line\",\n        layers: Optional[List] = None,\n        morph_plot_kwargs: Dict = {},\n        synapse_plot_kwargs: Dict = {},\n        synapse_scatter_kwargs: Dict = {},\n        networkx_options: Dict = {},\n        layer_kwargs: Dict = {},\n    ) -> Axes:\n        \"\"\"Visualize the module.\n\n        Args:\n            detail: Either of [point, full]. `point` visualizes every neuron in the\n                network as a dot (and it uses `networkx` to obtain cell positions).\n                `full` plots the full morphology of every neuron. It requires that\n                `compute_xyz()` has been run and allows for indivual neurons to be\n                moved with `.move()`.\n            col: The color in which cells are plotted. Only takes effect if\n                `detail='full'`.\n            type: Either `line` or `scatter`. Only takes effect if `detail='full'`.\n            synapse_col: The color in which synapses are plotted. Only takes effect if\n                `detail='full'`.\n            dims: Which dimensions to plot. 1=x, 2=y, 3=z coordinate. Must be a tuple of\n                two of them.\n            layers: Allows to plot the network in layers. Should provide the number of\n                neurons in each layer, e.g., [5, 10, 1] would be a network with 5 input\n                neurons, 10 hidden layer neurons, and 1 output neuron.\n            morph_plot_kwargs: Keyword arguments passed to the plotting function for\n                cell morphologies. Only takes effect for `detail='full'`.\n            synapse_plot_kwargs: Keyword arguments passed to the plotting function for\n                syanpses. Only takes effect for `detail='full'`.\n            synapse_scatter_kwargs: Keyword arguments passed to the scatter function\n                for the end point of synapses. Only takes effect for `detail='full'`.\n            networkx_options: Options passed to `networkx.draw()`. Only takes effect if\n                `detail='point'`.\n            layer_kwargs: Only used if `layers` is specified and if `detail='full'`.\n                Can have the following entries: `within_layer_offset` (float),\n                `between_layer_offset` (float), `vertical_layers` (bool).\n        \"\"\"\n        if detail == \"point\":\n            graph = self._build_graph(layers)\n\n            if layers is not None:\n                pos = nx.multipartite_layout(graph, subset_key=\"layer\")\n                nx.draw(graph, pos, with_labels=True, **networkx_options)\n            else:\n                nx.draw(graph, with_labels=True, **networkx_options)\n        elif detail == \"full\":\n            if layers is not None:\n                # Assemble cells in the network into layers.\n                global_counter = 0\n                layers_config = {\n                    \"within_layer_offset\": 500.0,\n                    \"between_layer_offset\": 1500.0,\n                    \"vertical_layers\": False,\n                }\n                layers_config.update(layer_kwargs)\n                for layer_ind, num_in_layer in enumerate(layers):\n                    for ind_within_layer in range(num_in_layer):\n                        if layers_config[\"vertical_layers\"]:\n                            x_offset = (\n                                ind_within_layer - (num_in_layer - 1) / 2\n                            ) * layers_config[\"within_layer_offset\"]\n                            y_offset = (len(layers) - 1 - layer_ind) * layers_config[\n                                \"between_layer_offset\"\n                            ]\n                        else:\n                            x_offset = layer_ind * layers_config[\"between_layer_offset\"]\n                            y_offset = (\n                                ind_within_layer - (num_in_layer - 1) / 2\n                            ) * layers_config[\"within_layer_offset\"]\n\n                        self.cell(global_counter).move_to(x=x_offset, y=y_offset, z=0)\n                        global_counter += 1\n            ax = super().vis(\n                dims=dims,\n                col=col,\n                ax=ax,\n                type=type,\n                morph_plot_kwargs=morph_plot_kwargs,\n            )\n\n            pre_locs = self.edges[\"pre_locs\"].to_numpy()\n            post_locs = self.edges[\"post_locs\"].to_numpy()\n            pre_comp = self.edges[\"pre_global_comp_index\"].to_numpy()\n            nodes = self.nodes.set_index(\"global_comp_index\")\n            pre_branch = nodes.loc[pre_comp, \"global_branch_index\"].to_numpy()\n            post_comp = self.edges[\"post_global_comp_index\"].to_numpy()\n            post_branch = nodes.loc[post_comp, \"global_branch_index\"].to_numpy()\n\n            dims_np = np.asarray(dims)\n\n            for pre_loc, post_loc, pre_b, post_b in zip(\n                pre_locs, post_locs, pre_branch, post_branch\n            ):\n                pre_coord = self.xyzr[pre_b]\n                if len(pre_coord) == 2:\n                    # If only start and end point of a branch are traced, perform a\n                    # linear interpolation to get the synpase location.\n                    pre_coord = pre_coord[0] + (pre_coord[1] - pre_coord[0]) * pre_loc\n                else:\n                    # If densely traced, use intermediate trace values for synapse loc.\n                    middle_ind = int((len(pre_coord) - 1) * pre_loc)\n                    pre_coord = pre_coord[middle_ind]\n\n                post_coord = self.xyzr[post_b]\n                if len(post_coord) == 2:\n                    # If only start and end point of a branch are traced, perform a\n                    # linear interpolation to get the synpase location.\n                    post_coord = (\n                        post_coord[0] + (post_coord[1] - post_coord[0]) * post_loc\n                    )\n                else:\n                    # If densely traced, use intermediate trace values for synapse loc.\n                    middle_ind = int((len(post_coord) - 1) * post_loc)\n                    post_coord = post_coord[middle_ind]\n\n                coords = np.stack([pre_coord[dims_np], post_coord[dims_np]]).T\n                ax.plot(\n                    coords[0],\n                    coords[1],\n                    c=synapse_col,\n                    **synapse_plot_kwargs,\n                )\n                ax.scatter(\n                    post_coord[dims_np[0]],\n                    post_coord[dims_np[1]],\n                    c=synapse_col,\n                    **synapse_scatter_kwargs,\n                )\n        else:\n            raise ValueError(\"detail must be in {full, point}.\")\n\n        return ax\n\n    def _build_graph(self, layers: Optional[List] = None, **options):\n        graph = nx.DiGraph()\n\n        def build_extents(*subset_sizes):\n            return nx.utils.pairwise(itertools.accumulate((0,) + subset_sizes))\n\n        if layers is not None:\n            extents = build_extents(*layers)\n            layers = [range(start, end) for start, end in extents]\n            for i, layer in enumerate(layers):\n                graph.add_nodes_from(layer, layer=i)\n        else:\n            graph.add_nodes_from(range(len(self._cells_in_view)))\n\n        pre_comp = self.edges[\"pre_global_comp_index\"].to_numpy()\n        nodes = self.nodes.set_index(\"global_comp_index\")\n        pre_cell = nodes.loc[pre_comp, \"global_cell_index\"].to_numpy()\n        post_comp = self.edges[\"post_global_comp_index\"].to_numpy()\n        post_cell = nodes.loc[post_comp, \"global_cell_index\"].to_numpy()\n\n        inds = np.stack([pre_cell, post_cell]).T\n        graph.add_edges_from(inds)\n\n        return graph\n\n    def _infer_synapse_type_ind(self, synapse_name):\n        syn_names = self.base.synapse_names\n        is_new_type = False if synapse_name in syn_names else True\n        type_ind = len(syn_names) if is_new_type else syn_names.index(synapse_name)\n        return type_ind, is_new_type\n\n    def _update_synapse_state_names(self, synapse_type):\n        # (Potentially) update variables that track meta information about synapses.\n        self.base.synapse_names.append(synapse_type._name)\n        self.base.synapse_param_names += list(synapse_type.synapse_params.keys())\n        self.base.synapse_state_names += list(synapse_type.synapse_states.keys())\n        self.base.synapses.append(synapse_type)\n\n    def _append_multiple_synapses(self, pre_nodes, post_nodes, synapse_type):\n        # Add synapse types to the module and infer their unique identifier.\n        synapse_name = synapse_type._name\n        type_ind, is_new = self._infer_synapse_type_ind(synapse_name)\n        if is_new:  # synapse is not known\n            self._update_synapse_state_names(synapse_type)\n\n        index = len(self.base.edges)\n        indices = [idx for idx in range(index, index + len(pre_nodes))]\n        global_edge_index = pd.DataFrame({\"global_edge_index\": indices})\n        post_loc = loc_of_index(\n            post_nodes[\"global_comp_index\"].to_numpy(),\n            post_nodes[\"global_branch_index\"].to_numpy(),\n            self.ncomp_per_branch,\n        )\n        pre_loc = loc_of_index(\n            pre_nodes[\"global_comp_index\"].to_numpy(),\n            pre_nodes[\"global_branch_index\"].to_numpy(),\n            self.ncomp_per_branch,\n        )\n\n        # Define new synapses. Each row is one synapse.\n        pre_nodes = pre_nodes[[\"global_comp_index\"]]\n        pre_nodes.columns = [\"pre_global_comp_index\"]\n        post_nodes = post_nodes[[\"global_comp_index\"]]\n        post_nodes.columns = [\"post_global_comp_index\"]\n        new_rows = pd.concat(\n            [\n                global_edge_index,\n                pre_nodes.reset_index(drop=True),\n                post_nodes.reset_index(drop=True),\n            ],\n            axis=1,\n        )\n        new_rows[\"type\"] = synapse_name\n        new_rows[\"type_ind\"] = type_ind\n        new_rows[\"pre_locs\"] = pre_loc\n        new_rows[\"post_locs\"] = post_loc\n        self.base.edges = concat_and_ignore_empty(\n            [self.base.edges, new_rows], ignore_index=True, axis=0\n        )\n        self._add_params_to_edges(synapse_type, indices)\n        self.base.edges[\"controlled_by_param\"] = 0\n        self._edges_in_view = self.edges.index.to_numpy()\n\n    def _add_params_to_edges(self, synapse_type, indices):\n        # Add parameters and states to the `.edges` table.\n        for key, param_val in synapse_type.synapse_params.items():\n            self.base.edges.loc[indices, key] = param_val\n\n        # Update synaptic state array.\n        for key, state_val in synapse_type.synapse_states.items():\n            self.base.edges.loc[indices, key] = state_val\n
"},{"location":"reference/modules/#jaxley.modules.network.Network.__init__","title":"__init__(cells)","text":"

Initialize network of cells and synapses.

Parameters:

Name Type Description Default cells List[Cell]

A list of cells that make up the network.

required Source code in jaxley/modules/network.py
def __init__(\n    self,\n    cells: List[Cell],\n):\n    \"\"\"Initialize network of cells and synapses.\n\n    Args:\n        cells: A list of cells that make up the network.\n    \"\"\"\n    super().__init__()\n    for cell in cells:\n        self.xyzr += deepcopy(cell.xyzr)\n\n    self._cells_list = cells\n    self.ncomp_per_branch = np.concatenate(\n        [cell.ncomp_per_branch for cell in cells]\n    )\n    self.ncomp = int(np.max(self.ncomp_per_branch))\n    self.cumsum_ncomp = cumsum_leading_zero(self.ncomp_per_branch)\n    self._internal_node_inds = np.arange(self.cumsum_ncomp[-1])\n    self._append_params_and_states(self.network_params, self.network_states)\n\n    self.nbranches_per_cell = [cell.total_nbranches for cell in cells]\n    self.total_nbranches = sum(self.nbranches_per_cell)\n    self._cumsum_nbranches = cumsum_leading_zero(self.nbranches_per_cell)\n\n    self.nodes = pd.concat([c.nodes for c in cells], ignore_index=True)\n    self.nodes[\"global_comp_index\"] = np.arange(self.cumsum_ncomp[-1])\n    self.nodes[\"global_branch_index\"] = np.repeat(\n        np.arange(self.total_nbranches), self.ncomp_per_branch\n    ).tolist()\n    self.nodes[\"global_cell_index\"] = list(\n        itertools.chain(\n            *[[i] * int(cell.cumsum_ncomp[-1]) for i, cell in enumerate(cells)]\n        )\n    )\n    self._update_local_indices()\n    self._init_view()\n\n    parents = [cell.comb_parents for cell in cells]\n    self.comb_parents = jnp.concatenate(\n        [p.at[1:].add(self._cumsum_nbranches[i]) for i, p in enumerate(parents)]\n    )\n\n    # Two columns: `parent_branch_index` and `child_branch_index`. One row per\n    # branch, apart from those branches which do not have a parent (i.e.\n    # -1 in parents). For every branch, tracks the global index of that branch\n    # (`child_branch_index`) and the global index of its parent\n    # (`parent_branch_index`).\n    self.branch_edges = pd.DataFrame(\n        dict(\n            parent_branch_index=self.comb_parents[self.comb_parents != -1],\n            child_branch_index=np.where(self.comb_parents != -1)[0],\n        )\n    )\n\n    # For morphology indexing of both `jax.sparse` and the custom `jaxley` solvers.\n    self._par_inds, self._child_inds, self._child_belongs_to_branchpoint = (\n        compute_children_and_parents(self.branch_edges)\n    )\n\n    # `nbranchpoints` in each cell == cell._par_inds (because `par_inds` are unique).\n    nbranchpoints = jnp.asarray([len(cell._par_inds) for cell in cells])\n    self._cumsum_nbranchpoints_per_cell = cumsum_leading_zero(nbranchpoints)\n\n    # Channels.\n    self._gather_channels_from_constituents(cells)\n\n    self._initialize()\n    del self._cells_list\n
"},{"location":"reference/modules/#jaxley.modules.network.Network.vis","title":"vis(detail='full', ax=None, col='k', synapse_col='b', dims=(0, 1), type='line', layers=None, morph_plot_kwargs={}, synapse_plot_kwargs={}, synapse_scatter_kwargs={}, networkx_options={}, layer_kwargs={})","text":"

Visualize the module.

Parameters:

Name Type Description Default detail str

Either of [point, full]. point visualizes every neuron in the network as a dot (and it uses networkx to obtain cell positions). full plots the full morphology of every neuron. It requires that compute_xyz() has been run and allows for indivual neurons to be moved with .move().

'full' col str

The color in which cells are plotted. Only takes effect if detail='full'.

'k' type str

Either line or scatter. Only takes effect if detail='full'.

'line' synapse_col str

The color in which synapses are plotted. Only takes effect if detail='full'.

'b' dims Tuple[int]

Which dimensions to plot. 1=x, 2=y, 3=z coordinate. Must be a tuple of two of them.

(0, 1) layers Optional[List]

Allows to plot the network in layers. Should provide the number of neurons in each layer, e.g., [5, 10, 1] would be a network with 5 input neurons, 10 hidden layer neurons, and 1 output neuron.

None morph_plot_kwargs Dict

Keyword arguments passed to the plotting function for cell morphologies. Only takes effect for detail='full'.

{} synapse_plot_kwargs Dict

Keyword arguments passed to the plotting function for syanpses. Only takes effect for detail='full'.

{} synapse_scatter_kwargs Dict

Keyword arguments passed to the scatter function for the end point of synapses. Only takes effect for detail='full'.

{} networkx_options Dict

Options passed to networkx.draw(). Only takes effect if detail='point'.

{} layer_kwargs Dict

Only used if layers is specified and if detail='full'. Can have the following entries: within_layer_offset (float), between_layer_offset (float), vertical_layers (bool).

{} Source code in jaxley/modules/network.py
def vis(\n    self,\n    detail: str = \"full\",\n    ax: Optional[Axes] = None,\n    col: str = \"k\",\n    synapse_col: str = \"b\",\n    dims: Tuple[int] = (0, 1),\n    type: str = \"line\",\n    layers: Optional[List] = None,\n    morph_plot_kwargs: Dict = {},\n    synapse_plot_kwargs: Dict = {},\n    synapse_scatter_kwargs: Dict = {},\n    networkx_options: Dict = {},\n    layer_kwargs: Dict = {},\n) -> Axes:\n    \"\"\"Visualize the module.\n\n    Args:\n        detail: Either of [point, full]. `point` visualizes every neuron in the\n            network as a dot (and it uses `networkx` to obtain cell positions).\n            `full` plots the full morphology of every neuron. It requires that\n            `compute_xyz()` has been run and allows for indivual neurons to be\n            moved with `.move()`.\n        col: The color in which cells are plotted. Only takes effect if\n            `detail='full'`.\n        type: Either `line` or `scatter`. Only takes effect if `detail='full'`.\n        synapse_col: The color in which synapses are plotted. Only takes effect if\n            `detail='full'`.\n        dims: Which dimensions to plot. 1=x, 2=y, 3=z coordinate. Must be a tuple of\n            two of them.\n        layers: Allows to plot the network in layers. Should provide the number of\n            neurons in each layer, e.g., [5, 10, 1] would be a network with 5 input\n            neurons, 10 hidden layer neurons, and 1 output neuron.\n        morph_plot_kwargs: Keyword arguments passed to the plotting function for\n            cell morphologies. Only takes effect for `detail='full'`.\n        synapse_plot_kwargs: Keyword arguments passed to the plotting function for\n            syanpses. Only takes effect for `detail='full'`.\n        synapse_scatter_kwargs: Keyword arguments passed to the scatter function\n            for the end point of synapses. Only takes effect for `detail='full'`.\n        networkx_options: Options passed to `networkx.draw()`. Only takes effect if\n            `detail='point'`.\n        layer_kwargs: Only used if `layers` is specified and if `detail='full'`.\n            Can have the following entries: `within_layer_offset` (float),\n            `between_layer_offset` (float), `vertical_layers` (bool).\n    \"\"\"\n    if detail == \"point\":\n        graph = self._build_graph(layers)\n\n        if layers is not None:\n            pos = nx.multipartite_layout(graph, subset_key=\"layer\")\n            nx.draw(graph, pos, with_labels=True, **networkx_options)\n        else:\n            nx.draw(graph, with_labels=True, **networkx_options)\n    elif detail == \"full\":\n        if layers is not None:\n            # Assemble cells in the network into layers.\n            global_counter = 0\n            layers_config = {\n                \"within_layer_offset\": 500.0,\n                \"between_layer_offset\": 1500.0,\n                \"vertical_layers\": False,\n            }\n            layers_config.update(layer_kwargs)\n            for layer_ind, num_in_layer in enumerate(layers):\n                for ind_within_layer in range(num_in_layer):\n                    if layers_config[\"vertical_layers\"]:\n                        x_offset = (\n                            ind_within_layer - (num_in_layer - 1) / 2\n                        ) * layers_config[\"within_layer_offset\"]\n                        y_offset = (len(layers) - 1 - layer_ind) * layers_config[\n                            \"between_layer_offset\"\n                        ]\n                    else:\n                        x_offset = layer_ind * layers_config[\"between_layer_offset\"]\n                        y_offset = (\n                            ind_within_layer - (num_in_layer - 1) / 2\n                        ) * layers_config[\"within_layer_offset\"]\n\n                    self.cell(global_counter).move_to(x=x_offset, y=y_offset, z=0)\n                    global_counter += 1\n        ax = super().vis(\n            dims=dims,\n            col=col,\n            ax=ax,\n            type=type,\n            morph_plot_kwargs=morph_plot_kwargs,\n        )\n\n        pre_locs = self.edges[\"pre_locs\"].to_numpy()\n        post_locs = self.edges[\"post_locs\"].to_numpy()\n        pre_comp = self.edges[\"pre_global_comp_index\"].to_numpy()\n        nodes = self.nodes.set_index(\"global_comp_index\")\n        pre_branch = nodes.loc[pre_comp, \"global_branch_index\"].to_numpy()\n        post_comp = self.edges[\"post_global_comp_index\"].to_numpy()\n        post_branch = nodes.loc[post_comp, \"global_branch_index\"].to_numpy()\n\n        dims_np = np.asarray(dims)\n\n        for pre_loc, post_loc, pre_b, post_b in zip(\n            pre_locs, post_locs, pre_branch, post_branch\n        ):\n            pre_coord = self.xyzr[pre_b]\n            if len(pre_coord) == 2:\n                # If only start and end point of a branch are traced, perform a\n                # linear interpolation to get the synpase location.\n                pre_coord = pre_coord[0] + (pre_coord[1] - pre_coord[0]) * pre_loc\n            else:\n                # If densely traced, use intermediate trace values for synapse loc.\n                middle_ind = int((len(pre_coord) - 1) * pre_loc)\n                pre_coord = pre_coord[middle_ind]\n\n            post_coord = self.xyzr[post_b]\n            if len(post_coord) == 2:\n                # If only start and end point of a branch are traced, perform a\n                # linear interpolation to get the synpase location.\n                post_coord = (\n                    post_coord[0] + (post_coord[1] - post_coord[0]) * post_loc\n                )\n            else:\n                # If densely traced, use intermediate trace values for synapse loc.\n                middle_ind = int((len(post_coord) - 1) * post_loc)\n                post_coord = post_coord[middle_ind]\n\n            coords = np.stack([pre_coord[dims_np], post_coord[dims_np]]).T\n            ax.plot(\n                coords[0],\n                coords[1],\n                c=synapse_col,\n                **synapse_plot_kwargs,\n            )\n            ax.scatter(\n                post_coord[dims_np[0]],\n                post_coord[dims_np[1]],\n                c=synapse_col,\n                **synapse_scatter_kwargs,\n            )\n    else:\n        raise ValueError(\"detail must be in {full, point}.\")\n\n    return ax\n
"},{"location":"reference/optimize/","title":"Optimization","text":""},{"location":"reference/optimize/#jaxley.optimize.optimizer.TypeOptimizer","title":"TypeOptimizer","text":"

optax wrapper which allows different argument values for different params.

Source code in jaxley/optimize/optimizer.py
class TypeOptimizer:\n    \"\"\"`optax` wrapper which allows different argument values for different params.\"\"\"\n\n    def __init__(\n        self,\n        optimizer: Callable,\n        optimizer_args: Dict[str, Any],\n        opt_params: List[Dict[str, jnp.ndarray]],\n    ):\n        \"\"\"Create the optimizers.\n\n        This requires access to `opt_params` in order to know how many optimizers\n        should be created. It creates `len(opt_params)` optimizers.\n\n        Example usage:\n        ```\n        lrs = {\"HH_gNa\": 0.01, \"radius\": 1.0}\n        optimizer = TypeOptimizer(lambda lr: optax.adam(lr), lrs, opt_params)\n        opt_state = optimizer.init(opt_params)\n        ```\n\n        ```\n        optimizer_args = {\"HH_gNa\": [0.01, 0.4], \"radius\": [1.0, 0.8]}\n        optimizer = TypeOptimizer(\n            lambda args: optax.sgd(args[0], momentum=args[1]),\n            optimizer_args,\n            opt_params\n        )\n        opt_state = optimizer.init(opt_params)\n        ```\n\n        Args:\n            optimizer: A Callable that takes the learning rate and returns the\n                `optax.optimizer` which should be used.\n            optimizer_args: The arguments for different kinds of parameters.\n                Each item of the dictionary will be passed to the `Callable` passed to\n                `optimizer`.\n            opt_params: The parameters to be optimized. The exact values are not used,\n                only the number of elements in the list and the key of each dict.\n        \"\"\"\n        self.base_optimizer = optimizer\n\n        self.optimizers = []\n        for params in opt_params:\n            names = list(params.keys())\n            assert len(names) == 1, \"Multiple parameters were added at once.\"\n            name = names[0]\n            optimizer = self.base_optimizer(optimizer_args[name])\n            self.optimizers.append({name: optimizer})\n\n    def init(self, opt_params: List[Dict[str, jnp.ndarray]]) -> List:\n        \"\"\"Initialize the optimizers. Equivalent to `optax.optimizers.init()`.\"\"\"\n        opt_states = []\n        for params, optimizer in zip(opt_params, self.optimizers):\n            name = list(optimizer.keys())[0]\n            opt_state = optimizer[name].init(params)\n            opt_states.append(opt_state)\n        return opt_states\n\n    def update(self, gradient: jnp.ndarray, opt_state: List) -> Tuple[List, List]:\n        \"\"\"Update the optimizers. Equivalent to `optax.optimizers.update()`.\"\"\"\n        all_updates = []\n        new_opt_states = []\n        for grad, state, opt in zip(gradient, opt_state, self.optimizers):\n            name = list(opt.keys())[0]\n            updates, new_opt_state = opt[name].update(grad, state)\n            all_updates.append(updates)\n            new_opt_states.append(new_opt_state)\n        return all_updates, new_opt_states\n
"},{"location":"reference/optimize/#jaxley.optimize.optimizer.TypeOptimizer.__init__","title":"__init__(optimizer, optimizer_args, opt_params)","text":"

Create the optimizers.

This requires access to opt_params in order to know how many optimizers should be created. It creates len(opt_params) optimizers.

Example usage:

lrs = {\"HH_gNa\": 0.01, \"radius\": 1.0}\noptimizer = TypeOptimizer(lambda lr: optax.adam(lr), lrs, opt_params)\nopt_state = optimizer.init(opt_params)\n

optimizer_args = {\"HH_gNa\": [0.01, 0.4], \"radius\": [1.0, 0.8]}\noptimizer = TypeOptimizer(\n    lambda args: optax.sgd(args[0], momentum=args[1]),\n    optimizer_args,\n    opt_params\n)\nopt_state = optimizer.init(opt_params)\n

Parameters:

Name Type Description Default optimizer Callable

A Callable that takes the learning rate and returns the optax.optimizer which should be used.

required optimizer_args Dict[str, Any]

The arguments for different kinds of parameters. Each item of the dictionary will be passed to the Callable passed to optimizer.

required opt_params List[Dict[str, ndarray]]

The parameters to be optimized. The exact values are not used, only the number of elements in the list and the key of each dict.

required Source code in jaxley/optimize/optimizer.py
def __init__(\n    self,\n    optimizer: Callable,\n    optimizer_args: Dict[str, Any],\n    opt_params: List[Dict[str, jnp.ndarray]],\n):\n    \"\"\"Create the optimizers.\n\n    This requires access to `opt_params` in order to know how many optimizers\n    should be created. It creates `len(opt_params)` optimizers.\n\n    Example usage:\n    ```\n    lrs = {\"HH_gNa\": 0.01, \"radius\": 1.0}\n    optimizer = TypeOptimizer(lambda lr: optax.adam(lr), lrs, opt_params)\n    opt_state = optimizer.init(opt_params)\n    ```\n\n    ```\n    optimizer_args = {\"HH_gNa\": [0.01, 0.4], \"radius\": [1.0, 0.8]}\n    optimizer = TypeOptimizer(\n        lambda args: optax.sgd(args[0], momentum=args[1]),\n        optimizer_args,\n        opt_params\n    )\n    opt_state = optimizer.init(opt_params)\n    ```\n\n    Args:\n        optimizer: A Callable that takes the learning rate and returns the\n            `optax.optimizer` which should be used.\n        optimizer_args: The arguments for different kinds of parameters.\n            Each item of the dictionary will be passed to the `Callable` passed to\n            `optimizer`.\n        opt_params: The parameters to be optimized. The exact values are not used,\n            only the number of elements in the list and the key of each dict.\n    \"\"\"\n    self.base_optimizer = optimizer\n\n    self.optimizers = []\n    for params in opt_params:\n        names = list(params.keys())\n        assert len(names) == 1, \"Multiple parameters were added at once.\"\n        name = names[0]\n        optimizer = self.base_optimizer(optimizer_args[name])\n        self.optimizers.append({name: optimizer})\n
"},{"location":"reference/optimize/#jaxley.optimize.optimizer.TypeOptimizer.init","title":"init(opt_params)","text":"

Initialize the optimizers. Equivalent to optax.optimizers.init().

Source code in jaxley/optimize/optimizer.py
def init(self, opt_params: List[Dict[str, jnp.ndarray]]) -> List:\n    \"\"\"Initialize the optimizers. Equivalent to `optax.optimizers.init()`.\"\"\"\n    opt_states = []\n    for params, optimizer in zip(opt_params, self.optimizers):\n        name = list(optimizer.keys())[0]\n        opt_state = optimizer[name].init(params)\n        opt_states.append(opt_state)\n    return opt_states\n
"},{"location":"reference/optimize/#jaxley.optimize.optimizer.TypeOptimizer.update","title":"update(gradient, opt_state)","text":"

Update the optimizers. Equivalent to optax.optimizers.update().

Source code in jaxley/optimize/optimizer.py
def update(self, gradient: jnp.ndarray, opt_state: List) -> Tuple[List, List]:\n    \"\"\"Update the optimizers. Equivalent to `optax.optimizers.update()`.\"\"\"\n    all_updates = []\n    new_opt_states = []\n    for grad, state, opt in zip(gradient, opt_state, self.optimizers):\n        name = list(opt.keys())[0]\n        updates, new_opt_state = opt[name].update(grad, state)\n        all_updates.append(updates)\n        new_opt_states.append(new_opt_state)\n    return all_updates, new_opt_states\n
"},{"location":"reference/optimize/#jaxley.optimize.transforms.AffineTransform","title":"AffineTransform","text":"

Bases: Transform

Source code in jaxley/optimize/transforms.py
class AffineTransform(Transform):\n    def __init__(self, scale: ArrayLike, shift: ArrayLike):\n        \"\"\"This transform rescales and shifts the input.\n\n        Args:\n            scale (ArrayLike): Scaling factor.\n            shift (ArrayLike): Additive shift.\n\n        Raises:\n            ValueError: Scale needs to be larger than 0\n        \"\"\"\n        if jnp.allclose(scale, 0):\n            raise ValueError(\"a cannot be zero, must be invertible\")\n        self.a = scale\n        self.b = shift\n\n    def forward(self, x: ArrayLike) -> Array:\n        return self.a * x + self.b\n\n    def inverse(self, x: ArrayLike) -> Array:\n        return (x - self.b) / self.a\n
"},{"location":"reference/optimize/#jaxley.optimize.transforms.AffineTransform.__init__","title":"__init__(scale, shift)","text":"

This transform rescales and shifts the input.

Parameters:

Name Type Description Default scale ArrayLike

Scaling factor.

required shift ArrayLike

Additive shift.

required

Raises:

Type Description ValueError

Scale needs to be larger than 0

Source code in jaxley/optimize/transforms.py
def __init__(self, scale: ArrayLike, shift: ArrayLike):\n    \"\"\"This transform rescales and shifts the input.\n\n    Args:\n        scale (ArrayLike): Scaling factor.\n        shift (ArrayLike): Additive shift.\n\n    Raises:\n        ValueError: Scale needs to be larger than 0\n    \"\"\"\n    if jnp.allclose(scale, 0):\n        raise ValueError(\"a cannot be zero, must be invertible\")\n    self.a = scale\n    self.b = shift\n
"},{"location":"reference/optimize/#jaxley.optimize.transforms.ChainTransform","title":"ChainTransform","text":"

Bases: Transform

Chaining together multiple transformations

Source code in jaxley/optimize/transforms.py
class ChainTransform(Transform):\n    \"\"\"Chaining together multiple transformations\"\"\"\n\n    def __init__(self, transforms: Sequence[Transform]) -> None:\n        \"\"\"A chain of transformations\n\n        Args:\n            transforms (Sequence[Transform]): Transforms to apply\n        \"\"\"\n        super().__init__()\n        self.transforms = transforms\n\n    def forward(self, x: ArrayLike) -> Array:\n        for transform in self.transforms:\n            x = transform(x)\n        return x\n\n    def inverse(self, y: ArrayLike) -> Array:\n        for transform in reversed(self.transforms):\n            y = transform.inverse(y)\n        return y\n
"},{"location":"reference/optimize/#jaxley.optimize.transforms.ChainTransform.__init__","title":"__init__(transforms)","text":"

A chain of transformations

Parameters:

Name Type Description Default transforms Sequence[Transform]

Transforms to apply

required Source code in jaxley/optimize/transforms.py
def __init__(self, transforms: Sequence[Transform]) -> None:\n    \"\"\"A chain of transformations\n\n    Args:\n        transforms (Sequence[Transform]): Transforms to apply\n    \"\"\"\n    super().__init__()\n    self.transforms = transforms\n
"},{"location":"reference/optimize/#jaxley.optimize.transforms.CustomTransform","title":"CustomTransform","text":"

Bases: Transform

Custom transformation

Source code in jaxley/optimize/transforms.py
class CustomTransform(Transform):\n    \"\"\"Custom transformation\"\"\"\n\n    def __init__(self, forward_fn: Callable, inverse_fn: Callable) -> None:\n        \"\"\"A custom transformation using a user-defined froward and\n        inverse function\n\n        Args:\n            forward_fn (Callable): Forward transformation\n            inverse_fn (Callable): Inverse transformation\n        \"\"\"\n        super().__init__()\n        self.forward_fn = forward_fn\n        self.inverse_fn = inverse_fn\n\n    def forward(self, x: ArrayLike) -> Array:\n        return self.forward_fn(x)\n\n    def inverse(self, y: ArrayLike) -> Array:\n        return self.inverse_fn(y)\n
"},{"location":"reference/optimize/#jaxley.optimize.transforms.CustomTransform.__init__","title":"__init__(forward_fn, inverse_fn)","text":"

A custom transformation using a user-defined froward and inverse function

Parameters:

Name Type Description Default forward_fn Callable

Forward transformation

required inverse_fn Callable

Inverse transformation

required Source code in jaxley/optimize/transforms.py
def __init__(self, forward_fn: Callable, inverse_fn: Callable) -> None:\n    \"\"\"A custom transformation using a user-defined froward and\n    inverse function\n\n    Args:\n        forward_fn (Callable): Forward transformation\n        inverse_fn (Callable): Inverse transformation\n    \"\"\"\n    super().__init__()\n    self.forward_fn = forward_fn\n    self.inverse_fn = inverse_fn\n
"},{"location":"reference/optimize/#jaxley.optimize.transforms.MaskedTransform","title":"MaskedTransform","text":"

Bases: Transform

Source code in jaxley/optimize/transforms.py
class MaskedTransform(Transform):\n    def __init__(self, mask: ArrayLike, transform: Transform) -> None:\n        \"\"\"A masked transformation\n\n        Args:\n            mask (ArrayLike): Which elements to transform\n            transform (Transform): Transformation to apply\n        \"\"\"\n        super().__init__()\n        self.mask = mask\n        self.transform = transform\n\n    def forward(self, x: ArrayLike) -> Array:\n        return jnp.where(self.mask, self.transform.forward(x), x)\n\n    def inverse(self, y: ArrayLike) -> Array:\n        return jnp.where(self.mask, self.transform.inverse(y), y)\n
"},{"location":"reference/optimize/#jaxley.optimize.transforms.MaskedTransform.__init__","title":"__init__(mask, transform)","text":"

A masked transformation

Parameters:

Name Type Description Default mask ArrayLike

Which elements to transform

required transform Transform

Transformation to apply

required Source code in jaxley/optimize/transforms.py
def __init__(self, mask: ArrayLike, transform: Transform) -> None:\n    \"\"\"A masked transformation\n\n    Args:\n        mask (ArrayLike): Which elements to transform\n        transform (Transform): Transformation to apply\n    \"\"\"\n    super().__init__()\n    self.mask = mask\n    self.transform = transform\n
"},{"location":"reference/optimize/#jaxley.optimize.transforms.NegSoftplusTransform","title":"NegSoftplusTransform","text":"

Bases: SoftplusTransform

Negative softplus transformation.

Source code in jaxley/optimize/transforms.py
class NegSoftplusTransform(SoftplusTransform):\n    \"\"\"Negative softplus transformation.\"\"\"\n\n    def __init__(self, upper: ArrayLike) -> None:\n        \"\"\"This transform maps any value bijectively to the interval (-inf, upper].\n\n        Args:\n            upper (ArrayLike): Upper bound of the interval.\n        \"\"\"\n        super().__init__(upper)\n\n    def forward(self, x: ArrayLike) -> Array:\n        return -super().forward(-x)\n\n    def inverse(self, y: ArrayLike) -> Array:\n        return -super().inverse(-y)\n
"},{"location":"reference/optimize/#jaxley.optimize.transforms.NegSoftplusTransform.__init__","title":"__init__(upper)","text":"

This transform maps any value bijectively to the interval (-inf, upper].

Parameters:

Name Type Description Default upper ArrayLike

Upper bound of the interval.

required Source code in jaxley/optimize/transforms.py
def __init__(self, upper: ArrayLike) -> None:\n    \"\"\"This transform maps any value bijectively to the interval (-inf, upper].\n\n    Args:\n        upper (ArrayLike): Upper bound of the interval.\n    \"\"\"\n    super().__init__(upper)\n
"},{"location":"reference/optimize/#jaxley.optimize.transforms.ParamTransform","title":"ParamTransform","text":"

Parameter transformation utility.

This class is used to transform parameters usually from an unconstrained space to a constrained space and back (bacause most biophysical parameter are bounded). The user can specify a PyTree of transforms that are applied to the parameters.

Attributes:

Name Type Description tf_dict

A PyTree of transforms for each parameter.

Source code in jaxley/optimize/transforms.py
class ParamTransform:\n    \"\"\"Parameter transformation utility.\n\n    This class is used to transform parameters usually from an unconstrained space to a constrained space\n    and back (bacause most biophysical parameter are bounded). The user can specify a PyTree of transforms\n    that are applied to the parameters.\n\n    Attributes:\n        tf_dict: A PyTree of transforms for each parameter.\n\n    \"\"\"\n\n    def __init__(self, tf_dict: List[Dict[str, Transform]] | Transform) -> None:\n        \"\"\"Creates a new ParamTransform object.\n\n        Args:\n            tf_dict: A PyTree of transforms for each parameter.\n        \"\"\"\n\n        self.tf_dict = tf_dict\n\n    def forward(\n        self, params: List[Dict[str, ArrayLike]] | ArrayLike\n    ) -> Dict[str, Array]:\n        \"\"\"Pushes unconstrained parameters through a tf such that they fit the interval.\n\n        Args:\n            params: A list of dictionaries (or any PyTree) with unconstrained parameters.\n\n        Returns:\n            A list of dictionaries (or any PyTree) with transformed parameters.\n\n        \"\"\"\n\n        return jax.tree_util.tree_map(lambda x, tf: tf.forward(x), params, self.tf_dict)\n\n    def inverse(\n        self, params: List[Dict[str, ArrayLike]] | ArrayLike\n    ) -> Dict[str, Array]:\n        \"\"\"Takes parameters from within the interval and makes them unconstrained.\n\n        Args:\n            params: A list of dictionaries (or any PyTree) with transformed parameters.\n\n        Returns:\n            A list of dictionaries (or any PyTree) with unconstrained parameters.\n        \"\"\"\n\n        return jax.tree_util.tree_map(lambda x, tf: tf.inverse(x), params, self.tf_dict)\n
"},{"location":"reference/optimize/#jaxley.optimize.transforms.ParamTransform.__init__","title":"__init__(tf_dict)","text":"

Creates a new ParamTransform object.

Parameters:

Name Type Description Default tf_dict List[Dict[str, Transform]] | Transform

A PyTree of transforms for each parameter.

required Source code in jaxley/optimize/transforms.py
def __init__(self, tf_dict: List[Dict[str, Transform]] | Transform) -> None:\n    \"\"\"Creates a new ParamTransform object.\n\n    Args:\n        tf_dict: A PyTree of transforms for each parameter.\n    \"\"\"\n\n    self.tf_dict = tf_dict\n
"},{"location":"reference/optimize/#jaxley.optimize.transforms.ParamTransform.forward","title":"forward(params)","text":"

Pushes unconstrained parameters through a tf such that they fit the interval.

Parameters:

Name Type Description Default params List[Dict[str, ArrayLike]] | ArrayLike

A list of dictionaries (or any PyTree) with unconstrained parameters.

required

Returns:

Type Description Dict[str, Array]

A list of dictionaries (or any PyTree) with transformed parameters.

Source code in jaxley/optimize/transforms.py
def forward(\n    self, params: List[Dict[str, ArrayLike]] | ArrayLike\n) -> Dict[str, Array]:\n    \"\"\"Pushes unconstrained parameters through a tf such that they fit the interval.\n\n    Args:\n        params: A list of dictionaries (or any PyTree) with unconstrained parameters.\n\n    Returns:\n        A list of dictionaries (or any PyTree) with transformed parameters.\n\n    \"\"\"\n\n    return jax.tree_util.tree_map(lambda x, tf: tf.forward(x), params, self.tf_dict)\n
"},{"location":"reference/optimize/#jaxley.optimize.transforms.ParamTransform.inverse","title":"inverse(params)","text":"

Takes parameters from within the interval and makes them unconstrained.

Parameters:

Name Type Description Default params List[Dict[str, ArrayLike]] | ArrayLike

A list of dictionaries (or any PyTree) with transformed parameters.

required

Returns:

Type Description Dict[str, Array]

A list of dictionaries (or any PyTree) with unconstrained parameters.

Source code in jaxley/optimize/transforms.py
def inverse(\n    self, params: List[Dict[str, ArrayLike]] | ArrayLike\n) -> Dict[str, Array]:\n    \"\"\"Takes parameters from within the interval and makes them unconstrained.\n\n    Args:\n        params: A list of dictionaries (or any PyTree) with transformed parameters.\n\n    Returns:\n        A list of dictionaries (or any PyTree) with unconstrained parameters.\n    \"\"\"\n\n    return jax.tree_util.tree_map(lambda x, tf: tf.inverse(x), params, self.tf_dict)\n
"},{"location":"reference/optimize/#jaxley.optimize.transforms.SigmoidTransform","title":"SigmoidTransform","text":"

Bases: Transform

Sigmoid transformation.

Source code in jaxley/optimize/transforms.py
class SigmoidTransform(Transform):\n    \"\"\"Sigmoid transformation.\"\"\"\n\n    def __init__(self, lower: ArrayLike, upper: ArrayLike) -> None:\n        \"\"\"This transform maps any value bijectively to the interval [lower, upper].\n\n        Args:\n            lower (ArrayLike): Lower bound of the interval.\n            upper (ArrayLike): Upper bound of the interval.\n        \"\"\"\n        super().__init__()\n        self.lower = lower\n        self.width = upper - lower\n\n    def forward(self, x: ArrayLike) -> Array:\n        y = 1.0 / (1.0 + save_exp(-x))\n        return self.lower + self.width * y\n\n    def inverse(self, y: ArrayLike) -> Array:\n        x = (y - self.lower) / self.width\n        x = -jnp.log((1.0 / x) - 1.0)\n        return x\n
"},{"location":"reference/optimize/#jaxley.optimize.transforms.SigmoidTransform.__init__","title":"__init__(lower, upper)","text":"

This transform maps any value bijectively to the interval [lower, upper].

Parameters:

Name Type Description Default lower ArrayLike

Lower bound of the interval.

required upper ArrayLike

Upper bound of the interval.

required Source code in jaxley/optimize/transforms.py
def __init__(self, lower: ArrayLike, upper: ArrayLike) -> None:\n    \"\"\"This transform maps any value bijectively to the interval [lower, upper].\n\n    Args:\n        lower (ArrayLike): Lower bound of the interval.\n        upper (ArrayLike): Upper bound of the interval.\n    \"\"\"\n    super().__init__()\n    self.lower = lower\n    self.width = upper - lower\n
"},{"location":"reference/optimize/#jaxley.optimize.transforms.SoftplusTransform","title":"SoftplusTransform","text":"

Bases: Transform

Softplus transformation.

Source code in jaxley/optimize/transforms.py
class SoftplusTransform(Transform):\n    \"\"\"Softplus transformation.\"\"\"\n\n    def __init__(self, lower: ArrayLike) -> None:\n        \"\"\"This transform maps any value bijectively to the interval [lower, inf).\n\n        Args:\n            lower (ArrayLike): Lower bound of the interval.\n        \"\"\"\n        super().__init__()\n        self.lower = lower\n\n    def forward(self, x: ArrayLike) -> Array:\n        return jnp.log1p(save_exp(x)) + self.lower\n\n    def inverse(self, y: ArrayLike) -> Array:\n        return jnp.log(save_exp(y - self.lower) - 1.0)\n
"},{"location":"reference/optimize/#jaxley.optimize.transforms.SoftplusTransform.__init__","title":"__init__(lower)","text":"

This transform maps any value bijectively to the interval [lower, inf).

Parameters:

Name Type Description Default lower ArrayLike

Lower bound of the interval.

required Source code in jaxley/optimize/transforms.py
def __init__(self, lower: ArrayLike) -> None:\n    \"\"\"This transform maps any value bijectively to the interval [lower, inf).\n\n    Args:\n        lower (ArrayLike): Lower bound of the interval.\n    \"\"\"\n    super().__init__()\n    self.lower = lower\n
"},{"location":"reference/utils/","title":"Utils","text":""},{"location":"reference/utils/#jaxley.utils.cell_utils.build_radiuses_from_xyzr","title":"build_radiuses_from_xyzr(radius_fns, branch_indices, min_radius, ncomp)","text":"

Return the radiuses of branches given SWC file xyzr.

Returns an array of shape (num_branches, ncomp).

Parameters:

Name Type Description Default radius_fns List[Callable]

Functions which, given compartment locations return the radius.

required branch_indices List[int]

The indices of the branches for which to return the radiuses.

required min_radius Optional[float]

If passed, the radiuses are clipped to be at least as large.

required ncomp int

The number of compartments that every branch is discretized into.

required Source code in jaxley/utils/cell_utils.py
def build_radiuses_from_xyzr(\n    radius_fns: List[Callable],\n    branch_indices: List[int],\n    min_radius: Optional[float],\n    ncomp: int,\n) -> jnp.ndarray:\n    \"\"\"Return the radiuses of branches given SWC file xyzr.\n\n    Returns an array of shape `(num_branches, ncomp)`.\n\n    Args:\n        radius_fns: Functions which, given compartment locations return the radius.\n        branch_indices: The indices of the branches for which to return the radiuses.\n        min_radius: If passed, the radiuses are clipped to be at least as large.\n        ncomp: The number of compartments that every branch is discretized into.\n    \"\"\"\n    # Compartment locations are at the center of the internal nodes.\n    non_split = 1 / ncomp\n    range_ = np.linspace(non_split / 2, 1 - non_split / 2, ncomp)\n\n    # Build radiuses.\n    radiuses = np.asarray([radius_fns[b](range_) for b in branch_indices])\n    radiuses_each = radiuses.ravel(order=\"C\")\n    if min_radius is None:\n        assert np.all(\n            radiuses_each > 0.0\n        ), \"Radius 0.0 in SWC file. Set `read_swc(..., min_radius=...)`.\"\n    else:\n        radiuses_each[radiuses_each < min_radius] = min_radius\n\n    return radiuses_each\n
"},{"location":"reference/utils/#jaxley.utils.cell_utils.compute_axial_conductances","title":"compute_axial_conductances(comp_edges, params)","text":"

Given comp_edges, radius, length, r_a, cm, compute the axial conductances.

Note that the resulting axial conductances will already by divided by the capacitance cm.

Source code in jaxley/utils/cell_utils.py
def compute_axial_conductances(\n    comp_edges: pd.DataFrame, params: Dict[str, jnp.ndarray]\n) -> jnp.ndarray:\n    \"\"\"Given `comp_edges`, radius, length, r_a, cm, compute the axial conductances.\n\n    Note that the resulting axial conductances will already by divided by the\n    capacitance `cm`.\n    \"\"\"\n    # `Compartment-to-compartment` (c2c) axial coupling conductances.\n    condition = comp_edges[\"type\"].to_numpy() == 0\n    source_comp_inds = np.asarray(comp_edges[condition][\"source\"].to_list())\n    sink_comp_inds = np.asarray(comp_edges[condition][\"sink\"].to_list())\n\n    if len(sink_comp_inds) > 0:\n        conds_c2c = (\n            vmap(compute_coupling_cond, in_axes=(0, 0, 0, 0, 0, 0))(\n                params[\"radius\"][sink_comp_inds],\n                params[\"radius\"][source_comp_inds],\n                params[\"axial_resistivity\"][sink_comp_inds],\n                params[\"axial_resistivity\"][source_comp_inds],\n                params[\"length\"][sink_comp_inds],\n                params[\"length\"][source_comp_inds],\n            )\n            / params[\"capacitance\"][sink_comp_inds]\n        )\n    else:\n        conds_c2c = jnp.asarray([])\n\n    # `branchpoint-to-compartment` (bp2c) axial coupling conductances.\n    condition = comp_edges[\"type\"].isin([1, 2])\n    sink_comp_inds = np.asarray(comp_edges[condition][\"sink\"].to_list())\n\n    if len(sink_comp_inds) > 0:\n        conds_bp2c = (\n            vmap(compute_coupling_cond_branchpoint, in_axes=(0, 0, 0))(\n                params[\"radius\"][sink_comp_inds],\n                params[\"axial_resistivity\"][sink_comp_inds],\n                params[\"length\"][sink_comp_inds],\n            )\n            / params[\"capacitance\"][sink_comp_inds]\n        )\n    else:\n        conds_bp2c = jnp.asarray([])\n\n    # `compartment-to-branchpoint` (c2bp) axial coupling conductances.\n    condition = comp_edges[\"type\"].isin([3, 4])\n    source_comp_inds = np.asarray(comp_edges[condition][\"source\"].to_list())\n\n    if len(source_comp_inds) > 0:\n        conds_c2bp = vmap(compute_impact_on_node, in_axes=(0, 0, 0))(\n            params[\"radius\"][source_comp_inds],\n            params[\"axial_resistivity\"][source_comp_inds],\n            params[\"length\"][source_comp_inds],\n        )\n        # For numerical stability. These values are very small, but their scale\n        # does not matter.\n        conds_c2bp *= 1_000\n    else:\n        conds_c2bp = jnp.asarray([])\n\n    # All axial coupling conductances.\n    return jnp.concatenate([conds_c2c, conds_bp2c, conds_c2bp])\n
"},{"location":"reference/utils/#jaxley.utils.cell_utils.compute_children_and_parents","title":"compute_children_and_parents(branch_edges)","text":"

Build indices used during `._init_morph_custom_spsolve().

Source code in jaxley/utils/cell_utils.py
def compute_children_and_parents(\n    branch_edges: pd.DataFrame,\n) -> Tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray, int]:\n    \"\"\"Build indices used during `._init_morph_custom_spsolve().\"\"\"\n    par_inds = branch_edges[\"parent_branch_index\"].to_numpy()\n    child_inds = branch_edges[\"child_branch_index\"].to_numpy()\n    child_belongs_to_branchpoint = remap_to_consecutive(par_inds)\n    par_inds = np.unique(par_inds)\n    return par_inds, child_inds, child_belongs_to_branchpoint\n
"},{"location":"reference/utils/#jaxley.utils.cell_utils.compute_children_indices","title":"compute_children_indices(parents)","text":"

Return all children indices of every branch.

Example:

parents = [-1, 0, 0]\ncompute_children_indices(parents) -> [[1, 2], [], []]\n

Source code in jaxley/utils/cell_utils.py
def compute_children_indices(parents) -> List[jnp.ndarray]:\n    \"\"\"Return all children indices of every branch.\n\n    Example:\n    ```\n    parents = [-1, 0, 0]\n    compute_children_indices(parents) -> [[1, 2], [], []]\n    ```\n    \"\"\"\n    num_branches = len(parents)\n    child_indices = []\n    for b in range(num_branches):\n        child_indices.append(np.where(parents == b)[0])\n    return child_indices\n
"},{"location":"reference/utils/#jaxley.utils.cell_utils.compute_coupling_cond","title":"compute_coupling_cond(rad1, rad2, r_a1, r_a2, l1, l2)","text":"

Return the coupling conductance between two compartments.

Equations taken from https://en.wikipedia.org/wiki/Compartmental_neuron_models.

radius: um r_a: ohm cm length_single_compartment: um coupling_conds: S * um / cm / um^2 = S / cm / um -> *10**7 -> mS / cm^2

Source code in jaxley/utils/cell_utils.py
def compute_coupling_cond(rad1, rad2, r_a1, r_a2, l1, l2):\n    \"\"\"Return the coupling conductance between two compartments.\n\n    Equations taken from `https://en.wikipedia.org/wiki/Compartmental_neuron_models`.\n\n    `radius`: um\n    `r_a`: ohm cm\n    `length_single_compartment`: um\n    `coupling_conds`: S * um / cm / um^2 = S / cm / um -> *10**7 -> mS / cm^2\n    \"\"\"\n    # Multiply by 10**7 to convert (S / cm / um) -> (mS / cm^2).\n    return rad1 * rad2**2 / (r_a1 * rad2**2 * l1 + r_a2 * rad1**2 * l2) / l1 * 10**7\n
"},{"location":"reference/utils/#jaxley.utils.cell_utils.compute_coupling_cond_branchpoint","title":"compute_coupling_cond_branchpoint(rad, r_a, l)","text":"

Return the coupling conductance between one compartment and a comp with l=0.

From https://en.wikipedia.org/wiki/Compartmental_neuron_models

If one compartment has l=0.0 then the equations simplify.

R_long = \\sum_i r_a * L_i/2 / crosssection_i

with crosssection = pi * r**2

For a single compartment with L>0, this turns into: R_long = r_a * L/2 / crosssection

Then, g_long = crosssection * 2 / L / r_a

Then, the effective conductance is g_long / zylinder_area. So: g = pi * r**2 * 2 / L / r_a / 2 / pi / r / L g = r / r_a / L**2

Source code in jaxley/utils/cell_utils.py
def compute_coupling_cond_branchpoint(rad, r_a, l):\n    r\"\"\"Return the coupling conductance between one compartment and a comp with l=0.\n\n    From https://en.wikipedia.org/wiki/Compartmental_neuron_models\n\n    If one compartment has l=0.0 then the equations simplify.\n\n    R_long = \\sum_i r_a * L_i/2 / crosssection_i\n\n    with crosssection = pi * r**2\n\n    For a single compartment with L>0, this turns into:\n    R_long = r_a * L/2 / crosssection\n\n    Then, g_long = crosssection * 2 / L / r_a\n\n    Then, the effective conductance is g_long / zylinder_area. So:\n    g = pi * r**2 * 2 / L / r_a / 2 / pi / r / L\n    g = r / r_a / L**2\n    \"\"\"\n    return rad / r_a / l**2 * 10**7  # Convert (S / cm / um) -> (mS / cm^2)\n
"},{"location":"reference/utils/#jaxley.utils.cell_utils.compute_impact_on_node","title":"compute_impact_on_node(rad, r_a, l)","text":"

Compute the weight with which a compartment influences its node.

In order to satisfy Kirchhoffs current law, the current at a branch point must be proportional to the crosssection of the compartment. We only require proportionality here because the branch point equation reads: g_1 * (V_1 - V_b) + g_2 * (V_2 - V_b) = 0.0

Because R_long = r_a * L/2 / crosssection, we get g_long = crosssection * 2 / L / r_a \\propto rad**2 / L / r_a

This equation can be multiplied by any constant.

Source code in jaxley/utils/cell_utils.py
def compute_impact_on_node(rad, r_a, l):\n    r\"\"\"Compute the weight with which a compartment influences its node.\n\n    In order to satisfy Kirchhoffs current law, the current at a branch point must be\n    proportional to the crosssection of the compartment. We only require proportionality\n    here because the branch point equation reads:\n    `g_1 * (V_1 - V_b) + g_2 * (V_2 - V_b) = 0.0`\n\n    Because R_long = r_a * L/2 / crosssection, we get\n    g_long = crosssection * 2 / L / r_a \\propto rad**2 / L / r_a\n\n    This equation can be multiplied by any constant.\"\"\"\n    return rad**2 / r_a / l\n
"},{"location":"reference/utils/#jaxley.utils.cell_utils.compute_morphology_indices_in_levels","title":"compute_morphology_indices_in_levels(num_branchpoints, child_belongs_to_branchpoint, par_inds, child_inds)","text":"

Return (row, col) to build the sparse matrix defining the voltage eqs.

This is run at init, not during runtime.

Source code in jaxley/utils/cell_utils.py
def compute_morphology_indices_in_levels(\n    num_branchpoints,\n    child_belongs_to_branchpoint,\n    par_inds,\n    child_inds,\n):\n    \"\"\"Return (row, col) to build the sparse matrix defining the voltage eqs.\n\n    This is run at `init`, not during runtime.\n    \"\"\"\n    branchpoint_inds_parents = jnp.arange(num_branchpoints)\n    branchpoint_inds_children = child_belongs_to_branchpoint\n    branch_inds_parents = par_inds\n    branch_inds_children = child_inds\n\n    children = jnp.stack([branch_inds_children, branchpoint_inds_children])\n    parents = jnp.stack([branch_inds_parents, branchpoint_inds_parents])\n\n    return {\"children\": children.T, \"parents\": parents.T}\n
"},{"location":"reference/utils/#jaxley.utils.cell_utils.convert_point_process_to_distributed","title":"convert_point_process_to_distributed(current, radius, length)","text":"

Convert current point process (nA) to distributed current (uA/cm2).

This function gets called for synapses and for external stimuli.

Parameters:

Name Type Description Default current ndarray

Current in nA.

required radius ndarray

Compartment radius in um.

required length ndarray

Compartment length in um.

required Return

Current in uA/cm2.

Source code in jaxley/utils/cell_utils.py
def convert_point_process_to_distributed(\n    current: jnp.ndarray, radius: jnp.ndarray, length: jnp.ndarray\n) -> jnp.ndarray:\n    \"\"\"Convert current point process (nA) to distributed current (uA/cm2).\n\n    This function gets called for synapses and for external stimuli.\n\n    Args:\n        current: Current in `nA`.\n        radius: Compartment radius in `um`.\n        length: Compartment length in `um`.\n\n    Return:\n        Current in `uA/cm2`.\n    \"\"\"\n    area = 2 * pi * radius * length\n    current /= area  # nA / um^2\n    return current * 100_000  # Convert (nA / um^2) to (uA / cm^2)\n
"},{"location":"reference/utils/#jaxley.utils.cell_utils.equal_segments","title":"equal_segments(branch_property, ncomp_per_branch)","text":"

Generates segments where some property is the same in each segment.

Parameters:

Name Type Description Default branch_property list

List of values of the property in each branch. Should have len(branch_property) == num_branches.

required Source code in jaxley/utils/cell_utils.py
def equal_segments(branch_property: list, ncomp_per_branch: int):\n    \"\"\"Generates segments where some property is the same in each segment.\n\n    Args:\n        branch_property: List of values of the property in each branch. Should have\n            `len(branch_property) == num_branches`.\n    \"\"\"\n    assert isinstance(branch_property, list), \"branch_property must be a list.\"\n    return jnp.asarray([branch_property] * ncomp_per_branch).T\n
"},{"location":"reference/utils/#jaxley.utils.cell_utils.get_num_neighbours","title":"get_num_neighbours(num_children, ncomp_per_branch, num_branches)","text":"

Number of neighbours of each compartment.

Source code in jaxley/utils/cell_utils.py
def get_num_neighbours(\n    num_children: jnp.ndarray,\n    ncomp_per_branch: int,\n    num_branches: int,\n):\n    \"\"\"\n    Number of neighbours of each compartment.\n    \"\"\"\n    num_neighbours = 2 * jnp.ones((num_branches * ncomp_per_branch))\n    num_neighbours = num_neighbours.at[ncomp_per_branch - 1].set(1.0)\n    num_neighbours = num_neighbours.at[jnp.arange(num_branches) * ncomp_per_branch].set(\n        num_children + 1.0\n    )\n    return num_neighbours\n
"},{"location":"reference/utils/#jaxley.utils.cell_utils.group_and_sum","title":"group_and_sum(values_to_sum, inds_to_group_by, num_branchpoints)","text":"

Group values by whether they have the same integer and sum values within group.

This is used to construct the last diagonals at the branch points.

Written by ChatGPT.

Source code in jaxley/utils/cell_utils.py
def group_and_sum(\n    values_to_sum: jnp.ndarray, inds_to_group_by: jnp.ndarray, num_branchpoints: int\n) -> jnp.ndarray:\n    \"\"\"Group values by whether they have the same integer and sum values within group.\n\n    This is used to construct the last diagonals at the branch points.\n\n    Written by ChatGPT.\n    \"\"\"\n    # Initialize an array to hold the sum of each group\n    group_sums = jnp.zeros(num_branchpoints)\n\n    # `.at[inds]` requires that `inds` is not empty, so we need an if-case here.\n    # `len(inds) == 0` is the case for branches and compartments.\n    if num_branchpoints > 0:\n        group_sums = group_sums.at[inds_to_group_by].add(values_to_sum)\n\n    return group_sums\n
"},{"location":"reference/utils/#jaxley.utils.cell_utils.interpolate_xyzr","title":"interpolate_xyzr(loc, coords)","text":"

Perform a linear interpolation between xyz-coordinates.

Parameters:

Name Type Description Default loc float

The location in [0,1] along the branch.

required coords ndarray

Array containing the reconstructed xyzr points of the branch.

required Return

Interpolated xyz coordinate at loc, shape `(3,).

Source code in jaxley/utils/cell_utils.py
def interpolate_xyzr(loc: float, coords: np.ndarray):\n    \"\"\"Perform a linear interpolation between xyz-coordinates.\n\n    Args:\n        loc: The location in [0,1] along the branch.\n        coords: Array containing the reconstructed xyzr points of the branch.\n\n    Return:\n        Interpolated xyz coordinate at `loc`, shape `(3,).\n    \"\"\"\n    dl = np.sqrt(np.sum(np.diff(coords[:, :3], axis=0) ** 2, axis=1))\n    pathlens = np.insert(np.cumsum(dl), 0, 0)  # cummulative length of sections\n    norm_pathlens = pathlens / np.maximum(1e-8, pathlens[-1])  # norm lengths to [0,1].\n\n    return v_interp(loc, norm_pathlens, coords)\n
"},{"location":"reference/utils/#jaxley.utils.cell_utils.linear_segments","title":"linear_segments(initial_val, endpoint_vals, parents, ncomp_per_branch)","text":"

Generates segments where some property is linearly interpolated.

Parameters:

Name Type Description Default initial_val float

The value at the tip of the soma.

required endpoint_vals list

The value at the endpoints of each branch.

required Source code in jaxley/utils/cell_utils.py
def linear_segments(\n    initial_val: float, endpoint_vals: list, parents: jnp.ndarray, ncomp_per_branch: int\n):\n    \"\"\"Generates segments where some property is linearly interpolated.\n\n    Args:\n        initial_val: The value at the tip of the soma.\n        endpoint_vals: The value at the endpoints of each branch.\n    \"\"\"\n    branch_property = endpoint_vals + [initial_val]\n    num_branches = len(parents)\n    # Compute radiuses by linear interpolation.\n    endpoint_radiuses = jnp.asarray(branch_property)\n\n    def compute_rad(branch_ind, loc):\n        start = endpoint_radiuses[parents[branch_ind]]\n        end = endpoint_radiuses[branch_ind]\n        return (end - start) * loc + start\n\n    branch_inds_of_each_comp = jnp.tile(jnp.arange(num_branches), ncomp_per_branch)\n    locs_of_each_comp = jnp.linspace(1, 0, ncomp_per_branch).repeat(num_branches)\n    rad_of_each_comp = compute_rad(branch_inds_of_each_comp, locs_of_each_comp)\n\n    return jnp.reshape(rad_of_each_comp, (ncomp_per_branch, num_branches)).T\n
"},{"location":"reference/utils/#jaxley.utils.cell_utils.loc_of_index","title":"loc_of_index(global_comp_index, global_branch_index, ncomp_per_branch)","text":"

Return location corresponding to global compartment index.

Source code in jaxley/utils/cell_utils.py
def loc_of_index(global_comp_index, global_branch_index, ncomp_per_branch):\n    \"\"\"Return location corresponding to global compartment index.\"\"\"\n    cumsum_ncomp = cumsum_leading_zero(ncomp_per_branch)\n    index = global_comp_index - cumsum_ncomp[global_branch_index]\n    ncomp = ncomp_per_branch[global_branch_index]\n    return (0.5 + index) / ncomp\n
"},{"location":"reference/utils/#jaxley.utils.cell_utils.local_index_of_loc","title":"local_index_of_loc(loc, global_branch_ind, ncomp_per_branch)","text":"

Returns the local index of a comp given a loc [0, 1] and the index of a branch.

This is used because we specify locations such as synapses as a value between 0 and 1. We have to convert this onto a discrete segment here.

Parameters:

Name Type Description Default branch_ind

Index of the branch.

required loc float

Location (in [0, 1]) along that branch.

required ncomp_per_branch int

Number of segments of each branch.

required

Returns:

Type Description int

The local index of the compartment.

Source code in jaxley/utils/cell_utils.py
def local_index_of_loc(\n    loc: float, global_branch_ind: int, ncomp_per_branch: int\n) -> int:\n    \"\"\"Returns the local index of a comp given a loc [0, 1] and the index of a branch.\n\n    This is used because we specify locations such as synapses as a value between 0 and\n    1. We have to convert this onto a discrete segment here.\n\n    Args:\n        branch_ind: Index of the branch.\n        loc: Location (in [0, 1]) along that branch.\n        ncomp_per_branch: Number of segments of each branch.\n\n    Returns:\n        The local index of the compartment.\n    \"\"\"\n    ncomp = ncomp_per_branch[global_branch_ind]  # only for convenience.\n    possible_locs = np.linspace(0.5 / ncomp, 1 - 0.5 / ncomp, ncomp)\n    ind_along_branch = np.argmin(np.abs(possible_locs - loc))\n    return ind_along_branch\n
"},{"location":"reference/utils/#jaxley.utils.cell_utils.merge_cells","title":"merge_cells(cumsum_num_branches, cumsum_num_branchpoints, arrs, exclude_first=True)","text":"

Build full list of which branches are solved in which iteration.

From the branching pattern of single cells, this \u201cmerges\u201d them into a single ordering of branches.

Parameters:

Name Type Description Default cumsum_num_branches List[int]

cumulative number of branches. E.g., for three cells with 10, 15, and 5 branches respectively, this will should be a list containing [0, 10, 25, 30].

required arrs List[List[ndarray]]

A list of a list of arrays that should be merged.

required exclude_first bool

If True, the first element of each list in arrs will remain unchanged. Useful if a -1 (which indicates \u201cno parent\u201d) entry should not be changed.

True

Returns:

Type Description ndarray

A list of arrays which contain the branch indices that are computed at each

ndarray

level (i.e., iteration).

Source code in jaxley/utils/cell_utils.py
def merge_cells(\n    cumsum_num_branches: List[int],\n    cumsum_num_branchpoints: List[int],\n    arrs: List[List[np.ndarray]],\n    exclude_first: bool = True,\n) -> np.ndarray:\n    \"\"\"\n    Build full list of which branches are solved in which iteration.\n\n    From the branching pattern of single cells, this \"merges\" them into a single\n    ordering of branches.\n\n    Args:\n        cumsum_num_branches: cumulative number of branches. E.g., for three cells with\n            10, 15, and 5 branches respectively, this will should be a list containing\n            `[0, 10, 25, 30]`.\n        arrs: A list of a list of arrays that should be merged.\n        exclude_first: If `True`, the first element of each list in `arrs` will remain\n            unchanged. Useful if a `-1` (which indicates \"no parent\") entry should not\n            be changed.\n\n    Returns:\n        A list of arrays which contain the branch indices that are computed at each\n        level (i.e., iteration).\n    \"\"\"\n    ps = []\n    for i, att in enumerate(arrs):\n        p = att\n        if exclude_first:\n            raise NotImplementedError\n            p = [p[0]] + [p_in_level + cumsum_num_branches[i] for p_in_level in p[1:]]\n        else:\n            p = [\n                p_in_level\n                + np.asarray([cumsum_num_branches[i], cumsum_num_branchpoints[i]])\n                for p_in_level in p\n            ]\n        ps.append(p)\n\n    max_len = max([len(att) for att in arrs])\n    combined_parents_in_level = []\n    for i in range(max_len):\n        current_ps = []\n        for p in ps:\n            if len(p) > i:\n                current_ps.append(p[i])\n        combined_parents_in_level.append(np.concatenate(current_ps))\n\n    return combined_parents_in_level\n
"},{"location":"reference/utils/#jaxley.utils.cell_utils.params_to_pstate","title":"params_to_pstate(params, indices_set_by_trainables)","text":"

Make outputs get_parameters() conform with outputs of .data_set().

make_trainable() followed by params=get_parameters() does not return indices because these indices would also be differentiated by jax.grad (as soon as the params are passed to def simulate(params). Therefore, in jx.integrate, we run the function to add indices to the dict. The outputs of params_to_pstate are of the same shape as the outputs of .data_set().

Source code in jaxley/utils/cell_utils.py
def params_to_pstate(\n    params: List[Dict[str, jnp.ndarray]],\n    indices_set_by_trainables: List[jnp.ndarray],\n):\n    \"\"\"Make outputs `get_parameters()` conform with outputs of `.data_set()`.\n\n    `make_trainable()` followed by `params=get_parameters()` does not return indices\n    because these indices would also be differentiated by `jax.grad` (as soon as\n    the `params` are passed to `def simulate(params)`. Therefore, in `jx.integrate`,\n    we run the function to add indices to the dict. The outputs of `params_to_pstate`\n    are of the same shape as the outputs of `.data_set()`.\"\"\"\n    return [\n        {\"key\": list(p.keys())[0], \"val\": list(p.values())[0], \"indices\": i}\n        for p, i in zip(params, indices_set_by_trainables)\n    ]\n
"},{"location":"reference/utils/#jaxley.utils.cell_utils.query_channel_states_and_params","title":"query_channel_states_and_params(d, keys, idcs)","text":"

Get dict with subset of keys and values from d.

This is used to restrict a dict where every item contains all states to only the ones that are relevant for the channel. E.g.

states = {'eCa': Array([ 0., 0., nan]}

will be states = {'eCa': Array([ 0., 0.]}

Only loops over necessary keys, as opposed to looping over d.items().

Source code in jaxley/utils/cell_utils.py
def query_channel_states_and_params(d, keys, idcs):\n    \"\"\"Get dict with subset of keys and values from d.\n\n    This is used to restrict a dict where every item contains __all__ states to only\n    the ones that are relevant for the channel. E.g.\n\n    ```states = {'eCa': Array([ 0.,  0., nan]}```\n\n    will be\n    ```states = {'eCa': Array([ 0.,  0.]}```\n\n    Only loops over necessary keys, as opposed to looping over `d.items()`.\"\"\"\n    return dict(zip(keys, (v[idcs] for v in map(d.get, keys))))\n
"},{"location":"reference/utils/#jaxley.utils.cell_utils.remap_to_consecutive","title":"remap_to_consecutive(arr)","text":"

Maps an array of integers to an array of consecutive integers.

E.g. [0, 0, 1, 4, 4, 6, 6] -> [0, 0, 1, 2, 2, 3, 3]

Source code in jaxley/utils/cell_utils.py
def remap_to_consecutive(arr):\n    \"\"\"Maps an array of integers to an array of consecutive integers.\n\n    E.g. `[0, 0, 1, 4, 4, 6, 6] -> [0, 0, 1, 2, 2, 3, 3]`\n    \"\"\"\n    _, inverse_indices = jnp.unique(arr, return_inverse=True)\n    return inverse_indices\n
"},{"location":"reference/utils/#jaxley.utils.plot_utils.compute_rotation_matrix","title":"compute_rotation_matrix(axis, angle)","text":"

Return the rotation matrix associated with counterclockwise rotation about the given axis by the given angle.

Can be used to rotate a coordinate vector by multiplying it with the rotation matrix.

Parameters:

Name Type Description Default axis ndarray

The axis of rotation.

required angle float

The angle of rotation in radians.

required

Returns:

Type Description ndarray

A 3x3 rotation matrix.

Source code in jaxley/utils/plot_utils.py
def compute_rotation_matrix(axis: ndarray, angle: float) -> ndarray:\n    \"\"\"\n    Return the rotation matrix associated with counterclockwise rotation about\n    the given axis by the given angle.\n\n    Can be used to rotate a coordinate vector by multiplying it with the rotation\n    matrix.\n\n    Args:\n        axis: The axis of rotation.\n        angle: The angle of rotation in radians.\n\n    Returns:\n        A 3x3 rotation matrix.\n    \"\"\"\n    axis = axis / np.sqrt(np.dot(axis, axis))\n    a = np.cos(angle / 2.0)\n    b, c, d = -axis * np.sin(angle / 2.0)\n    aa, bb, cc, dd = a * a, b * b, c * c, d * d\n    bc, ad, ac, ab, bd, cd = b * c, a * d, a * c, a * b, b * d, c * d\n    return np.array(\n        [\n            [aa + bb - cc - dd, 2 * (bc + ad), 2 * (bd - ac)],\n            [2 * (bc - ad), aa + cc - bb - dd, 2 * (cd + ab)],\n            [2 * (bd + ac), 2 * (cd - ab), aa + dd - bb - cc],\n        ]\n    )\n
"},{"location":"reference/utils/#jaxley.utils.plot_utils.create_cone_frustum_mesh","title":"create_cone_frustum_mesh(length, radius_bottom, radius_top, bottom_dome=False, top_dome=False, resolution=100)","text":"

Generates mesh points for a cone frustum, with optional domes at either end.

This is used to render the traced morphology in 3D (and to project it to 2D) as part of plot_morph. Sections between two traced coordinates with two different radii can be represented by a cone frustum. Additionally, the ends of the frustum can be capped with hemispheres to ensure that two neighbouring frustums are connected smoothly (like ball joints).

Parameters:

Name Type Description Default length float

The length of the frustum.

required radius_bottom float

The radius of the bottom of the frustum.

required radius_top float

The radius of the top of the frustum.

required bottom_dome bool

If True, a dome is added to the bottom of the frustum. The dome is a hemisphere with radius radius_bottom.

False top_dome bool

If True, a dome is added to the top of the frustum. The dome is a hemisphere with radius radius_top.

False resolution int

defines the resolution of the mesh. If too low (typically <10), can result in errors. Useful too have a simpler mesh for plotting.

100

Returns:

Type Description ndarray

An array of mesh points.

Source code in jaxley/utils/plot_utils.py
def create_cone_frustum_mesh(\n    length: float,\n    radius_bottom: float,\n    radius_top: float,\n    bottom_dome: bool = False,\n    top_dome: bool = False,\n    resolution: int = 100,\n) -> ndarray:\n    \"\"\"Generates mesh points for a cone frustum, with optional domes at either end.\n\n    This is used to render the traced morphology in 3D (and to project it to 2D)\n    as part of `plot_morph`. Sections between two traced coordinates with two\n    different radii can be represented by a cone frustum. Additionally, the ends\n    of the frustum can be capped with hemispheres to ensure that two neighbouring\n    frustums are connected smoothly (like ball joints).\n\n    Args:\n        length: The length of the frustum.\n        radius_bottom: The radius of the bottom of the frustum.\n        radius_top: The radius of the top of the frustum.\n        bottom_dome: If True, a dome is added to the bottom of the frustum.\n            The dome is a hemisphere with radius `radius_bottom`.\n        top_dome: If True, a dome is added to the top of the frustum.\n            The dome is a hemisphere with radius `radius_top`.\n        resolution: defines the resolution of the mesh.\n            If too low (typically <10), can result in errors.\n            Useful too have a simpler mesh for plotting.\n\n    Returns:\n        An array of mesh points.\n    \"\"\"\n\n    t = np.linspace(0, 2 * np.pi, resolution)\n\n    # Determine the total height including domes\n    total_height = length\n    total_height += radius_bottom if bottom_dome else 0\n    total_height += radius_top if top_dome else 0\n\n    z = np.linspace(0, total_height, resolution)\n    t_grid, z_coords = np.meshgrid(t, z)\n\n    # Initialize arrays\n    x_coords = np.zeros_like(t_grid)\n    y_coords = np.zeros_like(t_grid)\n    r_coords = np.zeros_like(t_grid)\n\n    # Bottom hemisphere\n    if bottom_dome:\n        dome_mask = z_coords < radius_bottom\n        arg = 1 - z_coords[dome_mask] / radius_bottom\n        arg[np.isclose(arg, 1, atol=1e-6, rtol=1e-6)] = 1\n        arg[np.isclose(arg, -1, atol=1e-6, rtol=1e-6)] = -1\n        phi = np.arccos(1 - z_coords[dome_mask] / radius_bottom)\n        r_coords[dome_mask] = radius_bottom * np.sin(phi)\n        z_coords[dome_mask] = z_coords[dome_mask]\n\n    # Frustum\n    frustum_start = radius_bottom if bottom_dome else 0\n    frustum_end = total_height - (radius_top if top_dome else 0)\n    frustum_mask = (z_coords >= frustum_start) & (z_coords <= frustum_end)\n    z_frustum = z_coords[frustum_mask] - frustum_start\n    r_coords[frustum_mask] = radius_bottom + (radius_top - radius_bottom) * (\n        z_frustum / length\n    )\n\n    # Top hemisphere\n    if top_dome:\n        dome_mask = z_coords > (total_height - radius_top)\n        arg = (z_coords[dome_mask] - (total_height - radius_top)) / radius_top\n        arg[np.isclose(arg, 1, atol=1e-6, rtol=1e-6)] = 1\n        arg[np.isclose(arg, -1, atol=1e-6, rtol=1e-6)] = -1\n        phi = np.arccos(arg)\n        r_coords[dome_mask] = radius_top * np.sin(phi)\n\n    x_coords = r_coords * np.cos(t_grid)\n    y_coords = r_coords * np.sin(t_grid)\n\n    return np.stack([x_coords, y_coords, z_coords])\n
"},{"location":"reference/utils/#jaxley.utils.plot_utils.create_cylinder_mesh","title":"create_cylinder_mesh(length, radius, resolution=100)","text":"

Generates mesh points for a cylinder.

This is used to render cylindrical compartments in 3D (and to project it to 2D) as part of plot_comps.

Parameters:

Name Type Description Default length float

The length of the cylinder.

required radius float

The radius of the cylinder.

required resolution int

defines the resolution of the mesh. If too low (typically <10), can result in errors. Useful too have a simpler mesh for plotting.

100

Returns:

Type Description ndarray

An array of mesh points.

Source code in jaxley/utils/plot_utils.py
def create_cylinder_mesh(\n    length: float, radius: float, resolution: int = 100\n) -> ndarray:\n    \"\"\"Generates mesh points for a cylinder.\n\n    This is used to render cylindrical compartments in 3D (and to project it to 2D)\n    as part of `plot_comps`.\n\n    Args:\n        length: The length of the cylinder.\n        radius: The radius of the cylinder.\n        resolution: defines the resolution of the mesh.\n            If too low (typically <10), can result in errors.\n            Useful too have a simpler mesh for plotting.\n\n    Returns:\n        An array of mesh points.\n    \"\"\"\n    # Define cylinder\n    t = np.linspace(0, 2 * np.pi, resolution)\n    z_coords = np.linspace(-length / 2, length / 2, resolution)\n    t_grid, z_coords = np.meshgrid(t, z_coords)\n\n    x_coords = radius * np.cos(t_grid)\n    y_coords = radius * np.sin(t_grid)\n    return np.stack([x_coords, y_coords, z_coords])\n
"},{"location":"reference/utils/#jaxley.utils.plot_utils.create_sphere_mesh","title":"create_sphere_mesh(radius, resolution=100)","text":"

Generates mesh points for a sphere.

This is used to render spherical compartments in 3D (and to project it to 2D) as part of plot_comps.

Parameters:

Name Type Description Default radius float

The radius of the sphere.

required resolution int

defines the resolution of the mesh. If too low (typically <10), can result in errors. Useful too have a simpler mesh for plotting.

100

Returns:

Type Description ndarray

An array of mesh points.

Source code in jaxley/utils/plot_utils.py
def create_sphere_mesh(radius: float, resolution: int = 100) -> np.ndarray:\n    \"\"\"Generates mesh points for a sphere.\n\n    This is used to render spherical compartments in 3D (and to project it to 2D)\n    as part of `plot_comps`.\n\n    Args:\n        radius: The radius of the sphere.\n        resolution: defines the resolution of the mesh.\n            If too low (typically <10), can result in errors.\n            Useful too have a simpler mesh for plotting.\n\n    Returns:\n        An array of mesh points.\n    \"\"\"\n    phi = np.linspace(0, np.pi, resolution)\n    theta = np.linspace(0, 2 * np.pi, resolution)\n\n    # Create a 2D meshgrid for phi and theta\n    phi_coords, theta_coords = np.meshgrid(phi, theta)\n\n    # Convert spherical coordinates to Cartesian coordinates\n    x_coords = radius * np.sin(phi_coords) * np.cos(theta_coords)\n    y_coords = radius * np.sin(phi_coords) * np.sin(theta_coords)\n    z_coords = radius * np.cos(phi_coords)\n\n    return np.stack([x_coords, y_coords, z_coords])\n
"},{"location":"reference/utils/#jaxley.utils.plot_utils.extract_outline","title":"extract_outline(points)","text":"

Get the outline of a 2D/3D shape.

Extracts the subset of points which form the convex hull, i.e. the outline of the input points.

Parameters:

Name Type Description Default points ndarray

An array of points / corrdinates.

required

Returns:

Type Description ndarray

An array of points which form the convex hull.

Source code in jaxley/utils/plot_utils.py
def extract_outline(points: ndarray) -> ndarray:\n    \"\"\"Get the outline of a 2D/3D shape.\n\n    Extracts the subset of points which form the convex hull, i.e. the outline of\n    the input points.\n\n    Args:\n        points: An array of points / corrdinates.\n\n    Returns:\n        An array of points which form the convex hull.\n    \"\"\"\n    hull = ConvexHull(points)\n    hull_points = points[hull.vertices]\n    return hull_points\n
"},{"location":"reference/utils/#jaxley.utils.plot_utils.plot_comps","title":"plot_comps(module_or_view, dims=(0, 1), col='k', ax=None, comp_plot_kwargs={}, true_comp_length=True, resolution=100)","text":"

Plot compartmentalized neural morphology.

Plots the projection of the cylindrical compartments.

Parameters:

Name Type Description Default module_or_view Union[Module, View]

The module or view to plot.

required dims Tuple[int]

The dimensions to plot / to project the cylinder onto, i.e. [0,1] xy-plane or [0,1,2] for 3D.

(0, 1) col str

The color for all compartments

'k' ax Optional[Axes]

The matplotlib axis to plot on.

None comp_plot_kwargs Dict

The plot kwargs for plt.fill.

{} true_comp_length bool

If True, the length of the compartment is used, i.e. the length of the traced neurite. This means for zig-zagging neurites the cylinders will be longer than the straight-line distance between the start and end point of the neurite. This can lead to overlapping and miss-aligned cylinders. Setting this False will use the straight-line distance instead for nicer plots.

True resolution int

defines the resolution of the mesh. If too low (typically <10), can result in errors. Useful too have a simpler mesh for plotting.

100

Returns:

Type Description Axes

Plot of the compartmentalized morphology.

Source code in jaxley/utils/plot_utils.py
def plot_comps(\n    module_or_view: Union[\"jx.Module\", \"jx.View\"],\n    dims: Tuple[int] = (0, 1),\n    col: str = \"k\",\n    ax: Optional[Axes] = None,\n    comp_plot_kwargs: Dict = {},\n    true_comp_length: bool = True,\n    resolution: int = 100,\n) -> Axes:\n    \"\"\"Plot compartmentalized neural morphology.\n\n    Plots the projection of the cylindrical compartments.\n\n    Args:\n        module_or_view: The module or view to plot.\n        dims: The dimensions to plot / to project the cylinder onto,\n            i.e. [0,1] xy-plane or [0,1,2] for 3D.\n        col: The color for all compartments\n        ax: The matplotlib axis to plot on.\n        comp_plot_kwargs: The plot kwargs for plt.fill.\n        true_comp_length: If True, the length of the compartment is used, i.e. the\n            length of the traced neurite. This means for zig-zagging neurites the\n            cylinders will be longer than the straight-line distance between the\n            start and end point of the neurite. This can lead to overlapping and\n            miss-aligned cylinders. Setting this False will use the straight-line\n            distance instead for nicer plots.\n        resolution: defines the resolution of the mesh.\n            If too low (typically <10), can result in errors.\n            Useful too have a simpler mesh for plotting.\n\n    Returns:\n        Plot of the compartmentalized morphology.\n    \"\"\"\n    if ax is None:\n        fig = plt.figure(figsize=(3, 3))\n        ax = fig.add_subplot(111) if len(dims) < 3 else plt.axes(projection=\"3d\")\n\n    assert not np.any(\n        np.isnan(module_or_view.xyzr[0][:, :3])\n    ), \"missing xyz coordinates.\"\n    if \"x\" not in module_or_view.nodes.columns:\n        module_or_view.compute_compartment_centers()\n\n    for idx, xyzr in zip(module_or_view._branches_in_view, module_or_view.xyzr):\n        locs = xyzr[:, :3]\n        if locs.shape[0] == 1:  # assume spherical comp\n            radius = xyzr[:, -1]\n            center = xyzr[0, :3]\n            if len(dims) == 3:\n                xyz = create_sphere_mesh(radius, resolution)\n                ax = plot_mesh(\n                    xyz,\n                    np.array([0, 0, 1]),\n                    center,\n                    np.array(dims),\n                    ax,\n                    color=col,\n                    **comp_plot_kwargs,\n                )\n            else:\n                ax.add_artist(plt.Circle(locs[0, dims], radius, color=col))\n        else:\n            lens = np.sqrt(np.nansum(np.diff(locs, axis=0) ** 2, axis=1))\n            lens = np.cumsum([0] + lens.tolist())\n            comp_ends = v_interp(\n                np.linspace(0, lens[-1], module_or_view.ncomp + 1), lens, locs\n            ).T\n            axes = np.diff(comp_ends, axis=0)\n            cylinder_lens = np.sqrt(np.sum(axes**2, axis=1))\n\n            branch_df = module_or_view.nodes[\n                module_or_view.nodes[\"global_branch_index\"] == idx\n            ]\n            for l, axis, (i, comp) in zip(cylinder_lens, axes, branch_df.iterrows()):\n                center = comp[[\"x\", \"y\", \"z\"]]\n                radius = comp[\"radius\"]\n                length = comp[\"length\"] if true_comp_length else l\n                xyz = create_cylinder_mesh(length, radius, resolution)\n                ax = plot_mesh(\n                    xyz,\n                    axis,\n                    center,\n                    np.array(dims),\n                    ax,\n                    color=col,\n                    **comp_plot_kwargs,\n                )\n    return ax\n
"},{"location":"reference/utils/#jaxley.utils.plot_utils.plot_graph","title":"plot_graph(xyzr, dims=(0, 1), col='k', ax=None, type='line', morph_plot_kwargs={})","text":"

Plot morphology.

Parameters:

Name Type Description Default xyzr ndarray

The coordinates of the morphology.

required dims Tuple[int]

Which dimensions to plot. 1=x, 2=y, 3=z coordinate. Must be a tuple of two or three of them.

(0, 1) col str

The color for all branches.

'k' ax Optional[Axes]

The matplotlib axis to plot on.

None type str

Either line or scatter.

'line' morph_plot_kwargs Dict

The plot kwargs for plt.plot or plt.scatter.

{} Source code in jaxley/utils/plot_utils.py
def plot_graph(\n    xyzr: ndarray,\n    dims: Tuple[int] = (0, 1),\n    col: str = \"k\",\n    ax: Optional[Axes] = None,\n    type: str = \"line\",\n    morph_plot_kwargs: Dict = {},\n) -> Axes:\n    \"\"\"Plot morphology.\n\n    Args:\n        xyzr: The coordinates of the morphology.\n        dims: Which dimensions to plot. 1=x, 2=y, 3=z coordinate. Must be a tuple of\n            two or three of them.\n        col: The color for all branches.\n        ax: The matplotlib axis to plot on.\n        type: Either `line` or `scatter`.\n        morph_plot_kwargs: The plot kwargs for plt.plot or plt.scatter.\n    \"\"\"\n\n    if ax is None:\n        fig = plt.figure(figsize=(3, 3))\n        ax = fig.add_subplot(111) if len(dims) < 3 else plt.axes(projection=\"3d\")\n\n    for coords_of_branch in xyzr:\n        points = coords_of_branch[:, dims].T\n\n        if \"line\" in type.lower():\n            _ = ax.plot(*points, color=col, **morph_plot_kwargs)\n        elif \"scatter\" in type.lower():\n            _ = ax.scatter(*points, color=col, **morph_plot_kwargs)\n        else:\n            raise NotImplementedError\n\n    return ax\n
"},{"location":"reference/utils/#jaxley.utils.plot_utils.plot_mesh","title":"plot_mesh(mesh_points, orientation, center, dims, ax=None, **kwargs)","text":"

Plot the 2D projection of a volume mesh on a cardinal plane.

Project the projection of a cylinder that is oriented in 3D space. - Create cylinder mesh - rotate cylinder mesh to orient it lengthwise along a given orientation vector. - move its center - project onto plane - compute outline of projected mesh. - fill area inside the outline

Parameters:

Name Type Description Default mesh_points ndarray

coordinates of the xyz mesh that define the volume

required orientation ndarray

orientation vector. The cylinder will be oriented along this vector.

required center ndarray

The x,y,z coordinates of the center of the cylinder.

required dims Tuple[int]

The dimensions to plot / to project the cylinder onto,

required ax Axes

The matplotlib axis to plot on.

None

Returns:

Type Description Axes

Plot of the cylinder projection.

Source code in jaxley/utils/plot_utils.py
def plot_mesh(\n    mesh_points: ndarray,\n    orientation: ndarray,\n    center: ndarray,\n    dims: Tuple[int],\n    ax: Axes = None,\n    **kwargs,\n) -> Axes:\n    \"\"\"Plot the 2D projection of a volume mesh on a cardinal plane.\n\n    Project the projection of a cylinder that is oriented in 3D space.\n    - Create cylinder mesh\n    - rotate cylinder mesh to orient it lengthwise along a given orientation vector.\n    - move its center\n    - project onto plane\n    - compute outline of projected mesh.\n    - fill area inside the outline\n\n    Args:\n        mesh_points: coordinates of the xyz mesh that define the volume\n        orientation: orientation vector. The cylinder will be oriented along this vector.\n        center: The x,y,z coordinates of the center of the cylinder.\n        dims: The dimensions to plot / to project the cylinder onto,\n        i.e. [0,1] xy-plane or [0,1,2] for 3D.\n        ax: The matplotlib axis to plot on.\n\n    Returns:\n        Plot of the cylinder projection.\n    \"\"\"\n    if ax is None:\n        fig = plt.figure(figsize=(3, 3))\n        ax = fig.add_subplot(111) if len(dims) < 3 else plt.axes(projection=\"3d\")\n\n    # Normalize axis vector\n    orientation = np.array(orientation)\n    orientation = orientation / np.linalg.norm(orientation)\n\n    # Create a rotation matrix to align the cylinder with the given axis\n    z_axis = np.array([0, 0, 1])\n    rotation_axis = np.cross(z_axis, orientation)\n    rotation_angle = np.arccos(np.dot(z_axis, orientation))\n\n    if np.allclose(rotation_axis, 0):\n        rotation_matrix = np.eye(3)\n    else:\n        rotation_matrix = compute_rotation_matrix(rotation_axis, rotation_angle)\n\n    # Rotate mesh\n    x_mesh, y_mesh, z_mesh = mesh_points\n    rotated_mesh_points = np.dot(\n        rotation_matrix,\n        np.array([x_mesh.flatten(), y_mesh.flatten(), z_mesh.flatten()]),\n    )\n    rotated_mesh_points = rotated_mesh_points.reshape(3, -1)\n\n    # project onto plane and move\n    rotated_mesh_points = rotated_mesh_points[dims]\n    rotated_mesh_points += np.array(center)[dims, np.newaxis]\n\n    if len(dims) < 3:\n        # get outline of cylinder mesh\n        mesh_outline = extract_outline(rotated_mesh_points.T).T\n        ax.fill(*mesh_outline.reshape(mesh_outline.shape[0], -1), **kwargs)\n    else:\n        # plot 3d mesh\n        ax.plot_surface(*rotated_mesh_points.reshape(*mesh_points.shape), **kwargs)\n    return ax\n
"},{"location":"reference/utils/#jaxley.utils.plot_utils.plot_morph","title":"plot_morph(module_or_view, dims=(0, 1), col='k', ax=None, resolution=100, morph_plot_kwargs={})","text":"

Plot the detailed morphology.

Plots the traced morphology it was traced. That means at every point that was traced a disc of radius r is plotted. The outline of the discs are then connected to form the morphology. This means every trace segement can be represented by a cone frustum. To prevent breaks in the morphology, each segement is connected with a ball joint.

Parameters:

Name Type Description Default module_or_view Union[Module, View]

The module or view to plot.

required dims Tuple[int]

The dimensions to plot / to project the cylinder onto, i.e. [0,1] xy-plane or [0,1,2] for 3D.

(0, 1) col str

The color for all branches

'k' ax Optional[Axes]

The matplotlib axis to plot on.

None morph_plot_kwargs Dict

The plot kwargs for plt.fill.

{} resolution int

defines the resolution of the mesh. If too low (typically <10), can result in errors. Useful too have a simpler mesh for plotting.

100

Returns:

Type Description Axes

Plot of the detailed morphology.

Source code in jaxley/utils/plot_utils.py
def plot_morph(\n    module_or_view: Union[\"jx.Module\", \"jx.View\"],\n    dims: Tuple[int] = (0, 1),\n    col: str = \"k\",\n    ax: Optional[Axes] = None,\n    resolution: int = 100,\n    morph_plot_kwargs: Dict = {},\n) -> Axes:\n    \"\"\"Plot the detailed morphology.\n\n    Plots the traced morphology it was traced. That means at every point that was\n    traced a disc of radius `r` is plotted. The outline of the discs are then\n    connected to form the morphology. This means every trace segement can be\n    represented by a cone frustum. To prevent breaks in the morphology, each\n    segement is connected with a ball joint.\n\n    Args:\n        module_or_view: The module or view to plot.\n        dims: The dimensions to plot / to project the cylinder onto,\n            i.e. [0,1] xy-plane or [0,1,2] for 3D.\n        col: The color for all branches\n        ax: The matplotlib axis to plot on.\n        morph_plot_kwargs: The plot kwargs for plt.fill.\n\n        resolution: defines the resolution of the mesh.\n            If too low (typically <10), can result in errors.\n            Useful too have a simpler mesh for plotting.\n\n    Returns:\n        Plot of the detailed morphology.\"\"\"\n    if ax is None:\n        fig = plt.figure(figsize=(3, 3))\n        ax = fig.add_subplot(111) if len(dims) < 3 else plt.axes(projection=\"3d\")\n    if len(dims) == 3:\n        warn(\n            \"rendering large morphologies in 3D can take a while. Consider projecting to 2D instead.\"\n        )\n\n    assert not np.any(\n        np.isnan(module_or_view.xyzr[0][:, :3])\n    ), \"missing xyz coordinates.\"\n\n    for xyzr in module_or_view.xyzr:\n        if len(xyzr) > 1:\n            for xyzr1, xyzr2 in zip(xyzr[1:, :], xyzr[:-1, :]):\n                dxyz = xyzr2[:3] - xyzr1[:3]\n                length = np.sqrt(np.sum(dxyz**2))\n                points = create_cone_frustum_mesh(\n                    length,\n                    xyzr1[-1],\n                    xyzr2[-1],\n                    bottom_dome=True,\n                    top_dome=True,\n                    resolution=resolution,\n                )\n                plot_mesh(\n                    points,\n                    dxyz,\n                    xyzr1[:3],\n                    np.array(dims),\n                    color=col,\n                    ax=ax,\n                    **morph_plot_kwargs,\n                )\n        else:\n            points = create_cone_frustum_mesh(\n                0,\n                xyzr[:, -1],\n                xyzr[:, -1],\n                bottom_dome=True,\n                top_dome=True,\n                resolution=resolution,\n            )\n            plot_mesh(\n                points,\n                np.ones(3),\n                xyzr[0, :3],\n                dims=np.array(dims),\n                color=col,\n                ax=ax,\n                **morph_plot_kwargs,\n            )\n\n    return ax\n
"},{"location":"reference/utils/#jaxley.utils.jax_utils.nested_checkpoint_scan","title":"nested_checkpoint_scan(f, init, xs, length=None, *, nested_lengths, scan_fn=jax.lax.scan, checkpoint_fn=jax.checkpoint)","text":"

A version of lax.scan that supports recursive gradient checkpointing.

Code taken from: https://github.com/google/jax/issues/2139

The interface of nested_checkpoint_scan exactly matches lax.scan, except for the required nested_lengths argument.

The key feature of nested_checkpoint_scan is that gradient calculations require O(max(nested_lengths)) memory, vs O(prod(nested_lengths)) for unnested scans, which it achieves by re-evaluating the forward pass len(nested_lengths) - 1 times.

nested_checkpoint_scan reduces to lax.scan when nested_lengths has a single element.

Parameters:

Name Type Description Default f Callable[[Carry, Dict[str, ndarray]], Tuple[Carry, Output]]

function to scan over.

required init Carry

initial value.

required xs Dict[str, ndarray]

scanned over values.

required length Optional[int]

leading length of all dimensions

None nested_lengths Sequence[int]

required list of lengths to scan over for each level of checkpointing. The product of nested_lengths must match length (if provided) and the size of the leading axis for all arrays in xs.

required scan_fn

function matching the API of lax.scan

scan checkpoint_fn Callable[[Func], Func]

function matching the API of jax.checkpoint.

checkpoint Source code in jaxley/utils/jax_utils.py
def nested_checkpoint_scan(\n    f: Callable[[Carry, Dict[str, jnp.ndarray]], Tuple[Carry, Output]],\n    init: Carry,\n    xs: Dict[str, jnp.ndarray],\n    length: Optional[int] = None,\n    *,\n    nested_lengths: Sequence[int],\n    scan_fn=jax.lax.scan,\n    checkpoint_fn: Callable[[Func], Func] = jax.checkpoint,\n):\n    \"\"\"A version of lax.scan that supports recursive gradient checkpointing.\n\n    Code taken from: https://github.com/google/jax/issues/2139\n\n    The interface of `nested_checkpoint_scan` exactly matches lax.scan, except for\n    the required `nested_lengths` argument.\n\n    The key feature of `nested_checkpoint_scan` is that gradient calculations\n    require O(max(nested_lengths)) memory, vs O(prod(nested_lengths)) for unnested\n    scans, which it achieves by re-evaluating the forward pass\n    `len(nested_lengths) - 1` times.\n\n    `nested_checkpoint_scan` reduces to `lax.scan` when `nested_lengths` has a\n    single element.\n\n    Args:\n        f: function to scan over.\n        init: initial value.\n        xs: scanned over values.\n        length: leading length of all dimensions\n        nested_lengths: required list of lengths to scan over for each level of\n            checkpointing. The product of nested_lengths must match length (if\n            provided) and the size of the leading axis for all arrays in ``xs``.\n        scan_fn: function matching the API of lax.scan\n        checkpoint_fn: function matching the API of jax.checkpoint.\n    \"\"\"\n    if length is not None and length != math.prod(nested_lengths):\n        raise ValueError(f\"inconsistent {length=} and {nested_lengths=}\")\n\n    def nested_reshape(x):\n        x = jnp.asarray(x)\n        new_shape = tuple(nested_lengths) + x.shape[1:]\n        return x.reshape(new_shape)\n\n    sub_xs = jax.tree_util.tree_map(nested_reshape, xs)\n    return _inner_nested_scan(f, init, sub_xs, nested_lengths, scan_fn, checkpoint_fn)\n
"},{"location":"reference/utils/#jaxley.utils.syn_utils.gather_synapes","title":"gather_synapes(number_of_compartments, post_syn_comp_inds, current_each_synapse_voltage_term, current_each_synapse_constant_term)","text":"

Compute current at the post synapse.

All this does it that it sums the synaptic currents that come into a particular compartment. It returns an array of as many elements as there are compartments.

Source code in jaxley/utils/syn_utils.py
def gather_synapes(\n    number_of_compartments: jnp.ndarray,\n    post_syn_comp_inds: np.ndarray,\n    current_each_synapse_voltage_term: jnp.ndarray,\n    current_each_synapse_constant_term: jnp.ndarray,\n) -> Tuple[jnp.ndarray, jnp.ndarray]:\n    \"\"\"Compute current at the post synapse.\n\n    All this does it that it sums the synaptic currents that come into a particular\n    compartment. It returns an array of as many elements as there are compartments.\n    \"\"\"\n    incoming_currents_voltages = jnp.zeros((number_of_compartments,))\n    incoming_currents_contant = jnp.zeros((number_of_compartments,))\n\n    dnums = ScatterDimensionNumbers(\n        update_window_dims=(),\n        inserted_window_dims=(0,),\n        scatter_dims_to_operand_dims=(0,),\n    )\n    incoming_currents_voltages = scatter_add(\n        incoming_currents_voltages,\n        post_syn_comp_inds[:, None],\n        current_each_synapse_voltage_term,\n        dnums,\n    )\n    incoming_currents_contant = scatter_add(\n        incoming_currents_contant,\n        post_syn_comp_inds[:, None],\n        current_each_synapse_constant_term,\n        dnums,\n    )\n    return incoming_currents_voltages, incoming_currents_contant\n
"},{"location":"tutorial/00_jaxley_api/","title":"Key concepts in Jaxley","text":"

In this tutorial, we will introduce you to the basic concepts of Jaxley. You will learn about:

  • Modules (e.g., Cell, Network,\u2026)
    • nodes
    • edges
  • Views
    • Groups
  • Channels
  • Synapses

Here is a code snippet which you will learn to understand in this tutorial:

import jaxley as jx\nfrom jaxley.channels import Na, K, Leak\nfrom jaxley.synapses import IonotropicSynapse\nfrom jaxley.connect import connect\nimport matplotlib.pyplot as plt\nimport numpy as np\n\n\n# Assembling different Modules into a Network\ncomp = jx.Compartment()\nbranch = jx.Branch(comp, ncomp=1)\ncell = jx.Cell(branch, parents=[-1, 0, 0])\nnet = jx.Network([cell]*3)\n\n# Navigating and inspecting the Modules using Views\ncell0 = net.cell(0)\ncell0.nodes\n\n# How to group together parts of Modules\nnet.cell(1).add_to_group(\"cell1\")\n\n# inserting channels in the membrane\nwith net.cell(0) as cell0:\n    cell0.insert(Na())\n    cell0.insert(K())\n\n# connecting two cells using a Synapse\npre_comp = cell0.branch(1).comp(0)\npost_comp = net.cell1.branch(0).comp(0)\n\nconnect(pre_comp, post_comp)\n

First, we import the relevant libraries:

from jax import config\nconfig.update(\"jax_enable_x64\", True)\nconfig.update(\"jax_platform_name\", \"cpu\")\n\nimport jaxley as jx\nfrom jaxley.channels import Na, K, Leak\nfrom jaxley.synapses import IonotropicSynapse\nfrom jaxley.connect import connect\nimport matplotlib.pyplot as plt\nimport numpy as np\n
"},{"location":"tutorial/00_jaxley_api/#modules","title":"Modules","text":"

In Jaxley, we heavily rely on the concept of Modules to build biophyiscal models of neural systems at various scales. Jaxley implements four types of Modules: - Compartment - Branch - Cell - Network

Modules can be connected together to build increasingly detailed and complex models. Compartment -> Branch -> Cell -> Network.

Compartments are the atoms of biophysical models in Jaxley. All mechanisms and synaptic connections live on the level of Compartments and can already be simulated using jx.integrate on their own. Everything you do in Jaxley starts with a Compartment.

comp = jx.Compartment() # single compartment model.\n

Mutliple Compartments can be connected together to form longer, linear cables, which we call Branches and are equivalent to sections in NEURON.

ncomp = 4\nbranch = jx.Branch([comp] * ncomp)\n

In order to construct cell morphologies in Jaxley, multiple Branches can to be connected together as a Cell:

# -1 indicates that the first branch has no parent branch.\n# The other two branches both have the 0-eth branch as their parent.\nparents = [-1, 0, 0]\ncell = jx.Cell([branch] * len(parents), parents)\n

Finally, several Cells can be grouped together to form a Network, which can than be connected together using Synpases.

ncells = 2\nnet = jx.Network([cell]*ncells)\n\nnet.shape # shows you the num_cells, num_branches, num_comps\n
(2, 6, 24)\n

Every module tracks information about its current state and parameters in two Dataframes called nodes and edges. nodes contains all the information that we associate with compartments in the model (each row corresponds to one compartment) and edges tracks all the information relevant to synapses.

This means that you can easily keep track of the current state of your Module and how it changes at all times.

net.nodes\n
local_cell_index local_branch_index local_comp_index length radius axial_resistivity capacitance v global_cell_index global_branch_index global_comp_index controlled_by_param 0 0 0 0 10.0 1.0 5000.0 1.0 -70.0 0 0 0 0 1 0 0 1 10.0 1.0 5000.0 1.0 -70.0 0 0 1 0 2 0 0 2 10.0 1.0 5000.0 1.0 -70.0 0 0 2 0 3 0 0 3 10.0 1.0 5000.0 1.0 -70.0 0 0 3 0 4 0 1 0 10.0 1.0 5000.0 1.0 -70.0 0 1 4 0 5 0 1 1 10.0 1.0 5000.0 1.0 -70.0 0 1 5 0 6 0 1 2 10.0 1.0 5000.0 1.0 -70.0 0 1 6 0 7 0 1 3 10.0 1.0 5000.0 1.0 -70.0 0 1 7 0 8 0 2 0 10.0 1.0 5000.0 1.0 -70.0 0 2 8 0 9 0 2 1 10.0 1.0 5000.0 1.0 -70.0 0 2 9 0 10 0 2 2 10.0 1.0 5000.0 1.0 -70.0 0 2 10 0 11 0 2 3 10.0 1.0 5000.0 1.0 -70.0 0 2 11 0 12 1 0 0 10.0 1.0 5000.0 1.0 -70.0 1 3 12 0 13 1 0 1 10.0 1.0 5000.0 1.0 -70.0 1 3 13 0 14 1 0 2 10.0 1.0 5000.0 1.0 -70.0 1 3 14 0 15 1 0 3 10.0 1.0 5000.0 1.0 -70.0 1 3 15 0 16 1 1 0 10.0 1.0 5000.0 1.0 -70.0 1 4 16 0 17 1 1 1 10.0 1.0 5000.0 1.0 -70.0 1 4 17 0 18 1 1 2 10.0 1.0 5000.0 1.0 -70.0 1 4 18 0 19 1 1 3 10.0 1.0 5000.0 1.0 -70.0 1 4 19 0 20 1 2 0 10.0 1.0 5000.0 1.0 -70.0 1 5 20 0 21 1 2 1 10.0 1.0 5000.0 1.0 -70.0 1 5 21 0 22 1 2 2 10.0 1.0 5000.0 1.0 -70.0 1 5 22 0 23 1 2 3 10.0 1.0 5000.0 1.0 -70.0 1 5 23 0
net.edges.head() # this is currently empty since we have not made any connections yet\n
global_edge_index global_pre_comp_index global_post_comp_index pre_locs post_locs type type_ind"},{"location":"tutorial/00_jaxley_api/#views","title":"Views","text":"

Since these Modules can become very complex, Jaxley utilizes so called Views to make working with Modules easy and intuitive.

The simplest way to navigate Modules is by navigating them via the hierachy that we introduced above. A View is what you get when you index into the module. For example, for a Network:

net.cell(0)\n
View with 0 different channels. Use `.nodes` for details.\n

Views behave very similarly to Modules, i.e. the cell(0) (the 0th cell of the network) behaves like the cell we instantiated earlier. As such, cell(0) also has a nodes attribute, which keeps track of it\u2019s part of the network:

net.cell(0).nodes\n
local_cell_index local_branch_index local_comp_index length radius axial_resistivity capacitance v global_cell_index global_branch_index global_comp_index controlled_by_param 0 0 0 0 10.0 1.0 5000.0 1.0 -70.0 0 0 0 0 1 0 0 1 10.0 1.0 5000.0 1.0 -70.0 0 0 1 0 2 0 0 2 10.0 1.0 5000.0 1.0 -70.0 0 0 2 0 3 0 0 3 10.0 1.0 5000.0 1.0 -70.0 0 0 3 0 4 0 1 0 10.0 1.0 5000.0 1.0 -70.0 0 1 4 0 5 0 1 1 10.0 1.0 5000.0 1.0 -70.0 0 1 5 0 6 0 1 2 10.0 1.0 5000.0 1.0 -70.0 0 1 6 0 7 0 1 3 10.0 1.0 5000.0 1.0 -70.0 0 1 7 0 8 0 2 0 10.0 1.0 5000.0 1.0 -70.0 0 2 8 0 9 0 2 1 10.0 1.0 5000.0 1.0 -70.0 0 2 9 0 10 0 2 2 10.0 1.0 5000.0 1.0 -70.0 0 2 10 0 11 0 2 3 10.0 1.0 5000.0 1.0 -70.0 0 2 11 0

Let\u2019s use Views to visualize only parts of the Network. Before we do that, we create x, y, and z coordinates for the Network:

# Compute xyz coordinates of the cells.\nnet.compute_xyz()\n\n# Move cells (since they are placed on top of each other by default).\nnet.cell(0).move(y=30)\n

We can now visualize the entire net (i.e., the entire Module) with the .vis() method\u2026

# We can use the vis function to visualize Modules.\nfig, ax = plt.subplots(1, 1, figsize=(3,3))\nnet.vis(ax=ax)\n
<Axes: >\n

\u2026but we can also create a View to visualize only parts of the net:

# ... and Views\nfig, ax = plt.subplots(1,1, figsize=(3,3))\nnet.cell(0).vis(ax=ax, col=\"blue\") # View of the 0th cell of the network\nnet.cell(1).vis(ax=ax, col=\"red\") # View of the 1st cell of the network\n\nnet.cell(0).branch(0).vis(ax=ax, col=\"green\") # View of the 1st branch of the 0th cell of the network\nnet.cell(1).branch(1).comp(1).vis(ax=ax, col=\"black\", type=\"scatter\") # View of the 0th comp of the 1st branch of the 0th cell of the network\n
<Axes: >\n

"},{"location":"tutorial/00_jaxley_api/#how-to-create-views","title":"How to create Views","text":"

Above, we used net.cell(0) to generate a View of the 0-eth cell. Jaxley supports many ways of performing such indexing:

# several types of indices are supported (lists, ranges, ...)\nnet.cell([0,1]).branch(\"all\").comp(0)  # View of all 0th comps of all branches of cell 0 and 1\n\nbranch.loc(0.1)  # Equivalent to `NEURON`s `loc`. Assumes branches are continous from 0-1.\n\nnet[0,0,0]  # Modules/Views can also be lazily indexed\n\ncell0 = net.cell(0)  # Views can be assigned to variables and only track the parts of the Module they belong to\ncell0.branch(1).comp(0)  # Views can be continuely indexed\n
View with 0 different channels. Use `.nodes` for details.\n
cell0.nodes\n
local_cell_index local_branch_index local_comp_index length radius axial_resistivity capacitance v x y z global_cell_index global_branch_index global_comp_index controlled_by_param 0 0 0 0 10.0 1.0 5000.0 1.0 -70.0 5.000000 30.000000 0.0 0 0 0 0 1 0 0 1 10.0 1.0 5000.0 1.0 -70.0 15.000000 30.000000 0.0 0 0 1 0 2 0 0 2 10.0 1.0 5000.0 1.0 -70.0 25.000000 30.000000 0.0 0 0 2 0 3 0 0 3 10.0 1.0 5000.0 1.0 -70.0 35.000000 30.000000 0.0 0 0 3 0 4 0 1 0 10.0 1.0 5000.0 1.0 -70.0 44.850713 28.787322 0.0 0 1 4 0 5 0 1 1 10.0 1.0 5000.0 1.0 -70.0 54.552138 26.361966 0.0 0 1 5 0 6 0 1 2 10.0 1.0 5000.0 1.0 -70.0 64.253563 23.936609 0.0 0 1 6 0 7 0 1 3 10.0 1.0 5000.0 1.0 -70.0 73.954988 21.511253 0.0 0 1 7 0 8 0 2 0 10.0 1.0 5000.0 1.0 -70.0 44.850713 31.212678 0.0 0 2 8 0 9 0 2 1 10.0 1.0 5000.0 1.0 -70.0 54.552138 33.638034 0.0 0 2 9 0 10 0 2 2 10.0 1.0 5000.0 1.0 -70.0 64.253563 36.063391 0.0 0 2 10 0 11 0 2 3 10.0 1.0 5000.0 1.0 -70.0 73.954988 38.488747 0.0 0 2 11 0
net.shape\n
(2, 6, 24)\n

Note: In case you need even more flexibility in how you select parts of a Module, Jaxley provides a select method, to give full control over the exact parts of the nodes and edges that are part of a View. On examples of how this can be used, see the tutorial on advanced indexing.

You can also iterate over networks, cells, and branches:

# We set the radiuses to random values...\nradiuses = np.random.rand((24))\nnet.set(\"radius\", radiuses)\n\n# ...and then we set the length to 100.0 um if the radius is >0.5.\nfor cell in net:\n    for branch in cell:\n        for comp in branch:\n            if comp.nodes.iloc[0][\"radius\"] > 0.5:\n                comp.set(\"length\", 100.0)\n\n# Show the first five compartments:\nnet.nodes[[\"radius\", \"length\"]][:5]\n
radius length 0 0.763057 100.0 1 0.334882 10.0 2 0.805696 100.0 3 0.717921 100.0 4 0.079569 10.0

Finally, you can also use Views in a context manager:

with net.cell(0).branch(0) as branch0:\n    branch0.set(\"radius\", 2.0)\n    branch0.set(\"length\", 2.5)\n\n# Show the first five compartments.\nnet.nodes[[\"radius\", \"length\"]][:5]\n
radius length 0 2.000000 2.5 1 2.000000 2.5 2 2.000000 2.5 3 2.000000 2.5 4 0.079569 10.0"},{"location":"tutorial/00_jaxley_api/#channels","title":"Channels","text":"

The Modules that we have created above will not do anything interesting, since by default Jaxley initializes them without any mechanisms in the membrane. To change this, we have to insert channels into the membrane. For this purpose Jaxley implements Channels that can be inserted into any compartment using the insert method of a Module or a View:

# insert a Leak channel into all compartments in the Module.\nnet.insert(Leak())\nnet.nodes.head() # Channel parameters are now also added to `nodes`.\n
local_cell_index local_branch_index local_comp_index length radius axial_resistivity capacitance v global_cell_index global_branch_index global_comp_index controlled_by_param x y z Leak Leak_gLeak Leak_eLeak 0 0 0 0 2.5 2.000000 5000.0 1.0 -70.0 0 0 0 0 5.000000 30.000000 0.0 True 0.0001 -70.0 1 0 0 1 2.5 2.000000 5000.0 1.0 -70.0 0 0 1 0 15.000000 30.000000 0.0 True 0.0001 -70.0 2 0 0 2 2.5 2.000000 5000.0 1.0 -70.0 0 0 2 0 25.000000 30.000000 0.0 True 0.0001 -70.0 3 0 0 3 2.5 2.000000 5000.0 1.0 -70.0 0 0 3 0 35.000000 30.000000 0.0 True 0.0001 -70.0 4 0 1 0 10.0 0.079569 5000.0 1.0 -70.0 0 1 4 0 44.850713 28.787322 0.0 True 0.0001 -70.0

This is also were Views come in handy, as it allows to easily target the insertion of channels to specific compartments.

# inserting several channels into parts of the network\nwith net.cell(0) as cell0:\n    cell0.insert(Na())\n    cell0.insert(K())\n\n# # The above is equivalent to:\n# net.cell(0).insert(Na())\n# net.cell(0).insert(K())\n\n# K and Na channels were only insert into cell 0\nnet.cell(\"all\").branch(0).comp(0).nodes[[\"global_cell_index\", \"Na\", \"K\", \"Leak\"]]\n
global_cell_index Na K Leak 0 0 True True True 12 1 False False True"},{"location":"tutorial/00_jaxley_api/#synapses","title":"Synapses","text":"

To connect different cells together, Jaxley implements a connect method, that can be used to couple 2 compartments together using a Synapse. Synapses in Jaxley work only on the compartment level, that means to be able to connect two cells, you need to specify the exact compartments on a given cell to make the connections between. Below is an example of this:

# connecting two cells using a Synapse\npre_comp = cell0.branch(1).comp(0)\npost_comp = net.cell(1).branch(0).comp(0)\n\nconnect(pre_comp, post_comp, IonotropicSynapse())\n\nnet.edges\n
global_edge_index global_pre_comp_index global_post_comp_index type type_ind pre_locs post_locs IonotropicSynapse_gS IonotropicSynapse_e_syn IonotropicSynapse_k_minus IonotropicSynapse_s controlled_by_param 0 0 4 12 IonotropicSynapse 0 0.125 0.125 0.0001 0.0 0.025 0.2 0

As you can see above, now the edges dataframe is also updated with the information of the newly added synapse.

Congrats! You should now have an intuitive understand of how to use Jaxley\u2019s API to construct, navigate and manipulate neuron models.

"},{"location":"tutorial/01_morph_neurons/","title":"Basics of Jaxley","text":"

In this tutorial, you will learn how to:

  • build your first morphologically detailed cell or read it from SWC
  • stimulate the cell
  • record from the cell
  • visualize cells
  • run your first simulation

Here is a code snippet which you will learn to understand in this tutorial:

import jaxley as jx\nfrom jaxley.channels import Na, K, Leak\nimport matplotlib.pyplot as plt\n\n\n# Build the cell.\ncomp = jx.Compartment()\nbranch = jx.Branch(comp, ncomp=2)\ncell = jx.Cell(branch, parents=[-1, 0, 0, 1, 1])\n\n# Insert channels.\ncell.insert(Leak())\ncell.branch(0).insert(Na())\ncell.branch(0).insert(K())\n\n# Change parameters.\ncell.set(\"axial_resistivity\", 200.0)\n\n# Visualize the morphology.\ncell.compute_xyz()\nfig, ax = plt.subplots(1, 1, figsize=(4, 4))\ncell.vis(ax=ax)\n\n# Stimulate.\ncurrent = jx.step_current(i_delay=1.0, i_dur=1.0, i_amp=0.1, delta_t=0.025, t_max=10.0)\ncell.branch(0).loc(0.0).stimulate(current)\n\n# Record.\ncell.branch(0).loc(0.0).record(\"v\")\n\n# Simulate and plot.\nv = jx.integrate(cell, delta_t=0.025)\nplt.plot(v.T)\n

First, we import the relevant libraries:

from jax import config\nconfig.update(\"jax_enable_x64\", True)\nconfig.update(\"jax_platform_name\", \"cpu\")\n\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport jax.numpy as jnp\nfrom jax import jit\n\nimport jaxley as jx\nfrom jaxley.channels import Na, K, Leak\nfrom jaxley.synapses import IonotropicSynapse\nfrom jaxley.connect import fully_connect\n

We will now build our first cell in Jaxley. You have two options to do this: you can either build a cell bottom-up by defining the morphology yourselve, or you can load cells from SWC files.

"},{"location":"tutorial/01_morph_neurons/#define-the-cell-from-scratch","title":"Define the cell from scratch","text":"

To define a cell from scratch you first have to define a single compartment and then assemble those compartments into a branch:

comp = jx.Compartment()\nbranch = jx.Branch(comp, ncomp=2)\n

Next, we can assemble branches into a cell. To do so, we have to define for each branch what its parent branch is. A -1 entry means that this branch does not have a parent.

parents = jnp.asarray([-1, 0, 0, 1, 1])\ncell = jx.Cell(branch, parents=parents)\n

To learn more about Compartments, Branches, and Cells, see this tutorial.

"},{"location":"tutorial/01_morph_neurons/#read-the-cell-from-an-swc-file","title":"Read the cell from an SWC file","text":"

Alternatively, you could also load cells from SWC with

cell = jx.read_swc(fname, ncomp=4)

Details on handling SWC files can be found in this tutorial.

"},{"location":"tutorial/01_morph_neurons/#visualize-the-cells","title":"Visualize the cells","text":"

Cells can be visualized as follows:

cell.compute_xyz()  # Only needed for visualization.\n\nfig, ax = plt.subplots(1, 1, figsize=(4, 2))\n_ = cell.vis(ax=ax, col=\"k\")\n

"},{"location":"tutorial/01_morph_neurons/#insert-mechanisms","title":"Insert mechanisms","text":"

Currently, the cell does not contain any kind of ion channel (not even a leak). We can fix this by inserting a leak channel into the entire cell, and by inserting sodium and potassium into the zero-eth branch.

cell.insert(Leak())\ncell.branch(0).insert(Na())\ncell.branch(0).insert(K())\n

Once the cell is created, we can inspect its .nodes attribute which lists all properties of the cell:

cell.nodes\n
local_cell_index local_branch_index local_comp_index length radius axial_resistivity capacitance v global_cell_index global_branch_index ... Na Na_gNa eNa vt Na_m Na_h K K_gK eK K_n 0 0 0 0 10.0 1.0 5000.0 1.0 -70.0 0 0 ... True 0.05 50.0 -60.0 0.2 0.2 True 0.005 -90.0 0.2 1 0 0 1 10.0 1.0 5000.0 1.0 -70.0 0 0 ... True 0.05 50.0 -60.0 0.2 0.2 True 0.005 -90.0 0.2 2 0 1 0 10.0 1.0 5000.0 1.0 -70.0 0 1 ... False NaN NaN NaN NaN NaN False NaN NaN NaN 3 0 1 1 10.0 1.0 5000.0 1.0 -70.0 0 1 ... False NaN NaN NaN NaN NaN False NaN NaN NaN 4 0 2 0 10.0 1.0 5000.0 1.0 -70.0 0 2 ... False NaN NaN NaN NaN NaN False NaN NaN NaN 5 0 2 1 10.0 1.0 5000.0 1.0 -70.0 0 2 ... False NaN NaN NaN NaN NaN False NaN NaN NaN 6 0 3 0 10.0 1.0 5000.0 1.0 -70.0 0 3 ... False NaN NaN NaN NaN NaN False NaN NaN NaN 7 0 3 1 10.0 1.0 5000.0 1.0 -70.0 0 3 ... False NaN NaN NaN NaN NaN False NaN NaN NaN 8 0 4 0 10.0 1.0 5000.0 1.0 -70.0 0 4 ... False NaN NaN NaN NaN NaN False NaN NaN NaN 9 0 4 1 10.0 1.0 5000.0 1.0 -70.0 0 4 ... False NaN NaN NaN NaN NaN False NaN NaN NaN

10 rows \u00d7 25 columns

Note that Jaxley uses the same units as the NEURON simulator, which are listed here.

You can also inspect just parts of the cell, for example its 1st branch:

cell.branch(1).nodes\n
local_cell_index local_branch_index local_comp_index length radius axial_resistivity capacitance v Leak Leak_gLeak ... Na_m Na_h K K_gK eK K_n global_cell_index global_branch_index global_comp_index controlled_by_param 2 0 0 0 10.0 1.0 5000.0 1.0 -70.0 True 0.0001 ... NaN NaN False NaN NaN NaN 0 1 2 1 3 0 0 1 10.0 1.0 5000.0 1.0 -70.0 True 0.0001 ... NaN NaN False NaN NaN NaN 0 1 3 1

2 rows \u00d7 25 columns

The easiest way to know which branch is the 1st branch (or, e.g., the zero-eth compartment of the 1st branch) is to plot it in a different color:

fig, ax = plt.subplots(1, 1, figsize=(4, 2))\n_ = cell.vis(ax=ax, col=\"k\")\n_ = cell.branch(1).vis(ax=ax, col=\"r\")\n_ = cell.branch(1).comp(1).vis(ax=ax, col=\"b\")\n

More background and features on indexing as cell.branch(0) is in this tutorial.

"},{"location":"tutorial/01_morph_neurons/#change-parameters-of-the-cell","title":"Change parameters of the cell","text":"

You can change properties of the cell with the .set() method:

cell.branch(1).set(\"axial_resistivity\", 200.0)\n

And we can again inspect the .nodes to make sure that the axial resistivity indeed changed:

cell.branch(1).nodes\n
local_cell_index local_branch_index local_comp_index length radius axial_resistivity capacitance v Leak Leak_gLeak ... Na_m Na_h K K_gK eK K_n global_cell_index global_branch_index global_comp_index controlled_by_param 2 0 0 0 10.0 1.0 200.0 1.0 -70.0 True 0.0001 ... NaN NaN False NaN NaN NaN 0 1 2 1 3 0 0 1 10.0 1.0 200.0 1.0 -70.0 True 0.0001 ... NaN NaN False NaN NaN NaN 0 1 3 1

2 rows \u00d7 25 columns

In a similar way, you can modify channel properties or initial states (units are again here):

cell.branch(0).set(\"K_gK\", 0.01)  # modify potassium conductance.\ncell.set(\"v\", -65.0)  # modify initial voltage.\n
"},{"location":"tutorial/01_morph_neurons/#stimulate-the-cell","title":"Stimulate the cell","text":"

We next stimulate one of the compartments with a step current. For this, we first define the step current (units are again here):

dt = 0.025\nt_max = 10.0\ntime_vec = np.arange(0, t_max+dt, dt)\ncurrent = jx.step_current(i_delay=1.0, i_dur=2.0, i_amp=0.08, delta_t=dt, t_max=t_max)\n\nfig, ax = plt.subplots(1, 1, figsize=(4, 2))\n_ = plt.plot(time_vec, current)\n

We then stimulate one of the compartments of the cell with this step current:

cell.delete_stimuli()\ncell.branch(0).loc(0.0).stimulate(current)\n
Added 1 external_states. See `.externals` for details.\n
"},{"location":"tutorial/01_morph_neurons/#define-recordings","title":"Define recordings","text":"

Next, you have to define where to record the voltage. In this case, we will record the voltage at two locations:

cell.delete_recordings()\ncell.branch(0).loc(0.0).record(\"v\")\ncell.branch(3).loc(1.0).record(\"v\")\n
Added 1 recordings. See `.recordings` for details.\nAdded 1 recordings. See `.recordings` for details.\n

We can again visualize these locations to understand where we inserted recordings:

fig, ax = plt.subplots(1, 1, figsize=(4, 2))\n_ = cell.vis(ax=ax)\n_ = cell.branch(0).loc(0.0).vis(ax=ax, col=\"b\")\n_ = cell.branch(3).loc(1.0).vis(ax=ax, col=\"g\")\n

"},{"location":"tutorial/01_morph_neurons/#simulate-the-cell-response","title":"Simulate the cell response","text":"

Having set up the cell, inserted stimuli and recordings, we are now ready to run a simulation with jx.integrate:

voltages = jx.integrate(cell, delta_t=dt)\nprint(\"voltages.shape\", voltages.shape)\n
voltages.shape (2, 402)\n

The jx.integrate function returns an array of shape (num_recordings, num_timepoints). In our case, we inserted 2 recordings and we simulated for 10ms at a 0.025 time step, which leads to 402 time steps.

We can now visualize the voltage response:

fig, ax = plt.subplots(1, 1, figsize=(4, 2))\n_ = ax.plot(voltages[0], c=\"b\")\n_ = ax.plot(voltages[1], c=\"orange\")\n

At the location of the first recording (in blue) the cell spiked, whereas at the second recording, it did not. This makes sense because we only inserted sodium and potassium channels into the first branch, but not in the entire cell.

Congrats! You have just run your first morphologically detailed neuron simulation in Jaxley. We suggest to continue by learning how to build networks. If you are only interested in single cell simulations, you can directly jump to learning how to speed up simulations. If you want to simulate detailed morphologies from SWC files, checkout our tutorial on working with detailed morphologies.

"},{"location":"tutorial/02_small_network/","title":"Network simulations in Jaxley","text":"

In this tutorial, you will learn how to:

  • connect neurons into a network
  • visualize networks
  • use the .edges attribute to inspect and change synaptic parameters

Here is a code snippet which you will learn to understand in this tutorial:

import jaxley as jx\nfrom jaxley.synapses import IonotropicSynapse\nfrom jaxley.connect import connect\n\n\n# Define a network. `cell` is defined as in previous tutorial.\nnet = jx.Network([cell for _ in range(11)])\n\n# Define synapses.\nfully_connect(\n    net.cell(range(10)),\n    net.cell(10),\n    IonotropicSynapse(),\n)\n\n# Change synaptic parameters.\nnet.select(edges=[0, 1]).set(\"IonotropicSynapse_gS\", 0.1)  # nS\n\n# Visualize the network.\nnet.compute_xyz()\nfig, ax = plt.subplots(1, 1, figsize=(4, 4))\nnet.vis(ax=ax, detail=\"full\", layers=[10, 1])  # or `detail=\"point\"`.\n

In the previous tutorial, you learned how to build single cells with morphological detail, how to insert stimuli and recordings, and how to run a first simulation. In this tutorial, we will define networks of multiple cells and connect them with synapses. Let\u2019s get started:

from jax import config\nconfig.update(\"jax_enable_x64\", True)\nconfig.update(\"jax_platform_name\", \"cpu\")\n\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport jax.numpy as jnp\nfrom jax import jit\n\nimport jaxley as jx\nfrom jaxley.channels import Na, K, Leak\nfrom jaxley.synapses import IonotropicSynapse\nfrom jaxley.connect import fully_connect, connect\n
"},{"location":"tutorial/02_small_network/#define-the-network","title":"Define the network","text":"

First, we define a cell as you saw in the previous tutorial.

comp = jx.Compartment()\nbranch = jx.Branch(comp, ncomp=4)\ncell = jx.Cell(branch, parents=[-1, 0, 0, 1, 1, 2, 2])\n

We can assemble multiple cells into a network by using jx.Network, which takes a list of jx.Cells. Here, we assemble 11 cells into a network:

num_cells = 11\nnet = jx.Network([cell for _ in range(num_cells)])\n

At this point, we can already visualize this network:

net.compute_xyz()\nnet.rotate(180)\nfig, ax = plt.subplots(1, 1, figsize=(3, 6))\n_ = net.vis(ax=ax, detail=\"full\", layers=[10, 1], layer_kwargs={\"within_layer_offset\": 150, \"between_layer_offset\": 200})\n

Note: you can use move_to to have more control over the location of cells, e.g.: network.cell(i).move_to(x=0, y=200).

As you can see, the neurons are not connected yet. Let\u2019s fix this by connecting neurons with synapses. We will build a network consisting of two layers: 10 neurons in the input layer and 1 neuron in the output layer.

We can use Jaxley\u2019s fully_connect method to connect these layers:

pre = net.cell(range(10))\npost = net.cell(10)\nfully_connect(pre, post, IonotropicSynapse())\n

Let\u2019s visualize this again:

fig, ax = plt.subplots(1, 1, figsize=(3, 6))\n_ = net.vis(ax=ax, detail=\"full\", layers=[10, 1], layer_kwargs={\"within_layer_offset\": 150, \"between_layer_offset\": 200})\n

As you can see, the full_connect method inserted one synapse (in blue) from every neuron in the first layer to the output neuron. The fully_connect method builds this synapse from the zero-eth compartment and zero-eth branch of the presynaptic neuron onto a random branch of the postsynaptic neuron. If you want more control over the pre- and post-synaptic branches, you can use the connect method:

pre = net.cell(0).branch(5).loc(1.0)\npost = net.cell(10).branch(0).loc(0.0)\nconnect(pre, post, IonotropicSynapse())\n
fig, ax = plt.subplots(1, 1, figsize=(3, 6))\n_ = net.vis(ax=ax, detail=\"full\", layers=[10, 1], layer_kwargs={\"within_layer_offset\": 150, \"between_layer_offset\": 200})\n

"},{"location":"tutorial/02_small_network/#inspecting-and-changing-synaptic-parameters","title":"Inspecting and changing synaptic parameters","text":"

You can inspect synaptic parameters via the .edges attribute:

net.edges\n
global_edge_index global_pre_comp_index global_post_comp_index type type_ind pre_locs post_locs IonotropicSynapse_gS IonotropicSynapse_e_syn IonotropicSynapse_k_minus IonotropicSynapse_s controlled_by_param 0 0 0 286 IonotropicSynapse 0 0.125 0.625 0.0001 0.0 0.025 0.2 0 1 1 28 298 IonotropicSynapse 0 0.125 0.625 0.0001 0.0 0.025 0.2 0 2 2 56 286 IonotropicSynapse 0 0.125 0.625 0.0001 0.0 0.025 0.2 0 3 3 84 295 IonotropicSynapse 0 0.125 0.875 0.0001 0.0 0.025 0.2 0 4 4 112 302 IonotropicSynapse 0 0.125 0.625 0.0001 0.0 0.025 0.2 0 5 5 140 288 IonotropicSynapse 0 0.125 0.125 0.0001 0.0 0.025 0.2 0 6 6 168 287 IonotropicSynapse 0 0.125 0.875 0.0001 0.0 0.025 0.2 0 7 7 196 305 IonotropicSynapse 0 0.125 0.375 0.0001 0.0 0.025 0.2 0 8 8 224 299 IonotropicSynapse 0 0.125 0.875 0.0001 0.0 0.025 0.2 0 9 9 252 284 IonotropicSynapse 0 0.125 0.125 0.0001 0.0 0.025 0.2 0 10 10 23 280 IonotropicSynapse 0 0.875 0.125 0.0001 0.0 0.025 0.2 0

To modify a parameter of all synapses you can again use .set():

net.set(\"IonotropicSynapse_gS\", 0.0003)  # nS\n

To modify individual syanptic parameters, use the .select() method. Below, we change the values of the first two synapses:

net.select(edges=[0, 1]).set(\"IonotropicSynapse_gS\", 0.0004)  # nS\n

For more details on how to flexibly set synaptic parameters (e.g., by cell type, or by pre-synaptic cell index,\u2026), see this tutorial.

"},{"location":"tutorial/02_small_network/#stimulating-recording-and-simulating-the-network","title":"Stimulating, recording, and simulating the network","text":"

We will now set up a simulation of the network. This works exactly as it does for single neurons:

# Stimulus.\ni_delay = 3.0  # ms\ni_amp = 0.05  # nA\ni_dur = 2.0  # ms\n\n# Duration and step size.\ndt = 0.025  # ms\nt_max = 50.0  # ms\n
time_vec = jnp.arange(0.0, t_max + dt, dt)\n

As a simple example, we insert sodium, potassium, and leak into every compartment of every cell of the network.

net.insert(Na())\nnet.insert(K())\nnet.insert(Leak())\n

We stimulate every neuron in the input layer and record the voltage from the output neuron:

current = jx.step_current(i_delay, i_dur, i_amp, dt, t_max)\nnet.delete_stimuli()\nfor stim_ind in range(10):\n    net.cell(stim_ind).branch(0).loc(0.0).stimulate(current)\n\nnet.delete_recordings()\nnet.cell(10).branch(0).loc(0.0).record()\n
Added 1 external_states. See `.externals` for details.\nAdded 1 external_states. See `.externals` for details.\nAdded 1 external_states. See `.externals` for details.\nAdded 1 external_states. See `.externals` for details.\nAdded 1 external_states. See `.externals` for details.\nAdded 1 external_states. See `.externals` for details.\nAdded 1 external_states. See `.externals` for details.\nAdded 1 external_states. See `.externals` for details.\nAdded 1 external_states. See `.externals` for details.\nAdded 1 external_states. See `.externals` for details.\nAdded 1 recordings. See `.recordings` for details.\n

Finally, we can again run the network simulation and plot the result:

s = jx.integrate(net, delta_t=dt)\n
fig, ax = plt.subplots(1, 1, figsize=(4, 2))\n_ = ax.plot(s.T)\n

That\u2019s it! You now know how to simulate networks of morphologically detailed neurons. We recommend that you now have a look at how you can speed up your simulation. To learn more about handling synaptic parameters, we recommend to check out this tutorial.

"},{"location":"tutorial/04_jit_and_vmap/","title":"Speeding up simulations","text":"

In this tutorial, you will learn how to:

  • make parameter sweeps in Jaxley
  • use jit to compile your simulations and make them faster
  • use vmap to parallelize simulations on GPUs

Here is a code snippet which you will learn to understand in this tutorial:

from jax import jit, vmap\n\n\ncell = ...  # See tutorial on Basics of Jaxley.\n\ndef simulate(params):\n    param_state = None\n    param_state = cell.data_set(\"Na_gNa\", params[0], param_state)\n    param_state = cell.data_set(\"K_gK\", params[1], param_state)\n    return jx.integrate(cell, param_state=param_state, delta_t=0.025)\n\n# Define 100 sets of sodium and potassium conductances.\nall_params = jnp.asarray(np.random.rand(100, 2))\n\n# Fast for-loops with jit compilation.\njitted_simulate = jit(simulate)\nvoltages = [jitted_simulate(params) for params in all_params]\n\n# Using vmap for parallelization.\nvmapped_simulate = vmap(jitted_simulate, in_axes=(0,))\nvoltages = vmapped_simulate(all_params)\n

In the previous tutorials, you learned how to build single cells or networks and how to change their parameters. In this tutorial, you will learn how to speed up such simulations by many orders of magnitude. This can be achieved in to ways:

  • by using JIT compilation
  • by using GPU parallelization

Let\u2019s get started!

"},{"location":"tutorial/04_jit_and_vmap/#using-gpu-or-cpu","title":"Using GPU or CPU","text":"

In Jaxley you can set whether you want to use gpu or cpu with the following lines at the beginning of your script:

from jax import config\nconfig.update(\"jax_platform_name\", \"cpu\")\n

JAX (and Jaxley) also allow to choose between float32 and float64. Especially on GPUs, float32 will be faster, but we have experienced stability issues when simulating morphologically detailed neurons with float32.

config.update(\"jax_enable_x64\", True)  # Set to false to use `float32`.\n

Next, we will import relevant libraries:

import matplotlib.pyplot as plt\nimport numpy as np\nimport jax.numpy as jnp\nfrom jax import jit, vmap\n\nimport jaxley as jx\nfrom jaxley.channels import Na, K, Leak\n
"},{"location":"tutorial/04_jit_and_vmap/#building-the-cell-or-network","title":"Building the cell or network","text":"

We first build a cell (or network) in the same way as we showed in the previous tutorials:

dt = 0.025\nt_max = 10.0\n\ncomp = jx.Compartment()\nbranch = jx.Branch(comp, ncomp=4)\ncell = jx.Cell(branch, parents=[-1, 0, 0, 1, 1, 2, 2])\n\ncell.insert(Na())\ncell.insert(K())\ncell.insert(Leak())\n\ncell.delete_stimuli()\ncurrent = jx.step_current(i_delay=1.0, i_dur=1.0, i_amp=0.1, delta_t=dt, t_max=t_max)\ncell.branch(0).loc(0.0).stimulate(current)\n\ncell.delete_recordings()\ncell.branch(0).loc(0.0).record()\n
Added 1 external_states. See `.externals` for details.\nAdded 1 recordings. See `.recordings` for details.\n
"},{"location":"tutorial/04_jit_and_vmap/#parameter-sweeps","title":"Parameter sweeps","text":"

Assume you want to run the same cell with many different values for the sodium and potassium conductance, for example for genetic algorithms or for parameter sweeps. To do this efficiently in Jaxley, you have to use the data_set() method (in combination with jit and vmap, as shown later):

def simulate(params):\n    param_state = None\n    param_state = cell.data_set(\"Na_gNa\", params[0], param_state)\n    param_state = cell.data_set(\"K_gK\", params[1], param_state)\n    return jx.integrate(cell, param_state=param_state, delta_t=dt)\n

The .data_set() method takes three arguments:

1) the name of the parameter you want to set. Jaxley allows to set the following parameters: \u201cradius\u201d, \u201clength\u201d, \u201caxial_resistivity\u201d, as well as all parameters of channels and synapses. 2) the value of the parameter. 3) a param_state which is initialized as None and is modified by .data_set(). This has to be passed to jx.integrate().

Having done this, the simplest (but least efficient) way to perform the parameter sweep is to run a for-loop over many parameter sets:

# Define 5 sets of sodium and potassium conductances.\nall_params = jnp.asarray(np.random.rand(5, 2))\n\nvoltages = jnp.asarray([simulate(params) for params in all_params])\nprint(\"voltages.shape\", voltages.shape)\n
voltages.shape (5, 1, 402)\n

The resulting voltages have shape (num_simulations, num_recordings, num_timesteps).

"},{"location":"tutorial/04_jit_and_vmap/#stimulus-sweeps","title":"Stimulus sweeps","text":"

In addition to running sweeps across multiple parameters, you can also run sweeeps across multiple stimuli (e.g. step current stimuli of different amplitudes. You can achieve this with the data_stimulate() method:

def simulate(i_amp):\n    current = jx.step_current(1.0, 1.0, i_amp, 0.025, 10.0)\n\n    data_stimuli = None\n    data_stimuli = cell.branch(0).comp(0).data_stimulate(current, data_stimuli)\n    return jx.integrate(cell, data_stimuli=data_stimuli)\n

"},{"location":"tutorial/04_jit_and_vmap/#speeding-up-for-loops-via-jit-compilation","title":"Speeding up for loops via jit compilation","text":"

We can speed up such parameter sweeps (or stimulus sweeps) with jit compilation. jit compilation will compile the simulation when it is run for the first time, such that every other simulation will be must faster. This can be achieved by defining a new function which uses JAX\u2019s jit():

jitted_simulate = jit(simulate)\n
# First run, will be slow.\nvoltages = jitted_simulate(all_params[0])\n
# More runs, will be much faster.\nvoltages = jnp.asarray([jitted_simulate(params) for params in all_params])\nprint(\"voltages.shape\", voltages.shape)\n
voltages.shape (5, 1, 402)\n

jit compilation can be up to 10k times faster, especially for small simulations with few compartments. For very large models, the gain obtained with jit will be much smaller (jit may even provide no speed up at all).

"},{"location":"tutorial/04_jit_and_vmap/#speeding-up-with-gpu-parallelization-via-vmap","title":"Speeding up with GPU parallelization via vmap","text":"

Another way to speed up parameter sweeps is with GPU parallelization. Parallelization in Jaxley can be achieved by using vmap of JAX. To do this, we first create a new function that handles multiple parameter sets directly:

# Using vmap for parallelization.\nvmapped_simulate = vmap(jitted_simulate)\n

We can then run this method on all parameter sets (all_params.shape == (100, 2)), and Jaxley will automatically parallelize across them. Of course, you will only get a speed-up if you have a GPU available and you specified gpu as device in the beginning of this tutorial.

voltages = vmapped_simulate(all_params)\n

GPU parallelization with vmap can give a large speed-up, which can easily be 2-3 orders of magnitude.

"},{"location":"tutorial/04_jit_and_vmap/#combining-jit-and-vmap","title":"Combining jit and vmap","text":"

Finally, you can also combine using jit and vmap. For example, you can run multiple batches of many parallel simulations. Each batch can be parallelized with vmap and simulating each batch can be compiled with jit:

jitted_vmapped_simulate = jit(vmap(simulate))\n
for batch in range(10):\n    all_params = jnp.asarray(np.random.rand(5, 2))\n    voltages_batch = jitted_vmapped_simulate(all_params)\n

That\u2019s all you have to know about jit and vmap! If you have worked through this and the previous tutorials, you should be ready to set up your first network simulations.

"},{"location":"tutorial/04_jit_and_vmap/#next-steps","title":"Next steps","text":"

If you want to learn more, we recommend you to read the tutorial on building channel and synapse models.

Alternatively, you can also directly jump ahead to the tutorial on training biophysical networks which will teach you how you can optimize parameters of biophysical models with gradient descent.

Finally, if you want to learn more about JAX, check out their tutorial on jit or their tutorial on vmap.

"},{"location":"tutorial/05_channel_and_synapse_models/","title":"Building ion channel models","text":"

In this tutorial, you will learn how to:

  • define your own ion channel models beyond the preconfigured channels in Jaxley

This tutorial assumes that you have already learned how to build basic simulations.

from jax import config\nconfig.update(\"jax_enable_x64\", True)\nconfig.update(\"jax_platform_name\", \"cpu\")\n\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport jax\nimport jax.numpy as jnp\nfrom jax import jit, value_and_grad\n\nimport jaxley as jx\n

First, we define a cell as you saw in the previous tutorial:

comp = jx.Compartment()\nbranch = jx.Branch(comp, ncomp=4)\ncell = jx.Cell(branch, parents=[-1, 0, 0, 1, 1, 2, 2])\n

You have also already learned how to insert preconfigured channels into Jaxley models:

cell.insert(Na())\ncell.insert(K())\ncell.insert(Leak())\n

In this tutorial, we will show you how to build your own channel and synapse models.

"},{"location":"tutorial/05_channel_and_synapse_models/#your-own-channel","title":"Your own channel","text":"

Below is how you can define your own channel. We will go into detail about individual parts of the code in the next couple of cells.

import jax.numpy as jnp\nfrom jaxley.channels import Channel\nfrom jaxley.solver_gate import solve_gate_exponential\n\n\ndef exp_update_alpha(x, y):\n    return x / (jnp.exp(x / y) - 1.0)\n\nclass Potassium(Channel):\n    \"\"\"Potassium channel.\"\"\"\n\n    def __init__(self, name = None):\n        self.current_is_in_mA_per_cm2 = True\n        super().__init__(name)\n        self.channel_params = {\"gK_new\": 1e-4}\n        self.channel_states = {\"n_new\": 0.0}\n        self.current_name = \"i_K\"\n\n    def update_states(self, states, dt, v, params):\n        \"\"\"Update state.\"\"\"\n        ns = states[\"n_new\"]\n        alpha = 0.01 * exp_update_alpha(-(v + 55), 10)\n        beta = 0.125 * jnp.exp(-(v + 65) / 80)\n        new_n = solve_gate_exponential(ns, dt, alpha, beta)\n        return {\"n_new\": new_n}\n\n    def compute_current(self, states, v, params):\n        \"\"\"Return current.\"\"\"\n        ns = states[\"n_new\"]\n        kd_conds = params[\"gK_new\"] * ns**4  # S/cm^2\n\n        e_kd = -77.0        \n        return kd_conds * (v - e_kd)\n\n    def init_state(self, states, v, params, delta_t):\n        alpha = 0.01 * exp_update_alpha(-(v + 55), 10)\n        beta = 0.125 * jnp.exp(-(v + 65) / 80)\n        return {\"n_new\": alpha / (alpha + beta)}\n

Let\u2019s look at each part of this in detail.

The below is simply a helper function for the solver of the gate variables:

def exp_update_alpha(x, y):\n    return x / (jnp.exp(x / y) - 1.0)\n

Next, we define our channel as a class. It should inherit from the Channel class and define channel_params, channel_states, and current_name. You also need to set self.current_is_in_mA_per_cm2=True as the first line on your __init__() method. This is to acknowledge that your current is returned in mA/cm2 (not in uA/cm2, as would have been required in Jaxley versions 0.4.0 or older).

class Potassium(Channel):\n    \"\"\"Potassium channel.\"\"\"\n\n    def __init__(self, name=None):\n        self.current_is_in_mA_per_cm2 = True\n        super().__init__(name)\n        self.channel_params = {\"gK_new\": 1e-4}\n        self.channel_states = {\"n_new\": 0.0}\n        self.current_name = \"i_K\"\n

Next, we have the update_states() method, which updates the gating variables:

    def update_states(self, states, dt, v, params):\n

Every channel you define must have an update_states() method which takes exactly these five arguments (self, states, dt, v, params). The inputs states to the update_states method is a dictionary which contains all states that are updated (including states of other channels). v is a jnp.ndarray which contains the voltage of a single compartment (shape ()). Let\u2019s get the state of the potassium channel which we are building here:

ns = states[\"n_new\"]\n

Next, we update the state of the channel. In this example, we do this with exponential Euler, but you can implement any solver yourself:

alpha = 0.01 * exp_update_alpha(-(v + 55), 10)\nbeta = 0.125 * jnp.exp(-(v + 65) / 80)\nnew_n = solve_gate_exponential(ns, dt, alpha, beta)\nreturn {\"n_new\": new_n}\n

A channel also needs a compute_current() method which returns the current throught the channel:

    def compute_current(self, states, v, params):\n        ns = states[\"n_new\"]\n\n        # Multiply with 1000 to convert Siemens to milli Siemens.\n        kd_conds = params[\"gK_new\"] * ns**4  # S/cm^2\n\n        e_kd = -77.0        \n        current = kd_conds * (v - e_kd)\n        return current\n

Finally, the init_state() method can be implemented optionally. It can be used to automatically compute the initial state based on the voltage when cell.init_states() is run.

Alright, done! We can now insert this channel into any jx.Module such as our cell:

cell.insert(Potassium())\n
cell.delete_stimuli()\ncurrent = jx.step_current(1.0, 1.0, 0.1, 0.025, 10.0)\ncell.branch(0).comp(0).stimulate(current)\n\ncell.delete_recordings()\ncell.branch(0).comp(0).record()\n
Added 1 external_states. See `.externals` for details.\nAdded 1 recordings. See `.recordings` for details.\n
s = jx.integrate(cell)\n
fig, ax = plt.subplots(1, 1, figsize=(4, 2))\n_ = ax.plot(s.T[:-1])\n_ = ax.set_ylim([-80, 50])\n_ = ax.set_xlabel(\"Time (ms)\")\n_ = ax.set_ylabel(\"Voltage (mV)\")\n

"},{"location":"tutorial/05_channel_and_synapse_models/#your-own-synapse","title":"Your own synapse","text":"

The parts below assume that you have already learned how to build network simulations in Jaxley.

Note that again, a synapse needs to have the two functions update_states and compute_current with all input arguments shown below.

The below is an example of how to define your own synapse model in Jaxley:

import jax.numpy as jnp\nfrom jaxley.synapses.synapse import Synapse\n\n\nclass TestSynapse(Synapse):\n    \"\"\"\n    Compute syanptic current and update syanpse state.\n    \"\"\"\n    def __init__(self, name = None):\n        super().__init__(name)\n        self.synapse_params = {\"gChol\": 0.001, \"eChol\": 0.0}\n        self.synapse_states = {\"s_chol\": 0.1}\n\n    def update_states(self, states, delta_t, pre_voltage, post_voltage, params):\n        \"\"\"Return updated synapse state and current.\"\"\"\n        s_inf = 1.0 / (1.0 + jnp.exp((-35.0 - pre_voltage) / 10.0))\n        exp_term = jnp.exp(-delta_t)\n        new_s = states[\"s_chol\"] * exp_term + s_inf * (1.0 - exp_term)\n        return {\"s_chol\": new_s}\n\n    def compute_current(self, states, pre_voltage, post_voltage, params):\n        g_syn = params[\"gChol\"] * states[\"s_chol\"]\n        return g_syn * (post_voltage - params[\"eChol\"])\n

As you can see above, synapses follow closely how channels are defined. The main difference is that the compute_current method takes two voltages: the pre-synaptic voltage (a jnp.ndarray of shape ()) and the post-synaptic voltage (a jnp.ndarray of shape ()).

net = jx.Network([cell for _ in range(3)])\n
from jaxley.connect import connect\n\npre = net.cell(0).branch(0).loc(0.0)\npost = net.cell(1).branch(0).loc(0.0)\nconnect(pre, post, TestSynapse())\n
net.cell(0).branch(0).loc(0.0).stimulate(jx.step_current(1.0, 2.0, 0.1, 0.025, 10.0))\nfor i in range(3):\n    net.cell(i).branch(0).loc(0.0).record()\n
Added 1 external_states. See `.externals` for details.\nAdded 1 recordings. See `.recordings` for details.\nAdded 1 recordings. See `.recordings` for details.\nAdded 1 recordings. See `.recordings` for details.\n
s = jx.integrate(net)\n
fig, ax = plt.subplots(1, 1, figsize=(4, 2))\n_ = ax.plot(s.T[:-1])\n_ = ax.set_ylim([-80, 50])\n_ = ax.set_xlabel(\"Time (ms)\")\n_ = ax.set_ylabel(\"Voltage (mV)\")\n

That\u2019s it! You are now ready to build your own custom simulations and equip them with channel and synapse models!

This tutorial does not have an immediate follow-up tutorial. If you have not done so already, you can check out our tutorial on training biophysical networks which will teach you how you can optimize parameters of biophysical models with gradient descent.

"},{"location":"tutorial/06_groups/","title":"Defining groups","text":"

In this tutorial, you will learn how to:

  • define groups (aka sectionlists) to simplify iteractions with Jaxley

Here is a code snippet which you will learn to understand in this tutorial:

from jax import jit, vmap\n\n\nnet = ...  # See tutorial on Basics of Jaxley.\n\nnet.cell(0).add_to_group(\"fast_spiking\")\nnet.cell(1).add_to_group(\"slow_spiking\")\n\ndef simulate(params):\n    param_state = None\n    param_state = net.fast_spiking.data_set(\"HH_gNa\", params[0], param_state)\n    param_state = net.slow_spiking.data_set(\"HH_gNa\", params[1], param_state)\n    return jx.integrate(net, param_state=param_state)\n\n# Define sodium for fast and slow spiking neurons.\nparams = jnp.asarray([1.0, 0.1])\n\n# Run simulation.\nvoltages = simulate(params)\n

In many cases, you might want to group several compartments (or branches, or cells) and assign a unique parameter or mechanism to this group. For example, you might want to define a couple of branches as basal and then assign a Hodgkin-Huxley mechanism only to those branches. Or you might define a couple of cells as fast spiking and assign them a high value for the sodium conductance. We describe how you can do this in this tutorial.

from jax import config\nconfig.update(\"jax_enable_x64\", True)\nconfig.update(\"jax_platform_name\", \"cpu\")\n\nimport time\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport jax\nimport jax.numpy as jnp\nfrom jax import jit, value_and_grad\n\nimport jaxley as jx\nfrom jaxley.channels import Na, K, Leak\nfrom jaxley.synapses import IonotropicSynapse\nfrom jaxley.connect import fully_connect\n

First, we define a network as you saw in the previous tutorial:

comp = jx.Compartment()\nbranch = jx.Branch(comp, ncomp=2)\ncell = jx.Cell(branch, parents=[-1, 0, 0, 1])\nnetwork = jx.Network([cell for _ in range(3)])\n\npre = network.cell([0, 1])\npost = network.cell([2])\nfully_connect(pre, post, IonotropicSynapse())\n\nnetwork.insert(Na())\nnetwork.insert(K())\nnetwork.insert(Leak())\n
"},{"location":"tutorial/06_groups/#group-apical-dendrites","title":"Group: apical dendrites","text":"

Assume that, in each of the five neurons in this network, the second and forth branch are apical dendrites. We can define this as:

for cell_ind in range(3):\n    network.cell(cell_ind).branch(1).add_to_group(\"apical\")\n    network.cell(cell_ind).branch(3).add_to_group(\"apical\")\n

After this, we can access network.apical as we previously accesses anything else:

network.apical.set(\"radius\", 0.3)\n
network.apical.view\n
View with 3 different channels. Use `.nodes` for details.\n
"},{"location":"tutorial/06_groups/#group-fast-spiking","title":"Group: fast spiking","text":"

Similarly, you could define a group of fast-spiking cells. Assume that the first and second cell are fast-spiking:

network.cell(0).add_to_group(\"fast_spiking\")\nnetwork.cell(1).add_to_group(\"fast_spiking\")\n
network.fast_spiking.set(\"Na_gNa\", 0.4)\n
network.fast_spiking.view\n
View with 3 different channels. Use `.nodes` for details.\n
"},{"location":"tutorial/06_groups/#groups-from-swc-files","title":"Groups from SWC files","text":"

If you are reading .swc morphologigies, you can automatically assign groups with

jx.read_swc(file_name, nseg=n, assign_groups=True).\n
After that, you can directly use cell.soma, cell.apical, cell.basal, or cell.axon.

"},{"location":"tutorial/06_groups/#how-groups-are-interpreted-by-make_trainable","title":"How groups are interpreted by .make_trainable()","text":"

If you make a parameter of a group trainable, then it will be treated as a single shared parameter for a given property:

network.fast_spiking.make_trainable(\"Na_gNa\")\n
Number of newly added trainable parameters: 1. Total number of trainable parameters: 1\n

As such, get_parameters() returns only a single trainable parameter, which will be the sodium conductance for every compartment of every fast-spiking neuron:

network.get_parameters()\n
[{'Na_gNa': Array([0.4], dtype=float64)}]\n

If, instead, you would want a separate parameter for every fast-spiking cell, you should not use the group, but instead do the following (remember that fast-spiking neurons had indices [0,1]):

network.cell([0,1]).make_trainable(\"axial_resistivity\")\n
Number of newly added trainable parameters: 2. Total number of trainable parameters: 3\n
network.get_parameters()\n
[{'Na_gNa': Array([0.4], dtype=float64)},\n {'axial_resistivity': Array([5000., 5000.], dtype=float64)}]\n

This generated two parameters for the axial resistivitiy, each corresponding to one cell.

"},{"location":"tutorial/06_groups/#summary","title":"Summary","text":"

Groups allow you to organize your simulation in a more intuitive way, and they allow to perform parameter sharing with make_trainable().

"},{"location":"tutorial/07_gradient_descent/","title":"Training biophysical models","text":"

In this tutorial, you will learn how to train biophysical models in Jaxley. This includes the following:

  • compute the gradient with respect to parameters
  • use parameter transformations
  • use multi-level checkpointing
  • define optimizers
  • write dataloaders and parallelize across data

Here is a code snippet which you will learn to understand in this tutorial:

from jax import jit, vmap, value_and_grad\nimport jaxley as jx\nimport jaxley.optimize.transforms as jt\n\nnet = ...  # See tutorial on the basics of `Jaxley`.\n\n# Define which parameters to train.\nnet.cell(\"all\").make_trainable(\"HH_gNa\")\nnet.IonotropicSynapse.make_trainable(\"IonotropicSynapse_gS\")\nparameters = net.get_parameters()\n\n# Define parameter transform and apply it to the parameters.\ntransform = jx.ParamTransform([\n    {\"IonotropicSynapse_gS\": jt.SigmoidTransform(0.0, 1.0)},\n    {\"HH_gNa\":jt.SigmoidTransform(0.0, 1, 0)}\n])\n\nopt_params = transform.inverse(parameters)\n\n# Define simulation and batch it across stimuli.\ndef simulate(params, datapoint):\n    current = jx.datapoint_to_step_currents(i_delay=1.0, i_dur=1.0, i_amps=datapoint, dt=0.025, t_max=5.0)\n    data_stimuli = net.cell(0).branch(0).comp(0).data_stimulate(current, None)\n    return jx.integrate(net, params=params, data_stimuli=data_stimuli, checkpoint_inds=[20, 20], delta_t=0.025)\n\nbatch_simulate = vmap(simulate, in_axes=(None, 0))\n\n# Define loss function and its gradient.\ndef loss_fn(opt_params, datapoints, label):\n    params = transform.forward(opt_params)\n    voltages = batch_simulate(params, datapoints)\n    return jnp.abs(jnp.mean(voltages) - label)\n\ngrad_fn = jit(value_and_grad(loss_fn, argnums=0))\n\n# Define data and dataloader.\ndata = jnp.asarray(np.random.randn(100, 3))\ndataloader = Dataset.from_tensor_slices((inputs, labels))\ndataloader = dataloader.shuffle(dataloader.cardinality()).batch(4)\n\n# Define the optimizer.\noptimizer = optax.Adam(lr=0.01)\nopt_state = optimizer.init_state(opt_params)\n\nfor epoch in range(10):\n    for batch in dataloader:\n        stimuli = batch[0].numpy()\n        labels = batch[1].numpy()\n        loss, gradient = grad_fn(opt_params, stimuli, labels)\n\n        # Optimizer step.\n        updates, opt_state = optimizer.update(gradient, opt_state)\n        opt_params = optax.apply_updates(opt_params, updates)\n

from jax import config\nconfig.update(\"jax_enable_x64\", True)\nconfig.update(\"jax_platform_name\", \"cpu\")\n\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport jax\nimport jax.numpy as jnp\nfrom jax import jit, vmap, value_and_grad\n\nimport jaxley as jx\nfrom jaxley.channels import Leak\nfrom jaxley.synapses import TanhRateSynapse\nfrom jaxley.connect import fully_connect\n

First, we define a network as you saw in the previous tutorial:

_ = np.random.seed(0)  # For synaptic locations.\n\ncomp = jx.Compartment()\nbranch = jx.Branch(comp, ncomp=2)\ncell = jx.Cell(branch, parents=[-1, 0, 0])\nnet = jx.Network([cell for _ in range(3)])\n\npre = net.cell([0, 1])\npost = net.cell([2])\nfully_connect(pre, post, TanhRateSynapse())\n\n# Change some default values of the tanh synapse.\nnet.TanhRateSynapse.set(\"TanhRateSynapse_x_offset\", -60.0)\nnet.TanhRateSynapse.set(\"TanhRateSynapse_gS\", 1e-3)\nnet.TanhRateSynapse.set(\"TanhRateSynapse_slope\", 0.1)\n\nnet.insert(Leak())\n

This network consists of three neurons arranged in two layers:

net.compute_xyz()\nnet.rotate(180)\nfig, ax = plt.subplots(1, 1, figsize=(3, 2))\n_ = net.vis(ax=ax, detail=\"full\", layers=[2, 1], layer_kwargs={\"within_layer_offset\": 100.0, \"between_layer_offset\": 100.0}) \n

We consider the last neuron as the output neuron and record the voltage from there:

net.delete_recordings()\nnet.cell(0).branch(0).loc(0.0).record()\nnet.cell(1).branch(0).loc(0.0).record()\nnet.cell(2).branch(0).loc(0.0).record()\n
Added 1 recordings. See `.recordings` for details.\nAdded 1 recordings. See `.recordings` for details.\nAdded 1 recordings. See `.recordings` for details.\n
"},{"location":"tutorial/07_gradient_descent/#defining-a-dataset","title":"Defining a dataset","text":"

We will train this biophysical network on a classification task. The inputs will be values and the label is binary:

inputs = jnp.asarray(np.random.rand(100, 2))\nlabels = jnp.asarray((inputs[:, 0] + inputs[:, 1]) > 1.0)\n
fig, ax = plt.subplots(1, 1, figsize=(3, 2))\n_ = ax.scatter(inputs[labels, 0], inputs[labels, 1])\n_ = ax.scatter(inputs[~labels, 0], inputs[~labels, 1])\n

labels = labels.astype(float)\n
"},{"location":"tutorial/07_gradient_descent/#defining-trainable-parameters","title":"Defining trainable parameters","text":"
net.delete_trainables()\n

This follows the same API as .set() seen in the previous tutorial. If you want to use a single parameter for all radiuses in the entire network, do:

net.make_trainable(\"radius\")\n
Number of newly added trainable parameters: 1. Total number of trainable parameters: 1\n

We can also define parameters for individual compartments. To do this, use the \"all\" key. The following defines a separate parameter the sodium conductance for every compartment in the entire network:

net.cell(\"all\").branch(\"all\").loc(\"all\").make_trainable(\"Leak_gLeak\")\n
Number of newly added trainable parameters: 18. Total number of trainable parameters: 19\n
"},{"location":"tutorial/07_gradient_descent/#making-synaptic-parameters-trainable","title":"Making synaptic parameters trainable","text":"

Synaptic parameters can be made trainable in the exact same way. To use a single parameter for all syanptic conductances in the entire network, do

net.TanhRateSynapse.make_trainable(\"TanhRateSynapse_gS\")\n

Here, we use a different syanptic conductance for all syanpses. This can be done as follows:

net.TanhRateSynapse.edge(\"all\").make_trainable(\"TanhRateSynapse_gS\")\n
Number of newly added trainable parameters: 2. Total number of trainable parameters: 21\n
"},{"location":"tutorial/07_gradient_descent/#running-the-simulation","title":"Running the simulation","text":"

Once all parameters are defined, you have to use .get_parameters() to obtain all trainable parameters. This is also the time to check how many trainable parameters your network has:

params = net.get_parameters()\n

You can now run the simulation with the trainable parameters by passing them to the jx.integrate function.

s = jx.integrate(net, params=params, t_max=10.0)\n
"},{"location":"tutorial/07_gradient_descent/#stimulating-the-network","title":"Stimulating the network","text":"

The network above does not yet get any stimuli. We will use the 2D inputs from the dataset to stimulate the two input neurons. The amplitude of the step current corresponds to the input value. Below is the simulator that defines this:

def simulate(params, inputs):\n    currents = jx.datapoint_to_step_currents(i_delay=1.0, i_dur=1.0, i_amp=inputs / 10, delta_t=0.025, t_max=10.0)\n\n    data_stimuli = None\n    data_stimuli = net.cell(0).branch(2).loc(1.0).data_stimulate(currents[0], data_stimuli=data_stimuli)\n    data_stimuli = net.cell(1).branch(2).loc(1.0).data_stimulate(currents[1], data_stimuli=data_stimuli)\n\n    return jx.integrate(net, params=params, data_stimuli=data_stimuli, delta_t=0.025)\n\nbatched_simulate = vmap(simulate, in_axes=(None, 0))\n

We can also inspect some traces:

traces = batched_simulate(params, inputs[:4])\n
fig, ax = plt.subplots(1, 1, figsize=(4, 2))\n_ = ax.plot(traces[:, 2, :].T)\n

"},{"location":"tutorial/07_gradient_descent/#defining-a-loss-function","title":"Defining a loss function","text":"

Let us define a loss function to be optimized:

def loss(params, inputs, labels):\n    traces = batched_simulate(params, inputs)  # Shape `(batchsize, num_recordings, timepoints)`.\n    prediction = jnp.mean(traces[:, 2], axis=1)  # Use the average over time of the output neuron (2) as prediction.\n    prediction = (prediction + 72.0) / 5  # Such that the prediction is roughly in [0, 1].\n    losses = jnp.abs(prediction - labels)  # Mean absolute error loss.\n    return jnp.mean(losses)  # Average across the batch.\n

And we can use JAX\u2019s inbuilt functions to take the gradient through the entire ODE:

jitted_grad = jit(value_and_grad(loss, argnums=0))\n
value, gradient = jitted_grad(params, inputs[:4], labels[:4])\n
"},{"location":"tutorial/07_gradient_descent/#defining-parameter-transformations","title":"Defining parameter transformations","text":"

Before training, however, we will enforce for all parameters to be within a prespecified range (such that, e.g., conductances can not become negative)

import jaxley.optimize.transforms as jt\n
# Define a function to create appropriate transforms for each parameter\ndef create_transform(name):\n    if name == \"axial_resistivity\":\n        # Must be positive; apply Softplus and scale to match initialization\n        return jt.ChainTransform([jt.SoftplusTransform(0), jt.AffineTransform(5000, 0)])\n    elif name == \"length\":\n        # Apply Softplus and affine transform for the 'length' parameter\n        return jt.ChainTransform([jt.SoftplusTransform(0), jt.AffineTransform(10, 0)])\n    else:\n        # Default to a Softplus transform for other parameters\n        return jt.SoftplusTransform(0)\n\n# Apply the transforms to the parameters\ntransforms = [{k: create_transform(k) for k in param} for param in params]\ntf = jt.ParamTransform(transforms)\n
transform = jx.ParamTransform([{\"radius\": jt.SigmoidTransform(0.1, 5.0)},\n                               {\"Leak_gLeak\":jt.SigmoidTransform(1e-5, 1e-3)},\n                               {\"TanhRateSynapse_gS\" : jt.SigmoidTransform(1e-5, 1e-2)}])\n

With these modify the loss function acocrdingly:

def loss(opt_params, inputs, labels):\n    transform.forward(opt_params)\n\n    traces = batched_simulate(params, inputs)  # Shape `(batchsize, num_recordings, timepoints)`.\n    prediction = jnp.mean(traces[:, 2], axis=1)  # Use the average over time of the output neuron (2) as prediction.\n    prediction = (prediction + 72.0)  # Such that the prediction is around 0.\n    losses = jnp.abs(prediction - labels)  # Mean absolute error loss.\n    return jnp.mean(losses)  # Average across the batch.\n
"},{"location":"tutorial/07_gradient_descent/#using-checkpointing","title":"Using checkpointing","text":"

Checkpointing allows to vastly reduce the memory requirements of training biophysical models (see also JAX\u2019s full tutorial on checkpointing).

t_max = 5.0\ndt = 0.025\n\nlevels = 2\ntime_points = t_max // dt + 2\ncheckpoints = [int(np.ceil(time_points**(1/levels))) for _ in range(levels)]\n

To enable checkpointing, we have to modify the simulate function appropriately and use

jx.integrate(..., checkpoint_inds=checkpoints)\n
as done below:

def simulate(params, inputs):\n    currents = jx.datapoint_to_step_currents(i_delay=1.0, i_dur=1.0, i_amp=inputs / 10.0, delta_t=dt, t_max=t_max)\n\n    data_stimuli = None\n    data_stimuli = net.cell(0).branch(2).loc(1.0).data_stimulate(currents[0], data_stimuli=data_stimuli)\n    data_stimuli = net.cell(1).branch(2).loc(1.0).data_stimulate(currents[1], data_stimuli=data_stimuli)\n\n    return jx.integrate(net, params=params, data_stimuli=data_stimuli, checkpoint_lengths=checkpoints)\n\nbatched_simulate = vmap(simulate, in_axes=(None, 0))\n\n\ndef predict(params, inputs):\n    traces = simulate(params, inputs)  # Shape `(batchsize, num_recordings, timepoints)`.\n    prediction = jnp.mean(traces[2])  # Use the average over time of the output neuron (2) as prediction.\n    return prediction + 72.0  # Such that the prediction is around 0.\n\nbatched_predict = vmap(predict, in_axes=(None, 0))\n\n\ndef loss(opt_params, inputs, labels):\n    params = transform.forward(opt_params)\n\n    predictions = batched_predict(params, inputs)\n    losses = jnp.abs(predictions - labels)  # Mean absolute error loss.\n    return jnp.mean(losses)  # Average across the batch.\n\njitted_grad = jit(value_and_grad(loss, argnums=0))\n
"},{"location":"tutorial/07_gradient_descent/#training","title":"Training","text":"

We will use the ADAM optimizer from the optax library to optimize the free parameters (you have to install the package with pip install optax first):

import optax\n
opt_params = transform.inverse(params)\noptimizer = optax.adam(learning_rate=0.01)\nopt_state = optimizer.init(opt_params)\n
"},{"location":"tutorial/07_gradient_descent/#writing-a-dataloader","title":"Writing a dataloader","text":"

Below, we just write our own (very simple) dataloader. Alternatively, you could use the dataloader from any deep learning library such as pytorch or tensorflow:

class Dataset:\n    def __init__(self, inputs: np.ndarray, labels: np.ndarray):\n        \"\"\"Simple Dataloader.\n\n        Args:\n            inputs: Array of shape (num_samples, num_dim)\n            labels: Array of shape (num_samples,)\n        \"\"\"\n        assert len(inputs) == len(labels), \"Inputs and labels must have same length\"\n        self.inputs = inputs\n        self.labels = labels\n        self.num_samples = len(inputs)\n        self._rng_state = None\n        self.batch_size = 1\n\n    def shuffle(self, seed=None):\n        \"\"\"Shuffle the dataset in-place\"\"\"\n        self._rng_state = np.random.get_state()[1][0] if seed is None else seed\n        np.random.seed(self._rng_state)\n        indices = np.random.permutation(self.num_samples)\n        self.inputs = self.inputs[indices]\n        self.labels = self.labels[indices]\n        return self\n\n    def batch(self, batch_size):\n        \"\"\"Create batches of the data\"\"\"\n        self.batch_size = batch_size\n        return self\n\n    def __iter__(self):\n        self.shuffle(seed=self._rng_state)\n        for start in range(0, self.num_samples, self.batch_size):\n            end = min(start + self.batch_size, self.num_samples)\n            yield self.inputs[start:end], self.labels[start:end]\n        self._rng_state += 1\n
"},{"location":"tutorial/07_gradient_descent/#training-loop","title":"Training loop","text":"
batch_size = 4\ndataloader = Dataset(inputs, labels)\ndataloader = dataloader.shuffle(seed=0).batch(batch_size)\n\nfor epoch in range(10):\n    epoch_loss = 0.0\n\n    for batch_ind, batch in enumerate(dataloader):\n        current_batch, label_batch = batch\n        loss_val, gradient = jitted_grad(opt_params, current_batch, label_batch)\n        updates, opt_state = optimizer.update(gradient, opt_state)\n        opt_params = optax.apply_updates(opt_params, updates)\n        epoch_loss += loss_val\n\n    print(f\"epoch {epoch}, loss {epoch_loss}\")\n\nfinal_params = transform.forward(opt_params)\n
epoch 0, loss 25.033223182772293\nepoch 1, loss 21.00894915349165\nepoch 2, loss 15.092242959956026\nepoch 3, loss 9.061544660383163\nepoch 4, loss 6.925509860325612\nepoch 5, loss 6.273630037897756\nepoch 6, loss 6.1757316054693145\nepoch 7, loss 6.135132525725265\nepoch 8, loss 6.145608619185389\nepoch 9, loss 6.135660902068834\n
ntest = 32\npredictions = batched_predict(final_params, inputs[:ntest])\n
fig, ax = plt.subplots(1, 1, figsize=(3, 2))\n_ = ax.scatter(labels[:ntest], predictions)\n_ = ax.set_xlabel(\"Label\")\n_ = ax.set_ylabel(\"Prediction\")\n

Indeed, the loss goes down and the network successfully classifies the patterns.

"},{"location":"tutorial/07_gradient_descent/#summary","title":"Summary","text":"

Puh, this was a pretty dense tutorial with a lot of material. You should have learned how to:

  • compute the gradient with respect to parameters
  • use parameter transformations
  • use multi-level checkpointing
  • define optimizers
  • write dataloaders and parallelize across data

This was the last \u201cbasic\u201d tutorial of the Jaxley toolbox. If you want to learn more, check out our Advanced Tutorials. If anything is still unclear please create a discussion. If you find any bugs, please open an issue. Happy coding!

"},{"location":"tutorial/08_importing_morphologies/","title":"Working with morphologies","text":"

In this tutorial, you will learn how to:

  • Load morphologies and make them compatible with Jaxley
  • Use the visualization features
  • Assemble a small network of morphologically accurate cells.

Here is a code snippet which you will learn to understand in this tutorial:

import jaxley as jx\n\ncell = jx.read_swc(\"my_cell.swc\", ncomp=4)\ncell.branch(2).set_ncomp(2)  # Modify the number of compartments of a branch.\n

To work with more complicated morphologies, Jaxley supports importing morphological reconstructions via .swc files. .swc is currently the only supported format. Other formats like .asc need to be converted to .swc first, for example using the BlueBrain\u2019s morph-tool. For more information on the exact specifications of .swc see here.

import jaxley as jx\nfrom jaxley.synapses import IonotropicSynapse\nimport matplotlib.pyplot as plt\n

To work with .swc files, Jaxley implements a custom .swc reader. The reader traces the morphology and identifies all uninterrupted sections. These are then partitioned into branches, each of which will be approximated by a number of equally many compartments that can be simulated fully in parallel.

To demonstrate this, let\u2019s import an example morphology of a Layer 5 pyramidal cell and visualize it.

# import swc file into jx.Cell object\nfname = \"data/morph.swc\"\ncell = jx.read_swc(fname, ncomp=8)  # Use four compartments per branch.\n\n# print shape (num_branches, num_comps)\nprint(cell.shape)\n\ncell.show()\n
(157, 1256)\n
local_comp_index global_comp_index local_branch_index global_branch_index local_cell_index global_cell_index 0 0 0 0 0 0 0 1 1 1 0 0 0 0 2 2 2 0 0 0 0 3 3 3 0 0 0 0 4 4 4 0 0 0 0 ... ... ... ... ... ... ... 1251 3 1251 156 156 0 0 1252 4 1252 156 156 0 0 1253 5 1253 156 156 0 0 1254 6 1254 156 156 0 0 1255 7 1255 156 156 0 0

1256 rows \u00d7 6 columns

As we can see, this yields a morphology that is approximated by 1256 compartments. Depending on the amount of detail that you need, you can also change the number of compartments in each branch:

cell = jx.read_swc(fname, ncomp=2)\n\n# print shape (num_branches, num_comps)\nprint(cell.shape)\n\ncell.show()\n
(157, 314)\n
local_comp_index global_comp_index local_branch_index global_branch_index local_cell_index global_cell_index 0 0 0 0 0 0 0 1 1 1 0 0 0 0 2 0 2 1 1 0 0 3 1 3 1 1 0 0 4 0 4 2 2 0 0 ... ... ... ... ... ... ... 309 1 309 154 154 0 0 310 0 310 155 155 0 0 311 1 311 155 155 0 0 312 0 312 156 156 0 0 313 1 313 156 156 0 0

314 rows \u00d7 6 columns

The above assigns the same number of compartments to every branch. To use a different number of compartments in individual branches, you can use .set_ncomp():

cell.branch(1).set_ncomp(4)\n

As you can see below, branch 0 has two compartments (because this is what was passed to jx.read_swc(..., ncomp=2)), but branch 1 has four compartments:

cell.branch([0, 1]).nodes\n
local_cell_index local_branch_index local_comp_index length radius axial_resistivity capacitance v global_cell_index global_branch_index global_comp_index controlled_by_param 0 0 0 0 0.050000 8.119000 5000.0 1.0 -70.0 0 0 0 0 1 0 0 1 0.050000 8.119000 5000.0 1.0 -70.0 0 0 1 0 2 0 1 0 3.120779 7.806172 5000.0 1.0 -70.0 0 1 2 1 3 0 1 1 3.120779 7.111231 5000.0 1.0 -70.0 0 1 3 1 4 0 1 2 3.120779 5.652394 5000.0 1.0 -70.0 0 1 4 1 5 0 1 3 3.120779 3.869247 5000.0 1.0 -70.0 0 1 5 1

Once imported the compartmentalized morphology can be viewed using vis.

# visualize the cell\ncell.vis()\nplt.axis(\"off\")\nplt.title(\"L5PC\")\nplt.show()\n

vis can be called on any jx.Module and every View of the module. This means we can also for example use vis to highlight each branch. This can be done by iterating over each branch index and calling cell.branch(i).vis(). Within the loop.

fig, ax = plt.subplots(1, 1, figsize=(5, 5))\n# define colorwheel with 10 colors\ncolors = plt.cm.tab10.colors\nfor i, branch in enumerate(cell.branches):\n    branch.vis(ax=ax, col=colors[i % 10])\nplt.axis(\"off\")\nplt.title(\"Branches\")\nplt.show()\n

While we only use two compartments to approximate each branch in this example, we can see the morphology is still plotted in great detail. This is because we always plot the full .swc reconstruction irrespective of the number of compartments used. The morphology lives seperately in the cell.xyzr attribute in a per branch fashion.

In addition to plotting the full morphology of the cell using points vis(type=\"scatter\") or lines vis(type=\"line\"), Jaxley also supports plotting a detailed morphological vis(type=\"morph\") or approximate compartmental reconstruction vis(type=\"comp\") that correctly considers the thickness of the neurite. Note that \"comp\" plots the lengths of each compartment which is equal to the length of the traced neurite. While neurites can be zigzaggy, the compartments that approximate them are straight lines. This can lead to miss-aligment of the compartment ends. For details see the documentation of vis.

The morphologies can either be projected onto 2D or also rendered in 3D.

# visualize the cell\nfig, ax = plt.subplots(1, 4, figsize=(10, 3), layout=\"constrained\", sharex=True, sharey=True)\ncell.vis(ax=ax[0], type=\"morph\", dims=[0,1])\ncell.vis(ax=ax[1], type=\"comp\", dims=[0,1])\ncell.vis(ax=ax[2], type=\"scatter\", dims=[0,1], morph_plot_kwargs={\"s\": 1})\ncell.vis(ax=ax[3], type=\"line\", dims=[0,1])\nfig.suptitle(\"Comparison of plot types\")\nplt.show()\n

# set to interactive mode\n# %matplotlib notebook\n
# plot in 3D\nfig = plt.figure()\nax = fig.add_subplot(111, projection='3d')\ncell.vis(ax=ax, type=\"line\", dims=[2,0,1])\nax.view_init(elev=20, azim=5)\nplt.show()\n

Since Jaxley supports grouping different branches or compartments together, we can also use the id labels provided by the .swc file to assign group labels to the jx.Cell object.

print(list(cell.groups.keys()))\n\nfig, ax = plt.subplots(1, 1, figsize=(5, 5))\ncolors = plt.cm.tab10.colors\ncell.basal.vis(ax=ax, col=colors[2])\ncell.soma.vis(ax=ax, col=colors[1])\ncell.apical.vis(ax=ax, col=colors[0])\nplt.axis(\"off\")\nplt.title(\"Groups\")\nplt.show()\n
['soma', 'basal', 'apical', 'custom']\n

To build a network of morphologically detailed cells, we can now connect several reconstructed cells together and also visualize the network. However, since all cells are going to have the same center, Jaxley will naively plot all of them on top of each other. To seperate out the cells, we therefore have to move them to a new location first.

net = jx.Network([cell]*5)\njx.connect(net[0,0,0], net[2,0,0], IonotropicSynapse())\njx.connect(net[0,0,0], net[3,0,0], IonotropicSynapse())\njx.connect(net[0,0,0], net[4,0,0], IonotropicSynapse())\n\njx.connect(net[1,0,0], net[2,0,0], IonotropicSynapse())\njx.connect(net[1,0,0], net[3,0,0], IonotropicSynapse())\njx.connect(net[1,0,0], net[4,0,0], IonotropicSynapse())\n\nnet.rotate(-90)\n\nnet.cell(0).move(0, 300)\nnet.cell(1).move(0, 500)\n\nnet.cell(2).move(900, 200)\nnet.cell(3).move(900, 400)\nnet.cell(4).move(900, 600)\n\nnet.vis()\nplt.axis(\"off\")\nplt.show()\n

Congrats! You have now learned how to vizualize and build networks out of very complex morphologies. To simulate this network, you can follow the steps in the tutorial on how to build a network.

"},{"location":"tutorial/09_advanced_indexing/","title":"Customizing synaptic parameters","text":"

In this tutorial, you will learn how to:

  • use the select() method to fully customize network simulations with Jaxley.
  • use the copy_node_property_to_edges() method to flexibly modify synapses.

Here is a code snippet which you will learn to understand in this tutorial:

net = ...  # See tutorial on Basics of Jaxley.\n\n# Set synaptic conductance of the synapse with index 0 and 1.\nnet.select(edges=[0, 1]).set(\"Ionotropic_gS\", 0.1)\n\n# Set synaptic conductance of all synapses that have cells 3 or 4 as presynaptic neuron.\nnet.copy_node_property_to_edges(\"global_cell_index\")\ndf = net.edges\ndf = df.query(\"pre_global_cell_index in [3, 4]\")\nnet.select(edges=df.index).set(\"Ionotropic_gS\", 0.2)\n\n# Set synaptic conductance of all synapses that\n# 1) have cells 2 or 3 as presynaptic neuron and\n# 2) has cell 5 as postsynaptic neuron\ndf = net.edges\ndf = df.query(\"pre_global_cell_index in [2, 3]\")\ndf = df.query(\"post_global_cell_index == 5\")\nnet.select(edges=df.index).set(\"Ionotropic_gS\", 0.3)\n

In a previous tutorial you learned how to set parameters of a jx.Network. In that tutorial, we briefly mentioned the select() method which allowed to set individual synapses to particular values. In this tutorial, we will go into detail in how you can fully customize your Jaxley simulation.

Let\u2019s go!

import jaxley as jx\nfrom jaxley.channels import Na, K, Leak\nfrom jaxley.connect import fully_connect\nfrom jaxley.synapses import IonotropicSynapse\n
"},{"location":"tutorial/09_advanced_indexing/#preface-building-the-network","title":"Preface: Building the network","text":"

We first build a network consisting of six neurons, in the same way as we showed in the previous tutorials:

dt = 0.025\nt_max = 10.0\n\ncomp = jx.Compartment()\nbranch = jx.Branch(comp, nseg=2)\ncell = jx.Cell(branch, parents=[-1, 0])\nnet = jx.Network([cell for _ in range(6)])\nfully_connect(net.cell([0, 1, 2]), net.cell([3, 4, 5]), IonotropicSynapse())\n
"},{"location":"tutorial/09_advanced_indexing/#setting-individual-synapse-parameters","title":"Setting individual synapse parameters","text":"

As always, you can use the .edges table to inspect synaptic parameters of the network:

net.edges\n
global_edge_index pre_global_comp_index post_global_comp_index type type_ind pre_locs post_locs IonotropicSynapse_gS IonotropicSynapse_e_syn IonotropicSynapse_k_minus IonotropicSynapse_s controlled_by_param 0 0 0 13 IonotropicSynapse 0 0.25 0.75 0.0001 0.0 0.025 0.2 0 1 1 0 19 IonotropicSynapse 0 0.25 0.75 0.0001 0.0 0.025 0.2 0 2 2 0 20 IonotropicSynapse 0 0.25 0.25 0.0001 0.0 0.025 0.2 0 3 3 4 12 IonotropicSynapse 0 0.25 0.25 0.0001 0.0 0.025 0.2 0 4 4 4 16 IonotropicSynapse 0 0.25 0.25 0.0001 0.0 0.025 0.2 0 5 5 4 21 IonotropicSynapse 0 0.25 0.75 0.0001 0.0 0.025 0.2 0 6 6 8 13 IonotropicSynapse 0 0.25 0.75 0.0001 0.0 0.025 0.2 0 7 7 8 17 IonotropicSynapse 0 0.25 0.75 0.0001 0.0 0.025 0.2 0 8 8 8 21 IonotropicSynapse 0 0.25 0.75 0.0001 0.0 0.025 0.2 0

This table has nine rows, each corresponding to one synapse. This makes sense because we fully connected three neurons (0, 1, 2) to three other neurons (3, 4, 5), giving a total of 3x3=9 synapses.

You can modify parameters of individual synapses as follows:

net.select(edges=[3, 4, 5]).set(\"IonotropicSynapse_gS\", 0.2)\n

Above, we are modifying the synapses with indices [3, 4, 5] (i.e., the indices of the net.edges DataFrame). The resulting values are indeed changed:

net.edges.IonotropicSynapse_gS\n
0    0.0001\n1    0.0001\n2    0.0001\n3    0.2000\n4    0.2000\n5    0.2000\n6    0.0001\n7    0.0001\n8    0.0001\nName: IonotropicSynapse_gS, dtype: float64\n
"},{"location":"tutorial/09_advanced_indexing/#example-1-setting-synaptic-parameters-which-connect-particular-neurons","title":"Example 1: Setting synaptic parameters which connect particular neurons","text":"

This is great, but setting synaptic parameters just by their index can be exhausting, in particular in very large networks. Instead, we would want to, for example, set the maximal conductance of all synapses that connect from cell 0 or 1 to any other neuron.

In Jaxley, such customization can be achieved by filtering the .edges dataframe accordingly, as shown below:

net = jx.Network([cell for _ in range(6)])\nfully_connect(net.cell([0, 1, 2]), net.cell([3, 4, 5]), IonotropicSynapse())\n\nnet.copy_node_property_to_edges(\"global_cell_index\")\ndf = net.edges\ndf = df.query(\"pre_global_cell_index in [0, 1]\")\nnet.select(edges=df.index).set(\"IonotropicSynapse_gS\", 0.23)\n
net.edges.IonotropicSynapse_gS\n
0    0.2300\n1    0.2300\n2    0.2300\n3    0.2300\n4    0.2300\n5    0.2300\n6    0.0001\n7    0.0001\n8    0.0001\nName: IonotropicSynapse_gS, dtype: float64\n

Indeed, the first six synapses now have the value 0.23! Let\u2019s look at the individual lines to understand how this worked:

We want to set parameter by cell index. However, by default, the pre- or post-synaptic cell-indices are not listed in net.edges. We can add the cell index to the .edges dataframe by calling .copy_node_property_to_edges():

net.copy_node_property_to_edges(\"global_cell_index\")\n

After this, the pre- and post-synaptic cell indices are listed in net.edges as pre_global_cell_index and post_global_cell_index.

Next, we take .edges, which is a pandas DataFrame:

df = net.edges\n

We then modify this DataFrame to only contain those rows where the global cell index is in 0 or 1:

df = df.query(\"pre_global_cell_index in [0, 1]\")\n

For the above step, you use any column of the DataFrame to filter it (you can see all columns with df.columns). Note that, while we used .query() here, you can really filter the pandas DataFrame however you want. For example, the query above is identical to df = df[df[\"pre_global_cell_index\"].isin([0, 1])].

Finally, we use the .select() method, which returns a subset of the Network at the specified indices. This subset of the network can be modified with .set():

net.select(edges=df.index).set(\"IonotropicSynapse_gS\", 0.23)\n

"},{"location":"tutorial/09_advanced_indexing/#example-2-setting-parameters-given-pre-and-post-synaptic-cell-indices","title":"Example 2: Setting parameters given pre- and post-synaptic cell indices","text":"

Say you want to select all synapses that have cells 1 or 2 as presynaptic neuron and cell 4 or 5 as postsynaptic neuron.

net = jx.Network([cell for _ in range(6)])\nfully_connect(net.cell([0, 1, 2]), net.cell([3, 4, 5]), IonotropicSynapse())\n

Just like before, we can simply use .query() as already shown above. However, this time, call .query() to twice to filter by pre- and post-synaptic cell indices:

net.copy_node_property_to_edges(\"global_cell_index\")\n\ndf = net.edges\ndf = df.query(\"pre_global_cell_index in [1, 2]\")\ndf = df.query(\"post_global_cell_index in [4, 5]\")\nnet.select(edges=df.index).set(\"IonotropicSynapse_gS\", 0.3)\n
net.edges.IonotropicSynapse_gS\n
0    0.0001\n1    0.0001\n2    0.0001\n3    0.0001\n4    0.3000\n5    0.3000\n6    0.0001\n7    0.3000\n8    0.3000\nName: IonotropicSynapse_gS, dtype: float64\n
"},{"location":"tutorial/09_advanced_indexing/#example-3-applying-this-strategy-to-cell-level-parameters","title":"Example 3: Applying this strategy to cell level parameters","text":"

You had previously seen that you can modify parameters with, e.g., net.cell(0).set(...). However, if you need more flexibility than this, you can also use the above strategy to modify cell-level parameters:

net = jx.Network([cell for _ in range(6)])\nfully_connect(net.cell([0, 1, 2]), net.cell([3, 4, 5]), IonotropicSynapse())\n\ndf = net.nodes\ndf = df.query(\"global_cell_index in [0, 1]\")\nnet.select(nodes=df.index).set(\"radius\", 0.1)\n
"},{"location":"tutorial/09_advanced_indexing/#example-4-flexibly-setting-parameters-based-on-their-groups","title":"Example 4: Flexibly setting parameters based on their groups","text":"

If you are using groups, as shown in this tutorial, then you can also use this for querying synapses. To demonstrate this, let\u2019s create a group of excitatory neurons (e.g., cells 0, 3, 5):

# Redefine network.\nnet = jx.Network([cell for _ in range(6)])\nfully_connect(net.cell([0, 1, 2]), net.cell([3, 4, 5]), IonotropicSynapse())\n\nnet.cell([0, 3, 5]).add_to_group(\"exc\")\n

Now, say we want all synapses that start from these excitatory neurons. You can do this as follows:

# First, we have to identify which cells are in the `exc` group.\nindices_of_excitatory_cells = net.exc.nodes[\"global_cell_index\"].unique().tolist()  # [0, 3, 5]\n\n# Then we can proceed as before:\nnet.copy_node_property_to_edges(\"global_cell_index\")\ndf = net.edges\ndf = df.query(f\"pre_global_cell_index in {indices_of_excitatory_cells}\")\nnet.select(edges=df.index).set(\"IonotropicSynapse_gS\", 0.4)\n
"},{"location":"tutorial/09_advanced_indexing/#example-5-setting-synaptic-parameters-based-on-properties-of-the-presynaptic-cell","title":"Example 5: Setting synaptic parameters based on properties of the presynaptic cell","text":"

Let\u2019s discuss one more example: Imagine we only want to modify those synapses whose presynaptic compartment has a sodium channel. Let\u2019s first add a sodium channel to some of the cells:

net = jx.Network([cell for _ in range(6)])\nfully_connect(net.cell([0, 1, 2]), net.cell([3, 4, 5]), IonotropicSynapse())\n\nnet.cell(0).branch(0).comp(0).insert(Na())\nnet.cell(2).branch(1).comp(1).insert(Na())\n

Now, let us query which cells have the desired synapses:

df = net.nodes\ndf = df.query(\"Na\")\nindices_of_sodium_compartments = df[\"global_comp_index\"].unique().tolist()\n

indices_of_sodium_compartments lists all compartments which contained sodium:

print(indices_of_sodium_compartments)\n
[0, 11]\n

Then, we can proceed as always and filter for the global pre-synaptic compartment index:

df = net.edges\ndf = df.query(f\"pre_global_comp_index in {indices_of_sodium_compartments}\")\nnet.select(edges=df.index).set(\"IonotropicSynapse_gS\", 0.6)\n
net.edges.IonotropicSynapse_gS\n
0    0.6000\n1    0.6000\n2    0.6000\n3    0.0001\n4    0.0001\n5    0.0001\n6    0.0001\n7    0.0001\n8    0.0001\nName: IonotropicSynapse_gS, dtype: float64\n

Indeed, only synapses coming from the first neuron were modified (as its presynaptic compartment contained sodium), in contrast to synapses from neuron 2 (whose presynaptic compartment did not).

"},{"location":"tutorial/09_advanced_indexing/#summary","title":"Summary","text":"

In this tutorial, you learned how to fully customize your Jaxley simulation. This works by querying rows from the .edges DataFrame.

"},{"location":"tutorial/10_advanced_parameter_sharing/","title":"Synaptic parameter sharing","text":"

In this tutorial, you will learn how to:

  • flexibly share parameters of synapses

Here is a code snippet which you will learn to understand in this tutorial:

net = ...  # See tutorial on Basics of Jaxley.\n\n# The same parameter for all synapses\nnet.make_trainable(\"Ionotropic_gS\")\n\n# An individual parameter for every synapse.\nnet.select(edges=\"all\").make_trainable(\"Ionotropic_gS\")\n\n# Share synaptic conductances emerging from the same neurons.\nnet.copy_node_property_to_edges(\"cell_index\")\nsub_net = net.select(edges=[0, 1, 2])\nsub_net.edges[\"controlled_by_param\"] = sub_net.edges[\"pre_global_cell_index\"]\nsub_net.make_trainable(\"Ionotropic_gS\")\n

In a previous tutorial about training networks, we briefly touched on parameter sharing. In this tutorial, we will show you how you can flexibly share parameters within a network.

import jaxley as jx\nfrom jaxley.channels import Na, K, Leak\nfrom jaxley.connect import fully_connect\nfrom jaxley.synapses import IonotropicSynapse\n
"},{"location":"tutorial/10_advanced_parameter_sharing/#preface-building-the-network","title":"Preface: Building the network","text":"

We first build a network consisting of six neurons, in the same way as we showed in the previous tutorials:

dt = 0.025\nt_max = 10.0\n\ncomp = jx.Compartment()\nbranch = jx.Branch(comp, ncomp=2)\ncell = jx.Cell(branch, parents=[-1, 0])\nnet = jx.Network([cell for _ in range(6)])\nfully_connect(net.cell([0, 1, 2]), net.cell([3, 4, 5]), IonotropicSynapse())\n
"},{"location":"tutorial/10_advanced_parameter_sharing/#sharing-parameters-by-modifying-controlled_by_param","title":"Sharing parameters by modifying controlled_by_param","text":"
net.copy_node_property_to_edges(\"global_cell_index\")\n\ndf = net.edges\ndf = df.query(\"pre_global_cell_index in [0, 1, 2]\")\nsubnetwork = net.select(edges=df.index)\n\ndf = subnetwork.edges\ndf[\"controlled_by_param\"] = df[\"pre_global_cell_index\"]\nsubnetwork.make_trainable(\"IonotropicSynapse_gS\")\n
Number of newly added trainable parameters: 3. Total number of trainable parameters: 3\n

Let\u2019s look at this line by line. First, we exactly follow the previous tutorial in selecting the synapses which we are interested in training (i.e., the ones whose presynaptic neuron has index 0, 1, 2):

df = net.edges\ndf = df.query(\"pre_global_cell_index in [0, 1, 2]\")\nsubnetwork = net.select(edges=df.index)\n

As second step, we enable parameter sharing. This is done by setting the controlled_by_param. Synapses that have the same value in controlled_by_param will be shared. Let\u2019s inspect controlled_by_param before we modify it:

subnetwork.edges[[\"pre_global_cell_index\", \"controlled_by_param\"]]\n
pre_global_cell_index controlled_by_param 0 0 0 1 0 1 2 0 2 3 1 3 4 1 4 5 1 5 6 2 6 7 2 7 8 2 8

Every synapse has a different value. Because of this, no synaptic parameters will be shared. To enable parameter sharing we override the controlled_by_param column with the presynaptic cell index:

df = subnetwork.edges\ndf[\"controlled_by_param\"] = df[\"pre_global_cell_index\"]\n
df[[\"pre_global_cell_index\", \"controlled_by_param\"]]\n
pre_global_cell_index controlled_by_param 0 0 0 1 0 0 2 0 0 3 1 1 4 1 1 5 1 1 6 2 2 7 2 2 8 2 2

Now, all we have to do is to make these synaptic parameters trainable with the make_trainable() method:

subnetwork.make_trainable(\"IonotropicSynapse_gS\")\n
Number of newly added trainable parameters: 3. Total number of trainable parameters: 6\n

It correctly says that we added three parameters (because we have three cells, and we share individual synaptic parameters). We now have 6 trainable parameters in total (because we already added 3 trainable parameters above).

"},{"location":"tutorial/10_advanced_parameter_sharing/#a-more-involved-example-sharing-by-pre-and-post-synaptic-cell-type","title":"A more involved example: sharing by pre- and post-synaptic cell type","text":"

As an example, consider the following: We have a fully connected network of six cells. Each cell falls into one of three cell types:

from typing import Union, List\n
net = jx.Network([cell for _ in range(6)])\nfully_connect(net.cell(\"all\"), net.cell(\"all\"), IonotropicSynapse())\n\nnet.cell([0, 1]).add_to_group(\"exc\")\nnet.cell([2, 3]).add_to_group(\"inh\")\nnet.cell([4, 5]).add_to_group(\"unknown\")\n

We want to make all synapses that start from excitatory or inhibitory neurons trainable. In addition, we want to use the same parameter for synapses if they have the same pre- and post-synaptic cell type.

To achieve this, we will first want a column in net.nodes which indicates the cell type.

for group, inds in net.groups.items():\n    net.nodes.loc[inds, \"cell_type\"] = group\n
net.nodes[\"cell_type\"]\n
0         exc\n1         exc\n2         exc\n3         exc\n4         exc\n5         exc\n6         exc\n7         exc\n8         inh\n9         inh\n10        inh\n11        inh\n12        inh\n13        inh\n14        inh\n15        inh\n16    unknown\n17    unknown\n18    unknown\n19    unknown\n20    unknown\n21    unknown\n22    unknown\n23    unknown\nName: cell_type, dtype: object\n

The cell_type is now part of the net.nodes. However, we would like to do parameter sharing of synapses based on the pre- and post-synaptic node values. To do so, we import the cell_type column into net.edges. To do this, we use the .copy_node_property_to_edges() which the name of the property you are copying from nodes:

net.copy_node_property_to_edges(\"cell_type\")\n

After this, you have columns in the .edges which indicate the pre- and post-synaptic cell type:

net.edges[[\"pre_cell_type\", \"post_cell_type\"]]\n
pre_cell_type post_cell_type 0 exc exc 1 exc exc 2 exc inh 3 exc inh 4 exc unknown 5 exc unknown 6 exc exc 7 exc exc 8 exc inh 9 exc inh 10 exc unknown 11 exc unknown 12 inh exc 13 inh exc 14 inh inh 15 inh inh 16 inh unknown 17 inh unknown 18 inh exc 19 inh exc 20 inh inh 21 inh inh 22 inh unknown 23 inh unknown 24 unknown exc 25 unknown exc 26 unknown inh 27 unknown inh 28 unknown unknown 29 unknown unknown 30 unknown exc 31 unknown exc 32 unknown inh 33 unknown inh 34 unknown unknown 35 unknown unknown

Next, we specify which parts of the network we actually want to change (in this case, all synapses which have excitatory or inhibitory presynaptic neurons):

df = net.edges\ndf = df.query(f\"pre_cell_type in ['exc', 'inh']\")\nprint(f\"There are {len(df)} synapses to be changed.\")\n\nsubnetwork = net.select(edges=df.index)\n
There are 24 synapses to be changed.\n

As the last step, we again have to specify parameter sharing by setting controlled_by_param. In this case, we want to share parameters that have the same pre- and post-synaptic neuron. We achieve this by grouping the synpases by their pre- and post-synaptic cell type (see pd.DataFrame.groupby for details):

# Step 6: use groupby to specify parameter sharing and make the parameters trainable.\nsubnetwork.edges[\"controlled_by_param\"] = subnetwork.edges.groupby([\"pre_cell_type\", \"post_cell_type\"]).ngroup()\nsubnetwork.make_trainable(\"IonotropicSynapse_gS\")\n
Number of newly added trainable parameters: 6. Total number of trainable parameters: 6\n

This created six trainable parameters, which makes sense as we have two types of pre-synaptic neurons (excitatory and inhibitory) and each has three options for the postsynaptic neuron (pre, post, unknown).

"},{"location":"tutorial/10_advanced_parameter_sharing/#summary","title":"Summary","text":"

In this tutorial, you learned how you can flexibly share synaptic parameters. This works by first using select() to identify which synapses to make trainable, and by then modifying controlled_by_param to customize parameter sharing.

"}]} \ No newline at end of file diff --git a/dev/tutorial/00_jaxley_api/index.html b/dev/tutorial/00_jaxley_api/index.html index 0827f35c..415857ca 100644 --- a/dev/tutorial/00_jaxley_api/index.html +++ b/dev/tutorial/00_jaxley_api/index.html @@ -882,7 +882,7 @@

Key concepts in Jaxley# Assembling different Modules into a Network comp = jx.Compartment() -branch = jx.Branch(comp, nseg=1) +branch = jx.Branch(comp, ncomp=1) cell = jx.Cell(branch, parents=[-1, 0, 0]) net = jx.Network([cell]*3) @@ -927,9 +927,9 @@

Modules
comp = jx.Compartment() # single compartment model.
 

-

Mutliple Compartments can be connected together to form longer, linear segments / cables, which we call Branches and are equivalent to sections in NEURON.

-
axial_resistivity capacitance vxy...Na_mNa_hKK_gKeKK_n global_cell_index global_branch_index global_comp_index0 0 10.00.0207031.0 5000.0 1.0 -70.0NaNNaN...NaNNaNFalseNaNNaNNaN 0 0 00 1 10.00.0207031.0 5000.0 1.0 -70.0NaNNaN...NaNNaNFalseNaNNaNNaN 0 0 10 2 10.00.0207031.0 5000.0 1.0 -70.0NaNNaN...NaNNaNFalseNaNNaNNaN 0 0 20 3 10.00.0207031.0 5000.0 1.0 -70.0NaNNaN...NaNNaNFalseNaNNaNNaN 0 0 31 0 10.00.0207031.0 5000.0 1.0 -70.0NaNNaN...NaNNaNFalseNaNNaNNaN 0 1 41 1 10.00.0207031.0 5000.0 1.0 -70.0NaNNaN...NaNNaNFalseNaNNaNNaN 0 1 51 2 10.00.0207031.0 5000.0 1.0 -70.0NaNNaN...NaNNaNFalseNaNNaNNaN 0 1 61 3 10.00.0207031.0 5000.0 1.0 -70.0NaNNaN...NaNNaNFalseNaNNaNNaN 0 1 72 0 10.00.0207031.0 5000.0 1.0 -70.0NaNNaN...NaNNaNFalseNaNNaNNaN 0 2 82 1 10.00.0207031.0 5000.0 1.0 -70.0NaNNaN...NaNNaNFalseNaNNaNNaN 0 2 92 2 10.00.0207031.0 5000.0 1.0 -70.0NaNNaN...NaNNaNFalseNaNNaNNaN 0 2 102 3 10.00.0207031.0 5000.0 1.0 -70.0NaNNaN...NaNNaNFalseNaNNaNNaN 0 2 11
-

12 rows × 28 columns

Let’s use Views to visualize only parts of the Network. Before we do that, we create x, y, and z coordinates for the Network:

@@ -1963,7 +1619,7 @@

Views&

We can now visualize the entire net (i.e., the entire Module) with the .vis() method…

# We can use the vis function to visualize Modules.
-fig, ax = plt.subplots(1,1, figsize=(3,3))
+fig, ax = plt.subplots(1, 1, figsize=(3,3))
 net.vis(ax=ax)
 
-
View with 1 different channels. Use `.nodes` for details.
+
View with 0 different channels. Use `.nodes` for details.
 
cell0.nodes
@@ -2028,13 +1684,7 @@ 

How to create ViewsHow to create ViewsHow to create ViewsHow to create ViewsHow to create ViewsHow to create ViewsHow to create ViewsHow to create ViewsHow to create ViewsHow to create ViewsHow to create ViewsHow to create ViewsHow to create ViewsHow to create Views
net.shape
@@ -2381,28 +1958,28 @@ 

How to create ViewsHow to create ViewsChannelsChannelsChannelsChannelsChannelsChannelsBasics of Jaxley# Build the cell. comp = jx.Compartment() -branch = jx.Branch(comp, nseg=2) +branch = jx.Branch(comp, ncomp=2) cell = jx.Cell(branch, parents=[-1, 0, 0, 1, 1]) # Insert channels. @@ -1057,7 +1057,7 @@

Basics of JaxleyDefine the cell from scratch

To define a cell from scratch you first have to define a single compartment and then assemble those compartments into a branch:

comp = jx.Compartment()
-branch = jx.Branch(comp, nseg=2)
+branch = jx.Branch(comp, ncomp=2)
 

Next, we can assemble branches into a cell. To do so, we have to define for each branch what its parent branch is. A -1 entry means that this branch does not have a parent.

parents = jnp.asarray([-1, 0, 0, 1, 1])
@@ -1066,7 +1066,7 @@ 

Define the cell from scratchthis tutorial.

Read the cell from an SWC file

Alternatively, you could also load cells from SWC with

-

cell = jx.read_swc(fname, nseg=4)

+

cell = jx.read_swc(fname, ncomp=4)

Details on handling SWC files can be found in this tutorial.

Visualize the cells

Cells can be visualized as follows:

@@ -1475,7 +1475,7 @@

Insert mechanisms
fig, ax = plt.subplots(1, 1, figsize=(4, 2))
 _ = cell.vis(ax=ax, col="k")
 _ = cell.branch(1).vis(ax=ax, col="r")
-_ = cell.branch(1).comp(1).vis(ax=ax, col="b", type="scatter")
+_ = cell.branch(1).comp(1).vis(ax=ax, col="b")
 

png

More background and features on indexing as cell.branch(0) is in this tutorial.

@@ -1616,8 +1616,8 @@

Define recordings
fig, ax = plt.subplots(1, 1, figsize=(4, 2))
 _ = cell.vis(ax=ax)
-_ = cell.branch(0).loc(0.0).vis(ax=ax, col="b", type="scatter")
-_ = cell.branch(3).loc(1.0).vis(ax=ax, col="g", type="scatter")
+_ = cell.branch(0).loc(0.0).vis(ax=ax, col="b")
+_ = cell.branch(3).loc(1.0).vis(ax=ax, col="g")
 

png

Simulate the cell response

diff --git a/dev/tutorial/01_morph_neurons_files/01_morph_neurons_21_0.png b/dev/tutorial/01_morph_neurons_files/01_morph_neurons_21_0.png index 929c6756fcc8762afc25ce10a23fa781d7585771..fd8637211e88509a3df36571e3332268c0281932 100644 GIT binary patch literal 8843 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 literal 8916 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 literal 9137 zcmb7q2Q-%d|M!i^-f@!=2}wrE9vNBLBtlk5_Q=dAd-FxvC969-Gkb(YL}u1430b$1 z?EdfT`}?2g{GW54^E~Hq4sPeVuFv%u@AvEddcEGFbhK0`NSR4dC=`X7s*)ZGh35*N zyNHS4r=~qd6Mji}-ZJvkcd_;Kwsf~aXlpgprF(Lx6AEWHpN4!hpl3T z4Gaw0zJ8@+V2ES4BWr1kiHVtCU;ivc#1UHE(9xmbW1q1qJN`1C$X|?1kIh2G~$^qe-2!j0$e7(!@>~isP ze-8%Xv5PkewIXS*@bNv<;8yhV5}lr&#&uH3y)thNM6Sxi)uc~-eVrASovu4SHDRgq zBN%<$pFc~iF=(25VG5Bn|MV>qlaY0P&J>$pT8h}+_2hGTjqLYDkNFNla^~yAq+$30 zM|-#K-D7U>T{l^^mJkc`6B3A0lbq zl$3n8f%9Tby-IeEZN)k~hEA57>rz z$DVlBG*Kt!U#qLa0f(L+y1LrO#-4lgbE05q*J{g7qW)|t##rJ0mvZcl-kaeyH8(bm zlrA8T1`CJ(nG0gS{hS{U#c$R?Z&K^%yS76`;!AJ#QBhUu>%;3t6&851M{6Rew!`16 zHMg`>J#?PZ4aXzz^51h@n`va^Od@_6u*dXsAiGbB;on34rlzS$!L6MWdr>96ZFrd0 ze{ZcFw$ZnT6JME7OH~S4Wty;cVoJ)HN~75NdU2&&x3=PMorx#SN)$D`e?R`?N7XDT zZ_4=i`2CH>O(7qg1#AV$D}0`Oip%t*p%D=a-)bCO-Q46A6!7o+tz3euQOMP}19w(YHI3MywaIBnC9;FhMD8zC%p8eA0J#>7sVJvW3d)lGXAwggQH_( z3I%#AO~k@PJlZ)*K0e|p>X$k?I$RHT?d#vahrQN7*WusgrAMK>y}h&7H#RoD{UJB5 zb7sxg!|W}tug?@pkl05nMo@C_@sT1qDEmYla_~Z`IvgUmmaF%Gaf9dO6!n6=J4Qwy zrD%mcAh5bWMA7Nqy&Dll$MgMT(#49pI>?$dJZHjLT-uxAVZH42f`WpCfY1~p{%|GE zAzzw~KG;6oui|orydiAO`29{;yoI%OCma~vH0kvCI3pVyo2t6{u(LkKKs@MFT0ud< zr;9UR4Q;)T4WX)VomW7$nYc8E3AXIxKY#u_E1AY)xR7z%z|fFWP*D5GBQQ|*+O=zt zP-pk|_brBA-`a<4A8PjZ-akCF>WV&x+ucP1@!K^9j;kH$&c@A_n4r_0m#E+y4r3N@ zojOVh#;p4K{I=yk$;nKp=J68aDzr7#bJ{Ya%4hEz8$~LO`ok$%Ke@ZRXZ&WZj!mpC zIz05-J6tVy_-lOnBU#%=g_9f3iW^|n`S0owDS#v(5}d~8h-A?_uvttZw%E3(1Qit( zZ7<9Nqb95DcHne$^!?*D!W2~L4DQ~w?g%9jcl&;(q_nisWllDhT`mWr=qIGFFMg_e z7yw|Fga?tAmsey$f{K#TnN6cmNYQQaI+wX_+S-uw8@7igor{xO^xJmcO|5{`nu=J zaF{p00{Lrvo*zMgdO{CAQm4FlaZy|xefjd`gl0i1jwaowboki)tp&8O)fv7kSA^k~ zEXl{DtdgEs)|kW|c+}SA@97Cz#FqTgqenuIMtG6c06M^Gwv`O9A4&pF50=s-AM+uw z(+GLfht81J0TtEbeVmJ=wTntFZU0hvF zKR-WT@AsR})YR1V`v;OUXU`%lDu$xVjd}G71;_XP{d*h^*VdcNGgPF_K3hp_e+PEs z2Ygw;(ev{2E4>gfbNw}PdH>)5j$XAm?+Rn2TJOh?H;s(Yq-10?)YQTM7C5VSu$-C1orkf)kwZz=%^WpjP7k~*)Z>|O@=Hl zKYTO#NFz-M&phblhNGh+GN2i{i)J4tF$CB=@$DNY#1f-`DV7y8$|8BjbDfl&ja(!9 z5G&W{6;P2~-T(ZmiIBq>Ka8WKqGAn!WH9@o#8nXI(t5%W!zkGO_D-?u!M4pALP8Es zPJA01o2I;)iCo3DP?EOI)>UUgKEH}=_2U(x?h3Elm!#3lgitm5Hzq5u=JJ)OFPPYV{e^kQ^urE0^Gc%K`!%H4$JJawuQN7^Xi@G{* zf2B*x-RM9h1`QvzrkSUs-%Sb~oqP@0OXiztPQ0%*cq2 zj3j1}^d!Eh9E)X*OZ4h_Oxo4mO>}bfM@3KXAFoX1Jb67mTBsP}+zt}AZ)3k|vmYM} z?Kn`#1#Oe0{`s1y%7vgR=f#V<`uZWddCKzu!w6z*)C*n&920KY|4z0x+kD>G*f>qh zh3)Ozw?e>pW{2)MPK#r)?PrTM*?unmwm7PHd*YssFC``Qp{K`s`$zAa^75XP%lf#@ zpud@beBaLfk-(`XbI(J-8ohVFL{CbXChQvaRWj_ zJC}M`56BpMz%I>LnHd>se`@YD{8kKueBO|&ma^&;;3&5|7w!DXpShl&pEL1LnW9b| z4&}|^5Of2*#P(8<#Q9AJKQkwq{l)mNUafQ(7tqL&HeUK$?CXk(MA*H1ga}f~%gd|Y zz8wNd1%;NHnK^oT+7y8uVR$Qp-|q5LK&jp3Ap$}|LWG--7T*P$l79<8YZAA$)t)Y5 zM@>&33KRt4=Su_G3s6>yfQt4l!FDk=&s z>WD^;-u;Ivi-db9OerG{7IW$B*|RJ%e)K@aZ<(2~b|(7#)gFF+l?Qa9}*9|+NiM%x=PA+~3RG6T5wwo1H8xr4-QkzX7 zgK~daAO@!DTzX=eL|p%#2EaZEM?!5@0s4D(?#T{CNl6Lrv)G))@&48u3}&7+hFS)Z zAVR*|4_IWMprLMx`|a{7$FoF5N0Y+SL!gROFG8LqB_^U&R8+WlcoeylbDNs7U^A+b zH^<6{5Y6wdd|m#MOCVrc9}Iuy=I0}Og+iE!63{bFC>9}5>Es1oTm)7u<}&*OUJQaT zXMMi81 zTL8pJPEK;j%CaSMYjyxi3B!ILFUkFd14yGA>xOvf1@b++x7qqP$Joe7C`=)Drq*dn zlsie)$jQkmIjB_+^8l5mhjEOn54~;@5FnE&Vn<2PhN=K?PxI#Dbk$tH;p~C68_c=& zQZ@j)u~9a|KtqEE6tTa*Us>+1i3wS%dch87C=Vd-+C(M6X44KO3R_w?+wp6(c*Uza z|Fhc_S9q=dEUH@W3VpP)Tpa|aYJuK*-_%s2rU&gXJ%kpbdLq~Z0e`A&DHsIrYt-dk za(Zh_$aelbS(57R#9d&bCN_ls%1QMCy(2z;e#fty7xOV7x}Xw=Yi5e#p=1w!&^4VN zuIdzz0>Rd|8O_R`3p{daI{a0fFjEBbUGvZkxPQIt{Ih{t$lG=QIGUI)>XX`Hl&nn(jW{;k!k{Uttl_b>$L%2QhIt6V1m_;k4aA3 zte^RCKY4k1Z~FR5kdl(VGAxF3#zWzDc0%&`xOWfZYjJ$ZZ2YNkd1c zsKWPwEB3q?CA_~AbOvHIT!1KqKSK=o7Fpw}U;p~`2*Mq*!ofT}J^kCt%s{r(0%W~Q zrl3o)jZl^C=V5_jzx~v#tdf}}0BuJw3Hk>H;@>wkysMiF%$#WrYBioy>?4kQDBQe>0#QA`xYz|6 z?Nv()+jzOTimh$ofbL^wXXm`T(cWtlG*6#CU7l$a!zUmB^&NS?+3$wmxx-{&FW($V z!>G6aBIOTq`d|4mFrW-N8==5*a&ogn{m-Fn%ga9`?#})E8$&rwGz<(-#;-Y$Hx{nHcxsgAzK0d#4-Py7ea8B(qiO|Zj&`r zYWO9;gdCkG)8{5yT3Y;eMqs@aH8oB>Kdvf*ONAO48L{}3a#{Szz7SHXLDFbsOQw34 zc-;k230(P(%dZ7+Y~p6=OFWha{8UC{{P#lh@~owbTmq`$81P`QTTsg(iMB@Xnp=Wg z1L7Vxt75)9^5_O2ahcffEg&JH4N8Nfqj#1f8!V%8Vq$Jf6^Y3rwvw%lO&Dy1JZF-{ z>R4%-T#zh?S}SX7UlspH6t{QTWOCC|Q^PVcm_uGBsdD-8&=C+2yuUg`hYyR64F=vS z>M~1iXJ=Pp)@bNI{<-z<$)<9R`aiWLc{Eh@QVYCR17X7eAmv2t@N@B$&g(Ov)40=rP$d|+j3%MDf@#fFQM)66oh z%%e;qTUcNMMv=P0FygXNI)`tLr4hawLmeWHA03hs?ORLjpfh(Nx*V$W}^A}g2 zJ$t4s_Z0+6^Tl7m!6^BwLn4=uhcTD+3){dsrQ_E4SJXkt0bY?m+4tkuuV0ConZ;ti z-A)dFE3+T`Tv@?*E6o6~Bpbc9Q-8=R&Y>)D=@Ox?t}ak-C5Y3Ozo-75%U|SOZaFehWt!uVi6f}7csBjfLSwC9wa z_p9gXkcYebuu0B%g;IiHAetS{Mq9i+9Qer@8Gs*A-@e_;zMRt3)TFGf?&sr! zequ#_B4wTR5W-JHA4vDwnqzY^aO+veT@hJXS--DWks!^^HajBZ@q>fnKm`LVhiGdG z3J8eSx2ke=R1L}&*GU$rB(POfR@yJ^{sh;7dfNzA^|8Nyq*3}5YN1elEAXr1lat?t zRt=9=m0xt%U4uobRtb0O7#bFOR|~x8gFOV{0aRysvZ`p-AT}X^DqA{09S$hbuoJCl zX_-4abST(*yggB~6wXGzwy~krb$SN5ELsHE|MF$IcXdv7_RR9=OK>T^WSUcvljj%9 zu`3X25ziOSpI%9j{W9-%n^o%ZuLro(D-bN}Svhhri^!q(-kUk?1@WyXdnS1=SB48> z+NCbYe%4c|QI4yAc0I));Asyei3@y!!$E?)!t1Imnr7*J%mcOZRKkb=sWDn zN)&vuo_uE`>7Z7Azd1TB4UM=p0NY481@ohsJBSJ>S7Gt|<(mmP(gD-@!Q@vTH|cp0 zCZ9@?kT*Q$=?F>2oHdttXmDxZ1hWft=F7wD6P4CykB^Tpyyz3Q8(=ZGb7!9Y?|y{n z1MnO{nbx|@#XF3baY#xs4-5?WEX0X@<74Jfx{%}hlZl)~oEq?M#MwGFx4PP3^P!o* zi@bO5^p5OU4N=o5&*Qn(pded4B1rXHBr!tYf~s}3ilns~wQ?Q7&Y7xp>V%j_p^y*n zZ2Dg5@K&oV0#`;5@m*CF#c4c*AC$jGL53Iu+}>W{MOTG>7He|Q2!1?Ti|Otoqhm~` zi4@fZY&$Q|v8>_@K;VD2j3gb=M|1SO5L5wgIq>wr4%G(4Iad$k3NFsNY;SKb`q~3x z0G9dh-(UZc0s$5WX5q3^5%fCH+mE})(@tNm++^}J4?+nwT~J<-pZ+>Cnuj4Wm_8a4k-Y#XgSu@SBu>Sq!yacvJZK$2# z*;UIig3kwjOPaVF=gXy@mV7 zhr`B_l>aKfu9;aJlB0A*uxZdIQg(9_hVuWoC2!txkm`lMzx3MsNqt@*29I`a#V8=z z3bVfo2ncld3AV=K5fE7b`>m+h|7)iz{h9t#-otaG96!`3ZCE=(3FuHai#}vlk=`oM zvwtBeYq~)i)`2}`U|;}BrD$Ps0m|=qiIha`Bz<&d=6NuZGOAs4b!iZSe|mCc4R9^F zNPz;?XjJRS1TCX-v&M92ngj*?<|1PGm!ZUm^D}coJ-ba?&5GL9QH$A zYpWb^g~|uLv>zAU!Ae?KNRV^hzNR1vM}Pf}zgbi-Iz%Bi*i;}w5Az|Egf8Q^??$nX z*ZQ|GaCOf=*_28$7HWM(?E;=qYC2#i#2ome z&WzjDso_julXXduK=Um=T-JY>ybep&G%pe0d4rqd!w8g~n|!IF(2mo#dUTzco{_P$ zw-;k9_R_xoOlw8OmmgYz!xN!V-P{^Z!lK=1K0ZDG0*v=bf9IK?GtQQlc!GlEMW;GV<_}*{*P_PPmW|ypp-0stGVLnV8hH!z@=mXQIE%QNRH zsdPPy_u{A2Fb0%KB1itpd`PNJ2tF@(5Va1#WJl zLFpZG7FJf_z+<13=g-~Mr=K#=^qF>|{~xN00bG*1K*@OMoZ(9Rik^ic00q zom8)vb+dkCP`wc5rKhj2Yib(nwvcmds%VW}TnziYI=%qb-u}VTOT+_@Ws_kBQGg1S zsU>0*y#FDdRq9Kfi+T3C0=o$a9$h`X*8v4ZMGkWd*F-cHO(8Dz7h)vbI<#|nkFikh&w>{chUr5BQfDJ8-;=getRd~;WWo&GW zUqC?XU;H3w17or8ifv4NeSHNK!(py&LtWi=AVdRz0+X-gLnb=H$e64<{`#B1evy%q zUfX9YuS}nz@c~N&^5;ytM&XPtL>V|gRd$2!8e;6)?4_;EJ_H8qNzoxMFyv(yl=Yu~`WtExt-#a@M@2HhoTjnY`yyecNfn3I#U zJXU%hx=7*Ap5dd8ySeupX++1@r|NPIi|0WBSAo093WX|5uSPaZ0S`sgGl|Qget4ak z|6VmQ(&_<)LX2&n07nbEwpLPAp4bifdLuM66poSzIt)lp1*&CYQWD2<<@#JJ`wKzy z?yeZd-?Mawb0)yS;C8~GprDkL6pPOpH^6-gF%?)66|K7B%E>P*ES%W4DCrSb*fuxE z&cwt73>z5J1>4UVVSwMWmG|;1DyShBl%T@`nc>@G{}}AdFQ@Na=32l{Rz~c3u#qMv zC*67D0X&q1j zpk9Bi3FcQ$PE5274pPD1_5eg9P8hfo24AcjJy&Qyefoqu-Wlxt^oi8a&`?!VvkNv8 z1PUSa1>564Qv*Xsj5S|h-|5rY{<1bFN5?O%K|x%XF4@=FUuTBj99&$X*6+p^S69hk zM%iv}4fvoNI=kG{i|)*HX^Gn3_sNnCUptkOg3KFAC>3UoBLe68 z8QT=J>D*xNA3S*Q&|I(yhYu4eJgL&@u~oIPxrA6lpc1!^{2i*k<|#WuSZMGwRDlAi zG!HxoYu@TU-#~?T_P)VEJFuv1p#Flgu1x09zSa$Ct0L1x8YXO+unw?d3+*m` zIk}f7`wIyg+{waF2Vlp$(F-D?Rs62q=&2@Ok)5qAOHonLwSpJ-t^TzsF_Zt#zT)XT@fdn>5DY?Kk%)RO>p1y} zaIaO${2Ib%+@9dp%=jSr_}2%>1S5F$WWHLm(f#{5djV|Lc}GyCB5$V8umCYhBLx=w jKmV8G|L60YzXZIEYBX);*=OM2XHaUlw3NyeES~)r_#Itm diff --git a/dev/tutorial/02_small_network/index.html b/dev/tutorial/02_small_network/index.html index b35a1df6..91efc1f7 100644 --- a/dev/tutorial/02_small_network/index.html +++ b/dev/tutorial/02_small_network/index.html @@ -953,7 +953,7 @@

Network simulations in JaxleyDefine the network

First, we define a cell as you saw in the previous tutorial.

comp = jx.Compartment()
-branch = jx.Branch(comp, nseg=4)
+branch = jx.Branch(comp, ncomp=4)
 cell = jx.Cell(branch, parents=[-1, 0, 0, 1, 1, 2, 2])
 

We can assemble multiple cells into a network by using jx.Network, which takes a list of jx.Cells. Here, we assemble 11 cells into a network:

@@ -1029,11 +1029,11 @@

Inspecting and changing syn

0 0 0307286 IonotropicSynapse 0 0.1250.8750.625 0.0001 0.0 0.0251 1 28303298 IonotropicSynapse 0 0.1250.8750.625 0.0001 0.0 0.0252 2 56280286 IonotropicSynapse 0 0.1250.1250.625 0.0001 0.0 0.0253 3 84281295 IonotropicSynapse 0 0.1250.3750.875 0.0001 0.0 0.0254 4 112306302 IonotropicSynapse 0 0.1255 5 140298288 IonotropicSynapse 0 0.1250.6250.125 0.0001 0.0 0.0256 6 168301287 IonotropicSynapse 0 0.1250.3750.875 0.0001 0.0 0.0257 7 196293305 IonotropicSynapse 0 0.1258 8 224300299 IonotropicSynapse 0 0.1250.1250.875 0.0001 0.0 0.0259 9 252303284 IonotropicSynapse 0 0.1250.8750.125 0.0001 0.0 0.025
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
comp_indexbranch_indexcell_indexlengthradiusaxial_resistivitycapacitancevNaNa_gNa...K_gKeKK_nLeakLeak_gLeakLeak_eLeakglobal_comp_indexglobal_branch_indexglobal_cell_indexcontrolled_by_param
221010.00.35000.01.0-70.0True0.05...0.005-90.00.2True0.0001-70.02100
331010.00.35000.01.0-70.0True0.05...0.005-90.00.2True0.0001-70.03100
663010.00.35000.01.0-70.0True0.05...0.005-90.00.2True0.0001-70.06300
773010.00.35000.01.0-70.0True0.05...0.005-90.00.2True0.0001-70.07300
10105110.00.35000.01.0-70.0True0.05...0.005-90.00.2True0.0001-70.010510
11115110.00.35000.01.0-70.0True0.05...0.005-90.00.2True0.0001-70.011510
14147110.00.35000.01.0-70.0True0.05...0.005-90.00.2True0.0001-70.014710
15157110.00.35000.01.0-70.0True0.05...0.005-90.00.2True0.0001-70.015710
18189210.00.35000.01.0-70.0True0.05...0.005-90.00.2True0.0001-70.018920
19199210.00.35000.01.0-70.0True0.05...0.005-90.00.2True0.0001-70.019920
222211210.00.35000.01.0-70.0True0.05...0.005-90.00.2True0.0001-70.0221120
232311210.00.35000.01.0-70.0True0.05...0.005-90.00.2True0.0001-70.0231120
-

12 rows × 25 columns

-
+
View with 3 different channels. Use `.nodes` for details.
+

Group: fast spiking

Similarly, you could define a group of fast-spiking cells. Assume that the first and second cell are fast-spiking:

@@ -1356,436 +1020,8 @@

Group: fast spiking
network.fast_spiking.view
 

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
comp_indexbranch_indexcell_indexlengthradiusaxial_resistivitycapacitancevNaNa_gNa...K_gKeKK_nLeakLeak_gLeakLeak_eLeakglobal_comp_indexglobal_branch_indexglobal_cell_indexcontrolled_by_param
000010.01.05000.01.0-70.0True0.4...0.005-90.00.2True0.0001-70.00000
110010.01.05000.01.0-70.0True0.4...0.005-90.00.2True0.0001-70.01000
221010.00.35000.01.0-70.0True0.4...0.005-90.00.2True0.0001-70.02100
331010.00.35000.01.0-70.0True0.4...0.005-90.00.2True0.0001-70.03100
442010.01.05000.01.0-70.0True0.4...0.005-90.00.2True0.0001-70.04200
552010.01.05000.01.0-70.0True0.4...0.005-90.00.2True0.0001-70.05200
663010.00.35000.01.0-70.0True0.4...0.005-90.00.2True0.0001-70.06300
773010.00.35000.01.0-70.0True0.4...0.005-90.00.2True0.0001-70.07300
884110.01.05000.01.0-70.0True0.4...0.005-90.00.2True0.0001-70.08410
994110.01.05000.01.0-70.0True0.4...0.005-90.00.2True0.0001-70.09410
10105110.00.35000.01.0-70.0True0.4...0.005-90.00.2True0.0001-70.010510
11115110.00.35000.01.0-70.0True0.4...0.005-90.00.2True0.0001-70.011510
12126110.01.05000.01.0-70.0True0.4...0.005-90.00.2True0.0001-70.012610
13136110.01.05000.01.0-70.0True0.4...0.005-90.00.2True0.0001-70.013610
14147110.00.35000.01.0-70.0True0.4...0.005-90.00.2True0.0001-70.014710
15157110.00.35000.01.0-70.0True0.4...0.005-90.00.2True0.0001-70.015710
-

16 rows × 25 columns

-
+
View with 3 different channels. Use `.nodes` for details.
+

Groups from SWC files

If you are reading .swc morphologigies, you can automatically assign groups with diff --git a/dev/tutorial/07_gradient_descent/index.html b/dev/tutorial/07_gradient_descent/index.html index 25c93c00..ce9f76bf 100644 --- a/dev/tutorial/07_gradient_descent/index.html +++ b/dev/tutorial/07_gradient_descent/index.html @@ -1149,7 +1149,7 @@

Training biophysical models
_ = np.random.seed(0)  # For synaptic locations.
 
 comp = jx.Compartment()
-branch = jx.Branch(comp, nseg=2)
+branch = jx.Branch(comp, ncomp=2)
 cell = jx.Cell(branch, parents=[-1, 0, 0])
 net = jx.Network([cell for _ in range(3)])
 
diff --git a/dev/tutorial/08_importing_morphologies/index.html b/dev/tutorial/08_importing_morphologies/index.html
index 19d20f66..d162f878 100644
--- a/dev/tutorial/08_importing_morphologies/index.html
+++ b/dev/tutorial/08_importing_morphologies/index.html
@@ -822,7 +822,8 @@ 

Working with morphologies
import jaxley as jx
 
-cell = jx.read_swc("my_cell.swc", nseg=4, assign_groups=True)
+cell = jx.read_swc("my_cell.swc", ncomp=4)
+cell.branch(2).set_ncomp(2)  # Modify the number of compartments of a branch.
 

To work with more complicated morphologies, Jaxley supports importing morphological reconstructions via .swc files. .swc is currently the only supported format. Other formats like .asc need to be converted to .swc first, for example using the BlueBrain’s morph-tool. For more information on the exact specifications of .swc see here.

@@ -834,7 +835,7 @@

Working with morphologies
# import swc file into jx.Cell object
 fname = "data/morph.swc"
-cell = jx.read_swc(fname, nseg=8, max_branch_len=2000.0, assign_groups=True)
+cell = jx.read_swc(fname, ncomp=8)  # Use four compartments per branch.
 
 # print shape (num_branches, num_comps)
 print(cell.shape)
@@ -976,7 +977,7 @@ 

Working with morphologies
cell = jx.read_swc(fname, nseg=2, max_branch_len=2000.0, assign_groups=True)
+
+

As you can see below, branch 0 has two compartments (because this is what was passed to jx.read_swc(..., ncomp=2)), but branch 1 has four compartments:

+
cell.branch([0, 1]).nodes
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
local_cell_indexlocal_branch_indexlocal_comp_indexlengthradiusaxial_resistivitycapacitancevglobal_cell_indexglobal_branch_indexglobal_comp_indexcontrolled_by_param
00000.0500008.1190005000.01.0-70.00000
10010.0500008.1190005000.01.0-70.00010
20103.1207797.8061725000.01.0-70.00121
30113.1207797.1112315000.01.0-70.00131
40123.1207795.6523945000.01.0-70.00141
50133.1207793.8692475000.01.0-70.00151
+
+

Once imported the compartmentalized morphology can be viewed using vis.

-

png

+

png

vis can be called on any jx.Module and every View of the module. This means we can also for example use vis to highlight each branch. This can be done by iterating over each branch index and calling cell.branch(i).vis(). Within the loop.

-

png

+

png

While we only use two compartments to approximate each branch in this example, we can see the morphology is still plotted in great detail. This is because we always plot the full .swc reconstruction irrespective of the number of compartments used. The morphology lives seperately in the cell.xyzr attribute in a per branch fashion.

In addition to plotting the full morphology of the cell using points vis(type="scatter") or lines vis(type="line"), Jaxley also supports plotting a detailed morphological vis(type="morph") or approximate compartmental reconstruction vis(type="comp") that correctly considers the thickness of the neurite. Note that "comp" plots the lengths of each compartment which is equal to the length of the traced neurite. While neurites can be zigzaggy, the compartments that approximate them are straight lines. This can lead to miss-aligment of the compartment ends. For details see the documentation of vis.

The morphologies can either be projected onto 2D or also rendered in 3D.

@@ -1148,7 +1282,7 @@

Working with morphologiesfig.suptitle("Comparison of plot types") plt.show()

-

png

+

png

# set to interactive mode
 # %matplotlib notebook
 
@@ -1159,7 +1293,7 @@

Working with morphologiesax.view_init(elev=20, azim=5) plt.show()

-

png

+

png

Since Jaxley supports grouping different branches or compartments together, we can also use the id labels provided by the .swc file to assign group labels to the jx.Cell object.

-

png

+

png

To build a network of morphologically detailed cells, we can now connect several reconstructed cells together and also visualize the network. However, since all cells are going to have the same center, Jaxley will naively plot all of them on top of each other. To seperate out the cells, we therefore have to move them to a new location first.

-

png

+

png

Congrats! You have now learned how to vizualize and build networks out of very complex morphologies. To simulate this network, you can follow the steps in the tutorial on how to build a network.

diff --git a/dev/tutorial/08_importing_morphologies_files/08_importing_morphologies_11_0.png b/dev/tutorial/08_importing_morphologies_files/08_importing_morphologies_11_0.png index ed8f3ed52ce929fdfe82413b1f106537cbb52d3f..32835187cc6250497577443aee4be840cf5d9071 100644 GIT binary patch literal 23037 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 AUH||9 diff --git a/dev/tutorial/08_importing_morphologies_files/08_importing_morphologies_13_0.png b/dev/tutorial/08_importing_morphologies_files/08_importing_morphologies_13_0.png index 5bbe5f236b36650ca9c97f218491097065bc4199..c00bad6d5ef675f34722d4ef8ae73df5a3a2f465 100644 GIT binary patch literal 55950 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�uLSx3#dckW`!==Mef3T==8a^;{PgT3n|pT& zp2mSF@Lx&qH;ZBj$j8bcGiK2>e*Nwp8@mO(x_vuRAO>aEL{TWEadWDQE?>bVEfYQo z3F+d@bjw9YcDX9k(QGjm|C%Elme>;&T=YRG5I$^rt!rVovCcRjo14ZUIk<4k2jiq@ zjaW>b^{wm~pC$Pb3ksim3hER+cRrSYhe5rkpEA-UZhmtDj+3W#2H{^*2fZEnM6Z(t za-bw^o)4(IE%2_y+vE!*fIq~+&?f@UrW+x;@HxkyXCh#g{N?60C%@uNDW8TBNT60> z%ljYBS9Q941!rbvM6P<^VB4#yTnT{_Emg&arlWH@M5}IOu6J3CN>%&@ zAW{8W5}diSSpo0&27ePaALC~5+PpUzij*kD7oC@!q5@8@5qIlSS4V1x(mYVjNxpI~ zztsx8ovu;eM%%PHrR^}M7?7>NAd~RM|2fQ-a)B)h{{N(jkv2Nc;)B#o@2Mx-y<5k1 zPgcpCwLb(xlL%Td=_s}0_IX?nrEC3C8ZGXLt=1Qy4W;3*iYZAJwh@3&168&sgt|;`0mMt9d@v0_hL@v&5)pWYR z$tSV_F{br-(NXlzg@h^@l11&#?^7*3Z9t!MQRj`xQUaY^-?5ksHw*#HPq%#+>d4{l zML_o_OV`z0+eV550NOILx^CvXdofj0vu6}MK5$1M4)5<}1SGu2F*ie5%8rg7nm7I1 zKkstt)c}CJB%mOuakIkVXb*^ac%~3G|76KD)j~BO1*fKIm74Y&9*&dx@+&P#NFpW%$aAg!6`LnXM-Uw?YCx3t#3wp9Gxh@js56w`fW zztT|ydGM!Ox!q?+$@%#2gqVb%irsgWw4piE!Ht7{PA)L0{G0fCees2tjQcueZoq{U zh_>TnZ|BxMW%xtw4kLq70})$n)AJH;qJ?k(JRmSSaUQx%a~*u#0lxQd_U2rsv|SGB z>k=!RvHAxL?8FOFZ>q{>6k5TOkwb%#d+7`45%RN)97nISq|Nclr{5X?d`+kG^|}tP z0Yc>w$Gr{ZuI|3oJ#SZBc3Os$t8{jwS=IEeKq_h7?9gFB-I0i>Xw%_Wr?HJMexjm^ zjiX>e4?blk7x+$?=~~}NOrk?_Y7-Pf)@g1OR$RuHceF!7@*yFug+Tv&-_y2%qDc66 zM>9)o@L7UoyzuTZm`yE_^ZR{MqijKGiA#;g))N$JdiC$gSC5f+4iyoHFHC^9=9(PuPW z@?dk&83&h8aq1x1fY$ zVpLLJ`O_!QR2V-rG`zD%d_hb}@Q@CS)V|u`+L~>Cetyq@kKzLjYrdC~?~ik3N(F@6 z_Qcc4GH>I=nG6GtZ~jn|cTKz~=~9ufRs*hhcKIAM00 z;2WFjP$LE=hxTPCFE}`dk^B+gT`K<_@oOaouN>5G1AMB@R}xGJWiLlocCENjGBXc* z%$WCOdo2<-UehTRtXPo6$H-9KPj&u8S;hqlHqb7ftnXE>N2A=8$zK%^WGn7mI!_+Q zGRCk-mW79jQZbEPe1O!hjhO$atllkY7=P{qV@VOT`~slZYeb}^HUmlb$e5+;Ju=>p zY%RGwDw6(}A|HKWMvkO&Q?2u?cK1#@7Ru>K!NOJgGRCR`>0K`!k* zZ~P^{xt_QfEi^u}xRNAf5~eO()(!sXbUgZ2|H@@n_CX~}hSSdPf&kfMWi>tI@t74v z2@$k{7N@cES`2>v*S@Q|Ed?zvKij|i-U(__A9nulsG)uOsTevq_%;oVvbVSQ*2t}& z9q(tS6Vp#rP78CSEz{VD8~{LqKjipV$NznJZ7cVH8>9Bq9ds|Au!|Y&ypOwH%k`u4 zJmN&#KL|KxUCcFjD1DGwSm_R(egEVpcOr4-u;;+bbYoLhRo=XMF+%;nc9W3u zj*v$rK`t-zO}B5nlSE+|?x=tW`j4ObgF?(F+TeR;&7=&jxjwCOFhY%LNnix2`TD$QIyx&OG^9m*aFCE zU%$3SBg?sDWHJW@S~LMh@{|V<{D`&9nU(%lzf+9Jciq2%q7L`Y;;}z8bOc*x7+arW z8^qQI_z{UbEzHmzo5Yqh98e)3q^#pVZTP z5mkc42e1H?hP1_VC%?9~*24>{_?EzR=fjNWP9~`F(SS0FSg0ErKkf|}JJ?eB+NG`g z)FW+kdouNd&f{sgpCjqP_xn8L4~iiSm4H9e{-oz!aK0pJblnh0g?UMZ8BNfyfWG!l0Ib1LHL{QlQtZuyJ>XvSl7`4+C>hwq9_~ zxl-9oR#)Y}4n7$I8Y!@N1pmH!b|*s1lJ?dD;+A~@A}sUxPB^K?%8Jm$3oKPb_Z zMGlD}O=`p*fVWgFu(x(V9q{#9q<6;M3miV2XGvK5yyN)k*`kM7+S!{0i^+FUa11x> zjGbWrQk0;(pXUL%9wFq6AwWZZg>f!*u!3?4toOVL#w1;iVJRAdV`qDPldu)b8LG?U zRfJi`a_jDfr+4W+i7PSTyHQ|gqQzm-R=rX5*L?+p zMkl}us-Y@C8=N}SWBNGMbsV8*8PK+x!`1I?E_YaU&tb0C`WnF_P1|wubKRWPMgD}f zPLxd4=i1%%8H;BhZzxC)86Q#{&xq ziz5iY6hRzQeb7E9e__t7{x%R7%DOS?a4(spUJ)hG49(7?yuT$cr}^v15F{D<`SfTL zx6}IpcE(-Kf}P{vDA)mgreYe#+3x{2&KAfS9mSwS$hfd^AxKELG9(bm7;eHGqeqXk z+_RZupPcP1;cV^%6aNVY^Sea|MrDM?;S}kv; z^n*^4?_i836n>poCr=@v&8U*F#^`WWCgbN;-{hta{9(xH+x{`ZY=NqYMbgn`q&;Y1 ztzC=QvO+*CzZthuNX&>Rt{@Bk=qCT%x9$G=w{{zB5QU4+StK=o`N->Ry;2TNTb)cR zD(a-^?{yH19cEKl1GBoGa@ zc(bLMA!u~RqvO&y_|_9TS%wMF`+MyYulr@nS|F)e>{n9YAt86-F$?PTo~(+fpL-6B zB5LZ*Ip|}4UA`{d`Ez<1WRlKqGVekOCznhD7Qvyb*4a~tz)VG2YJl1+eAc=R4f;Oy z7tQp+oeDp^%5_wffo2G0#*#9Xj$5#HX!xvL(;Zzq9q6g8pZPNW)LyU4uJ#)k8L3rl zX-33xinbX&y%`F)~X=YX4LMxw6034iJGtd|$+Y>@1!XO5gla*=;byi_OGBKclU z7+W0#FqzF8qU3k(iy>!AVDnK;UzUPXCIz$ZNs_a{w6;m^5z&I;|JCiE@f=CeMwZzX zwjZ?eJb+;T)nnmK%b!`4T+ZH0_q6!Z_`75j;&BjbHu%Ix#z9-JL`#RF)_yJBw&>3- zd?g|>{aQqJp9QrvYhFgJC4zw&LC-ZJf4|~1Kyh&h?mYZbPhGE=U#>|hzzNQa9e7*% z&zR#RUfg|(6B?I0IgXl{Y>_3KO(OkNJ^rmxS|Et*?L|fI&ViaiW_BSWr-{qj(dGTG zg6^cBCoNyWO>=c$u1``2aUiq={}N8tbx* zEW}7b^R<*oi6G?F7m^h9|5?Pql##dT*;L~}({HB!y2bn|w1t#=%RLcYv2!C=NlpYy-k z$DG3vC{Ix2lL3p$J(#!Y4}smXr@Bx8of zlUPytrJKiyn>LLxu75?>y>yu(+%)4rQD}9Q53zErcPK8HK)YAM&aUeBliB^e*1Nha zUViQIax$hvccg=$Pz1W)m44wQ+QK{d+_o=*3QB$voawrO(QK_^b=~-xdfk{3?ZyLc zYxdclq{Wx~G4p0+A-7g1RB~<8`!D-12XHPq%yB_A*-nPhYXwFIcXl~>K6wN2{h zFJIr$3}pY3up?kN*q7YwCl+?mO^Z)TwjA5BS6)lK9R1YNSxoiP{U4-pGu95;k<5_|JDwei>i7^2 zcq6BD61^^lbG@Ete$Z!~^3jWqM`sZ2yh*QfgVh+@vJZ6ue)xovk5^q!`)rfc@pbz< z5yf~A76&%i^yM9f0{KwT^)k119%Yazxw8Iy8~`@W&s9SiwxOzNUzF8s%{bI99_>RE7^w+$=1+#!lQI( zle0Y;{uBa2k0OwhOoXRqg=eIMre;k|CFI{tl>-e6SJex;{&RNKZQ4Z+ZUrK^3a|u< zJU?`i+C_@zxWM3*?0N;<>gC+Z<=kM~c~$`mYGY2i5%SNN;VE`Z_gOSQcn(lV=$b#x z|5#Y$@#?Q|jUT*mfAKD!%Kr8*gyMt80L`nZUTR8Oh$^^M(Ek9*=<@LZ literal 55297 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 F{{ZIAJP7~* literal 0 HcmV?d00001 diff --git a/dev/tutorial/08_importing_morphologies_files/08_importing_morphologies_15_1.png b/dev/tutorial/08_importing_morphologies_files/08_importing_morphologies_15_1.png deleted file mode 100644 index 72c78f0fe81bdbc940b5760cdbf22065e72aa058..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 52550 zcmc$`Ralk#7e9yviiC7X7?gBK*AWz?L20B*x;vFnl#p%^5s>aqrMtUx)7?31|K|V9 z-CWGoT=;knZuj2r`>yX=pIQdU%Zg*7lcJ-bpkPY8efrM3K?8w>G!3H#gCH;%I1VXJTc^!OX+V&iLe`y}h*^9}A1c|MLQ7 zD_dihCl}XE@Fowe->TW6pkV7Ff8F{coMD23(p4<+`jw({!e*j_yV5dc>umYRv}VCy z;?(NuS9k9!;3(kV#eb+#Sk2ndlzg9#`YH8vqP|biU#^j9kHyRDitPr&JE&M(b1oNE zEIWc{3Duov35a-fi7=|Ww?F;&{)m#YH;{zm{(}d9J37!A7#QL>%~}rE1_Qo+{j)Rw;I0!n zx`KwZb#<}hmU>Ij<1gyDD}T`_e;am2(M3xBN=-f6$gi)kp1-}fw|9DRF?u3@?{hWk zQ|8Z;aR-Rr!6HMc%gf8LDhGz7q$HXR2R?E;M9=b!+fMW7XnBi%L_~y+g@p%$Kf|Lb z&ilnNb7d>xazZJ;fBR~CU6sZap!^v&P4YZ9QB$RGS;8?lH|O!X77+Ev_&YKZg+}TA zi1l07L9cK~em={}%F0WV0pDJsi$JO5dR$ywc{=>d9j}{gM`k~fzZzDx)Lxz&x#jTF z*W%(Fj+?5Ro14~-jz%k8u@TYH@tf5y2S!y68~#2%ceuE?kZ+};?NN$V_w>_GPx;Xk zl)ImA{!NscnM_r%k@Gv|90~Cy27kJPHjt~qEp)NTk&~12gDL>$xmbb6g(HRACYySx zX|wRnWn+$7@mmRr`A(Mlnt8Tha{ef9Zxo@+T~RDDo`z9vFBvT@iid=ZC{O6=zv(vm z_y2nHaK;5e-qqdB>$FYgcCuYNZ-;*ODCiNk4pzDqm6eNh8&N-e_+Yt8cKem6XiW3n zawb1NKa{btF-48hY!#Ldnwra_PO~1yMc3`L$uG}pJg;08=?Ub7uS(kY5y@T@5@BNn zx{a72cISs{G|9=X>o3j{P_VGDycOm!P#ZCm#h-k93xzsWHa0PFTOK~jVj5fcvSFn= zUai7fx75M3l$4T^GTe}e`9nizr<9NpK}t%>{`q5S9-gr!yHv3tdHP5yDk>CK%PF2J z$E}W~_HY{~C&cx|j`7++n#jky<(&T*X(}uV%gb8_2gBRi+DtdH+*y^lccs|4xy3VU z3K8^vA_|MPwOU?{tpz0|2BUeJ=CgH)4jUtdHj83WjA|muLLOr!CV2Fb>;5hDkA`UV zR-RSllJMEnA$k%Q`%*-2)_AV3uYaefzf)8+C=%e~Q`f?tTV1_R?7HE=#eq{u z;c=+I&d#2dn{Pon`s1l+-^P*q#a1~@XZ_{A6h8VrA2e*#hMpcQA|fKktqH@T>8VP) zeCN&1E5TX!159uCuBoXh(~A<*VRTkh%KLZApCj)^?gK0=qrHXZKuX~x8kx8@lJhVs z960|A&EFO1BZu7?qdDA9tzA#HyUp@z#Y9AIGij6=45rKEXw}9UC%Lpxy))lmYIi^E z7ne>Fh=tEBG#h2^NfLBx)^BZX<z%B~>{kwCCPBXf2b1}&I z@V&ge*f}_2q1=RogzPs)axLrVKFujVhc<%(H#I!TeP1HeX0asO3hHd9t0bTmj&mJ71|I?nKdZ1l@<`e(=|Mtp-|NmYv zgd8t(l9vP*L=XHP9G+~**+s9=;eR`)2!rWE4TI?SIfd_k@_5*JUO&F(+()^+b%&(? z*k@4@2uN8U=s9j8#!nTH2*sLpOFa-+5na{>(&!> zA41miAgzs4(#5#oYy=MaU2bgTIZk@MCLVLxOJ&||KF(=LWNO&>y?~`Zs({6T(yS*} zS9~7}CA~bZCxY<3Z>F@h__Hv&d*y<+j&FJQ?`i*H%AEcC;g(qt=hfB4fkSe>OU{`- zio4SSzhlN`9wPL?U2ZhwXHXDH5#8Rr{o@BusLQXqFLp{Ls7!@tg(xxEU3-OlnFyT! z^FsdH!u-N03lA4ITΨ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 literal 31380 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 diff --git a/dev/tutorial/08_importing_morphologies_files/08_importing_morphologies_19_1.png b/dev/tutorial/08_importing_morphologies_files/08_importing_morphologies_19_1.png new file mode 100644 index 0000000000000000000000000000000000000000..a48db6a7daa8828954fd0e53b93d92fb2fde9444 GIT binary patch literal 51434 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 B0GR*) literal 0 HcmV?d00001 diff --git a/dev/tutorial/08_importing_morphologies_files/08_importing_morphologies_21_0.png b/dev/tutorial/08_importing_morphologies_files/08_importing_morphologies_21_0.png new file mode 100644 index 0000000000000000000000000000000000000000..38bf7005d109745271d8686420005aaf75f57d24 GIT binary patch literal 31380 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 literal 0 HcmV?d00001 diff --git a/dev/tutorial/08_importing_morphologies_files/08_importing_morphologies_7_0.png b/dev/tutorial/08_importing_morphologies_files/08_importing_morphologies_7_0.png deleted file mode 100644 index 2b14ac3863f1b9b126b7e7c0dde1c461929f0ae6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23037 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 diff --git a/dev/tutorial/08_importing_morphologies_files/08_importing_morphologies_9_0.png b/dev/tutorial/08_importing_morphologies_files/08_importing_morphologies_9_0.png deleted file mode 100644 index 753ba919a7004b922ac3a4a812d432b8e3e38a0d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 55895 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: Building the networkt_max = 10.0 comp = jx.Compartment() -branch = jx.Branch(comp, nseg=2) +branch = jx.Branch(comp, ncomp=2) cell = jx.Cell(branch, parents=[-1, 0]) net = jx.Network([cell for _ in range(6)]) fully_connect(net.cell([0, 1, 2]), net.cell([3, 4, 5]), IonotropicSynapse())