Analysis Software
Documentation for sPHENIX simulation software
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Groups Pages
SimulationActor.hpp
Go to the documentation of this file. Or view the newest version in sPHENIX GitHub for file SimulationActor.hpp
1 // This file is part of the Acts project.
2 //
3 // Copyright (C) 2018-2021 CERN for the benefit of the Acts project
4 //
5 // This Source Code Form is subject to the terms of the Mozilla Public
6 // License, v. 2.0. If a copy of the MPL was not distributed with this
7 // file, You can obtain one at http://mozilla.org/MPL/2.0/.
8 
9 #pragma once
10 
17 
18 #include <algorithm>
19 #include <cassert>
20 #include <limits>
21 
22 namespace ActsFatras {
23 namespace detail {
24 
37 template <typename generator_t, typename decay_t, typename interactions_t,
38  typename hit_surface_selector_t>
41 
44  // This references the SimulationActor to automatically access its result
45  // type.
47 
48  template <typename propagator_state_t, typename stepper_t,
49  typename navigator_t>
50  constexpr bool operator()(propagator_state_t & /*state*/,
51  const stepper_t & /*stepper*/,
52  const navigator_t & /*navigator*/,
53  const result_type &result,
54  const Acts::Logger & /*logger*/) const {
55  // must return true if the propagation should abort
56  return not result.isAlive;
57  }
58  };
59 
61  generator_t *generator = nullptr;
63  decay_t decay;
65  interactions_t interactions;
67  hit_surface_selector_t selectHitSurface;
70 
73 
83  template <typename propagator_state_t, typename stepper_t,
84  typename navigator_t>
85  void operator()(propagator_state_t &state, stepper_t &stepper,
86  navigator_t &navigator, result_type &result,
87  const Acts::Logger & /*logger*/) const {
88  assert(generator and "The generator pointer must be valid");
89 
90  // actors are called once more after the propagation terminated
91  if (not result.isAlive) {
92  return;
93  }
94 
95  // check if we are still on the start surface and skip if so
96  if ((navigator.startSurface(state.navigation) != nullptr) &&
97  (navigator.startSurface(state.navigation) ==
98  navigator.currentSurface(state.navigation))) {
99  return;
100  }
101 
102  // update the particle state first. this also computes the proper time which
103  // needs the particle state from the previous step for reference. that means
104  // this must happen for every step (not just on surface) and before
105  // everything, e.g. any interactions that could modify the state.
106  if (std::isnan(result.properTimeLimit)) {
107  // first step is special: there is no previous state and we need to arm
108  // the decay simulation for all future steps.
109  result.particle =
110  makeParticle(initialParticle, state, stepper, navigator);
111  result.properTimeLimit =
112  decay.generateProperTimeLimit(*generator, initialParticle);
113  } else {
114  result.particle =
115  makeParticle(result.particle, state, stepper, navigator);
116  }
117 
118  // decay check. needs to happen at every step, not just on surfaces.
119  if (result.properTimeLimit - result.particle.properTime() <
121  auto descendants = decay.run(generator, result.particle);
122  for (auto &&descendant : descendants) {
123  result.generatedParticles.emplace_back(std::move(descendant));
124  }
125  result.isAlive = false;
126  return;
127  }
128 
129  // Regulate the step size
130  if (std::isfinite(result.properTimeLimit)) {
131  // beta² = p²/E²
132  // gamma = 1 / sqrt(1 - beta²) = sqrt(m² + p²) / m = E / m
133  // time = proper-time * gamma
134  // ds = beta * dt = (p/E) dt (E/m) = (p/m) proper-time
135  const auto properTimeDiff =
136  result.properTimeLimit - result.particle.properTime();
137  // Evaluate the step size for massive particle, assuming massless
138  // particles to be stable
139  const auto stepSize = properTimeDiff *
140  result.particle.absoluteMomentum() /
141  result.particle.mass();
142  stepper.setStepSize(state.stepping, stepSize,
144  }
145 
146  // arm the point-like interaction limits in the first step
147  if (std::isnan(result.x0Limit) or std::isnan(result.l0Limit)) {
149  }
150 
151  // If we are on target, everything should have been done
152  if (navigator.targetReached(state.navigation)) {
153  return;
154  }
155  // If we are not on a surface, there is nothing further for us to do
156  if (not navigator.currentSurface(state.navigation)) {
157  return;
158  }
159  const Acts::Surface &surface = *navigator.currentSurface(state.navigation);
160 
161  // we need the particle state before and after the interaction for the hit
162  // creation. create a copy since the particle will be modified in-place.
163  const Particle before = result.particle;
164 
165  // interactions only make sense if there is material to interact with.
166  if (surface.surfaceMaterial()) {
167  // TODO is this the right thing to do when globalToLocal fails?
168  // it should in principle never happen, so probably it would be best
169  // to change to a model using transform() directly
170  auto lpResult = surface.globalToLocal(state.geoContext, before.position(),
171  before.direction());
172  if (lpResult.ok()) {
173  Acts::Vector2 local = lpResult.value();
174  Acts::MaterialSlab slab =
175  surface.surfaceMaterial()->materialSlab(local);
176  // again: interact only if there is valid material to interact with
177  if (slab) {
178  // adapt material for non-zero incidence
179  auto normal = surface.normal(state.geoContext, local);
180  // dot-product(unit normal, direction) = cos(incidence angle)
181  // particle direction is normalized, not sure about surface normal
182  auto cosIncidenceInv = normal.norm() / normal.dot(before.direction());
183  // apply abs in case `normal` and `before` produce an angle > 90°
184  slab.scaleThickness(std::abs(cosIncidenceInv));
185  // run the interaction simulation
186  interact(slab, result); // MARK: fpeMask(FLTUND, 1, #2346)
187  }
188  }
189  }
190  const Particle &after = result.particle;
191 
192  // store results of this interaction step, including potential hits
193  if (selectHitSurface(surface)) {
194  result.hits.emplace_back(
195  surface.geometryId(), before.particleId(),
196  // the interaction could potentially modify the particle position
197  Hit::Scalar(0.5) * (before.fourPosition() + after.fourPosition()),
198  before.fourMomentum(), after.fourMomentum(), result.hits.size());
199  }
200 
201  if (after.absoluteMomentum() == 0.0) {
202  result.isAlive = false;
203  return;
204  }
205 
206  // continue the propagation with the modified parameters
207  stepper.update(state.stepping, after.position(), after.direction(),
208  after.qOverP(), after.time());
209  }
210 
212  template <typename propagator_state_t, typename stepper_t,
213  typename navigator_t>
214  Particle makeParticle(const Particle &previous, propagator_state_t &state,
215  stepper_t &stepper, navigator_t &navigator) const {
216  // a particle can lose energy and thus its gamma factor is not a constant
217  // of motion. since the stepper provides only the lab time, we need to
218  // compute the change in proper time for each step separately. this assumes
219  // that the gamma factor is constant over one stepper step.
220  const auto deltaLabTime = stepper.time(state.stepping) - previous.time();
221  // proper-time = time / gamma = (1/gamma) * time
222  // beta² = p²/E²
223  // gamma = 1 / sqrt(1 - beta²) = sqrt(m² + p²) / m
224  // 1/gamma = m / sqrt(m² + p²) = m / E
225  const auto gammaInv = previous.mass() / previous.energy();
226  const auto properTime = previous.properTime() + gammaInv * deltaLabTime;
227  const Acts::Surface *currentSurface = nullptr;
228  if (navigator.currentSurface(state.navigation) != nullptr) {
229  currentSurface = navigator.currentSurface(state.navigation);
230  }
231  // copy all properties and update kinematic state from stepper
232  return Particle(previous)
233  .setPosition4(stepper.position(state.stepping),
234  stepper.time(state.stepping))
235  .setDirection(stepper.direction(state.stepping))
236  .setAbsoluteMomentum(stepper.absoluteMomentum(state.stepping))
237  .setProperTime(properTime)
238  .setReferenceSurface(currentSurface);
239  }
240 
243  result_type &result) const {
244  auto selection = interactions.armPointLike(*generator, particle);
245  result.x0Limit = selection.x0Limit;
246  result.l0Limit = selection.l0Limit;
247  result.x0Process = selection.x0Process;
248  result.l0Process = selection.l0Process;
249  }
250 
255  void interact(const Acts::MaterialSlab &slab, result_type &result) const {
256  // run the continuous processes over a fraction of the material. returns
257  // true on break condition (same as the underlying physics lists).
258  auto runContinuousPartial = [&, this](float fraction) {
259  Acts::MaterialSlab partialSlab = slab;
260  partialSlab.scaleThickness(fraction);
261  // material after passing this slab
262  const auto x0 = result.particle.pathInX0() + partialSlab.thicknessInX0();
263  const auto l0 = result.particle.pathInX0() + partialSlab.thicknessInL0();
264  bool retval = false;
265  if (interactions.runContinuous(*(this->generator), partialSlab,
266  result.particle,
267  result.generatedParticles)) {
268  result.isAlive = false;
269  retval = true;
270  }
271  // the SimulationActor is in charge of keeping track of the material.
272  // since the accumulated material is stored in the particle it could (but
273  // should not) be modified by a physics process. to avoid issues, the
274  // material is updated only after process simulation has occured. this
275  // intentionally overwrites any material updates made by the process.
276  result.particle.setMaterialPassed(x0, l0);
277  return retval;
278  };
279 
280  // material thickness measured in radiation/interaction lengths
281  const auto slabX0 = slab.thicknessInX0();
282  const auto slabL0 = slab.thicknessInL0();
283  // remaining radiation/interaction length to next point-like interaction
284  // NOTE for limit=inf this should result in dist=inf
285  const auto x0Dist = result.x0Limit - result.particle.pathInX0();
286  const auto l0Dist = result.l0Limit - result.particle.pathInL0();
287 
288  // something point-like could happen within this material and we need to
289  // select which process would come first. x0/l0 measures the propagated path
290  // along different scales. to be able to check which one would happen first
291  // they need to be translated to a common scale.
292 
293  // relative fraction within material where the interaction occurs.
294  //
295  // fraction < 0:
296  // this is an error case where the point-like interaction should have
297  // occured before reaching the material. not sure how this could happen,
298  // but in such a case the point-like interaction happens immediately.
299  // 1 < fraction:
300  // the next point-like interaction does not occur within the current
301  // material. simulation is limited to the continuous processes.
302  //
303  // `clamp` ensures a valid range in all cases.
304  const float fracX0 = std::clamp(float(x0Dist / slabX0), 0.0f, 1.0f);
305  const float fracL0 = std::clamp(float(l0Dist / slabL0), 0.0f, 1.0f);
306  // fraction of the material where the first point-like interaction occurs
307  const float frac = std::min(fracX0, fracL0);
308 
309  // do not run if there is zero material before the point-like interaction
310  if (0.0f < frac) {
311  // simulate continuous processes before the point-like interaction
312  if (runContinuousPartial(frac)) {
313  return;
314  }
315  }
316  // do not run if there is no point-like interaction
317  if (frac < 1.0f) {
318  // select which process to simulate
319  const size_t process =
320  (fracX0 < fracL0) ? result.x0Process : result.l0Process;
321  // simulate the selected point-like process
322  if (interactions.runPointLike(*generator, process, result.particle,
323  result.generatedParticles)) {
324  result.isAlive = false;
325  return;
326  }
327  // simulate continuous processes after the point-like interaction
328  if (runContinuousPartial(1.0 - frac)) {
329  return;
330  }
331 
332  // particle is still alive and point-like interactions can occur again.
333  // in principle, the re-arming should occur directly after the point-like
334  // process. this could lead to a situation where the next interaction
335  // should already occur within the same material slab. thus, re-arming is
336  // done after all processes are simulated to enforce the
337  // one-interaction-per-slab rule.
338  armPointLikeInteractions(result.particle, result);
339  }
340  }
341 };
342 
343 } // namespace detail
344 } // namespace ActsFatras