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