Example #1
0
    def test_5_1D_poisson_solver(self):
        """Test a Poisson solve in 1D radial spherical coordinates starting at r = 0.

           The mesh and mesh BC markers used here read from files written by
           test_1D_spherical_mesh in test_UserMesh.py.  

           The charge-density is from a file written by
           test_6_compute_charge_density_on_1Dmesh() in test_ChargeDensity.py.

           The potential is set to zero at the outer radius at 5 meters.  At the
           inner radius (0) no value is set, and so the slope is zero.

           See Calc file test_FieldSolve.ods:test_5 for calculation of potential

        """

        fncName = '(' + __file__ + ') ' + sys._getframe(
        ).f_code.co_name + '():\n'
        print('\ntest: ', fncName)

        ### Specialized mesh and field-solver modules for this test ###

        from UserMesh_y_Fields_Spherical1D_Module import UserMesh1DS_C
        from UserMesh_y_Fields_Spherical1D_Module import UserMeshInput1DS_C
        from UserMesh_y_Fields_Spherical1D_Module import UserPoissonSolve1DS_C

        ### Read the mesh and boundary-condition markers ###

        # Create a mesh object and read in the mesh.
        coordinateSystem = '1D-spherical-radial'
        mesh1d_M = Mesh_C(
            mesh_file='mesh_1D_radial_r0.xml',
            coordinate_system=coordinateSystem,
            field_boundary_marker_file='mesh_1D_radial_r0_Fbcs.xml',
            compute_dictionaries=True,
            compute_cpp_arrays=False,
            compute_tree=True,
            plot_flag=self.plot_mesh)

        ### Create the charge-density vector ###

        chargeDensityElementType = 'Lagrange'
        chargeDensityElementDegree = 1
        chargeDensityFieldType = 'scalar'

        chargeDensity_F = Field_C(mesh_M=mesh1d_M,
                                  element_type=chargeDensityElementType,
                                  element_degree=chargeDensityElementDegree,
                                  field_type=chargeDensityFieldType)

        ### Set the charge density values ###

        # Read in the charge-density written by
        # test_ChargeDensity.py:test_5_compute_charge_density_on_1Dmesh
        file = df_m.File("charge_1DS_r0.xml")
        file >> chargeDensity_F.function

        ### Set the Poisson solver parameters ###

        linearSolver = 'lu'
        preconditioner = None

        ### Set the field boundary conditions ###

        # Copy over from test_ChargeDensity.py:test_5 for convenience in using the
        # integer markers to set boundary values.
        #
        # This associates the field marker integers with boundary names.
        rminIndx = 1
        rmaxIndx = 2
        fieldBoundaryDict = {
            'rmin': rminIndx,
            'rmax': rmaxIndx,
        }

        # Dirichlet boundaries are applied using a mesh function that marks the outer
        # edges of the mesh.
        fieldBoundaryMarker = mesh1d_M.field_boundary_marker

        # Specify the boundary values of the potential
        phiVals = {
            'rmin': 'unset',  # Analytic potential inside the particle radius
            'rmax': 0.0,
        }

        phiBCs = dict((bnd, [fieldBoundaryDict[bnd], phiVals[bnd]])
                      for bnd in list(fieldBoundaryDict.keys()))

        ### Create vectors for for the potential and electric field ###

        phiElementType = 'Lagrange'
        phiElementDegree = 1
        phiFieldType = 'scalar'

        phi_F = Field_C(mesh_M=mesh1d_M,
                        element_type=phiElementType,
                        element_degree=phiElementDegree,
                        field_type=phiFieldType)

        if phiElementDegree == 1:
            # For linear elements, grad(phi) is discontinuous across
            # elements. To get these values, we need Discontinuous Galerkin
            # elements.
            electricFieldElementType = "DG"
        else:
            electricFieldElementType = "Lagrange"

        negElectricField_F = Field_C(mesh_M=mesh1d_M,
                                     element_type=electricFieldElementType,
                                     element_degree=phiElementDegree - 1,
                                     field_type='vector')

        ### Create the Poisson-solver object ###

        potentialsolve = UserPoissonSolve1DS_C(
            phi_F,
            linearSolver,
            preconditioner,
            fieldBoundaryMarker,
            phiBCs,
            neg_electric_field=negElectricField_F)

        # Create the source term from a given density
        # (In this test, the charge comes from kinetic point particles, rather
        # than from a density function, so the following isn't needed.)
        #        poissonsolve.assemble_source_expression(charge_density)

        #        self.b = df_m.assemble(self.L)

        # Solve for the potential
        plotTitle = os.path.basename(
            __file__) + ": " + sys._getframe().f_code.co_name
        #        poissonsolve.solve_for_phi(plot_flag=plotFlag, plot_title=plotTitle)

        spacechargeFactor = 1.0
        potentialsolve.solve_for_phi(assembled_charge=chargeDensity_F,
                                     assembled_charge_factor=spacechargeFactor,
                                     plot_flag=self.plot_results,
                                     plot_title=plotTitle)

        # Write the potential to a file in VTK and XML formats
        file = df_m.File('phi_test_5_1D.pvd')
        # phi_name goes into the output file; phi_label doesn't
        phi_F.function.rename("phi1D", "phi_label")
        file << phi_F.function
        file = df_m.File('phi_test_5_1D.xml')
        file << phi_F.function

        # Write -E to a file in VTK and XML formats
        if negElectricField_F is not None:
            negElectricField_F.function.rename("E1D", "E_label")
            # !!! VTK cannot write a vector field on a 1D grid.
            #            file = df_m.File("negE_test_5_1D.pvd")
            #            file << negElectricField_F.function
            file = df_m.File("negE_test_5_1D.xml")
            file << negElectricField_F.function

        # Check that -E has the analytic value at r=1.842
        # See test_FieldSolve.ods for values
        negE_arr = negElectricField_F.function.vector().get_local()
        #        print("negE_arr =", negE_arr)
        negE_at_1_842 = 2649589637.58845
        ratio = negE_at_1_842 / negE_arr[5]
        decimal_places = 2
        self.assertAlmostEqual(ratio,
                               1.0,
                               places=decimal_places,
                               msg="negE is not correct")
        print("test_5: negE is correct to %d decimal places; ratio = %.5g" %
              (decimal_places, ratio))

        return
Example #2
0
    def test_3_1D_poisson_solver(self):
        """Test a 1D Poisson equation in cartesian coordinates.

           Note: the mesh, mesh BC markers, and the charge-density are read from
           external files written by test_4_compute_charge_density_on_1Dmesh() in
           test_ChargeDensity.py. Boundary values are specified here.

           See Calc file test_FieldSolve.ods for calculation of electric field.

        """

        fncName = '(' + __file__ + ') ' + sys._getframe(
        ).f_code.co_name + '():\n'
        print('\ntest: ', fncName)

        ### Specialized mesh and field-solver modules for this test ###

        from UserMesh_y_Fields_FE_XYZ_Module import UserMesh_C
        from UserMesh_y_Fields_FE_XYZ_Module import UserMeshInput_C
        from UserMesh_y_Fields_FE_XYZ_Module import UserPoissonSolve1D_C

        ### Read the mesh and boundary-condition markers ###

        # Create the mesh object and read in the mesh and boundary markers
        coordinateSystem = 'Cartesian'
        mesh1d_M = Mesh_C(mesh_file='mesh_2cell_1D.xml',
                          coordinate_system=coordinateSystem,
                          field_boundary_marker_file='mesh_2cell_1D_Fbcs.xml',
                          compute_dictionaries=True,
                          compute_cpp_arrays=False,
                          compute_tree=True,
                          plot_flag=self.plot_mesh)

        ### Create the charge-density vector ###

        # Copy over from test_ChargeDensity.py:test_4
        # This associates the field marker integers with boundaries.
        # IS THIS NEEDED?
        xminIndx = 1
        xmaxIndx = 2
        fieldBoundaryDict = {
            'xmin': xminIndx,
            'xmax': xmaxIndx,
        }

        chargeDensityElementType = 'Lagrange'
        chargeDensityElementDegree = 1
        chargeDensityFieldType = 'scalar'

        chargeDensity_F = Field_C(mesh_M=mesh1d_M,
                                  element_type=chargeDensityElementType,
                                  element_degree=chargeDensityElementDegree,
                                  field_type=chargeDensityFieldType)

        ### Set the charge density values ###

        # Read in the charge-density written by
        # test_ChargeDensity.py:test_4_compute_charge_density_on_1Dmesh
        file = df_m.File("charge_1D.xml")
        file >> chargeDensity_F.function

        ### Set the Poisson solver parameters ###

        linearSolver = 'lu'
        preconditioner = None

        ### Set the field boundary conditions ###

        # Dirichlet boundaries are applied using a mesh function that marks the outer
        # edges of the mesh.
        fieldBoundaryMarker = mesh1d_M.field_boundary_marker

        # Specify the boundary values of the potential
        phiVals = {
            'xmin': -2.0,
            'xmax': 1.0,
        }

        phiBCs = dict((bnd, [fieldBoundaryDict[bnd], phiVals[bnd]])
                      for bnd in list(fieldBoundaryDict.keys()))

        ### Create vectors for for the potential and electric field ###

        phiElementType = 'Lagrange'
        phiElementDegree = 1
        phiFieldType = 'scalar'

        phi_F = Field_C(mesh_M=mesh1d_M,
                        element_type=phiElementType,
                        element_degree=phiElementDegree,
                        field_type=phiFieldType)

        if phiElementDegree == 1:
            # For linear elements, grad(phi) is discontinuous across
            # elements. To get these values, we need Discontinuous Galerkin
            # elements.
            electricFieldElementType = "DG"
        else:
            electricFieldElementType = "Lagrange"

        negElectricField_F = Field_C(mesh_M=mesh1d_M,
                                     element_type=electricFieldElementType,
                                     element_degree=phiElementDegree - 1,
                                     field_type='vector')

        ### Create the Poisson-solver object ###

        poissonsolve = UserPoissonSolve1D_C(
            phi_F,
            linearSolver,
            preconditioner,
            fieldBoundaryMarker,
            phiBCs,
            neg_electric_field=negElectricField_F)

        # Create the source term from a given density
        # (In this test, the charge comes from kinetic point particles, rather
        # than from a density function, so the following isn't needed.)
        #        poissonsolve.assemble_source_expression(charge_density)

        #        self.b = df_m.assemble(self.L)

        # Solve for the potential
        plotTitle = os.path.basename(
            __file__) + ": " + sys._getframe().f_code.co_name
        poissonsolve.solve_for_phi(plot_flag=self.plot_results,
                                   plot_title=plotTitle)

        return
Example #3
0
    def test_1_1D_spherical_laplace_solver(self):
        """Test a 1D Laplace equation in spherical coordinates.

           This is a Laplace solve as there is no source term, only
           boundary-conditions.

           NB: The potential is represented with quadratic elements are used instead
           of the usual linear ones.
        """

        fncName = '(' + __file__ + ') ' + sys._getframe(
        ).f_code.co_name + '():\n'
        print('\ntest: ', fncName)

        ## Specialized modules for the mesh and field solver
        from UserMesh_y_Fields_Spherical1D_Module import UserMeshInput1DS_C
        from UserMesh_y_Fields_Spherical1D_Module import UserMesh1DS_C
        from UserMesh_y_Fields_Spherical1D_Module import UserPoissonSolve1DS_C

        # For this calculation, we use the Dolfin framework for finite-element fields.
        #        field_infrastructure_module = 'Dolfin_Module'
        #        fI_M = im_m.import_module(field_infrastructure_module)

        umi = UserMeshInput1DS_C()

        ## Make the mesh

        # radial
        umi.rmin, umi.rmax = 1.0, 5.0  # Mesh goes from rmin to rmax in radius
        umi.nr = 10  # Number of divisions in r direction
        umi.stretch = 1.3  # Stretch parameter

        # Name the Dirichlet boundaries and assign integers to them.
        # These are the boundary-name -> int pairs used to mark mesh
        # facets:
        rminIndx = 1
        rmaxIndx = 2
        fieldBoundaryDict = {
            'rmin': rminIndx,
            'rmax': rmaxIndx,
        }

        umi.field_boundary_dict = fieldBoundaryDict

        plotTitle = os.path.basename(
            __file__) + ": " + sys._getframe().f_code.co_name
        mesh_M = UserMesh1DS_C(umi,
                               compute_tree=False,
                               plot_flag=self.plot_mesh,
                               plot_title=plotTitle)
        #        yesno = raw_input("Just called UserMesh1DS_C() in test_1_1D_spherical_laplace_solver")

        ## Storage for the potential and electric field

        phiElementType = 'Lagrange'
        phiElementDegree = 2
        phiFieldType = 'scalar'

        phi_F = Field_C(mesh_M=mesh_M,
                        element_type=phiElementType,
                        element_degree=phiElementDegree,
                        field_type=phiFieldType)

        if phiElementDegree == 1:
            # For linear elements, grad(phi_F) is discontinuous across
            # elements. To represent this field, we need Discontinuous Galerkin
            # elements.
            electricFieldElementType = 'DG'
        else:
            electricFieldElementType = 'Lagrange'

        negElectricField_F = Field_C(mesh_M=mesh_M,
                                     element_type=electricFieldElementType,
                                     element_degree=phiElementDegree - 1,
                                     field_type='vector')

        ## The Laplace solver parameters
        # Gives PETSc LU solver, (null). (Direct solver).
        linearSolver = 'lu'
        preconditioner = None

        # Dirichlet boundaries are marked with a boundary marker mesh function
        fieldBoundaryMarker = mesh_M.field_boundary_marker

        # Boundary values of the potential
        phiVals = {
            'rmin': 0.0,
            'rmax': -1.5,
        }

        phiBCs = dict((bnd, [fieldBoundaryDict[bnd], phiVals[bnd]])
                      for bnd in list(fieldBoundaryDict.keys()))

        # Compute the electrostatic field from phi
        laplacesolve = UserPoissonSolve1DS_C(
            phi_F,
            linearSolver,
            preconditioner,
            fieldBoundaryMarker,
            phiBCs,
            neg_electric_field=negElectricField_F)

        # Set the source term: it's zero for Laplace's equation.
        # This sets the b vector
        laplacesolve.assemble_source_expression(0.0)

        # Solve for the potential
        plotTitle = os.path.basename(
            __file__) + ": " + sys._getframe().f_code.co_name
        laplacesolve.solve_for_phi(plot_flag=self.plot_results,
                                   plot_title=plotTitle)

        #        yesno = raw_input("Looks OK [Y/n]?")
        #        self.assertNotEqual(yesno, 'n', "Problem with mesh")

        #        potentialFieldOutputFile = ctrl.title + "-phi" + ".xdmf"
        #        potentialFieldOutputObj = df_m.XDMFFile(potentialFieldOutputFile)

        # Write the potential to a file in VTK and XML formats
        file = df_m.File('phi_test_1_1D.pvd')
        # phi_name goes into the output file; phi_label doesn't
        phi_F.function.rename("phi1D", "phi_label")
        file << phi_F.function
        file = df_m.File('phi_test_1_1D.xml')
        file << phi_F.function

        # Write the potential in XDMF format
        xf = df_m.XDMFFile("phi_test_1_1D.xdmf")
        xf.write(phi_F.function)

        # Write -E to a file in VTK and XML formats
        # !!! VTK cannot write a vector field on a 1D grid.
        # file = df_m.File('negE_test_1_1D.pvd')
        # file << fieldsolveCI.negE
        file = df_m.File('negE_test_1_1D.xml')
        file << negElectricField_F.function

        # Write -E in XDMF format
        xf = df_m.XDMFFile("negE_test_1_1D.xdmf")
        xf.write(negElectricField_F.function)

        # Check the 1/r^2 fall-off in E:

        (Emin, Emax) = (negElectricField_F.function(umi.rmin),
                        negElectricField_F.function(umi.rmax))
        #        print "Emin, Emax =", Emin, Emax
        ratio = Emin * umi.rmin**2 / (Emax * umi.rmax**2)
        #        print "ratio:", ratio

        decimal_places = 1  # Note: can get 2 decimal places for degree-3 elements.
        self.assertAlmostEqual(ratio,
                               1.0,
                               places=decimal_places,
                               msg="Falloff in E is not 1/r^2 to %d decimals" %
                               decimal_places)

        r_arr = mesh_M.mesh.coordinates()[:, 0]
        phi_arr = phi_F.function.vector().get_local()
        nnodes = len(phi_arr)
        nvertices = len(r_arr)
        #        print "nnode =", nnodes
        #        print "nvertex =", nvertex
        #        print "r_arr =", r_arr
        #        print "phi_arr =", phi_arr

        # Compute E from a simple derivative, for linear elements
        if nnodes == nvertices:
            # Check the derivative: (nvertices*dim array, so need the ,0)

            # Set up temp arrays
            ncells = len(r_arr) - 1
            negEexp = np_m.zeros(ncells)
            dr = np_m.zeros(ncells)

            dr[0:ncells] = r_arr[1:ncells + 1] - r_arr[0:ncells]

            # Have to reverse phi_arr to get it in same order as r_arr
            phi_arr = phi_arr[::-1]

            # Compute dphi/dr
            negEexp[0:ncells] = (phi_arr[1:ncells + 1] -
                                 phi_arr[0:ncells]) / dr[0:ncells]
            #       print "negEexp =", negEexp

            negEget = laplacesolve.negE.vector().get_local()
            #        print "negEget =", fieldsolveCI.negE.vector().array()

            # Check that the derivative equals the stored negE
            for ic in range(ncells):
                self.assertAlmostEqual(
                    negEget[ic],
                    negEexp[ic],
                    msg="Expected and stored values of negE are not the same")

        return
Example #4
0
particle_P.pmesh_M = pmesh1D

# Set the initial cell index containing particle
# particle_P.compute_mesh_cell_indices()

#-# Electric field properties #-#

electricFieldElementDegree = 0
electricFieldElementType = 'DG'

#-# The external electric field #-#

# The particle mover uses the negative electric field.
randomExternalElectricField_F = Field_C(
    mesh_M=pmesh1D,
    element_type=electricFieldElementType,
    element_degree=electricFieldElementDegree,
    field_type='vector')

# This sets electric-field values over the mesh or subdomain.
randomExternalElectricField_F.set_values(randomExternalElectricFieldAmplitude)

# This sets a value over a mesh or subdomain.
#externalElectricField_F.set_gaussian_values(externalElectricFieldAmplitude)

#-# Histories #-#

# Particle histories
particleHistoryInput = ParticleHistoryInput_C()

particleHistoryInput.max_points = None  # Set to None to get every point
Example #5
0
    def test_2_2D_cyl_laplace_solver(self):
        """Test a 2D Laplace equation in cylindrical coordinates.

           This is a Laplace solve as there is no source term, only
           boundary-conditions.

        """

        fncName = '(' + __file__ + ') ' + sys._getframe(
        ).f_code.co_name + '():\n'
        print('\ntest: ', fncName)

        ## Specialized modules for the mesh and field solver
        from UserMesh_y_Fields_FE2D_Module import UserMesh2DCirc_C
        from UserMesh_y_Fields_FE2D_Module import UserMeshInput2DCirc_C
        from UserMesh_y_Fields_FE2D_Module import UserPoissonSolve2DCirc_C

        # For this calculation, we use the Dolfin framework for finite-element fields.
        #        field_infrastructure_module = 'Dolfin_Module'
        #        fI_M = im_m.import_module(field_infrastructure_module)

        umi = UserMeshInput2DCirc_C()

        # Make the mesh
        # radial
        umi.rmin, umi.rmax = 1.0, 5.0  # Mesh goes from rmin to rmax in radius
        umi.nr = 10  # Number of divisions in r direction
        umi.stretch = 1.3  # Stretch parameter

        # Name the Dirichlet boundaries and assign integers to them.
        # These are the boundary-name -> int pairs used to mark mesh
        # facets:
        rminIndx = 1
        rmaxIndx = 2
        fieldBoundaryDict = {
            'rmin': rminIndx,
            'rmax': rmaxIndx,
        }

        umi.field_boundary_dict = fieldBoundaryDict

        # theta, starts at 0
        umi.tmax = math.pi / 2  # quarter-circle
        umi.nt = 20  # Number of divisions in theta direction

        # The diagonal that makes the triangular mesh
        # Options: 'left, 'right', 'left/right', 'crossed'
        umi.diagonal = 'crossed'

        mesh_M = UserMesh2DCirc_C(umi,
                                  compute_tree=False,
                                  plot_flag=self.plot_mesh)

        # Storage for the potential and electric field
        phiElementType = 'Lagrange'
        phiElementDegree = 1
        phiFieldType = 'scalar'

        phi_F = Field_C(mesh_M=mesh_M,
                        element_type=phiElementType,
                        element_degree=phiElementDegree,
                        field_type=phiFieldType)

        if phiElementDegree == 1:
            # For linear elements, grad(phi) is discontinuous across
            # elements. To get these values, we need Discontinuous Galerkin
            # elements.
            electricFieldElementType = "DG"
        else:
            electricFieldElementType = "Lagrange"

        negElectricField_F = Field_C(mesh_M=mesh_M,
                                     element_type=electricFieldElementType,
                                     element_degree=phiElementDegree - 1,
                                     field_type='vector')

        # The Laplace solver parameters

        linearSolver = 'lu'
        preconditioner = None
        # Don't get exactly the same solutions with the following
        #        linearSolver = 'cg'
        #        preconditioner = 'ilu'

        # Dirichlet Boundaries are marked with a boundary marker mesh
        # function
        fieldBoundaryMarker = mesh_M.field_boundary_marker

        # Boundary values of the potential.  These names have to be
        # the same as those assigned to the boundaries above.
        phiVals = {
            'rmin': 0.0,
            'rmax': -1.5,
        }

        phiBCs = dict((bnd, [fieldBoundaryDict[bnd], phiVals[bnd]])
                      for bnd in list(fieldBoundaryDict.keys()))

        computeEflag = True

        laplacesolve = UserPoissonSolve2DCirc_C(
            phi_F,
            linearSolver,
            preconditioner,
            fieldBoundaryMarker,
            phiBCs,
            neg_electric_field=negElectricField_F)

        laplacesolve.assemble_source_expression(0.0)

        plotTitle = os.path.basename(
            __file__) + ": " + sys._getframe().f_code.co_name
        laplacesolve.solve_for_phi(plot_flag=self.plot_results,
                                   plot_title=plotTitle)

        #        yesno = raw_input("Looks OK [Y/n]?")
        #        self.assertNotEqual(yesno, 'n', "Problem with mesh")

        # Write the potential to a file in VTK and XML formats
        file = df_m.File("phi_test_2_2D.pvd")
        phi_F.function.rename("phi2D", "phi_label")
        file << phi_F.function
        file = df_m.File("phi_test_2_2D.xml")
        file << phi_F.function

        # Write -E to a file in VTK and XML formats
        if negElectricField_F is not None:
            negElectricField_F.function.rename("E2D", "E_label")
            file = df_m.File("negE_test_2_2D.pvd")
            file << negElectricField_F.function
            file = df_m.File("negE_test_2_2D.xml")
            file << negElectricField_F.function

        # Try XDMF format
        xf = df_m.XDMFFile("phi_test_2_2D.xdmf")
        xf.write(phi_F.function)

        return
Example #6
0
    def setUp(self):
        # initializations for each test go here...

        # Create mesh from a file
        coordinateSystem = 'Cartesian'
        mesh2D_M = Mesh_C(mesh_file="mesh_quarter_circle_crossed.xml",
                          coordinate_system=coordinateSystem,
                          compute_dictionaries=True,
                          compute_cpp_arrays=False,
                          compute_tree=True,
                          plot_flag=False)

        #        df_m.plot(self.mesh, title='cylindrical mesh', axes=True)
        #        df_m.interactive()

        phi_element_type = 'Lagrange'
        phi_element_degree = 1
        self.phi = Field_C(mesh2D_M,
                           element_type=phi_element_type,
                           element_degree=phi_element_degree,
                           field_type='scalar')

        # Read the potential from a file
        file = df_m.File("phi_test_2_2D.xml")
        file >> self.phi.function

        # Plot phi

        #        df_m.plot(self.phi)
        #        df_m.interactive()

        if phi_element_degree == 1:
            # For linear elements, grad(phi) is discontinuous across
            # elements. To represent this field, we need Discontinuous Galerkin
            # elements.
            electric_field_element_type = "DG"
        else:
            electric_field_element_type = "Lagrange"

        self.neg_electric_field = Field_C(
            mesh2D_M,
            element_type=electric_field_element_type,
            element_degree=phi_element_degree - 1,
            field_type='vector')

        # Read the electric field from a file
        file = df_m.File("negE_test_2_2D.xml")
        file >> self.neg_electric_field.function

        # Create the points for interpolation of the E-field

        # 1st point
        x0 = 1.0
        y0 = 0.0
        z0 = 0.0
        ux0 = 3000.0
        uy0 = 2000.0
        uz0 = 1000.0
        weight0 = 1.0  # number of electrons per macroparticle

        # Can these be named? Yes, with a different dtype.
        p0 = np_m.array([x0, y0, z0, ux0, uy0, uz0, weight0], dtype=float)

        # 2nd point
        x1 = 5.0
        y1 = 0.0
        z1 = 1.0
        ux1 = uy1 = 0.0
        uz1 = -uz0
        weight1 = 2.0

        p1 = np_m.array([x1, y1, z1, ux1, uy1, uz1, weight1], dtype=float)

        # 3nd point: same cell as 2nd point, so same E for DG0 a element.
        x2 = 4.5
        y2 = 0.0
        z2 = 1.0
        ux2 = uy2 = 0.0
        uz2 = -uz0
        weight2 = 2.0

        p2 = np_m.array([x2, y2, z2, ux2, uy2, uz2, weight2], dtype=float)

        self.points = np_m.array([p0, p1, p2])

        # Also create a version with the standard set of attributes for particles
        # with 3D Cartesian coordinates

        # Here is the dtype dictionary:
        self.particle_dtype = {
            'names': [
                'x', 'y', 'z', 'x0', 'y0', 'z0', 'ux', 'uy', 'uz', 'weight',
                'bitflags', 'cell_index', 'unique_ID', 'crossings'
            ],
            'formats': [
                np_m.float64, np_m.float64, np_m.float64, np_m.float64,
                np_m.float64, np_m.float64, np_m.float64, np_m.float64,
                np_m.float64, np_m.float64, np_m.int32, np_m.int32, np_m.int32,
                np_m.int32
            ]
        }

        cell_index0 = 1
        particle0 = (x0, y0, z0, x0, y0, z0, ux0, uy0, uz0, weight0, 0,
                     cell_index0, 0, 0)
        cell_index1 = 36
        particle1 = (x1, y1, z1, x1, y1, z1, ux1, uy1, uz1, weight1, 0,
                     cell_index1, 0, 0)
        cell_index2 = 36
        particle2 = (x2, y2, z2, x2, y2, z2, ux2, uy2, uz2, weight2, 0,
                     cell_index2, 0, 0)

        self.pseg = np_m.array([particle0, particle1, particle2],
                               dtype=self.particle_dtype)

        #print("Mesh cell index of particle 0 is",mesh2D_M.compute_cell_index(self.pseg[0]))
        #print("Mesh cell index of particle 1 is",mesh2D_M.compute_cell_index(self.pseg[1]))
        #print("Mesh cell index of particle 2 is",mesh2D_M.compute_cell_index(self.pseg[2]))

        # dtype of E field at the particles
        self.Ecomps = [
            'x',
            'y',
        ]
        Eexpected = np_m.empty(
            [len(self.points), len(self.Ecomps)], dtype=float)
        Eexpected[0] = [-0.84919658, -0.03336502]
        Eexpected[1] = [-0.19694748, -0.00773809]
        Eexpected[2] = [-0.19694748, -0.00773809]
        self.E_expected = Eexpected

        return
Example #7
0
    def setUp(self):

        self.plot_mesh = False
        self.plot_results = False
        self.plot_phase_space = False

        # Turn plots off if there's no display.
        if os.environ.get('DISPLAY') is None:
            self.plot_mesh = False
            self.plot_results = False

        fncName = '(' + __file__ + ') ' + sys._getframe(
        ).f_code.co_name + '():\n'

        # initializations for each test go here...

        self.ctrl = DTcontrol_C()

        self.ctrl.title = "test_ParticleTrajectory"
        self.ctrl.author = "tph"

        self.ctrl.dt = 1.0e-6
        self.ctrl.MAX_FACET_CROSS_COUNT = 100

        # Initialize time counters
        self.ctrl.timeloop_count = 0
        self.ctrl.time = 0.0

        # This gives a list of species to apply E to.
        # 'None' value means that the field is applied to ALL species
        self.ctrl.apply_solved_electric_field = None

        self.ctrl.write_trajectory_files = True

        ### 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

        # Define an electron species called 'trajelectrons'.
        speciesName = 'trajelectrons'
        charge = -1.0 * MyPlasmaUnits_C.elem_charge
        mass = 1.0 * MyPlasmaUnits_C.electron_mass
        dynamics = 'charged'
        trajelectrons_S = ParticleSpecies_C(speciesName, charge, mass,
                                            dynamics)

        # Define an electron species called 'test_electrons'.
        speciesName = 'test_electrons'
        charge = -1.0 * MyPlasmaUnits_C.elem_charge
        mass = 1.0 * MyPlasmaUnits_C.electron_mass
        dynamics = 'charged'
        test_electrons_S = ParticleSpecies_C(speciesName, charge, mass,
                                             dynamics)

        # Add the electrons to particle input
        pin.particle_species = (
            trajelectrons_S,
            test_electrons_S,
        )

        ## Make the particle storage array for all species.
        self.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_2D_e"

        # Import this module
        userParticlesModule = im_m.import_module(userParticlesModuleName)

        # self.particle_P.user_particles_module_name = userParticlesModuleName
        self.particle_P.user_particles_class = userParticlesClass = userParticlesModule.UserParticleDistributions_C

        ###  Mesh creation

        umi = UserMeshInput2DCirc_C()

        # Make the mesh & fields from saved files
        umi.mesh_file = 'mesh_quarter_circle_crossed.xml'
        umi.particle_boundary_file = 'mesh_quarter_circle_crossed_Pbcs.xml'

        ### Input for initial particles (i.e., particles present at t=0)

        # A. trajelectrons
        # Name the species (it should be in species_names above)
        speciesName = 'trajelectrons'

        # Check that this species has been defined above
        if speciesName in self.particle_P.species_names:
            charge = self.particle_P.charge[speciesName]
            mass = self.particle_P.mass[speciesName]
        else:
            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.
        trajElectronParams = {
            'species_name': speciesName,
            'initial_distribution_type': initialDistributionType,
        }

        # B. test_electrons
        # Name the species (it should be in species_names above)
        speciesName = 'test_electrons'

        # Check that this species has been defined above
        if speciesName in self.particle_P.species_names:
            charge = self.particle_P.charge[speciesName]
            mass = self.particle_P.mass[speciesName]
        else:
            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.
        test_electronParams = {
            'species_name': speciesName,
            'initial_distribution_type': initialDistributionType,
        }

        ## Collect the initial particles into a dictionary

        # The dictionary keys are mnemonics for the initialized particles
        initialParticlesDict = {
            'initial_trajelectrons': (trajElectronParams, ),
            'initial_test_electrons': (test_electronParams, ),
        }

        # Add the initialized particles to the Particle_C object
        self.particle_P.initial_particles_dict = initialParticlesDict

        # Create the particle mesh object
        # 'crossed' diagonals
        #        self.pmesh2D = Mesh_C(meshFile="quarter_circle_mesh_crossed.xml", computeDictionaries=True, computeTree=True, plotFlag=False)
        # 'left' diagonal
        #        self.pmesh2D = Mesh_C(meshFile="quarter_circle_mesh_left.xml", computeDictionaries=True, computeTree=True, plotFlag=False)

        # 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

        # Read the mesh from an existing file

        #        pin.pmesh_M = UserMesh_C(meshFile='quarter_circle_mesh_crossed.xml', particleBoundaryFile='Pbcs_quarter_circle_mesh_crossed.xml', computeDictionaries=True, computeTree=True, plotFlag=False)

        # Can this be attached to Particle_C after Particle_C construction? YES
        pmesh_M = UserMesh2DCirc_C(umi,
                                   compute_dictionaries=True,
                                   compute_cpp_arrays=False,
                                   compute_tree=True,
                                   plot_flag=self.plot_mesh)

        self.particle_P.pmesh_M = pmesh_M

        ### Field creation

        # The following value should correspond to the element degree
        # used in the potential from which negE was obtained
        phi_element_degree = 1

        if phi_element_degree == 1:
            # For linear elements, grad(phi) is discontinuous across
            # elements. To represent this field, we need Discontinuous Galerkin
            # elements.
            electric_field_element_type = "DG"
        else:
            electric_field_element_type = "Lagrange"

        ## Create the electric field from a file written by test_FieldSolve.py.

        # Put the negative electric field directly on the particle mesh
        self.neg_electric_field = Field_C(
            pmesh_M,
            element_type=electric_field_element_type,
            element_degree=phi_element_degree - 1,
            field_type='vector')

        file = df_m.File(
            "negE2D_crossed-2016.xml"
        )  # Use a frozen version instead of the one created each time test_FieldSolve.py runs.
        file >> self.neg_electric_field.function

        ## Particle boundary-conditions

        # UserParticleBoundaryFunctions_C is where the facet-crossing callback
        # functions are defined.
        #        userPBndFnsClass = userParticlesModule.UserParticleBoundaryFunctions_C # LHS is an abbreviation
        userPBndFns = userParticlesModule.UserParticleBoundaryFunctions_C(
            self.particle_P.position_coordinates, self.particle_P.dx)

        # Make the particle-mesh boundary-conditions object and add it
        # to the particle object.
        spNames = self.particle_P.species_names
        #        pmeshBCs = ParticleMeshBoundaryConditions_C(spNames, pmesh_M, userPBndFnsClass, print_flag=False)
        pmeshBCs = ParticleMeshBoundaryConditions_C(spNames,
                                                    pmesh_M,
                                                    userPBndFns,
                                                    print_flag=False)
        self.particle_P.pmesh_bcs = pmeshBCs

        ### 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 = 10  # Set to 1 to make sure one boundary-crossing can be
        # accommodated. Set to a larger value if there are
        # multiple boundary reflections. 10 is needed for
        # all the the reflections in test_4.

        # 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', 'crossings', 'Ex', 'Ey']
        format_list = format_list_base + [
            numpy.float32 for i in range(len(charged_attributes))
        ]
        self.trajin.charged_dict = {
            'names': name_list_base + charged_attributes,
            'formats': format_list
        }

        neutral_attributes = ['x', 'ux', 'y', 'uy']
        format_list = format_list_base + [
            numpy.float32 for i in range(len(neutral_attributes))
        ]
        self.trajin.neutral_dict = {
            'names': name_list_base + neutral_attributes,
            'formats': format_list
        }

        return
Example #8
0
class TestFieldInterpolation(unittest.TestCase):
    """Test the field-to-particle interpolation functions in Field_C class

       The mesh is a on a 2D quarter-circle and is read from a file. The E-field
       is also read from a file.

    """
    def setUp(self):
        # initializations for each test go here...

        # Create mesh from a file
        coordinateSystem = 'Cartesian'
        mesh2D_M = Mesh_C(mesh_file="mesh_quarter_circle_crossed.xml",
                          coordinate_system=coordinateSystem,
                          compute_dictionaries=True,
                          compute_cpp_arrays=False,
                          compute_tree=True,
                          plot_flag=False)

        #        df_m.plot(self.mesh, title='cylindrical mesh', axes=True)
        #        df_m.interactive()

        phi_element_type = 'Lagrange'
        phi_element_degree = 1
        self.phi = Field_C(mesh2D_M,
                           element_type=phi_element_type,
                           element_degree=phi_element_degree,
                           field_type='scalar')

        # Read the potential from a file
        file = df_m.File("phi_test_2_2D.xml")
        file >> self.phi.function

        # Plot phi

        #        df_m.plot(self.phi)
        #        df_m.interactive()

        if phi_element_degree == 1:
            # For linear elements, grad(phi) is discontinuous across
            # elements. To represent this field, we need Discontinuous Galerkin
            # elements.
            electric_field_element_type = "DG"
        else:
            electric_field_element_type = "Lagrange"

        self.neg_electric_field = Field_C(
            mesh2D_M,
            element_type=electric_field_element_type,
            element_degree=phi_element_degree - 1,
            field_type='vector')

        # Read the electric field from a file
        file = df_m.File("negE_test_2_2D.xml")
        file >> self.neg_electric_field.function

        # Create the points for interpolation of the E-field

        # 1st point
        x0 = 1.0
        y0 = 0.0
        z0 = 0.0
        ux0 = 3000.0
        uy0 = 2000.0
        uz0 = 1000.0
        weight0 = 1.0  # number of electrons per macroparticle

        # Can these be named? Yes, with a different dtype.
        p0 = np_m.array([x0, y0, z0, ux0, uy0, uz0, weight0], dtype=float)

        # 2nd point
        x1 = 5.0
        y1 = 0.0
        z1 = 1.0
        ux1 = uy1 = 0.0
        uz1 = -uz0
        weight1 = 2.0

        p1 = np_m.array([x1, y1, z1, ux1, uy1, uz1, weight1], dtype=float)

        # 3nd point: same cell as 2nd point, so same E for DG0 a element.
        x2 = 4.5
        y2 = 0.0
        z2 = 1.0
        ux2 = uy2 = 0.0
        uz2 = -uz0
        weight2 = 2.0

        p2 = np_m.array([x2, y2, z2, ux2, uy2, uz2, weight2], dtype=float)

        self.points = np_m.array([p0, p1, p2])

        # Also create a version with the standard set of attributes for particles
        # with 3D Cartesian coordinates

        # Here is the dtype dictionary:
        self.particle_dtype = {
            'names': [
                'x', 'y', 'z', 'x0', 'y0', 'z0', 'ux', 'uy', 'uz', 'weight',
                'bitflags', 'cell_index', 'unique_ID', 'crossings'
            ],
            'formats': [
                np_m.float64, np_m.float64, np_m.float64, np_m.float64,
                np_m.float64, np_m.float64, np_m.float64, np_m.float64,
                np_m.float64, np_m.float64, np_m.int32, np_m.int32, np_m.int32,
                np_m.int32
            ]
        }

        cell_index0 = 1
        particle0 = (x0, y0, z0, x0, y0, z0, ux0, uy0, uz0, weight0, 0,
                     cell_index0, 0, 0)
        cell_index1 = 36
        particle1 = (x1, y1, z1, x1, y1, z1, ux1, uy1, uz1, weight1, 0,
                     cell_index1, 0, 0)
        cell_index2 = 36
        particle2 = (x2, y2, z2, x2, y2, z2, ux2, uy2, uz2, weight2, 0,
                     cell_index2, 0, 0)

        self.pseg = np_m.array([particle0, particle1, particle2],
                               dtype=self.particle_dtype)

        #print("Mesh cell index of particle 0 is",mesh2D_M.compute_cell_index(self.pseg[0]))
        #print("Mesh cell index of particle 1 is",mesh2D_M.compute_cell_index(self.pseg[1]))
        #print("Mesh cell index of particle 2 is",mesh2D_M.compute_cell_index(self.pseg[2]))

        # dtype of E field at the particles
        self.Ecomps = [
            'x',
            'y',
        ]
        Eexpected = np_m.empty(
            [len(self.points), len(self.Ecomps)], dtype=float)
        Eexpected[0] = [-0.84919658, -0.03336502]
        Eexpected[1] = [-0.19694748, -0.00773809]
        Eexpected[2] = [-0.19694748, -0.00773809]
        self.E_expected = Eexpected

        return

    def test_1_interpolate_vectorField_to_points(self):
        """Python test. Provide 3 points and compute the values of a vector field at
           these points.

           The point data has spatial coordinates and other data, such as velocities.
           The number of spatial coordinates can be greater than the spatial
           dimension of the vector field. E.g., a point can have (x, y, z) spatial
           coordinates, while the vector may be defined only on an (x, y) space. The
           extra particle coordinates are ignored in evaluating the vector field.

        """
        fncname = sys._getframe().f_code.co_name
        print('\ntest: ', fncname, '(' + __file__ + ')')

        force_precision = np_m.float64
        # Make a structured Numpy array to hold the field values at the points
        E_points = np_m.empty(self.points.shape[0],
                              dtype={
                                  'names': self.Ecomps,
                                  'formats': (force_precision, force_precision)
                              })

        self.neg_electric_field.interpolate_field_to_points(
            self.points, E_points)

        # Check the interpolated electric field against the expected values
        for ip in range(len(self.points)):
            for ic in range(len(self.Ecomps)):
                #                print "Ecalc, Eexpect =", E_points[ip][ic], self.E_expected[ip][ic]
                self.assertAlmostEqual(E_points[ip][ic],
                                       self.E_expected[ip][ic],
                                       places=3,
                                       msg="Wrong value of E")

        return
#    def test_1_interpolate_vectorField_to_points(self): ENDDEF

    def test_2_cpp_interpolate_vectorField_to_points(self):
        """C++ test. Provide 3 points and compute the values of a vector field
           at these points.

           The point data has spatial coordinates and other data, such as velocities.
           The number of spatial coordinates can be greater than the spatial
           dimension of the vector field. E.g., a point can have (x, y, z) spatial
           coordinates, while the vector may be defined only on an (x, y) space. The
           extra particle coordinates are ignored in evaluating the vector field.

        """
        fncname = sys._getframe().f_code.co_name
        print('\ntest: ', fncname, '(' + __file__ + ')')

        force_precision = np_m.float64
        nComps = len(self.Ecomps)
        # Make a Numpy 2D array of doubles to hold the field evaluated at the points.
        E_points = np_m.empty((len(self.points), nComps),
                              dtype=force_precision)

        npoints = len(self.points)
        df_so.interpolate_field_to_points_cartesian_xyz(
            self.neg_electric_field, self.pseg, npoints, E_points)

        # Check the interpolated electric field against the expected values
        for ip in range(len(self.points)):
            for ic in range(len(self.Ecomps)):
                #                print "Ecalc, Eexpect =", E_points[ip][ic], self.E_expected[ip][ic]
                self.assertAlmostEqual(E_points[ip][ic],
                                       self.E_expected[ip][ic],
                                       places=3,
                                       msg="Wrong value of E")

        return
Example #9
0
    def test_3_particle_densities_on_2D_mesh(self):
        """Compute the number-densities generated by line particles on a 2D mesh, using the
           C++ functions interpolate_weights_to_dofs3D() and add_weights_to_cells3D().

           This is a copy of
           test_ChargeDensity.py:test_2_interpolate_particle_density_to_2D_mesh(self),
           using dnt_cpp.so instead of pure Python.

           Macroparticles with 3D coordinates are created within a 2D meshed region
           and are weighted to nodal points on the mesh by integrating with the nodal
           shape-function.  To get the cell particle-density, the macroparticle
           weights are summed in each cell and then divided by the cell volume (area
           in this case).
           
           See test_ChargeDensity.ods:test_2 for calculated values of the
           density.

        """

        fncName = '('+__file__+') ' + sys._getframe().f_code.co_name + '():\n'
        print('\ntest: ', fncName, '('+__file__+')')
        
        # Describe a 2D mesh from (-10,-10) to (10,10) with 2 cells on a side.
        # The mesh is triangular, so there's a total of 4x2 = 8 cells.
        umi2d_I = UserMeshInput_C()
        umi2d_I.pmin = df_m.Point(-10.0, -10.0)
        umi2d_I.pmax = df_m.Point(10.0, 10.0)
        umi2d_I.cells_on_side = (2, 2)
        umi2d_I.diagonal = 'left'

        # UserMesh_FE_XYZ_Module can make the mesh from the above input.
        plotFlag = False
        plotTitle = os.path.basename(__file__) + ": " + sys._getframe().f_code.co_name + ": XY mesh"

        pmesh2d_M = UserMesh_C(umi2d_I, compute_dictionaries=True, compute_tree=True, plot_flag=plotFlag, plot_title=plotTitle)
#        pmesh2d_M.compute_cell_vertex_dict()
#        pmesh2d_M.compute_cell_dict()

        ########## Kinetic particles ##########

        # Create an instance of the DTparticleInput class
        pin = self.pin = ParticleInput_C()
        # Set up particle variables
        pin.precision = np_m.float64

        pin.particle_integration_loop = 'loop-on-particles'
        pin.position_coordinates = ['x', 'y', 'z'] # determines the particle-storage dimensions
        pin.force_components = ['x', 'y', 'z']
        pin.force_precision = np_m.float64

        ### Particle species input

        # Give the properties of the particle species.  The charges and masses
        # are normally those of the physical particles, and not the
        # computational macroparticles.

        speciesName = 'plasma_electrons'
        charge = -1.0*MyPlasmaUnits_C.elem_charge
        mass = 1.0*MyPlasmaUnits_C.electron_mass
        dynamics = 'implicit'
        plasmaElectron_S = ParticleSpecies_C(speciesName, charge, mass, dynamics)

        # speciesName = 'H_plus'
        # charge = 2.0*MyPlasmaUnits_C.elem_charge
        # mass = 1.0*MyPlasmaUnits_C.AMU
        # dynamics = 'implicit'
        # Hplus_S = ParticleSpecies_C(speciesName, charge, mass, dynamics)

        # Add these two species to particle input
#        pin.particle_species = (plasmaElectron_S, Hplus_S,)
        pin.particle_species = (plasmaElectron_S,)

        # Make the particle object from pin...
        particles_P = Particle_C(pin, print_flag=False)

        # ...and attach the particle mesh
        particles_P.pmesh_M = pmesh2d_M

        # Put 3 particles inside the meshed region

        x0 = -5.0; y0 = -5.0; z0 = 0.0
        ux0 = 0.0; uy0 = 0.0; uz0 = 0.0
        weight0 = 2.0e10 # number of electrons per macroparticle
        bitflags0 = 0b0
        cell_index0 = 1
        unique_ID = Particle_C.UNIQUE_ID_COUNTER; Particle_C.UNIQUE_ID_COUNTER += 1
        crossings = 0

        p0 = (x0,y0,z0, x0,y0,z0, ux0,uy0,uz0, weight0, bitflags0, cell_index0, unique_ID, crossings)

        # 2nd particle
        x1 = 1.0; y1 = 1.0; z1 = 1.0
        ux1 = uy1 = 0.0; uz1 = -uz0
        weight1 = 3.0e10
        bitflags1 = 0b0
        cell_index1 = 6
        unique_ID = Particle_C.UNIQUE_ID_COUNTER; Particle_C.UNIQUE_ID_COUNTER += 1
        crossings = 0

        p1 = (x1,y1,z1, x1,y1,z1, ux1,uy1,uz1, weight1, bitflags1, cell_index1, unique_ID, crossings)

        # 3nd particle
        x2 = -9.0; y2 = 1.0; z2 = 1.0
        ux2 = uy2 = 0.0; uz2 = -uz0
        weight2 = 4.0e10
        bitflags2 = 0b0
        cell_index2 = 4 # Particle lies on boundary between 0 and 1
        unique_ID = Particle_C.UNIQUE_ID_COUNTER; Particle_C.UNIQUE_ID_COUNTER += 1
        crossings = 0

        p2 = (x2,y2,z2, x2,y2,z2, ux2,uy2,uz2, weight2, bitflags2, cell_index2, unique_ID, crossings)

        # Put the particles into an ndarray with the above type
        nparticles = 3
        particles = (p0, p1, p2)

        ### Put these particles into storage

        # Electrons
        species_name = 'plasma_electrons'

        number_of_macroparticles = len(particles)

        pseg_arr = particles_P.pseg_arr[species_name] # The SegmentedArrayPair_C object for this species

        for i in range(number_of_macroparticles):
#            print ('species_name, particles[i] = ', species_name, particles[i])
            p, pindex = pseg_arr.push_back(particles[i])

        # Ions
#         species_name = 'H_plus'

#         number_of_macroparticles = 3

#         pseg_arr = particles_P.pseg_arr[species_name] # The SegmentedArrayPair_C object for this species

#         for i in range(number_of_macroparticles):
# #            print 'species_name, particles[i] = ', species_name, particles[i]
#             p, pindex = pseg_arr.put(particles[i])

        ###### DoF density vector
        
        # Allocate storage for the number-density values.  The number-density array
        # stores the integral of the physical density-distribution times the element
        # basis functions.

        dofNumberDensityElementType = 'Lagrange'
        dofNumberDensityElementDegree = 1
        dofNumberDensityFieldType = 'scalar'
        dofNumberDensity_F = Field_C(pmesh2d_M,
                                  element_type=dofNumberDensityElementType,
                                  element_degree=dofNumberDensityElementDegree,
                                  field_type=dofNumberDensityFieldType)

#        print "size of dofNumberDensity:", dofNumberDensity_F.function.vector().size()

        # The expected number-density values from test_ChargeDensity.ods:test_2
        dofNumberDensityExpected = np_m.empty(dofNumberDensity_F.function.vector().size(), dtype=np_m.float64)

        # Note that these are in the vertex numbering order, not DoF order. We will
        # need to compare dofNumberDensityExpected[ivert] with dofNumberDensityCalc[idof],
        # where idof is the DoF number corresponding to ivert (see conversion below).
        dofNumberDensityExpected[0] = 0.0
        dofNumberDensityExpected[1] = 1.0e10
        dofNumberDensityExpected[2] = 0.0
        dofNumberDensityExpected[3] = 4.2e10
        dofNumberDensityExpected[4] = 2.8e10
        dofNumberDensityExpected[5] = 3.0e9
        dofNumberDensityExpected[6] = 4.0e9
        dofNumberDensityExpected[7] = 3.0e9
        dofNumberDensityExpected[8] = 0.0

        # Compute the DoF density vector using the C++ functions we're testing here.

        psa = particles_P.pseg_arr[species_name] # segmented array for this species
        (npSeg, pseg) = psa.init_out_loop()

        while isinstance(pseg, np_m.ndarray):

            # Call the (x, y, z) version:
            dnt_cpp.interpolate_weights_to_dofs3D(pseg, dofNumberDensity_F.function._cpp_object)
            (npSeg, pseg) = psa.get_next_segment('out')

        # Get a np_m array of the calculated values
        dofNumberDensityCalc = dofNumberDensity_F.function.vector().get_local()
#        print fncName, dofNumberDensityCalc

        functionSpace = dofNumberDensity_F.function_space
        gdim = dofNumberDensity_F.mesh_gdim
        # Reshape the coordinates to get (x), or (x,y), or (x,y,z) tuples.
        ndofcoords = functionSpace.tabulate_dof_coordinates().reshape((-1, gdim))

#        print "dofcoords=", dofcoords

        # Plot the result

        plotTitle = os.path.basename(__file__) + ": " + sys._getframe().f_code.co_name + ": number density"
        df_m.plot(dofNumberDensity_F.function, title=plotTitle)
        mplot_m.show()
#        yesno = raw_input("Just called show() in test_2_interpolate_particle_density_to_2D_mesh")

        ## Check the values vs. those computed in test_ChargeDensity.ods:test_2

        # Convert vertex indices to DoF indices.  For CG1 elements, there are
        # the same number of vertices as DoFs.
        v2d=df_m.vertex_to_dof_map(functionSpace)

        for ivert in range(len(dofNumberDensityExpected)):
            idof = v2d[ivert] # The DoF number for this vertex.
#            print "vertex", ivert, "is DoF index", idof
#            print "dofNumberDensityCalc, dofNumberDensityExpected =", dofNumberDensityCalc[idof], dofNumberDensityExpected[ivert]
            # "places" means numbers after the decimal point:
            self.assertAlmostEqual(dofNumberDensityCalc[idof], dofNumberDensityExpected[ivert], places=4, msg="Wrong value of dofNumberDensity")


        ###### Cell density vector

        # Allocate storage for the cell number-density values.

        cellNumberDensityElementType = 'DG'
        cellNumberDensityElementDegree = 0
        cellNumberDensityFieldType = 'scalar'
        cellNumberDensity_F = Field_C(pmesh2d_M,
                                      element_type=cellNumberDensityElementType,
                                      element_degree=cellNumberDensityElementDegree,
                                      field_type=cellNumberDensityFieldType)

#        print "size of cellNumberDensity:", cellNumberDensity_F.function.vector().size()

        psa = particles_P.pseg_arr[species_name] # segmented array for this species
        (npSeg, pseg) = psa.init_out_loop()

        while isinstance(pseg, np_m.ndarray):

            # Call the 3D (x, y, z) version:
            dnt_cpp.add_weights_to_cells3D(pseg, cellNumberDensity_F.function._cpp_object)
            
            (npSeg, pseg) = psa.get_next_segment('out')

        # dolfin_cpp.print_dict(particles_P.pmesh_M.cell_volume_dict)

        # Convert the cell values to a cell density in C++
        if cellNumberDensity_F is not None:
            dnt_cpp.divide_by_cell_volumes(cellNumberDensity_F.function._cpp_object, particles_P.pmesh_M.cell_volume_dict)
            
        # Get an array of the density values
        cellNumberDensityCalc = cellNumberDensity_F.function.vector().get_local()
        #print(fncName, "cellNumberDensityCalc =", cellNumberDensityCalc)

        # Make a numpy array to hole the expected number-density values from
        # test_ChargeDensity.ods:test_2
        cellNumberDensityExpected = np_m.empty(cellNumberDensity_F.function.vector().size(), dtype=np_m.float64)

        # Note that these are in the cell numbering order, not DoF order. We will
        # need to compare cellNumberDensityExpected[cellIndex] with
        # cellNumberDensityCalc[dofIndex], where dofIndex is the DoF number
        # corresponding to cellIndex.
        cellNumberDensityExpected[0] = 0.0
        cellNumberDensityExpected[1] = 4.0e8
        cellNumberDensityExpected[2] = 0.0
        cellNumberDensityExpected[3] = 0.0
        cellNumberDensityExpected[4] = 8.0e8
        cellNumberDensityExpected[5] = 0.0
        cellNumberDensityExpected[6] = 6.0e8
        cellNumberDensityExpected[7] = 0.0

        # Compare results

        for cell in df_m.cells(pmesh2d_M.mesh):
            cellIndex = cell.index()
            # There's only 1 DoF in the cell, since this function uses constant DG elements
            dofIndex = cellNumberDensity_F.function_space.dofmap().cell_dofs(cellIndex) # return type: np_m.ndarray
#            print("cellNumberDensityCalc[dofIndex][0]=", cellNumberDensityCalc[dofIndex][0], "cellNumberDensityExpected[cellIndex]=",cellNumberDensityExpected[cellIndex]) 
            self.assertAlmostEqual(cellNumberDensityCalc[dofIndex][0], cellNumberDensityExpected[cellIndex], places=4, msg="Wrong value of cellNumberDensity")

        return
Example #10
0
    def test_2_particle_densities_on_2D_mesh(self):
        """Compute the number-density generated by line particles on a 2D mesh,
           using a C++ function.

           Macroparticles are created within a 2D meshed region and are weighted
           to nodal points on the mesh.
           
           No species data is defined for the particles.  
           No segmented-array particle storage is used for the particles, just
           one np_m array is used, with a particle dtype.

           See test_ChargeDensity.ods:test_2 for calculated values of the
           density.

        """

        fncName = '('+__file__+') ' + sys._getframe().f_code.co_name + '():\n'
        print('\ntest: ', fncName, '('+__file__+')')
        
        # Describe a 2D mesh from (-10,-10) to (10,10) with 2 cells on a side.
        umi2d_I = UserMeshInput_C()
        umi2d_I.pmin = df_m.Point(-10.0, -10.0)
        umi2d_I.pmax = df_m.Point(10.0, 10.0)
        umi2d_I.cells_on_side = (2, 2)
        umi2d_I.diagonal = 'left'

        # UserMesh_FE_XYZ_Module can make the mesh from the above input.
        plotFlag = False
        plotTitle = os.path.basename(__file__) + ": " + sys._getframe().f_code.co_name + ": XY mesh"

        pmesh2d_M = UserMesh_C(umi2d_I, compute_dictionaries=True, compute_tree=True, plot_flag=plotFlag, plot_title=plotTitle)
#        pmesh2d_M.compute_cell_vertex_dict()
#        pmesh2d_M.compute_cell_dict()

        # Put 3 particles inside the meshed region

        x0 = -5.0; y0 = -5.0; z0 = 0.0
        ux0 = 0.0; uy0 = 0.0; uz0 = 0.0
        weight0 = 2.0e10 # number of electrons per macroparticle
        bitflags0 = 0b0
        cell_index0 = 1
        unique_ID = Particle_C.UNIQUE_ID_COUNTER; Particle_C.UNIQUE_ID_COUNTER += 1
        crossings = 0

        p0 = (x0,y0,z0, ux0,uy0,uz0, weight0, bitflags0, cell_index0, unique_ID, crossings)

        # 2nd particle
        x1 = 1.0; y1 = 1.0; z1 = 1.0
        ux1 = uy1 = 0.0; uz1 = -uz0
        weight1 = 3.0e10
        bitflags1 = 0b0
        cell_index1 = 6
        unique_ID = Particle_C.UNIQUE_ID_COUNTER; Particle_C.UNIQUE_ID_COUNTER += 1
        crossings = 0

        p1 = (x1,y1,z1, ux1,uy1,uz1, weight1, bitflags1, cell_index1, unique_ID, crossings)

        # 3nd particle
        x2 = -9.0; y2 = 1.0; z2 = 1.0
        ux2 = uy2 = 0.0; uz2 = -uz0
        weight2 = 4.0e10
        bitflags2 = 0b0
        cell_index2 = 4 # Particle lies on boundary between 0 and 1
        unique_ID = Particle_C.UNIQUE_ID_COUNTER; Particle_C.UNIQUE_ID_COUNTER += 1
        crossings = 0

        p2 = (x2,y2,z2, ux2,uy2,uz2, weight2, bitflags2, cell_index2, unique_ID, crossings)

        # Create the DT particle record type
        pvars = ['x', 'y', 'z', 'ux', 'uy', 'uz', 'weight', 'bitflags', 'cell_index', 'unique_ID', 'crossings']
        pvartypes = [np_m.float64]*7
        pvartypes.append(np_m.int32) # bitflags
        pvartypes.append(np_m.int32) # cell_index
        pvartypes.append(np_m.int32) # unique_ID
        pvartypes.append(np_m.int32) # crossings

        p_dtype = {'names' : pvars, 'formats': pvartypes}

        # Put the particles into an ndarray with the above type
        nparticles = 3
        particles = np_m.empty(nparticles, dtype=p_dtype)
        particles[0] = p0
        particles[1] = p1
        particles[2] = p2

        ###### DoF density vector
        
        # Allocate storage for the number-density values.  The number-density array
        # stores the integral of the physical density-distribution times the element
        # basis functions.

        dofNumberDensityElementType = 'Lagrange'
        dofNumberDensityElementDegree = 1
        dofNumberDensityFieldType = 'scalar'
        dofNumberDensity_F = Field_C(pmesh2d_M,
                                  element_type=dofNumberDensityElementType,
                                  element_degree=dofNumberDensityElementDegree,
                                  field_type=dofNumberDensityFieldType)

#        print "size of dofNumberDensity:", dofNumberDensity_F.function.vector().size()

        # The expected number-density values from test_ChargeDensity.ods:test_2
        dofNumberDensityExpected = np_m.empty(dofNumberDensity_F.function.vector().size(), dtype=np_m.float64)

        # Note that these are in the vertex numbering order, not DoF order. We will
        # need to compare dofNumberDensityExpected[ivert] with dofNumberDensityCalc[idof],
        # where idof is the DoF number corresponding to ivert (see conversion below).
        dofNumberDensityExpected[0] = 0.0
        dofNumberDensityExpected[1] = 1.0e10
        dofNumberDensityExpected[2] = 0.0
        dofNumberDensityExpected[3] = 4.2e10
        dofNumberDensityExpected[4] = 2.8e10
        dofNumberDensityExpected[5] = 3.0e9
        dofNumberDensityExpected[6] = 4.0e9
        dofNumberDensityExpected[7] = 3.0e9
        dofNumberDensityExpected[8] = 0.0

        # Compute the DoF density vector using the DnT functions we're testing here.
        for p in particles:
            dofNumberDensity_F.interpolate_delta_function_to_dofs(p)

        # Get a np_m array of the calculated values
        dofNumberDensityCalc = dofNumberDensity_F.function.vector().get_local()
#        print fncName, dofNumberDensityCalc

        functionSpace = dofNumberDensity_F.function_space
        gdim = dofNumberDensity_F.mesh_gdim
        # Reshape the DoF coordinates to get (x), or (x,y), or (x,y,z) tuples.
        if df_m.__version__ > "1.5.0":
            dofcoords = functionSpace.tabulate_dof_coordinates().reshape((-1, gdim))
        else:
            print('\n!!!WARNING!!!: ', fncName, ": DOLFIN too old.  Skipping rest of test")
            return

#        print "dofcoords=", dofcoords

        # Plot the result

        plotTitle = os.path.basename(__file__) + ": " + sys._getframe().f_code.co_name + ": number density"
        df_m.plot(dofNumberDensity_F.function, title=plotTitle)
        mplot_m.show()
#        yesno = raw_input("Just called show() in test_2_interpolate_particle_density_to_2D_mesh")

        ## Check the values vs. those computed in test_ChargeDensity.ods:test_2

        # Convert vertex indices to DoF indices.  For CG1 elements, there are
        # the same number of vertices as DoFs.
        v2d=df_m.vertex_to_dof_map(functionSpace)

        for ivert in range(len(dofNumberDensityExpected)):
            idof = v2d[ivert] # The DoF number for this vertex.
#            print "vertex", ivert, "is DoF index", idof
#            print "dofNumberDensityCalc, dofNumberDensityExpected =", dofNumberDensityCalc[idof], dofNumberDensityExpected[ivert]
            # "places" means numbers after the decimal point:
            self.assertAlmostEqual(dofNumberDensityCalc[idof], dofNumberDensityExpected[ivert], places=4, msg="Wrong value of dofNumberDensity")


        ###### Cell density vector

        # Allocate storage for the cell number-density values.

        cellNumberDensityElementType = 'DG'
        cellNumberDensityElementDegree = 0
        cellNumberDensityFieldType = 'scalar'
        cellNumberDensity_F = Field_C(pmesh2d_M,
                                      element_type=cellNumberDensityElementType,
                                      element_degree=cellNumberDensityElementDegree,
                                      field_type=cellNumberDensityFieldType)

#        print "size of cellNumberDensity:", cellNumberDensity_F.function.vector().size()

        # Compute the cell density using the DnT functions that we're testing
        for p in particles:
            cellNumberDensity_F.add_weight_to_cell(p)
        cellNumberDensity_F.divide_by_cell_volumes()

        # Get an array of the density values
        cellNumberDensityCalc = cellNumberDensity_F.function.vector().get_local()
#        print fncName, "cellNumberDensityCalc =", cellNumberDensityCalc

        # The expected number-density values from test_ChargeDensity.ods:test_2
        cellNumberDensityExpected = np_m.empty(cellNumberDensity_F.function.vector().size(), dtype=np_m.float64)

        # Note that these are in the cell numbering order, not DoF order. We will
        # need to compare cellNumberDensityExpected[cellIndex] with
        # cellNumberDensityCalc[dofIndex], where dofIndex is the DoF number
        # corresponding to cellIndex.
        cellNumberDensityExpected[0] = 0.0
        cellNumberDensityExpected[1] = 4.0e8
        cellNumberDensityExpected[2] = 0.0
        cellNumberDensityExpected[3] = 0.0
        cellNumberDensityExpected[4] = 8.0e8
        cellNumberDensityExpected[5] = 0.0
        cellNumberDensityExpected[6] = 6.0e8
        cellNumberDensityExpected[7] = 0.0

        # Compare results

        for cell in df_m.cells(pmesh2d_M.mesh):
            cellIndex = cell.index()
#            cellVol = mesh_M.cell_volume_dict[cellIndex]
            # There's only 1 DoF in the cell, since this function uses constant DG elements
            dofIndex = cellNumberDensity_F.function_space.dofmap().cell_dofs(cellIndex) # return type: np_m.ndarray
            self.assertAlmostEqual(cellNumberDensityCalc[dofIndex], cellNumberDensityExpected[cellIndex], places=4, msg="Wrong value of cellNumberDensity")

        return
Example #11
0
    def setUp(self):

        fncName = '(' + __file__ + ') ' + sys._getframe(
        ).f_code.co_name + '():\n'

        # initializations for each test go here...

        # Create an instance of the DTparticleInput class
        pin = ParticleInput_C()

        # Initialize particles
        pin.precision = numpy.float64
        #        pin.copy_field_mesh = False
        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 Python version of particle movers.

        # Specify the particle properties
        # 1. electrons
        # pin.particle_species = (('testelectrons',
        #                      {'initial_distribution_type' : 'listed',
        #                       'charge' : -1.0*MyPlasmaUnits_C.elem_charge,
        #                       'mass' : 1.0*MyPlasmaUnits_C.electron_mass,
        #                       'dynamics' : 'explicit',
        #                       }
        #                      ),
        #                     )

        speciesName = 'two_electrons'
        charge = -1.0 * MyPlasmaUnits_C.elem_charge
        mass = 1.0 * MyPlasmaUnits_C.electron_mass
        dynamics = 'explicit'
        testElectrons_S = ParticleSpecies_C(speciesName, charge, mass,
                                            dynamics)

        # Add these species to particle input
        pin.particle_species = (testElectrons_S, )
        # Make the particle object from pin
        self.particle_P = Particle_C(pin, print_flag=False)

        # Give the name of the .py file containing additional particle data (lists of
        # particles, boundary conditions, source regions, etc.)
        userParticlesModuleName = "UserParticles_2D_e"

        # Import this module
        userParticlesModule = im_m.import_module(userParticlesModuleName)
        self.particle_P.user_particles_class = userParticlesClass = userParticlesModule.UserParticleDistributions_C

        ### two_electrons are present at t=0

        # Name the initialized species (it should be in species_names above)
        speciesName = 'two_electrons'
        # Check that this species has been defined above
        if speciesName not in self.particle_P.species_names:
            print(fncName + "The species", speciesName, "has not been defined")
            sys.exit()

        # Specify how the species will be initialized
        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 %s for species %s " % (
                speciesName, userParticlesModuleName, speciesName)
            sys.exit(errorMsg)

        # Collect the parameters into a dictionary
        # The 'listed' type will expect a function with the same name as the species.
        testElectronParams = {
            'species_name': speciesName,
            'initial_distribution_type': initialDistributionType,
        }

        # The dictionary keys are mnemonics for the initialized particles
        initialParticlesDict = {
            'initial_two_electrons': (testElectronParams, ),
        }

        self.particle_P.initial_particles_dict = initialParticlesDict

        # Create the initial particles
        for ip in initialParticlesDict:
            ipList = initialParticlesDict[ip]
            ipParams = ipList[0]
            s = ipParams['species_name']
            initialDistributionType = ipParams['initial_distribution_type']
            if initialDistributionType == 'listed':
                # Put user-listed particles into the storage array
                self.particle_P.create_from_list(s, False)

        ### Mesh creation

        umi = UserMeshInput2DCirc_C()

        # Create mesh from a file
        umi.mesh_file = 'mesh_quarter_circle_crossed.xml'
        umi.particle_boundary_file = 'mesh_quarter_circle_crossed_Pbcs.xml'

        # These are the (int boundary-name) pairs used to mark mesh
        # facets. The string value of the int is used as the index.
        rmin_indx = 1
        rmax_indx = 2
        thmin_indx = 4
        thmax_indx = 8
        particleBoundaryDict = {
            'rmin': rmin_indx,
            'rmax': rmax_indx,
            'thmin': thmin_indx,
            'thmax': thmax_indx,
        }

        umi.particle_boundary_dict = particleBoundaryDict

        pmesh2D_M = UserMesh2DCirc_C(umi,
                                     compute_dictionaries=True,
                                     compute_cpp_arrays=False,
                                     compute_tree=True,
                                     plot_flag=False)

        self.particle_P.pmesh_M = pmesh2D_M
        self.particle_P.initialize_particle_integration()

        ### Particle boundary-conditions

        # UserParticleBoundaryFunctions_C is where the facet-crossing callback
        # functions are defined.
        # userPBndFnsClass = userParticlesModule.UserParticleBoundaryFunctions_C # Now need an instantiation, not just a class name:
        userPBndFns = userParticlesModule.UserParticleBoundaryFunctions_C(
            self.particle_P.position_coordinates, self.particle_P.dx)

        spNames = self.particle_P.species_names
        pmeshBCs = ParticleMeshBoundaryConditions_C(spNames,
                                                    pmesh2D_M,
                                                    userPBndFns,
                                                    print_flag=False)
        self.particle_P.pmesh_bcs = pmeshBCs

        # The following value should correspond to the element degree
        # used in the potential from which negE was obtained
        phi_element_degree = 1

        # Create a Field_C object to store the field, and to provide
        # interpolation methods.

        if phi_element_degree == 1:
            # For linear elements, grad(phi) is discontinuous across
            # elements. To represent this field, we need Discontinuous
            # Galerkin elements.
            electric_field_element_type = "DG"
        else:
            electric_field_element_type = "Lagrange"

        # Create the negative electric field directly on the particle mesh
        self.neg_electric_field = Field_C(
            pmesh2D_M,
            element_type=electric_field_element_type,
            element_degree=phi_element_degree - 1,
            field_type='vector')

        file = df_m.File("negE_test_2_2D.xml")
        file >> self.neg_electric_field.function

        # Get the initial cell index of each particle.
        self.particle_P.compute_mesh_cell_indices()

        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