Source code for gtsimulation._simulator

import datetime
import json
import logging
import math
import os
import sys
from abc import ABC, abstractmethod
from timeit import default_timer as timer

import numpy as np
from numba import njit

from gtsimulation import functions
from gtsimulation.electric_field import GeneralFieldE
from gtsimulation.common import Constants, Units, Regions, BreakCode, BreakIndex, SaveCode, SaveDef, BreakDef, vecRotMat
from gtsimulation.interaction import NuclearInteraction, G4Decay, SynchCounter, RadLossStep
from gtsimulation.magnetic_field import AbsBfield
from gtsimulation.magnetic_field.magnetosphere import Functions, Additions
from gtsimulation.medium import GTGeneralMedium
from gtsimulation.particle import ConvertT2R, GetAntiParticle, Flux
from gtsimulation.particle.generator import distribution, spectrum


[docs] class GTSimulator(ABC): """ Main simulation class for particle trajectory calculation in various space environments. This abstract base class provides the core functionality for simulating particle trajectories, taking into account electromagnetic fields, particle interactions, and various physical processes. Parameters ---------- Particles : :py:class:`~gtsimulation.particle.Flux` Particle flux generator defining initial spectrum, distribution and composition. See :py:mod:`gtsimulation.particle`. Num : int Maximum number of simulation steps. Step : float or dict Time step configuration. If float: fixed time step in seconds. If dict: adaptive step configuration with keys: * ``UseAdaptiveStep``: bool – Enable adaptive stepping (default: False). * ``InitialStep``: float – Initial time step in seconds (used only if adaptive). * ``MinLarmorRad``: int – Minimum number of Larmor radii per step. * ``MaxLarmorRad``: int – The minimal number of points per Larmor radius. * ``LarmorRad``: int – The fixed number of points per Larmor radius (used when a fixed resolution is desired). Bfield : :py:class:`~gtsimulation.magnetic_field.AbsBfield` or None, optional Magnetic field model object. If None, no magnetic field is applied. Default is None. Efield : :py:class:`~gtsimulation.electric_field.GeneralFieldE` or None, optional Electric field model object. If None, no electric field is applied. Default is None. Medium : :py:class:`~gtsimulation.medium.GTGeneralMedium` or None, optional Propagation medium for particles. Defines density and composition of the environment through which particles travel. Required when nuclear interactions are enabled (via `InteractNUC` parameter), but optional for simulations without interactions. See :py:mod:`gtsimulation.medium`. Default is None. Region : :py:class:`~gtsimulation.common.regions.Regions`, optional Simulation region specifying the physical environment. Available options: :py:attr:`~gtsimulation.common.regions.Regions.Magnetosphere`, :py:attr:`~gtsimulation.common.regions.Regions.Heliosphere`, :py:attr:`~gtsimulation.common.regions.Regions.Galaxy`. See :py:mod:`gtsimulation.common.regions`. Default is `Regions.Undefined`. BreakCondition : dict or list or None, optional Simulation termination conditions. If dict: {condition: value}. If list format: [conditions_dict, center_point_array]. Available conditions: see :py:data:`~gtsimulation.common.codes.BreakCode`. Default is None. UseDecay : bool, optional Enable particle decay processes. Default is False. InteractNUC : :py:class:`~gtsimulation.interaction.NuclearInteraction` or None, optional Nuclear interaction module. Requires Medium to be set. Default is None. RadLosses : bool or list, optional Radiation losses configuration. If False, radiation losses are disabled. If list format: [True, {"Photons": True/False, "MinE": float, "MaxE": float}]. Default is False. Date : datetime.datetime, optional Date for field model initialization. Default is datetime.datetime(2008, 1, 1). Save : int or list, optional Save interval configuration. If int N, save every N steps. If list format: [N, {"Clock": True, ...}] to save additional parameters. See :py:data:`~gtsimulation.common.codes.SaveCode`. Default is 1. Nfiles : int or list, optional Number of output files. If int: create N files. If list: create files with specified indices. Default is 1. Output : str or None, optional Output file base name. If None, results are not saved to disk. Default is None. Verbose : int, optional Verbosity level: 0 (no output), 1 (short output), 2 (verbose output). Default is 1. ForwardTrck : {1, -1} or None, optional Tracing direction: 1 for forward, -1 for backward. If None, determined from Particles. Default is None. TrackParams : bool or dict, optional Additional parameter tracking. If True, all available parameters are tracked. If dict, specify which parameters to track: {'Invariants': True, 'GuidingCenter': True, etc.}. See :py:attr:`~gtsimulation.common.regions._AbsRegion.SaveAdd`. Default is False. ParticleOrigin : bool, optional Enable particle origin calculation through backtracing. Default is False. IsFirstRun : bool, optional Flag indicating first simulation run. Affects logging and some calculations. Default is True. Returns ------- result : list or None Results of simulation. * If `Output` is None: Returns a list of simulation results. The structure depends on `Nfiles`: - `Nfiles = 1` — a flat list of dictionaries, one per simulated particle (the number of particles is determined by the `Particles` argument). - `Nfiles > 1` — a list of lists. Each inner list corresponds to one output file and contains dictionaries for the particles processed in that file. * If `Output` is not None: Results are saved to disk (NumPy binary files) and the method returns `None`. Notes ----- For detailed information about the structure of the particle data dictionaries, see the **Simulation output** section in the online documentation. See Also -------- :py:class:`gtsimulation.particle.Flux` : Particle flux generation :py:data:`gtsimulation.common.codes.SaveCode` : Available save parameters :py:data:`gtsimulation.common.codes.BreakCode` : Available break conditions :py:class:`gtsimulation.magnetic_field` : Magnetic field module :py:class:`gtsimulation.medium` : Medium module """ def __init__( self, Particles: Flux, Num: int, Step: float, Bfield: AbsBfield | None = None, Efield: GeneralFieldE | None = None, Medium: GTGeneralMedium | None = None, Region: Regions = Regions.Undefined, BreakCondition: dict | list | None = None, UseDecay: bool = False, InteractNUC: NuclearInteraction | None = None, RadLosses: bool | list = False, Date: datetime.datetime = datetime.datetime(2008, 1, 1), Save: int | list = 1, Nfiles: int | list = 1, Output: str | None = None, Verbose: int = 1, ForwardTrck=None, TrackParams=False, ParticleOrigin=False, IsFirstRun=True, ): self.ParamDict = locals().copy() del self.ParamDict['self'] self.logger = logging.getLogger(__name__) if Verbose < 1: self.logger.setLevel(logging.WARNING) elif Verbose == 1: self.logger.setLevel(logging.INFO) else: self.logger.setLevel(logging.DEBUG) if Verbose > 0 and not self.logger.hasHandlers(): h = logging.StreamHandler(stream=sys.stdout) h.setLevel(self.logger.level) h.setFormatter(logging.Formatter("%(message)s")) self.logger.addHandler(h) self.logger.propagate = False self.logger.debug("Creating simulator object...") self.Date = Date self.logger.debug("Date: %s", self.Date) self.StepParams = Step self.Step = None self.UseAdaptiveStep = False self.__SetStep(Step) self.Num = int(Num) self.logger.debug("Number of steps: %d", self.Num) self.__SetUseRadLosses(RadLosses) self.Region = Region self.logger.debug("Region: %s", self.Region.name) self.logger.debug("%s", self.Region.value.ret_str()) self.ParticleOrigin = ParticleOrigin self.ParticleOriginIsOn = bool(self.ParticleOrigin) if self.ParticleOrigin: TrackParams = True self.TrackParamsIsOn = False self.TrackParams = self.Region.value.SaveAdd self.__SetAdditions(TrackParams, Save) self.IsFirstRun = IsFirstRun self.Nfiles = 1 if Nfiles is None or Nfiles == 0 else Nfiles self.Output = Output self.Npts = 2 self.Save = SaveDef.copy() self.SaveCode = dict([(key, SaveCode[key][1]) for key in SaveCode.keys()]) self.SaveColumnLen = 22 self.logger.debug("Number of files: %s", self.Nfiles) self.logger.debug("Output file name: %s_num.npy", self.Output) if BreakCondition is not None and hasattr(BreakCondition, 'keys') and 'MaxRev' in BreakCondition.keys(): if not isinstance(Save, list): Save = [Save, {'GuidingCenter': True, 'PitchAngles': True}] else: Save[1] = Save[1] | {'GuidingCenter': True, 'PitchAngles': True} self.__SetSave(Save) self.Efield = Efield self.logger.debug("Electric field: %s", self.Efield) self.Bfield = Bfield self.logger.debug("Magnetic field: %s", self.Bfield) self.Medium = Medium self.logger.debug("Medium: %s", self.Medium) self.UseDecay = UseDecay self.logger.debug("Decay: %s", self.UseDecay) if self.Medium is None and InteractNUC is not None: raise ValueError('Nuclear interaction is enabled but Medium is not set') self.nuclear_interaction = InteractNUC self.logger.debug("Nuclear Interactions: %s", self.nuclear_interaction) self.__gen = 1 # self.UseDecay = False # self.nuclear_interaction = None # self.__set_nuclear_interaction(UseDecay, InteractNUC) self.Particles = None self.ForwardTracing = 1 self.__SetFlux(Particles, ForwardTrck) self.__brck_index = BreakCode.copy() self.__brck_index.pop("Loop") self.__index_brck = BreakIndex.copy() self.__brck_arr = BreakDef.copy() self.__set_break_condition(BreakCondition) self.index = 0 self.logger.debug("Simulator object created!\n") def __SetStep(self, Step): if isinstance(Step, (int, float)): self.Step = Step self.logger.debug("Time step: %s seconds", self.Step) elif isinstance(Step, dict): self.UseAdaptiveStep = Step.get("UseAdaptiveStep", False) self.Step = Step.get("InitialStep", 1) self.time_step_max = Step.get("MaxTimeStep", np.inf) self.logger.debug("Using adaptive time step: %s", self.UseAdaptiveStep) self.logger.debug("Initial time step: %f seconds", self.Step) N = Step.get("LarmorRad", None) if N is not None: self.N1 = self.N2 = N self.logger.debug("Steps per Larmor radius: %d", N) else: self.N1 = Step.get("MinLarmorRad", 600) self.N2 = Step.get("MaxLarmorRad", 600) self.logger.debug("Min steps per Larmor radius: %d", self.N1) self.logger.debug("Max steps per Larmor radius: %d", self.N2) assert isinstance(self.UseAdaptiveStep, bool) assert isinstance(self.Step, (int, float)) assert isinstance(self.N1, int) and isinstance(self.N2, int) assert self.N1 <= self.N2 else: raise Exception("Step should be numeric or dict") def __SetUseRadLosses(self, RadLosses): if isinstance(RadLosses, bool): self.UseRadLosses = [RadLosses, False] if isinstance(RadLosses, list) and (RadLosses[0] == True) and (RadLosses[1]["Photons"] == False): self.UseRadLosses = [True, False] if isinstance(RadLosses, list) and (RadLosses[0] == True) and (RadLosses[1]["Photons"] == True): MinMax = np.array([0, np.inf]) if "MinE" in RadLosses[1]: if RadLosses[1]["MinE"] > 0: MinMax[0] = RadLosses[1]["MinE"] if "MaxE" in RadLosses[1]: if RadLosses[1]["MaxE"] > 0: MinMax[1] = RadLosses[1]["MaxE"] self.UseRadLosses = [True, True, MinMax] self.logger.debug("Radiation Losses: %s", self.UseRadLosses[0]) self.logger.debug("Synchrotron Emission: %s", self.UseRadLosses[1]) def __SetAdditions(self, TrackParams, Save): # Change save settings due to dependencies if isinstance(TrackParams, dict): if "GuidingCenter" in TrackParams.keys() and TrackParams["GuidingCenter"]: TrackParams["PitchAngles"] = True if not isinstance(Save, list): Save = [Save, {"GuidingCenter": True, "PitchAngles": True}] else: Save[1] = Save[1] | {"GuidingCenter": True, "PitchAngles": True} if "Lshell" in TrackParams.keys() and TrackParams["Lshell"]: TrackParams["Invariants"] = True if "Invariants" in TrackParams.keys() and TrackParams["Invariants"]: TrackParams["MirrorPoints"] = True if "MirrorPoints" in TrackParams.keys() and TrackParams["MirrorPoints"]: TrackParams["PitchAngles"] = True if not isinstance(Save, list): Save = [Save, {"PitchAngles": True}] else: Save[1] = Save[1] | {"PitchAngles": True} if isinstance(TrackParams, bool): self.TrackParamsIsOn = TrackParams if self.TrackParamsIsOn: self.TrackParams.update((key, True) for key in self.TrackParams) elif isinstance(TrackParams, dict): self.TrackParamsIsOn = True for add in TrackParams.keys(): assert add in self.TrackParams.keys(), f'No such option as "{add}" is allowed' self.TrackParams[add] = TrackParams[add] if self.TrackParamsIsOn: if not isinstance(Save, list): Save = [Save, {"Bfield": True}] else: Save[1] = Save[1] | {"Bfield": True} # def __set_nuclear_interaction(self, UseDecay, UseInteractNUC): # self.UseDecay = UseDecay # if self.Medium is None and UseInteractNUC is not None: # raise ValueError('Nuclear interaction is enabled but Medium is not set') # self.nuclear_interaction = UseInteractNUC # if self.nuclear_interaction is not None and 'l' in self.nuclear_interaction.get("ExcludeParticleList", []): # self.nuclear_interaction['ExcludeParticleList'].extend([11, 12, 13, 14, 15, 16, 17, 18, # -11, -12, -13, -14, -15, -16, -17, -18]) # self.logger.debug("Decay: %s", self.UseDecay) # self.logger.debug("Nuclear Interactions: %s", self.nuclear_interaction) def __set_break_condition(self, Brck): center = np.array([0, 0, 0]) if Brck is not None: if isinstance(Brck, list): center = Brck[1] assert isinstance(center, np.ndarray) and center.shape == (3,) Brck = Brck[0] assert isinstance(Brck, dict) for key in Brck.keys(): self.__brck_arr[self.__brck_index[key]] = Brck[key] self.logger.debug("Break Conditions:") for key in self.__brck_index.keys(): self.logger.debug("\t%s: %s", key, self.__brck_arr[self.__brck_index[key]]) self.logger.debug("BC center: %s", center) self.BCcenter = center def __SetFlux(self, flux, forward_trck): assert flux is not None self.Particles = flux self.logger.debug("Flux: %s", self.Particles) if forward_trck is not None: self.ForwardTracing = forward_trck return self.ForwardTracing = self.Particles.Mode.value self.logger.debug("Tracing: %s", "Inward" if self.ForwardTracing == 1 else "Outward") def __SetSave(self, Save): Nsave = Save if not isinstance(Save, list) else Save[0] self.Region.value.checkSave(self, Nsave) self.Npts = math.ceil(self.Num / Nsave) if Nsave != 0 else 1 self.Nsave = Nsave self.logger.debug("Save every %s step of:", self.Nsave) if isinstance(Save, list): for saves in Save[1].keys(): self.Save[saves] = Save[1][saves] sorted_keys = sorted(SaveCode, key = lambda x: SaveCode[x][0]) for i, key in enumerate(sorted_keys): if not self.Save[key]: val = SaveCode[key][1] num = 0 self.SaveCode[key] = None if isinstance(val, int): num = 1 elif isinstance(val, slice): num = val.stop - val.start self.SaveColumnLen -= num for j in range(i + 1, len(sorted_keys)): val_ = self.SaveCode[sorted_keys[j]] if isinstance(val_, int): val_ -= num elif isinstance(val_, slice): val_ = np.s_[val_.start - num:val_.stop - num:1] self.SaveCode[sorted_keys[j]] = val_ for saves in self.Save.keys(): self.logger.debug("\t%s: %s", saves, self.Save[saves]) def __call__(self): Track = [] self.logger.debug("Launching simulation...\n") file_nums = np.arange(self.Nfiles) if isinstance(self.Nfiles, int) else self.Nfiles for (idx, i) in enumerate(file_nums): if self.IsFirstRun: self.logger.info("File %d/%d started", idx + 1, len(file_nums)) self.logger.debug("") if self.Output is not None: file = self.Output.split(os.sep) folder = os.sep.join(file[:-1]) if len(file) != 1 and not os.path.isdir(folder): os.mkdir(folder) def custom_serializer(obj): if isinstance(obj, (AbsBfield, GTGeneralMedium)): lines = [el.strip() for el in str(obj).strip().split('\n')] return [lines[0], dict([el.split(': ') for el in lines[1:]])] if isinstance(obj, Flux): return dict([el.strip().split(': ') for el in str(obj).strip().split('\n')]) if isinstance(obj, Regions): return str(obj) if isinstance(obj, datetime.datetime): return obj.isoformat() raise TypeError(f"Type {type(obj)} not serializable") def custom_serializer_safe(obj): return str(obj) with open(f'{self.Output}_params.json', 'w') as file: try: json.dump(self.ParamDict, file, default=custom_serializer, indent=4) except: json.dump(self.ParamDict, file, default=custom_serializer_safe, indent=4) RetArr = self.CallOneFile() if self.Output is not None: if self.Nfiles == 1: np.save(f"{self.Output}.npy", RetArr) else: np.save(f"{self.Output}_{i}.npy", RetArr) self.logger.info("File %d/%d saved\n", idx + 1, len(file_nums)) RetArr.clear() else: Track.append(RetArr) self.logger.info("Simulation completed!") if self.Output is None: return Track
[docs] def CallOneFile(self): self.Particles.generate() RetArr = [] SaveR = self.Save["Coordinates"] SaveV = self.Save["Velocities"] SaveE = self.Save["Efield"] SaveB = self.Save["Bfield"] SaveA = self.Save["Angles"] SaveP = self.Save["Path"] SaveD = self.Save["Density"] SaveC = self.Save["Clock"] SaveT = self.Save["Energy"] SavePA = self.Save["PitchAngles"] SaveLR = self.Save["LarmorRadii"] SaveGC = self.Save["GuidingCenter"] Gen = self.__gen GenMax = 1 if self.nuclear_interaction is None else self.nuclear_interaction.max_generations UseAdditionalEnergyLosses = self.Region.value.CalcAdditional() n_events = len(self.Particles) progress_step = self.Num // 10 for self.index in range(n_events): self.logger.debug("Event %d/%d started", self.index + 1, n_events) TotTime, TotPathLen, TotPathDen = 0, 0, 0 if self.Medium is not None and self.nuclear_interaction is not None: local_den, n_local, local_path_den = 0, 0, 0 local_chem_comp = np.zeros(len(self.Medium.get_element_list())) local_path_den_vector = [] local_coordinate = [] local_velocity = [] lon_total, lon_prev, full_revolutions = np.array([[0.]]), np.array([[0.]]), np.array([[0.]]) particle = self.Particles[self.index] Saves = [] BrckArr = self.__brck_arr BCcenter = self.BCcenter tau = particle.tau rnd_dec = 0 self.IsPrimDeath = False prod_tracks = [] if self.UseDecay: rnd_dec = np.random.rand() self.logger.debug("Use Decay: %s", self.UseDecay) self.logger.debug("Decay rnd: %f", rnd_dec) if self.ForwardTracing == -1: self.logger.debug("Backtracing mode is ON") self.logger.debug("Redefinition of particle to antiparticle") GetAntiParticle(particle) particle.velocities = -particle.velocities Q = particle.Z * Constants.e M = particle.M m = M*Units.MeV2kg T = particle.T V_normalized = np.array(particle.velocities) # unit vector of velocity (beta vector) V_norm = Constants.c * np.sqrt(particle.E ** 2 - M ** 2) / particle.E # scalar speed [m/s] Vm = V_norm * V_normalized # vector of velocity [m/s] r0 = np.array(particle.coordinates) r = np.array(particle.coordinates) r_old = r B = np.array(self.Bfield.GetBfield(*r)) if self.Bfield is not None else np.zeros(3) E = np.array(self.Efield.GetEfield(*r)) if self.Efield is not None else np.zeros(3) Step = self.Step if Q == 0: Step *= 1e2 self.logger.debug( "Particle: %s (M = %f [MeV/c2], Z = %d)", particle.Name, M, self.Particles[self.index].Z ) self.logger.debug( "Energy: %f [MeV], Rigidity: %f [GV]", T, ConvertT2R(T, M, particle.A, particle.Z) / 1000 if particle.Z != 0 else np.inf ) self.logger.debug("Coordinates: %s [m]", r) self.logger.debug("Velocity: %s", V_normalized) self.logger.debug("Beta: %s", V_norm / Constants.c) self.logger.debug("Beta * dt: %f [m]", V_norm * Step) # Calculation of EAS for magnetosphere self.Region.value.do_before_loop(self, Gen, prod_tracks) brk = BreakCode["Loop"] Num = self.Num Nsave = self.Nsave if self.Nsave != 0 else Num + 1 i_save = 0 st = timer() if self.UseRadLosses[1]: synch_record = SynchCounter() else: synch_record = 0 self.logger.debug("Calculating:") PitchAngle = None LarmorRadius = None GuidingCenter = None for i in range(Num): if SavePA: PitchAngle = functions.CalcPitchAngles(B, Vm) if SaveLR: LarmorRadius = functions.CalcLarmorRadii(np.linalg.norm(B), T, PitchAngle, M, particle.Z) if SaveGC: GuidingCenter = functions.CalcGuidingCenter(r, Vm, B, T, PitchAngle, M, particle.Z) if i % Nsave == 0 or i == Num - 1 or i_save == 0: self._save_step( r_old, V_norm, TotPathLen, TotPathDen, TotTime, Vm, r, T, E, B, PitchAngle, LarmorRadius, GuidingCenter, Saves, self.SaveColumnLen, self.SaveCode["Coordinates"], self.SaveCode["Velocities"], self.SaveCode["Efield"], self.SaveCode["Bfield"], self.SaveCode["Angles"], self.SaveCode["Path"], self.SaveCode["Density"], self.SaveCode["Clock"], self.SaveCode["Energy"], self.SaveCode["PitchAngles"], self.SaveCode["LarmorRadii"], self.SaveCode["GuidingCenter"], SaveR, SaveV, SaveE, SaveB, SaveA, SaveP, SaveD, SaveC, SaveT, SavePA, SaveLR, SaveGC ) i_save += 1 if self.UseAdaptiveStep: Step = self._adaptive_step(Q, m, B, Vm, T, M, Step, self.N1, self.N2, self.time_step_max) if i == 0: self.Step = Step PathLen = V_norm * Step r_old = r r, Vp, Yp, Ya = self.AlgoStep(T, M, Q, Vm, r, B, E) V_norm, TotPathLen, TotTime = self._update(PathLen, Step, TotPathLen, TotTime, Vm) if self.UseRadLosses[1]: synch_record.add_iteration(T, B, Vm, Step) if self.UseRadLosses[0]: Vm, T, new_photons, synch_record = RadLossStep.MakeRadLossStep( Vp, Vm, Yp, Ya, M, Q, r, Step, self.ForwardTracing, self.UseRadLosses[1:], particle, Gen, Constants, synch_record ) prod_tracks.extend(new_photons) elif M > 0: T = M * (Yp - 1) Vm = Vp if UseAdditionalEnergyLosses: Vm, T = self.Region.value.AdditionalEnergyLosses(r, Vm, T, M, Step, self.ForwardTracing, Constants.c) # Medium if self.Medium is not None: self.Medium.calculate_model(*r) Den = self.Medium.get_density() # kg/m3 PathDen = (Den * 1e-3) * (PathLen * 1e2) # g/cm2 TotPathDen += PathDen # g/cm2 if self.nuclear_interaction is not None and Den > 0: local_den += Den local_chem_comp += self.Medium.get_element_abundance() n_local += 1 local_path_den += PathDen local_path_den_vector.append(local_path_den) local_coordinate.append(r) local_velocity.append(Vm) # Decay if self.UseDecay and not self.IsPrimDeath: lifetime = tau * (T / M + 1) if M > 0 else np.inf if rnd_dec > np.exp(-TotTime / lifetime): self.__Decay(Gen, GenMax, T, TotTime, V_norm, Vm, particle, prod_tracks, r) self.IsPrimDeath = True # Nuclear interaction check_interaction = ( self.nuclear_interaction is not None and local_path_den > self.nuclear_interaction.grammage_threshold and not self.IsPrimDeath ) if check_interaction: # Construct Rotation Matrix & Save velocity before possible interaction rotationMatrix = vecRotMat(np.array([0, 0, 1]), Vm / V_norm) primary, secondary = self.nuclear_interaction.run_matter_layer( pdg=particle.PDG, energy=T, mass=local_path_den, density=(local_den * 1e-3) / n_local, element_name=self.Medium.get_element_list(), element_abundance=local_chem_comp / n_local ) T = primary['KineticEnergy'] if T > 0 and T > 1: # Cut particles with T < 1 MeV # Only ionization losses V_norm = Constants.c * np.sqrt(1 - (M / (T + M)) ** 2) Vm = V_norm * rotationMatrix @ primary['MomentumDirection'] local_den, n_local, local_path_den = 0, 0, 0 local_chem_comp = np.zeros(len(self.Medium.get_element_list())) local_path_den_vector.clear() local_coordinate.clear() local_velocity.clear() else: # Death due to ionization losses or nuclear interaction self.IsPrimDeath = True if secondary.size > 0 and Gen < GenMax: self.logger.debug( "Nuclear interaction %s: %d secondaries, total energy %f MeV", primary["LastProcess"], secondary.size, np.sum(secondary["KineticEnergy"]), ) self.logger.debug("%s", secondary) # Coordinates of interaction point in XYZ local_path_den_vector = np.array(local_path_den_vector) path_den_cylinder = (np.linalg.norm(primary['Position']) * 1e2) * (local_den * 1e-3 / n_local) # Path in cylinder [g/cm2] r_interaction = np.array(local_coordinate)[np.argmax(local_path_den_vector > path_den_cylinder), :] v_interaction = np.array(local_velocity)[np.argmax(local_path_den_vector > path_den_cylinder), :] rotationMatrix = vecRotMat(np.array([0, 0, 1]), v_interaction / np.linalg.norm(v_interaction)) for p in secondary: V_p = rotationMatrix @ p['MomentumDirection'] T_p = p['KineticEnergy'] PDGcode_p = p["PDGcode"] # Parameters for recursive call of GT params = self.ParamDict.copy() params["Date"] += datetime.timedelta(seconds=TotTime) params["Particles"] = Flux( Distribution=distribution.UserInput(R0=r_interaction, V0=V_p), Spectrum=spectrum.UserInput(energy=T_p), PDGcode=PDGcode_p ) # if (PDGcode_p in self.nuclear_interaction.get("ExcludeParticleList", []) # or T_p < self.nuclear_interaction.get("Emin", 0)): # params["Num"] = 1 # params["UseDecay"] = False # params["InteractNUC"] = None if PDGcode_p in [12, 14, 16, 18, -12, -14, -16, -18]: params["Medium"] = None params["InteractNUC"] = None new_process = self.__class__(**params) new_process.__gen = Gen + 1 prod_tracks.append(new_process.CallOneFile()[0]) B = np.array(self.Bfield.GetBfield(*r)) if self.Bfield is not None else np.zeros(3) E = np.array(self.Efield.GetEfield(*r)) if self.Efield is not None else np.zeros(3) # TODO the code is region specific # Full revolution if self.Region == Regions.Magnetosphere: if self.ParticleOriginIsOn or self.__brck_arr[self.__brck_index["MaxRev"]] != BreakDef[-1]: a_, b_, _ = Functions.transformations.geo2mag_eccentric(GuidingCenter[0][0], GuidingCenter[0][1], GuidingCenter[0][2], 1, self.Bfield.g, self.Bfield.h) lon_total, lon_prev, full_revolutions = Additions.AddLon(lon_total, lon_prev, full_revolutions, i, a_, b_) brck = self._check_break(r, r0, BCcenter, TotPathLen, TotTime, full_revolutions, BrckArr) brk = brck[1] if brck[0] or self.IsPrimDeath: if SavePA: PitchAngle = functions.CalcPitchAngles(B, Vm) if SaveLR: LarmorRadius = functions.CalcLarmorRadii(np.linalg.norm(B), T, PitchAngle, M, particle.Z) if SaveGC: GuidingCenter = functions.CalcGuidingCenter(r, Vm, B, T, PitchAngle, M, particle.Z) if brk != -1: self._save_step( r_old, V_norm, TotPathLen, TotPathDen, TotTime, Vm, r, T, E, B, PitchAngle, LarmorRadius, GuidingCenter, Saves, self.SaveColumnLen, self.SaveCode["Coordinates"], self.SaveCode["Velocities"], self.SaveCode["Efield"], self.SaveCode["Bfield"], self.SaveCode["Angles"], self.SaveCode["Path"], self.SaveCode["Density"], self.SaveCode["Clock"], self.SaveCode["Energy"], self.SaveCode["PitchAngles"], self.SaveCode["LarmorRadii"], self.SaveCode["GuidingCenter"], SaveR, SaveV, SaveE, SaveB, SaveA, SaveP, SaveD, SaveC, SaveT, SavePA, SaveLR, SaveGC ) i_save += 1 if self.IsPrimDeath: brk = self.__brck_index["Death"] self.logger.debug("### Break due to %s ###", self.__index_brck[brk]) break if i % progress_step == 0: self.logger.debug("\tProgress: %d%%", int(i / self.Num * 100)) self.logger.debug("\tProgress: 100%") if self.IsFirstRun: self.logger.info( "Event %d/%d finished in %.3f seconds", self.index + 1, n_events, timer() - st, ) self.logger.debug("") Saves = np.array(Saves) track = {} if SaveR: track['Coordinates'] = Saves[:, self.SaveCode["Coordinates"]] if SaveV: track["Velocities"] = Saves[:, self.SaveCode["Velocities"]] if SaveE: track["Efield"] = Saves[:, self.SaveCode["Efield"]] if SaveB: track["Bfield"] = Saves[:, self.SaveCode["Bfield"]] if SaveA: track["Angles"] = Saves[:, self.SaveCode["Angles"]] if SaveP: track["Path"] = Saves[:, self.SaveCode["Path"]] if SaveC: track["Clock"] = Saves[:, self.SaveCode["Clock"]] if SaveT: track["Energy"] = Saves[:, self.SaveCode["Energy"]] if SaveD: track["Density"] = Saves[:, self.SaveCode["Density"]] if SavePA: track["PitchAngles"] = Saves[:, self.SaveCode["PitchAngles"]] if SaveLR: track["LarmorRadii"] = Saves[:, self.SaveCode["LarmorRadii"]] if SaveGC: track["GuidingCenter"] = Saves[:, self.SaveCode["GuidingCenter"]] RetArr.append({"Track": track, "BC": {"WOut": brk}, "Particle": {"PDG": particle.PDG, "M": M, "Ze": particle.Z, "Gen": Gen, "R0": particle.coordinates, "V0": particle.velocities, "T0": particle.T}, "Child": prod_tracks}) # TODO refactor if self.Region == Regions.Magnetosphere: # Particles in magnetosphere (Part 1) if self.TrackParamsIsOn: self.logger.debug("Calculating additional parameters ...") TrackParams_i = Additions.GetTrackParams(self, RetArr[self.index]) if self.__brck_arr[self.__brck_index["MaxRev"]] != BreakDef[-1]: TrackParams_i["LonTotal"] = lon_total RetArr[self.index]["Additions"] = TrackParams_i # Particles in magnetosphere (Part 2) if self.ParticleOriginIsOn and self.IsFirstRun: self.logger.debug("Finding particle origin ...") origin = Additions.FindParticleOrigin(self, RetArr[self.index]) RetArr[self.index]["Additions"]["ParticleOrigin"] = origin self.logger.debug("Particle origin: %s", origin.name) return RetArr
def __Decay(self, Gen, GenMax, T, TotTime, V_norm, Vm, particle, prod_tracks, r): if Gen < GenMax: secondary = G4Decay(particle.PDG, T) rotationMatrix = vecRotMat(np.array([0, 0, 1]), Vm / V_norm) for p in secondary: V_p = rotationMatrix @ p['MomentumDirection'] r_p = r T_p = p['KineticEnergy'] PDGcode_p = p["PDGcode"] params = self.ParamDict.copy() params["Particles"] = Flux( Distribution=distribution.UserInput(R0=r_p, V0=V_p), Spectrum=spectrum.UserInput(energy=T_p), PDGcode=PDGcode_p ) params["Date"] = params["Date"] + datetime.timedelta(seconds=TotTime) if PDGcode_p in [12, 14, 16, 18, -12, -14, -16, -18]: params["Medium"] = None params["InteractNUC"] = None new_process = self.__class__(**params) new_process.__gen = Gen + 1 prod_tracks.append(new_process.CallOneFile()[0]) @staticmethod @njit(fastmath=True) def _check_break(r, r0, center, TotPath, TotTime, full_revolutions, Brck): radi = np.linalg.norm(r - center) dst2path = np.linalg.norm(r - r0) / TotPath cond = np.concatenate((np.array([*np.abs(r), radi, dst2path]) < Brck[:5], np.array([*np.abs(r), radi, TotPath, TotTime]) > Brck[5:-1], np.array([full_revolutions[0][0]]) >= Brck[-1])) if np.any(cond): return True, np.where(cond)[0][0] return False, -1 @staticmethod @njit(fastmath=True) def _update(PathLen, Step, TotPathLen, TotTime, Vm): V_norm = np.linalg.norm(Vm) TotTime += Step TotPathLen += PathLen return V_norm, TotPathLen, TotTime @staticmethod # @njit(fastmath=True) def _save_step(r_old, V_norm, TotPathLen, TotPathDen, TotTime, Vm, r, T, E, B, PitchAngles, LarmorRadii, GuidingCenter, Saves, ColLen, RCode, VCode, ECode, BCode, ACode, PCode, DCode, CCode, TCode, PACode, LRCode, GCCode, SaveR, SaveV, SaveE, SaveB, SaveA, SaveP, SaveD, SaveC, SaveT, SavePA, SaveLR, SaveGC): sv = np.zeros(ColLen) if SaveR: sv[RCode] = r if SaveV: sv[VCode] = Vm / V_norm if SaveE: sv[ECode] = E if SaveB: sv[BCode] = B if SaveA: sv[ACode] = np.arctan2(np.linalg.norm(np.cross(r_old, r)), np.dot(r, r_old)) if SaveP: sv[PCode] = TotPathLen if SaveD: sv[DCode] = TotPathDen if SaveC: sv[CCode] = TotTime if SaveT: sv[TCode] = T if SavePA: sv[PACode] = PitchAngles if SaveLR: sv[LRCode] = LarmorRadii if SaveGC: sv[GCCode] = GuidingCenter Saves.append(sv) @staticmethod @njit(fastmath=True) def _adaptive_step(q, m, B, V, T, M, dt, N1, N2, dt_max): Y = T / M + 1 B_n = np.linalg.norm(B) cos_theta = B @ V / (np.linalg.norm(V) * B_n) sin_theta = np.sqrt(1 - cos_theta ** 2) T = Y * m * sin_theta / (np.abs(q) * B_n) if N1 <= T / dt <= N2: return dt dt = T / np.sqrt(N1 * N2) if dt > dt_max: return dt_max return dt
[docs] @abstractmethod def AlgoStep(self, T, M, Q, Vm, r, H, E): pass