Commit 221cc732 authored by Håkan Wennlöf's avatar Håkan Wennlöf
Browse files

Merge branch 'f-spice-netlist' into 'master'

first version of the new SPICE module

See merge request allpix-squared/allpix-squared!1147
parents 86cb7aee 051b6d60
Loading
Loading
Loading
Loading
+11 −0
Original line number Diff line number Diff line
# SPDX-FileCopyrightText: 2024-2025 CERN and the Allpix Squared authors
# SPDX-License-Identifier: MIT

# Define module and return the generated name as MODULE_NAME
ALLPIX_DETECTOR_MODULE(MODULE_NAME)

# Add source files to library
ALLPIX_MODULE_SOURCES(${MODULE_NAME} NetlistWriterModule.cpp)

# Provide standard install target
ALLPIX_MODULE_INSTALL(${MODULE_NAME})
+309 −0
Original line number Diff line number Diff line
/**
 * @file
 * @brief Implementation of NetlistWriter module
 *
 * @copyright Copyright (c) 2024-2025 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
 */

#include "NetlistWriterModule.hpp"

#include <filesystem>
#include <regex>
#include <string>
#include <utility>

#include "core/utils/log.h"

#include <TFormula.h>

using namespace allpix;

NetlistWriterModule::NetlistWriterModule(Configuration& config, Messenger* messenger, std::shared_ptr<Detector> detector)
    : Module(config, detector), detector_(std::move(detector)), messenger_(messenger) {

    // Enable multithreading of this module if multithreading is enabled
    allow_multithreading();

    // Require PixelCharge message for single detector
    messenger_->bindSingle<PixelChargeMessage>(this, MsgFlags::REQUIRED);

    target_ = config.get<Target>("target");

    // Get the template netlist to modify:
    netlist_path_ = config.getPath("netlist_template", true);

    // Get the generated netlist name
    config_.setDefault<std::string>("file_name", "output_netlist_event");
    file_name_ = config.get<std::string>("file_name");

    // Get the source type, name and the circuit connected to it, as defined in the netlist
    source_type_ = config.get<SourceType>("source_type");
    source_name_ = config.get<std::string>("source_name");
    subckt_instance_name_ = config.get<std::string>("subckt_name");

    // Get the names of the common nets of the circuit the source is connected to (for ex. Vdd, etc.)
    auto com_nets = config_.getArray<std::string>("common_nets");
    common_nets_.insert(com_nets.begin(), com_nets.end());

    // Get the names of the waveforms (nets or nets) to save
    auto to_save = config_.getArray<std::string>("waveform_to_save");
    waveform_to_save_.insert(to_save.begin(), to_save.end());

    // Options to add the the uelec simulation command
    if(config_.has("simulator_command")) {
        run_netlist_simulation_ = true;
        simulator_command_ = config_.get<std::string>("simulator_command");
    }

    // Parameters for the ISOURCE_PULSE option (pulse shape)
    config_.setDefault<double>("t_delay", Units::get(0, "ns"));
    delay_ = config_.get<double>("t_delay");

    config_.setDefault<double>("t_rise", Units::get(1, "ns"));
    rise_ = config_.get<double>("t_rise");

    config_.setDefault<double>("t_fall", Units::get(1, "ns"));
    fall_ = config_.get<double>("t_fall");

    config_.setDefault<double>("t_width", Units::get(3, "ns"));
    width_ = config_.get<double>("t_width");
}

void NetlistWriterModule::initialize() {

    // Reads the template netlist specified
    std::ifstream netlist_file(netlist_path_);
    // Store the extension from the template netlist file
    extension_ = netlist_path_.extension().string();

    bool found_source = false;
    bool found_subckt = false;

    std::string line;
    size_t line_number = 0;
    while(getline(netlist_file, line)) {
        line_number++;
        // Writes the content of the netlist file to the new one
        file_lines_.push_back(line);

        // Identifies the ISOURCE declaration line
        if(line.rfind(source_name_, 0) == 0) {
            source_line_number_ = line_number;
            // regex for the ISOURCE line
            std::regex source_regex("\\((.+)\\)");
            std::smatch connection_match;
            if(std::regex_search(line, connection_match, source_regex)) {
                LOG(INFO) << "Found connections in netlist template: " << connection_match[0];
                // connections_[1] instead of connections_[0], to get back the nets without the ()
                connections_ = connection_match[1];

                // Identifies the ISOURCE instance nets names, the three following lines allow to have the two net names in
                // separate variables
                size_t space_pos = connections_.find(' ');
                source_net1_ = connections_.substr(0, space_pos);
                source_net2_ = connections_.substr(space_pos + 1);
            } else {
                throw ModuleError("Could not find net connections");
            }
            LOG(DEBUG) << "Found the source line!";
            found_source = true;
        }

        if(line.rfind(subckt_instance_name_, 0) == 0) {
            // For the subckt nets
            subckt_line_number_ = line_number;
            // regex for the circuit line
            std::regex subckt_regex("^(\\w+)\\s+\\((.+)\\)\\s+(\\w+)");
            std::smatch connection_match;
            if(std::regex_search(line, connection_match, subckt_regex)) {
                // connections_[1] instead of connections_[0], to get back the nets without the ()
                connections_ = connection_match[1];
                LOG(INFO) << "Found subckt connections in netlist template: " << connections_;
                std::istringstream iss(connection_match[2]);
                std::string net;
                // Reads each word separated by a space and adds it to net_list
                while(iss >> net) {
                    net_list_.push_back(net);
                }
                subckt_name_ = connection_match[3];
                LOG(DEBUG) << "Subckt name: " << subckt_name_ << ", nets: ";
                for(const auto& n : net_list_) {
                    LOG(DEBUG) << "\t" << n;
                }
            } else {
                throw ModuleError("Could not find net connections of the subckt");
            }
            LOG(DEBUG) << "Found the subckt line!";
            found_subckt = true;
        }
    }

    LOG(DEBUG) << "Read " << file_lines_.size() << " lines from file";

    if(!found_subckt || !found_source) {
        throw InvalidValueError(config_,
                                (found_source ? "subckt_name" : "source_name"),
                                "Could not find identifier in provided netlist template");
    }
}

void NetlistWriterModule::run(Event* event) {

    // Messages: Fetch the (previously registered) messages for this event from the messenger:
    auto message = messenger_->fetchMessage<PixelChargeMessage>(this, event);

    if(message->getData().empty()) {
        LOG(DEBUG) << "Empty event, skipping";
        return;
    }

    // Prepare output file for this event:
    const auto file_name = createOutputFile(file_name_ + "_event" + std::to_string(event->number), extension_);
    auto file = std::ofstream(file_name);
    if(!file.good()) {
        throw ModuleError("Could not create output file " + file_name);
    }
    LOG(INFO) << "Created output file at " << file_name;

    // Write the header on the new netlist
    size_t current_line = 0;
    for(; current_line < std::min(source_line_number_, subckt_line_number_) - 1; current_line++) {
        file << file_lines_[current_line] << '\n';
    }

    // waveform to be saved
    std::ostringstream to_be_saved;

    // Loop over all pixels
    for(const auto& pixel_charge : message->getData()) {
        const auto index = pixel_charge.getIndex();
        auto inputcharge = static_cast<double>(pixel_charge.getCharge());

        if(std::fabs(inputcharge) > std::numeric_limits<double>::epsilon()) {
            LOG(DEBUG) << "Received pixel " << index << ", charge " << Units::display(inputcharge, "e");

            // Get pixel address
            auto detector_model = detector_->getModel();
            auto idx = std::to_string(index.x() * static_cast<int>(detector_model->getNPixels().Y()) + index.y());

            if(target_ == Target::SPECTRE) {
                file << source_name_ << "\\<" << idx << "\\> (";
                // The source "gnd" (written "0") doesn't need to be incremented, this double "if" checks which net of the
                // source the gnd is connected to. Also a condition if none of the 2 nets are gnd
                if(source_net1_ == "0") {
                    file << source_net1_ << " " << source_net2_ << "\\<" << idx << "\\>";
                } else if(source_net2_ == "0") {
                    file << source_net1_ << "\\<" << idx << "\\> " << source_net2_;
                } else {
                    file << source_net1_ << "\\<" << idx << "\\> " << source_net2_ << "\\<" << idx << "\\>";
                }
            } else if(target_ == Target::SPICE) {
                file << "I_" << idx << " ";

                //   The source "gnd" (written "0") doesn't need to be incremented, this double "if" checks which net of the
                //   source the gnd is connected to. Also a condition if none of the 2 nets are gnd
                if(source_net1_ == "0") {
                    file << source_net1_ << " " << source_net2_ << "_" << idx << " ";
                } else if(source_net2_ == "0") {
                    file << source_net1_ << "_" << idx << " " << source_net2_;
                } else {
                    file << source_net1_ << "_" << idx << " " << source_net2_ << "_" << idx << " ";
                }
            }

            // ------- ISOURCE_PWL-------

            if(source_type_ == SourceType::ISOURCE_PWL) {

                // Get pulse and timepoints
                const auto& pulse = pixel_charge.getPulse(); // the pulse containing charges and times
                if(!pulse.isInitialized()) {
                    throw ModuleError("No pulse information available.");
                }
                const auto step = pulse.getBinning();

                (target_ == Target::SPECTRE) ? file << ") isource delay=" << delay_ << "n type=pwl wave=[" : file << "PWL(";

                for(auto bin = pulse.begin(); bin != pulse.end(); ++bin) {
                    auto time = Units::convert(step, "s") * static_cast<double>(std::distance(pulse.begin(), bin));
                    double current_bin = *bin / step;
                    auto current = Units::convert(current_bin, "nC");

                    file << std::setprecision(15) << time << " " << current << (bin < pulse.end() - 1 ? " " : "");
                }

                (target_ == Target::SPECTRE) ? file << "]\n" : file << ")\n";

                // ------- ISOURCE_PULSE -------

            } else if(source_type_ == SourceType::ISOURCE_PULSE) {

                const auto i_diode = Units::convert(inputcharge, "nC") / (rise_ / 2 + width_ + fall_ / 2);

                (target_ == Target::SPECTRE)
                    ? (file << ") isource type=pulse val0=0 val1=" << i_diode << " delay=" << delay_ << "n rise=" << rise_
                            << "n fall=" << fall_ << "n width=" << width_ << "n\n")
                    : (file << "PULSE(0 " << i_diode << " " << delay_ << "n " << rise_ << "n " << fall_ << "n " << width_
                            << "n)\n");
            }

            // Writing the subckt instance declaration
            (target_ == Target::SPECTRE) ? (file << subckt_instance_name_ << "\\<" << idx << "\\> (")
                                         : (file << subckt_instance_name_ << "_" << idx << " ");

            // Select whether the net needs to be iterated or not
            for(const auto& net : net_list_) {
                if(common_nets_.find(net) != common_nets_.end()) {
                    // must NOT be iterated !
                    file << net << " ";
                } else {
                    // must be iterated !
                    (target_ == Target::SPECTRE) ? file << net << "\\<" << idx << "\\>"
                                                        << " "
                                                 : file << net << "_" << idx << " ";
                }
            }
            file.seekp(-1, std::ios_base::cur);

            (target_ == Target::SPECTRE) ? file << ") " << subckt_name_ << "\n" : file << " " << subckt_name_ << "\n";

            // Add the increment (address of fired pixel) on each waveform to save, and concatenate a single string
            // (added later to the generated netlist)
            for(const auto& wave : waveform_to_save_) {
                (target_ == Target::SPECTRE) ? to_be_saved << wave << "\\<" << idx << "\\> "
                                             : to_be_saved << wave << "_" << idx << " ";
            }
        }
    }

    for(current_line++; current_line < std::max(source_line_number_, subckt_line_number_) - 1; current_line++) {
        file << file_lines_[current_line] << '\n';
    }

    for(current_line++; current_line < file_lines_.size(); current_line++) {
        file << file_lines_[current_line] << '\n';
    }

    //'save' line
    (target_ == Target::SPECTRE) ? file << "save " << to_be_saved.str() << '\n'
                                 : file << ".save " << to_be_saved.str() << '\n';

    file.close();
    LOG(DEBUG) << "Successfully written netlist to file " << file_name;

    // Runs the external uelec simulation, if selected in the configuration file, on the same terminal (ie. with uelec soft.
    // env variables loaded)
    if(run_netlist_simulation_ == true) {
        std::string uelec_sim_command = simulator_command_;
        uelec_sim_command += " ";
        uelec_sim_command += file_name;

        LOG(INFO) << uelec_sim_command;

        std::system(uelec_sim_command.c_str()); // NOLINT
    }
}
+101 −0
Original line number Diff line number Diff line
/**
 * @file
 * @brief Definition of NetlistWriter module
 *
 * @copyright Copyright (c) 2024-2025 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
 */

#include <filesystem>
#include <fstream>
#include <regex>
#include <string>

#include "core/config/Configuration.hpp"
#include "core/geometry/DetectorModel.hpp"
#include "core/messenger/Messenger.hpp"
#include "core/module/Event.hpp"
#include "core/module/Module.hpp"

#include "objects/PixelCharge.hpp"

namespace allpix {
    /**
     * @ingroup Modules
     * @brief Module which generates netlists to be fed to SPICE simulators
     */
    class NetlistWriterModule : public Module {
    public:
        enum class Target {
            SPICE,
            SPECTRE,
        };

        enum class SourceType {
            ISOURCE_PWL,
            ISOURCE_PULSE,
        };

        /**
         * @brief Constructor for the NetlistWriter module
         *
         * @param config Configuration object for this module as retrieved from the steering file
         * @param messenger Pointer to the messenger object to allow binding to messages on the bus
         * @param detector Pointer to the detector for this module instance
         */
        NetlistWriterModule(Configuration& config, Messenger* messenger, std::shared_ptr<Detector> detector);

        /**
         * @brief Initialization method of the module
         */
        void initialize() override;

        /**
         * @brief Run method of the module
         */
        void run(Event* event) override;

    private:
        // Pointers to the central geometry manager and the messenger for interaction with the framework core:
        std::shared_ptr<Detector> detector_;
        Messenger* messenger_;

        // Module parameters
        std::filesystem::path netlist_path_;
        std::string extension_{};
        std::string file_name_{};
        Target target_;
        SourceType source_type_;

        std::string source_name_{};
        std::string subckt_instance_name_{};

        std::string connections_;
        std::set<std::string> common_nets_;

        std::set<std::string> waveform_to_save_;

        bool run_netlist_simulation_{};
        std::string simulator_command_{};

        // isource_pulse
        double delay_{};
        double rise_{};
        double fall_{};
        double width_{};

        std::string source_net1_;
        std::string source_net2_;

        std::vector<std::string> net_list_;
        std::vector<std::string> file_lines_;

        std::string source_line_;
        std::string subckt_name_;
        size_t subckt_line_number_ = 0;
        size_t source_line_number_ = 0;
    };
} // namespace allpix
+123 −0
Original line number Diff line number Diff line
---
# SPDX-FileCopyrightText: 2024-2025 CERN and the Allpix Squared authors
# SPDX-License-Identifier: CC-BY-4.0 OR MIT
title: "NetlistWriter"
description: "A module to generate netlists for SPICE simulators"
module_status: "Immature"
module_maintainers: ["Elio Sacchetti (<elio.sacchetti@iphc.cnrs.fr>)", "Simon Spannagel (<simon.spannagel@cern.ch>)"]
module_inputs: ["PixelCharge"]
---

## Description

Integrates micro-electronics simulation elements in the Allpix Squared simulation flow. Allows the user to generate netlists (input file used by an electrical simulator to simulate the behavior of the circuit) from a given netlist template. `SPECTRE` (Cadence environment) and `SPICE` syntax are allowed and can be selected using the `target` parameter. This module is mostly intended for analog front-end electrical simulation using the `PixelCharge` object data.

The netlist template needs to be formatted as described and illustrated (`SPECTRE` syntax) below:

* The netlist header.
* A sub-circuit describing the circuit of interest (analog front-end for example).
* If necessary, other instances (for example other voltage or current sources of the front-end).
* A current source, which will be used to replicate the electrical behavior of the collection electrode. A particular attention should be given to the polarity of the source.
* The sub-circuit written as an instance, connected to the source.
* The netlist footer and the simulator options.

```ini
--- netlist header ---

subckt front_end Pix_in Comp_vout Comp_vref SUB VDDA VSSA Vfbk

    description of the circuit 'front_end'

ends front_end

V1 (Comp_vref 0) vsource dc=Comp_vref type=dc
V2 (SUB 0) vsource dc=0 type=dc
V3 (Vfbk 0) vsource dc=Vfbk type=dc
V4 (VDDA 0) vsource dc=1.8 type=dc

Instance_source (0 Pix_in) isource type=pulse
Instance_front_end (Pix_in Comp_vout Comp_vref SUB VDDA VSSA Vfbk) front_end

--- netlist footer and simulator options ---
```

One way to get a netlist already formatted could be to extract it from the Cadence Virtuoso environment ("schematic" view).

A new netlist is written for each event, reusing the header, footer, and circuit description from the netlist template specified with the `netlist_template` parameter. For each fired pixel, a source / circuit instance pair is added to the template.

The new source written can be parameterized with the parameter `source_type`. Two different types of sources can be used: `ISOURCE_PWL` and `ISOURCE_PULSE`:

* `ISOURCE_PWL` allows writing all the temporal current waveform using a PWL (Piecewise Linear). This requires the use of the `[PulseTransfer]` module to get the current waveform. A delay can also be added using `t_delay`
* In order to lightweight the generated netlists, the `ISOURCE_PULSE` can be selected: it uses the total collected charge Q (instead of the current pulse). Charge and current are linked by $ Q = \int I(t)dt $. The current pulse is set with the parameters `t_delay`, `t_rise`, `t_width` and `t_fall`. The following equation is then used to determine the current: $ I=\frac{Q}{\frac{t_{rise}+t_{fall}}{2}+t_{width}} $

The generated netlist file name can be configured with a prefix taken from the `file_name` parameter, and contains the number of the event the netlist was generated for. The file extension is taken from the input netlist template file.

The pixel address is used to identify the fired pixels in the netlist, a linearization following

```math
i = x * N_y + y
```

is used. Here, `x` and `y` are the respective pixel coordinates and $`N_y`$ is the number of pixels along `y`.
This means that e.g. considering pixel (6,3) fired (7th column and 4th row) in a 10x10 matrix, the index 63 will be written in the netlist for this pixel..

The parameter `waveform_to_save` is used to write at the end of the generated netlist the waveform(s) to be saved (always using the index notation to identify the fired pixels).
The electrical circuit simulation can be performed within the Allpix Squared event using the parameter `simulator_command` which is used to specify the command line to execute. Not setting it switches the feature off, setting a value will enable it. The generated netlist name to execute is appended at the end of the command, as illustrated below for `SPECTRE` syntax:

```shell
spectre -f nutascii <file_name_event1.scs>
```

If performed, the electrical simulation puts in stand-by the execution of the event. This electrical simulation is performed in the same terminal as the Allpix event, thus requiring the electrical simulator environment to be correctly set.


## Parameters

* `target`: Syntax for the additional data to be written in the netlist, either `SPECTRE` or `SPICE`.
* `netlist_template`: Location of file containing the netlist template of the circuit in one of the supported formats.
* `file_name` : Generated netlist prefix name to which the event number is added as suffix. Defaults to `output_netlist_event_`.
* `source_type`: Type of current source to be used, `ISOURCE_PWL` and `ISOURCE_PULSE`.
* `source_name`: Name of the current source instance in the netlist.
* `subckt_name`: Name of the circuit the source is connected to.
* `common_nets`: Nets shared between the pixels.
* `t_delay`: delay from 0 before the current pulse starts, defaults to 0 ns
* `t_rise`: rise time of the current pulse, defaults to 1 ns, only works for the `ISOURCE_PULSE`
* `t_width`: width of the current pulse, defaults to 3 ns, only works for the `ISOURCE_PULSE`
* `t_fall`: fall time of the current pulse, defaults to 1 ns, only works for the `ISOURCE_PULSE`
* `waveform_to_save`: Name of the waveforms to save
* `simulator_command`: If specified, launches the electrical simulation. Command to be executed in the terminal, the generated netlist name is appended at the end of the command.


## Usage

A possible configuration is using a `ISOURCE_PWL` and the `SPICE` syntax, requiring the collecting electrode capacitance:

```ini
[NetlistWriter]
target = SPICE
netlist_template = "front_end.asc"
source_type = ISOURCE_PWL
source_name = Instance_source
subckt_name = Instance_front_end
common_nets = Comp_vref, SUB, VDDA, VSSA, Vfbk
waveform_to_save = Pix_in, CSA_out, Comp_vout
simulator_command = "wine 'your\path\LTSpiceXVII\XVIIx64.exe' -run"
```

A current pulse `ISOURCE_PULSE` and the `SPECTRE` syntax is used is this example:

```ini
[NetlistWriter]
target = SPECTRE
netlist_template = "front_end.scs"
source_type = ISOURCE_PULSE
t_delay = 200ns
t_rise = 5ns
t_width = 20ns
t_fall = 5ns
source_name = Instance_source
subckt_name = Instance_front_end
common_nets = Comp_vref, SUB, VDDA, VSSA, Vfbk
waveform_to_save = Comp_vout
simulator_command = "spectre +aps -warn -info -log -debug -f nutascii"
```
+1 −0
Original line number Diff line number Diff line
@@ -52,6 +52,7 @@ namespace allpix {
        Units::add("e", 1);
        Units::add("ke", 1e3);
        Units::add("fC", 1 / 1.602176634e-4);
        Units::add("nC", 1 / 1.602176634e-10);
        Units::add("C", 1 / 1.602176634e-19);

        // VOLTAGE