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)
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
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
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]]
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)