src/modules/DepositionLaser/DepositionLaserModule.cpp

Implementation of [DepositionLaser] module. More…

Detailed Description

Implementation of [DepositionLaser] module.

Copyright: Copyright (c) 2022-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

Source code


#include <DepositionLaserModule.hpp>

#include <core/utils/distributions.h>
#include <core/utils/log.h>
#include <objects/DepositedCharge.hpp>
#include <objects/MCParticle.hpp>
#include <tools/liang_barsky.h>

#include <Math/AxisAngle.h>
#include <TMath.h>

#include <algorithm>
#include <cmath>
#include <filesystem>
#include <fstream>
#include <iterator>

using namespace allpix;

DepositionLaserModule::DepositionLaserModule(Configuration& config, Messenger* messenger, GeometryManager* geo_manager)
    : Module(config), geo_manager_(geo_manager), messenger_(messenger) {

    allow_multithreading();

    //
    // Read beam parameters from config
    //

    source_position_ = config_.get<ROOT::Math::XYZPoint>("source_position");
    LOG(DEBUG) << "Source position: " << Units::display(source_position_, {"mm"});

    // Make beam_direction a unity vector, so t-values produced by clipping algorithm are in actual length units
    beam_direction_ = config_.get<ROOT::Math::XYZVector>("beam_direction").Unit();
    LOG(DEBUG) << "Beam direction: " << beam_direction_;

    beam_geometry_ = config_.get<BeamGeometry>("beam_geometry");
    size_t convergence_params_count = config_.count({"focal_distance", "beam_convergence_angle"});
    if(beam_geometry_ == BeamGeometry::CYLINDRICAL) {
        LOG(DEBUG) << "Beam geometry: cylindrical";
        if(convergence_params_count > 0) {
            LOG(DEBUG) << "Beam convergence parameters are ignored for a cylindrical beam";
        }
    } else if(beam_geometry_ == BeamGeometry::CONVERGING) {
        LOG(DEBUG) << "Beam geometry: converging";
        if(convergence_params_count < 2) {
            throw InvalidCombinationError(config_,
                                          {"beam_geometry", "focal_distance", "beam_convergence_angle"},
                                          "Both focal distance and convergence should be specified for a gaussian beam");
        }
        focal_distance_ = config_.get<double>("focal_distance");
        beam_convergence_angle_ = config_.get<double>("beam_convergence_angle");
        LOG(DEBUG) << "Focal distance: " << Units::display(focal_distance_, "mm")
                   << ", convergence angle: " << Units::display(beam_convergence_angle_, "deg");
    }

    config_.setDefault<double>("beam_waist", 0.02);
    beam_waist_ = config_.get<double>("beam_waist");
    LOG(DEBUG) << "Beam waist: " << Units::display(beam_waist_, "um");
    if(beam_waist_ < 0) {
        throw InvalidValueError(config_, "beam_waist", "Beam waist should be a positive value");
    }

    config_.setDefault<int>("number_of_photons", 10000);
    number_of_photons_ = config_.get<size_t>("number_of_photons");
    LOG(DEBUG) << "Number of photons: " << number_of_photons_;
    if(number_of_photons_ == 0) {
        throw InvalidValueError(config_, "number_of_photons", "Number of photons should be a nonzero value");
    }

    config_.setDefault<int>("group_photons", 1);
    group_photons_ = config_.get<size_t>("group_photons");
    if(group_photons_ == 0) {
        throw InvalidValueError(config_, "group_photons", "Should be a nonzero value");
    } else if(group_photons_ > 1) {
        number_of_photons_ /= group_photons_;
        LOG(DEBUG) << "Photons will be generated as " << number_of_photons_ << " groups of " << group_photons_;
    }

    config_.setDefault<double>("pulse_duration", 0.5);
    pulse_duration_ = config_.get<double>("pulse_duration");
    LOG(DEBUG) << "Pulse duration: " << Units::display(pulse_duration_, "ns");
    if(pulse_duration_ < 0) {
        throw InvalidValueError(config_, "pulse_duration_", "Pulse should be a positive value");
    }

    // Select user optics or silicon absorption lookup:
    is_user_optics_ = (config_.count({"absorption_length", "refractive_index"}) == 2);

    if(config_.count({"absorption_length", "refractive_index", "wavelength"}) == 3) {
        throw InvalidCombinationError(config_,
                                      {"absorption_length", "refractive_index", "wavelength"},
                                      "User definition for optical parameters and wavelength are mutually exclusive!");
    }

    if(is_user_optics_) {
        absorption_length_ = config_.get<double>("absorption_length");
        refractive_index_ = config_.get<double>("refractive_index");
        LOG(DEBUG) << "Setting user-defined optical properties for sensor material";
    } else {
        wavelength_ = config_.get<double>("wavelength");
        if(Units::convert(wavelength_, "nm") < 250 || Units::convert(wavelength_, "nm") > 1450) {
            throw InvalidValueError(config_, "wavelength", "Currently supported wavelengths are 250 -- 1450 nm");
        }

        // Register lookup path for data files:
        if(config_.has("data_path")) {
            auto path = config_.getPath("data_path", true);
            if(!std::filesystem::is_directory(path)) {
                throw InvalidValueError(config_, "data_path", "path does not point to a directory");
            }
            LOG(TRACE) << "Registered absorption data path from configuration: " << path;
        } else {
            if(std::filesystem::is_directory(ALLPIX_LASER_DATA_DIRECTORY)) {
                config_.set<std::string>("data_path", ALLPIX_LASER_DATA_DIRECTORY);
                LOG(TRACE) << "Registered absorption data path from system: " << ALLPIX_LASER_DATA_DIRECTORY;
            } else {
                const char* data_dirs_env = std::getenv("XDG_DATA_DIRS");
                if(data_dirs_env == nullptr || strlen(data_dirs_env) == 0) {
                    data_dirs_env = "/usr/local/share/:/usr/share/:";
                }

                auto data_dirs = split<std::filesystem::path>(data_dirs_env, ":");
                for(auto& data_dir : data_dirs) {
                    data_dir /= std::filesystem::path(ALLPIX_PROJECT_NAME) / "data";
                    if(std::filesystem::is_directory(data_dir)) {
                        config_.set<std::string>("data_path", data_dir);
                        LOG(TRACE) << "Registered absorption data path from XDG_DATA_DIRS: " << data_dir;
                    } else {
                        throw ModuleError(
                            "Cannot find absorption data files, provide them in the configuration, via XDG_DATA_DIRS or "
                            "in system directory " +
                            std::string(ALLPIX_LASER_DATA_DIRECTORY));
                    }
                }
            }
        }
    }

    config_.setDefault<bool>("output_plots", false);
    output_plots_ = config.get<bool>("output_plots");
}

void DepositionLaserModule::initialize() {
    // Check if there are user-specified optical properties for materials
    if(!is_user_optics_) {
        // wavelength: {absorption_length, refractive_index}
        std::map<double, std::pair<double, double>> optics_lut;
        double wl = 0;
        double abs_length = 0;
        double refr_ind = 0;

        // Load data
        auto file_path = config_.getPath("data_path", true) / "silicon_photoabsorption.data";
        std::ifstream f(file_path);
        LOG(DEBUG) << "Loading optical properties for sensor material from LUT: " << std::endl << file_path.string();

        if(!f || !std::filesystem::is_regular_file(file_path)) {
            throw ModuleError("Could not open optical properties reference file at \"" + file_path.string() + "\"");
        }

        while(f >> wl >> abs_length >> refr_ind) {
            optics_lut[Units::get(wl, "nm")] = {abs_length, refr_ind};
        }

        // Find or interpolate absorption depth for given wavelength
        if(optics_lut.count(wavelength_) != 0) {
            absorption_length_ = optics_lut[wavelength_].first;
            refractive_index_ = optics_lut[wavelength_].second;
        } else {
            auto it = optics_lut.upper_bound(wavelength_);
            double wl1 = (*prev(it)).first;
            double wl2 = (*it).first;
            absorption_length_ =
                (optics_lut[wl1].first * (wl2 - wavelength_) + optics_lut[wl2].first * (wavelength_ - wl1)) / (wl2 - wl1);
            refractive_index_ =
                (optics_lut[wl1].second * (wl2 - wavelength_) + optics_lut[wl2].second * (wavelength_ - wl1)) / (wl2 - wl1);
        }
    }
    LOG(DEBUG) << "Wavelength = " << Units::display(wavelength_, "nm")
               << ", absorption length: " << Units::display(absorption_length_, {"um", "mm"})
               << ", refractive index: " << refractive_index_;

    // Check for unsupported detector materials, warn user if present

    std::vector<std::shared_ptr<Detector>> detectors = geo_manager_->getDetectors();
    for(auto& detector : detectors) {
        auto material = detector->getModel()->getSensorMaterial();
        if(material != SensorMaterial::SILICON && !is_user_optics_) {
            LOG(WARNING) << "Detector " << detector->getName() << " has unsupported material and will be ignored";
        }
    }
    // Check for incompatible passive objects, warn user if there are any
    auto passive_configs = geo_manager_->getPassiveElements();
    for(const auto& item : passive_configs) {
        auto shape = item.get<std::string>("type");
        if(shape != "box") {
            LOG(WARNING) << item.getName() << " passive object has unsupported type (" << shape << ") and will be ignored";
        }
    }

    // Create Histograms
    if(output_plots_) {
        LOG(DEBUG) << "Initializing histograms";
        Int_t nbins = 100;
        double nsigmas = 3;
        double focalplane_histsize = beam_waist_ * nsigmas;

        h_intensity_focalplane_ = CreateHistogram<TH2D>("intensity_focalplane",
                                                        "Beam profile in focal plane, a.u.;x [mm];y [mm]",
                                                        nbins,
                                                        -focalplane_histsize,
                                                        focalplane_histsize,
                                                        nbins,
                                                        -focalplane_histsize,
                                                        focalplane_histsize);

        double sourceplane_histsize = focalplane_histsize;
        if(beam_geometry_ == BeamGeometry::CONVERGING) {
            sourceplane_histsize += focal_distance_ * sin(beam_convergence_angle_);
        }

        h_intensity_sourceplane_ = CreateHistogram<TH2D>("intensity_sourceplane",
                                                         "Beam profile at source, a.u.;x [mm];y [mm]",
                                                         nbins,
                                                         -sourceplane_histsize,
                                                         sourceplane_histsize,
                                                         nbins,
                                                         -sourceplane_histsize,
                                                         sourceplane_histsize);

        h_angular_phi_ = CreateHistogram<TH1D>(
            "phi_distribution", "Phi_distribution w.r.t. beam direction;Phi [rad];Counts", nbins, -3.5, 3.5);
        h_angular_theta_ = CreateHistogram<TH1D>(
            "theta_distribution", "Theta distribution w.r.t. beam direction;Theta [deg];Counts", nbins, 0, 45);
        h_pulse_shape_ =
            CreateHistogram<TH1D>("pulse_shape", "Pulse shape;t [ns];Intensity [a.u.]", nbins, 0, 8 * pulse_duration_);

        for(const auto& detector : detectors) {
            std::string name = "dep_charge_" + detector->getName();
            std::string title = name + ";x [mm];y [mm];z [mm]";
            auto sensor = detector->getModel()->getSensorSize();

            h_deposited_charge_shapes_[detector] = CreateHistogram<TH3D>(name.c_str(),
                                                                         title.c_str(),
                                                                         100,
                                                                         -sensor.X() / 2,
                                                                         sensor.X() / 2,
                                                                         100,
                                                                         -sensor.Y() / 2,
                                                                         sensor.Y() / 2,
                                                                         100,
                                                                         -sensor.Z() / 2,
                                                                         sensor.Z() / 2);
        }
    }
}

void DepositionLaserModule::run(Event* event) {

    // Containers for output messages

    std::map<std::shared_ptr<Detector>, std::vector<MCParticle>> mc_particles;
    std::map<std::shared_ptr<Detector>, std::vector<DepositedCharge>> deposited_charges;

    // Lambda generator to yield pulse shape
    auto yield_starting_time = [&]() {
        int cut_sigmas = 4;
        double result = -1;
        while(result < 0) {
            result =
                allpix::normal_distribution<double>(cut_sigmas * pulse_duration_, pulse_duration_)(event->getRandomEngine());
        }
        return result;
    };

    // Containers for timestamps
    std::vector<double> starting_times(number_of_photons_);
    std::for_each(begin(starting_times), end(starting_times), [&](auto& item) {
        item = yield_starting_time();
        if(output_plots_) {
            h_pulse_shape_->Fill(item);
        }
    });

    std::sort(begin(starting_times), end(starting_times));

    // To correctly offset local time for each detector
    std::map<std::shared_ptr<Detector>, double> local_time_offsets;

    // Loop over photons in a single laser pulse
    // In time order
    for(size_t i_photon = 0; i_photon < number_of_photons_; ++i_photon) {

        LOG_PROGRESS(INFO, "photon_counter")
            << "Event " << event->number << ": photon " << i_photon + 1 << " of " << number_of_photons_;

        // Starting point and direction for this exact photon
        auto [starting_point, photon_direction] = generate_photon_geometry(event);

        // Get starting time in the pulse
        double starting_time = starting_times[i_photon];
        LOG(DEBUG) << "    Starting timestamp: " << Units::display(starting_time, "ns");

        // Generate penetration depth
        double penetration_depth =
            allpix::exponential_distribution<double>(1 / absorption_length_)(event->getRandomEngine());
        LOG(DEBUG) << "    Penetration depth: " << Units::display(penetration_depth, "um");

        // Perform tracking
        std::optional<PhotonHit> hit_opt = track(starting_point, photon_direction, penetration_depth);

        // If this photon did not hit any of the detectors, skip this iteration
        if(!hit_opt) {
            continue;
        }

        PhotonHit hit = hit_opt.value();

        // If this was the first hit in this detector in this event,
        // remember entry timestamp as local t=0 for this detector.
        // It is assumed that photon that is created earlier also hits earlier
        if(local_time_offsets.count(hit.detector) == 0) {
            local_time_offsets[hit.detector] = starting_time + hit.time_to_entry;
        }

        // Create and store corresponding MCParticle and DepositedCharge
        auto entry_local = hit.detector->getLocalPosition(hit.entry_global);
        auto hit_local = hit.detector->getLocalPosition(hit.hit_global);

        double time_entry_global = starting_time + hit.time_to_entry;
        double time_hit_global = starting_time + hit.time_to_hit;
        double time_entry_local = time_entry_global - local_time_offsets[hit.detector];
        double time_hit_local = time_hit_global - local_time_offsets[hit.detector];

        LOG(DEBUG) << "    Hit in " << hit.detector->getName();
        LOG(DEBUG) << "        global: " << Units::display(hit.hit_global, {"mm"}) << Units::display(time_hit_global, "ns");
        LOG(DEBUG) << "        local: " << Units::display(hit_local, {"mm"}) << Units::display(time_hit_local, "ns");

        if(output_plots_) {
            h_deposited_charge_shapes_[hit.detector]->Fill(hit_local.X(), hit_local.Y(), hit_local.Z());
        }

        // If that is a first hit in this detector, create map entries
        if(mc_particles.count(hit.detector) == 0) {
            mc_particles[hit.detector] = std::vector<MCParticle>();
        }
        if(deposited_charges.count(hit.detector) == 0) {
            deposited_charges[hit.detector] = std::vector<DepositedCharge>();
        }

        // Construct all necessary objects in-place
        // allpix::MCParticle
        mc_particles[hit.detector].emplace_back(entry_local,
                                                hit.entry_global,
                                                hit_local,
                                                hit.hit_global,
                                                22, // gamma
                                                time_entry_local,
                                                time_entry_global);
        // Count electrons and holes:
        mc_particles[hit.detector].back().setTotalDepositedCharge(2);

        // allpix::DepositedCharge for electron
        deposited_charges[hit.detector].emplace_back(hit_local,
                                                     hit.hit_global,
                                                     CarrierType::ELECTRON,
                                                     group_photons_, // value
                                                     time_hit_local,
                                                     time_hit_global);

        // allpix::DepositedCharge for hole
        deposited_charges[hit.detector].emplace_back(hit_local,
                                                     hit.hit_global,
                                                     CarrierType::HOLE,
                                                     group_photons_, // value
                                                     time_hit_local,
                                                     time_hit_global);

    } // loop over photons

    LOG(INFO) << "Registered hits in " << mc_particles.size() << " detectors";

    // After all the containers are filled, assign MCParticle links in DepositedCharges

    for(const auto& [detector, data] : mc_particles) {
        for(size_t i = 0; i < size(mc_particles[detector]); ++i) {
            deposited_charges[detector][2 * i].setMCParticle(&(data[i]));
            deposited_charges[detector][2 * i + 1].setMCParticle(&(data[i]));
        }
    }

    // Dispatch messages
    for(auto& [detector, data] : mc_particles) {
        LOG(INFO) << "    " << detector->getName() << ": " << data.size() << " hits";
        auto mcparticle_message = std::make_shared<MCParticleMessage>(std::move(data), detector);
        messenger_->dispatchMessage(this, std::move(mcparticle_message), event);
    }

    for(auto& [detector, data] : deposited_charges) {
        auto charge_message = std::make_shared<DepositedChargeMessage>(std::move(data), detector);
        messenger_->dispatchMessage(this, std::move(charge_message), event);
    }
}

void DepositionLaserModule::finalize() {
    if(output_plots_) {
        h_intensity_focalplane_->Write();
        h_intensity_sourceplane_->Write();
        h_angular_phi_->Write();
        h_angular_theta_->Write();
        h_pulse_shape_->Write();
        for(auto& [detector, histo] : h_deposited_charge_shapes_) {
            histo->Write();
        }
    }
}

std::pair<ROOT::Math::XYZPoint, ROOT::Math::XYZVector> DepositionLaserModule::generate_photon_geometry(Event* event) {
    // Lambda to generate two unit vectors, orthogonal to beam direction
    // Adapted from TVector3::Orthogonal()
    auto orthogonal_pair = [](const ROOT::Math::XYZVector& v) {
        // Additional convenience variables for components' absolute values
        double abs_x = v.X() < 0.0 ? -v.X() : v.X();
        double abs_y = v.Y() < 0.0 ? -v.Y() : v.Y();
        double abs_z = v.Z() < 0.0 ? -v.Z() : v.Z();

        ROOT::Math::XYZVector v1, v2;

        if(abs_x < abs_y) {
            v1 = (abs_x < abs_z ? ROOT::Math::XYZVector(0, v.Z(), -v.Y()) : ROOT::Math::XYZVector(v.Y(), -v.X(), 0));
        } else {
            v1 = (abs_y < abs_z ? ROOT::Math::XYZVector(-v.Z(), 0, v.X()) : ROOT::Math::XYZVector(v.Y(), -v.X(), 0));
        }

        v2 = v.Cross(v1);
        return std::make_pair(v1.Unit(), v2.Unit());
    };

    // Lambda to generate a smearing vector
    auto beam_pos_smearing = [&](auto size) {
        auto [v1, v2] = orthogonal_pair(beam_direction_);

        // Beam waist is equal to 2*sigma
        double dx = allpix::normal_distribution<double>(0, size / 2.)(event->getRandomEngine());
        double dy = allpix::normal_distribution<double>(0, size / 2.)(event->getRandomEngine());
        return v1 * dx + v2 * dy;
    };

    // Lambda to get scalar orthogonal components w.r.t. beam_direction
    auto orthogonal_components = [&](const ROOT::Math::XYZVector& v) {
        auto [v1, v2] = orthogonal_pair(beam_direction_);
        return std::make_pair(v.Dot(v1), v.Dot(v2));
    };

    ROOT::Math::XYZPoint starting_point{};
    ROOT::Math::XYZVector photon_direction{};

    // Generate starting point and direction in the beam
    if(beam_geometry_ == BeamGeometry::CONVERGING) {
        // Converging beam case

        // Generate correct position in the focal plane
        auto focal_position = source_position_ + beam_direction_ * focal_distance_ + beam_pos_smearing(beam_waist_);

        // Generate angles
        double phi = allpix::uniform_real_distribution<double>(0, 2 * TMath::Pi())(event->getRandomEngine());
        double cos_theta =
            allpix::uniform_real_distribution<double>(cos(beam_convergence_angle_), 1)(event->getRandomEngine());

        // Rotate direction by given angles
        // First, define and apply theta rotation
        ROOT::Math::XYZVector theta_axis = orthogonal_pair(beam_direction_).first;
        ROOT::Math::AxisAngle theta_rotation(theta_axis, acos(cos_theta));
        photon_direction = theta_rotation(beam_direction_);

        // Second, rotate that around the beam axis
        ROOT::Math::AxisAngle phi_rotation(beam_direction_, phi);
        photon_direction = phi_rotation(photon_direction);

        // Backtrack position from the focal plane to the source plane
        // This calculation is nuts, will add an explanation somewhere
        starting_point = focal_position + photon_direction * beam_direction_.Dot(source_position_ - focal_position) /
                                              beam_direction_.Dot(photon_direction);

        if(output_plots_) {
            // Fill beam profile histos
            auto [dx_focal, dy_focal] = orthogonal_components(focal_position - source_position_);
            h_intensity_focalplane_->Fill(dx_focal, dy_focal);
            auto [dx_source, dy_source] = orthogonal_components(starting_point - source_position_);
            h_intensity_sourceplane_->Fill(dx_source, dy_source);
        }

    } else {
        // Cylindrical beam case
        starting_point = source_position_ + beam_pos_smearing(beam_waist_);
        photon_direction = beam_direction_;

        if(output_plots_) {
            // Fill beam profle histos
            auto [dx_source, dy_source] = orthogonal_components(starting_point - source_position_);
            h_intensity_sourceplane_->Fill(dx_source, dy_source);
            h_intensity_focalplane_->Fill(dx_source, dy_source);
        }
    }

    // Fill histograms if needed
    if(output_plots_) {
        // Both are unit vectors
        double theta = static_cast<double>(Units::convert(acos(beam_direction_.Dot(photon_direction)), "deg"));
        auto [dx, dy] = orthogonal_components(photon_direction);
        double phi = atan2(dy, dx);
        h_angular_phi_->Fill(phi);
        h_angular_theta_->Fill(theta);
    }

    LOG(DEBUG) << "    Starting point: " << Units::display(starting_point, {"mm"}) << ", direction: " << photon_direction;

    return {starting_point, photon_direction};
}

std::optional<DepositionLaserModule::PhotonHit> DepositionLaserModule::track(const ROOT::Math::XYZPoint& position,
                                                                             const ROOT::Math::XYZVector& direction,
                                                                             double penetration_depth) const {

    // Lambda for angle calculation
    auto angle = [](const ROOT::Math::XYZVector& v1, const ROOT::Math::XYZVector& v2) {
        return acos(v1.Unit().Dot(v2.Unit()));
    };

    double c = TMath::C() * 100; // speed of light in mm/ns

    std::vector<std::shared_ptr<Detector>> detectors = geo_manager_->getDetectors();
    std::vector<std::pair<std::shared_ptr<Detector>, std::pair<double, double>>> intersection_segments;

    for(auto& detector : detectors) {
        if(detector->getModel()->getSensorMaterial() != SensorMaterial::SILICON && !is_user_optics_) {
            continue;
        }
        auto intersection = intersect_with_sensor(detector, position, direction);
        if(intersection) {
            intersection_segments.emplace_back(detector, intersection.value());
        }
    }

    if(intersection_segments.empty()) {
        LOG(DEBUG) << "No intersections with sensitive detectors";
        return std::nullopt;
    }

    auto it_first_detector = std::min_element(begin(intersection_segments),
                                              end(intersection_segments),
                                              [](const auto& p1, const auto& p2) { return p1.second < p2.second; });

    auto detector = it_first_detector->first;
    double t0 = it_first_detector->second.first;

    auto intersect_passive = intersect_with_passives(position, direction);
    if(intersect_passive) {
        if(intersect_passive.value().first < t0) {
            LOG(DEBUG) << "Absorbed by (" << intersect_passive.value().second << ") passive object";
            return std::nullopt;
        }
    }

    auto normal_vector = -1 * intersection_normal_vector(detector, position + direction * t0);

    double incidence_angle = angle(direction, normal_vector);
    double refraction_angle = asin(sin(incidence_angle) / refractive_index_);

    // Construct direction of the refracted ray
    auto binormal = direction.Cross(normal_vector);
    ROOT::Math::AxisAngle refraction_rotation(binormal, incidence_angle - refraction_angle);
    ROOT::Math::XYZVector new_direction = refraction_rotation(direction);

    LOG(DEBUG) << "    Intersection with " << detector->getName();
    LOG(DEBUG) << "        entry at " << Units::display(position + direction * t0, {"mm"});
    LOG(DEBUG) << "        normal at entry: " << normal_vector << ", binormal: " << binormal.Unit();
    LOG(DEBUG) << "        incidence angle: " << Units::display(incidence_angle, "deg")
               << ", refraction angle: " << Units::display(refraction_angle, "deg");
    LOG(DEBUG) << "        direction after refraction: " << new_direction;

    // Intersect the refracted ray with the detector
    auto intersection = intersect_with_sensor(detector, position + direction * t0, new_direction);
    auto [t0_refract, t1_refract] = intersection.value();
    double crossing_distance = t1_refract - t0_refract;

    LOG(DEBUG) << "        crossing_distance: " << Units::display(crossing_distance, {"um", "mm"});

    if(crossing_distance < penetration_depth) {
        LOG(DEBUG) << "    Photon is not absorbed";
        return std::nullopt;
    }

    // Construct a hit

    return PhotonHit{detector,
                     position + direction * t0,
                     position + direction * t0 + new_direction * penetration_depth,
                     t0 / c,
                     t0 / c + penetration_depth / c * refractive_index_};
}

std::optional<std::pair<double, double>>
DepositionLaserModule::intersect_with_sensor(const std::shared_ptr<const Detector>& detector,
                                             const ROOT::Math::XYZPoint& position_global,
                                             const ROOT::Math::XYZVector& direction_global) const {
    // Obtain total sensor size
    auto sensor = detector->getModel()->getSensorSize();

    // Transform original position and direction to a sensor-related coordinate system,

    // Construct transformation from the sensor system to the global one
    // * The rotation into the global coordinate system
    // * The shift from the origin to the detector position
    ROOT::Math::Rotation3D rotation_center(detector->getOrientation());
    ROOT::Math::Translation3D translation_center(static_cast<ROOT::Math::XYZVector>(detector->getPosition()));
    ROOT::Math::Transform3D transform_center(rotation_center, translation_center);

    // Apply inverse of that transformation
    auto position_local = transform_center.Inverse()(position_global);

    // Direction vector can directly be rotated
    auto direction_local = detector->getOrientation().Inverse()(direction_global);

    return LiangBarsky::intersectionDistances(direction_local, position_local, sensor);
}

std::optional<std::pair<double, std::string>>
DepositionLaserModule::intersect_with_passives(const ROOT::Math::XYZPoint& position_global,
                                               const ROOT::Math::XYZVector& direction_global) const {

    std::optional<std::pair<double, std::string>> result{};
    auto passive_configs = geo_manager_->getPassiveElements();

    for(const auto& item : passive_configs) {
        auto shape = item.get<std::string>("type");
        if(shape != "box") {
            continue;
        }

        auto [passive_position, passive_orientation] = geo_manager_->getPassiveElementOrientation(item.getName());
        auto passive_size = item.get<ROOT::Math::XYZVector>("size");

        ROOT::Math::Rotation3D rotation_center(passive_orientation);
        ROOT::Math::Translation3D translation_center(static_cast<ROOT::Math::XYZVector>(passive_position));
        ROOT::Math::Transform3D transform_center(rotation_center, translation_center);
        auto position_local = transform_center.Inverse()(position_global);
        auto direction_local = rotation_center.Inverse()(direction_global);

        auto intersect = LiangBarsky::intersectionDistances(direction_local, position_local, passive_size);

        if(!intersect) {
            continue;
        }

        double distance = intersect.value().first;

        if(!result) {
            result = {distance, item.getName()};
        }

        if(distance < result.value().first) {
            result = {distance, item.getName()};
        }
    }

    return result;
}

ROOT::Math::XYZVector DepositionLaserModule::intersection_normal_vector(const std::shared_ptr<const Detector>& detector,
                                                                        const ROOT::Math::XYZPoint& position_global) const {
    // Obtain total sensor size
    auto sensor = detector->getModel()->getSensorSize();

    // Transform original position and direction to a sensor-related coordinate system,

    // Construct transformation from the sensor system to the global one
    // * The rotation into the global coordinate system
    // * The shift from the origin to the detector position
    ROOT::Math::Rotation3D rotation_center(detector->getOrientation());
    ROOT::Math::Translation3D translation_center(static_cast<ROOT::Math::XYZVector>(detector->getPosition()));
    ROOT::Math::Transform3D transform_center(rotation_center, translation_center);

    // Apply inverse of that transformation
    auto position_local = transform_center.Inverse()(position_global);

    std::vector<double> distances_to_faces = {std::abs(position_local.X() - sensor.X() / 2),
                                              std::abs(position_local.X() + sensor.X() / 2),
                                              std::abs(position_local.Y() - sensor.Y() / 2),
                                              std::abs(position_local.Y() + sensor.Y() / 2),
                                              std::abs(position_local.Z() - sensor.Z() / 2),
                                              std::abs(position_local.Z() + sensor.Z() / 2)};

    std::vector<ROOT::Math::XYZVector> normals_to_faces = {
        {1, 0, 0},
        {-1, 0, 0},
        {0, 1, 0},
        {0, -1, 0},
        {0, 0, 1},
        {0, 0, -1},
    };

    auto iter_min = std::min_element(begin(distances_to_faces), end(distances_to_faces));
    size_t index_min = static_cast<size_t>(std::abs(iter_min - begin(distances_to_faces))); // avoid implicit conversion

    return rotation_center(normals_to_faces[index_min]);
}

Updated on 2024-12-13 at 08:31:37 +0000