def test_1_pass_DnTcontrol(self): """Test that we can pass and access attributes of a DnTcontrol Python object in C++. """ fncName = '('+__file__+') ' + sys._getframe().f_code.co_name + '():\n' print('\ntest: ', fncName) ctrl = DTcontrol_C() # Run identifier ctrl.title = "Test of argument-passing from Python to C++" # Run author ctrl.author = "tph" ctrl.time = 0.0 ctrl.dt = 0.5 ctrl.n_timesteps = 19 # Pass a DT_control argument to C++: test_so.function_with_DTcontrol_C_arg(ctrl) return
#-# User input #-# numberOfParticles = 1000 numberOfTrajectories = 0 numberOfTrajectories = min(numberOfTrajectories, numberOfParticles) ctrl = DTcontrol_C() # Timestepping ctrl.n_timesteps = 100 # 100000 ctrl.dt = 4.0e-7 # # Initialize time counters ctrl.timeloop_count = 0 ctrl.time = 0.0 # Set random seed ctrl.random_seed = 1 np_m.random.seed(ctrl.random_seed) # Electric field control randomExternalElectricFieldAmplitude = 0.1 # Set the switches to empty dictionaries here if and only if they'll get set when the species are # defined below. The default is that all defined electric fields will be applied to all # species. #ctrl.apply_solved_electric_field = {} #ctrl.apply_random_external_electric_field = {} # Initial conditions for the electron(s)
def test_1D_particle_source_region(self): """Set up a one-dimensional source region and display it. """ fncName = '(' + __file__ + ') ' + sys._getframe( ).f_code.co_name + '():\n' print('\ntest: ', fncName, '(' + __file__ + ')') ## Set control variables ctrl = DTcontrol_C() ctrl.title = "test_ParticleGeneration.py:test_1D_particle_source_region" ctrl.author = "tph" ctrl.timeloop_count = 0 ctrl.time = 0.0 ctrl.dt = 1.0e-6 ctrl.n_timesteps = 1 ctrl.write_trajectory_files = False pin = self.pin pin.coordinate_system = 'cartesian_x' pin.force_components = [ 'x', ] ### Particle input ## Define electron species charge = -1.0 * MyPlasmaUnits_C.elem_charge mass = 1.0 * MyPlasmaUnits_C.electron_mass dynamics = 'explicit' electrons_S = ParticleSpecies_C('electrons', charge, mass, dynamics) # Add the electrons to particle input pin.particle_species = (electrons_S, ) ## Make the particle storage array for all species. particle_P = Particle_C(pin, print_flag=True) ## Give the name of the .py file containing special particle data (lists of # particles, boundary conditions, source regions, etc.) userParticlesModuleName = "UserParticles_1D" # Import this module userParticlesModule = im_m.import_module(userParticlesModuleName) # particle_P.user_particles_module_name = userParticlesModuleName particle_P.user_particles_class = userParticlesClass = userParticlesModule.UserParticleDistributions_C ### Mesh input for a 1D mesh # Specify the mesh parameters umi1D = UserMeshInput_C() umi1D.pmin = df_m.Point(-10.0) umi1D.pmax = df_m.Point(10.0) umi1D.cells_on_side = (20, ) # Need the comma to indicate a tuple ### Create the 1D particle mesh and add to the Particle_C object pmesh1D = UserMesh_C(umi1D, compute_dictionaries=True, compute_cpp_arrays=False, compute_tree=True, plot_flag=False) particle_P.pmesh_M = pmesh1D ### Input for particle sources # For each source: # a. Define the source region # b. Provide a particle-generating function # c. Provide the species name and physical source parameters # Create numpy storage needed below driftVelocity = np_m.empty(particle_P.particle_dimension, dtype=particle_P.precision) ## 1. Hot electron source on left side of the mesh # a. Provide the species name and physical parameters # for this source speciesName = 'electrons' # Check that this species has been defined if speciesName in particle_P.species_names: charge = particle_P.charge[speciesName] mass = particle_P.mass[speciesName] else: print("The species", speciesName, "has not been defined") sys.exit() sourceDistributionType = 'functional' # Compute a physical number-density creation rate for this source region # The value should always be positive chargeDensityRate = -1.0 # The timestep interval between calls to the creation function timeStepInterval = 1 # Get number-density added per invocation numberDensity = chargeDensityRate * timeStepInterval * ctrl.dt / charge # Check for positivity if numberDensity < 0: print("Check number density for species", speciesName, "is negative. Should be positive") sys.exit() # Compute a value for thermalSpeed from the temperature in eV temperature_eV = 2.0 temp_joule = temperature_eV * MyPlasmaUnits_C.elem_charge thermalSpeed = np_m.sqrt(2.0 * temp_joule / mass) velocity_coordinate_system = 'x_y_z' # This sets the interpretation of the # following values # Set a drift velocity vdrift_1 = 1.0 vdrift_2 = 2.0 vdrift_3 = 3.0 # driftVelocity = (vdrift_1, vdrift_2, vdrift_3) driftVelocity[0] = vdrift_1 # The particle storage arrays have only a v[0] velocity component # driftVelocity[1] = vdrift_2 # driftVelocity[2] = vdrift_3 # Set desired particles-per-cell numberPerCell = 1 # Collect the parameters for this source in a dictionary hotElectronParams = { 'species_name': speciesName, 'source_distribution_type': sourceDistributionType, 'number_density': numberDensity, 'thermal_speed': thermalSpeed, 'velocity_coordinate_system': velocity_coordinate_system, 'drift_velocity': driftVelocity, 'timestep_interval': timeStepInterval, 'number_per_cell': numberPerCell } # b. Name the particle-creation function. maxwellianGenerator = particle_P.create_maxwellian_particles # c. Source-region geometry xmin = -9.0 xmax = -4.0 hotElectronsRegion = RectangularRegion_C(pmesh1D, xmin, xmax) ## 2. Background electrons over the whole mesh ## Specify one or more species to be generated in the 2nd region. # a. Provide the species name and physical parameters for this source # Note that this is the same species as in ## 1. above. It's not necessary # for each source region to have a different species name. speciesName = 'electrons' # Check that this species has been defined above if speciesName in particle_P.species_names: charge = particle_P.charge[speciesName] mass = particle_P.mass[speciesName] else: print("The species", speciesName, "has not been defined") sys.exit() sourceDistributionType = 'functional' # Compute a value for numberDensity, the physical number-density # creation rate for this source region. # The value should always be positive. chargeDensityRate = -0.1 # The timestep interval between calls to the creation function timeStepInterval = 1 # Get charge-density per invocation numberDensity = chargeDensityRate * timeStepInterval * ctrl.dt / charge # Check for positivity if numberDensity < 0: print("Check number density for species", speciesName, "is negative. Should be positive") sys.exit() # Compute a value for thermalSpeed temperature_eV = 2.0 temp_joule = temperature_eV * MyPlasmaUnits_C.elem_charge thermalSpeed = np_m.sqrt(2.0 * temp_joule / mass) velocity_coordinate_system = 'x_y_z' # This sets the interpretation of the # following values # Set a drift velocity vdrift_1 = 0.5 vdrift_2 = 1.5 vdrift_3 = 2.5 # driftVelocity = (vdrift_1, vdrift_2, vdrift_3) # The particle storage arrays have only a v[0] velocity component driftVelocity[0] = vdrift_1 # Set desired particles-per-cell numberPerCell = 10 # Collect the parameters for this source in a dictionary backgroundElectronParams = { 'species_name': speciesName, 'source_distribution_type': sourceDistributionType, 'number_density': numberDensity, 'thermal_speed': thermalSpeed, 'velocity_coordinate_system': velocity_coordinate_system, 'drift_velocity': driftVelocity, 'timestep_interval': timeStepInterval, 'number_per_cell': numberPerCell } # b. Name the first function that will generate particles in the above region: # Just use same maxwellianGenerator function as above. # c. Source geometry: in this case, it's the whole mesh: wholeMesh = WholeMesh_C(pmesh1D) ## Put all the particle source data into a dictionary # The dictionary keys are mnemonic source names. # The dictionary values are lists of tuples giving the # a. the physical source parameters. # b. particle creation function and # c. source region ## Note: the names here are source-region names, NOT species names! particleSourceDict = { 'hot_electron_source': (hotElectronParams, maxwellianGenerator, hotElectronsRegion), 'background_electron_source': (backgroundElectronParams, maxwellianGenerator, wholeMesh), } particle_P.particle_source_dict = particleSourceDict ### Create input for a particle trajectory object # Use an input object to collect initialization data for the trajectory object self.trajin = TrajectoryInput_C() self.trajin.maxpoints = None # Set to None to get every point self.trajin.extra_points = 1 # Set to 1 to make sure one boundary-crossing can be # accommodated. Set to a larger value if there are # multiple boundary reflections. self.trajin.explicit_dict = { 'names': [ 'x', 'ux', ], 'formats': [np_m.float32] * 2 } ## Create the trajectory object and attach it to the particle object. # No trajectory storage is created until particles # with TRAJECTORY_FLAG on are encountered. p_P = particle_P traj_T = Trajectory_C(self.trajin, ctrl, p_P.charged_species, p_P.neutral_species, p_P.species_index, p_P.mass, p_P.charge) particle_P.traj_T = traj_T ## Invoke the source functions and write out the particles ### Select output for particles ctrl.particle_output_file = "particleGeneration.h5part" ctrl.particle_output_interval = 1 ctrl.particle_output_attributes = ('species_index', 'x', 'ux', 'weight') # Check these values particle_P.check_particle_output_parameters(ctrl) particle_P.add_more_particles(ctrl) # Dump the particle data to a file particle_P.initialize_particle_output_file(ctrl) # Write the particle attributes particle_P.write_particles_to_file(ctrl) return
def test_3_function_initialized_particles(self): """ Test initialization of particles using a function over a region of the mesh. """ fncName = '(' + __file__ + ') ' + sys._getframe( ).f_code.co_name + '():\n' print('\ntest: ', fncName, '(' + __file__ + ')') ## Control struct ctrl = DTcontrol_C() ## Mesh input mi2d_UMI = UserMeshInput_C() # Replace with TUPLES since we're at toplevel mi2d_UMI.pmin = df_m.Point(-10.0, -10.0) mi2d_UMI.pmax = df_m.Point(10.0, 10.0) mi2d_UMI.cells_on_side = (2, 2) mi2d_UMI.diagonal = 'left' ## Create a 2D particle mesh, since we're going to initialize particles ## on a region of a mesh. # UserMesh_y_Fields_FE_XYZ_Module can make the mesh from the above input. plotFlag = False plotTitle = os.path.basename( __file__) + ": " + sys._getframe().f_code.co_name + ": mesh" pmesh2d_UM = UserMesh_C(mi2d_UMI, compute_dictionaries=True, compute_cpp_arrays=False, compute_tree=True, plot_flag=plotFlag, plot_title=plotTitle) p_P = self.particle_P userParticlesClass = p_P.user_particles_class ## 1. Initial hot electrons on bottom-left side of the mesh # a. Provide the species name and physical parameters # for this particle-initialization speciesName = 'plasma_electrons' # Check that this species has been defined if speciesName in p_P.species_names: charge = p_P.charge[speciesName] mass = p_P.mass[speciesName] else: print("The species", speciesName, "needs to be defined.") sys.exit() initialDistributionType = 'function_over_region' # Specify the initial physical number-density for this species numberDensity = 1.0e12 # Check for positivity if numberDensity <= 0: print("Check number density for species", speciesName, "is zero or negative. Should be positive") sys.exit() # Compute a value for thermalSpeed from the temperature in eV temperature_eV = 2.0 temp_joule = temperature_eV * MyPlasmaUnits_C.elem_charge thermalSpeed = np_m.sqrt(2.0 * temp_joule / mass) # Set a drift velocity # Set a drift velocity vdrift_1 = 1.0 vdrift_2 = 2.0 vdrift_3 = 3.0 velocity_coordinate_system = 'x_y_z' # This sets the interpretation of the # following values driftVelocity = (vdrift_1, vdrift_2, vdrift_3) # driftVelocity[:] = (1.0,) # Set desired particles-per-cell numberPerCell = 1000 # Collect the parameters for this initialization in a dictionary hotElectronParams = { 'species_name': speciesName, 'initial_distribution_type': initialDistributionType, 'number_density': numberDensity, 'thermal_speed': thermalSpeed, 'velocity_coordinate_system': velocity_coordinate_system, 'drift_velocity': driftVelocity, 'number_per_cell': numberPerCell } # b. Name the particle-creation function. maxwellianGenerator = p_P.create_maxwellian_particles # c. Initialization-region geometry xmin = -9.0 ymin = -9.0 xmax = -4.0 ymax = -4.0 # Don't use df_m in the toplevel routine, if possible # pmin = df_m.Point(xmin, ymin) # pmax = df_m.Point(xmax, ymax) pmin = (xmin, ymin) pmax = (xmax, ymax) hotElectronsRegion_RR = RectangularRegion_C(pmesh2d_UM, pmin, pmax) ## Put all the particle-initialization data into a dictionary # The dictionary keys are mnemonics for particle-initialization names. # The dictionary values are lists of tuples giving the # a. the physical initialization parameters. # b. particle creation function and # c. initialization region ## Note: the names here are particle-initialization names, NOT species names! initialParticlesDict = { 'initial_hot_electrons': (hotElectronParams, maxwellianGenerator, hotElectronsRegion_RR), # 'initial_background_electrons': (backgroundElectronParams, maxwellianGenerator, wholeMesh), } # Replace the 'listed' initial particles made in Setup() with # the 'function_initialized' initial particles created above p_P.initial_particles_dict = initialParticlesDict # Loop on the initialization methods in the dictionary # For 'listed' initialization, the dictionary has entries like # {ipName: (ipParams,)} # For 'function_over_region' initialization, the dictionary has entries # like # {ipName: (ipParams, ipFunc, ipRegion_RR)} nCreatedTotal = 0 for ipName, ipTuple in initialParticlesDict.items(): ipParams = ipTuple[0] s = ipParams['species_name'] initialDistributionType = ipParams['initial_distribution_type'] if initialDistributionType == 'function_over_region': print(fncName, "Initializating", ipName, "particles") ipFunc = ipTuple[1] ipRegion_RR = ipTuple[2] # Invoke the creation function step = 0 time = 0.0 neg_E_field = None ipFunc(step, time, ipRegion_RR, ipParams, neg_E_field) # Check the number of stored particles for each species nCreated = ipRegion_RR.ncell * ipParams['number_per_cell'] print(fncName, "Number of particles created =", nCreated) nCreatedTotal += nCreated nStored = p_P.get_species_particle_count(s, print_flag=False) self.assertEqual( nStored, nCreated, msg="Number of stored particles is not correct") # check total number of stored particles nStored = p_P.get_total_particle_count(print_flag=False) print(fncName, "Total number of particles created =", nCreatedTotal) self.assertEqual(nStored, nCreatedTotal, msg="Total number of stored particles is not correct") # Write out the particles to an HDF5 file ctrl.timeloop_count = 0 ctrl.time = 0.0 # Run identifier ctrl.title = "test_ParticleInitialization" # Run author ctrl.author = "tph" ### Select output for particles ctrl.particle_output_file = "particleInitialization.h5part" ctrl.particle_output_interval = 1 ctrl.particle_output_attributes = ('species_index', 'x', 'y', 'z', 'ux', 'uy', 'uz', 'weight') # Check these values p_P.check_particle_output_parameters(ctrl) # Dump the particle data to a file p_P.initialize_particle_output_file(ctrl) # Write the particle attributes p_P.write_particles_to_file(ctrl) return
def test_3D_particle_migration(self): """Test particle migration across cells on a 3D mesh. """ fncName = '('+__file__+') ' + sys._getframe().f_code.co_name + '():\n' print('\ntest: ', fncName) ctrl = DTcontrol_C() ctrl.time = 0.0 ctrl.dt = 0.5 ctrl.n_timesteps = 19 ctrl.MAX_FACET_CROSS_COUNT = 100 # Run for 3D mesh self.particle_P.pmesh_M = self.pmesh3D_M self.particle_P.initialize_particle_integration() # Get the initial cell index of each particle. self.particle_P.compute_mesh_cell_indices() # Save the initial conditions (p_ic) for plotting p_ic = [] sp = self.particle_P.neutral_species[0] for ip in [0, 1]: (pseg, offset) = self.particle_P.sap_dict[sp].get_segment_and_offset(ip) p = pseg[offset].copy() # Have to make a copy! Otherwise you overwrite the only copy of the particle p_ic.append(p) # The expected final position and cell # First particle xsp0 = -9.5; ysp0 = -9.5; zsp0 = 0.0 vxsp0 = -2.0; vysp0 = 0.0; vzsp0 = 0.0 weight0 = 2.0 bitflag0 = 2 cell_index0 = 98 unique_ID0 = 0 crossings = 0 psp0 = (xsp0,ysp0,zsp0, vxsp0,vysp0,vzsp0, weight0, bitflag0, cell_index0, unique_ID0, crossings) # Second particle xsp1 = -9.5; ysp1 = -9.5; zsp1 = -9.5 vxsp1 = -2.0; vysp1 = -2.0; vzsp1 = -2.0 weight1 = 3.0 bitflag1 = 2 cell_index1 = 0 unique_ID1 = 0 crossings = 0 psp1 = (xsp1,ysp1,zsp1, vxsp1,vysp1,vzsp1, weight1, bitflag1, cell_index1, unique_ID1, crossings) p_expected = (psp0, psp1) # Integrate for n_timesteps print("Advancing", self.particle_P.get_total_particle_count(), "particles for", ctrl.n_timesteps, "timesteps on a 3D mesh") for istep in range(ctrl.n_timesteps): # Advance each species for 1 timestep for sp in self.particle_P.species_names: self.particle_P.integrators[sp](sp, ctrl) # Create a mesh plotter to display the trajectory (just the # first and last positions) plotTitle = os.path.basename(__file__) + ": " + sys._getframe().f_code.co_name + ": First & last positions" if self.plot_results is True: plotter=df_m.plot(self.particle_P.pmesh_M.mesh, title=plotTitle) # Check the results ncoords = self.particle_P.particle_dimension # number of particle coordinates to check for sp in self.particle_P.neutral_species: for ip in [0, 1]: (pseg, offset) = self.particle_P.sap_dict[sp].get_segment_and_offset(ip) getparticle = pseg[offset] # Retrieve the particle from the SAP. if self.plot_results is True: mplot_m.plot([p_ic[ip][0], getparticle[0]], [p_ic[ip][1], getparticle[1]], [p_ic[ip][2], getparticle[2]]) # print 'expected = ', p_expected[ip] # print 'calculated = ', getparticle # path = np_m.array([p_ic[ip][0], p_ic[ip][1], p_ic[ip][2], getparticle[0], getparticle[1], getparticle[2]]) # Replace this with a point plot: # plotter.add_polygon(path) for ic in range(ncoords): self.assertAlmostEqual(p_expected[ip][ic], getparticle[ic], places=6, msg="Particle is not in correct position") cell_index_position = -3 # print fncName, "expected cell =", p_expected[ip][cell_index_position], "computed cell =", getparticle[cell_index_position] self.assertEqual(p_expected[ip][cell_index_position], getparticle[cell_index_position], msg="Particle is not in correct cell") if self.plot_results is True: mplot_m.show() # yesno = raw_input("Just called show() in test_3D_particle_migration") return
def test_1D_particle_migration(self): """Test particle migration across cells on a 1D mesh. """ fncName = '('+__file__+') ' + sys._getframe().f_code.co_name + '():\n' print('\ntest: ', fncName) # List all the possible spatial coordinates # spatial_coordinates = ('x','y','z') ctrl = DTcontrol_C() # Run identifier ctrl.title = "test_ParticleMigration.py:test_1D_particle_migration" # Run author ctrl.author = "tph" ctrl.time = 0.0 ctrl.dt = 0.5 ctrl.n_timesteps = 19 ctrl.MAX_FACET_CROSS_COUNT = 100 # Run for 1D mesh self.particle_P.pmesh_M = self.pmesh1D self.particle_P.initialize_particle_integration() # Get the initial cell index of each particle. self.particle_P.compute_mesh_cell_indices() ### Put the expected ending results into the p_expected tuple ### # First particle xsp0 = -9.5; ysp0 = -9.5; zsp0 = 0.0 vxsp0 = -2.0; vysp0 = 0.0; vzsp0 = 0.0 weight0 = 2.0 bitflag0 = 2 cell_index0 = 0 unique_ID0 = 0 crossings = 0 psp0 = (xsp0,ysp0,zsp0, vxsp0,vysp0,vzsp0, weight0, bitflag0, cell_index0, unique_ID0, crossings) # Second particle xsp1 = -9.5; ysp1 = -9.5; zsp1 = -9.5 vxsp1 = -2.0; vysp1 = -2.0; vzsp1 = -2.0 weight1 = 3.0 bitflag1 = 2 cell_index1 = 0 unique_ID1 = 1 crossings = 0 psp1 = (xsp1,ysp1,zsp1, vxsp1,vysp1,vzsp1, weight1, bitflag1, cell_index1, unique_ID1, crossings) p_expected = (psp0, psp1) # Integrate for n_timesteps print("Advancing", self.particle_P.get_total_particle_count(), "particles for", ctrl.n_timesteps, "timesteps on a 1D mesh") for istep in range(ctrl.n_timesteps): # Advance each species for 1 timestep for sp in self.particle_P.neutral_species: self.particle_P.integrators[sp](sp, ctrl) # Check the results ncoords = self.particle_P.particle_dimension # number of particle coordinates to check for sp in self.particle_P.neutral_species: for ip in [0, 1]: (pseg, offset) = self.particle_P.sap_dict[sp].get_segment_and_offset(ip) getparticle = pseg[offset] # Retrieve the particle from the SAP. # print 'expected = ', p_expected[ip] # print 'calculated = ', getparticle for ic in range(ncoords): self.assertAlmostEqual(p_expected[ip][ic], getparticle[ic], places=6, msg="Particle is not in correct position") cell_index_position = -3 # print fncName, "expected cell =", p_expected[ip][cell_index_position], "computed cell =", getparticle[cell_index_position] self.assertEqual(p_expected[ip][cell_index_position], getparticle[cell_index_position], msg="Particle is not in correct cell") return
def test_1_electric_field_push_1step(self): """ Check that the electric field push is correct. Push test particles for 1 step on a 2D 1/4-circle mesh. """ fncName = '(' + __file__ + ') ' + sys._getframe( ).f_code.co_name + '():\n' print('\ntest: ', fncName, '(' + __file__ + ')') ctrl = DTcontrol_C() ctrl.dt = 1.0e-5 ctrl.n_timesteps = 1 ctrl.MAX_FACET_CROSS_COUNT = 100 dt = ctrl.dt # The expected results from ParticleNonuniformE.ods # First electron xp1 = 1.005609924 yp1 = 0.8131367827 zp1 = 0.0 vxp1 = -235314.728666394 vyp1 = -254562.042790713 vzp1 = 0.0 weight1 = 1.0 p1 = (xp1, yp1, vxp1, vyp1, weight1) # Second electron xp2 = -9.3684645671 yp2 = -10.1983686032 zp2 = 0.0 vxp2 = -1014628.2027 vyp2 = -1097618.606318 vzp2 = 0.0 weight2 = 3.0 p2 = (xp2, yp2, vxp2, vyp2, weight2) p_expected = (p1, p2) # species_names = self.particle_P.names # first particle: getp = self.particle_P.sap_dict['two_electrons'].get_item(0) # print 'getp is a', type(getp) # print('1st electron:', getp) # second particle: getparticle = self.particle_P.sap_dict['two_electrons'].get_item(1) # print('2nd electron:', getparticle) # Advance the particles one timestep print("Moving", self.particle_P.get_total_particle_count(), "particles for one timestep") ctrl.time_step = 0 ctrl.time = 0.0 self.particle_P.advance_charged_particles_in_E_field( ctrl, neg_E_field=self.neg_electric_field) # Check the results ncoords = self.particle_P.particle_dimension # number of particle coordinates to check isp = 0 for sp in self.particle_P.species_names: # print 'species count = ', self.particle_P.get_species_particle_count(sp) if self.particle_P.get_species_particle_count(sp) == 0: continue # Check that the first two particles in the array reaches the correct values for ip in [0, 1]: (pseg, offset ) = self.particle_P.sap_dict[sp].get_segment_and_offset(ip) getparticle = pseg[offset] # print('calculated = ', getparticle) # print('expected = ', p_expected[ip]) for ic in range(ncoords): # print "result:", getparticle[ic]/p_expected[ip][ic] # Note: for different field solver, may have to reduce the places: self.assertAlmostEqual( getparticle[ic] / p_expected[ip][ic], 1.0, places=6, msg="Particle is not in correct position") # print "ic", ic, "is OK" isp += 1 return
def test_3_2D_r_theta_absorbing_boundary(self): """ Check that particles are deleted correctly when they cross a 2D absorbing boundary. The particles are electrons and there's an applied Electric field """ fncName = '(' + __file__ + ') ' + sys._getframe( ).f_code.co_name + '():\n' print('\ntest: ', fncName) if os.environ.get('DISPLAY') is None: plotFlag = False else: plotFlag = self.plot_results ## Set control variables ctrl = DTcontrol_C() # Run identifier ctrl.title = "test_ParticleBoundaryConditions.py:test_3_2D_r_theta_absorbing_boundary" # Run author ctrl.author = "tph" ctrl.timeloop_count = 0 ctrl.time = 0.0 # These are fast electrons, so the timestep is small ctrl.dt = 1.0e-6 ctrl.n_timesteps = 14 ctrl.MAX_FACET_CROSS_COUNT = 100 ctrl.write_trajectory_files = False ### Particle species input # Create an instance of the DTparticleInput class pin = ParticleInput_C() # Initialize particles pin.precision = numpy.float64 pin.particle_integration_loop = 'loop-on-particles' pin.coordinate_system = 'cartesian_xy' pin.force_components = [ 'x', 'y', ] pin.force_precision = numpy.float64 pin.use_cpp_integrators = False # Use C++ code to advance particles. # Specify the particle-species properties # Define an electron species. Use this name later to initialize this species # or to create a source of this species. speciesName = 'trajelectrons' charge = -1.0 * MyPlasmaUnits_C.elem_charge mass = 1.0 * MyPlasmaUnits_C.electron_mass dynamics = 'explicit' trajelectrons_S = ParticleSpecies_C(speciesName, charge, mass, dynamics) # Add the electrons to particle input pin.particle_species = (trajelectrons_S, ) ## Make the particle storage array for all species. particle_P = Particle_C(pin, print_flag=False) ## Give the name of the .py file containing special particle data (lists of # particles, boundary conditions, source regions, etc.) userParticlesModuleName = "UserParticles_2D_e" # Import this module userParticlesModule = im_m.import_module(userParticlesModuleName) # particle_P.user_particles_module_name = userParticlesModuleName particle_P.user_particles_class = userParticlesClass = userParticlesModule.UserParticleDistributions_C ### Add a ref to a Trajectory_C object to particle_P # Create input object for trajectories trajin = TrajectoryInput_C() trajin.maxpoints = None # Set to None to get every point trajin.extra_points = 1 # Set to 1 to make sure one boundary-crossing can be # accommodated. # Specify which particle variables to save ('step' and 't' are required). This has the # form of a numpy dtype specification. format_list_base = [int] format_list = format_list_base + [numpy.float32] * 7 trajin.charged_dict = { 'names': ['step', 't', 'x', 'ux', 'y', 'uy', 'Ex', 'Ey'], 'formats': format_list } format_list = format_list_base + [numpy.float32] * 4 trajin.implicit_dict = { 'names': ['step', 't', 'x', 'ux', 'phi'], 'formats': format_list } format_list = format_list_base + [numpy.float32] * 5 trajin.neutral_dict = { 'names': ['step', 't', 'x', 'ux', 'y', 'uy'], 'formats': format_list } ### Mesh and Fields input for the particle mesh. ## The mesh input from UserMesh_y_Fields_FE2D_Module import UserMeshInput2DCirc_C ## The mesh to be created is in an existing file umi = UserMeshInput2DCirc_C() umi.mesh_file = 'mesh_quarter_circle_crossed.xml' umi.particle_boundary_file = 'mesh_quarter_circle_crossed_Pbcs.xml' # These are the boundary-name -> int pairs used to mark mesh facets: rminIndx = 1 rmaxIndx = 2 thminIndx = 4 thmaxIndx = 8 particleBoundaryDict = { 'rmin': rminIndx, 'rmax': rmaxIndx, 'thmin': thminIndx, 'thmax': thmaxIndx, } umi.particle_boundary_dict = particleBoundaryDict ## Create the particle mesh from UserMesh_y_Fields_FE2D_Module import UserMesh2DCirc_C pmesh_M = UserMesh2DCirc_C(umi, compute_dictionaries=True, compute_cpp_arrays=False, compute_tree=True, plot_flag=self.plot_mesh) # Add this to the particle object: particle_P.pmesh_M = pmesh_M ## Get the electric field from an existing file # The following value should correspond to the element degree # used in the potential from which negE was obtained phiElementDegree = 1 if phiElementDegree == 1: # For linear elements, grad(phi) is discontinuous across # elements. To represent this field, we need Discontinuous Galerkin # elements. electricFieldElementType = 'DG' else: electricFieldElementType = 'Lagrange' # Create the negative electric field directly on the particle mesh negElectricField = Field_C(particle_P.pmesh_M, element_type=electricFieldElementType, element_degree=phiElementDegree - 1, field_type='vector') file = df_m.File('negE_test_2_2D.xml') file >> negElectricField.function ### Input for initial particles (i.e., particles present at t=0) # Name the species (it should be in species_names above) speciesName = 'trajelectrons' # Check that this species has been defined above if speciesName not in particle_P.species_names: print("The species", speciesName, "has not been defined") sys.exit() initialDistributionType = 'listed' # Check that there's a function listing the particles particles printFlag = True if hasattr(userParticlesClass, speciesName): if printFlag: print(fncName + "(DnT INFO) Initial distribution for", speciesName, "is the function of that name in", userParticlesClass) # Write error message and exit if no distribution function exists else: errorMsg = fncName + "(DnT ERROR) Need to define a particle distribution function %s in UserParticle.py for species %s " % ( speciesName, speciesName) sys.exit(errorMsg) # Collect the parameters into a dictionary # For a 'listed' type, there needs to be a function with the same name as the # species in userParticlesClass trajElectronParams = { 'species_name': speciesName, 'initial_distribution_type': initialDistributionType, } # The dictionary keys are mnemonics strings for identifying each set of # initialized particles. They're not the names of particle species. initialParticlesDict = { 'initial_trajelectrons': (trajElectronParams, ), } # Add the initialized particles to the Particle_C object particle_P.initial_particles_dict = initialParticlesDict ## Particle boundary-conditions # UserParticleBoundaryFunctions_C is where the facet-crossing callback # functions are defined. userPBndFnsClass = userParticlesModule.UserParticleBoundaryFunctions_C # abbreviation # Make the particle-mesh boundary-conditions object and add it # to the particle object. spNames = particle_P.species_names pmeshBCs = ParticleMeshBoundaryConditions_C(spNames, pmesh_M, userPBndFnsClass, print_flag=False) particle_P.pmesh_bcs = pmeshBCs # The trajectory object can now be created and added to particle_P p_P = particle_P # p_P.traj_T = Trajectory_C(trajin, ctrl, p_P.explicit_species, p_P.implicit_species, p_P.neutral_species) p_P.traj_T = Trajectory_C(trajin, ctrl, p_P.charged_species, p_P.neutral_species, p_P.species_index, p_P.mass, p_P.charge) # Initialize the particles printFlags = {} for sp in p_P.species_names: printFlags[sp] = False p_P.initialize_particles(printFlags) # Get the initial cell index of each particle. p_P.compute_mesh_cell_indices() p_P.initialize_particle_integration() ### Particle loop print("Moving", p_P.get_total_particle_count(), "particles for", ctrl.n_timesteps, "timesteps") for istep in range(ctrl.n_timesteps): # print("istep", istep) if p_P.traj_T is not None: # print 'p_P.traj_T.skip:', p_P.traj_T.skip if istep % p_P.traj_T.skip == 0: p_P.record_trajectory_data(ctrl.timeloop_count, ctrl.time, neg_E_field=negElectricField) p_P.advance_charged_particles_in_E_field( ctrl, neg_E_field=negElectricField) ctrl.timeloop_count += 1 ctrl.time += ctrl.dt # Record the LAST point on the particle trajectory if p_P.traj_T is not None: p_P.record_trajectory_data(ctrl.timeloop_count, ctrl.time, neg_E_field=negElectricField) # Plot the trajectory onto the particle mesh if self.plot_results is True: mesh = p_P.pmesh_M.mesh plotTitle = os.path.basename( __file__) + ": " + sys._getframe().f_code.co_name + ": XY mesh" holdPlot = True # Set to True to stop the plot from disappearing. p_P.traj_T.plot_trajectories_on_mesh( mesh, plotTitle, hold_plot=holdPlot ) # Plots trajectory spatial coordinates on top of the particle mesh return
def test_1_2D_x_y_absorbing_boundary(self): """ Check that particles are deleted correctly when they cross an absorbing boundary on a 2D Cartesian mesh. The particles are neutral H. """ fncName = '(' + __file__ + ') ' + sys._getframe( ).f_code.co_name + '():\n' print('\ntest: ', fncName) if os.environ.get('DISPLAY') is None: plotFlag = False else: plotFlag = self.plot_mesh ctrl = DTcontrol_C() # Run identifier ctrl.title = "test_ParticleBoundaryConditions.py:test_1_2D_x_y_absorbing_boundary" # Run author ctrl.author = "tph" ctrl.timeloop_count = 0 ctrl.time = 0.0 ctrl.dt = 0.5 ctrl.n_timesteps = 100 ctrl.MAX_FACET_CROSS_COUNT = 100 ctrl.write_trajectory_files = False # Create an instance of the DTparticleInput class pin = ParticleInput_C() # Settings common to all species pin.precision = numpy.float64 pin.particle_integration_loop = 'loop-on-particles' pin.coordinate_system = 'cartesian_xyz' pin.force_precision = numpy.float64 pin.use_cpp_integrators = False # Use C++ code to advance particles. # Specify the particle species properties speciesName = 'neutral_H' # Use this name later to initialize this # species or to create a source of this # species. charge = 0.0 mass = 1.0 * MyPlasmaUnits_C.AMU dynamics = 'neutral' neutralH_S = ParticleSpecies_C(speciesName, charge, mass, dynamics) # Add the neutral hydrogen to particle input pin.particle_species = (neutralH_S, ) ## Make the particle storage array for all species. particle_P = Particle_C(pin, print_flag=False) # Provide the particle distributions for the above species # This could be done more like how the mesh is specified: # import UserParticles_3D as userParticlesModule # Particles have a 3D/3D phase-space even though mesh is just 2D userParticlesModuleName = 'UserParticles_3D' # Import this module infoMsg = "%s\tImporting %s" % (fncName, userParticlesModuleName) print(infoMsg) userParticlesModule = im_m.import_module(userParticlesModuleName) # particle_P.user_particles_module_name = userParticlesModuleName particle_P.user_particles_class = userParticlesClass = userParticlesModule.UserParticleDistributions_C ### Create a trajectory object and add it to particle_P # Create input object for trajectories trajin = TrajectoryInput_C() trajin.maxpoints = None # Set to None to get every point trajin.extra_points = 1 # Set to 1 to make sure one boundary-crossing can be # accommodated. # Specify which particle variables to save. This has the # form of a numpy dtype specification. name_list_base = ['step', 't'] # These are always recorded format_list_base = [int, numpy.float32 ] # Start off the format list with types for # 'step' and 't' charged_attributes = ['x', 'ux', 'y', 'uy', 'Ex', 'Ey'] format_list = format_list_base + [numpy.float32 ] * len(charged_attributes) trajin.charged_dict = { 'names': name_list_base + charged_attributes, 'formats': format_list } #implicit_attributes = ['x', 'ux', 'phi'] #format_list = format_list_base + [numpy.float32]*len(implicit_attributes) # trajin.implicit_dict = {'names': name_list_base+implicit_attributes, 'formats': format_list} neutral_attributes = ['x', 'ux', 'y', 'uy'] format_list = format_list_base + [numpy.float32 ] * len(neutral_attributes) trajin.neutral_dict = { 'names': name_list_base + neutral_attributes, 'formats': format_list } # Add a traj_T reference to the particle object p_P = particle_P # abbreviation #p_P.traj_T = Trajectory_C(trajin, ctrl, p_P.explicit_species, p_P.implicit_species, p_P.neutral_species) p_P.traj_T = Trajectory_C(trajin, ctrl, p_P.charged_species, p_P.neutral_species, p_P.species_index, p_P.mass, p_P.charge) ## Mesh input for the particle mesh, including particle boundary conditions. # Create a 2D Cartesian mesh to use for advancing the particles. The particles # themselves are given 3D coordinates. infoMsg = "%s\tImporting UserMeshInput_C from UserMesh_y_Fields_FE_XYZ_Module" % ( fncName) print(infoMsg) from UserMesh_y_Fields_FE_XYZ_Module import UserMeshInput_C # 2D mesh input umi2D = UserMeshInput_C() (xmin, ymin) = (-10.0, -10.0) (xmax, ymax) = (10.0, 10.0) umi2D.pmin = df_m.Point(xmin, ymin) umi2D.pmax = df_m.Point(xmax, ymax) umi2D.cells_on_side = (4, 2) # umi2D.diagonal = 'crossed' # This could be automated, given a list of the boundary names: ['xmin', 'xmax', ...] xminIndx = 1 xmaxIndx = 2 yminIndx = 4 ymaxIndx = 8 particleBoundaryDict = { 'xmin': xminIndx, 'xmax': xmaxIndx, 'ymin': yminIndx, 'ymax': ymaxIndx, } umi2D.particle_boundary_dict = particleBoundaryDict ## Create the 2D Cartesian mesh infoMsg = "%s\tImporting UserMesh_C from UserMesh_y_Fields_FE_XYZ_Module" % ( fncName) print(infoMsg) from UserMesh_y_Fields_FE_XYZ_Module import UserMesh_C plotTitle = os.path.basename( __file__) + ": " + sys._getframe().f_code.co_name + ": XY mesh" pmesh_M = UserMesh_C(umi2D, compute_dictionaries=True, compute_cpp_arrays=False, compute_tree=True, plot_flag=self.plot_mesh, plot_title=plotTitle) # Add this to the particle object: # p_P.pmesh_M = pmesh_M # 1. Attach the particle mesh to p_P. # 2. Attach the Python particle movers. # 3. Compute the cell-neighbors and facet-normals for the particle movers. p_P.initialize_particle_mesh(pmesh_M) ### Input for initial particles (i.e., particles present at t=0) # a. Name the species (it should be in species_names above) speciesName = 'neutral_H' # Check that this species has been defined above if speciesName not in p_P.species_names: print("The species", speciesName, "has not been defined") sys.exit() initialDistributionType = 'listed' # Check that there's a function listing the particles particles printFlag = True if hasattr(userParticlesClass, speciesName): if printFlag: print(fncName + "(DnT INFO) Initial distribution for", speciesName, "is the function of that name in", userParticlesClass) # Write error message and exit if no distribution function exists else: errorMsg = fncName + "(DnT ERROR) Need to define a particle distribution function %s in UserParticle.py for species %s " % ( speciesName, speciesName) sys.exit(errorMsg) # Collect the parameters into a dictionary # The 'listed' type will expect a function with the same name as the species. neutralHParams = { 'species_name': speciesName, 'initial_distribution_type': initialDistributionType, } # The dictionary keys are mnemonics for initialized particle distributions initialParticlesDict = { 'initial_neutral_H': (neutralHParams, ), } # Add the initialized particles to the Particle_C object p_P.initial_particles_dict = initialParticlesDict # Particle boundary-conditions # UserParticleBoundaryFunctions_C is where the facet-crossing callback # functions are defined by the user. # Make an instance of UserParticleBoundaryFunctions_C userPBndFns = userParticlesModule.UserParticleBoundaryFunctions_C # abbreviation # Make the particle-mesh boundary-conditions object and add it to the # particle object. The user has to supply the facet-crossing callback # functions in the UserParticleBoundaryFunctions_C object above. spNames = p_P.species_names pmeshBCs = ParticleMeshBoundaryConditions_C(spNames, pmesh_M, userPBndFns, print_flag=False) p_P.pmesh_bcs = pmeshBCs # Create the initial particles printFlags = {} for sp in p_P.species_names: printFlags[sp] = True p_P.initialize_particles(printFlags) # Get the initial cell index of each particle. # Should this be something the pmesh computes? No: pmesh computes the # index of a single particle. It doesn't know the particle storage # infrastructure. p_P.compute_mesh_cell_indices() p_P.initialize_particle_integration() # Advance the particles for n_timesteps print("Moving", p_P.get_total_particle_count(), "particles for", ctrl.n_timesteps, "timesteps") for istep in range(ctrl.n_timesteps): if p_P.traj_T is not None: if istep % p_P.traj_T.skip == 0: p_P.record_trajectory_data(ctrl.timeloop_count, ctrl.time) p_P.advance_neutral_particles(ctrl) ctrl.timeloop_count += 1 ctrl.time += ctrl.dt # Record the LAST point on the particle trajectory if p_P.traj_T is not None: p_P.record_trajectory_data(ctrl.timeloop_count, ctrl.time) # Plot the trajectory onto the particle mesh if self.plot_results is True: mesh = p_P.pmesh_M.mesh plotTitle = os.path.basename( __file__) + ": " + sys._getframe().f_code.co_name + ": XY mesh" holdPlot = True # Set to True to stop the plot from disappearing. p_P.traj_T.plot_trajectories_on_mesh( mesh, plotTitle, hold_plot=holdPlot ) # Plots trajectory spatial coordinates on top of the particle mesh return
def test_3D_particle_migration(self): """Test particle migration across cells on a 3D mesh. """ fncName = '(' + __file__ + ') ' + sys._getframe( ).f_code.co_name + '():\n' print('\ntest: ', fncName) ctrl = DTcontrol_C() # Run identifier ctrl.title = "test_ParticleMigration.py:test_3D_particle_migration" # Run author ctrl.author = "tph" ctrl.time = 0.0 ctrl.timeloop_count = 0 ctrl.dt = 0.5 ctrl.n_timesteps = 19 ctrl.MAX_FACET_CROSS_COUNT = 100 # Run for 3D mesh # 1. Attach the particle mesh to particle_P # 2. Attach the C++ particle movers. # 3. Compute the cell-neighbors and facet-normals for the particle movers. self.particle_P.initialize_particle_mesh(self.pmesh3D) # Attach the particle integrators self.particle_P.initialize_particle_integration() ### Particle boundary-conditions # See the TODO of 4apr20 for redoing this. # Make a dictionary associating the above-named boundaries of the particle mesh with # user-supplied call-back functions. # First, we need to make a UserParticleBoundaryFunctions_... object, which # contains the boundary-crossing callback functions. spNames = self.particle_P.species_names # Import C++ particle boundary-conditions userParticleBoundaryFunctionsSOlibName = "user_particle_boundary_functions_cartesian_xyz_solib" if userParticleBoundaryFunctionsSOlibName not in sys.modules: infoMsg = "%s\t\"Importing %s\"" % ( fncName, userParticleBoundaryFunctionsSOlibName) print(infoMsg) userParticleBoundaryFunctionsSOlib = im_m.import_module( userParticleBoundaryFunctionsSOlibName) # Call the constructor to make a UserParticleBoundaryFunctions_... object userPBndFns = userParticleBoundaryFunctionsSOlib.UserParticleBoundaryFunctions( self.particle_P.position_coordinates) # Create the map from mesh facets to particle callback functions: pmeshBCs = self.particle_P.particle_solib.ParticleMeshBoundaryConditions( spNames, self.particle_P.pmesh_M, userPBndFns, print_flag=False) # Add pmeshBCs to the Particle_C object self.particle_P.pmesh_bcs = pmeshBCs # cell_dict{} is needed by the Python version of is_inside_cell(), which is # used in compute_mesh_cell_indices() below. self.particle_P.pmesh_M.compute_cell_dict() # Get the initial cell index of each particle. self.particle_P.compute_mesh_cell_indices() # Save the initial conditions (p_ic) for plotting p_ic = [] sp = self.particle_P.neutral_species[0] for ip in [0, 1]: (pseg, offset) = self.particle_P.sap_dict[sp].get_segment_and_offset(ip) p = pseg[offset].copy( ) # Have to make a copy! Otherwise you overwrite the only copy of the particle p_ic.append(p) # The expected final position and cell index # First particle xsp0 = -9.5 ysp0 = -9.5 zsp0 = 0.0 vxsp0 = -2.0 vysp0 = 0.0 vzsp0 = 0.0 weight0 = 2.0 bitflag0 = 2 cell_index0 = 98 unique_ID0 = 0 crossings = 0 psp0 = (xsp0, ysp0, zsp0, vxsp0, vysp0, vzsp0, weight0, bitflag0, cell_index0, unique_ID0, crossings) # Second particle xsp1 = -9.5 ysp1 = -9.5 zsp1 = -9.5 vxsp1 = -2.0 vysp1 = -2.0 vzsp1 = -2.0 weight1 = 3.0 bitflag1 = 2 cell_index1 = 0 unique_ID1 = 0 crossings = 0 psp1 = (xsp1, ysp1, zsp1, vxsp1, vysp1, vzsp1, weight1, bitflag1, cell_index1, unique_ID1, crossings) p_expected = (psp0, psp1) # Integrate for n_timesteps print("Advancing", self.particle_P.get_total_particle_count(), "particles for", ctrl.n_timesteps, "timesteps on a 3D mesh") for istep in range(ctrl.n_timesteps): self.particle_P.advance_neutral_particles(ctrl) # Create a mesh plotter to display the trajectory (just the # first and last positions) if self.plot_results is True: plotTitle = os.path.basename(__file__) + ": " + sys._getframe( ).f_code.co_name + ": First & last positions" plotter = df_m.plot(self.particle_P.pmesh_M.mesh, title=plotTitle) # Check the results ncoords = self.particle_P.particle_dimension # number of particle coordinates to check for sp in self.particle_P.neutral_species: for ip in [0, 1]: (pseg, offset ) = self.particle_P.sap_dict[sp].get_segment_and_offset(ip) getparticle = pseg[ offset] # Retrieve the particle from the SAP. if self.plot_results is True: mplot_m.plot([p_ic[ip][0], getparticle[0]], [p_ic[ip][1], getparticle[1]], [p_ic[ip][2], getparticle[2]]) # print 'expected = ', p_expected[ip] # print 'calculated = ', getparticle # path = np_m.array([p_ic[ip][0], p_ic[ip][1], p_ic[ip][2], getparticle[0], getparticle[1], getparticle[2]]) # Replace this with a point plot: # plotter.add_polygon(path) for ic in range(ncoords): self.assertAlmostEqual( p_expected[ip][ic], getparticle[ic], places=6, msg="Particle is not in correct position") cell_index_position = -3 # print (fncName, "expected cell =", p_expected[ip][cell_index_position], ", computed cell =", getparticle[cell_index_position]) self.assertEqual(p_expected[ip][cell_index_position], getparticle[cell_index_position], msg="Particle is not in correct cell") if self.plot_results is True: mplot_m.show() # yesno = raw_input("Just called show() in test_3D_particle_migration") return
def test_1D_particle_migration(self): """Test that we can call a C++ function to push neutral particles. Initial particle positions are in UserParticles_3D.py. The first particle moves in -x only, from 9.5 to -9.5. The first particle starts at: (x0, y0, z0) = (9.5, -9.5, 0.0), with velocity: (ux0, uy0, uz0) = (-2.0, 0.0, 0.0) It moves to (-9.5, -9.5, 0.0) The second particle starts at (x1, y1, z1) = (9.5, 9.5, 9.5) (ux1, uy1, uz1) = (-2.0, -2.0, -2.0) It moves in -x, -y, -z to the opposite corner. """ fncName = '(' + __file__ + ') ' + sys._getframe( ).f_code.co_name + '():\n' print('\ntest: ', fncName) ctrl = DTcontrol_C() # Run identifier ctrl.title = "1D particle advance using C++" # Run author ctrl.author = "tph" ctrl.time = 0.0 ctrl.timeloop_count = 0 ctrl.dt = 0.5 ctrl.n_timesteps = 19 ctrl.MAX_FACET_CROSS_COUNT = 100 # Run on a 1D mesh # 1. Attach the particle mesh to particle_P # 2. Attach the C++ particle movers. # 3. Compute the cell-neighbors and facet-normals for the particle movers. self.particle_P.initialize_particle_mesh(self.pmesh1D) # Attach the particle integrators self.particle_P.initialize_particle_integration() ### Particle boundary-conditions # See the TODO of 4apr20 for redoing this. # Make a dictionary associating the above-named boundaries of the particle mesh with # user-supplied call-back functions. # First, we need to make a UserParticleBoundaryFunctions_... object, which # contains the boundary-crossing callback functions. spNames = self.particle_P.species_names # Import C++ particle boundary-conditions userParticleBoundaryFunctionsSOlibName = "user_particle_boundary_functions_cartesian_xyz_solib" if userParticleBoundaryFunctionsSOlibName not in sys.modules: infoMsg = "%s\t\"Importing %s\"" % ( fncName, userParticleBoundaryFunctionsSOlibName) print(infoMsg) userParticleBoundaryFunctionsSOlib = im_m.import_module( userParticleBoundaryFunctionsSOlibName) # Call the constructor to make a UserParticleBoundaryFunctions_... object userPBndFns = userParticleBoundaryFunctionsSOlib.UserParticleBoundaryFunctions( self.particle_P.position_coordinates) # Create the map from mesh facets to particle callback functions: pmeshBCs = self.particle_P.particle_solib.ParticleMeshBoundaryConditions( spNames, self.particle_P.pmesh_M, userPBndFns, print_flag=False) # Add pmeshBCs to the Particle_C object self.particle_P.pmesh_bcs = pmeshBCs # Get the initial cell index of each particle. # Note: this could be moved down closer to the beginning of the # particle-advance loop. # cell_dict{} is needed by the Python version of is_inside_cell(), which is # used in compute_mesh_cell_indices() below. self.particle_P.pmesh_M.compute_cell_dict() self.particle_P.compute_mesh_cell_indices() ### Put the expected ending results for the two particles into the p_expected tuple ### # First particle xsp0 = -9.5 ysp0 = -9.5 zsp0 = 0.0 xsp00 = -8.5 ysp00 = -9.5 zsp00 = 0.0 vxsp0 = -2.0 vysp0 = 0.0 vzsp0 = 0.0 weight0 = 2.0 bitflag0 = 2 cell_index0 = 0 unique_ID0 = 0 crossings = 0 psp0 = (xsp0, ysp0, zsp0, xsp00, ysp00, zsp00, vxsp0, vysp0, vzsp0, weight0, bitflag0, cell_index0, unique_ID0, crossings) # Second particle xsp1 = -9.5 ysp1 = -9.5 zsp1 = -9.5 xsp10 = -8.5 ysp10 = -8.5 zsp10 = -8.5 vxsp1 = -2.0 vysp1 = -2.0 vzsp1 = -2.0 weight1 = 3.0 bitflag1 = 2 cell_index1 = 0 unique_ID1 = 1 crossings = 0 psp1 = (xsp1, ysp1, zsp1, xsp10, ysp10, zsp10, vxsp1, vysp1, vzsp1, weight1, bitflag1, cell_index1, unique_ID1, crossings) p_expected = (psp0, psp1) # # Move the particles and check the final positions # speciesName = 'neutral_H' sap = self.particle_P.sap_dict[ speciesName] # segmented array for this species # Print the cell indices # print("cell_vertices_dict = ", self.particle_P.pmesh_M.cell_vertices_dict) # Integrate for n_timesteps print("Advancing", self.particle_P.get_total_particle_count(), "particles for", ctrl.n_timesteps, "timesteps on a 1D mesh") for istep in range(ctrl.n_timesteps): # print(fncName, "istep:", istep) self.particle_P.advance_neutral_particles(ctrl) # Check the results ncoords = self.particle_P.particle_dimension # number of particle coordinates to check for sp in self.particle_P.neutral_species: for ip in [0, 1]: # getparticle = self.particle_P.sap_dict[sp].get(ip) # Instead of .get(), retrieve the particle structure using the returned Numpy array it's in. (pseg, offset ) = self.particle_P.sap_dict[sp].get_segment_and_offset(ip) getparticle = pseg[ offset] # Retrieve the particle from the SAP. # print('expected = ', p_expected[ip]) # print('calculated = ', getparticle) for ic in range(2 * ncoords): self.assertAlmostEqual( p_expected[ip][ic], getparticle[ic], places=6, msg="Particle is not in correct position") cell_index_position = -3 # print fncName, "expected cell =", p_expected[ip][cell_index_position], "computed cell =", getparticle[cell_index_position] self.assertEqual(p_expected[ip][cell_index_position], getparticle[cell_index_position], msg="Particle is not in correct cell") return