The Aperture Framework
The Aperture
framework is inspired by the Entity-Component-System (ECS)
paradigm of modern game engines. In a sense, a numerical simulation is very
similar to a video game, where different quantities are evolved over time in a
giant loop, inside which every module is called sequentially to do their job. A
simulation can be much simpler than a video game, since usually no interactivity
is needed. However, there can still be very complex logical dependency between
different modules, and this framework is designed to make it relatively easy to
add new modules or to setup new physical scenarios.
A simulation code does not (usually) need to create and destroy "entities" in
real time, like a game would. Therefore, in Aperture
, there are only two main
categories of classes: system
and data
. data
is what holds the
simulation data, e.g. fields and particles, while system
refers to any
module that works on the data, e.g. pushing particles or evolving fields.
The benefit of such a configuration is that both system
and data
are
flexible and can be plugged in and out depending on the problem. It is also very
straightforward to handle data IO, since we can simply serialize a list of named
data
objects.
For lack of a better name, the sim_environment
class is a coordinator
that ties things together. It keeps a registry of systems and data components,
and calls every system in order in a giant loop for the duration of the
simulation.
Systems
Every system derives from the common base class system_t
. There are three
virtual functions a system can override: register_data_components()
,
init()
, and update()
.
register_data_components()
A system needs to work on some data components. Depending on what systems are
initialized, some data components may or may not be used. Therefore, systems are
responsible for managing their own data dependency. For example, a
field_solver
system needs to work on \(\mathbf{E}\) and
\(\mathbf{B}\) fields, so it needs to register this dependency by overriding the register_data_components()
function:
void register_data_components() {
// E is a raw pointer to vector_field<Conf>
E = m_env.register_data<vector_field<Conf>>(
"E", m_grid, field_type::edge_centered);
B = m_env.register_data<vector_field<Conf>>(
"B", m_grid, field_type::edge_centered);
}
E
and B
are pointers to vector_field<Conf>
and are constructed here.
register_data_components()
takes an std::string
for a name, followed by
parameters that are passed to the constructor of the data component. If these
data components are registered already by another system under the same name,
then the register_data() function will only return the pointer. No two
components with the same name can co-exist. register_data_components()
is
called right after the constructor of the system, in register_system()
.
init()
update()
Data Components
Every data component derives from the common base class data_t
.