3. Simulation And Objects¶
Pressomancy is organized around one hard separation of responsibilities. The
Simulation class owns the global system: the
ESPResSo handle, the box, the registered top-level objects, and the shared
particle-type bookkeeping. Simulation objects own everything local: what they
are made of, how much space they require, and how they should materialize once
a position and orientation have been assigned. This chapter explains why that
split exists and what it means when you author a new object class.
3.1. Why The Split Matters¶
The simulation box is a shared resource, so it must be managed in one place.
That is why Simulation is responsible for
placement. An individual object class does not decide where it sits globally.
Instead, it declares the information the simulation needs in order to place it
sensibly: its size, its required features, and the local construction routine
that should be used once a placement target has been chosen.
The object then takes over. After the simulation has assigned a position and orientation, the object decides what particles should be created, how those particles should be typed, which bonds should exist locally, and whether child objects should be placed recursively beneath it. In other words, the simulation answers “where does this top-level thing go?” and the object answers “what exactly gets built there?”
That distinction becomes essential once objects are hierarchical.
Filament may own
Quadriplex children,
which may in turn own
Quartet children. The
simulation places only the filament. The filament then places its
quadriplexes, and each quadriplex places its quartets. Pressomancy therefore
builds from the top down even though the actual particles are created at the
leaves.
The placement pipeline follows that logic directly.
set_objects() calls
partition_cubic_volume() to generate
candidate regions in the box. The helper starts from an FCC lattice because it
provides a dense and regular set of trial centers. For each accepted center,
the simulation calls the object’s build_function to ask what local point
pattern belongs inside that allocated region. The result is then handed to
set_object(),
which is where the real ESPResSo particles finally appear.
3.2. What A Simulation Object Is¶
The common contract for all object classes is enforced by the metaclass
Simulation_Object. That is
important not just because it standardises class shape, but because the entire
library depends on that consistency as a prerequisite for interoperability.
If different objects did not obey the same bookkeeping rules, the simulation
manager could not safely store them, place them, recurse through them, or
write their ownership structure to disk in a uniform way.
At class definition time, the metaclass expects the object family to declare
the metadata the simulation needs in order to reason about it:
required_features, numInstances, part_types, simulation_type,
and config. At instance level, the object is expected to expose
who_am_i, type_part_dict, associated_objects, and sys. Those
requirements are not there for style. They are what lets a
GenericPart, a
Filament, and a
Quadriplex all be
handled by the same Simulation machinery.
The metaclass also operationalises the worker functions that keep the peace
across the object library. It injects shared methods such as
add_particle(),
change_part_type(),
get_owned_part(),
bond_owned_part_pair(),
and delete_owned_parts().
Those methods are not merely conveniences. They are the routes through which
pressomancy keeps track of ownership chains, part-type assignments, and the
relationship between object-local state and simulation-level state.
This is why direct ad hoc manipulation can become dangerous. You can always
reach into ESPResSo directly, take a particle handle, and change its type by
hand. Pressomancy cannot stop you. But if you do that outside the library’s
worker functions, pressomancy may no longer know what happened. The local
type_part_dict may become inconsistent, the simulation-level
part_types registry may no longer reflect reality, and later logic that
relies on shared bookkeeping can quietly drift out of sync. In practice, the
metaclass exists to make that sort of drift less likely.
A particularly non-obvious part of this arrangement is
modify_system_attribute(). During
store_objects(), objects are wired to
that simulation-side method so that controlled changes to shared attributes can
be propagated through the library without the user having to manage every piece
by hand. This is one of the more hidden parts of pressomancy, but it is also
one of the reasons the object library can remain internally consistent while
still allowing object-local methods to update simulation-level bookkeeping.
One detail matters more than it may first appear to. Every simulation object
participates in the same build pipeline through build_function. If a class
does not define one explicitly, the metaclass supplies a default
RoutineWithArgs instance. For simple
single-particle objects, that may be enough. For chain-like or composite
objects such as
Filament and
TelSeq, build_function
is usually configured in __init__ so that the simulation knows how many
local positions to request, how far apart they should be, and what geometric
constraints should apply during placement.
The class-level config object is the other half of the contract.
ObjectConfigParams provides a
strict configuration template for each object family. Common keys such as
espresso_handle, associated_objects, size, and n_parts recur
across the library, while each class may extend that base with its own
parameters. config.specify(...) is the intended way to produce
instance-specific configurations. It is deliberately strict: you may override
known keys, but you may not quietly invent new ones at call site.
3.3. Extension Patterns¶
The most important point here is that the metaclass is the primary extension manager in pressomancy. If you want a new object to participate cleanly in the library, the first question is not “what should I inherit from?” but rather “what contract must this object satisfy so that it remains interoperable with all the others?” Inheritance is secondary to that. It is useful when an object really is a specialisation of an existing implementation, but the metaclass is what keeps the overall ecosystem coherent.
In practice, there are three useful ways to extend the library. The first, and
in many ways the most direct, is to use metaclass=Simulation_Object
explicitly. This is the right route when the object is really a manager of
other objects, or when its construction logic is distinctive enough that you do
not gain much by pretending it is just a special case of an existing base.
That is the world of
Quadriplex,
Filament,
TelSeq, and
OTP.
The second route is ordinary inheritance from an existing primitive base such
as GenericPart or
GenericRigidObj. This is the
right choice when a new object genuinely adds or changes behaviour. For
example, EGGPart is not
just a renamed primitive. It changes required features, part-type structure,
and object setup in a substantive way. Likewise,
MulticorePart extends
rigid-object behaviour with additional magnetic functionality.
The third route is lightweight aliasing, and this is important because it is
what stops the object library from ballooning for trivial variations. Very
often you do not need a conceptually new implementation. You need a mnemonic
specialisation of an existing primitive or rigid object, with different
metadata, a different preset resource, or a clearer semantic identity in a
script. In that case, a thin alias-style class is enough. The rigid-object
side makes this especially explicit through config['alias'], which selects
resources/<alias>.txt. Classes such as
RaspberrySphere and
MulticorePart largely
reuse GenericRigidObj and pin a
particular alias or add only a little extra behaviour. The same general idea
applies more broadly: if the difference is mostly one of identity, preset
configuration, or mnemonic clarity, a lightweight alias is preferable to a new
heavy implementation.
Put differently: use the metaclass contract first, inheritance when behaviour really changes, and alias-style specialisation when what you mostly need is a stable name and a preset shape inside the library.
Feature requirements fit naturally into the same story. Object-level checks
come from required_features and are enforced during
store_objects(). Method-level checks
happen inside the relevant methods. On the current branch,
EGGPart requires
EGG_MODEL,
SWPart requires
THERMAL_STONER_WOHLFARTH, and classes derived from
GenericRigidObj require
VIRTUAL_SITES_RELATIVE.
3.4. Minimal Template¶
The following skeleton shows the smallest direct use of the metaclass. It is not a universal recipe, but it does capture the contract that every custom object class must satisfy.
from pressomancy.object_classes.object_class import Simulation_Object, ObjectConfigParams
from pressomancy.helper_functions import PartDictSafe, SinglePairDict
class MyObject(metaclass=Simulation_Object):
required_features = []
numInstances = 0
simulation_type = SinglePairDict("my_object", 123)
part_types = PartDictSafe({"real": 1})
config = ObjectConfigParams(my_param=1.0)
def __init__(self, config: ObjectConfigParams):
self.sys = config["espresso_handle"]
self.params = config
self.associated_objects = config["associated_objects"]
self.who_am_i = MyObject.numInstances
MyObject.numInstances += 1
self.type_part_dict = PartDictSafe({"real": []})
def set_object(self, pos, ori):
part = self.add_particle(type_name="real", pos=pos, rotation=(True, True, True))
part.director = ori
return self
For a true container object, set_object usually should not create the
entire structure directly. It should translate the received placement context
into local placements for its associated_objects and delegate to them.
That is how
Quadriplex,
Filament, and
TelSeq work.
One practical rule matters more than the template itself: register objects
before you place them.
store_objects() recursively stores
child objects, binds modify_system_attribute, and updates the global
particle-type bookkeeping. Without that step, the object may exist as a Python
instance, but it is not yet integrated into the shared simulation workflow.
3.5. Common Mistakes¶
The most common authoring mistake is to treat size and build_function
as descriptive extras. They are not. size is the object’s statement about
how much global room it occupies. build_function is the object’s statement
about what local arrangement should be generated within that room. If either
one is wrong, placement problems will surface later and often in misleading
ways.
The second common mistake is to treat associated_objects as passive data.
In composite classes, child objects are part of the construction logic itself.
A robust set_object implementation should therefore be explicit about the
assumptions it makes: exact child counts, expected object types, required
part-type keys, and any geometry-specific invariants. The existing composite
classes are useful templates precisely because they encode those assumptions
openly instead of relying on informal convention.
If you want to see the same ideas applied to a concrete scientific example, continue to G-quadruplex Assembly.