src/modules/DepositionGeant4/DepositionGeant4Module.cpp
Implementation of Geant4 deposition module. More…
Defines
Name | |
---|---|
G4_NUM_SEEDS |
Detailed Description
Implementation of Geant4 deposition module.
Copyright: Copyright (c) 2017-2024 CERN and the Allpix Squared authors. This software is distributed under the terms of the MIT License, copied verbatim in the file “LICENSE.md”. In applying this license, CERN does not waive the privileges and immunities granted to it by virtue of its status as an Intergovernmental Organization or submit itself to any jurisdiction. SPDX-License-Identifier: MIT
Remark: Based on code from Mathieu Benoit
Macros Documentation
define G4_NUM_SEEDS
#define G4_NUM_SEEDS 10
Source code
#include "DepositionGeant4Module.hpp"
#include <limits>
#include <string>
#include <utility>
#include <G4Box.hh>
#include <G4EmParameters.hh>
#include <G4HadronicParameters.hh>
#include <G4HadronicProcessStore.hh>
#include <G4LogicalVolume.hh>
#include <G4NuclearLevelData.hh>
#include <G4PhysListFactory.hh>
#include <G4ProcessTable.hh>
#include <G4RadioactiveDecayPhysics.hh>
#include <G4StepLimiterPhysics.hh>
#include <G4UImanager.hh>
#include <G4UserLimits.hh>
#include "G4FieldManager.hh"
#include "G4TransportationManager.hh"
#include "G4UniformMagField.hh"
#include "core/config/exceptions.h"
#include "core/geometry/GeometryManager.hpp"
#include "core/geometry/RadialStripDetectorModel.hpp"
#include "core/module/exceptions.h"
#include "core/utils/log.h"
#include "objects/DepositedCharge.hpp"
#include "physics/MaterialProperties.hpp"
#include "tools/ROOT.h"
#include "tools/geant4/MTRunManager.hpp"
#include "tools/geant4/RunManager.hpp"
#include "tools/geant4/geant4.h"
#include "AdditionalPhysicsLists.hpp"
#include "ActionInitializationG4.hpp"
#include "GeneratorActionG4.hpp"
#include "SDAndFieldConstruction.hpp"
#include "SensitiveDetectorActionG4.hpp"
#include "SetTrackInfoUserHookG4.hpp"
#define G4_NUM_SEEDS 10
using namespace allpix;
thread_local std::unique_ptr<TrackInfoManager> DepositionGeant4Module::track_info_manager_ = nullptr;
thread_local std::vector<SensitiveDetectorActionG4*> DepositionGeant4Module::sensors_;
DepositionGeant4Module::DepositionGeant4Module(Configuration& config, Messenger* messenger, GeometryManager* geo_manager)
: SequentialModule(config), messenger_(messenger), geo_manager_(geo_manager) {
// Enable multithreading of this module if multithreading is enabled
allow_multithreading();
// Waive any sequence requirement: base module not sequential, but derived modules might be
waive_sequence_requirement();
// Set default physics list
config_.setDefault("physics_list", "FTFP_BERT_LIV");
config_.setDefault("pai_model", "pai");
config_.setDefault("source_type", "beam");
config_.setDefault<bool>("output_plots", false);
config_.setDefault<int>("output_plots_scale", Units::get(100, "ke"));
config_.setDefault<double>("max_step_length", Units::get(1.0, "um"));
// Default value chosen to ensure proper gamma generation for Cs137 decay
config_.setDefault<double>("cutoff_time", 2.21e+11);
// By default, only record MCTracks connected to MCParticles in the sensitive volume
config_.setDefault<bool>("record_all_tracks", false);
// Defaults for energy deposition in implants
config_.setDefault<bool>("deposit_in_frontside_implants", true);
config_.setDefault<bool>("deposit_in_backside_implants", false);
// Create user limits for maximum step length and maximum event time in the sensor
user_limits_ =
std::make_unique<G4UserLimits>(config_.get<double>("max_step_length"), DBL_MAX, config_.get<double>("cutoff_time"));
// Create user limits for maximum event time in the world volume:
user_limits_world_ = std::make_unique<G4UserLimits>(DBL_MAX, DBL_MAX, config_.get<double>("cutoff_time"));
// If macro, parse for positions of sources and add these as points to the GeoManager to extend the world:
if(config.get<GeneratorActionG4::SourceType>("source_type") == GeneratorActionG4::SourceType::MACRO) {
std::ifstream file(config.getPath("file_name", true));
std::string line;
while(std::getline(file, line)) {
if(line.rfind("/gps/position", 0) == 0 || line.rfind("/gps/pos/centre") == 0) {
LOG(TRACE) << "Macro contains source position: \"" << line << "\"";
std::stringstream sstr(line);
std::string command, units;
double pos_x = NAN, pos_y = NAN, pos_z = NAN;
sstr >> command >> pos_x >> pos_y >> pos_z >> units;
ROOT::Math::XYZPoint source_position(
Units::get(pos_x, units), Units::get(pos_y, units), Units::get(pos_z, units));
LOG(DEBUG) << "Found source positioned at " << Units::display(source_position, {"mm", "cm"});
geo_manager_->addPoint(source_position);
}
}
}
// Add the particle source position to the geometry
geo_manager_->addPoint(config_.get<ROOT::Math::XYZPoint>("source_position", ROOT::Math::XYZPoint()));
}
void DepositionGeant4Module::initialize() {
MTRunManager* run_manager_mt = nullptr;
number_of_particles_ = config_.get<unsigned int>("number_of_particles", 1);
output_plots_ = config_.get<bool>("output_plots");
// Load the G4 run manager (which is owned by the geometry builder)
if(multithreadingEnabled()) {
run_manager_g4_ = G4MTRunManager::GetMasterRunManager();
run_manager_mt = static_cast<MTRunManager*>(run_manager_g4_);
G4Threading::SetMultithreadedApplication(true);
} else {
run_manager_g4_ = G4RunManager::GetRunManager();
}
if(run_manager_g4_ == nullptr) {
throw ModuleError("Cannot deposit charges using Geant4 without a Geant4 geometry builder");
}
// Apply optional PAI model
if(config_.get<bool>("enable_pai", false)) {
LOG(TRACE) << "Enabling PAI model on all detectors";
G4EmParameters::Instance();
auto pai_model = config_.get<std::string>("pai_model");
auto lcase_model = allpix::transform(pai_model, ::tolower);
if(lcase_model == "pai") {
pai_model = "PAI";
} else if(lcase_model == "paiphoton") {
pai_model = "PAIphoton";
} else {
throw InvalidValueError(config_, "pai_model", "model has to be either 'pai' or 'paiphoton'");
}
for(auto& detector : geo_manager_->getDetectors()) {
// Get logical volume
auto logical_volume = geo_manager_->getExternalObject<G4LogicalVolume>(detector->getName(), "sensor_log");
if(logical_volume == nullptr) {
throw ModuleError("Detector " + detector->getName() + " has no sensitive device (broken Geant4 geometry)");
}
// Create region
auto* region = new G4Region(detector->getName() + "_sensor_region");
region->AddRootLogicalVolume(logical_volume.get());
G4EmParameters::Instance()->AddPAIModel("all", region->GetName(), pai_model);
}
}
// Find the physics list
auto physics_list_up = allpix::transform(config_.get<std::string>("physics_list"), ::toupper);
auto physics_list = config_.get<std::string>("physics_list");
G4PhysListFactory physListFactory;
G4VModularPhysicsList* physicsList{nullptr};
try {
// Check if present in AdditionalPhysicsLists
physicsList = physicslists::getList(physics_list);
// Otherwise, attempt to get the physics list from the factory
if(physicsList == nullptr) {
// Geant4 throws an exception if the list is not found
physicsList = physListFactory.GetReferencePhysList(physics_list);
}
// Upper-case version of config
if(physicsList == nullptr) {
physicsList = physListFactory.GetReferencePhysList(physics_list_up);
}
if(physicsList == nullptr) {
throw ModuleError("");
}
} catch(ModuleError&) {
std::string message = "specified physics list does not exists";
std::vector<G4String> base_lists = physListFactory.AvailablePhysLists();
message += "\nAvailable base lists are: ";
for(auto& base_list : base_lists) {
message += base_list;
message += ", ";
}
message = message.substr(0, message.size() - 2);
message += "\nwith optional suffixes for electromagnetic lists: ";
std::vector<G4String> em_lists = physListFactory.AvailablePhysListsEM();
for(auto& em_list : em_lists) {
if(em_list.empty()) {
continue;
}
message += em_list;
message += ", ";
}
message = message.substr(0, message.size() - 2);
throw InvalidValueError(config_, "physics_list", message);
}
LOG(INFO) << "Using G4 physics list \"" << physics_list << "\"";
// Register a step limiter (uses the user limits defined earlier)
LOG(DEBUG) << "Registering Geant4 step limiter physics list";
physicsList->RegisterPhysics(new G4StepLimiterPhysics());
// Register radioactive decay physics lists unless the list already has it registered:
if(physics_list_up.find("HP") == std::string::npos && physics_list.find("Shielding") == std::string::npos) {
LOG(DEBUG) << "Registering Geant4 radioactive decay physics list";
physicsList->RegisterPhysics(new G4RadioactiveDecayPhysics());
}
// If the specified physics list is one of the microelec variations, apply a target region to the volumes with silicon
// materials
if(physics_list == "MICROELEC" || physics_list == "MICROELEC-SIONLY") {
// Create target region
auto* region = new G4Region("Target");
for(auto& detector : geo_manager_->getDetectors()) {
// Get the logical volume
auto logical_volume = geo_manager_->getExternalObject<G4LogicalVolume>(detector->getName(), "sensor_log");
if(logical_volume == nullptr) {
throw ModuleError("Detector " + detector->getName() + " has no sensitive device (broken Geant4 geometry)");
}
// Assign region to target if silicon
if(logical_volume->GetMaterial()->GetName() == "G4_Si") {
region->AddRootLogicalVolume(logical_volume.get());
LOG(DEBUG) << "Added " << logical_volume->GetName() << " to region " << region->GetName()
<< " for MicroElec physics"
<< "\n";
}
}
}
// Set the range-cut off threshold for secondary production:
double production_cut = NAN;
if(config_.has("range_cut")) {
production_cut = config_.get<double>("range_cut");
LOG(INFO) << "Setting configured G4 production cut to " << Units::display(production_cut, {"mm", "um"});
} else {
// Define the production cut as one fifth of the minimum size (thickness, pitch) among the detectors
double min_size = std::numeric_limits<double>::max();
std::string min_detector;
for(auto& detector : geo_manager_->getDetectors()) {
auto model = detector->getModel();
double prev_min_size = min_size;
min_size =
std::min({min_size, model->getPixelSize().x(), model->getPixelSize().y(), model->getSensorSize().z()});
if(min_size != prev_min_size) {
min_detector = detector->getName();
}
}
production_cut = min_size / 5;
LOG(INFO) << "Setting G4 production cut to " << Units::display(production_cut, {"mm", "um"})
<< ", derived from properties of detector \"" << min_detector << "\"";
}
physicsList->SetDefaultCutValue(production_cut);
// Set minimum remaining kinetic energy for a track
double min_charge_creation_energy{};
if(config_.has("charge_creation_energy")) {
min_charge_creation_energy = config_.get<double>("charge_creation_energy");
LOG(INFO) << "Setting minimum kinetic energy for tracks to " << Units::display(min_charge_creation_energy, {"eV"});
} else {
// Find minimum ionization energy from used detectors
min_charge_creation_energy = std::numeric_limits<double>::max();
std::string min_detector;
for(auto& detector : geo_manager_->getDetectors()) {
double this_min_charge_creation_energy = allpix::ionization_energies[detector->getModel()->getSensorMaterial()];
if(this_min_charge_creation_energy < min_charge_creation_energy) {
min_charge_creation_energy = this_min_charge_creation_energy;
min_detector = detector->getName();
}
}
LOG(INFO) << "Setting minimum kinetic energy for tracks to " << Units::display(min_charge_creation_energy, {"eV"})
<< ", derived from material of detector \"" << min_detector << "\"";
}
user_limits_world_->SetUserMinEkine(min_charge_creation_energy);
// Set user limits on world volume:
auto world_log_volume = geo_manager_->getExternalObject<G4LogicalVolume>("", "world_log");
if(world_log_volume != nullptr) {
// Quickly estimate longest distance in world and limit max track length
auto* world_box = static_cast<G4Box*>(world_log_volume->GetSolid());
auto max_track_length =
2e2 * (world_box->GetXHalfLength() + world_box->GetYHalfLength() + world_box->GetZHalfLength());
user_limits_world_->SetUserMaxTrackLength(max_track_length);
LOG(DEBUG) << "Setting world volume user limits to constrain event time to "
<< Units::display(config_.get<double>("cutoff_time"), {"ns", "us", "ms", "s"})
<< " and maximum track length to " << Units::display(max_track_length, {"mm", "cm", "m"});
world_log_volume->GetRegion()->SetUserLimits(user_limits_world_.get());
}
// Initialize the physics list
LOG(TRACE) << "Initializing physics processes";
run_manager_g4_->SetUserInitialization(physicsList);
run_manager_g4_->InitializePhysics();
// Disable verbose messages from processes
physicsList->SetVerboseLevel(0);
G4ProcessTable::GetProcessTable()->SetVerboseLevel(0);
G4EmParameters::Instance()->SetVerbose(0);
G4HadronicProcessStore::Instance()->SetVerbose(0);
G4HadronicParameters::Instance()->SetVerboseLevel(0);
G4NuclearLevelData::GetInstance()->GetParameters()->SetVerbose(0);
// Initialize the full run manager to ensure correct state flags
run_manager_g4_->Initialize();
// Build particle generator
// User hook to store additional information at track initialization and termination as well as custom track ids
LOG(TRACE) << "Constructing particle source";
initialize_g4_action();
// Construct the sensitive detectors and fields.
if(run_manager_mt == nullptr) {
// Create the info track manager for the main thread before creating the Sensitive detectors.
track_info_manager_ = std::make_unique<TrackInfoManager>(config_.get<bool>("record_all_tracks"));
construct_sensitive_detectors_and_fields();
} else {
// In MT-mode we register a builder that will be called for each thread to construct the SD when needed.
auto detector_construction = std::make_unique<SDAndFieldConstruction>(this);
run_manager_mt->SetSDAndFieldConstruction(std::move(detector_construction));
}
// Flush the Geant4 stream buffer because some elements in the initialization never do:
G4cout << G4endl;
}
void DepositionGeant4Module::initialize_g4_action() {
auto* action_initialization =
new ActionInitializationG4<GeneratorActionG4, GeneratorActionInitializationMaster>(config_);
run_manager_g4_->SetUserInitialization(action_initialization);
}
void DepositionGeant4Module::initializeThread() {
LOG(DEBUG) << "Initializing run manager";
// Initialize the thread local G4RunManager in case of MT
if(multithreadingEnabled()) {
auto* run_manager_mt = static_cast<MTRunManager*>(run_manager_g4_);
// In MT-mode the sensitive detectors will be created with the calls to BeamOn. So we construct the
// track manager for each calling thread here.
if(track_info_manager_ == nullptr) {
track_info_manager_ = std::make_unique<TrackInfoManager>(config_.get<bool>("record_all_tracks"));
}
run_manager_mt->InitializeForThread();
}
// Set selected tracking verbosity, defaulting to zero. Higher levels can be useful for tracing individual Geant4 events
G4RunManagerKernel::GetRunManagerKernel()->GetTrackingManager()->SetVerboseLevel(
config_.get<int>("geant4_tracking_verbosity", 0));
}
void DepositionGeant4Module::run(Event* event) {
// Seed the sensitive detectors RNG
for(auto& sensor : sensors_) {
sensor->seed(event->getRandomNumber());
}
// Start a single event from the beam
LOG(TRACE) << "Enabling beam";
auto seed1 = event->getRandomNumber();
auto seed2 = event->getRandomNumber();
LOG(DEBUG) << "Seeding Geant4 event with seeds " << seed1 << " " << seed2;
try {
if(multithreadingEnabled()) {
auto* run_manager_mt = static_cast<MTRunManager*>(run_manager_g4_);
run_manager_mt->Run(static_cast<int>(number_of_particles_), seed1, seed2);
} else {
auto* run_manager = static_cast<RunManager*>(run_manager_g4_);
run_manager->Run(static_cast<int>(number_of_particles_), seed1, seed2);
}
uint64_t last_event_num = last_event_num_.load();
last_event_num_.compare_exchange_strong(last_event_num, event->number);
track_info_manager_->createMCTracks();
track_info_manager_->dispatchMessage(this, messenger_, event);
// Dispatch the necessary messages
for(auto& sensor : sensors_) {
sensor->dispatchMessages(this, messenger_, event);
// Fill output plots if requested:
if(output_plots_) {
double charge = static_cast<double>(Units::convert(sensor->getDepositedCharge(), "ke"));
charge_per_event_[sensor->getName()]->Fill(charge);
double deposited_energy = static_cast<double>(Units::convert(sensor->getDepositedEnergy(), "keV"));
energy_per_event_[sensor->getName()]->Fill(deposited_energy);
for(const auto& track_position : sensor->getTrackIncidentPositions()) {
incident_track_position_[sensor->getName()]->Fill(track_position.x(), track_position.y());
}
}
}
} catch(AbortEventException& e) {
// Clear charge deposits of all sensors
for(auto& sensor : sensors_) {
sensor->clearEventInfo();
}
run_manager_g4_->AbortRun();
track_info_manager_->resetTrackInfoManager();
throw;
}
track_info_manager_->resetTrackInfoManager();
}
void DepositionGeant4Module::finalize() {
if(output_plots_) {
// Write histograms
LOG(TRACE) << "Writing output plots to file";
for(auto& histogram : charge_per_event_) {
histogram.second->Write();
}
for(auto& histogram : energy_per_event_) {
histogram.second->Write();
}
for(auto& histogram : incident_track_position_) {
histogram.second->Write();
}
}
// Print summary or warns if module did not output any charges
if(number_of_sensors_ > 0 && total_charges_ > 0 && last_event_num_ > 0) {
size_t average_charge = total_charges_ / number_of_sensors_ / last_event_num_;
LOG(INFO) << "Deposited total of " << total_charges_ << " charges in " << number_of_sensors_
<< " sensor(s) (average of " << average_charge << " per sensor for every event)";
} else {
LOG(WARNING) << "No charges deposited";
}
}
void DepositionGeant4Module::finalizeThread() {
// Record the number of sensors and the total charges
record_module_statistics();
if(multithreadingEnabled()) {
auto* run_manager_mt = static_cast<MTRunManager*>(run_manager_g4_);
run_manager_mt->TerminateForThread();
}
}
void DepositionGeant4Module::construct_sensitive_detectors_and_fields() {
if(geo_manager_->hasMagneticField()) {
MagneticFieldType magnetic_field_type_ = geo_manager_->getMagneticFieldType();
if(magnetic_field_type_ == MagneticFieldType::CONSTANT) {
ROOT::Math::XYZVector b_field = geo_manager_->getMagneticField(ROOT::Math::XYZPoint(0., 0., 0.));
G4MagneticField* magField = new G4UniformMagField(G4ThreeVector(b_field.x(), b_field.y(), b_field.z()));
G4FieldManager* globalFieldMgr = G4TransportationManager::GetTransportationManager()->GetFieldManager();
globalFieldMgr->SetDetectorField(magField);
globalFieldMgr->CreateChordFinder(magField);
} else {
throw ModuleError("Magnetic field enabled, but not constant. This can't be handled by this module yet.");
}
}
// Loop through all detectors and set the sensitive detector action that handles the particle passage
bool useful_deposition = false;
for(auto& detector : geo_manager_->getDetectors()) {
// Do not add sensitive detector for detectors that have no listeners for the deposited charges
if(!messenger_->hasReceiver(this,
std::make_shared<DepositedChargeMessage>(std::vector<DepositedCharge>(), detector)) &&
!messenger_->hasReceiver(this, std::make_shared<MCParticleMessage>(std::vector<MCParticle>(), detector)) &&
!messenger_->hasReceiver(this, std::make_shared<MCTrackMessage>(std::vector<MCTrack>()))) {
LOG(INFO) << "Not depositing charges in " << detector->getName()
<< " because there is no listener for its output";
continue;
}
useful_deposition = true;
// Get ionization energy and Fano factor
auto model = detector->getModel();
auto charge_creation_energy =
(config_.has("charge_creation_energy") ? config_.get<double>("charge_creation_energy")
: allpix::ionization_energies[model->getSensorMaterial()]);
auto fano_factor = (config_.has("fano_factor") ? config_.get<double>("fano_factor")
: allpix::fano_factors[model->getSensorMaterial()]);
LOG(DEBUG) << "Detector " << detector->getName() << " uses charge creation energy "
<< Units::display(charge_creation_energy, "eV") << " and Fano factor " << fano_factor;
// Cut-off time for particle generation:
auto cutoff_time = config_.get<double>("cutoff_time");
// Get model of the sensitive device
auto* sensitive_detector_action = new SensitiveDetectorActionG4(
detector, track_info_manager_.get(), charge_creation_energy, fano_factor, cutoff_time);
auto logical_volume = geo_manager_->getExternalObject<G4LogicalVolume>(detector->getName(), "sensor_log");
if(logical_volume == nullptr) {
throw ModuleError("Detector " + detector->getName() + " has no sensitive device (broken Geant4 geometry)");
}
// Apply the user limits to this element
logical_volume->SetUserLimits(user_limits_.get());
// Add the sensitive detector action
logical_volume->SetSensitiveDetector(sensitive_detector_action);
// Add the sensitive detector action to fronmtside implant volumes
std::regex regex;
if(config_.get<bool>("deposit_in_frontside_implants") && config_.get<bool>("deposit_in_backside_implants")) {
regex = "implant_log_.*";
} else if(config_.get<bool>("deposit_in_frontside_implants")) {
regex = "implant_log_frontside_.*";
} else if(config_.get<bool>("deposit_in_backside_implants")) {
regex = "implant_log_backside_.*";
}
for(const auto& implant : geo_manager_->getExternalObjects<G4LogicalVolume>(detector->getName(), regex)) {
implant->SetUserLimits(user_limits_.get());
implant->SetSensitiveDetector(sensitive_detector_action);
}
sensors_.push_back(sensitive_detector_action);
// If requested, prepare output plots
if(output_plots_) {
LOG(TRACE) << "Creating output plots for detector " << sensitive_detector_action->getName();
// Plot axis are in kilo electrons - convert from framework units!
int maximum_charge = static_cast<int>(Units::convert(config_.get<int>("output_plots_scale"), "ke"));
double maximum_energy =
(static_cast<int>(maximum_charge / 2. * Units::convert(charge_creation_energy, "eV")) / 10) * 10 + 10;
int nbins = 5 * maximum_charge;
// Get detector model size
auto sensor_size = detector->getModel()->getSensorSize();
auto pixel_size = detector->getModel()->getPixelSize();
// Create histograms if needed
{
std::lock_guard<std::mutex> lock(histogram_mutex_);
std::string plot_name = "deposited_charge_" + sensitive_detector_action->getName();
if(charge_per_event_.find(sensitive_detector_action->getName()) == charge_per_event_.end()) {
charge_per_event_[sensitive_detector_action->getName()] =
CreateHistogram<TH1D>(plot_name.c_str(),
"deposited charge per event;deposited charge [ke];events",
nbins,
0,
maximum_charge);
}
plot_name = "deposited_energy_" + sensitive_detector_action->getName();
if(energy_per_event_.find(sensitive_detector_action->getName()) == energy_per_event_.end()) {
energy_per_event_[sensitive_detector_action->getName()] =
CreateHistogram<TH1D>(plot_name.c_str(),
"deposited energy per event;deposited energy [keV];events",
nbins,
0,
maximum_energy);
}
plot_name = "incident_track_position_" + sensitive_detector_action->getName();
if(incident_track_position_.find(sensitive_detector_action->getName()) == incident_track_position_.end()) {
incident_track_position_[sensitive_detector_action->getName()] =
CreateHistogram<TH2D>(plot_name.c_str(),
"incident track position;X [mm];Y [mm];Z",
5000,
-pixel_size.X() / 2,
sensor_size.X() - pixel_size.X() / 2,
5000,
-pixel_size.Y() / 2,
sensor_size.Y() - pixel_size.Y() / 2);
}
}
}
}
if(!useful_deposition) {
LOG(ERROR) << "Not a single listener for deposited charges, module is useless!";
}
}
void DepositionGeant4Module::record_module_statistics() {
// Since sensors is thread local, some instances may not be used, hence, skip them
auto num_sensors = sensors_.size();
if(num_sensors > 0) {
number_of_sensors_ = num_sensors;
}
// We calculate the total deposited charges here, since sensors exist per thread
for(auto& sensor : sensors_) {
total_charges_ += sensor->getTotalDepositedCharge();
}
}
Updated on 2025-02-27 at 14:14:46 +0000