src/core/geometry/DetectorModel.cpp

Implementation of detector model. More…

Detailed Description

Implementation of detector model.

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

Source code


#include "DetectorModel.hpp"
#include "core/module/exceptions.h"

#include "core/geometry/HexagonalPixelDetectorModel.hpp"
#include "core/geometry/PixelDetectorModel.hpp"
#include "core/geometry/RadialStripDetectorModel.hpp"
#include "tools/liang_barsky.h"

#include <Math/Translation3D.h>

using namespace allpix;

std::shared_ptr<DetectorModel> DetectorModel::factory(const std::string& name, const ConfigReader& reader) {
    Configuration config = reader.getHeaderConfiguration();

    // Sensor geometry
    // FIXME we might want to deprecate this default at some point?
    if(!config.has("geometry")) {
        LOG(WARNING) << "Model file " << config.getFilePath() << " does not provide a geometry parameter, using default";
    }
    auto geometry = config.get<std::string>("geometry", "pixel");

    // Assembly type
    if(!config.has("type")) {
        LOG(FATAL) << "Model file " << config.getFilePath() << " does not provide a type parameter";
    }
    auto type = config.get<std::string>("type");

    std::shared_ptr<DetectorAssembly> assembly;
    if(type == "hybrid") {
        assembly = std::make_shared<HybridAssembly>(config);
    } else if(type == "monolithic") {
        assembly = std::make_shared<MonolithicAssembly>(config);
    } else {
        LOG(FATAL) << "Model file " << config.getFilePath() << " type parameter is not valid";
        throw InvalidValueError(config, "type", "model type is not supported");
    }

    // Instantiate the correct detector model
    std::shared_ptr<DetectorModel> model;
    if(geometry == "pixel") {
        model = std::make_shared<PixelDetectorModel>(name, assembly, reader, config);
    } else if(geometry == "radial_strip") {
        model = std::make_shared<RadialStripDetectorModel>(name, assembly, reader, config);
    } else if(geometry == "hexagonal") {
        model = std::make_shared<HexagonalPixelDetectorModel>(name, assembly, reader, config);
    } else {
        LOG(FATAL) << "Model file " << config.getFilePath() << " geometry parameter is not valid";
        // FIXME: The model can probably be silently ignored if we have more model readers later
        throw InvalidValueError(config, "geometry", "model geometry is not supported");
    }

    // Validate the detector model - we call this here because validation might depend on derived class properties:
    model->validate();

    auto unused_keys = config.getUnusedKeys();
    if(!unused_keys.empty()) {
        std::stringstream st;
        st << "Unused configuration keys in global section of sensor geometry definition:";
        for(auto& key : unused_keys) {
            st << std::endl << key;
        }
        LOG(WARNING) << st.str();
    }

    return model;
}

DetectorModel::DetectorModel(std::string type,
                             std::shared_ptr<DetectorAssembly> assembly,
                             const ConfigReader& reader, // NOLINT
                             const Configuration& config)
    : type_(std::move(type)), assembly_(std::move(assembly)), reader_(reader) {
    using namespace ROOT::Math;

    // Sensor thickness
    setSensorThickness(config.get<double>("sensor_thickness"));

    // Excess around the sensor from the pixel grid
    auto default_sensor_excess = config.get<double>("sensor_excess", 0);
    setSensorExcessTop(config.get<double>("sensor_excess_top", default_sensor_excess));
    setSensorExcessBottom(config.get<double>("sensor_excess_bottom", default_sensor_excess));
    setSensorExcessLeft(config.get<double>("sensor_excess_left", default_sensor_excess));
    setSensorExcessRight(config.get<double>("sensor_excess_right", default_sensor_excess));

    // Sensor material:
    sensor_material_ = config.get<SensorMaterial>("sensor_material", SensorMaterial::SILICON);

    // Issue a warning for pre-3.0 implant definitions:
    if(config.has("implant_size")) {
        LOG(WARNING) << "Parameter \"implant_size\" of model " << config.getFilePath() << " not supported," << std::endl
                     << "Individual [implant] sections must be used for implant definitions";
    }

    // Read implants
    for(auto& implant_config : reader_.getConfigurations("implant")) {
        auto imtype = implant_config.get<Implant::Type>("type");
        auto shape = implant_config.get<Implant::Shape>("shape", Implant::Shape::RECTANGLE);
        auto size = implant_config.get<XYZVector>("size");
        auto offset = implant_config.get<XYVector>("offset", {0, 0});
        auto orientation = implant_config.get<double>("orientation", 0.);

        addImplant(imtype, shape, std::move(size), offset, orientation, implant_config);

        auto unused_keys = implant_config.getUnusedKeys();
        if(!unused_keys.empty()) {
            std::stringstream st;
            st << "Unused configuration keys in [implant] section of sensor geometry definition:";
            for(auto& key : unused_keys) {
                st << std::endl << key;
            }
            LOG(WARNING) << st.str();
        }
    }

    // Read support layers
    LOG(DEBUG) << "Number of [support] sections: " << reader_.getConfigurations("support").size();
    for(auto& support_config : reader_.getConfigurations("support")) {
        auto thickness = support_config.get<double>("thickness");
        auto size = support_config.get<XYVector>("size");
        auto location = support_config.get<SupportLayer::Location>("location", SupportLayer::Location::CHIP);

        XYZVector offset;
        if(location == SupportLayer::Location::ABSOLUTE) {
            offset = support_config.get<XYZVector>("offset");
        } else {
            auto xy_offset = support_config.get<XYVector>("offset", {0, 0});
            offset = XYZVector(xy_offset.x(), xy_offset.y(), 0);
        }

        auto material = support_config.get<std::string>("material", "g10");
        auto hole_type = support_config.get<std::string>("hole_type", "rectangular");
        std::transform(hole_type.begin(), hole_type.end(), hole_type.begin(), ::tolower);
        auto hole_size = support_config.get<XYVector>("hole_size", {0, 0});
        auto hole_offset = support_config.get<XYVector>("hole_offset", {0, 0});
        addSupportLayer(size,
                        thickness,
                        std::move(offset),
                        std::move(material),
                        std::move(hole_type),
                        location,
                        hole_size,
                        std::move(hole_offset));

        auto unused_keys = support_config.getUnusedKeys();
        if(!unused_keys.empty()) {
            std::stringstream st;
            st << "Unused configuration keys in [support] section of sensor geometry definition:";
            for(auto& key : unused_keys) {
                st << std::endl << key;
            }
            LOG(WARNING) << st.str();
        }
    }
}

void DetectorModel::addImplant(const Implant::Type& type,
                               const Implant::Shape& shape,
                               ROOT::Math::XYZVector size,
                               const ROOT::Math::XYVector& offset,
                               double orientation,
                               const Configuration& config) {
    // Calculate offset from sensor center - sign of the shift depends on whether it's on front- or backside:
    auto offset_z = (getSensorSize().z() - size.z()) / 2. * (type == Implant::Type::FRONTSIDE ? 1 : -1);
    ROOT::Math::XYZVector full_offset(offset.x(), offset.y(), offset_z);
    implants_.push_back(
        Implant(type, shape, std::move(size), std::move(full_offset), ROOT::Math::RotationZ(orientation), config));
}

void DetectorModel::validate() {
    // FIXME at some point we might make this a requirement and throw an exception instead?
    LOG(WARNING) << "No validation implemented for this detector geometry";
}

bool DetectorModel::Implant::contains(const ROOT::Math::XYZVector& position) const {
    // Shift position to implant coordinate system and apply rotation around z axis:
    auto pos = orientation_(position - offset_);

    // Check z-position to be within implant
    if(std::fabs(pos.z()) >= size_.z() / 2) {
        return false;
    }

    if(shape_ == Implant::Shape::RECTANGLE) {
        // Check if point is within rectangle with side lengths size_
        if(std::fabs(pos.x()) <= size_.x() / 2 && std::fabs(pos.y()) <= size_.y() / 2) {
            return true;
        }
    } else if(shape_ == Implant::Shape::ELLIPSE) {
        // Check if point is within ellipsis with major axis size_
        if(pos.x() * pos.x() / (size_.x() * size_.x() / 4) + pos.y() * pos.y() / (size_.y() * size_.y() / 4) <= 1) {
            return true;
        }
    }
    return false;
}

ROOT::Math::XYZPoint DetectorModel::getModelCenter() const {

    // Prepare detector assembly stack (sensor, chip, supports) with z-positions and thicknesses:
    std::vector<std::pair<double, double>> stack = {{getSensorCenter().z(), getSensorSize().z()},
                                                    {getChipCenter().z(), getChipSize().z()}};
    for(auto& support_layer : getSupportLayers()) {
        stack.emplace_back(support_layer.getCenter().z(), support_layer.getSize().z());
    }

    // Find first and last element of detector assembly stack:
    auto boundaries = std::minmax_element(
        stack.begin(), stack.end(), [](std::pair<double, double> const& s1, std::pair<double, double> const& s2) {
            return s1.first < s2.first;
        });

    auto element_first = *boundaries.first;
    auto element_last = *boundaries.second;

    // Calculate geometrical center as mid-point between boundaries (first element minus half thickness, last element plus
    // half thickness)
    auto center =
        ((element_first.first - element_first.second / 2.0) + (element_last.first + element_last.second / 2.0)) / 2.0;
    return {getMatrixCenter().x(), getMatrixCenter().y(), center};
}

std::vector<Configuration> DetectorModel::getConfigurations() const {
    std::vector<Configuration> configurations;
    // Initialize global base configuration
    auto global_config_ = reader_.getHeaderConfiguration();

    for(auto& config : reader_.getConfigurations()) {
        if(config.getName().empty()) {
            // Merge all global sections with the global config
            global_config_.merge(config);
        } else {
            // Store all others
            configurations.push_back(config);
        }
    }

    // Prepend global config and return vector:
    configurations.insert(configurations.begin(), global_config_);
    return configurations;
}

ROOT::Math::XYZVector DetectorModel::getSize() const {
    ROOT::Math::XYZVector max(
        std::numeric_limits<double>::lowest(), std::numeric_limits<double>::lowest(), std::numeric_limits<double>::lowest());
    ROOT::Math::XYZVector min(
        std::numeric_limits<double>::max(), std::numeric_limits<double>::max(), std::numeric_limits<double>::max());

    std::array<ROOT::Math::XYZPoint, 2> centers = {{getSensorCenter(), getChipCenter()}};
    std::array<ROOT::Math::XYZVector, 2> sizes = {{getSensorSize(), getChipSize()}};

    for(size_t i = 0; i < 2; ++i) {
        max.SetX(std::max(max.x(), (centers.at(i) + sizes.at(i) / 2.0).x()));
        max.SetY(std::max(max.y(), (centers.at(i) + sizes.at(i) / 2.0).y()));
        max.SetZ(std::max(max.z(), (centers.at(i) + sizes.at(i) / 2.0).z()));
        min.SetX(std::min(min.x(), (centers.at(i) - sizes.at(i) / 2.0).x()));
        min.SetY(std::min(min.y(), (centers.at(i) - sizes.at(i) / 2.0).y()));
        min.SetZ(std::min(min.z(), (centers.at(i) - sizes.at(i) / 2.0).z()));
    }

    for(auto& support_layer : getSupportLayers()) {
        const auto& size = support_layer.getSize();
        const auto& center = support_layer.getCenter();
        max.SetX(std::max(max.x(), (center + size / 2.0).x()));
        max.SetY(std::max(max.y(), (center + size / 2.0).y()));
        max.SetZ(std::max(max.z(), (center + size / 2.0).z()));
        min.SetX(std::min(min.x(), (center - size / 2.0).x()));
        min.SetY(std::min(min.y(), (center - size / 2.0).y()));
        min.SetZ(std::min(min.z(), (center - size / 2.0).z()));
    }

    ROOT::Math::XYZVector size;
    size.SetX(2 * std::max(max.x() - getMatrixCenter().x(), getMatrixCenter().x() - min.x()));
    size.SetY(2 * std::max(max.y() - getMatrixCenter().y(), getMatrixCenter().y() - min.y()));
    size.SetZ((max.z() - getMatrixCenter().z()) +
              (getMatrixCenter().z() - min.z())); // max.z() is positive (chip side) and min.z() is negative (sensor side)

    // FIXME need a better solution than this!
    auto assembly = std::dynamic_pointer_cast<HybridAssembly>(getAssembly());
    if(assembly != nullptr) {
        auto bump_grid = getSensorSize() + 2 * ROOT::Math::XYZVector(std::fabs(assembly->getBumpsOffset().x()),
                                                                     std::fabs(assembly->getBumpsOffset().y()),
                                                                     0);

        // Extend size unless it's already large enough to cover shifted bump bond grid:
        return {std::max(size.x(), bump_grid.x()), std::max(size.y(), bump_grid.y()), std::max(size.z(), bump_grid.z())};
    }
    return size;
}

std::vector<SupportLayer> DetectorModel::getSupportLayers() const {
    auto ret_layers = support_layers_;

    auto sensor_offset = -getSensorSize().z() / 2.0;
    auto chip_offset = getSensorSize().z() / 2.0 + getChipSize().z() + assembly_->getChipOffset().z();
    for(auto& layer : ret_layers) {
        ROOT::Math::XYZVector offset = layer.offset_;
        if(layer.location_ == SupportLayer::Location::SENSOR) {
            offset.SetZ(sensor_offset - layer.size_.z() / 2.0);
            sensor_offset -= layer.size_.z();
        } else if(layer.location_ == SupportLayer::Location::CHIP) {
            offset.SetZ(chip_offset + layer.size_.z() / 2.0);
            chip_offset += layer.size_.z();
        }

        layer.center_ = getMatrixCenter() + offset;
    }

    return ret_layers;
}

std::optional<DetectorModel::Implant> DetectorModel::isWithinImplant(const ROOT::Math::XYZPoint& local_pos) const {

    // Bail out if we have no implants - no need to transform coordinates:
    if(implants_.empty()) {
        return std::nullopt;
    }

    auto [xpixel, ypixel] = getPixelIndex(local_pos);
    auto inPixelPos = local_pos - getPixelCenter(xpixel, ypixel);

    for(const auto& implant : implants_) {
        if(implant.contains(inPixelPos)) {
            return implant;
        }
    }
    return std::nullopt;
}

ROOT::Math::XYZPoint DetectorModel::getImplantIntercept(const Implant& implant,
                                                        const ROOT::Math::XYZPoint& outside,
                                                        const ROOT::Math::XYZPoint& inside) const {
    // Get direction vector of motion *into* implant
    auto direction = (inside - outside).Unit();
    // Get positions relative to pixel center:
    auto [xpixel_in, ypixel_in] = getPixelIndex(inside);
    auto transl = ROOT::Math::Translation3D(static_cast<ROOT::Math::XYZVector>(getPixelCenter(xpixel_in, ypixel_in)));

    // Call implant intersection and re-transform back to local coordinates:
    auto intercept = implant.intersect(direction, transl.Inverse()(outside));
    if(!intercept.has_value()) {
        return inside;
    }
    return transl(intercept.value());
}

std::optional<ROOT::Math::XYZPoint> DetectorModel::Implant::intersect(const ROOT::Math::XYZVector& direction,
                                                                      const ROOT::Math::XYZPoint& position) const {
    // Shift position to implant coordinate system and apply rotation around z axis!
    if(shape_ == Implant::Shape::RECTANGLE) {
        // Use Liang-Barsky line clipping method:
        auto intercept = LiangBarsky::closestIntersection(orientation_(direction), orientation_(position - offset_), size_);
        if(intercept.has_value()) {
            // Translate back into local coordinates of the sensor:
            intercept = orientation_.Inverse()(intercept.value()) + offset_;
        }
        return intercept;
    } else if(shape_ == Implant::Shape::ELLIPSE) {
        // Translate so the ellipse is centered at the origin.
        auto pos = orientation_(position - offset_);
        auto dir = orientation_(direction);

        // Normal vector for top and bottom faces of cylinder
        const auto norm = ROOT::Math::XYZVector(0, 0, 1);

        auto intersection_caps = [&](const ROOT::Math::XYZVector& p, const ROOT::Math::XYZVector& d) {
            auto d1 =
                (ROOT::Math::XYZVector(0, 0, size_.z() / 2) - static_cast<ROOT::Math::XYZVector>(p)).Dot(norm) / d.Dot(norm);
            auto d2 = (ROOT::Math::XYZVector(0, 0, -size_.z() / 2) - static_cast<ROOT::Math::XYZVector>(p)).Dot(norm) /
                      d.Dot(norm);
            // Return the smaller d value, closer to the reference point
            return std::min(d1, d2);
        };

        // Calculate quadratic parameters with semimajor/minor axes (half-diameter) and discriminant.
        double A = 4 * dir.x() * dir.x() / size_.x() / size_.x() + 4 * dir.y() * dir.y() / size_.y() / size_.y();
        double B = 8 * pos.x() * dir.x() / size_.x() / size_.x() + 8 * pos.y() * dir.y() / size_.y() / size_.y();
        double C = 4 * pos.x() * pos.x() / size_.x() / size_.x() + 4 * pos.y() * pos.y() / size_.y() / size_.y() - 1;
        auto discriminant = B * B - 4 * A * C;

        // No intersection with negative discriminant:
        if(discriminant < 0) {
            return std::nullopt;
        }

        auto t1 = (-B - std::sqrt(discriminant));
        auto t2 = (-B + std::sqrt(discriminant));

        // Two intersections and both are in direction of motion
        if(discriminant > 0 && t1 > 0 && t2 > 0) {
            auto intersect = pos + dir * std::min(t1, t2) / 2 / A;
            // Closer solution hits cylinder wall, return as intersection
            if(std::fabs(intersect.z()) < size_.z() / 2) {
                return orientation_.Inverse()(intersect) + offset_;
            }
        }

        // Only one solution - either discriminant is 0 or one solution is in negative direction of motion.
        // We only check for cylinder cap intersections here, pure contact solutions (discr = 0) are ignored
        auto t = intersection_caps(static_cast<ROOT::Math::XYZVector>(pos), dir);
        auto intersection = pos + dir * t;

        // Check if solution found is within cylinder area, i.e. end cap:
        if(intersection.x() * intersection.x() / (size_.x() * size_.x() / 4) +
               intersection.y() * intersection.y() / (size_.y() * size_.y() / 4) <=
           1) {
            return orientation_.Inverse()(intersection) + offset_;
        }

        // No intersection or only contact point found.
        return std::nullopt;
    }
    throw std::invalid_argument("Intersection not implemented for this implant type");
}

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