src/modules/ElectricFieldReader/ElectricFieldReaderModule.cpp
Implementation of module to read electric fields. More…
Detailed Description
Implementation of module to read electric fields.
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 "ElectricFieldReaderModule.hpp"
#include <fstream>
#include <limits>
#include <memory>
#include <new>
#include <stdexcept>
#include <string>
#include <utility>
#include <Math/Vector3D.h>
#include <TFormula.h>
#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;
ElectricFieldReaderModule::ElectricFieldReaderModule(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();
// Set default units for interpreting input field files in:
config_.setDefault("file_units", "V/cm");
// NOTE use voltage as a synonym for bias voltage
config_.setAlias("bias_voltage", "voltage");
// NOTE use field_depth as a synonym for depletion_depth
config_.setAlias("depletion_depth", "field_depth");
}
void ElectricFieldReaderModule::initialize() {
// Check field strength
auto field_model = config_.get<ElectricField>("model");
if((field_model == ElectricField::CONSTANT || field_model == ElectricField::LINEAR) &&
config_.get<double>("bias_voltage") > Units::get(5.0, "kV")) {
LOG(WARNING) << "Very high bias voltage of " << Units::display(config_.get<double>("bias_voltage"), "kV")
<< " set, this is most likely not desired.";
}
// Check we don't have both depletion depth and depletion voltage:
if(config_.count({"depletion_voltage", "depletion_depth"}) > 1) {
throw InvalidCombinationError(
config_, {"depletion_voltage", "depletion_depth"}, "Depletion voltage and depth are mutually exclusive.");
}
// Set depletion depth to full sensor:
auto model = detector_->getModel();
auto depletion_depth = config_.get<double>("depletion_depth", model->getSensorSize().z());
if(depletion_depth - model->getSensorSize().z() > 1e-9) {
throw InvalidValueError(config_, "depletion_depth", "depletion depth can not be larger than the sensor thickness");
}
// Calculate thickness domain
auto sensor_max_z = model->getSensorCenter().z() + model->getSensorSize().z() / 2.0;
auto thickness_domain = std::make_pair(sensor_max_z - depletion_depth, sensor_max_z);
// Calculate the field depending on the configuration
if(field_model == ElectricField::MESH) {
// Read field mapping from configuration
auto field_mapping = config_.get<FieldMapping>("field_mapping");
LOG(DEBUG) << "Electric field 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) << "Electric field will be scaled with factors " << scales;
field_scale = {{scales.x(), scales.y()}};
}
// Get the field offset in fractions of the field size, default is 0.0x0.0, i.e. no offset
auto offset = config_.get<ROOT::Math::XYVector>("field_offset", {0.0, 0.0});
if(offset.x() > 1.0 || offset.y() > 1.0) {
throw InvalidValueError(
config_, "field_offset", "shifting electric field by more than one pixel (offset > 1.0) is not allowed");
}
if(offset.x() < 0.0 || offset.y() < 0.0) {
throw InvalidValueError(config_, "field_offset", "offsets for the electric field have to be positive");
}
LOG(DEBUG) << "Electric field has offset of " << offset << " fractions of the field size";
detector_->setElectricFieldGrid(field_data.getData(),
field_data.getDimensions(),
field_data.getSize(),
field_mapping,
field_scale,
{{offset.x(), offset.y()}},
thickness_domain);
} else if(field_model == ElectricField::CONSTANT) {
LOG(TRACE) << "Adding constant electric field";
auto field_z = config_.get<double>("bias_voltage") / getDetector()->getModel()->getSensorSize().z();
LOG(INFO) << "Set constant electric field with magnitude " << Units::display(field_z, {"V/um", "V/mm"});
FieldFunction<ROOT::Math::XYZVector> function = [field_z](const ROOT::Math::XYZPoint&) {
return ROOT::Math::XYZVector(0, 0, field_z);
};
detector_->setElectricFieldFunction(std::move(function), thickness_domain, FieldType::CONSTANT);
} else if(field_model == ElectricField::LINEAR) {
LOG(TRACE) << "Adding linear electric field";
// Get depletion voltage, defaults to bias voltage:
auto depletion_voltage = config_.get<double>("depletion_voltage", config_.get<double>("bias_voltage"));
LOG(INFO) << "Setting linear electric field from " << Units::display(config_.get<double>("bias_voltage"), "V")
<< " bias voltage and " << Units::display(depletion_voltage, "V") << " depletion voltage";
detector_->setElectricFieldFunction(
get_linear_field_function(depletion_voltage, thickness_domain), thickness_domain, FieldType::LINEAR);
} else if(field_model == ElectricField::PARABOLIC) {
LOG(TRACE) << "Adding parabolic electric field";
LOG(INFO) << "Setting parabolic electric field with minimum field "
<< Units::display(config_.get<double>("minimum_field"), "V/cm") << " at position "
<< Units::display(config_.get<double>("minimum_position"), {"um", "mm"}) << " and maximum field "
<< Units::display(config_.get<double>("maximum_field"), "V/cm") << " at electrode";
detector_->setElectricFieldFunction(
get_parabolic_field_function(thickness_domain), thickness_domain, FieldType::CUSTOM1D);
} else if(field_model == ElectricField::CUSTOM) {
LOG(TRACE) << "Adding custom electric field";
auto [field_function, field_type] = get_custom_field_function();
detector_->setElectricFieldFunction(field_function, thickness_domain, field_type);
}
// Produce histograms if needed
if(config_.get<bool>("output_plots", false)) {
create_output_plots();
}
}
FieldFunction<ROOT::Math::XYZVector>
ElectricFieldReaderModule::get_linear_field_function(double depletion_voltage, std::pair<double, double> thickness_domain) {
LOG(TRACE) << "Calculating function for the linear electric field.";
// We always deplete from the implants:
auto bias_voltage = std::fabs(config_.get<double>("bias_voltage"));
depletion_voltage = std::fabs(depletion_voltage);
// But the direction of the field depends on the applied voltage:
auto direction = std::signbit(config_.get<double>("bias_voltage"));
auto deplete_from_implants = config_.get<bool>("deplete_from_implants", true);
double eff_thickness = thickness_domain.second - thickness_domain.first;
// Reduce the effective thickness of the sensor if voltage is below full depletion
if(bias_voltage < depletion_voltage) {
eff_thickness *= sqrt(bias_voltage / depletion_voltage);
depletion_voltage = bias_voltage;
}
LOG(TRACE) << "Effective thickness of the electric field: " << Units::display(eff_thickness, {"um", "mm"});
LOG(DEBUG) << "Depleting the sensor from the " << (deplete_from_implants ? "implant side." : "back side.");
return [bias_voltage, depletion_voltage, direction, eff_thickness, thickness_domain, deplete_from_implants](
const ROOT::Math::XYZPoint& pos) {
double z_rel = thickness_domain.second - pos.z();
double field_z = std::max(0.0,
(bias_voltage - depletion_voltage) / eff_thickness +
2 * (depletion_voltage / eff_thickness) *
(deplete_from_implants ? (1 - z_rel / eff_thickness) : z_rel / eff_thickness));
return ROOT::Math::XYZVector(0, 0, (direction ? -1 : 1) * field_z);
};
}
FieldFunction<ROOT::Math::XYZVector>
ElectricFieldReaderModule::get_parabolic_field_function(std::pair<double, double> thickness_domain) {
LOG(TRACE) << "Calculating function for the parabolic electric field.";
auto z_min = config_.get<double>("minimum_position");
auto e_max = config_.get<double>("maximum_field");
double eff_thickness = thickness_domain.second - thickness_domain.first;
if(z_min <= thickness_domain.first || z_min >= thickness_domain.second) {
throw InvalidValueError(config_,
"minimum_position",
"Minimum field position must be within defined region of the electric field (" +
Units::display(thickness_domain.first, "um") + "," +
Units::display(thickness_domain.second, "um") + ")");
}
auto a = (e_max - config_.get<double>("minimum_field")) /
(z_min * z_min + thickness_domain.second * thickness_domain.second - eff_thickness * z_min);
auto b = -2 * a * z_min;
auto c = e_max - a * (thickness_domain.second * thickness_domain.second - eff_thickness * z_min);
return [a, b, c](const ROOT::Math::XYZPoint& pos) {
double field_z = a * pos.z() * pos.z() + b * pos.z() + c;
return ROOT::Math::XYZVector(0, 0, field_z);
};
}
std::pair<FieldFunction<ROOT::Math::XYZVector>, FieldType> ElectricFieldReaderModule::get_custom_field_function() {
auto field_functions = config_.getArray<std::string>("field_function");
auto field_parameters = config_.getArray<double>("field_parameters");
// 1D field, interpret as field along z-axis:
if(field_functions.size() == 1) {
LOG(DEBUG) << "Found definition of 1D custom field, applying to z axis";
auto z = std::make_shared<TFormula>("ez", field_functions.front().c_str(), false);
// Check if number of parameters match up
if(static_cast<size_t>(z->GetNpar()) != field_parameters.size()) {
throw InvalidValueError(
config_,
"field_parameters",
"The number of function parameters does not line up with the amount of parameters in the function.");
}
// Apply parameters to the function
for(size_t n = 0; n < field_parameters.size(); ++n) {
z->SetParameter(static_cast<int>(n), field_parameters.at(n));
}
LOG(DEBUG) << "Value of custom field at pixel center: " << Units::display(z->Eval(0., 0., 0.), "V/cm");
return {[z = std::move(z)](const ROOT::Math::XYZPoint& pos) {
return ROOT::Math::XYZVector(0, 0, z->Eval(pos.x(), pos.y(), pos.z()));
},
FieldType::CUSTOM1D};
} else if(field_functions.size() == 3) {
LOG(DEBUG) << "Found definition of 3D custom field, applying to three Cartesian axes";
auto x = std::make_shared<TFormula>("ex", field_functions.at(0).c_str(), false);
auto y = std::make_shared<TFormula>("ey", field_functions.at(1).c_str(), false);
auto z = std::make_shared<TFormula>("ez", field_functions.at(2).c_str(), false);
// Check if number of parameters match up
if(static_cast<size_t>(x->GetNpar() + y->GetNpar() + z->GetNpar()) != field_parameters.size()) {
throw InvalidValueError(
config_,
"field_parameters",
"The number of function parameters does not line up with the sum of parameters in all functions.");
}
// Apply parameters to the functions
for(auto n = 0; n < x->GetNpar(); ++n) {
x->SetParameter(n, field_parameters.at(static_cast<size_t>(n)));
}
for(auto n = 0; n < y->GetNpar(); ++n) {
y->SetParameter(n, field_parameters.at(static_cast<size_t>(n + x->GetNpar())));
}
for(auto n = 0; n < z->GetNpar(); ++n) {
z->SetParameter(n, field_parameters.at(static_cast<size_t>(n + x->GetNpar() + y->GetNpar())));
}
LOG(DEBUG) << "Value of custom field at pixel center: "
<< Units::display(ROOT::Math::XYZVector(x->Eval(0., 0., 0.), y->Eval(0., 0., 0.), z->Eval(0., 0., 0.)),
{"V/cm"});
return {[x = std::move(x), y = std::move(y), z = std::move(z)](const ROOT::Math::XYZPoint& pos) {
return ROOT::Math::XYZVector(x->Eval(pos.x(), pos.y(), pos.z()),
y->Eval(pos.x(), pos.y(), pos.z()),
z->Eval(pos.x(), pos.y(), pos.z()));
},
FieldType::CUSTOM};
} else {
throw InvalidValueError(config_,
"field_function",
"field function either needs one component (z) or three components (x,y,z) but " +
std::to_string(field_functions.size()) + " were given");
}
}
FieldParser<double> ElectricFieldReaderModule::field_parser_(FieldQuantity::VECTOR);
FieldData<double> ElectricFieldReaderModule::read_field() {
try {
LOG(TRACE) << "Fetching electric field from mesh file";
// Get field from file
auto field_data =
field_parser_.getByFileName(config_.getPath("file_name", true), config_.get<std::string>("file_units"));
// Warn at field values larger than 1MV/cm / 10 MV/mm. Simple lookup per vector component, not total field magnitude
auto max_field = *std::max_element(std::begin(*field_data.getData()), std::end(*field_data.getData()));
if(max_field > 10) {
LOG(WARNING) << "Very high electric field of " << Units::display(max_field, "kV/cm")
<< ", this is most likely not desired.";
}
LOG(INFO) << "Set electric field with " << field_data.getDimensions().at(0) << "x"
<< field_data.getDimensions().at(1) << "x" << field_data.getDimensions().at(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");
}
}
void ElectricFieldReaderModule::create_output_plots() {
LOG(TRACE) << "Creating output plots";
auto steps = config_.get<size_t>("output_plots_steps", 500);
auto project = config_.get<char>("output_plots_project", 'x');
if(project != 'x' && project != 'y' && project != 'z') {
throw InvalidValueError(config_, "output_plots_project", "can only project on x, y or z axis");
}
auto model = detector_->getModel();
// If we need to plot a single pixel, we use size and position of the pixel at the origin
auto single_pixel = config_.get<bool>("output_plots_single_pixel", true);
auto center = (single_pixel ? model->getPixelCenter(0, 0) : model->getSensorCenter());
auto size =
(single_pixel
? ROOT::Math::XYZVector(model->getPixelSize().x(), model->getPixelSize().y(), model->getSensorSize().z())
: model->getSensorSize());
double z_min = center.z() - size.z() / 2.0;
double z_max = center.z() + size.z() / 2.0;
// Determine minimum and maximum index depending on projection axis
double min1 = NAN, max1 = NAN;
double min2 = NAN, max2 = NAN;
if(project == 'x') {
min1 = center.y() - size.y() / 2.0;
max1 = center.y() + size.y() / 2.0;
min2 = z_min;
max2 = z_max;
} else if(project == 'y') {
min1 = center.x() - size.x() / 2.0;
max1 = center.x() + size.x() / 2.0;
min2 = z_min;
max2 = z_max;
} else {
min1 = center.x() - size.x() / 2.0;
max1 = center.x() + size.x() / 2.0;
min2 = center.y() - size.y() / 2.0;
max2 = center.y() + size.y() / 2.0;
}
// Create 2D histograms
auto* histogram = new TH2F("field_magnitude",
"Electric field magnitude",
static_cast<int>(steps),
min1,
max1,
static_cast<int>(steps),
min2,
max2);
histogram->SetMinimum(-0.01);
histogram->SetOption("colz");
auto* histogram_x = new TH2F(
"field_x", "Electric field (x-component)", static_cast<int>(steps), min1, max1, static_cast<int>(steps), min2, max2);
auto* histogram_y = new TH2F(
"field_y", "Electric field (y-component)", static_cast<int>(steps), min1, max1, static_cast<int>(steps), min2, max2);
auto* histogram_z = new TH2F(
"field_z", "Electric field (z-component)", static_cast<int>(steps), min1, max1, static_cast<int>(steps), min2, max2);
auto* histogram_lateral = new TH2F(
"field_lateral", "Lateral electric field", static_cast<int>(steps), min1, max1, static_cast<int>(steps), min2, max2);
histogram_x->SetOption("colz");
histogram_y->SetOption("colz");
histogram_z->SetOption("colz");
histogram_lateral->SetOption("colz");
// Create 1D histogram
auto* histogram1D = new TH1F(
"field1d_z", "Electric field (z-component);z (mm);field strength (V/cm)", static_cast<int>(steps), min2, max2);
histogram1D->SetOption("hist");
// Determine the coordinate to use for projection
double x = 0, y = 0, z = 0;
if(project == 'x') {
x = center.x() - size.x() / 2.0 + config_.get<double>("output_plots_projection_percentage", 0.5) * size.x();
histogram->GetXaxis()->SetTitle("y (mm)");
histogram_x->GetXaxis()->SetTitle("y (mm)");
histogram_y->GetXaxis()->SetTitle("y (mm)");
histogram_z->GetXaxis()->SetTitle("y (mm)");
histogram_lateral->GetXaxis()->SetTitle("y (mm)");
histogram->GetYaxis()->SetTitle("z (mm)");
histogram_x->GetYaxis()->SetTitle("z (mm)");
histogram_y->GetYaxis()->SetTitle("z (mm)");
histogram_z->GetYaxis()->SetTitle("z (mm)");
histogram_lateral->GetYaxis()->SetTitle("z (mm)");
histogram->SetTitle(("Electric field magnitude at x=" + std::to_string(x) + " mm").c_str());
histogram_x->SetTitle(("Electric field (x-component) at x=" + std::to_string(x) + " mm").c_str());
histogram_y->SetTitle(("Electric field (y-component) at x=" + std::to_string(x) + " mm").c_str());
histogram_z->SetTitle(("Electric field (z-component) at x=" + std::to_string(x) + " mm").c_str());
histogram_lateral->SetTitle(("Lateral electric field at x=" + std::to_string(x) + " mm").c_str());
} else if(project == 'y') {
y = center.y() - size.y() / 2.0 + config_.get<double>("output_plots_projection_percentage", 0.5) * size.y();
histogram->GetXaxis()->SetTitle("x (mm)");
histogram_x->GetXaxis()->SetTitle("x (mm)");
histogram_y->GetXaxis()->SetTitle("x (mm)");
histogram_z->GetXaxis()->SetTitle("x (mm)");
histogram_lateral->GetXaxis()->SetTitle("x (mm)");
histogram->GetYaxis()->SetTitle("z (mm)");
histogram_x->GetYaxis()->SetTitle("z (mm)");
histogram_y->GetYaxis()->SetTitle("z (mm)");
histogram_z->GetYaxis()->SetTitle("z (mm)");
histogram_lateral->GetYaxis()->SetTitle("z (mm)");
histogram->SetTitle(("Electric field magnitude at y=" + std::to_string(y) + " mm").c_str());
histogram_x->SetTitle(("Electric field (x-component) at y=" + std::to_string(y) + " mm").c_str());
histogram_y->SetTitle(("Electric field (y-component) at y=" + std::to_string(y) + " mm").c_str());
histogram_z->SetTitle(("Electric field (z-component) at y=" + std::to_string(y) + " mm").c_str());
histogram_lateral->SetTitle(("Lateral electric field at y=" + std::to_string(y) + " mm").c_str());
} else {
z = z_min + config_.get<double>("output_plots_projection_percentage", 0.5) * size.z();
histogram->GetXaxis()->SetTitle("x (mm)");
histogram_x->GetXaxis()->SetTitle("x (mm)");
histogram_y->GetXaxis()->SetTitle("x (mm)");
histogram_z->GetXaxis()->SetTitle("x (mm)");
histogram_lateral->GetXaxis()->SetTitle("x (mm)");
histogram->GetYaxis()->SetTitle("y (mm)");
histogram_x->GetYaxis()->SetTitle("y (mm)");
histogram_y->GetYaxis()->SetTitle("y (mm)");
histogram_z->GetYaxis()->SetTitle("y (mm)");
histogram_lateral->GetYaxis()->SetTitle("y (mm)");
histogram->SetTitle(("Electric field magnitude at z=" + std::to_string(z) + " mm").c_str());
histogram_x->SetTitle(("Electric field (x-component) at z=" + std::to_string(z) + " mm").c_str());
histogram_y->SetTitle(("Electric field (y-component) at z=" + std::to_string(z) + " mm").c_str());
histogram_z->SetTitle(("Electric field (z-component) at z=" + std::to_string(z) + " mm").c_str());
histogram_lateral->SetTitle(("Lateral electric field at z=" + std::to_string(z) + " mm").c_str());
}
// set z axis tile
histogram->GetZaxis()->SetTitle("field strength (V/cm)");
histogram_x->GetZaxis()->SetTitle("field (V/cm)");
histogram_y->GetZaxis()->SetTitle("field (V/cm)");
histogram_z->GetZaxis()->SetTitle("field (V/cm)");
histogram_lateral->GetZaxis()->SetTitle("field (V/cm)");
// Find the electric field at every index, scan axes in local coordinates!
for(size_t j = 0; j < steps; ++j) {
if(project == 'x') {
y = center.y() - size.y() / 2.0 + ((static_cast<double>(j) + 0.5) / static_cast<double>(steps)) * size.y();
} else if(project == 'y') {
x = center.x() - size.x() / 2.0 + ((static_cast<double>(j) + 0.5) / static_cast<double>(steps)) * size.x();
} else {
x = center.x() - size.x() / 2.0 + ((static_cast<double>(j) + 0.5) / static_cast<double>(steps)) * size.x();
}
for(size_t k = 0; k < steps; ++k) {
if(project == 'x') {
z = z_min + ((static_cast<double>(k) + 0.5) / static_cast<double>(steps)) * size.z();
} else if(project == 'y') {
z = z_min + ((static_cast<double>(k) + 0.5) / static_cast<double>(steps)) * size.z();
} else {
y = center.y() - size.y() / 2.0 + ((static_cast<double>(k) + 0.5) / static_cast<double>(steps)) * size.y();
}
// Get field strength from detector - directly convert to double to fill root histograms
auto field = detector_->getElectricField(ROOT::Math::XYZPoint(x, y, z));
auto field_strength = static_cast<double>(Units::convert(std::sqrt(field.Mag2()), "V/cm"));
auto field_x_strength = static_cast<double>(Units::convert(field.x(), "V/cm"));
auto field_y_strength = static_cast<double>(Units::convert(field.y(), "V/cm"));
auto field_z_strength = static_cast<double>(Units::convert(field.z(), "V/cm"));
auto field_lateral_strength = sqrt(field_x_strength * field_x_strength + field_y_strength * field_y_strength);
// Fill the main histogram
if(project == 'x') {
histogram->Fill(y, z, field_strength);
histogram_x->Fill(y, z, field_x_strength);
histogram_y->Fill(y, z, field_y_strength);
histogram_z->Fill(y, z, field_z_strength);
histogram_lateral->Fill(y, z, field_lateral_strength);
} else if(project == 'y') {
histogram->Fill(x, z, field_strength);
histogram_x->Fill(x, z, field_x_strength);
histogram_y->Fill(x, z, field_y_strength);
histogram_z->Fill(x, z, field_z_strength);
histogram_lateral->Fill(x, z, field_lateral_strength);
} else {
histogram->Fill(x, y, field_strength);
histogram_x->Fill(x, y, field_x_strength);
histogram_y->Fill(x, y, field_y_strength);
histogram_z->Fill(x, y, field_z_strength);
histogram_lateral->Fill(x, y, field_lateral_strength);
}
// Fill the 1d histogram
if(j == steps / 2) {
histogram1D->Fill(z, field_z_strength);
}
}
}
// Write the histograms to module file
histogram->Write();
histogram_x->Write();
histogram_y->Write();
histogram_z->Write();
histogram_lateral->Write();
histogram1D->Write();
}
Updated on 2025-02-27 at 14:14:46 +0000