def get_w_potential(): try: return tools.load( os.path.join(DATAFOLDER, 'pot_w_descr_%d_3D' % RESOLUTION)) except IOError: mesh = get_mesh() min_x, max_x, min_y, max_y = GEOM_DESCR.get_array_corners() nx = WIDTH_X * NPIXEL_X * 4 ny = WIDTH_Y * NPIXEL_Y * 4 w_potential = fields.calculate_3D_sensor_w_potential(mesh, WIDTH_X, WIDTH_Y, NPIXEL_X, NPIXEL_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) tools.save(pot_w_descr, os.path.join(DATAFOLDER, 'pot_w_descr_%d_3D' % RESOLUTION)) return pot_w_descr
def get_w_potential(thickness): try: return tools.load( os.path.join(DATAFOLDER, 'pot_w_descr_%d' % thickness)) except IOError: mesh = get_mesh(n_pixel=NPIXEL, width=WIDTH, thickness=thickness, resolution=RESOLUTION_1 if thickness == THICKNESS_1 else RESOLUTION_2) w_potential = fields.calculate_planar_sensor_w_potential( mesh=mesh, width=WIDTH, pitch=PITCH, n_pixel=NPIXEL, thickness=thickness) pot_w_descr = fields.Description( w_potential, min_x=float(mesh.getFaceCenters()[0, :].min()), max_x=float(mesh.getFaceCenters()[0, :].max()), min_y=0, max_y=thickness, nx=WIDTH * NPIXEL, ny=thickness, smoothing=SMOOTHING) tools.save(pot_w_descr, os.path.join(DATAFOLDER, 'pot_w_descr_%d' % thickness)) return pot_w_descr
def get_potential(V_bias, n_eff): try: return tools.load( os.path.join(DATAFOLDER, 'pot_descr_%d_%d_%d_3D' % (V_bias, n_eff, RESOLUTION))) except IOError: V_readout = 0. V_bi = -silicon.get_diffusion_potential(n_eff / 1e12, temperature=TEMP) mesh = get_mesh() potential = fields.calculate_3D_sensor_potential( mesh, WIDTH_X, WIDTH_Y, NPIXEL_X, NPIXEL_Y, RADIUS, ND, n_eff, V_bias, V_readout, V_bi) min_x, max_x, min_y, max_y = GEOM_DESCR.get_array_corners() nx = WIDTH_X * NPIXEL_X * 4 ny = WIDTH_Y * NPIXEL_Y * 4 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) tools.save( pot_descr, os.path.join(DATAFOLDER, 'pot_descr_%d_%d_%d_3D' % (V_bias, n_eff, RESOLUTION))) return pot_descr
def get_potential(V_bias, n_eff, thickness): try: return tools.load( os.path.join(DATAFOLDER, 'pot_descr_%d_%d_%d' % (V_bias, n_eff, thickness))) except IOError: V_readout = 0. V_bi = -silicon.get_diffusion_potential(n_eff / 1e12, temperature=TEMP) # mesh = MESH_1 if thickness == THICKNESS_1 else MESH_2 mesh = get_mesh(n_pixel=NPIXEL, width=WIDTH, thickness=thickness, resolution=RESOLUTION_1 if thickness == THICKNESS_1 else RESOLUTION_2) potential = fields.calculate_planar_sensor_potential( mesh=mesh, width=WIDTH, pitch=PITCH, n_pixel=NPIXEL, thickness=thickness, n_eff=n_eff, V_bias=V_bias, V_readout=V_readout, V_bi=V_bi) min_x = float(mesh.getFaceCenters()[0, :].min()) max_x = float(mesh.getFaceCenters()[0, :].max()) nx = WIDTH * NPIXEL ny = thickness pot_descr = fields.Description(potential, min_x=min_x, max_x=max_x, min_y=0, max_y=thickness, nx=nx, ny=ny, smoothing=SMOOTHING) tools.save( pot_descr, os.path.join(DATAFOLDER, 'pot_descr_%d_%d_%d' % (V_bias, n_eff, thickness))) return pot_descr
def test_save_and_load(self): ''' Check the saving and loading to disk. ''' # Create data mesh = geometry.mesh_planar_sensor(n_pixel=9, width=50., thickness=300., resolution=50., filename='planar_mesh_example.msh') potential = fields.calculate_planar_sensor_potential(mesh=mesh, width=50., pitch=45., n_pixel=9, thickness=300., n_eff=5e12, V_bias=-160., V_readout=0., V_bi=1.5) min_x = float(mesh.getFaceCenters()[0, :].min()) max_x = float(mesh.getFaceCenters()[0, :].max()) description = fields.Description(potential, min_x=min_x, max_x=max_x, min_y=0, max_y=300., nx=202, ny=200) # Force the creation of the potential and field functions description.get_field(0, 0) # Store and reload object tools.save(description, 'tmp.sc') description_2 = tools.load('tmp.sc') self.assertTrue(np.all(description.pot_data == description_2.pot_data)) self.assertTrue( np.all(description.potential_grid == description_2.potential_grid)) self.assertTrue( np.all( np.array( description.get_field(description._xx, description._yy)) == np.array( description_2.get_field(description_2._xx, description_2._yy))))
def planar_sensor(n_eff, V_bias, V_readout=0., temperature=300, n_pixel=9, width=50., pitch=45., thickness=200., selection=None, resolution=300., nx=None, ny=None, smoothing=0.05, mesh_file='planar_mesh.msh'): ''' Create a planar_sensor 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 : int Number of pixels width : number Width of one pixel in :math:`\mathrm{\mu m}` pitch : number Pitch (redout implant width) of one pixel in :math:`\mathrm{\mu m}` thickness : number Thickness of the sensor in :math:`\mathrm{\mu m}` 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 > 200000 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 no specified selection. ''' # Create mesh of the sensor and stores the result # The created file can be viewed with any mesh viewer (e.g. gmsh) mesh = geometry.mesh_planar_sensor(n_pixel=n_pixel, width=width, thickness=thickness, resolution=resolution, filename=mesh_file) min_x = float(mesh.getFaceCenters()[0, :].min()) max_x = float(mesh.getFaceCenters()[0, :].max()) # Set um resolution grid if not nx: nx = width * n_pixel if not ny: ny = thickness if not selection or 'drift' in selection: V_bi = -silicon.get_diffusion_potential(n_eff, temperature) # Numerically solve the Laplace equation on the mesh potential = fields.calculate_planar_sensor_potential( mesh=mesh, width=width, pitch=pitch, n_pixel=n_pixel, thickness=thickness, n_eff=n_eff, V_bias=V_bias, V_readout=V_readout, V_bi=V_bi) pot_descr = fields.Description(potential, min_x=min_x, max_x=max_x, min_y=0, max_y=thickness, nx=nx, ny=ny, smoothing=smoothing) if selection and 'drift' in selection: return pot_descr if not selection or 'weighting' in selection: # Numerically solve the Poisson equation on the mesh w_potential = fields.calculate_planar_sensor_w_potential( mesh=mesh, width=width, pitch=pitch, n_pixel=n_pixel, thickness=thickness) pot_w_descr = fields.Description(w_potential, min_x=min_x, max_x=max_x, min_y=0, max_y=thickness, nx=nx, ny=ny, smoothing=smoothing) if selection and 'weighting' in selection: return pot_w_descr return pot_w_descr, pot_descr
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 test_weighting_potential_planar(self): ''' Compares estimated weighting potential to analytical solution. ''' # Influences how correct the field for the center pixel(s) is # due to more far away infinite boundary condition n_pixel = 11 for i, width in enumerate([50., 200.]): # FIXME: 50 um thichness does not work for j, thickness in enumerate([50., 250.]): # Analytical solution only existing for pixel width = readout # pitch (100% fill factor) pitch = width # Tune resolution properly for time/accuracy trade off if i == 0 and j == 0: resolution = 200 continue elif i == 0 and j == 1: resolution = 100 continue elif i == 1 and j == 0: resolution = 600 continue # FIXME: 50 thichness / 200 width does not work elif i == 1 and j == 1: resolution = 200 else: raise RuntimeError('Loop index unknown') mesh = geometry.mesh_planar_sensor( n_pixel=n_pixel, width=width, thickness=thickness, resolution=resolution, filename='planar_mesh_tmp_2.msh') potential = fields.calculate_planar_sensor_w_potential( mesh=mesh, width=width, pitch=pitch, n_pixel=n_pixel, thickness=thickness) min_x, max_x = -width * float(n_pixel), width * float(n_pixel) min_y, max_y = 0., thickness nx, ny = 1000, 1000 potential_description = fields.Description(potential, min_x=min_x, max_x=max_x, min_y=min_y, max_y=max_y, nx=nx, ny=ny, smoothing=0.1) def potential_analytic(x, y): return fields.get_weighting_potential_analytic( x, y, D=thickness, S=width, is_planar=True) # Create x,y grid x = np.linspace(min_x, max_x, nx) y = np.linspace(min_y, max_y, ny) xx, yy = np.meshgrid(x, y, sparse=True) # Evaluate potential on a grid pot_analytic = potential_analytic(xx, yy) pot_numeric = potential_description.get_potential(xx, yy) # import matplotlib.pyplot as plt # for pos_x in [0, 10, 15, 30, 45]: # plt.plot(y, pot_analytic.T[nx / 2 + pos_x, :], # label='Analytic') # for pos_x in [0, 10, 15, 30, 45]: # plt.plot(y, pot_numeric.T[nx / 2 + pos_x, :], # label='Numeric') # plt.legend(loc=0) # plt.show() # Check only at center pixel, edge pixel are not interessting for pos_x in [-45, -30, -15, -10, 0, 10, 15, 30, 45]: sel = pot_analytic.T[nx / 2 + pos_x, :] > 0.01 # Check with very tiny and tuned error allowance self.assertTrue(np.allclose( pot_analytic.T[nx / 2 + pos_x, sel], pot_numeric.T[nx / 2 + pos_x, sel], rtol=0.01, atol=0.005))
def test_potential_smoothing(self): ''' Checks the smoothing of the potential to be independent of the potential values. ''' n_pixel = 11 width = 50. thickness = 50. # Create x,y grid min_x, max_x = -width * float(n_pixel), width * float(n_pixel) min_y, max_y = 0., thickness nx, ny = 1000, 1000 x = np.linspace(min_x, max_x, nx) y = np.linspace(min_y, max_y, ny) xx, yy = np.meshgrid(x, y, sparse=True) # Load potential solution to save time potential = dump.read( filename=os.path.join(constant.FIXTURE_FOLDER, 'potential.sc')) def upcale_potential(potential, V_readout, V_bias): ''' Scales potential to [V_bias, V_readout] to simulate other bias settings ''' return ((potential - np.nanmin(potential)) / (np.nanmax(potential) - np.nanmin(potential))) * \ (V_readout - V_bias) + V_readout def downscale_potential(potential): ''' Scales potential to [0, 1] to make the smoothing result comparible ''' return (potential - np.nanmin(potential)) / (np.nanmax(potential) - np.nanmin(potential)) potential_descr = fields.Description(potential, min_x=min_x, max_x=max_x, min_y=min_y, max_y=max_y, nx=nx, ny=ny) # Expected result for the std. smoothing value and a potential between # 0 and 1 pot_numeric = downscale_potential( potential_descr.get_potential_smooth(xx, yy)) for V_bias in [-100, -1000]: for V_readout in [50, 0, -50]: # Create fake data with different bias by upscaling potential_scaled = upcale_potential( potential, V_readout, V_bias) # Describe upscaled data potential_descr_scaled = fields.Description(potential_scaled, min_x=min_x, max_x=max_x, min_y=min_y, max_y=max_y, nx=nx, ny=ny) # Downscale smoothed potential for comparison pot_numeric_2 = downscale_potential( potential_descr_scaled.get_potential_smooth(xx, yy)) self.assertTrue( np.allclose(pot_numeric, pot_numeric_2, equal_nan=True))
def test_weighting_field_planar(self): ''' Compare weighting field to numerical solution. ''' width = 50. # Analytical solution only existing for pixel width = readout pitch # (100 % fill factor) pitch = width thickness = 200. n_pixel = 11 mesh = geometry.mesh_planar_sensor( n_pixel=n_pixel, width=width, thickness=thickness, resolution=200, filename='planar_mesh_tmp_2.msh') potential = fields.calculate_planar_sensor_w_potential( mesh=mesh, width=width, pitch=pitch, n_pixel=n_pixel, thickness=thickness) # Define field/potential domain min_x, max_x = -width * float(n_pixel), width * float(n_pixel) min_y, max_y = 0., thickness # Create x,y grid nx, ny = 1000, 1000 x = np.linspace(min_x, max_x, nx) y = np.linspace(min_y, max_y, ny) xx, yy = np.meshgrid(x, y, sparse=True) field_description = fields.Description(potential, min_x=min_x, max_x=max_x, min_y=min_y, max_y=max_y, nx=nx, ny=ny, smoothing=0.2) def field_analytic(x, y): return fields.get_weighting_field_analytic(x, y, D=thickness, S=width, is_planar=True) # Evaluate field on a grid f_analytic_x, f_analytic_y = field_analytic(xx, yy) f_numeric_x, f_numeric_y = field_description.get_field(xx, yy) # Check only at center pixel, edge pixel are not interessting for pox_x in [-45, -30, -15, -10, 0, 10, 15, 30, 45]: self.assertTrue(np.allclose( f_analytic_x.T[ nx / 2 + pox_x, :], f_numeric_x.T[nx / 2 + pox_x, :], rtol=0.01, atol=0.01)) self.assertTrue(np.allclose( f_analytic_y.T[ nx / 2 + pox_x, :], f_numeric_y.T[nx / 2 + pox_x, :], rtol=0.01, atol=0.01))