Design
Simulation
UAMMD-structured compiles into a single executable. However, it can also
be used as a library by utilizing specific parts of the code or
the entirety of it. To include the entire UAMMD-structured, we can use
#include "UAMMDstructured.cuh". This is precisely what we do when
generating the main binary:
#include "UAMMDstructured.cuh"
using namespace uammd::structured;
int main(int argc, char *argv[]) {
//Check if the input parameters are correct
if (argc < 2) {
uammd::System::log<uammd::System::CRITICAL>("No input file provided!");
return EXIT_FAILURE;
}
//Init system. System takes the input file path as argument
std::string inputFilePath = argv[1];
std::shared_ptr<ExtendedSystem> sys = std::make_shared<ExtendedSystem>(inputFilePath);
{
//Create simulation object
std::shared_ptr<Simulation> sim = std::make_shared<Simulation>(sys);
//Run the main simulation loop
sim->run();
}
//End simulation
sys->finish();
return EXIT_SUCCESS;
}
The code above is the way the simulation is executed. First, we
initialize System, which is then passed as an argument to the
Simulation class, which we will describe next. As observed, System
takes the path to the input file as an argument. System will parse the
file, and if correct, the rest of the classes will be able to request it
via sys->getInput() and access its information.
The initialization process is as follows:
class Simulation{
private:
...
public:
...
Simulation(std::shared_ptr<ExtendedSystem> sys):sys(sys){
// Load global data
gd = std::make_shared<GlobalData>(sys);
// Load particle data
pd = std::make_shared<ExtendedParticleData>(sys);
//Load topology
topology = std::make_shared<Topology>(gd, pd);
//Load force field
ff = std::make_shared<ForceField>(topology);
//Load integrators
//Note that the integrators are loaded after the topology and the force field
//this is because topology can set some particle properties that are needed
//by integrators initialization.
//For example, the particle mass and the particle radius.
integrators = std::make_shared<IntegratorManager>(topology);
//Load simulations steps
simulationSteps = std::make_shared<SimulationStepManager>(integrators,ff);
//Handle backup
...
}
...
};
Firstly, we load the
structures that store the data, namely, GlobalData and
ParticleData [1]. GlobalData processes the information in the
global section, and the state section will be processed by
ParticleData.
Subsequently, we use global data and particle data to load the topology.
In the topology, we process the structure section, where particle
types and the structures they belong to, like the molecule, residue,
etc., are defined. Also, in topology, we process the interactions
present in the forcefield section. When we process forcefield, state,
global, and structure have already been processed, so the
interactions can already assume this. We then extract the forcefield
object from the topology. In forcefield, we process the interactors to
get them all working together. The forcefield is itself an interactor.
In fact, it is an interactor that aggregates a set of interactors. Next,
we use topology to initialize the IntegratorManager. Finally, we use
integrators and topology to initialize the simulation steps. The
reason that the simulation steps need these two objects is that it
endows them with the capability to calculate energies, forces, etc.,
which is often necessary to calculate physical quantities of interest.
This concludes the initialization process of the simulation [2]
Once all the necessary elements have been initialized, we can start the
simulation. This is done via the run method of the Simulation
class:
class Simulation{
private:
...
public:
...
int run(){
Timer tim; // Timer to measure the time taken by the simulation
tim.tic(); // Start the timer
std::map<std::string,
std::shared_ptr<SimulationStep::SimulationStepBase>>
simSteps = simulationSteps->getSimulationSteps();
// Get the simulation steps are requested from the SimulationStepManager
// and stored in a map
System::log<System::MESSAGE>("[Simulation] Running simulation...");
for(auto& integratorInfo: integrators->getSortedIntegratorSteps()){
// Iterate through the integrators
// Get the integrator name and the number of steps for the integrator
std::string name = integratorInfo.name;
ullint steps = integratorInfo.steps;
System::log<System::MESSAGE>("[Simulation] Running integrator (%s)"
" for %llu steps...", name.c_str(),
steps);
// Get the integrator from the IntegratorManager
std::shared_ptr<Integrator> currentIntegrator =
integrators->getIntegrator(name);
// Load the force field into the integrators
currentIntegrator->addInteractor(ff);
// Initialize the simulation steps
System::log<System::DEBUG>("[Simulation] Initializing simulation "
"steps...");
for(auto sStep : simSteps){sStep.second->tryInit();}
// Iterate through the steps
for(ullint i = 0; i < steps; i++){
for(auto sStep : simSteps){
// At each step, apply the simulation steps
sStep.second->tryApplyStep();
}
// Move the integrator forward in time e.i. integrate
currentIntegrator->forwardTime();
}
}
// Stop the timer and get the total time taken by the simulation
auto totalTime = tim.toc();
// Print the mean time per step
real fps = real(gd->getFundamental()->getCurrentStep())/totalTime);
System::log<System::MESSAGE>("[Simulation] Mean FPS: %f",fps);
return 0; // Successful completion
}
}
The run begins by initializing a timer, which will tell us at the end of
the simulation how much time has passed and will be used to calculate
the number of steps (frames) per second (FPS). We then store in a map
all the simulation steps present in the simulation and then begin to
iterate over each of the integrators present. For each integrator, we
will perform a series of steps. Before starting the simulation itself,
we load the forcefield (which, as mentioned, is an interactor) into the
current integrator. This is done via the standard UAMMD interface
addInteractor(std::shared_ptr<uammd::Interactor> interactor). Once
the forcefield has been loaded, we prepare the simulation steps and
start the simulation. In each step, we try to apply the simulation
steps; the simulation step itself accesses the current step via global
data and applies itself according to its internal rules. Once all the
integration steps of all the integrators are completed, we stop the
timer and calculate the FPS, concluding the simulation.
System
In UAMMD-structured, the initial object to be initialized, and one that is unique in its instance, is the System. This component acts as a versatile container, encapsulating functionalities that do not comfortably fit within other objects. Due to its role as the first-created object, System is readily accessible in many parts of the code, making it a central element in UAMMD-structured’s architecture.
System stands as a universally available object, as it is a
prerequisite for other objects, which typically take it as an argument.
Furthermore, these objects offer a method, getSystem(), for easy
access. In UAMMD-structured, System not only carries out the
responsibilities it has in the standard UAMMD framework but also manages
two critical areas: the handling of the simulation’s input and
overseeing the state and backup processes.
The extended functionality of System in UAMMD-structured is
encapsulated in the class ExtendedSystem_, which inherits from
UAMMD’s System class. The class is defined with a template parameter
InputType_ and introduces an enumeration SIMULATION_STATE to
signify the running or stopped state of the simulation.
template<class InputType_>
class ExtendedSystem_ : public uammd::System {
public:
using InputType = InputType_;
//Enum available states, running and stopped
enum SIMULATION_STATE {RUNNING, STOPPED};
private:
//Simulation state
SIMULATION_STATE state = RUNNING;
//Smart pointer to input
std::shared_ptr<InputType> input;
//Other attributes (name,backup...)
...
void loadSimulationInformation(std::string entryName){...}
void loadSimulationBackup(std::string entryName){...}
void init(){
...
//Iterate over entries in system section
for(std::string entryName : input->getEntriesList(this->path)){
//If entry is Information, loadSimulatonInformation(entryName)
//If entry is Backup, loadSimulatonBackup(entryName)
//Else, emit an error.
...
}
//Check if Information was loaded, else emit an error.
}
public:
ExtendedSystem_(int argc, char *argv[],
std::string inputFilePath,
std::vector<std::string> path):
//Path refer to path in input file (JSON)
//By default, path = "system"
uammd::System(argc,argv),
path(path){
//Check if the input file exists
...
try{
input=std::make_shared<InputType>(inputFilePath);
}catch(std::exception &e){
//Emit error, bad input file
}
this->init();
}
std::shared_ptr<InputType> getInput(){return input;}
//Attributes getters and setters (STATE, seed, backup ...)
...
};
using ExtendedSystem = ExtendedSystem_<InputJSON::InputJSON>;
ExtendedSystem_ is customized to accommodate UAMMD-structured’s
specific requirements, including input processing and backup management.
This extended version of System ensures that the class is not only
backward compatible with UAMMD but also aligns with the additional
functionalities unique to UAMMD-structured. The use of polymorphism in
C++ allows ExtendedSystem_ to be used interchangeably with UAMMD’s
System.
System is integral to UAMMD-structured’s operation, responsible for monitoring the simulation’s state, which oscillates between RUNNING and STOPPED. A change to the STOPPED state triggers the cessation of the simulation. This feature is instrumental in terminating the simulation under certain predefined conditions. Given the system’s ubiquitous access across the framework, any object can intervene to cease the simulation by adjusting the System’s state.
Apart from state management, System also oversees the backup variables, facilitating a crucial aspect of simulation resilience.
Global
‘GlobalData‘ is essentially a class acting as a container for other class instances managing ‘Fundamental‘, ‘Units‘, ‘Types‘, and ‘Ensemble‘. For its initialization, ‘GlobalData‘ requires only a ‘System‘ instance, which it transmits to the other handler classes, mainly to access the input and read the necessary parameters.
class GlobalData{
private:
std::shared_ptr<ExtendedSystem> sys;
//Handlers objects
std::shared_ptr<Units::UnitsHandler> unitsHandler;
std::shared_ptr<Fundamental::FundamentalHandler> fundamentalHandler;
std::shared_ptr<Types::TypesHandler> typesHandler;
std::shared_ptr<Ensemble::EnsembleHandler> ensembleHandler;
void init(){
//With InputEntryMaknager we can access all the entries
//in the "path" section of input. In this case path = "global"
globalInfo = std::make_shared<InputEntryManager>(sys,path);
//Try to load fundamental
if(globalInfo->isEntryPresent("fundamental")){
fundamentalHandler =
FundamentalLoader::loadFundamental(sys,fundamentalPath);
}else{
//If entry fundamental is not present,
//we add a fundamental entry of type "Time"
//to input.
//Create fundamentalHandler
}
//Try to load units
if(globalInfo->isEntryPresent("units")){
unitsHandler =
UnitsLoader::loadUnits(sys,unitsPath);
}else{
//If entry units is not present,
//we add a units entry of type "None"
//to input.
//Create unitsHandler
}
//Load types and ensemble.
//If types or ensemble are not present
//error is emited and simulation ends
}
public:
GlobalData(std::shared_ptr<ExtendedSystem> sys,
//Path refer to path in input file (JSON)
//By default, path = "global"
std::vector<std::string> path):path(path){
this->init();
}
std::shared_ptr<ExtendedSystem> getSystem(){...}
std::shared_ptr<Units::UnitsHandler> getUnits() {...}
std::shared_ptr<Ensemble::EnsembleHandler> getEnsemble() {...}
std::shared_ptr<Types::TypesHandler> getTypes() {...}
std::shared_ptr<Fundamental::FundamentalHandler> getFundamental(){...}
};
Once the various handlers are initialized, they provide access to the stored information. Let’s examine each of these handlers separately. UAMMD-structured incorporates a unit manager. Setting a unit system essentially amounts to fixing the value of certain constants. This is implemented by making ‘UnitsHandler‘ a virtual class with public methods as follows:
class UnitsHandler{
protected:
//SubType will store the units name
std::string subType;
public:
UnitsHandler(DataEntry& data){
subType = data.getSubType();
}
...
virtual real getBoltzmannConstant(){
System::log<System::CRITICAL>(
"[Units] BoltzmannConstant not defined for units \"%s\".",
subType.c_str());
}
virtual real getElectricConversionFactor(){
System::log<System::CRITICAL>("[Units] ElectricConversionFactor not defined for units \"%s\".",
subType.c_str());
}
...
};
class KcalMol_A: public UnitsHandler{
public:
KcalMol_A(DataEntry& data):UnitsHandler(data){}
real getBoltzmannConstant() override {return 1.987191E-03;}
real getElectricConversionFactor() override {return 332.0716;}
};
When setting a unit system, we derive from this class and override the methods to return the values of these constants for the particular system. For instance, in the \((\text{Kcal/mol})/\text{Å}\) unit system, the electric conversion factor would be \(332.0716\), and Boltzmann’s constant would be \(1.987191E-03\).
Using polymorphism, when we assign a specific units object to
‘unitsHandler‘ (such as “KcalMol_A”), methods like
getBoltzmannConstant() will return values pertinent to the specific
system.
In the ‘GlobalData‘ code, the assignment of ‘unitsHandler‘ is done using
the auxiliary function loadUnits, which takes a ‘System‘ instance
and “unitsPath” as arguments. “unitsPath” is a vector of std::string
containing the path to the units information in the input; by default
“unitsPath” = [“global”,”units”]. The function is as follows:
std::shared_ptr<typename Units::UnitsHandler>
loadUnits(std::shared_ptr<ExtendedSystem> sys,
std::vector<std::string> path){
DataEntry data = sys->getInput()->getDataEntry(path);
std::string unitsType = data.getType();
std::string unitsSubType = data.getSubType();
std::shared_ptr<typename Units::UnitsHandler> units;
bool found = false;
if("Units" == unitsType and "None" == unitsSubType){
System::log<System::MESSAGE>(
"[UnitsLoader] (%s) Detected None units",
path.back().c_str());
units = std::make_shared<Units::None>(data);
found = true;
}
if("Units" == unitsType and "KcalMol_A" == unitsSubType){
System::log<System::MESSAGE>(
"[UnitsLoader] (%s) Detected KcalMol_A units",
path.back().c_str());
units = std::make_shared<Units::KcalMol_A>(data);
found = true;
}
if(not found){
System::log<System::CRITICAL>(
"[UnitsLoader] (%s) Could not find units %s::%s",
path.back().c_str(),unitsType.c_str(),unitsSubType.c_str());
}
return units;
}
‘Ensemble‘ and ‘Fundamental‘ work similarly, defining certain functions that are overwritten based on the type of ensemble or fundamental. For example, in ‘Ensemble‘, we define the ‘getBox‘ function, which returns a box. This function is overwritten for the NVT ensemble, but would emit an error in another ensemble where it does not apply.
The handling of ‘Types‘ is slightly different. Initially, we define a base class ‘TypesHandler‘, which processes the input and stores the information in a set of dictionaries. Internally, each type is associated with an integer. ‘TypesHandler‘ has a virtual method that takes a ‘ParticleData‘ instance and applies the information for each type.
An auxiliary class ‘Types_‘ is declared, inheriting from ‘TypesHandler‘. This class requires a template argument for the specific type, as shown in the example with ‘Basic‘. Each type must have two static methods: ‘loadType‘ and ‘loadTypesIntoParticleData‘. ‘loadType‘ processes the input and populates the ‘nameToData‘ dictionary, taking ‘typeData‘ as an argument. The function ‘loadTypesIntoParticleData‘ iterates over the particles in ‘ParticleData‘, associating relevant variables based on their type. For example, ‘Basic‘ associates mass, radius, and charge to each type. Thus, it iterates over each particle, setting these variables for each one.
class TypesHandler{
protected:
std::map<int,std::string> idToName;
std::map<std::string,int> nameToId;
std::map<std::string,std::map<std::string,real>> nameToData;
public:
TypesHandler(DataEntry& data){
auto typesData = data.getDataMap();
//Load type data, set up idToName,nomeToId adn nameToData
}
...
virtual void
loadTypesIntoParticleData(std::shared_ptr<ParticleData> pd) = 0;
};
template<class T>
class Types_: public TypesHandler{
public:
Types_(DataEntry& data): TypesHandler(data){
auto typesData = data.getDataMap();
for(auto& type: typesData){
try{T::loadType(this->nameToData,type);}
catch (std::exception& e){/*Emit error*/}
}
}
void loadTypesIntoParticleData(std::shared_ptr<ParticleData> pd)
override {
T::loadTypesIntoParticleData(pd,this->idToName,this->nameToData);
}
};
//Example from Basic.cuh:
struct Basic_{
template<typename T>
static void loadType(
std::map<std::string,std::map<std::string,real>>& nameToData,
std::map<std::string,T>& typeData){
std::string name = typeData.at("name");
nameToData[name]["mass"] = real(typeData.at("mass"));
...
}
static void loadTypesIntoParticleData(
std::shared_ptr<ParticleData> pd,
std::map<int,std::string>& idToName,
std::map<std::string,std::map<std::string,real>>& nameToData){
//Iterate over all particles and load type information (mass,radius and charge for Basic)
}
};
using Basic = Types_<Basic_>;
Note that when ‘Global‘ is declared in the system, particles have not been added or associated with types yet; this is done subsequently. It is at this point that ‘Global‘ requires the types, and the function ‘loadTypesIntoParticleData‘ is called.
State
In a manner similar to handling the ‘System‘, we work with an object derived from UAMMD, specifically an object derived from ParticleData, which we call ‘ExtendedParticleData‘. ‘ExtendedParticleData‘ is similar to ‘ParticleData‘, but it initializes by accessing the input to process and load the content of the ‘State‘ Data Entry into ‘ParticleData‘:
class ExtendedParticleData: public uammd::ParticleData{
private:
std::vector<std::string> path;
public:
ExtendedParticleData(std::shared_ptr<ExtendedSystem> sys,
std::vector<std::string> path):
uammd::ParticleData(sys->getInput()->getDataEntry(path).getDataSize(),
sys),
path(path){
auto data = this->getSystem()->getInput()->getDataEntry(path);
stateLoader(this, data);
}
...
};
The function ‘StateLoader‘ is responsible for loading the state information. It does this by checking all available options, and if an option is present in the labels, it requests this information from the input and loads it into each particle:
void stateLoader(ParticleData* pd,DataEntry& data){
std::vector<std::string> labels = data.getLabels();
...
//Id and position are compulsory
//Check id label is present
if(std::find(labels.begin(), labels.end(), "id") == labels.end()){
System::log<System::CRITICAL>(
"[StateLoader] Label 'id' not found in the state file."
);
} else {
//Load ids
...
}
//Check position label is present
if(std::find(labels.begin(), labels.end(), "position") == labels.end()){
System::log<System::CRITICAL>(
"[StateLoader] Label 'position' not found in the state file."
);
} else {
//Load position
...
}
if(std::find(labels.begin(), labels.end(), "velocity") != labels.end()){
//Load velocity
}
if(std::find(labels.begin(), labels.end(), "direction") != labels.end()){
//Load direction
}
...
//Load other avaible state variables
...
//Check all labels have been loaded properly
}
Two fields are always required: “id” and “position”, which respectively indicate the unique identifier for each particle during the simulation (a particle’s id remains constant throughout) and its position.
Expanding the capabilities of ‘StateLoader‘ is relatively straightforward. This involves adding a new label to the list of available labels and loading its content if it is present in the input.
Integrators
While Integrator is a standard UAMMD class, UAMMD-structured
necessitates a class to manage various integrators and handle the
schedule. This role is fulfilled by ‘IntegratorManager‘. Upon
initialization, this class invokes the functions void loadSchedule()
and void loadIntegrators(). The former function searches for and
processes the ‘schedule‘ entry, populating the ‘integratorSteps‘
dictionary, an attribute of ‘IntegratorManager‘. Subsequently, the
method void loadIntegrators() is called. This method iterates over
the ‘integrators‘ section, attempting to load all entries except
‘schedule‘. Loading is facilitated by
IntegratorLoader::loadIntegrators(...), which returns a pointer to the
integrator interface from which all integrators are derived. This
process links the type and subtype pair to an instance of an integrator
(a class derived from ‘Integrator‘). Integrators must be predefined in
the schedule and have a unique name; otherwise, an error is thrown,
terminating the program. These integrators are stored in the
‘integrators‘ dictionary.
class IntegratorManager{
private:
...
std::map<std::string,std::shared_ptr<Integrator>> integrators;
struct stepsInfo{
std::string name;
uint order;
ullint steps;
};
std::map<std::string,stepsInfo> integratorSteps;
...
void loadSchedule(){ /*Set up integratosSteps from schedule entry*/ }
...
void loadIntegrators(){
for(auto& entry : integratorsInfo->getEntriesInfo()){
if(IntegratorLoader::isIntegratorAvailable(sys,entry.second.path)){
std::shared_ptr<Integrator> integrator =
IntegratorLoader::loadIntegrator(sys,gd,
groups,
entry.second.path);
if(integrators.count(entry.second.name) == 0){
//Check if integrator is in schedule
integrators[entry.second.name] = integrator;
}
else{
//Emit error
}
}
}
...
}
public:
IntegratorManager(std::shared_ptr<ExtendedSystem> sys,
std::shared_ptr<GlobalData> gd,
std::shared_ptr<ExtendedParticleData> pd,
std::vector<std::string> path):
sys(sys),gd(gd),pd(pd),path(path){
...
this->loadSchedule();
this->loadIntegrators();
...
}
...
std::vector<stepsInfo> getSortedIntegratorSteps(){...}
...
std::shared_ptr<Integrator> getIntegrator(std::string name){
...
return integrators[name];
}
};
‘IntegratorManager‘ possesses several methods to access stored
information. getSortedIntegratorSteps() returns a list of integrator
information (name, order, and number of steps) sorted by order.
std::shared_ptr<Integrator> getIntegrator(std::string name)
retrieves an integrator instance by its name. The integration process
then follows: the list of integrators is retrieved, iterated upon, the
relevant integrator is requested from ‘IntegratorHandler‘, and then
integration occurs for a specified number of steps.
Topology
class Topology{
private:
...
std::map<std::string,
std::shared_ptr<VerletConditionalListSetBase>> VConListSet;
std::map<std::string,
std::shared_ptr<typename uammd::Interactor>> interactors;
void loadStructure(){...}
// Methods for force field section processing
...
void loadNeighbourLists(){...}
void loadInteractors(){
for(auto& entry : forceFieldInfo->getEntriesInfo()){
if(Potentials::GenericLoader::isInteractorAvailable(sys,
entry.second.path)){
std::shared_ptr<typename uammd::Interactor> inter =
Potentials::GenericLoader::loadGeneric(sys,
gd,groups,
VConListSet,
entry.second.path);
if(interactors.count(entry.second.name) == 0){
interactors[entry.second.name] = inter;
...
} else {/*Emit error*/}
}
}
...
}
public:
Topology(std::shared_ptr<ExtendedSystem> sys,
std::shared_ptr<GlobalData> gd,
std::shared_ptr<ExtendedParticleData> pd,
std::vector<std::string> path):sys(sys),gd(gd),pd(pd),path(path){
this->loadStructure();
//Load components
...
this->loadNeighbourLists();
this->loadInteractors();
}
//Add a new interactor to the system
void addInteractor(std::shared_ptr<typename uammd::Interactor> interactor,
std::string name){
...
}
//Getters
//Get neighbout list std::shared_ptr<VerletConditionalListSetBase>
//Get interactor std::shared_ptr<typename uammd::Interactor>, by name, type ...
...
};
Structure
The structure’s loading process is quite similar to that of the ‘State‘. It involves checking the available labels to see which ones are active and then loading them accordingly. The process for types, however, is slightly different.
class Topology{
private:
...
void loadStructure(){
...
auto structureData = sys->
getInput()->
getDataEntry(structurePath);
std::vector<int> id = structureData.getData<int>("id");
int N = id.size();
if (N != pd->getNumParticles()){/*Emit error*/}
std::vector<std::string> type =
structureData.getData<std::string>("type");
std::vector<int> resBuffer;
if(structureData.isDataAdded("resId")){
resBuffer = structureData.getData<int>("resId");
} else {
resBuffer.resize(N);
std::fill(resBuffer.begin(),resBuffer.end(),0);
}
// Same for chain, model and batch ...
try {
auto typeParamHandler = gd->getTypes();
const int * sortedIndex =
pd->getIdOrderedIndices(access::location::cpu);
auto pos = pd->getPos(access::location::cpu, access::mode::write);
auto res = pd->getResId(access::location::cpu,access::mode::write);
...
for(int i=0;i<N;i++){
int id_ = id[i];
pos[sortedIndex[id_]].w = int(typeParamHandler->getTypeId(type[i]));
res[sortedIndex[id_]] = resBuffer[i];
...
}
} catch(...) {/*Emit error*/}
{
//Load types info (mass,radius,...) into ParticleData
gd->getTypes()->loadTypesIntoParticleData(pd);
}
}
...
public:
Topology(...):...{...}
...
};
At the end of the function, the method loadTypesIntoParticleData is
called. This method updates the data for each particle based on the
types that have been defined. Once this process is complete, the
initialization of ‘ParticleData‘ is considered finished, as all data
meant to be added have been introduced either in ‘State‘ or here, in
‘Structure‘.
Force Field
The ‘Force Field‘ section in the input file serves not only as a grouping for the simulation’s interactions but also embodies a class:
class ForceField : public Interactor {
private:
struct schedule{
ullint start;
ullint end;
bool state; //true: on, false: off
};
...
std::map<std::string, std::shared_ptr<Interactor>> interactors;
std::map<std::string, std::shared_ptr<Interactor>> idleInteractors;
std::map<std::string,schedule> scheduledInteractors;
void stopInteractor(std::string interactorName){
// Move interactor from interactors to idleInteractors
}
void resumeInteractor(std::string interactorName){
// Move interactor from idleInteractors to interactors
}
public:
ForceFieldBase(std::shared_ptr<Topology> top,
std::string name):Interactor(top->getParticleGroup(),name),
top(top){}
...
interactors = top->getInteractors();
...
for(auto &interactor: interactors){
//Read interactor parameters
DataEntry data = ...
if(data.isParameterAdded("endStep") or
data.isParameterAdded("startStep")){
schedule sched;
sched.start = data.getParameter<ullint>("startStep",0);
...
scheduledInteractors[interactor.first] = sched;
}
}
ullint step = this->gd->getFundamental()->getCurrentStep();
// Init scheduledInteractors state according current step
...
}
void sum(Computables comp,cudaStream_t st) {
ullint step = this->gd->getFundamental()->getCurrentStep();
for(auto &interactor: scheduledInteractors){
//Check if interactor is active and move to/from idleInteractors
}
for(auto &interactor: interactors){
interactor.second->sum(comp,st);
}
}
};
The concept behind ‘ForceField‘ is to create an interactor that encompasses all interactions. This arrangement facilitates the execution of common operations, simplifications, or the addition of shared features. Currently, ‘ForceField‘ is primarily used for the latter purpose. ‘ForceField‘ analyzes the added interactors, checks if they have defined parameters like ‘startStep‘ or ‘endStep‘, and if so, adds them to the ‘scheduledInteractors‘ dictionary.
Upon declaration, ‘ForceField‘ requests all interactors from ‘Topology‘ (those declared within the ‘Force Field‘ section), storing them in the ‘interactors‘ dictionary, which maps each interactor (the object instance) to a string, its name. When evaluating the ‘ForceField‘ interactors, it checks which interactors are active. These include those not considered scheduled or, among the scheduled, those for which ‘currentStep‘ is greater than ‘startStep‘ and less than ‘endStep‘, hence active. Active interactors are ensured to be in the list of interactors to be computed, while inactive ones are placed in the ‘idleInteractors‘ list.