def _extract_current_results(data, curr, data_time): grid = data['models']['simulationGrid'] plate_spacing = _meters(grid['plate_spacing']) zmesh = np.linspace(0, plate_spacing, grid['num_z'] + 1) #holds the z-axis grid points in an array beam = data['models']['beam'] if data.models.simulationGrid.simulation_mode == '3d': cathode_area = _meters(grid['channel_width']) * _meters(grid['channel_height']) else: cathode_area = _meters(grid['channel_width']) RD_ideal = sources.j_rd(beam['cathode_temperature'], beam['cathode_work_function']) * cathode_area JCL_ideal = sources.cl_limit(beam['cathode_work_function'], beam['anode_work_function'], beam['anode_voltage'], plate_spacing) * cathode_area if beam['currentMode'] == '2' or (beam['currentMode'] == '1' and beam['beam_current'] >= JCL_ideal): curr2 = np.full_like(zmesh, JCL_ideal) y2_title = 'Child-Langmuir cold limit' else: curr2 = np.full_like(zmesh, RD_ideal) y2_title = 'Richardson-Dushman' return { 'title': 'Current for Time: {:.4e}s'.format(data_time), 'x_range': [0, plate_spacing], 'y_label': 'Current [A]', 'x_label': 'Z [m]', 'points': [ curr.tolist(), curr2.tolist(), ], 'x_points': zmesh.tolist(), 'y_range': [min(np.min(curr), np.min(curr2)), max(np.max(curr), np.max(curr2))], 'y1_title': 'Current', 'y2_title': y2_title, }
def _extract_current_results(data, curr, data_time): grid = data['models']['simulationGrid'] plate_spacing = _meters(grid['plate_spacing']) zmesh = np.linspace(0, plate_spacing, grid['num_z'] + 1) #holds the z-axis grid points in an array beam = data['models']['beam'] if _SIM_DATA.warpvnd_is_3d(data): cathode_area = _meters(grid['channel_width']) * _meters(grid['channel_height']) else: cathode_area = _meters(grid['channel_width']) RD_ideal = sources.j_rd(beam['cathode_temperature'], beam['cathode_work_function']) * cathode_area JCL_ideal = sources.cl_limit(beam['cathode_work_function'], beam['anode_work_function'], beam['anode_voltage'], plate_spacing) * cathode_area if beam['currentMode'] == '2' or (beam['currentMode'] == '1' and beam['beam_current'] >= JCL_ideal): curr2 = np.full_like(zmesh, JCL_ideal) y2_title = 'Child-Langmuir cold limit' else: curr2 = np.full_like(zmesh, RD_ideal) y2_title = 'Richardson-Dushman' return { 'title': 'Current for Time: {:.4e}s'.format(data_time), 'x_range': [0, plate_spacing], 'y_label': 'Current [A]', 'x_label': 'Z [m]', 'points': [ curr.tolist(), curr2.tolist(), ], 'x_points': zmesh.tolist(), 'y_range': [min(np.min(curr), np.min(curr2)), max(np.max(curr), np.max(curr2))], 'y1_title': 'Current', 'y2_title': y2_title, }
def main(x_struts, y_struts, V_grid, grid_height, strut_width, strut_height, rho_ew, T_em, phi_em, T_coll, phi_coll, rho_cw, gap_distance, rho_load, run_id, channel_width=100e-9, injection_type=2, magnetic_field=0.0, random_seed=True, install_grid=True, max_wall_time=1e9, particle_diagnostic_switch=False, field_diagnostic_switch=False, lost_diagnostic_switch=False): """ Run a simulation of a gridded TEC. Args: x_struts: Number of struts that intercept the x-axis. y_struts: Number of struts that intercept the y-axis V_grid: Voltage to place on the grid in Volts. grid_height: Distance from the emitter to the grid normalized by gap_distance, unitless. strut_width: Transverse extent of the struts in meters. strut_height: Longitudinal extent of the struts in meters. rho_ew: Emitter side wiring resistivity, ohms*cm. T_em: Emitter temperature, kelvin. phi_em: Emitter work function, eV. T_coll: Collector termperature, kelvin. phi_coll: Collector work function, eV. rho_cw: Collector side wiring resistivity, ohms*cm. gap_distance: Distance from emitter to collector, meters. rho_load: Load resistivity, ohms*cm. run_id: Run ID. Mainly used for parallel optimization. injection_type: 1: For constant current emission with only thermal velocity spread in z and CL limited emission. 2: For true thermionic emission. Velocity spread along all axes. random_seed: True/False. If True, will force a random seed to be used for emission positions. install_grid: True/False. If False the grid will not be installed. Results in simple parallel plate setup. If False then phi_em - phi_coll specifies the voltage on the collector. max_wall_time: Wall time to allow simulation to run for. Simulation is periodically checked and will halt if it appears the next segment of the simulation will exceed max_wall_time. This is not guaranteed to work since the guess is based on the run time up to that point. Intended to be used when running on system with job manager. particle_diagnostic_switch: True/False. Use openPMD compliant .h5 particle diagnostics. field_diagnostic_switch: True/False. Use rswarp electrostatic .h5 field diagnostics (Maybe openPMD compliant?). lost_diagnostic_switch: True/False. Enable collection of lost particle coordinates with rswarp.diagnostics.parallel.save_lost_particles. """ # record inputs and set parameters run_attributes = deepcopy(locals()) for key in run_attributes: if key in efficiency.tec_parameters: efficiency.tec_parameters[key][0] = run_attributes[key] # set new random seed if random_seed: top.seedranf(randint(1, 1e9)) # Control for printing in parallel if comm_world.size != 1: synchronizeQueuedOutput_mpi4py(out=True, error=True) if particle_diagnostic_switch or field_diagnostic_switch: # Directory paths diagDir = 'diags_id{}/hdf5/'.format(run_id) field_base_path = 'diags_id{}/fields/'.format(run_id) diagFDir = {'magnetic': 'diags_id{}/fields/magnetic'.format(run_id), 'electric': 'diags_id{}/fields/electric'.format(run_id)} # Cleanup command if directories already exist if comm_world.rank == 0: cleanupPrevious(diagDir, diagFDir) load_balance = LoadBalancer() ###################### # DOMAIN/GEOMETRY/MESH ###################### # Dimensions X_MAX = +channel_width / 2. X_MIN = -X_MAX Y_MAX = +channel_width / 2. Y_MIN = -Y_MAX Z_MAX = gap_distance Z_MIN = 0. # TODO: cells in all dimensions reduced by 10x for testing, will need to verify if this is reasonable (TEMP) # Grid parameters dx_want = 5e-9 dy_want = 5e-9 dz_want = 5e-9 NUM_X = int(round(channel_width / dx_want)) # 20 #128 #10 NUM_Y = int(round(channel_width / dy_want)) # 20 #128 #10 NUM_Z = int(round(gap_distance / dz_want)) # mesh spacing dz = (Z_MAX - Z_MIN) / NUM_Z dx = channel_width / NUM_X dy = channel_width / NUM_Y print "Channel width: {}, DX = {}".format(channel_width, dx) print "Channel width: {}, DY = {}".format(channel_width, dy) print "Channel length: {}, DZ = {}".format(gap_distance, dz) # Solver Geometry and Boundaries # Specify solver geometry w3d.solvergeom = w3d.XYZgeom # Set field boundary conditions w3d.bound0 = neumann w3d.boundnz = dirichlet w3d.boundxy = periodic # Particles boundary conditions top.pbound0 = absorb top.pboundnz = absorb top.pboundxy = periodic # Set mesh boundaries w3d.xmmin = X_MIN w3d.xmmax = X_MAX w3d.ymmin = Y_MIN w3d.ymmax = Y_MAX w3d.zmmin = 0. w3d.zmmax = Z_MAX # Set mesh cell counts w3d.nx = NUM_X w3d.ny = NUM_Y w3d.nz = NUM_Z ############################# # PARTICLE INJECTION SETTINGS ############################# # Cathode and anode settings EMITTER_TEMP = T_em EMITTER_PHI = phi_em # work function in eV COLLECTOR_PHI = phi_coll # Can be used if vacuum level is being set ACCEL_VOLTS = V_grid # ACCEL_VOLTS used for velocity and CL calculations collector_voltage = phi_em - phi_coll # Emitted species background_beam = Species(type=Electron, name='background') measurement_beam = Species(type=Electron, name='measurement') # Emitter area and position SOURCE_RADIUS_1 = 0.5 * channel_width # a0 parameter - X plane SOURCE_RADIUS_2 = 0.5 * channel_width # b0 parameter - Y plane Z_PART_MIN = dz / 1000. # starting particle z value # Compute cathode area for geomtry-specific current calculations if (w3d.solvergeom == w3d.XYZgeom): # For 3D cartesion geometry only cathode_area = 4. * SOURCE_RADIUS_1 * SOURCE_RADIUS_2 else: # Assume 2D XZ geometry cathode_area = 2. * SOURCE_RADIUS_1 * 1. # If using the XZ geometry, set so injection uses the same geometry top.linj_rectangle = (w3d.solvergeom == w3d.XZgeom or w3d.solvergeom == w3d.XYZgeom) # Returns velocity beam_beta (in units of beta) for which frac of emitted particles have v < beam_beta * c beam_beta = sources.compute_cutoff_beta(EMITTER_TEMP, frac=0.99) PTCL_PER_STEP = 100 if injection_type == 1: CURRENT_MODIFIER = 0.5 # Factor to multiply CL current by when setting beam current # Constant current density - beam transverse velocity fixed to zero, very small longitduinal velocity # Set injection flag top.inject = 1 # 1 means constant; 2 means space-charge limited injection;# 6 means user-specified top.npinject = PTCL_PER_STEP beam_current = 4. / 9. * eps0 * sqrt(2. * echarge / background_beam.mass) \ * ACCEL_VOLTS ** 1.5 / gap_distance ** 2 * cathode_area background_beam.ibeam = beam_current * CURRENT_MODIFIER background_beam.a0 = SOURCE_RADIUS_1 background_beam.b0 = SOURCE_RADIUS_2 background_beam.ap0 = .0e0 background_beam.bp0 = .0e0 w3d.l_inj_exact = True # Initial velocity settings (5% of c) vrms = np.sqrt(1 - 1 / (0.05 / 511e3 + 1) ** 2) * 3e8 top.vzinject = vrms if injection_type == 2: # True Thermionic injection top.inject = 1 # Set both beams to same npinject to keep weights the same background_beam.npinject = PTCL_PER_STEP measurement_beam.npinject = PTCL_PER_STEP w3d.l_inj_exact = True # Specify thermal properties background_beam.vthz = measurement_beam.vthz = np.sqrt(EMITTER_TEMP * kb_J / background_beam.mass) background_beam.vthperp = measurement_beam.vthperp = np.sqrt(EMITTER_TEMP * kb_J / background_beam.mass) top.lhalfmaxwellinject = 1 # inject z velocities as half Maxwellian beam_current = sources.j_rd(EMITTER_TEMP, EMITTER_PHI) * cathode_area # steady state current in Amps print('beam current expected: {}, current density {}'.format(beam_current, beam_current / cathode_area)) jcl = 4. / 9. * eps0 * sqrt(2. * echarge / background_beam.mass) \ * ACCEL_VOLTS ** 1.5 / gap_distance ** 2 * cathode_area print('child-langmuir limit: {}, current density {}'.format(jcl, jcl / cathode_area)) background_beam.ibeam = measurement_beam.ibeam = beam_current background_beam.a0 = measurement_beam.a0 = SOURCE_RADIUS_1 background_beam.b0 = measurement_beam.b0 = SOURCE_RADIUS_2 background_beam.ap0 = measurement_beam.ap0 = .0e0 background_beam.bp0 = measurement_beam.bp0 = .0e0 derivqty() ############## # FIELD SOLVER ############## # Add Uniform B_z field if turned on if magnetic_field: bz = np.zeros([w3d.nx, w3d.ny, w3d.nz]) bz[:, :, :] = magnetic_field z_start = w3d.zmmin z_stop = w3d.zmmax top.ibpush = 2 addnewbgrd(z_start, z_stop, xs=w3d.xmmin, dx=(w3d.xmmax - w3d.xmmin), ys=w3d.ymmin, dy=(w3d.ymmax - w3d.ymmin), nx=w3d.nx, ny=w3d.ny, nz=w3d.nz, bz=bz) # Set up fieldsolver f3d.mgtol = 1e-6 solverE = MultiGrid3D() registersolver(solverE) ######################## # CONDUCTOR INSTALLATION ######################## if install_grid: accel_grid, gl = create_grid(x_struts, y_struts, V_grid, grid_height * gap_distance, strut_width, strut_height, channel_width) accel_grid.voltage = V_grid # --- Anode Location zplate = Z_MAX # Create source conductors if install_grid: source = ZPlane(zcent=w3d.zmmin, zsign=-1., voltage=0., condid=2) else: source = ZPlane(zcent=w3d.zmmin, zsign=-1., voltage=0.) # Create ground plate total_rho = efficiency.tec_parameters['rho_load'][0] if install_grid: plate = ZPlane(zcent=zplate, condid=3) circuit = ExternalCircuit(top, solverE, total_rho, collector_voltage, cathode_area * 1e4, plate, debug=False) plate.voltage = circuit # plate.voltage = collector_voltage else: plate = ZPlane(zcent=zplate) circuit = ExternalCircuit(top, solverE, total_rho, collector_voltage, cathode_area * 1e4, plate, debug=False) plate.voltage = circuit # plate.voltage = collector_voltage if install_grid: installconductor(accel_grid) installconductor(source, dfill=largepos) installconductor(plate, dfill=largepos) scraper = ParticleScraper([accel_grid, source, plate], lcollectlpdata=True, lsaveintercept=True) scraper_dictionary = {'grid': 1, 'source': 2, 'collector': 3} else: installconductor(source, dfill=largepos) installconductor(plate, dfill=largepos) scraper = ParticleScraper([source, plate], lcollectlpdata=True, lsaveintercept=True) scraper_dictionary = {'source': 1, 'collector': 2} ############# # DIAGNOSTICS ############# # Particle/Field diagnostic options if particle_diagnostic_switch: particleperiod = 250 # TEMP particle_diagnostic_0 = ParticleDiagnostic(period=particleperiod, top=top, w3d=w3d, species={species.name: species for species in listofallspecies}, # if species.name == 'measurement'}, # TEMP comm_world=comm_world, lparallel_output=False, write_dir=diagDir[:-5]) installafterstep(particle_diagnostic_0.write) if field_diagnostic_switch: fieldperiod = 1000 efield_diagnostic_0 = FieldDiagnostic.ElectrostaticFields(solver=solverE, top=top, w3d=w3d, comm_world=comm_world, period=fieldperiod) installafterstep(efield_diagnostic_0.write) # Set externally derived parameters for efficiency calculation efficiency.tec_parameters['A_em'][0] = cathode_area * 1e4 # cm**2 if install_grid: efficiency.tec_parameters['occlusion'][0] = efficiency.calculate_occlusion(**efficiency.tec_parameters) else: efficiency.tec_parameters['occlusion'][0] = 0.0 ########################## # SOLVER SETTINGS/GENERATE ########################## # prevent gist from starting upon setup top.lprntpara = false top.lpsplots = false top.verbosity = -1 # Reduce solver verbosity solverE.mgverbose = -1 # further reduce output upon stepping - prevents websocket timeouts in Jupyter notebook init_iters = 20000 regular_iters = 200 init_tol = 1e-6 regular_tol = 1e-6 # Time Step # Determine an appropriate time step based upon estimated final velocity if install_grid: vz_accel = sqrt(2. * abs(V_grid) * np.abs(background_beam.charge) / background_beam.mass) else: vz_accel = sqrt(2. * abs(collector_voltage) * np.abs(background_beam.charge) / background_beam.mass) vzfinal = vz_accel + beam_beta * c dt = dz / vzfinal top.dt = dt solverE.mgmaxiters = init_iters solverE.mgtol = init_tol package("w3d") generate() solverE.mgtol = regular_tol solverE.mgmaxiters = regular_iters print("weights (background) (measurement): {}, {}".format(background_beam.sw, measurement_beam.sw)) # Use rnpinject to set number of macroparticles emitted background_beam.rnpinject = PTCL_PER_STEP measurement_beam.rnpinject = 0 # measurement beam is off at start ################## # CONTROL SEQUENCE ################## # Run until steady state is achieved (flat current profile at collector) (measurement species turned on) # Record data for effiency calculation # Switch off measurement species and wait for simulation to clear (background species is switched on) early_abort = 0 # If true will flag output data to notify startup_time = 4 * gap_distance / vz_accel # ~4 crossing times to approach steady-state with external circuit crossing_measurements = 10 # Number of crossing times to record for steps_per_crossing = int(gap_distance / vz_accel / dt) ss_check_interval = int(steps_per_crossing / 2.) ss_max_checks = 8 # Maximum number of of times to run steady-state check procedure before aborting times = [] # Write out timing of cycle steps to file clock = 0 # clock tracks the current, total simulation-runtime # Run initial block of steps record_time(stept, times, startup_time) clock += times[-1] stop_initialization = top.it # for diag file print("Completed Initialization on Step {}\nInitialization run time: {}".format(top.it, times[-1])) # Start checking for Steady State Operation tol = 0.01 ss_flag = 0 check_count = 0 # Track number of times steady-state check performed while ss_flag != 1 and check_count < ss_max_checks: if (max_wall_time - clock) < times[-1]: early_abort = 1 break record_time(step, times, ss_check_interval*4) clock += times[-1] tstart = (top.it - ss_check_interval) * top.dt _, current1 = plate.get_current_history(js=None, l_lost=1, l_emit=0, l_image=0, tmin=tstart, tmax=None, nt=1) current = np.sum(current1) if np.abs(current) < 0.5 * efficiency.tec_parameters['occlusion'][0] * beam_current: # If too little current is getting through run another check cycle check_count += 1 print("Completed check {}, insufficient current, running again for {} steps".format(check_count, ss_check_interval)) continue ss_flag = 1 # print np.abs(current), 0.5 * efficiency.tec_parameters['occlusion'][0] * beam_current # try: # # If steady_state check initialized no need to do it again # steady_state # except NameError: # # If this is the first pass with sufficient current then initialize the check # if check_count == 0: # # If the initial period was long enough to get current on collector then use that # steady_state = SteadyState(top, plate, steps_per_crossing) # else: # # If we had to run several steady state checks with no current then just use the period with current # steady_state = SteadyState(top, plate, ss_check_interval) # # ss_flag = steady_state(steps_per_crossing) check_count += 1 stop_ss_check = top.it # For diag file # If there was a failure to reach steady state after specified number of checks then pass directly end if check_count == ss_max_checks: early_abort = -1 crossing_measurements = 0 print("Failed to reach steady state. Aborting simulation.") else: # Start Steady State Operation print(" Steady State Reached.\nStarting efficiency " "recording for {} crossing times.\nThis will be {} steps".format(crossing_measurements, steps_per_crossing * crossing_measurements)) # particle_diagnostic_0.period = steps_per_crossing #TEMP commented out # Switch to measurement beam species measurement_beam.rnpinject = PTCL_PER_STEP background_beam.rnpinject = 0 # Install Zcrossing Diagnostic ZCross = ZCrossingParticles(zz=grid_height * gap_distance / 200., laccumulate=1) emitter_flux = [] crossing_wall_time = times[-1] * steps_per_crossing / ss_check_interval # Estimate wall time for one crossing print('crossing_wall_time estimate: {}, for {} steps'.format(crossing_wall_time, steps_per_crossing)) print('wind-down loop time estimate: {}, for {} steps'.format(crossing_wall_time * steps_per_crossing / ss_check_interval, ss_check_interval)) for sint in range(crossing_measurements): # Kill the loop and proceed to writeout if we don't have time to complete the loop if (max_wall_time - clock) < crossing_wall_time: early_abort = 2 break record_time(step, times, steps_per_crossing) clock += times[-1] # Re-evaluate time for next loop crossing_wall_time = times[-1] # Record velocities of emitted particles for later KE calculation velocity_array = np.array([ZCross.getvx(js=measurement_beam.js), ZCross.getvy(js=measurement_beam.js), ZCross.getvz(js=measurement_beam.js)]).transpose() # velocity_array = velocity_array[velocity_array[:, 2] >= 0.] # Filter particles moving to emitter emitter_flux.append(velocity_array) ZCross.clear() # Clear ZcrossingParticles memory print("Measurement: {} of {} intervals completed. Interval run time: {} s".format(sint + 1, crossing_measurements, times[-1])) stop_eff_calc = top.it # For diag file # Run wind-down until measurement particles have cleared measurement_beam.rnpinject = 0 background_beam.rnpinject = PTCL_PER_STEP initial_population = measurement_beam.npsim[0] measurement_tol = 0.03 # if particle_diagnostic_switch: # particle_diagnostic_0.period = ss_check_interval while measurement_beam.npsim[0] > measurement_tol * initial_population: # Kill the loop and proceed to writeout if we don't have time to complete the loop if (max_wall_time - clock) < crossing_wall_time * ss_check_interval / steps_per_crossing : early_abort = 3 break record_time(step, times, ss_check_interval) clock += times[-1] # Record velocities of emitted particles for later KE calculation # Check is required here as measurement_beam particles will not always be passing through if ZCross.getvx(js=measurement_beam.js).shape[0] > 0: velocity_array = np.array([ZCross.getvx(js=measurement_beam.js), ZCross.getvy(js=measurement_beam.js), ZCross.getvz(js=measurement_beam.js)]).transpose() print "Backwards particles: {}".format(np.where(velocity_array[:, 2] < 0.)[0].shape[0]) # velocity_array = velocity_array[velocity_array[:, 2] >= 0.] # Filter particles moving to emitter emitter_flux.append(velocity_array) ZCross.clear() # Clear ZcrossingParticles memory print(" Wind-down: Taking {} steps, On Step: {}, {} Particles Left".format(ss_check_interval, top.it, measurement_beam.npsim[0])) stop_winddown = top.it # For diag file ###################### # CALCULATE EFFICIENCY ###################### try: emitter_flux = np.vstack(emitter_flux) except ValueError: # If this triggered then measurement emission never took place # Run took too long probably and abort took place emitter_flux = np.array([[0., 0., 0.]]) # Find integrated charge on each conductor surface_charge = analyze_scraped_particles(top, measurement_beam, solverE) measured_charge = {} for key in surface_charge: # We can abuse the fact that js=0 for background species to filter it from the sum measured_charge[key] = np.sum(surface_charge[key][:, 1] * surface_charge[key][:, 3]) # Set derived parameters from simulation efficiency.tec_parameters['run_time'][0] = crossing_measurements * steps_per_crossing * dt if crossing_measurements == 0: # Set to large value to force all powers and currents to zero efficiency.tec_parameters['run_time'][0] = 1e20 # Find total number of measurement particles that were emitted total_macroparticles = measurement_beam.npsim[0] + np.sum([measured_charge[key] for key in surface_charge]) efficiency.tec_parameters['J_em'][0] = e * (total_macroparticles - measured_charge[scraper_dictionary['source']]) \ * measurement_beam.sw / \ efficiency.tec_parameters['run_time'][0] / efficiency.tec_parameters['A_em'][0] # If grid isn't being used then J_grid will not be in scraper dict try: efficiency.tec_parameters['J_grid'][0] = e * measured_charge[scraper_dictionary['grid']] * measurement_beam.sw / \ efficiency.tec_parameters['run_time'][0] / \ (efficiency.tec_parameters['occlusion'][0] * efficiency.tec_parameters['A_em'][0]) except KeyError: efficiency.tec_parameters['J_grid'][0] = 0.0 efficiency.tec_parameters['J_ec'][0] = e * measured_charge[scraper_dictionary['collector']] * measurement_beam.sw / \ efficiency.tec_parameters['run_time'][0] / efficiency.tec_parameters['A_em'][0] efficiency.tec_parameters['P_em'][0] = efficiency.calculate_power_flux(emitter_flux, measurement_beam.sw, efficiency.tec_parameters['phi_em'][0], **efficiency.tec_parameters) # Efficiency calculation print("Efficiency") efficiency_result = efficiency.calculate_efficiency(**efficiency.tec_parameters) print("Overall Efficiency: {}".format(efficiency_result['eta'])) print("Total steps: {}".format(top.it)) ###################### # FINAL RUN STATISTICS ###################### if comm_world.rank == 0: if not os.path.exists('diags_id{}'.format(run_id)): os.makedirs('diags_id{}'.format(run_id)) np.save('iv_data.npy', np.array([circuit.current_history, circuit.voltage_history])) write_parameter_file(run_attributes, filename='diags_id{}/'.format(run_id)) filename = 'efficiency_id{}.h5'.format(str(run_id)) with h5.File(os.path.join('diags_id{}'.format(run_id), filename), 'w') as h5file: # TODO: Add current history eff_group = h5file.create_group('/efficiency') run_group = h5file.create_group('/attributes') scrap_group = h5file.create_group('/scraper') h5file.attrs['complete'] = early_abort for key in efficiency_result: eff_group.attrs[key] = efficiency_result[key] for key in efficiency.tec_parameters: eff_group.attrs[key] = efficiency.tec_parameters[key] for key in run_attributes: run_group.attrs[key] = run_attributes[key] run_group.attrs['dt'] = top.dt run_group.attrs['stop_initialization'] = stop_initialization run_group.attrs['stop_ss_check'] = stop_ss_check run_group.attrs['stop_eff_calc'] = stop_eff_calc run_group.attrs['stop_winddown'] = stop_winddown # for key, value in scraper_dictionary.iteritems(): # scrap_group.attrs[key] = measured_charge[value] # inv_scraper_dict = {value: key for key, value in scraper_dictionary.iteritems()} for cond in solverE.conductordatalist: cond_objs = cond[0] scrap_group.attrs[inv_scraper_dict[cond_objs.condid]] = measured_charge[cond_objs.condid] _, bckgrnd_current = cond_objs.get_current_history(js=0, l_lost=1, l_emit=0, l_image=0, tmin=None, tmax=None, nt=top.it) _, msrmnt_current = cond_objs.get_current_history(js=1, l_lost=1, l_emit=0, l_image=0, tmin=None, tmax=None, nt=top.it) scrap_group.create_dataset('{}_background'.format(inv_scraper_dict[cond_objs.condid]), data=bckgrnd_current) scrap_group.create_dataset('{}_measurement'.format(inv_scraper_dict[cond_objs.condid]), data=msrmnt_current) h5file.create_dataset('times', data=times)
beam_current = sources.cl_limit(CATHODE_PHI, ANODE_WF, GRID_BIAS, PLATE_SPACING) * cathode_area beam.ibeam = beam_current beam.a0 = SOURCE_RADIUS_1 beam.b0 = SOURCE_RADIUS_2 w3d.l_inj_exact = True elif USER_INJECT == 3: #Thermionic injection #Set injection flag top.inject = 6 # 1 means constant; 2 means space-charge limited injection;# 6 means user-specified beam_current = sources.j_rd( CATHODE_TEMP, CATHODE_PHI) * cathode_area #steady state current in Amps beam.ibeam = beam_current beam.a0 = SOURCE_RADIUS_1 beam.b0 = SOURCE_RADIUS_2 myInjector = injectors.injectorUserDefined(beam, CATHODE_TEMP, CHANNEL_WIDTH, Z_PART_MIN, PTCL_PER_STEP) installuserinjection(myInjector.inject_thermionic) # These must be set for user injection top.ainject = 1.0 top.binject = 1.0 derivqty()
def main(injection_type, cathode_temperature, cathode_workfunction, anode_workfunction, anode_voltage, gate_voltage, lambdaR, beta, srefprob, drefprob, reflection_scheme, gap_voltage, dgap, dt, nsteps, particles_per_step, file_path, fieldperiod=100, particleperiod=1000, reflections=True): settings = deepcopy(locals()) ############################ # Domain / Geometry / Mesh # ############################ # Grid geometry w = 1.6e-3 # full hexagon width a = w / 2.0 # inner wall width b = 80e-6 # thickness r = np.sqrt(3) / 2. * w # distance between two opposite walls d = np.sqrt(b**2 / 4. + b**2) # h = 0.2e-3 # height of grid (length in z) PLATE_SPACING = dgap CHANNEL_WIDTH_X = w + 2 * d + a CHANNEL_WIDTH_Y = 2 * r + 2 * b # Dimensions X_MAX = +CHANNEL_WIDTH_X / 2. X_MIN = -X_MAX Y_MAX = +CHANNEL_WIDTH_Y / 2. Y_MIN = -Y_MAX Z_MAX = PLATE_SPACING Z_MIN = 0. # Grid parameters NUM_X = 100 NUM_Y = 100 NUM_Z = 25 # z step size dx = (X_MAX - X_MIN) / NUM_X dy = (Y_MAX - Y_MIN) / NUM_Y dz = (Z_MAX - Z_MIN) / NUM_Z print(" --- (xmin, ymin, zmin) = ({}, {}, {})".format(X_MIN, Y_MIN, Z_MIN)) print(" --- (xmax, ymax, zmax) = ({}, {}, {})".format(X_MAX, Y_MAX, Z_MAX)) print(" --- (dx, dy, dz) = ({}, {}, {})".format(dx, dy, dz)) # Solver Geometry and Boundaries # Specify solver geometry w3d.solvergeom = w3d.XYZgeom # Set field boundary conditions w3d.bound0 = dirichlet w3d.boundnz = dirichlet w3d.boundxy = periodic # Particles boundary conditions top.pbound0 = absorb top.pboundnz = absorb top.pboundxy = periodic # Set mesh boundaries w3d.xmmin = X_MIN w3d.xmmax = X_MAX w3d.ymmin = Y_MIN w3d.ymmax = Y_MAX w3d.zmmin = Z_MIN w3d.zmmax = Z_MAX # Set mesh cell counts w3d.nx = NUM_X w3d.ny = NUM_Y w3d.nz = NUM_Z ################ # FIELD SOLVER # ################ magnetic_field = True if magnetic_field: bz = np.zeros([w3d.nx, w3d.ny, w3d.nz]) bz[:, :, :] = 200e-3 z_start = w3d.zmmin z_stop = w3d.zmmax top.ibpush = 2 addnewbgrd(z_start, z_stop, xs=w3d.xmmin, dx=(w3d.xmmax - w3d.xmmin), ys=w3d.ymmin, dy=(w3d.ymmax - w3d.ymmin), nx=w3d.nx, ny=w3d.ny, nz=w3d.nz, bz=bz) # Set up fieldsolver f3d.mgtol = 1e-6 solverE = MultiGrid3D() registersolver(solverE) ############################### # PARTICLE INJECTION SETTINGS # ############################### volts_on_conductor = gap_voltage # INJECTION SPECIFICATION USER_INJECT = injection_type # Cathode and anode settings CATHODE_TEMP = cathode_temperature CATHODE_PHI = cathode_workfunction ANODE_WF = anode_workfunction # Can be used if vacuum level is being set CONDUCTOR_VOLTS = volts_on_conductor # ACCEL_VOLTS used for velocity and CL calculations # Emitted species background_beam = Species(type=Electron, name='background') # Reflected species reflected_electrons = Species(type=Electron, name='reflected') # Emitter area and position SOURCE_RADIUS_1 = 0.5 * CHANNEL_WIDTH_X # a0 parameter - X plane SOURCE_RADIUS_2 = 0.5 * CHANNEL_WIDTH_Y # b0 parameter - Y plane Z_PART_MIN = dz / 1000. # starting particle z value # Compute cathode area for geomtry-specific current calculations if (w3d.solvergeom == w3d.XYZgeom): # For 3D cartesion geometry only cathode_area = 4. * SOURCE_RADIUS_1 * SOURCE_RADIUS_2 else: # Assume 2D XZ geometry cathode_area = 2. * SOURCE_RADIUS_1 * 1. # If using the XZ geometry, set so injection uses the same geometry top.linj_rectangle = (w3d.solvergeom == w3d.XZgeom or w3d.solvergeom == w3d.XYZgeom) # Returns velocity beam_beta (in units of beta) for which frac of emitted particles have v < beam_beta * c beam_beta = sources.compute_cutoff_beta(CATHODE_TEMP, frac=0.99) PTCL_PER_STEP = particles_per_step if USER_INJECT == 1: CURRENT_MODIFIER = 0.5 # Factor to multiply CL current by when setting beam current # Constant current density - beam transverse velocity fixed to zero, very small longitduinal velocity # Set injection flag top.inject = 1 # 1 means constant; 2 means space-charge limited injection;# 6 means user-specified top.npinject = PTCL_PER_STEP beam_current = 4. / 9. * eps0 * sqrt(2. * echarge / background_beam.mass) \ * CONDUCTOR_VOLTS ** 1.5 / PLATE_SPACING ** 2 * cathode_area background_beam.ibeam = beam_current * CURRENT_MODIFIER background_beam.a0 = SOURCE_RADIUS_1 background_beam.b0 = SOURCE_RADIUS_2 background_beam.ap0 = .0e0 background_beam.bp0 = .0e0 w3d.l_inj_exact = True # Initial velocity settings (5% of c) vrms = np.sqrt(1 - 1 / (0.05 / 511e3 + 1)**2) * 3e8 top.vzinject = vrms if USER_INJECT == 2: # SC limited Thermionic injection top.inject = 2 # Set both beams to same npinject to keep weights the same background_beam.npinject = PTCL_PER_STEP top.finject = [1.0, 0.0] w3d.l_inj_exact = True # Specify thermal properties background_beam.vthz = np.sqrt(CATHODE_TEMP * kb_J / background_beam.mass) background_beam.vthperp = np.sqrt(CATHODE_TEMP * kb_J / background_beam.mass) top.lhalfmaxwellinject = 1 # inject z velocities as half Maxwellian beam_current = sources.j_rd( CATHODE_TEMP, CATHODE_PHI) * cathode_area # steady state current in Amps print('beam current expected: {}, current density {}'.format( beam_current, beam_current / cathode_area)) jcl = 0. if gap_voltage > 0.: jcl = 4. / 9. * eps0 * sqrt(2. * echarge / background_beam.mass) \ * CONDUCTOR_VOLTS ** 1.5 / PLATE_SPACING ** 2 * cathode_area print('child-langmuir limit: {}, current density {}'.format( jcl, jcl / cathode_area)) background_beam.ibeam = beam_current background_beam.a0 = SOURCE_RADIUS_1 background_beam.b0 = SOURCE_RADIUS_2 background_beam.ap0 = .0e0 background_beam.bp0 = .0e0 if USER_INJECT == 4: w3d.l_inj_exact = True w3d.l_inj_user_particles_v = True # Schottky model top.inject = 1 top.ninject = 1 top.lhalfmaxwellinject = 1 # inject z velocities as half Maxwellian top.zinject = np.asarray([Z_PART_MIN]) top.ainject = np.asarray([SOURCE_RADIUS_1]) top.binject = np.asarray([SOURCE_RADIUS_2]) top.finject = np.asarray([[1.0, 0.0]]) electric_field = 0 delta_w = np.sqrt(e**3 * electric_field / (4 * np.pi * eps0)) A0 = 1.20173e6 AR = A0 * lambdaR background_beam.a0 = SOURCE_RADIUS_1 background_beam.b0 = SOURCE_RADIUS_2 background_beam.ap0 = .0e0 background_beam.bp0 = .0e0 background_beam.vthz = np.sqrt(CATHODE_TEMP * kb_J / background_beam.mass) background_beam.vthperp = np.sqrt(CATHODE_TEMP * kb_J / background_beam.mass) # use Richardson current to estimate particle weight rd_current = AR * CATHODE_TEMP**2 * np.exp( -(CATHODE_PHI * e) / (CATHODE_TEMP * k)) * cathode_area electrons_per_second = rd_current / e electrons_per_step = electrons_per_second * dt background_beam.sw = electrons_per_step / PTCL_PER_STEP def schottky_emission(): # schottky emission at cathode side global num_particles_res Ez = solverE.getez() Ez_mean = np.mean(Ez[:, :, 0]) if w3d.inj_js == background_beam.js: delta_w = 0. if Ez_mean < 0.: delta_w = np.sqrt(beta * e**3 * np.abs(Ez_mean) / (4 * np.pi * eps0)) rd_current = AR * CATHODE_TEMP**2 * np.exp( -(CATHODE_PHI * e - delta_w) / (CATHODE_TEMP * k)) * cathode_area electrons_per_second = rd_current / e electrons_per_step = electrons_per_second * top.dt float_num_particles = electrons_per_step / background_beam.sw num_particles = int(float_num_particles + num_particles_res + np.random.rand()) num_particles_res += float_num_particles - num_particles # --- inject np particles of species electrons1 # --- Create the particles on the surface x = -background_beam.a0 + 2 * background_beam.a0 * np.random.rand( num_particles) y = -background_beam.b0 + 2 * background_beam.b0 * np.random.rand( num_particles) vz = np.random.rand(num_particles) vz = np.maximum(1e-14 * np.ones_like(vz), vz) vz = background_beam.vthz * np.sqrt(-2.0 * np.log(vz)) vrf = np.random.rand(num_particles) vrf = np.maximum(1e-14 * np.ones_like(vrf), vrf) vrf = background_beam.vthz * np.sqrt(-2.0 * np.log(vrf)) trf = 2 * np.pi * np.random.rand(num_particles) vx = vrf * np.cos(trf) vy = vrf * np.sin(trf) # --- Setup the injection arrays w3d.npgrp = num_particles gchange('Setpwork3d') # --- Fill in the data. All have the same z-velocity, vz1. w3d.xt[:] = x w3d.yt[:] = y w3d.uxt[:] = vx w3d.uyt[:] = vy w3d.uzt[:] = vz installuserparticlesinjection(schottky_emission) derivqty() print("weight:", background_beam.sw) ########################## # CONDUCTOR INSTALLATION # ########################## install_conductor = True # --- Anode Location zplate = Z_MAX # Create source conductors if install_conductor: emitter = ZPlane(zcent=w3d.zmmin, zsign=-1., voltage=0., condid=2) else: emitter = ZPlane(zcent=w3d.zmmin, zsign=-1., voltage=0.) # Create collector if install_conductor: collector = ZPlane(voltage=gap_voltage, zcent=zplate, condid=3) else: collector = ZPlane(voltage=gap_voltage, zcent=zplate) # Create grid if install_conductor: # Offsets to make sure grid is centered after install # Because case089 was not made with a hexagon at (0., 0.) it must be moved depending on STL file used and # version of STLconductor cxmin, cymin, czmin = -0.00260785012506, -0.00312974047847, 0.000135000009323 cxmax, cymax, czmax = 0.00339215015993, 0.00287025980651, 0.000335000018822 conductor = STLconductor( "honeycomb_case0.89t_xycenter_zcen470.stl", xcent=cxmin + (cxmax - cxmin) / 2., ycent=cymin + (cymax - cymin) / 2., #zcent=czmin + (czmax - czmin) / 2., disp=(0.,0.,1e-6), verbose="on", voltage=gate_voltage, normalization_factor=dz, condid=1) if install_conductor: installconductor(conductor, dfill=largepos) installconductor(emitter, dfill=largepos) installconductor(collector, dfill=largepos) scraper_diode = ParticleScraper([emitter, collector], lcollectlpdata=True, lsaveintercept=True) scraper_gate = ParticleScraper([conductor], lcollectlpdata=True, lsaveintercept=False) scraper_dictionary = {1: 'grid', 2: 'emitter', 3: 'collector'} else: installconductor(emitter, dfill=largepos) installconductor(collector, dfill=largepos) scraper = ParticleScraper([emitter, collector], lcollectlpdata=True, lsaveintercept=True) scraper_dictionary = {1: 'emitter', 2: 'collector'} ######################## # Hacked Grid Scraping # ######################## # Implements boxes with a reflection probability based on transparency attribute # used to crudely emulate particles reflected from the STLconductor honeycomb which cannot provide # scraper positions needed for reflection calculation grid_reflections = True if grid_reflections: grid_front = Box(xcent=0., ycent=0., zcent=0.135e-3 - dz / 2., xsize=2 * (X_MAX - X_MIN), ysize=2 * (Y_MAX - Y_MIN), zsize=dz) grid_back = Box(xcent=0., ycent=0., zcent=0.335e-3 + dz / 2, xsize=2 * (X_MAX - X_MIN), ysize=2 * (Y_MAX - Y_MIN), zsize=dz) scraper_front = ParticleScraperGrid(grid_front, lcollectlpdata=True, lsaveintercept=True) scraper_back = ParticleScraperGrid(grid_back, lcollectlpdata=True, lsaveintercept=True) # Fraction of particles expected to pass through the conductor (assuming they cross it in a single step) scraper_front.transparency = 0.80 scraper_back.transparency = 0.80 # Directional scraper to prevent particles from being scraped coming out of honeycomb interior scraper_front.directional_scraper = -1 scraper_back.directional_scraper = +1 ##################### # Diagnostics Setup # ##################### efield_diagnostic_0 = FieldDiagnostic.ElectrostaticFields( solver=solverE, top=top, w3d=w3d, comm_world=comm_world, period=fieldperiod, write_dir=os.path.join(file_path, 'fields')) installafterstep(efield_diagnostic_0.write) particle_diagnostic_0 = ParticleDiagnostic( period=particleperiod, top=top, w3d=w3d, species={species.name: species for species in listofallspecies}, comm_world=comm_world, lparallel_output=False, write_dir=file_path) installafterstep(particle_diagnostic_0.write) #################### # CONTROL SEQUENCE # #################### # prevent gist from starting upon setup top.lprntpara = false top.lpsplots = false top.verbosity = 1 # Reduce solver verbosity solverE.mgverbose = 1 # further reduce output upon stepping - prevents websocket timeouts in Jupyter notebook init_iters = 2000 regular_iters = 50 init_tol = 1e-5 regular_tol = 1e-6 # Time Step top.dt = dt # Define and install particle reflector if reflections: collector_reflector = ParticleReflector(scraper=scraper_diode, conductor=collector, spref=reflected_electrons, srefprob=srefprob, drefprob=drefprob, refscheme=reflection_scheme) installparticlereflector(collector_reflector) print("reflection_scheme = " + reflection_scheme) else: print("reflections: off") if grid_reflections: reflector_front = ParticleReflector(scraper=scraper_front, conductor=grid_front, spref=reflected_electrons, srefprob=srefprob, drefprob=0.75, refscheme=reflection_scheme) installparticlereflector(reflector_front) reflector_back = ParticleReflector(scraper=scraper_back, conductor=grid_back, spref=reflected_electrons, srefprob=srefprob, drefprob=0.75, refscheme=reflection_scheme) installparticlereflector(reflector_back) # initialize field solver and potential field solverE.mgmaxiters = init_iters solverE.mgtol = init_tol package("w3d") generate() # Specify particle weight for reflected_electrons reflected_electrons.sw = background_beam.sw print("weight: background_beam = {}, reflected = {}".format( background_beam.sw, reflected_electrons.sw)) solverE.mgmaxiters = regular_iters solverE.mgtol = regular_tol step(nsteps) #################### # Final Output # #################### surface_charge = analyze_collected_charge(top, solverE) if reflections: reflected_charge = analyze_reflected_charge(top, [collector_reflector], comm_world=comm_world) if comm_world.rank == 0: filename = os.path.join( file_path, "all_charge_anodeV_{}.h5".format(anode_voltage)) diag_file = h5.File(filename, 'w') # Write simulation parameters for key, val in settings.items(): diag_file.attrs[key] = val for dom_attr in [ 'xmmin', 'xmmax', 'ymmin', 'ymmax', 'zmmin', 'zmmax', 'nx', 'ny', 'nz' ]: diag_file.attrs[dom_attr] = eval('w3d.' + dom_attr) # Record scraped particles into scraper group of file scraper_data = diag_file.create_group('scraper') for key, val in scraper_dictionary.items(): scraper_data.attrs[val] = key for condid, cond_data in surface_charge.items(): cond_group = scraper_data.create_group('{}'.format( scraper_dictionary[condid])) for i, spec_dat in enumerate(cond_data): cond_group.create_dataset(listofallspecies[i].name, data=spec_dat) # Record reflected particles into reflector group if reflections: reflector_data = diag_file.create_group('reflector') for key in reflected_charge: reflector_data.attrs[scraper_dictionary[key]] = key for condid, ref_data in reflected_charge.items(): refl_group = reflector_data.create_group('{}'.format( scraper_dictionary[condid])) refl_group.create_dataset('reflected', data=ref_data) diag_file.close()
def main(x_struts, y_struts, V_grid, grid_height, strut_width, strut_height, rho_ew, T_em, phi_em, T_coll, phi_coll, rho_cw, gap_distance, rho_load, run_id, channel_width=100e-9, injection_type=2, magnetic_field=0.0, random_seed=True, install_grid=True, install_circuit=True, max_wall_time=1e9, particle_diagnostic_switch=False, field_diagnostic_switch=False, lost_diagnostic_switch=False): """ Run a simulation of a gridded TEC. Args: x_struts: Number of struts that intercept the x-axis. y_struts: Number of struts that intercept the y-axis V_grid: Voltage to place on the grid in Volts. grid_height: Distance from the emitter to the grid normalized by gap_distance, unitless. strut_width: Transverse extent of the struts in meters. strut_height: Longitudinal extent of the struts in meters. rho_ew: Emitter side wiring resistivity, ohms*cm. T_em: Emitter temperature, kelvin. phi_em: Emitter work function, eV. T_coll: Collector termperature, kelvin. phi_coll: Collector work function, eV. rho_cw: Collector side wiring resistivity, ohms*cm. gap_distance: Distance from emitter to collector, meters. rho_load: Load resistivity, ohms*cm. run_id: Run ID. Mainly used for parallel optimization. injection_type: 1: For constant current emission with only thermal velocity spread in z and CL limited emission. 2: For true thermionic emission. Velocity spread along all axes. random_seed: True/False. If True, will force a random seed to be used for emission positions. install_grid: True/False. If False the grid will not be installed. Results in simple parallel plate setup. If False then phi_em - phi_coll specifies the voltage on the collector. install_circuit: True/False. Include external circuit that will modulate gap voltage based on current flow and contact potential between cathode/anode. max_wall_time: Wall time to allow simulation to run for. Simulation is periodically checked and will halt if it appears the next segment of the simulation will exceed max_wall_time. This is not guaranteed to work since the guess is based on the run time up to that point. Intended to be used when running on system with job manager. particle_diagnostic_switch: True/False. Use openPMD compliant .h5 particle diagnostics. field_diagnostic_switch: True/False. Use rswarp electrostatic .h5 field diagnostics (Maybe openPMD compliant?). lost_diagnostic_switch: True/False. Enable collection of lost particle coordinates with rswarp.diagnostics.parallel.save_lost_particles. """ # record inputs and set parameters run_attributes = deepcopy(locals()) for key in run_attributes: if key in efficiency.tec_parameters: efficiency.tec_parameters[key][0] = run_attributes[key] # set new random seed if random_seed: top.seedranf(randint(1, 1e9)) # Control for printing in parallel if comm_world.size != 1: synchronizeQueuedOutput_mpi4py(out=True, error=True) if particle_diagnostic_switch or field_diagnostic_switch: # Directory paths diagDir = 'diags_id{}/hdf5/'.format(run_id) field_base_path = 'diags_id{}/fields/'.format(run_id) diagFDir = { 'magnetic': 'diags_id{}/fields/magnetic'.format(run_id), 'electric': 'diags_id{}/fields/electric'.format(run_id) } # Cleanup command if directories already exist if comm_world.rank == 0: cleanupPrevious(diagDir, diagFDir) load_balance = LoadBalancer() ###################### # DOMAIN/GEOMETRY/MESH ###################### # Dimensions X_MAX = +channel_width / 2. X_MIN = -X_MAX Y_MAX = +channel_width / 2. Y_MIN = -Y_MAX Z_MAX = gap_distance Z_MIN = 0. # TODO: cells in all dimensions reduced by 10x for testing, will need to verify if this is reasonable (TEMP) # Grid parameters dx_want = 5e-9 dy_want = 5e-9 dz_want = 5e-9 NUM_X = int(round(channel_width / dx_want)) # 20 #128 #10 NUM_Y = int(round(channel_width / dy_want)) # 20 #128 #10 NUM_Z = int(round(gap_distance / dz_want)) # mesh spacing dz = (Z_MAX - Z_MIN) / NUM_Z dx = channel_width / NUM_X dy = channel_width / NUM_Y print "Channel width: {}, DX = {}".format(channel_width, dx) print "Channel width: {}, DY = {}".format(channel_width, dy) print "Channel length: {}, DZ = {}".format(gap_distance, dz) # Solver Geometry and Boundaries # Specify solver geometry w3d.solvergeom = w3d.XYZgeom # Set field boundary conditions w3d.bound0 = neumann w3d.boundnz = dirichlet w3d.boundxy = periodic # Particles boundary conditions top.pbound0 = absorb top.pboundnz = absorb top.pboundxy = periodic # Set mesh boundaries w3d.xmmin = X_MIN w3d.xmmax = X_MAX w3d.ymmin = Y_MIN w3d.ymmax = Y_MAX w3d.zmmin = 0. w3d.zmmax = Z_MAX # Set mesh cell counts w3d.nx = NUM_X w3d.ny = NUM_Y w3d.nz = NUM_Z ############################# # PARTICLE INJECTION SETTINGS ############################# # Cathode and anode settings EMITTER_TEMP = T_em EMITTER_PHI = phi_em # work function in eV COLLECTOR_PHI = phi_coll # Can be used if vacuum level is being set ACCEL_VOLTS = V_grid # ACCEL_VOLTS used for velocity and CL calculations collector_voltage = phi_em - phi_coll # Emitted species background_beam = Species(type=Electron, name='background') measurement_beam = Species(type=Electron, name='measurement') # Emitter area and position SOURCE_RADIUS_1 = 0.5 * channel_width # a0 parameter - X plane SOURCE_RADIUS_2 = 0.5 * channel_width # b0 parameter - Y plane Z_PART_MIN = dz / 1000. # starting particle z value # Compute cathode area for geomtry-specific current calculations if (w3d.solvergeom == w3d.XYZgeom): # For 3D cartesion geometry only cathode_area = 4. * SOURCE_RADIUS_1 * SOURCE_RADIUS_2 else: # Assume 2D XZ geometry cathode_area = 2. * SOURCE_RADIUS_1 * 1. # If using the XZ geometry, set so injection uses the same geometry top.linj_rectangle = (w3d.solvergeom == w3d.XZgeom or w3d.solvergeom == w3d.XYZgeom) # Returns velocity beam_beta (in units of beta) for which frac of emitted particles have v < beam_beta * c beam_beta = sources.compute_cutoff_beta(EMITTER_TEMP, frac=0.99) PTCL_PER_STEP = 100 if injection_type == 1: CURRENT_MODIFIER = 0.5 # Factor to multiply CL current by when setting beam current # Constant current density - beam transverse velocity fixed to zero, very small longitduinal velocity # Set injection flag top.inject = 1 # 1 means constant; 2 means space-charge limited injection;# 6 means user-specified top.npinject = PTCL_PER_STEP beam_current = 4. / 9. * eps0 * sqrt(2. * echarge / background_beam.mass) \ * ACCEL_VOLTS ** 1.5 / gap_distance ** 2 * cathode_area background_beam.ibeam = beam_current * CURRENT_MODIFIER background_beam.a0 = SOURCE_RADIUS_1 background_beam.b0 = SOURCE_RADIUS_2 background_beam.ap0 = .0e0 background_beam.bp0 = .0e0 w3d.l_inj_exact = True # Initial velocity settings (5% of c) vrms = np.sqrt(1 - 1 / (0.05 / 511e3 + 1)**2) * 3e8 top.vzinject = vrms if injection_type == 2: # True Thermionic injection top.inject = 1 # Set both beams to same npinject to keep weights the same background_beam.npinject = PTCL_PER_STEP measurement_beam.npinject = PTCL_PER_STEP w3d.l_inj_exact = True # Specify thermal properties background_beam.vthz = measurement_beam.vthz = np.sqrt( EMITTER_TEMP * kb_J / background_beam.mass) background_beam.vthperp = measurement_beam.vthperp = np.sqrt( EMITTER_TEMP * kb_J / background_beam.mass) top.lhalfmaxwellinject = 1 # inject z velocities as half Maxwellian beam_current = sources.j_rd( EMITTER_TEMP, EMITTER_PHI) * cathode_area # steady state current in Amps print('beam current expected: {}, current density {}'.format( beam_current, beam_current / cathode_area)) jcl = 4. / 9. * eps0 * sqrt(2. * echarge / background_beam.mass) \ * ACCEL_VOLTS ** 1.5 / gap_distance ** 2 * cathode_area print('child-langmuir limit: {}, current density {}'.format( jcl, jcl / cathode_area)) background_beam.ibeam = measurement_beam.ibeam = beam_current background_beam.a0 = measurement_beam.a0 = SOURCE_RADIUS_1 background_beam.b0 = measurement_beam.b0 = SOURCE_RADIUS_2 background_beam.ap0 = measurement_beam.ap0 = .0e0 background_beam.bp0 = measurement_beam.bp0 = .0e0 derivqty() ############## # FIELD SOLVER ############## # Add Uniform B_z field if turned on if magnetic_field: bz = np.zeros([w3d.nx, w3d.ny, w3d.nz]) bz[:, :, :] = magnetic_field z_start = w3d.zmmin z_stop = w3d.zmmax top.ibpush = 2 addnewbgrd(z_start, z_stop, xs=w3d.xmmin, dx=(w3d.xmmax - w3d.xmmin), ys=w3d.ymmin, dy=(w3d.ymmax - w3d.ymmin), nx=w3d.nx, ny=w3d.ny, nz=w3d.nz, bz=bz) # Set up fieldsolver f3d.mgtol = 1e-6 solverE = MultiGrid3D() registersolver(solverE) ######################## # CONDUCTOR INSTALLATION ######################## if install_grid: accel_grid, gl = create_grid(x_struts, y_struts, V_grid, grid_height * gap_distance, strut_width, strut_height, channel_width) accel_grid.voltage = V_grid # --- Anode Location zplate = Z_MAX # Create source conductors if install_grid: source = ZPlane(zcent=w3d.zmmin, zsign=-1., voltage=0., condid=2) else: source = ZPlane(zcent=w3d.zmmin, zsign=-1., voltage=0.) # Create ground plate total_rho = efficiency.tec_parameters['rho_load'][0] if install_grid: plate = ZPlane(zcent=zplate, condid=3) if install_circuit: circuit = ExternalCircuit(top, solverE, total_rho, collector_voltage, cathode_area * 1e4, plate, debug=False) plate.voltage = circuit # plate.voltage = collector_voltage else: plate = ZPlane(zcent=zplate) if install_circuit: circuit = ExternalCircuit(top, solverE, total_rho, collector_voltage, cathode_area * 1e4, plate, debug=False) plate.voltage = circuit # plate.voltage = collector_voltage if install_grid: installconductor(accel_grid) installconductor(source, dfill=largepos) installconductor(plate, dfill=largepos) scraper = ParticleScraper([accel_grid, source, plate], lcollectlpdata=True, lsaveintercept=True) scraper_dictionary = {'grid': 1, 'source': 2, 'collector': 3} else: installconductor(source, dfill=largepos) installconductor(plate, dfill=largepos) scraper = ParticleScraper([source, plate], lcollectlpdata=True, lsaveintercept=True) scraper_dictionary = {'source': 1, 'collector': 2} ############# # DIAGNOSTICS ############# # Particle/Field diagnostic options if particle_diagnostic_switch: particleperiod = 250 # TEMP particle_diagnostic_0 = ParticleDiagnostic( period=particleperiod, top=top, w3d=w3d, species={species.name: species for species in listofallspecies}, # if species.name == 'measurement'}, # TEMP comm_world=comm_world, lparallel_output=False, write_dir=diagDir[:-5]) installafterstep(particle_diagnostic_0.write) if field_diagnostic_switch: fieldperiod = 1000 efield_diagnostic_0 = FieldDiagnostic.ElectrostaticFields( solver=solverE, top=top, w3d=w3d, comm_world=comm_world, period=fieldperiod) installafterstep(efield_diagnostic_0.write) # Set externally derived parameters for efficiency calculation efficiency.tec_parameters['A_em'][0] = cathode_area * 1e4 # cm**2 if install_grid: efficiency.tec_parameters['occlusion'][ 0] = efficiency.calculate_occlusion(**efficiency.tec_parameters) else: efficiency.tec_parameters['occlusion'][0] = 0.0 ########################## # SOLVER SETTINGS/GENERATE ########################## # prevent gist from starting upon setup top.lprntpara = false top.lpsplots = false top.verbosity = -1 # Reduce solver verbosity solverE.mgverbose = -1 # further reduce output upon stepping - prevents websocket timeouts in Jupyter notebook init_iters = 20000 regular_iters = 200 init_tol = 1e-6 regular_tol = 1e-6 # Time Step # Determine an appropriate time step based upon estimated final velocity if install_grid: vz_accel = sqrt(2. * abs(V_grid) * np.abs(background_beam.charge) / background_beam.mass) else: vz_accel = sqrt(2. * abs(collector_voltage) * np.abs(background_beam.charge) / background_beam.mass) vzfinal = vz_accel + beam_beta * c dt = dz / vzfinal top.dt = dt solverE.mgmaxiters = init_iters solverE.mgtol = init_tol package("w3d") generate() solverE.mgtol = regular_tol solverE.mgmaxiters = regular_iters print("weights (background) (measurement): {}, {}".format( background_beam.sw, measurement_beam.sw)) # Use rnpinject to set number of macroparticles emitted background_beam.rnpinject = PTCL_PER_STEP measurement_beam.rnpinject = 0 # measurement beam is off at start ################## # CONTROL SEQUENCE ################## # Run until steady state is achieved (flat current profile at collector) (measurement species turned on) # Record data for effiency calculation # Switch off measurement species and wait for simulation to clear (background species is switched on) early_abort = 0 # If true will flag output data to notify startup_time = 4 * gap_distance / vz_accel # ~4 crossing times to approach steady-state with external circuit crossing_measurements = 10 # Number of crossing times to record for steps_per_crossing = int(gap_distance / vz_accel / dt) ss_check_interval = int(steps_per_crossing / 2.) ss_max_checks = 8 # Maximum number of of times to run steady-state check procedure before aborting times = [] # Write out timing of cycle steps to file clock = 0 # clock tracks the current, total simulation-runtime # Run initial block of steps record_time(stept, times, startup_time) clock += times[-1] stop_initialization = top.it # for diag file print("Completed Initialization on Step {}\nInitialization run time: {}". format(top.it, times[-1])) # Start checking for Steady State Operation tol = 0.01 ss_flag = 0 check_count = 0 # Track number of times steady-state check performed while ss_flag != 1 and check_count < ss_max_checks: if (max_wall_time - clock) < times[-1]: early_abort = 1 break record_time(step, times, ss_check_interval * 4) clock += times[-1] tstart = (top.it - ss_check_interval) * top.dt _, current1 = plate.get_current_history(js=None, l_lost=1, l_emit=0, l_image=0, tmin=tstart, tmax=None, nt=1) current = np.sum(current1) if np.abs( current ) < 0.5 * efficiency.tec_parameters['occlusion'][0] * beam_current: # If too little current is getting through run another check cycle check_count += 1 print( "Completed check {}, insufficient current, running again for {} steps" .format(check_count, ss_check_interval)) continue ss_flag = 1 # print np.abs(current), 0.5 * efficiency.tec_parameters['occlusion'][0] * beam_current # try: # # If steady_state check initialized no need to do it again # steady_state # except NameError: # # If this is the first pass with sufficient current then initialize the check # if check_count == 0: # # If the initial period was long enough to get current on collector then use that # steady_state = SteadyState(top, plate, steps_per_crossing) # else: # # If we had to run several steady state checks with no current then just use the period with current # steady_state = SteadyState(top, plate, ss_check_interval) # # ss_flag = steady_state(steps_per_crossing) check_count += 1 stop_ss_check = top.it # For diag file # If there was a failure to reach steady state after specified number of checks then pass directly end if check_count == ss_max_checks: early_abort = -1 crossing_measurements = 0 print("Failed to reach steady state. Aborting simulation.") else: # Start Steady State Operation print( " Steady State Reached.\nStarting efficiency " "recording for {} crossing times.\nThis will be {} steps".format( crossing_measurements, steps_per_crossing * crossing_measurements)) # particle_diagnostic_0.period = steps_per_crossing #TEMP commented out # Switch to measurement beam species measurement_beam.rnpinject = PTCL_PER_STEP background_beam.rnpinject = 0 # Install Zcrossing Diagnostic ZCross = ZCrossingParticles(zz=grid_height * gap_distance / 200., laccumulate=1) emitter_flux = [] crossing_wall_time = times[ -1] * steps_per_crossing / ss_check_interval # Estimate wall time for one crossing print('crossing_wall_time estimate: {}, for {} steps'.format( crossing_wall_time, steps_per_crossing)) print('wind-down loop time estimate: {}, for {} steps'.format( crossing_wall_time * steps_per_crossing / ss_check_interval, ss_check_interval)) for sint in range(crossing_measurements): # Kill the loop and proceed to writeout if we don't have time to complete the loop if (max_wall_time - clock) < crossing_wall_time: early_abort = 2 break record_time(step, times, steps_per_crossing) clock += times[-1] # Re-evaluate time for next loop crossing_wall_time = times[-1] # Record velocities of emitted particles for later KE calculation velocity_array = np.array([ ZCross.getvx(js=measurement_beam.js), ZCross.getvy(js=measurement_beam.js), ZCross.getvz(js=measurement_beam.js) ]).transpose() # velocity_array = velocity_array[velocity_array[:, 2] >= 0.] # Filter particles moving to emitter emitter_flux.append(velocity_array) ZCross.clear() # Clear ZcrossingParticles memory print( "Measurement: {} of {} intervals completed. Interval run time: {} s" .format(sint + 1, crossing_measurements, times[-1])) stop_eff_calc = top.it # For diag file # Run wind-down until measurement particles have cleared measurement_beam.rnpinject = 0 background_beam.rnpinject = PTCL_PER_STEP initial_population = measurement_beam.npsim[0] measurement_tol = 0.03 # if particle_diagnostic_switch: # particle_diagnostic_0.period = ss_check_interval while measurement_beam.npsim[0] > measurement_tol * initial_population: # Kill the loop and proceed to writeout if we don't have time to complete the loop if (max_wall_time - clock ) < crossing_wall_time * ss_check_interval / steps_per_crossing: early_abort = 3 break record_time(step, times, ss_check_interval) clock += times[-1] # Record velocities of emitted particles for later KE calculation # Check is required here as measurement_beam particles will not always be passing through if ZCross.getvx(js=measurement_beam.js).shape[0] > 0: velocity_array = np.array([ ZCross.getvx(js=measurement_beam.js), ZCross.getvy(js=measurement_beam.js), ZCross.getvz(js=measurement_beam.js) ]).transpose() print "Backwards particles: {}".format( np.where(velocity_array[:, 2] < 0.)[0].shape[0]) # velocity_array = velocity_array[velocity_array[:, 2] >= 0.] # Filter particles moving to emitter emitter_flux.append(velocity_array) ZCross.clear() # Clear ZcrossingParticles memory print(" Wind-down: Taking {} steps, On Step: {}, {} Particles Left". format(ss_check_interval, top.it, measurement_beam.npsim[0])) stop_winddown = top.it # For diag file ###################### # CALCULATE EFFICIENCY ###################### try: emitter_flux = np.vstack(emitter_flux) except ValueError: # If this triggered then measurement emission never took place # Run took too long probably and abort took place emitter_flux = np.array([[0., 0., 0.]]) # Find integrated charge on each conductor surface_charge = analyze_scraped_particles(top, measurement_beam, solverE) measured_charge = {} for key in surface_charge: # We can abuse the fact that js=0 for background species to filter it from the sum measured_charge[key] = np.sum(surface_charge[key][:, 1] * surface_charge[key][:, 3]) # Set derived parameters from simulation efficiency.tec_parameters['run_time'][ 0] = crossing_measurements * steps_per_crossing * dt if crossing_measurements == 0: # Set to large value to force all powers and currents to zero efficiency.tec_parameters['run_time'][0] = 1e20 # Find total number of measurement particles that were emitted total_macroparticles = measurement_beam.npsim[0] + np.sum( [measured_charge[key] for key in surface_charge]) efficiency.tec_parameters['J_em'][0] = e * (total_macroparticles - measured_charge[scraper_dictionary['source']]) \ * measurement_beam.sw / \ efficiency.tec_parameters['run_time'][0] / efficiency.tec_parameters['A_em'][0] # If grid isn't being used then J_grid will not be in scraper dict try: efficiency.tec_parameters['J_grid'][0] = e * measured_charge[scraper_dictionary['grid']] * measurement_beam.sw / \ efficiency.tec_parameters['run_time'][0] / \ (efficiency.tec_parameters['occlusion'][0] * efficiency.tec_parameters['A_em'][0]) except KeyError: efficiency.tec_parameters['J_grid'][0] = 0.0 efficiency.tec_parameters['J_ec'][0] = e * measured_charge[scraper_dictionary['collector']] * measurement_beam.sw / \ efficiency.tec_parameters['run_time'][0] / efficiency.tec_parameters['A_em'][0] efficiency.tec_parameters['P_em'][0] = efficiency.calculate_power_flux( emitter_flux, measurement_beam.sw, efficiency.tec_parameters['phi_em'][0], **efficiency.tec_parameters) # Efficiency calculation print("Efficiency") efficiency_result = efficiency.calculate_efficiency( **efficiency.tec_parameters) print("Overall Efficiency: {}".format(efficiency_result['eta'])) print("Total steps: {}".format(top.it)) ###################### # FINAL RUN STATISTICS ###################### if comm_world.rank == 0: if not os.path.exists('diags_id{}'.format(run_id)): os.makedirs('diags_id{}'.format(run_id)) np.save('iv_data.npy', np.array([circuit.current_history, circuit.voltage_history])) write_parameter_file(run_attributes, filename='diags_id{}/'.format(run_id)) filename = 'efficiency_id{}.h5'.format(str(run_id)) with h5.File(os.path.join('diags_id{}'.format(run_id), filename), 'w') as h5file: # TODO: Add current history eff_group = h5file.create_group('/efficiency') run_group = h5file.create_group('/attributes') scrap_group = h5file.create_group('/scraper') h5file.attrs['complete'] = early_abort for key in efficiency_result: eff_group.attrs[key] = efficiency_result[key] for key in efficiency.tec_parameters: eff_group.attrs[key] = efficiency.tec_parameters[key] for key in run_attributes: run_group.attrs[key] = run_attributes[key] run_group.attrs['dt'] = top.dt run_group.attrs['stop_initialization'] = stop_initialization run_group.attrs['stop_ss_check'] = stop_ss_check run_group.attrs['stop_eff_calc'] = stop_eff_calc run_group.attrs['stop_winddown'] = stop_winddown # for key, value in scraper_dictionary.iteritems(): # scrap_group.attrs[key] = measured_charge[value] # inv_scraper_dict = { value: key for key, value in scraper_dictionary.iteritems() } for cond in solverE.conductordatalist: cond_objs = cond[0] scrap_group.attrs[inv_scraper_dict[ cond_objs.condid]] = measured_charge[cond_objs.condid] _, bckgrnd_current = cond_objs.get_current_history(js=0, l_lost=1, l_emit=0, l_image=0, tmin=None, tmax=None, nt=top.it) _, msrmnt_current = cond_objs.get_current_history(js=1, l_lost=1, l_emit=0, l_image=0, tmin=None, tmax=None, nt=top.it) scrap_group.create_dataset('{}_background'.format( inv_scraper_dict[cond_objs.condid]), data=bckgrnd_current) scrap_group.create_dataset('{}_measurement'.format( inv_scraper_dict[cond_objs.condid]), data=msrmnt_current) h5file.create_dataset('times', data=times)
beam_current = sources.cl_limit(CATHODE_PHI, ANODE_WF, GRID_BIAS, PLATE_SPACING)*cathode_area beam.ibeam = beam_current beam.a0 = SOURCE_RADIUS_1 beam.b0 = SOURCE_RADIUS_2 w3d.l_inj_exact = True elif USER_INJECT == 3: #Thermionic injection #Set injection flag top.inject = 6 # 1 means constant; 2 means space-charge limited injection;# 6 means user-specified beam_current = sources.j_rd(CATHODE_TEMP,CATHODE_PHI)*cathode_area #steady state current in Amps beam.ibeam = beam_current beam.a0 = SOURCE_RADIUS_1 beam.b0 = SOURCE_RADIUS_2 myInjector = injectors.injectorUserDefined(beam, CATHODE_TEMP, CHANNEL_WIDTH, Z_PART_MIN, PTCL_PER_STEP) installuserinjection(myInjector.inject_thermionic) # These must be set for user injection top.ainject = 1.0 top.binject = 1.0 derivqty()