Esempio n. 1
0
def get_3D_sensor_plot(fig,
                       width_x, width_y,
                       radius, nD,
                       n_pixel_x, n_pixel_y,
                       V_bias=None, V_readout=None,
                       pot_func=None,
                       field_func=None,
                       mesh=None,
                       title=None):

    ax = fig.add_subplot(111)

    desc = geometry.SensorDescription3D(width_x, width_y, n_pixel_x, n_pixel_y,
                                        radius, nD)

    min_x, max_x, min_y, max_y = desc.get_array_corners()

    # Define plot space with # >= 0.5 um resolution
    x = np.linspace(min_x, max_x, max(width_x * n_pixel_x,
                                      width_y * n_pixel_y))
    y = np.linspace(min_y, max_y, max(width_x * n_pixel_x,
                                      width_y * n_pixel_y))

    # Create x,y plot grid
    xx, yy = np.meshgrid(x, y, sparse=True)

    # BUG in matplotlib: aspect to be set to equal, otherwise contour plot
    # has wrong aspect ratio
    # http://stackoverflow.com/questions/28857673/wrong-aspect-ratio-for-contour-plot-with-python-matplotlib
    ax.set_aspect('equal')

    def get_column_mask(x, y):
        ''' Returns true for points inside a column.
            X, y as a sparse array representation are handled correctly.
        '''

        if x.shape != y.shape:  # Shapes do not fit
            if x.shape != y.T.shape:   # Sparse meshgrid assumption
                raise RuntimeError('The point representation in x,y in neither\
                 a grid nor a sparse grid.')
            x_dense, y_dense = np.meshgrid(x[0, :], y[:, 0], sparse=False)
        else:
            x_dense, y_dense = x, y
        # Reduce radius to prevent aliasing at round column edges
        return desc.position_in_column(x_dense, y_dense)

    if pot_func:
        # Plot Potential
        phi = pot_func(xx, yy)

        # Mask pot in columns, otherwise contour plot goes crazy
        phi_masked = np.ma.masked_array(phi, mask=get_column_mask(xx, yy))

        if V_bias is None:
            V_bias = phi_masked.min()

        if V_readout is None:
            V_readout = phi_masked.max()

        ax.contour(x, y, phi_masked, 10, colors='black')
        cmesh = ax.pcolormesh(x - np.diff(x)[0] / 2., y - np.diff(y)[0] / 2.,
                              phi, cmap=cm.get_cmap('Blues'), vmin=V_bias,
                              vmax=V_readout, rasterized=True)
        divider = make_axes_locatable(ax)
        cax = divider.append_axes("right", size="5%", pad=0.05)
        fig.colorbar(cmesh, cax=cax, orientation='vertical')

    # Plot E-Field
    if field_func:
        E_x, E_y = field_func(xx, yy)
        ax.streamplot(x, y, E_x, E_y, density=1.0, color='gray',
                      arrowstyle='-')
    elif pot_func:  # Get field from pot differentiation
        assert np.allclose(np.gradient(x), np.gradient(x)[0])
        assert np.allclose(np.gradient(y), np.gradient(y)[0])
        E_y, E_x = np.gradient(-phi, np.gradient(y)[0], np.gradient(x)[0])
        ax.streamplot(x, y, E_x, E_y, density=1.0, color='gray',
                      arrowstyle='-')

    if mesh:
        get_mesh_plot(fig, mesh, invert_y_axis=False)

    # Plot pixel bondaries in x
    for pos_x in desc.get_pixel_x_offsets():
        ax.plot([pos_x - width_x / 2., pos_x - width_x / 2.], [min_y, max_y],
                '--', color='black', linewidth=4)
    # Last pixel end
    ax.plot([pos_x + width_x / 2., pos_x + width_x / 2.], [min_y, max_y],
            '--', color='black', linewidth=4)

    # Plot pixel bondaries in y
    for pos_y in desc.get_pixel_y_offsets():
        ax.plot([min_x, max_x], [pos_y - width_y / 2., pos_y - width_y / 2.],
                '--', color='black', linewidth=4)
    ax.plot([min_x, max_x], [pos_y + width_y / 2., pos_y + width_y / 2.],
            '--', color='black', linewidth=4)  # Last pixel end

    # Plot readout pillars
    for pos_x, pos_y in desc.get_ro_col_offsets():
        ax.add_patch(plt.Circle((pos_x, pos_y), radius, color="darkred",
                                linewidth=0, zorder=5))

    # Plot full bias pillars
    for pos_x, pos_y in desc.get_center_bias_col_offsets():
        ax.add_patch(plt.Circle((pos_x, pos_y), radius, color="darkblue",
                                linewidth=0, zorder=5))

    # Plot side bias pillars
    for pos_x, pos_y in desc.get_side_bias_col_offsets():
        ax.add_patch(plt.Circle((pos_x, pos_y), radius, color="darkblue",
                                linewidth=0, zorder=5))

    # Plot edge bias pillars
    for pos_x, pos_y in desc.get_edge_bias_col_offsets():
        ax.add_patch(plt.Circle((pos_x, pos_y), radius, color="darkblue",
                                linewidth=0, zorder=5))

    ax.set_xlim((1.05 * min_x, 1.05 * max_x))
    ax.set_ylim((1.05 * min_y, 1.05 * max_y))
    ax.set_xlabel('Position x [um]', fontsize=18)
    ax.set_ylabel('Position y [um]', fontsize=18)
    if title:
        ax.set_title(title, fontsize=18)
Esempio n. 2
0
def sensor_3D(n_eff,
              V_bias,
              V_readout=0.,
              temperature=300,
              n_pixel_x=3,
              n_pixel_y=3,
              width_x=250.,
              width_y=50.,
              radius=6.,
              nD=2,
              selection=None,
              resolution=80.,
              nx=None,
              ny=None,
              smoothing=0.1,
              mesh_file='3D_mesh.msh'):
    ''' Create a 3D sensor pixel array.

        Parameters
        ----------
        n_eff : number
            Effective doping concentration in :math:`\mathrm{\frac{1}{cm^3}}`
        V_bias : number
            Bias voltage in Volt
        V_readout : number
            Readout voltage in Volt
        temperature : float
            Temperature in Kelvin
        n_pixel_x : int
            Number of pixels in x
        n_pixel_y : int
            Number of pixels in y
        width_x : number
            Width of one pixel in x in :math:`\mathrm{\mu m}`
        width_y : number
            Width of one pixel in y in :math:`\mathrm{\mu m}`
        radius : number
            Radius of readout and biasing columns in :math:`\mathrm{\mu m}`
        nD : int
            Number of readout columns per pixel
        selection : string
            Selects if the weighting potential / potentials or both are
            calculated.
            If not set: calculate weighting potential and drift potential
            If drift: calculate drift potential only
            If weighting: calculate weighting potential only
        resolution : number
            Mesh resolution. Should lead to > 300000 mesh points for exact
            results.
        nx : number
            Interpolation points in x for the potentials and fields
        ny : number
            Interpolation points in y for the potentials and fields
        smoothing : number
            Smoothing parameter for the potential. Higher number leads to
            more smooth looking potential, but be aware too much smoothing
            leads to wrong results!
        mesh_file : str
            File name of the created mesh file

        Returns
        -----
        Two scarce.fields.Description objects for the weighting potential and
        potential if not specified selection and a geometry desciption object.
    '''

    if not nx:
        nx = width_x * n_pixel_x * 4
    if not ny:
        ny = width_y * n_pixel_y * 4

    mesh = geometry.mesh_3D_sensor(width_x=width_x,
                                   width_y=width_y,
                                   n_pixel_x=n_pixel_x,
                                   n_pixel_y=n_pixel_y,
                                   radius=radius,
                                   nD=nD,
                                   resolution=resolution,
                                   filename=mesh_file)

    # Describe the 3D sensor array
    geom_descr = geometry.SensorDescription3D(width_x, width_y, n_pixel_x,
                                              n_pixel_y, radius, nD)
    min_x, max_x, min_y, max_y = geom_descr.get_array_corners()

    if not selection or 'drift' in selection:
        V_bi = -silicon.get_diffusion_potential(n_eff, temperature)
        potential = fields.calculate_3D_sensor_potential(
            mesh, width_x, width_y, n_pixel_x, n_pixel_y, radius, nD, n_eff,
            V_bias, V_readout, V_bi)
        pot_descr = fields.Description(
            potential,
            min_x=min_x,
            max_x=max_x,
            min_y=min_y,
            max_y=max_y,
            nx=nx,  # um res.
            ny=ny,  # um res.
            smoothing=smoothing)
        if selection and 'drift' in selection:
            return pot_descr, geom_descr

    if not selection or 'weighting' in selection:
        w_potential = fields.calculate_3D_sensor_w_potential(mesh,
                                                             width_x,
                                                             width_y,
                                                             n_pixel_x,
                                                             n_pixel_y,
                                                             radius,
                                                             nD=nD)
        pot_w_descr = fields.Description(w_potential,
                                         min_x=min_x,
                                         max_x=max_x,
                                         min_y=min_y,
                                         max_y=max_y,
                                         nx=nx,
                                         ny=ny,
                                         smoothing=smoothing)
        if selection and 'weighting' in selection:
            return pot_w_descr, geom_descr

    return pot_w_descr, pot_descr, geom_descr
Esempio n. 3
0
def calculate_3D_sensor_w_potential(mesh,
                                    width_x,
                                    width_y,
                                    n_pixel_x,
                                    n_pixel_y,
                                    radius,
                                    nD=2):
    _LOGGER.info('Calculating weighting potential')

    # Mesh validity check
    mesh_width = mesh.getFaceCenters()[0, :].max() - mesh.getFaceCenters()[
        0, :].min()
    mesh_height = mesh.getFaceCenters()[1, :].max() - mesh.getFaceCenters()[
        1, :].min()

    desc = geometry.SensorDescription3D(width_x, width_y, n_pixel_x, n_pixel_y,
                                        radius, nD)
    min_x, max_x, min_y, max_y = desc.get_array_corners()

    if mesh_width != max_x - min_x:
        raise ValueError(
            'Provided mesh width does not correspond to the sensor width')
    if mesh_height != max_y - min_y:
        raise ValueError(
            'Provided mesh height does not correspond to the sensor height')
    if (mesh.getFaceCenters()[0, :].min() != min_x
            or mesh.getFaceCenters()[0, :].max() != max_x):
        raise ValueError('The provided mesh has a wrong x position')
    if (mesh.getFaceCenters()[1, :].min() != min_y
            or mesh.getFaceCenters()[1, :].max() != max_y):
        raise ValueError('The provided mesh has a wrong y position')

    potential = fipy.CellVariable(mesh=mesh, name='potential', value=0.)
    permittivity = 1.
    potential.equation = (fipy.DiffusionTerm(coeff=permittivity) == 0.)

    bcs = []
    allfaces = mesh.getExteriorFaces()
    X, Y = mesh.getFaceCenters()

    # Set boundary conditions
    # Set readout pillars potentials
    for pos_x, pos_y in desc.get_ro_col_offsets():
        ring = allfaces & ((X - pos_x)**2 + (Y - pos_y)**2 < (radius)**2)
        # Center pixel, phi = 1
        if desc.position_in_center_pixel(pos_x, pos_y):
            bcs.append(fipy.FixedValue(value=1., faces=ring))
        else:  # Other pixel, phi = 0
            bcs.append(fipy.FixedValue(value=0., faces=ring))

    # Full bias pillars potentials = 0
    for pos_x, pos_y in desc.get_center_bias_col_offsets():
        ring = allfaces & ((X - pos_x)**2 + (Y - pos_y)**2 < (radius)**2)
        bcs.append(fipy.FixedValue(value=0., faces=ring))

    # Side bias pillars potentials = 0
    for pos_x, pos_y in desc.get_side_bias_col_offsets():
        ring = allfaces & ((X - pos_x)**2 + (Y - pos_y)**2 < (radius)**2)
        bcs.append(fipy.FixedValue(value=0., faces=ring))

    # Edge bias pillars potentials = 0
    for pos_x, pos_y in desc.get_edge_bias_col_offsets():
        ring = allfaces & ((X - pos_x)**2 + (Y - pos_y)**2 < (radius)**2)
        bcs.append(fipy.FixedValue(value=0., faces=ring))

    solver.solve(potential,
                 equation=potential.equation,
                 boundaryConditions=bcs)
    return potential
Esempio n. 4
0
NPIXEL_X = 3
NPIXEL_Y = 3
WIDTH_X = 250.
WIDTH_Y = 50.
RADIUS = 6.
ND = 2
RESOLUTION = 80
SMOOTHING = 0.1

VREADOUT = 0.
FLUENCE = 1000.
BIASES = [-20, -40, -60, -80]

NEFF0 = 4.6e11  # before irradiation

GEOM_DESCR = geometry.SensorDescription3D(WIDTH_X, WIDTH_Y, NPIXEL_X, NPIXEL_Y,
                                          RADIUS, ND)


def chi2_to_spline(spl, x, y):
    ''' Calculated the chi2 for the spline description of points '''
    return np.sum(np.square(spl(x) - y))


def plot_cce_mpv(data_files, data_files_iv, start_i, s=1):
    def get_voltage(vbias, v, i):
        interpolation = interp1d(x=v, y=i, kind='slinear', bounds_error=True)
        # 100 kOhm * uA = 100 Ohm * mA
        return vbias - 100. * interpolation(vbias) * 0.001

    data = np.loadtxt(fname=data_files[0], dtype=np.float, skiprows=1)
    data = data[data[:, 0].argsort()[::-1]]
Esempio n. 5
0
def calculate_3D_sensor_potential(mesh,
                                  width_x,
                                  width_y,
                                  n_pixel_x,
                                  n_pixel_y,
                                  radius,
                                  nD,
                                  n_eff,
                                  V_bias,
                                  V_readout,
                                  V_bi=0):
    ''' Calculates the potential of a planar sensor.

        Parameters
        ----------
        mesh : fipy.Gmsh2D
               Mesh where to solve the poisson equation
        width_x : number
                  Width in x of one pixel in :math:`\mathrm{\mu m}`
        width_y : number
                  Width in y of one pixel in :math:`\mathrm{\mu m}`
        n_pixel_x : int
            Number of pixels in x
        n_pixel_y : int
            Number of pixels in y
        radius : number
                  Radius of the columns in :math:`\mathrm{\mu m}`
        nD : number
                Number of readout columns per pixel
        n_eff : number
            Effective doping concentration in :math:`\mathrm{\frac{1}{cm^3}}`
        V_bias : number
            Bias voltage in Volt
        V_readout : number
            Readout voltage in Volt
        V_bi : number
            Build in voltage. Can be calculated by
            scarce.silicon.get_diffusion_potential()

        Notes
        -----
        So far the depletion zone cannot be calculated and a fully
        depleted sensor is assumed.
    '''

    _LOGGER.info('Calculating potential')

    # Mesh validity check
    mesh_width = mesh.getFaceCenters()[0, :].max() - mesh.getFaceCenters()[
        0, :].min()
    mesh_height = mesh.getFaceCenters()[1, :].max() - mesh.getFaceCenters()[
        1, :].min()

    desc = geometry.SensorDescription3D(width_x, width_y, n_pixel_x, n_pixel_y,
                                        radius, nD)
    min_x, max_x, min_y, max_y = desc.get_array_corners()

    if mesh_width != max_x - min_x:
        raise ValueError(
            'Provided mesh width does not correspond to the sensor width')
    if mesh_height != max_y - min_y:
        raise ValueError(
            'Provided mesh height does not correspond to the sensor height')
    if (mesh.getFaceCenters()[0, :].min() != min_x
            or mesh.getFaceCenters()[0, :].max() != max_x):
        raise ValueError('The provided mesh has a wrong x position')
    if (mesh.getFaceCenters()[1, :].min() != min_y
            or mesh.getFaceCenters()[1, :].max() != max_y):
        raise ValueError('The provided mesh has a wrong y position')

    # The field scales with rho / epsilon, thus scale to proper value to
    # counteract numerical instabilities
    rho = constants.elementary_charge * n_eff * \
        (1e-4) ** 3  # Charge density in C / um3
    epsilon = C.epsilon_s * 1e-6  # Permitticity of silicon in F/um
    epsilon_scaled = 1.
    rho_scale = rho / epsilon

    # Define cell variables
    potential = fipy.CellVariable(mesh=mesh, name='potential', value=0.)
    electrons = fipy.CellVariable(mesh=mesh, name='e-')
    electrons.valence = -1
    charge = electrons * electrons.valence
    charge.name = "charge"

    # Uniform charge distribution by setting a uniform concentration of
    # electrons = 1
    electrons.setValue(rho_scale)

    # Add build in potential to bias potential, although that might not be
    # correct. The analytic formular does it like this
    V_bias += V_bi

    def get_potential(max_iter=10):
        ''' Calculates the potential with boundary conditions.

        Can have a larger bias column radius to simulate a not fully depleted
        sensor
        '''

        r_bias = radius  # Start with full depletion assumption

        for i in range(max_iter):
            # Set boundary condition
            bcs = []
            allfaces = mesh.getExteriorFaces()
            X, Y = mesh.getFaceCenters()

            # Set boundary conditions
            # Set readout pillars potentials
            for pos_x, pos_y in desc.get_ro_col_offsets():
                ring = allfaces & ((X - pos_x)**2 + (Y - pos_y)**2 <
                                   (radius * 2)**2)
                bcs.append(fipy.FixedValue(value=V_readout, faces=ring))

            depletion_mask = None

            # Full bias pillars potentials = V_bias
            for pos_x, pos_y in desc.get_center_bias_col_offsets():
                ring = allfaces & ((X - pos_x)**2 + (Y - pos_y)**2 <
                                   (radius)**2)
                bcs.append(fipy.FixedValue(value=V_bias, faces=ring))
                if not np.any(depletion_mask):
                    depletion_mask = (potential.mesh.x - pos_x)**2 + \
                        (potential.mesh.y - pos_y)**2 < r_bias ** 2
                else:
                    depletion_mask |= (potential.mesh.x - pos_x)**2 + \
                        (potential.mesh.y - pos_y)**2 < (r_bias) ** 2

            # Side bias pillars potentials = V_bias
            for pos_x, pos_y in desc.get_side_bias_col_offsets():
                ring = allfaces & ((X - pos_x)**2 + (Y - pos_y)**2 <
                                   (radius)**2)
                bcs.append(fipy.FixedValue(value=V_bias, faces=ring))
                if not np.any(depletion_mask):
                    depletion_mask = (potential.mesh.x - pos_x)**2 + \
                        (potential.mesh.y - pos_y)**2 < r_bias ** 2
                else:
                    depletion_mask |= (potential.mesh.x - pos_x)**2 + \
                        (potential.mesh.y - pos_y)**2 < (r_bias) ** 2

            # Edge bias pillars potentials = V_bias
            for pos_x, pos_y in desc.get_edge_bias_col_offsets():
                ring = allfaces & ((X - pos_x)**2 + (Y - pos_y)**2 <
                                   (radius)**2)
                bcs.append(fipy.FixedValue(value=V_bias, faces=ring))
                if not np.any(depletion_mask):
                    depletion_mask = (potential.mesh.x - pos_x)**2 + \
                        (potential.mesh.y - pos_y)**2 < r_bias ** 2
                else:
                    depletion_mask |= (potential.mesh.x - pos_x)**2 + \
                        (potential.mesh.y - pos_y)**2 < (r_bias) ** 2

            # A depletion zone within the bulk requires an internal boundary
            # condition. Internal boundary conditions seem to challenge fipy
            # http://www.ctcms.nist.gov/fipy/documentation/USAGE.html#applying-internal-boundary-conditions

            large_value = 1e+10  # Hack for optimizer

            potential.equation = (fipy.DiffusionTerm(coeff=epsilon_scaled) +
                                  charge == fipy.ImplicitSourceTerm(
                                      depletion_mask * large_value) -
                                  depletion_mask * large_value * V_bias)

            solver.solve(potential,
                         equation=potential.equation,
                         boundaryConditions=bcs)

            # Check if fully depleted
            if not np.isclose(potential.arithmeticFaceValue().min(),
                              V_bias,
                              rtol=0.05,
                              atol=0.01):
                if i == 0:
                    logging.warning('Sensor is not fully depleted. '
                                    'Try to find depletion region. ')
            else:
                return potential

            # Get line between readout and bias column to check for full
            # depletion
            for x, y in desc.get_ro_col_offsets():
                if desc.position_in_center_pixel(x, y):
                    x_ro, y_ro = x, y
                    break
            for x, y in list(desc.get_center_bias_col_offsets()
                             ) + desc.get_edge_bias_col_offsets():
                if desc.position_in_center_pixel(x, y):
                    x_bias, y_bias = x, y
                    break

            pot_descr = Description(potential,
                                    min_x=min_x,
                                    max_x=max_x,
                                    min_y=min_y,
                                    max_y=max_y,
                                    nx=width_x * n_pixel_x,
                                    ny=width_y * n_pixel_y)

            N = 1000
            x = np.linspace(x_ro, x_bias, N)
            y = np.linspace(y_ro, y_bias, N)
            # Deselect position that is within the columns
            sel = ~desc.position_in_column(x, y)
            x, y = x[sel], y[sel]
            position = np.sqrt(x**2 + y**2)  # [um]
            phi = pot_descr.get_potential(x, y)
            x_r = position.max()
            x_min = position[np.atleast_1d(np.argmin(phi))[0]]
            #             import matplotlib.pyplot as plt
            #             plt.plot(position, phi, color='blue', linewidth=2,
            #                      label='Potential')
            #             plt.plot([x_r, x_r], plt.ylim())
            #             plt.plot([x_min, x_min], plt.ylim())
            #             plt.show()

            # Increase bias radius boundary to simulate not depleted
            # region sourrounding bias column
            r_bias += x_r - x_min

            logging.info('Depletion region error: %d um', x_r - x_min)

        raise RuntimeError('Unable to find the depletion region')

    return get_potential(10)