src/modules/WeightingPotentialReader/WeightingPotentialReaderModule.cpp
Implementation of module to read weighting fields. More…
Detailed Description
Implementation of module to read weighting fields.
Copyright: Copyright (c) 2018-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 "WeightingPotentialReaderModule.hpp"
#include <cmath>
#include <fstream>
#include <limits>
#include <memory>
#include <new>
#include <stdexcept>
#include <string>
#include <utility>
#include <TH2F.h>
#include "core/config/exceptions.h"
#include "core/geometry/DetectorModel.hpp"
#include "core/utils/log.h"
#include "core/utils/unit.h"
using namespace allpix;
WeightingPotentialReaderModule::WeightingPotentialReaderModule(Configuration& config,
Messenger*,
std::shared_ptr<Detector> detector)
: Module(config, detector), detector_(std::move(detector)) {
// Enable multithreading of this module if multithreading is enabled
allow_multithreading();
}
void WeightingPotentialReaderModule::initialize() {
auto field_model = config_.get<WeightingPotential>("model");
// Calculate thickness domain
auto model = detector_->getModel();
auto potential_depth = config_.get<double>("potential_depth", model->getSensorSize().z());
if(potential_depth - model->getSensorSize().z() > std::numeric_limits<double>::epsilon()) {
throw InvalidValueError(
config_, "potential_depth", "Weighting potential depth can not be larger than the sensor thickness");
}
auto sensor_max_z = model->getSensorCenter().z() + model->getSensorSize().z() / 2.0;
auto thickness_domain = std::make_pair(sensor_max_z - potential_depth, sensor_max_z);
// Calculate the potential depending on the configuration
if(field_model == WeightingPotential::MESH) {
// Read field mapping from configuration
auto field_mapping = config_.get<FieldMapping>("field_mapping");
// SENSOR style mapping does not work for Weighting potentials, we always need to center on an electrode:
if(field_mapping == FieldMapping::SENSOR) {
throw InvalidValueError(
config_, "field_mapping", "the weighting potential needs to be centered around an electrode");
}
LOG(DEBUG) << "Weighting potential maps to " << magic_enum::enum_name(field_mapping);
auto field_data = read_field();
// By default, set field scale from physical extent read from field file:
std::array<double, 2> field_scale{{1.0, 1.0}};
// Read the field scales from the configuration if the key is set:
if(config_.has("field_scale")) {
auto scales = config_.get<ROOT::Math::XYVector>("field_scale", {1.0, 1.0});
// FIXME Add sanity checks for scales here
LOG(DEBUG) << "Weighting potential will be scaled with factors " << scales;
field_scale = {{scales.x(), scales.y()}};
}
// Set the field grid, provide scale factors as fraction of the pixel pitch for correct scaling:
detector_->setWeightingPotentialGrid(field_data.getData(),
field_data.getDimensions(),
field_data.getSize(),
field_mapping,
field_scale,
{0.0, 0.0},
thickness_domain);
} else if(field_model == WeightingPotential::PAD) {
LOG(TRACE) << "Adding weighting potential from pad in plane condenser";
// Get pixel implant size from the detector model:
auto implants = model->getImplants();
if(implants.size() > 1) {
throw ModuleError("Detector model contains more than one implant, not supported for pad potential");
}
auto implant = (implants.empty() ? ROOT::Math::XYZVector(model->getPixelSize().x(), model->getPixelSize().y(), 0)
: implants.front().getSize());
// This module currently only works with pad definition, i.e. 2D implant definition:
if(implant.z() > std::numeric_limits<double>::epsilon()) {
throw InvalidValueError(
config_, "model", "model 'pad' can only be used with 2D implants, but non-zero thickness found");
}
auto function = get_pad_potential_function({implant.x(), implant.y()}, thickness_domain);
detector_->setWeightingPotentialFunction(function, thickness_domain, FieldType::CUSTOM);
}
// Produce histograms if needed
if(config_.get<bool>("output_plots", false)) {
create_output_plots();
}
}
FieldFunction<double>
WeightingPotentialReaderModule::get_pad_potential_function(const ROOT::Math::XYVector& implant,
std::pair<double, double> thickness_domain) {
LOG(TRACE) << "Calculating function for the plane condenser weighting potential." << std::endl;
return [implant, thickness_domain](const ROOT::Math::XYZPoint& pos) {
// Calculate values of the "f" function
auto f = [implant](double x, double y, double u) {
// Calculate arctan fractions
auto arctan = [](double a, double b, double c) {
return std::atan(a * b / c / std::sqrt(a * a + b * b + c * c));
};
// Shift the x and y coordinates by plus/minus half the implant size:
double x1 = x - implant.x() / 2;
double x2 = x + implant.x() / 2;
double y1 = y - implant.y() / 2;
double y2 = y + implant.y() / 2;
// Calculate arctan sum and return
return arctan(x1, y1, u) + arctan(x2, y2, u) - arctan(x1, y2, u) - arctan(x2, y1, u);
};
// Transform into coordinate system with sensor between d/2 < z < -d/2:
auto d = thickness_domain.second - thickness_domain.first;
auto local_z = -pos.z() + thickness_domain.second;
// Calculate the series expansion
double sum = 0;
for(int n = 1; n <= 100; n++) {
sum += f(pos.x(), pos.y(), 2 * n * d - local_z) - f(pos.x(), pos.y(), 2 * n * d + local_z);
}
return (1 / (2 * M_PI) * (f(pos.x(), pos.y(), local_z) - sum));
};
}
void WeightingPotentialReaderModule::create_output_plots() {
LOG(TRACE) << "Creating output plots";
auto model = detector_->getModel();
auto center = model->getPixelCenter(1, 1);
auto size =
ROOT::Math::XYZVector(3 * model->getPixelSize().x(), 3 * model->getPixelSize().y(), model->getSensorSize().z());
auto position = config_.get<ROOT::Math::XYPoint>("output_plots_position", {center.x(), center.y()});
auto steps = config_.get<size_t>("output_plots_steps", 500);
double x_min = center.x() - size.x() / 2.0;
double x_max = center.x() + size.x() / 2.0;
double y_min = center.y() - size.y() / 2.0;
double y_max = center.y() + size.y() / 2.0;
double z_min = center.z() - size.z() / 2.0;
double z_max = center.z() + size.z() / 2.0;
// Create 1D histograms
std::string title = "#phi_{w}/V_{w} at " + Units::display(position, {"um"}) + ";z (mm);unit potential";
auto* histogram = new TH1F("potential1d", title.c_str(), static_cast<int>(steps), z_min, z_max);
// Get the weighting potential at every index
for(size_t j = 0; j < steps; ++j) {
double z = z_min + ((static_cast<double>(j) + 0.5) / static_cast<double>(steps)) * (z_max - z_min);
auto pos = ROOT::Math::XYZPoint(position.x(), position.y(), z);
// Get potential from detector and fill the histogram
auto potential = detector_->getWeightingPotential(pos, Pixel::Index(1, 1));
histogram->Fill(z, potential);
}
auto zcut = config_.get<double>("output_plots_zcut", 0.0);
if(!model->isWithinSensor(ROOT::Math::XYZPoint(0, 0, zcut))) {
throw InvalidValueError(config_, "output_plots_zcut", "Position is outside the sensor");
}
// Create 2D histograms
auto* histogram2Dx = new TH2F("potential_x",
"#phi_{w}/V_{w} of Pixel(1,1);x (mm); z (mm); unit potential",
static_cast<int>(steps),
x_min,
x_max,
static_cast<int>(steps),
z_min,
z_max);
auto* histogram2Dy = new TH2F("potential_y",
"#phi_{w}/V_{w} of Pixel(1,1);y (mm); z (mm); unit potential",
static_cast<int>(steps),
y_min,
y_max,
static_cast<int>(steps),
z_min,
z_max);
auto* histogram2Dz = new TH2F("potential_z",
"#phi_{w}/V_{w} of Pixel(1,1);x (mm); y (mm); unit potential",
static_cast<int>(steps),
x_min,
x_max,
static_cast<int>(steps),
y_min,
y_max);
// Get the weighting potential at every index
for(size_t j = 0; j < steps; ++j) {
LOG_PROGRESS(INFO, "plotting") << "Plotting weighting potential: " << 100 * j * steps / (steps * steps) << "%";
double z = z_min + ((static_cast<double>(j) + 0.5) / static_cast<double>(steps)) * (z_max - z_min);
// Scan horizontally over three pixels (from -1.5 pitch to +1.5 pitch)
for(size_t k = 0; k < steps; ++k) {
double x =
center.x() - size.x() / 2.0 + ((static_cast<double>(k) + 0.5) / static_cast<double>(steps)) * size.x();
double y =
center.y() - size.y() / 2.0 + ((static_cast<double>(k) + 0.5) / static_cast<double>(steps)) * size.y();
// Get potential from detector and fill histogram. We calculate relative to pixel (1,1) so we need to shift:
auto potential_x = detector_->getWeightingPotential(ROOT::Math::XYZPoint(x, center.y(), z), Pixel::Index(1, 1));
auto potential_y = detector_->getWeightingPotential(ROOT::Math::XYZPoint(center.x(), y, z), Pixel::Index(1, 1));
histogram2Dx->Fill(x, z, potential_x);
histogram2Dy->Fill(y, z, potential_y);
}
}
for(size_t j = 0; j < steps; ++j) {
LOG_PROGRESS(INFO, "plotting") << "Plotting weighting potential: " << 100 * j * steps / (steps * steps) << "%";
double x = center.x() - size.x() / 2.0 + ((static_cast<double>(j) + 0.5) / static_cast<double>(steps)) * size.x();
// Scan horizontally over three pixels (from -1.5 pitch to +1.5 pitch)
for(size_t k = 0; k < steps; ++k) {
double y =
center.y() - size.y() / 2.0 + ((static_cast<double>(k) + 0.5) / static_cast<double>(steps)) * size.y();
auto potential_z = detector_->getWeightingPotential(ROOT::Math::XYZPoint(x, y, zcut), Pixel::Index(1, 1));
histogram2Dz->Fill(x, y, potential_z);
}
}
LOG_PROGRESS(INFO, "plotting") << "Plotting weighting potential: done ";
histogram->SetOption("hist");
histogram2Dx->SetOption("colz");
histogram2Dy->SetOption("colz");
histogram2Dz->SetOption("colz");
// Write the histogram to module file
histogram->Write();
histogram2Dx->Write();
histogram2Dy->Write();
histogram2Dz->Write();
}
FieldParser<double> WeightingPotentialReaderModule::field_parser_(FieldQuantity::SCALAR);
FieldData<double> WeightingPotentialReaderModule::read_field() {
using namespace ROOT::Math;
try {
LOG(TRACE) << "Fetching weighting potential from init file";
// Get field from file
auto field_data = field_parser_.getByFileName(config_.getPath("file_name", true));
// Check maximum/minimum values of the potential:
auto elements = std::minmax_element(field_data.getData()->begin(), field_data.getData()->end());
if(*elements.first < 0 || *elements.second > 1) {
throw InvalidValueError(config_,
"file_name",
"Unphysical weighting potential detected, found " + std::to_string(*elements.first) +
" < phi < " + std::to_string(*elements.second) + ", expected 0 < phi < 1");
}
// Check that we actually have a three-dimensional potential field, otherwise we get very unphysical results in
// neighboring pixels along the "missing" dimension:
if(field_data.getDimensionality() < 3) {
// check if wrong dimensionality should be ignored
if(config_.get<bool>("ignore_field_dimensions", false)) {
LOG(WARNING) << "Weighting potential with " << std::to_string(field_data.getDimensionality())
<< " dimensions detected, requiring three-dimensional scalar field - this might lead to "
"unexpected behavior.";
} else {
throw InvalidValueError(config_,
"file_name",
"Weighting potential with " + std::to_string(field_data.getDimensionality()) +
" dimensions detected, requiring three-dimensional scalar field - this might "
"lead to unexpected behavior.");
}
}
LOG(INFO) << "Set weighting field with " << field_data.getDimensions()[0] << "x" << field_data.getDimensions()[1]
<< "x" << field_data.getDimensions()[2] << " cells";
// Return the field data
return field_data;
} catch(std::invalid_argument& e) {
throw InvalidValueError(config_, "file_name", e.what());
} catch(std::runtime_error& e) {
throw InvalidValueError(config_, "file_name", e.what());
} catch(std::bad_alloc& e) {
throw InvalidValueError(config_, "file_name", "file too large");
}
}
Updated on 2024-12-13 at 08:31:37 +0000