Beispiel #1
0
    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
Beispiel #2
0
### Physical constants

m_e = 1.0 * MyPlasmaUnits_C.electron_mass
q_e = 1.0 * MyPlasmaUnits_C.elem_charge

#-# 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.
Beispiel #3
0
    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
Beispiel #4
0
    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
Beispiel #5
0
    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
Beispiel #6
0
    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
Beispiel #10
0
    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