G4AssemblyVolume with G4MultiFunctionalDetector

I have been developing a complex geometry using G4AssemblyVolume. It is proving to be a really simple way to group solids together and then create multiple, identical, instances. I’m using it to create multiple gamma detectors. In each of the imprinted assembly volumes is a scintillator detector, which I need to act as a SensitiveDetector, ideally a G4MultiFunctionalDetector.

I am aware that a SensitiveDetector must be linked to a Logical Volume, but how do I link a SensitiveDetector to a component within each of the G4AssemblyVolumes?

I suspect that I will need to use Copy Numbers or Replica Numbers. An example of what I need to do would be very helpful.

Thanks in advance.

Hold on, am I being silly here - do I just add the G4MultiFunctionalDetector to LogicalVolume before I add it to the G4AssemblyVolume?

You’ve got it! Then, in your SD code, you’ll want to traverse the G4Touchable associated with the step (preStepPoint, to avoid the boundary-crossing issue) to get the copy numbers you’re interested in. By using the touchable, you can collect any nested placements all the way up the chain, if that’s important.

Ah ok, so if I want to log the energy deposited in the G4MultiFunctionalDetector on a per event basis, can I do that in EventAction.cc using the copy number?

Apologies for all the questions - there appear to be very few examples of G4AssemblyVolume out there

That sounds right. We don’t use MFD in our simulation, so I’m looking at the .hh file now. The ProcessHits() command gets the G4Step and the correct G4TouchableHistory, from which the copy numbers can be extracted. If you write your own implementation of ProcessHits() in your subclass, then you do what you need to do there. Maybe you make separate scores for each placement?

In our SD classes, we populate our own HitsCollection, and we include a data member for each hit which is the detector copy number.

Ok, so I swapped the MFD for a SensitiveDetector and Hit approach. Here’s the problem… the only place I can add a SensitiveDetector to a LogicalVolume is within ConstructSDandField. But isn’t this after I’ve added the LV to the AssemblyVolume?

What you have to do is store the LV pointer when you create it into a data member. Then in ConstructSDandField() you can access that data member to do the attachment.

In our simulation, we have many volumes to which we attach SDs, so I have a std::vector<G4LogicalVolume*> data member in our DetectorConstruction subclass. The relevant LVs get pushed onto that, and in ConstructSDandField() (in each worker thread), we loop over it and attach the SD instance for that thread. We avoid stale pointers by clearing the vector at the start of ::Construct().

Thanks for your explanation.

I sort of get what you are saying but would you be able to show a simple example to ensure I’m not misunderstanding something.

I can’t post our experiment’s code directly ((a) it’s not considered “open”, and (b) it’s freakin’ huge :slight_smile: ). Here’s a sketch of what you’d do for the simple case of a single volume.

class MyGeometryBuilder : public G4VUserDetectorConstruction {
public:
  MyGeometryBuilder() {;}
  ~MyGeometryBuilder() {;}

  virtual void Construct();
  virtual void ConstructSDandField();

private:
  G4LogicalVolume* detectorLV;
};
void MyGeometryBuilder::Construct() {
  // Make the world here

  G4VSolid* detShape = G4Tubs("Detector", 0., 50*mm, 17*mm, 0., twopi);
  G4Material* detMat = nistManager->FindOrBuildMaterial("G4_Si");
  // Save the detector for later sensitization
  detectorLV = new G4LogicalVolume(detShape, detMat, detShape->GetName());

  // Place the detector (really, it'll be deep in some apparatus)
  new G4PVPlacement(G4Transform3D(), detectorLV, detectorLV->GetName(),
                     worldLV, false, detNum);
}

void MyGeometryBuilder::ConstructSDandField() {
  // Do all the stuff with G4SDManager!
  G4VSensitiveDetector* SD = new MySD;

  detectorLV->SetSensitiveDetector(SD);
}

@mkelsey thanks for your reply.

This works for a simple volume. However, if my geometry is defined with a G4AssemblyVolume and I replace detectorLV within ConstructSDandField() with a logical volume associated with one of the G4AssemblyVolume components, it fails. When I do this, ProcessHits() in my SensitiveDetector class is never called, which is obviously an issue. This is why I posted this post.

Do you know why this is and how I can solve it when using G4AssemblyVolume?

I could, of course, just do away with G4AssemblyVolume and define each instance of the same geometry individually. However, this is rather tiresome when G4AssemblyVolume does such a good job from a detector construction point of view.

This sounds unpleasant. We don’t use G4AssemblyVolume in my experiment, so I’m not able to help you directly. As you write above, it does seem as though G4AssemblyVolume is meant to be treated “like an LV”, and has it’s own internal methodology for getting placed (MakeImprint()). There don’t seem to be actual LVs or PVs returned.

I’m curious what happens if you try to use GetVolumesIterator() to access the “imprinted” PVs, do those PVs return a sensible “GetLogicalVolume()”? Or if you access one of the G4AssemblyTriplet::GetVolume() to get an LV?

It appears that @ivana was involved with G4AssemblyVolume. I wonder if she’d be able to give you advice about attaching SDs to the constituents.

Yep. Defnitely unpleasant.

I’ve searched and searched online for examples of how to do this and failed to find anything. I’ll have a look at the GetVolumesIterator() and GetLogicalVolume(). When I printed the logical volumes within the UI, it showed one instance of each component within the G4AssemblyVolume, even though I’ve used MakeImprint() twice (i.e. two copies).

Hopefully @ivana, or someone else on here, will be able to advise me on how to solve this. My fallback is to just revert to independent definitions for identical components but this makes my code much more complicated for no real gain. It also raises the question, how can anyone use G4AssemblyVolume with SensitiveDetector.

Thanks for your reply @mkelsey

I have the same question, both for SDs as well as for electric/magnetic fields.

Here’s a possibility, but I’m not promising anything. I wonder if the “imprinting” is done thread by thread? Maybe you need to stash the pointer to your G4AssemblyVolume in your detector class. Then, in your ConstructSDandField() function, do either the GetVolumesIterator() or GetTripletsIterator() to access the LVs on a per-thread basis.

Hello,

G4AssemblyVolume is just a technique for volume placement which combines the transformation of each assembly component with the transformation of the whole in the mother volume.

The G4AssemblyVolume::MakeImprint() should be called in UserDetectorConstruction::Construct (which is performed on master) as other volumes placements (G4PVPlacement etc.)

For example (taken from Application Developers Guide):

G4VPhysicalVolume* UserDetectorConstruction::Construct()
{
  // Define a plate
  G4Box* PlateBox = new G4Box( "PlateBox", plateX/2., plateY/2., plateZ/2. );
  G4LogicalVolume* plateLV = new G4LogicalVolume( PlateBox, Pb, "PlateLV", 0, 0, 0 );

  // Define assembly volume
  G4AssemblyVolume* assemblyDetector = new G4AssemblyVolume();

  // Rotation and translation of a plate inside the assembly
  G4RotationMatrix Ra;
  G4ThreeVector Ta;
  G4Transform3D Tr;

  // Rotation of the assembly inside the world
  G4RotationMatrix Rm;

  // Fill the assembly by the plates
  Ta.setX( caloX/4. ); Ta.setY( caloY/4. ); Ta.setZ( 0. );
  Tr = G4Transform3D(Ra,Ta);
  assemblyDetector->AddPlacedVolume( plateLV, Tr );

  /// ... skipped

  // Now instantiate the layers
  for( unsigned int i = 0; i < layers; i++ )
  {
    // Translation of the assembly inside the world
    G4ThreeVector Tm( 0,0,i*(caloZ + caloCaloOffset) - firstCaloPos );
    Tr = G4Transform3D(Rm,Tm);
    assemblyDetector->MakeImprint( worldLV, Tr );
  }

}

The sensitive volume should be set to the assembly components, called plateLV in the example above, in UserDetectorConstruction::ConstructSDandField();
To access these volumes from the assembly, you can use std::vector<G4AssemblyTriplet>::iterator GetTripletsIterator() to get the input "triplets’, and then G4AssemblyTriplet::GetVolume().

Best regards,

Thank you @ivana.

My geometry is looking good with G4AssemblyVolume.

I can follow your response up until:

To access these volumes from the assembly, you can use std::vector<G4AssemblyTriplet>::iterator GetTripletsIterator() to get the input "triplets’, and then G4AssemblyTriplet::GetVolume() .

Obviously this is my lack of understanding rather than your response, but would you be able to provide a very short example of how to assign SDs to specific components within an assembly, using the plateLV logical volume as an example?

You can add in UserDetectorConstruction.hh

    G4AssemblyVolume* fSensitiveAssembly;

and in UserDetectorConstruction::Construct():

   // ...
   // Define assembly volume
   G4AssemblyVolume* assemblyDetector = new G4AssemblyVolume();
   fSensitiveAssembly = assemblyDetector;
   // ...

and then implement in UserDetectorConstruction::ConstructSDandField():

  //  Create SD
  auto assemblySD = new CalorimeterSD("AssemblySD", "AssemblyHitsCollection");
  G4SDManager::GetSDMpointer()->AddNewDetector(assemblySD);

  //  Set SD to volumes placed via assembly
  std::size_t counter = 0;
  auto tripletIterator = fSensitiveAssembly->GetTripletsIterator(); 
  while (counter < fSensitiveAssembly->TotalTriplets()) {
    const auto& triplet = (*tripletIterator);
    auto lv = triplet.GetVolume();
    lv->SetSensitiveDetector(assemblySD);
    ++tripletIterator;
    ++counter;
  }

In the example above, the same SD object is assign to all LV’s placed via this assembly; but you can also define more SD objects and assign them only to selected volumes etc.

If your logical volume names are unique in your geometry, you can also set your SD in your DetectorConstruction via logical volume names, as it is demonstrated in basic example B4c:

  auto assemblySD = new CalorimeterSD("assemblySD", "AbsorberHitsCollection");
  G4SDManager::GetSDMpointer()->AddNewDetector(assemblySD);
  SetSensitiveDetector("PlateLV", assemblySD);

Ok, so in my code I’ve done the equivalent of the following:

 auto assemblySD = new CalorimeterSD("assemblySD", "AbsorberHitsCollection");
  G4SDManager::GetSDMpointer()->AddNewDetector(assemblySD);
  SetSensitiveDetector("PlateLV", assemblySD);

Where my equivalent of PlateLV is one of the components within my G4AssemblyVolume. I have imprinted two of the G4AssemblyVolumes into my world. When I run the code, events are created but ProcessHits() is never triggered in my equivalent of CalorimeterSD(). So, even though there are no errors, no events are ever registered. If I simply swap PlateLV for the logical volume for my world (WorldLV), everything works fine.

I have tested the code posted above within B4c example and the SD was called with both implementation.

Can you check by running with a geantino, ‘/tracking/verbose 1’ and adding a simple printing in your SD::ProcessHits() whether the SD is really skipped?

So….

After some further investigation, I think I have found the cause of why ProcessHits() was not being called. I’d missed a warning that a volume was overlapping with its mother volume. It wasn’t so much an overlap as such, more bad definition of the mother volume, which resulted to in an overlap. After finding and fixing that, ProcessHits() is now triggered.

I think I’m up and running again - phew! Thank you for your help.

I guess to distinguish between hits in SensitiveDetector, do I just use the CopyNo?