Using Heed and GEM Movie for Visualizing MWPC Cascade

Hi, I am trying to create a simulation of an isobutane-filled MWPC with two wire planes and a mylar cathode in between them. In this simulation, a 5-MeV alpha particle enters the volume of gas and leaves an ionization trail, which I consider primary electrons. I then want to take this trail of primary ionization electrons and propagate it through an MWPC, creating an avalanche of secondary electrons.

I am presently trying to make a movie for visualizing the process. I use HEED example to calculate the first ionization trail, save the positions and energies of resulting cluster primary electrons to a vector, and then feed this vector as primaries to code borrowed from single GEM movie example, which then calculates avalanches of secondary electrons.

The resulting simulation is very slow, which is perhaps unsurprising given the large number of particles I start with (a few hundred, which multiply to a few hundred thousand). More importantly, I am not sure I am approaching it correctly, and not fully understanding the HEED step of the process. I attach an image below of frame zero from my section of the code, which uses GEM example to produce an ionization movie. In my understanding, frame zero should represent the initial conditions fed to the GEM example. In the image, there are several sideways branches of ionization electrons visible, which suggests that there are already some secondary ionizations from electrons in the first step of the movie not resulting from the visible central alpha trace. I think I am likely misunderstanding how HEED works.

Am I interpreting this correctly? And if so, what would you recommend? I attach my full source code as well.

#include <iostream>
#include <cmath>
#include <vector>

#include <TFile.h>
#include <TCanvas.h>
#include <TH1I.h>
#include <TH2F.h>
#include <TString.h>
#include <TLatex.h>
#include <TApplication.h>
#include <TSystem.h>

//neBEM
#include "Garfield/SolidWire.hh" 
#include "Garfield/SolidBox.hh" 
#include "Garfield/GeometrySimple.hh"
#include "Garfield/MediumMagboltz.hh"
#include "Garfield/MediumConductor.hh"
#include "Garfield/MediumMylar.hh"
#include "Garfield/ComponentNeBem3d.hh"
#include "Garfield/ViewGeometry.hh"
#include "Garfield/ViewField.hh"

//Heed
#include "Garfield/Sensor.hh"
#include "Garfield/TrackHeed.hh"
#include "Garfield/ViewDrift.hh"

//Gem movie
#include "Garfield/AvalancheMicroscopic.hh"
#include "Garfield/AvalancheMC.hh"
#include "Garfield/Random.hh"

using namespace Garfield;

int main(int argc, char* argv[]){
	
  TApplication app("app", &argc, argv);
  
  const float P = 5.250432; //7 mbar in torr
  const float V_wire = 650.;
  const int N_alpha = 1;
  const double E_alpha = 5.; //MeV alpha particle
  const double radius = 0.003; //Wire radius
  const double halflength = 5.; //Wire half-length
  const float pitch = 0.2; //Distance between wires in cm
  //const float mylar_thickness = 0.00009;
  const float mylar_thickness = 0.;
  const float plot_z_height = 0.9;
  const float wires1_height = 0.3;
  const float wires2_height = 0.9;
  const float cathode_height = 0.6;
  const float cathode_eloss = 0.106451; //MeV, SRIM calculated energy loss of 5 MeV alpha in 0.9 um of Mylar
  
  const bool gif = 1; //Boolean to animate avalanches
  
  TFile* rootout = TFile::Open("MWPC.root", "recreate"); //ROOT output
  
  //Output histograms
  TH1I* h_de = new TH1I("h_de", "h_de", 700, 0, 7);
  TH1I* h_ne = new TH1I("h_ne", "h_ne", 1e4, 0, 1e4);
  TH1F* h_a_range = new TH1F("h_a_range", "h_a_range", 1000, 0, 1);
  TH1D* h_ne_fin = new TH1D("h_ne_fin", "h_ne_fin", 1e6, 0, 1e8);
  TH2F* h_E = new TH2F("h_E", "h_E", 1000, -5, 5, 1000, -5, 5);
	
  //Set up gas
  MediumMagboltz gas("ic4h10", 100.);
  
  gas.SetPressure(P);
  gas.SetTemperature(293.15); //Kelvin
  gas.EnableThermalMotion(true);

  //Geometry
  GeometrySimple geo;
  
  std::vector< std::vector<SolidWire> > Wires; //Vector to list wires
  
  Wires.resize(2); //Column 0 is for x wires, column 1 is for y wires
  
  float start_wire_pos = -1. * halflength; //Position in cm of first wire in cm in along +/-5 cm axis
  float end_wire_pos = halflength; //Same as above but last wire
  
  int N_wires = floor((end_wire_pos - start_wire_pos)) / pitch; //Number of wires fitting into that space
	
  MediumConductor metal;
  
  std::cout << std::endl << "Processing " << N_wires << " wires along x and y axis each." << std::endl << std::endl;
  
  //Iteratively calculate x and y wire plane positions and add them to vector
  for(int i = 0; i <= N_wires; i++){
  	
  	float wire_pos = start_wire_pos + (float)i * pitch;
  	
  	if(i == 1){std::cout << "   Starting wire position " << wire_pos << std::endl;}
  	
		SolidWire wire1(0., wire_pos, wires1_height, radius, halflength, 1, 0, 0);
		SolidWire wire2(wire_pos, 0., wires2_height, radius, halflength, 0, 1, 0);
		
		wire1.SetBoundaryPotential(V_wire);
		wire2.SetBoundaryPotential(V_wire);
		
		Wires[0].push_back(wire1);
		Wires[1].push_back(wire2);
  }
  
  //Add wires to geometry
  for(int i = 0; i < (int)Wires[0].size(); i++){
		
		geo.AddSolid(&Wires[0][i], &metal);
		geo.AddSolid(&Wires[1][i], &metal);
  }
  
  MediumConductor mylar;
  //MediumMylar mylar;
  
  //Create a solid cathode plane made of Mylar between x and y wire planes
  SolidBox cathode(0., 0., cathode_height, halflength, halflength, mylar_thickness / 2.);
  
  //Cathode is grounded
  cathode.SetBoundaryPotential(0.);
  
  geo.SetMedium(&gas);
  
  geo.AddSolid(&cathode, &mylar);
	
	//Initialize neBEM calculation for electric fields
  ComponentNeBem3d nebem;
  nebem.SetGeometry(&geo);
  nebem.SetTargetElementSize(1.);
  nebem.UseLUInversion();
  nebem.SetNumberOfThreads(18);
  nebem.Initialise();
  
  std::cout << std::endl;
	
  //Calculate field and potential at point  
  double ex = 0., ey = 0., ez = 0., V = 0.;
  int status = 0;
  Medium* medium = nullptr;
  
  //Make a 2D plot of electric field
  for(int i = 0; i < h_E->GetNbinsX(); i++){
  
  	for(int j = 0; j < h_E->GetNbinsY(); j++){
  	
  		nebem.ElectricField(-5. + 10. / (float)i, -5. + 10. / (float)j, plot_z_height, ex, ey, ez, V, medium, status);
  		
  		h_E->SetBinContent(i + 1, j + 1, V);
  		h_E->SetBinError(i + 1, j + 1, 0.);
  	}
  }
  
  std::cout << std::endl << "   Plotting electric fields " << std::endl << std::endl;
 
  ViewField fieldView1(&nebem);
  ViewField fieldView2(&nebem);
  ViewField fieldView3(&nebem);
  
  fieldView1.SetArea(-5., -5., 0., 5., 5., 1.);
  fieldView2.SetArea(-5., -5., 0., 5., 5., 1.);
  fieldView3.SetArea(-5., -5., 0., 5., 5., 1.);
  
  fieldView1.SetVoltageRange(-2000., 2000.);
  fieldView2.SetVoltageRange(-2000., 2000.);
  fieldView3.SetVoltageRange(-2000., 2000.);
  
  fieldView1.SetPlane(0., 0., 1., 0., 0., cathode_height);
  fieldView1.PlotContour("e");
  
  fieldView2.SetPlaneXZ();
  fieldView2.PlotContour("e");
  
  fieldView3.SetPlaneYZ();
  fieldView3.PlotContour("e");
  
  ViewGeometry geomView3d(&geo);
  geomView3d.Plot();
	
  //Gas file handling
  gas.WriteGasFile("isobutane.gas");
  
  //Track particle
  Sensor sensor;
  sensor.AddComponent(&nebem);
	
  rootout->cd();
	
  TCanvas canvas("c", "c", 600, 600);
	
  canvas.SetLeftMargin(0.16);
  
  double prev_z = 0.;
	
  std::cout << std::endl << "   Simulating avalanches for " << N_alpha << " alpha(s)" << std::endl << std::endl;
	
  //Loop over desired number of alpha particles
  for(int i = 0; i < N_alpha; i++){
	
		TrackHeed heed; //HEED calculates ionizations along alpha particle track in gas
		
		heed.SetSensor(&sensor);
		heed.EnableElectricField();
		heed.DisableDeltaElectronTransport();
		heed.Initialise(&gas, 1);
		
		//const float p_alpha = 1.e6 * sqrt(2. * 4. * 931.494 * E_alpha); //4 amu alpha momentum, momentum in eV/c
		
		heed.SetParticle("ALPHA");
		//heed.SetMomentum(p_alpha);
		heed.SetKineticEnergy(1.e6 * E_alpha); //This is in units of eV
		
		std::cout << std::endl << "   Simulating " << E_alpha << "-MeV alpha with momentum " << heed.GetMomentum() / 1.e6 << " MeV/c (beta = " << heed.GetBeta() << "% of c, read-in energy " << heed.GetKineticEnergy() << " eV)" << std::endl << std::endl;
		
		heed.NewTrack(0.1, 0.1, 0., 0., 0, 0, 1); //Particle goes in z direction
		
		double esum = 0.; //Total energy loss	
		int nsum = 0; //Total number of electrons produced along the track
				
		AvalancheMicroscopic aval(&sensor); //This is a list of initial particles for simulating avalanche cascades. For this example, we add all the electrons produced by alpha ionization here and propagate them later through the detector. 
		
		int n_clusters = 0;
		
		rootout->cd();
		
		TH1D* h_eloss = new TH1D(Form("h_eloss%d", i), Form("h_eloss%d", i), 1000, 0, 1);
		
		for(const auto& cluster : heed.GetClusters()){
			
			n_clusters++;
			
			if(n_clusters == heed.GetClusters().size()){
			
				h_a_range->Fill(cluster.z);
			}
			
			//Adjust alpha energy for energy lost to ionization
			heed.SetKineticEnergy(heed.GetKineticEnergy() - cluster.energy);
			
			//When alpha crosses the cathode, subtract SRIM-based energy loss in eV
			if(prev_z < cathode_height && cluster.z >= cathode_height){
			
				heed.SetKineticEnergy(heed.GetKineticEnergy() - 1.e6 * cathode_eloss);
			}
			
			prev_z = cluster.z;
			
			h_eloss->SetBinContent(h_eloss->FindBin(cluster.z), heed.GetKineticEnergy() / 1.e3);
				
			esum += cluster.energy;
			nsum += cluster.electrons.size();
			
			//Save all the ionization electrons for later cascade propagation
			for(const auto& electron : cluster.electrons){
				
				aval.AddElectron(electron.x, electron.y, electron.z, electron.t, electron.e);
			} //End of loop over electrons in a cluster
		} //End of loop over collision clusters
		
		std::cout << std::endl << "   Total energy loss " << esum << " eV" << std::endl;
		std::cout << "   Total secondary electrons " << nsum << std::endl << std::endl << std::endl;
		
		h_de->Fill(esum / 1.e3);
		h_ne->Fill(nsum);
		
		std::cout << "Animating the cascade" << std::endl;

		AvalancheMC drift(&sensor);

		ViewDrift driftView;
		
		aval.EnableExcitationMarkers(false);
		aval.EnableIonisationMarkers(true);
		aval.EnableAttachmentMarkers(false);
		
		aval.EnablePlotting(&driftView);
		drift.EnablePlotting(&driftView);
		
		driftView.SetPlaneXZ();
		driftView.SetArea(-5., 0., 5., 1.);
		//driftView.SetArea(-1., 0., 1., 0.4);
		driftView.SetCanvas(&canvas);
	
		const int nFrames = 50; //Number of steps for gif of avalanches calculation
		double tmin = 0.;
		double dt = 0.3; //Time steps in avalanche in units of ns
		
		drift.SetTimeSteps(dt);
		
		rootout->cd();
		
		TH1D* mult = new TH1D(Form("mult%d", i), Form("mult%d", i), nFrames, tmin, tmin + nFrames * dt);
		
		for(int frame = 0; frame < nFrames; frame++){
			
			std::cout << "       Frame " << frame << std::endl;
			std::cout << "       Ne-:  " << aval.GetElectrons().size() << std::endl;
			
			//Make a histogram of total electrons inside the detector per unit time
			mult->SetBinContent(mult->FindBin(tmin), aval.GetElectrons().size());

			driftView.Clear();
			
			if(!aval.GetElectrons().empty()){
			
				aval.SetTimeWindow(tmin, tmin + dt);
				drift.SetTimeWindow(tmin, tmin + dt);
				
				aval.ResumeAvalanche();
				drift.ResumeAvalanche();
			
				canvas.cd();
				canvas.Update();
				
				gSystem->ProcessEvents();
				
				tmin += dt;

				driftView.Plot2d(true, true);	
				
				if(gif){
					
					if(frame == nFrames - 1){
					
						canvas.Print("MWPC_Movie.gif++");
					}
					
					else{
					
						canvas.Print("MWPC_Movie.gif+3");
					}
				}
			} //End of if avalanche is empty
			
			else{frame = nFrames;}
		} //End of loop over frames for a movie
		
		h_ne_fin->Fill(aval.GetElectrons().size());
	}
	
  rootout->Write();
  rootout->Close();
	
	std::cout << std::endl << "Finished" << std::endl;
	
  app.Run();
  
  return 0;
}

Thanks @NSos for adding your source code, this was very useful for understanding the situation.

I do not think HEED is doing anything strange here, although you are using it a bit on the edge or even beyond it’s original scope. HEED = High-Energy Electron Dynamics is actually meant to simulate the energy loss for relativistic particles, your 5 MeV alpha is quite a long way from that (beta*gamma = 0.05), you might want to check the output of HEED with GEANT or SRIM/TRIM simulations.

What you actually plot in your movie / viewdrift is the position of the electrons at tmin+delta t, so frame 0 does not give you the pure ionization electrons from HEED, but gives you the electrons from the microscopic tracking after 300ps, which is already enough to see some electrons moving away. You can make a frame 0 by having a different timestep (e.g. 1fs):

if(frame==0) {
   aval.SetTimeWindow(tmin, tmin + 1e-6); // 1fs
   drift.SetTimeWindow(tmin, tmin + 1e-6); // 1fs
}
else {
  aval.SetTimeWindow(tmin, tmin + dt);
  drift.SetTimeWindow(tmin, tmin + dt);
}

Also doing a simple print-out of the position your ionization electrons inside the cluster (when you are in your loop over the clusters) shows you that x,y positions are all exactly 0.1, so no deviations as you show in your plot.
greets
Piet

1 Like

Hi Piet, thank you very much for your very helpful reply. I did not realize that frame zero will already have dT applied. Indeed, I get a straight line with 1 fs dT you suggest.

So am I correct in understanding, that HEED will not do any secondary ionization from alpha-ejected electrons? As in, a cluster only contains delta electrons that were ejected by an alpha? I did add the DisableDeltaElectronTransport() option, which sounds to me like it would prevent any avalanches before GEM bit of the code.

Regarding HEED, you are correct, I did notice it gives me warnings about low particle velocity, and I do know that Garfield++ is primarily aimed at particle rather nuclear physics applications. I checked against SRIM, and in 7mbar isobutane with a depth of 1 cm, I get 17 keV energy loss for a 5-MeV alpha, whereas HEED (if I am using it correctly) gives me 100 keV. Is there some other example in Garfield++, which you would recommend for such low-energy applications? I tried SRIM package in Garfield++, but that does not give me actual electron clusters, just an aggregate calculation of all of them.

Lastly, would you happen to have any tips for speeding up the GEM calculation? It is currently incredibly slow, but I don’t have a very good grasp of what to expect in ideal case. I set neBEM part of my code to run on 18 threads, and looking at my computer resource usage, it looks to be using all 18 threads in the GEM part of the code too, but I am not sure.

Sorry about this multitude of questions, I am quite new to Garfield, and am finding myself checking everything from source code, which to me is slow and uncertain. Thank you again for your help!

Dear @NSos

So am I correct in understanding, that HEED will not do any secondary ionization from alpha-ejected electrons? As in, a cluster only contains delta electrons that were ejected by an alpha? I did add the DisableDeltaElectronTransport() option, which sounds to me like it would prevent any avalanches before GEM bit of the code.

So if you set DisableDeltaElectronTransport() then HEED will only simulate the primary electrons liberated by the passing charged particle, and a substantial fraction of these primary electrons might have energy high enough to further ionize. If you provide these primary electrons to AvalancheMicroscopic then this class will do the ionization of these electrons and you will get a set of secondary electrons that cannot further ionize (unless they pick-up energy from the electric field).

If instead you have set EnableDeltaElectronTransport, HEED will simulate the ionization by these primary electrons with a build-in phenomenological algorithm. This simulation however is not complete. It tells you only the amount of secondary elecrrons created by this primary electron, but the [x,y,z,t] position will be equal to the [x,y,z,t] of the primary electron. I.E. HEED does not perform a spatial simulation. You can anyway check this out for yourself by doing a printout of [x,y,z,t] each electron of the cluster and check for the differences with DeltaElectronTransport enabled/disabled. Have a look also at the Garfield++ userguide (section 5.1.1).

I checked against SRIM, and in 7mbar isobutane with a depth of 1 cm, I get 17 keV energy loss for a 5-MeV alpha, whereas HEED (if I am using it correctly) gives me 100 keV.

mhm … not sure where you saw 100keV energy loss … maybe because of your printout:

Screenshot 2025-01-09 at 15.47.52

but you should also check the length of your track. I added heed.EnableDebugging() to get some more information about the HEED simulation, and there I can read that your detector volume is 10.6cm long in the z-direction.

Your track starts however at [0.1, 0.1, 0] and your ionization electrons are in the range 0.00 < z < 5.90 cm … so dividing 102.9keV by 5.9cm I get 17.44 keV/cm, which is in agreement with SRIM and is also somehow in agreement with the stopping powers printed out above by HEED (16.8 keV/cm). So that does not seem too bad.

Alternatives: use the output of SRIM or TRIM inside garfield++ (more info: UserGuide section 5.2 and 5.3).

Lastly, would you happen to have any tips for speeding up the GEM calculation? It is currently incredibly slow, but I don’t have a very good grasp of what to expect in ideal case.

Well, it slows down I believe because you are multiplying the electrons close to your wires. You could think of using AvalancheMC or DriftLineRKF classes instead of AvalancheMicroscopic given that your geometry consists of wires instead of Micro-Pattern structures. You could either:

  1. EnableDeltaElectronTransport() and then pass your electrons to DriftLineRKF that will take care of the calculation of the driftlines and the amplification. Note that here the electron will folllow strictly the fieldlines (as if there is no diffusion or scattering with the gas).
  2. EnableDeltaElectronTransport() and then pass your electrons to AvalancheMC.
  3. keep DisableDeltaElectronTransport() and pass your electrons to AvalancheMicroscopic as you are doing now, but only for about 0.5 - 1ns to simulate the delta electrons and then pass your electrons to AvalancheMC for the drift towards your wires and subsequent amplification.
  4. keep your code as it is, but lower your electric fields to reduce the amplification

PS: Note that for AvalancheMC and DriftLineRKF you need swarm parameters of your gas (e.g. drift velocity, Townsend coefficient etc) and not just the electron-gas cross sections as is the case for AvalancheMicroscopic. For this you need to configure your MediumGas or MediumMagboltz by loading a gasfile, not just by defining the gas mixture, temperature and pressure. You can see an example of how to create your gasfile in this example.

Kind regards
Piet

1 Like

Hi Piet, thank you very much again for these explanations! Regarding the chamber depth, the actual device is only 1 cm deep, I didn’t realize I was caclulating over such a long interval, but you’re right, I have not defined this in the code. I will need to find some way to limit the height to just 1 cm. I am very glad about agreement with SRIM though, that’s great news!

And thank you again for these GEM suggestions. I will try option 2, I think, and then see what happens.

I will mark the thread as solved now. Thank you again for having a look!

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.