def __init__(self, n_electrodes, radius, x_center, y_center):
        """Electrodes arranged in a circle

        Electrodes will be named 'A0', 'A1', ...

        Parameters
        ----------
        n_electrodes : int
            how many electrodes to arrange in a circle
        radius : float
            the radius of the circle (microns)
        x_center, y_center : float
            the x,y coordinates of the center of the circle (microns),
            where (0,0) is the center of the fovea
        """
        # The job of the constructor is to create the electrodes. We start
        # with an empty collection:
        self.electrodes = coll.OrderedDict()
        # We then generate a number `n_electrodes` of electrodes, arranged on
        # the circumference of a circle:
        for n in range(n_electrodes):
            # Angular position of the electrode:
            ang = 2.0 * np.pi / n_electrodes * n
            # Create the disk electrode:
            electrode = DiskElectrode(x_center + np.cos(ang) * radius,
                                      y_center + np.sin(ang) * radius, 0, 100)
            # Add the electrode to the collection:
            self.add_electrode('A' + str(n), electrode)
def test_Nanduri2012Temporal():
    model = Nanduri2012Temporal()
    # User can set their own params:
    model.dt = 0.1
    npt.assert_equal(model.dt, 0.1)
    model.build(dt=1e-4)
    npt.assert_equal(model.dt, 1e-4)
    # User cannot add more model parameters:
    with pytest.raises(FreezeError):
        model.rho = 100

    # Nothing in, None out:
    npt.assert_equal(model.predict_percept(ArgusI().stim), None)

    # Zero in = zero out:
    implant = ArgusI(stim=np.zeros((16, 100)))
    percept = model.predict_percept(implant.stim, t_percept=[0, 1, 2])
    npt.assert_equal(isinstance(percept, Percept), True)
    npt.assert_equal(percept.shape, (16, 1, 3))
    npt.assert_almost_equal(percept.data, 0)

    # Can't request the same time more than once (this would break the Cython
    # loop, because `idx_frame` is incremented after a write; also doesn't
    # make much sense):
    with pytest.raises(ValueError):
        implant.stim = np.ones((16, 100))
        model.predict_percept(implant.stim, t_percept=[0.2, 0.2])

    # Brightness scales differently with amplitude vs frequency:
    model = Nanduri2012Temporal(dt=5e-3)
    model.build()
    sdur = 1000.0  # stimulus duration (ms)
    pdur = 0.45  # (ms)
    t_percept = np.arange(0, sdur, 5)
    implant = ProsthesisSystem(ElectrodeArray(DiskElectrode(0, 0, 0, 260)))
    bright_amp = []
    for amp in np.linspace(0, 50, 5):
        # implant.stim = PulseTrain(model.dt, freq=20, amp=amp, dur=sdur,
        #                           pulse_dur=pdur, interphase_dur=pdur)
        implant.stim = BiphasicPulseTrain(20, amp, pdur, interphase_dur=pdur,
                                          stim_dur=sdur)
        percept = model.predict_percept(implant.stim, t_percept=t_percept)
        bright_amp.append(percept.data.max())
    bright_amp_ref = [0.0, 0.00890, 0.0657, 0.1500, 0.1691]
    npt.assert_almost_equal(bright_amp, bright_amp_ref, decimal=3)

    bright_freq = []
    for freq in np.linspace(0, 100, 5):
        # implant.stim = PulseTrain(model.dt, freq=freq, amp=20, dur=sdur,
        #                           pulse_dur=pdur, interphase_dur=pdur)
        implant.stim = BiphasicPulseTrain(freq, 20, pdur, interphase_dur=pdur,
                                          stim_dur=sdur)
        percept = model.predict_percept(implant.stim, t_percept=t_percept)
        bright_freq.append(percept.data.max())
    bright_freq_ref = [0.0, 0.0394, 0.0741, 0.1073, 0.1385]
    npt.assert_almost_equal(bright_freq, bright_freq_ref, decimal=3)
Exemple #3
0
def test_Nanduri2012Spatial():
    # Nanduri2012Spatial automatically sets `atten_a`:
    model = Nanduri2012Spatial(engine='serial', xystep=5)

    # User can set `atten_a`:
    model.atten_a = 12345
    npt.assert_equal(model.atten_a, 12345)
    model.build(atten_a=987)
    npt.assert_equal(model.atten_a, 987)

    # Nothing in, None out:
    npt.assert_equal(model.predict_percept(ArgusI()), None)

    # Zero in = zero out:
    implant = ArgusI(stim=np.zeros(16))
    percept = model.predict_percept(implant)
    npt.assert_equal(isinstance(percept, Percept), True)
    npt.assert_equal(percept.shape, list(model.grid.x.shape) + [1])
    npt.assert_almost_equal(percept.data, 0)

    # Only works for DiskElectrode arrays:
    with pytest.raises(TypeError):
        implant = ProsthesisSystem(ElectrodeArray(PointSource(0, 0, 0)))
        implant.stim = 1
        model.predict_percept(implant)
    with pytest.raises(TypeError):
        implant = ProsthesisSystem(
            ElectrodeArray(
                [DiskElectrode(0, 0, 0, 100),
                 PointSource(100, 100, 0)]))
        implant.stim = [1, 1]
        model.predict_percept(implant)

    # Multiple frames are processed independently:
    model = Nanduri2012Spatial(engine='serial',
                               atten_a=14000,
                               xystep=5,
                               xrange=(-20, 20),
                               yrange=(-15, 15))
    model.build()
    percept = model.predict_percept(ArgusI(stim={'A1': [1, 2]}))
    npt.assert_equal(percept.shape, list(model.grid.x.shape) + [2])
    pmax = percept.data.max(axis=(0, 1))
    npt.assert_almost_equal(percept.data[2, 3, :], pmax)
    npt.assert_almost_equal(pmax[1] / pmax[0], 2.0)

    # Nanduri model uses a linear dva2ret conversion factor:
    for factor in [0.0, 1.0, 2.0]:
        npt.assert_almost_equal(model.retinotopy.dva2ret(factor, factor),
                                (280.0 * factor, 280.0 * factor))
    for factor in [0.0, 1.0, 2.0]:
        npt.assert_almost_equal(
            model.retinotopy.ret2dva(280.0 * factor, 280.0 * factor),
            (factor, factor))
def test_plot_implant_on_axon_map():
    ax = plot_implant_on_axon_map(ArgusII())
    npt.assert_equal(isinstance(ax, Subplot), True)

    # Check axis limits:
    for xlim, ylim in zip([None, (-2000, 1500)], [(-3000, 1300), None]):
        ax = plot_implant_on_axon_map(ArgusII(), xlim=xlim, ylim=ylim)
        if xlim is None:
            xlim = (-4000, 4500)
        if ylim is None:
            ylim = (-2500, 3000)
        npt.assert_almost_equal(ax.get_xlim(), xlim)
        npt.assert_almost_equal(ax.get_ylim(), ylim)

    # Check optic disc center in both eyes:
    model = AxonMapSpatial()
    for eye in ['RE', 'LE']:
        for loc_od in [(15.5, 1.5), (17.9, -0.01)]:
            od = (-loc_od[0], loc_od[1]) if eye == 'LE' else loc_od
            ax = plot_implant_on_axon_map(ArgusII(eye=eye), loc_od=od)
            npt.assert_equal(len(ax.patches), 1)
            npt.assert_almost_equal(ax.patches[0].center, model.dva2ret(od))
            close(ax.figure)

    # Electrodes and quadrants can be annotated:
    for ann_el, n_el in [(True, 60), (False, 0)]:
        for ann_q, n_q in [(True, 4), (False, 0)]:
            ax = plot_implant_on_axon_map(ArgusII(),
                                          annotate_implant=ann_el,
                                          annotate_quadrants=ann_q)
            npt.assert_equal(len(ax.texts), n_el + n_q)
            npt.assert_equal(len(ax.collections[0]._paths), 60)
            close(ax.figure)

    # Stimulating electrodes are marked:
    ax = plot_implant_on_axon_map(ArgusII(stim=np.ones(60)))

    # Setting upside_down flips y axis:
    ax = plot_implant_on_axon_map(ArgusII(), upside_down=True)
    npt.assert_almost_equal(ax.get_xlim(), (-4000, 4500))
    npt.assert_almost_equal(ax.get_ylim(), (3000, -2500))

    with pytest.raises(TypeError):
        plot_implant_on_axon_map(DiskElectrode(0, 0, 0, 100))
    with pytest.raises(ValueError):
        plot_implant_on_axon_map(ArgusII(), n_bundles=0)
def test_ElectrodeArray_add_electrode():
    earray = ElectrodeArray([])
    npt.assert_equal(earray.n_electrodes, 0)

    with pytest.raises(TypeError):
        earray.add_electrode('A01', ElectrodeArray([]))

    # Add an electrode:
    key0 = 'A04'
    earray.add_electrode(key0, PointSource(0, 1, 2))
    npt.assert_equal(earray.n_electrodes, 1)
    # Both numeric and string index should work:
    for key in [key0, 0]:
        npt.assert_equal(isinstance(earray[key], PointSource), True)
        npt.assert_almost_equal(earray[key].x, 0)
        npt.assert_almost_equal(earray[key].y, 1)
        npt.assert_almost_equal(earray[key].z, 2)
    with pytest.raises(ValueError):
        # Can't add the same electrode twice:
        earray.add_electrode(key0, PointSource(0, 1, 2))

    # Add another electrode:
    key1 = 'A01'
    earray.add_electrode(key1, DiskElectrode(4, 5, 6, 7))
    npt.assert_equal(earray.n_electrodes, 2)
    # Both numeric and string index should work:
    for key in [key1, 1]:
        npt.assert_equal(isinstance(earray[key], DiskElectrode), True)
        npt.assert_almost_equal(earray[key].x, 4)
        npt.assert_almost_equal(earray[key].y, 5)
        npt.assert_almost_equal(earray[key].z, 6)
        npt.assert_almost_equal(earray[key].r, 7)

    # We can also get a list of electrodes:
    for keys in [[key0, key1], [0, key1], [key0, 1], [0, 1]]:
        selected = earray[keys]
        npt.assert_equal(isinstance(selected, list), True)
        npt.assert_equal(isinstance(selected[0], PointSource), True)
        npt.assert_equal(isinstance(selected[1], DiskElectrode), True)
def test_plot_implant_on_axon_map():
    fig, ax = plot_implant_on_axon_map(ArgusII())
    npt.assert_equal(isinstance(fig, Figure), True)
    npt.assert_equal(isinstance(ax, Subplot), True)

    # Check axis limits:
    xmin, xmax, ymin, ymax = dva2ret([-20, 20, -15, 15])
    npt.assert_equal(ax.get_xlim(), (xmin, xmax))
    npt.assert_equal(ax.get_ylim(), (ymin, ymax))

    # Check optic disc center in both eyes:
    for eye in ['RE', 'LE']:
        for loc_od in [(15.5, 1.5), (17.9, -0.01)]:
            od = (-loc_od[0], loc_od[1]) if eye == 'LE' else loc_od
            _, ax = plot_implant_on_axon_map(ArgusII(eye=eye), loc_od=od)
            npt.assert_equal(len(ax.patches), 1)
            npt.assert_almost_equal(ax.patches[0].center, dva2ret(od))

    # Electrodes and quadrants can be annotated:
    for ann_el, n_el in [(True, 60), (False, 0)]:
        for ann_q, n_q in [(True, 4), (False, 0)]:
            _, ax = plot_implant_on_axon_map(ArgusII(),
                                             annotate_implant=ann_el,
                                             annotate_quadrants=ann_q)
            npt.assert_equal(len(ax.texts), n_el + n_q)

    # Stimulating electrodes are marked:
    fig, ax = plot_implant_on_axon_map(ArgusII(stim=np.ones(60)))

    # Setting upside_down flips y axis:
    _, ax = plot_implant_on_axon_map(ArgusII(), upside_down=True)
    npt.assert_equal(ax.get_xlim(), (xmin, xmax))
    npt.assert_equal(ax.get_ylim(), (ymax, ymin))

    with pytest.raises(TypeError):
        plot_implant_on_axon_map(DiskElectrode(0, 0, 0, 100))
    with pytest.raises(ValueError):
        plot_implant_on_axon_map(ArgusII(), n_bundles=0)
stim.plot(time=(0, 60))

###############################################################################
# Creating an implant
# -------------------
#
# Before we can run the Nanduri model, we need to create a retinal implant to
# which we can assign the above pulse train.
#
# For the purpose of this exercise, we will create an
# :py:class:`~pulse2percept.implants.ElectrodeArray` consisting of a single
# :py:class:`~pulse2percept.implants.DiskElectrode` with radius=260um centered
# at (x,y) = (0,0); i.e., centered over the fovea:

from pulse2percept.implants import DiskElectrode, ElectrodeArray
earray = ElectrodeArray(DiskElectrode(0, 0, 0, 260))

###############################################################################
# Usually we would use a predefined retinal implant such as
# :py:class:`~pulse2percept.implants.ArgusII` or
# :py:class:`~pulse2percept.implants.AlphaIMS`. Alternatively, we can wrap the
# electrode array created above with a
# :py:class:`~pulse2percept.implants.ProsthesisSystem` to create our own
# retinal implant. We will also assign the above created stimulus to it:

from pulse2percept.implants import ProsthesisSystem
implant = ProsthesisSystem(earray, stim=stim)

###############################################################################
# Running the model
# -----------------
Exemple #8
0
def test_Nanduri2012Model_predict_percept():
    # Nothing in = nothing out:
    model = Nanduri2012Model(xrange=(0, 0), yrange=(0, 0), engine='serial')
    model.build()
    implant = ArgusI(stim=None)
    npt.assert_equal(model.predict_percept(implant), None)
    implant.stim = np.zeros(16)
    npt.assert_almost_equal(model.predict_percept(implant).data, 0)

    # Single-pixel model same as TemporalModel:
    implant = ProsthesisSystem(DiskElectrode(0, 0, 0, 100))
    # implant.stim = PulseTrain(5e-6)
    implant.stim = BiphasicPulseTrain(20, 20, 0.45, interphase_dur=0.45)
    t_percept = [0, 0.01, 1.0]
    percept = model.predict_percept(implant, t_percept=t_percept)
    temp = Nanduri2012Temporal().build()
    temp = temp.predict_percept(implant.stim, t_percept=t_percept)
    npt.assert_almost_equal(percept.data, temp.data, decimal=4)

    # Only works for DiskElectrode arrays:
    with pytest.raises(TypeError):
        implant = ProsthesisSystem(ElectrodeArray(PointSource(0, 0, 0)))
        implant.stim = 1
        model.predict_percept(implant)
    with pytest.raises(TypeError):
        implant = ProsthesisSystem(
            ElectrodeArray(
                [DiskElectrode(0, 0, 0, 100),
                 PointSource(100, 100, 0)]))
        implant.stim = [1, 1]
        model.predict_percept(implant)

    # Requested times must be multiples of model.dt:
    implant = ProsthesisSystem(ElectrodeArray(DiskElectrode(0, 0, 0, 260)))
    # implant.stim = PulseTrain(tsample)
    implant.stim = BiphasicPulseTrain(20, 20, 0.45)
    model.temporal.dt = 0.1
    with pytest.raises(ValueError):
        model.predict_percept(implant, t_percept=[0.01])
    with pytest.raises(ValueError):
        model.predict_percept(implant, t_percept=[0.01, 1.0])
    with pytest.raises(ValueError):
        model.predict_percept(implant, t_percept=np.arange(0, 0.5, 0.101))
    model.predict_percept(implant, t_percept=np.arange(0, 0.5, 1.0000001))

    # Can't request the same time more than once (this would break the Cython
    # loop, because `idx_frame` is incremented after a write; also doesn't
    # make much sense):
    with pytest.raises(ValueError):
        model.predict_percept(implant, t_percept=[0.2, 0.2])

    # It's ok to extrapolate beyond `stim` if the `extrapolate` flag is set:
    model.temporal.dt = 1e-2
    npt.assert_almost_equal(
        model.predict_percept(implant, t_percept=10000).data, 0)

    # Output shape must be determined by t_percept:
    npt.assert_equal(
        model.predict_percept(implant, t_percept=0).shape, (1, 1, 1))
    npt.assert_equal(
        model.predict_percept(implant, t_percept=[0, 1]).shape, (1, 1, 2))

    # Brightness vs. size (use values from Nanduri paper):
    model = Nanduri2012Model(xystep=0.5, xrange=(-4, 4), yrange=(-4, 4))
    model.build()
    implant = ProsthesisSystem(ElectrodeArray(DiskElectrode(0, 0, 0, 260)))
    amp_th = 30
    bright_th = 0.107
    stim_dur = 1000.0
    pdur = 0.45
    t_percept = np.arange(0, stim_dur, 5)
    amp_factors = [1, 6]
    frames_amp = []
    for amp_f in amp_factors:
        implant.stim = BiphasicPulseTrain(20,
                                          amp_f * amp_th,
                                          pdur,
                                          interphase_dur=pdur,
                                          stim_dur=stim_dur)
        percept = model.predict_percept(implant, t_percept=t_percept)
        idx_frame = np.argmax(np.max(percept.data, axis=(0, 1)))
        brightest_frame = percept.data[..., idx_frame]
        frames_amp.append(brightest_frame)
    npt.assert_equal([np.sum(f > bright_th) for f in frames_amp], [0, 161])
    freqs = [20, 120]
    frames_freq = []
    for freq in freqs:
        implant.stim = BiphasicPulseTrain(freq,
                                          1.25 * amp_th,
                                          pdur,
                                          interphase_dur=pdur,
                                          stim_dur=stim_dur)
        percept = model.predict_percept(implant, t_percept=t_percept)
        idx_frame = np.argmax(np.max(percept.data, axis=(0, 1)))
        brightest_frame = percept.data[..., idx_frame]
        frames_freq.append(brightest_frame)
    npt.assert_equal([np.sum(f > bright_th) for f in frames_freq], [21, 49])
def test_DiskElectrode():
    with pytest.raises(TypeError):
        DiskElectrode(0, 0, 0, [1, 2])
    with pytest.raises(TypeError):
        DiskElectrode(0, np.array([0, 1]), 0, 1)
    # Invalid radius:
    with pytest.raises(ValueError):
        DiskElectrode(0, 0, 0, -5)
    # Check params:
    electrode = DiskElectrode(0, 1, 2, 100)
    npt.assert_almost_equal(electrode.x, 0)
    npt.assert_almost_equal(electrode.y, 1)
    npt.assert_almost_equal(electrode.z, 2)
    # On the electrode surface (z=2, x^2+y^2<=100^2)
    npt.assert_almost_equal(electrode.electric_potential(0, 1, 2, 1), 1)
    npt.assert_almost_equal(electrode.electric_potential(30, -30, 2, 1), 1)
    npt.assert_almost_equal(electrode.electric_potential(0, 101, 2, 1), 1)
    npt.assert_almost_equal(electrode.electric_potential(0, -99, 2, 1), 1)
    npt.assert_almost_equal(electrode.electric_potential(100, 1, 2, 1), 1)
    npt.assert_almost_equal(electrode.electric_potential(-100, 1, 2, 1), 1)
    # Right off the surface (z=2, x^2+y^2>100^2)
    npt.assert_almost_equal(electrode.electric_potential(0, 102, 2, 1),
                            0.910,
                            decimal=3)
    npt.assert_almost_equal(electrode.electric_potential(0, -100, 2, 1),
                            0.910,
                            decimal=3)
    # Some distance away from the electrode (z>2):
    npt.assert_almost_equal(electrode.electric_potential(0, 1, 38, 1),
                            0.780,
                            decimal=3)
    # Slots:
    npt.assert_equal(hasattr(electrode, '__slots__'), True)
    npt.assert_equal(hasattr(electrode, '__dict__'), False)
    # Plots:
    ax = electrode.plot()
    npt.assert_equal(len(ax.texts), 0)
    npt.assert_equal(len(ax.patches), 1)
    npt.assert_equal(isinstance(ax.patches[0], Circle), True)
Exemple #10
0
def test_DiskElectrode():
    with pytest.raises(TypeError):
        DiskElectrode(0, 0, 0, [1, 2])
    with pytest.raises(TypeError):
        DiskElectrode(0, np.array([0, 1]), 0, 1)
    # Invalid radius:
    with pytest.raises(ValueError):
        DiskElectrode(0, 0, 0, -5)
    # Check params:
    electrode = DiskElectrode(0, 1, 2, 100)
    npt.assert_almost_equal(electrode.x, 0)
    npt.assert_almost_equal(electrode.y, 1)
    npt.assert_almost_equal(electrode.z, 2)
    # On the electrode surface (z=2, x^2+y^2<=100^2)
    npt.assert_almost_equal(electrode.electric_potential(0, 1, 2, 1), 1)
    npt.assert_almost_equal(electrode.electric_potential(30, -30, 2, 1), 1)
    npt.assert_almost_equal(electrode.electric_potential(0, 101, 2, 1), 1)
    npt.assert_almost_equal(electrode.electric_potential(0, -99, 2, 1), 1)
    npt.assert_almost_equal(electrode.electric_potential(100, 1, 2, 1), 1)
    npt.assert_almost_equal(electrode.electric_potential(-100, 1, 2, 1), 1)
    # Right off the surface (z=2, x^2+y^2>100^2)
    npt.assert_almost_equal(electrode.electric_potential(0, 102, 2, 1),
                            0.910,
                            decimal=3)
    npt.assert_almost_equal(electrode.electric_potential(0, -100, 2, 1),
                            0.910,
                            decimal=3)
    # Some distance away from the electrode (z>2):
    npt.assert_almost_equal(electrode.electric_potential(0, 1, 38, 1),
                            0.780,
                            decimal=3)
Exemple #11
0
    def _set_grid(self):
        """Private method to build the electrode grid"""

        n_elecs = np.prod(self.shape)
        rows, cols = self.shape

        # The user did not specify a unique naming scheme:
        if len(self.names) == 2:
            # Create electrode names, using either A-Z or 1-n:
            if self.name_rows.isalpha():
                rws = [
                    chr(i) for i in range(ord(self.name_rows),
                                          ord(self.name_rows) + rows + 1)
                ]
            elif self.name_rows.isdigit():
                rws = [
                    str(i) for i in range(int(self.name_rows), rows +
                                          int(self.name_rows))
                ]
            else:
                raise ValueError("rows must be alphabetic or numeric")

            if self.name_cols.isalpha():
                clms = [
                    chr(i) for i in range(ord(self.name_cols),
                                          ord(self.name_cols) + cols)
                ]
            elif self.name_cols.isdigit():
                clms = [
                    str(i) for i in range(int(self.name_cols), cols +
                                          int(self.name_cols))
                ]
            else:
                raise ValueError("Columns must be alphabetic or numeric.")

            # facilitating Argus I naming scheme
            if self.name_cols.isalpha() and not self.name_rows.isalpha():
                names = [
                    clms[j] + rws[i] for i in range(len(rws))
                    for j in range(len(clms))
                ]
            else:
                names = [
                    rws[i] + clms[j] for i in range(len(rws))
                    for j in range(len(clms))
                ]
        else:
            if len(self.names) != n_elecs:
                raise ValueError("If `names` specifies more than row/column "
                                 "names, it must have %d entries, not "
                                 "%d)." % (n_elecs, len(self.names)))
            names = self.names

        if isinstance(self.z, (list, np.ndarray)):
            # Specify different height for every electrode in a list:
            z_arr = np.asarray(self.z).flatten()
            if z_arr.size != n_elecs:
                raise ValueError("If `h` is a list, it must have %d entries, "
                                 "not %d." % (n_elecs, len(self.z)))
        else:
            # If `z` is a scalar, choose same height for all electrodes:
            z_arr = np.ones(n_elecs, dtype=float) * self.z

        # Make a 2D meshgrid from x, y coordinates:
        # For example, cols=3 with spacing=100 should give: [-100, 0, 100]
        x_arr_lshift = (np.arange(cols) * self.spacing -
                        (cols / 2.0 - 0.5) * self.spacing -
                        self.spacing * 0.25)
        x_arr_rshift = (np.arange(cols) * self.spacing -
                        (cols / 2.0 - 0.5) * self.spacing +
                        self.spacing * 0.25)
        y_arr = (np.arange(rows) * math.sqrt(3) * self.spacing / 2.0 -
                 (rows / 2.0 - 0.5) * self.spacing)
        x_arr_lshift, y_arr_lshift = np.meshgrid(x_arr_lshift,
                                                 y_arr,
                                                 sparse=False)
        x_arr_rshift, y_arr_rshift = np.meshgrid(x_arr_rshift,
                                                 y_arr,
                                                 sparse=False)

        # added code to interleave arrays
        x_arr = []
        for row in range(0, rows):
            if row % 2 == 0:
                x_arr.append(x_arr_lshift[row])
            else:
                x_arr.append(x_arr_rshift[row])
        x_arr = np.array(x_arr)
        y_arr = y_arr_rshift

        # Rotate the grid:
        rotmat = np.array([
            np.cos(self.rot), -np.sin(self.rot),
            np.sin(self.rot),
            np.cos(self.rot)
        ]).reshape((2, 2))
        xy = np.matmul(rotmat, np.vstack((x_arr.flatten(), y_arr.flatten())))
        x_arr = xy[0, :]
        y_arr = xy[1, :]

        # Apply offset to make the grid centered at (self.x, self.y):
        x_arr += self.x
        y_arr += self.y

        if issubclass(self.etype, DiskElectrode):
            if isinstance(self.r, (list, np.ndarray)):
                # Specify different radius for every electrode in a list:
                if len(self.r) != n_elecs:
                    err_s = ("If `r` is a list, it must have %d entries, not "
                             "%d)." % (n_elecs, len(self.r)))
                    raise ValueError(err_s)
                r_arr = self.r
            else:
                # If `r` is a scalar, choose same radius for all electrodes:
                r_arr = np.ones(n_elecs, dtype=float) * self.r

            # Create a grid of DiskElectrode objects:
            for x, y, z, r, name in zip(x_arr, y_arr, z_arr, r_arr, names):
                self.add_electrode(name, DiskElectrode(x, y, z, r))
        elif issubclass(self.etype, PointSource):
            # Create a grid of PointSource objects:
            for x, y, z, name in zip(x_arr, y_arr, z_arr, names):
                self.add_electrode(name, PointSource(x, y, z))
        else:
            raise NotImplementedError