Overview
The quansino
package allows you to easily create flexible and modular Monte Carlo simulations in Python. Below is an overview of the main concepts and components of the package.
The MonteCarlo
Class¶
The MonteCarlo
class is the core of the quansino
package. The role of this class is to manage the simulation process, including:
- Initializing the simulation with a set of parameters.
- Running the simulation for a specified number of iterations or convergence criteria.
- Managing the observers and their potential files.
- Yielding the move that are going to be run.
In quansino
, a simulation can be summarized via the following pseudo-code:
iterate over steps:
iterate over moves:
perform move
evaluate criteria
save or revert state
call observers
if "converged":
stop
else:
continue to next step
This skeleton is what is run when you call the run
method of the MonteCarlo
class. The method call the step
method, max_steps
times, which in turn calls the yield_moves
method to select the moves to be performed. At the end of each step, the observers are called to perform their actions, such as saving the state or logging information.
Convenience methods such as irun
or srun
are provided to run the simulation with more or less flexibility:
for steps in mc.srun(max_steps=1000):
pass
# Any custom code here will be run after each step, before observers are called
for steps in mc.irun(max_steps=1000):
for move_name in steps:
pass
# Any custom code here will be run before each move is performed
# Any custom code here will be run after each step, before observers are called
Users seeking even more flexibility are free to subclass the MonteCarlo
class and override the step
and yield_moves
methods to implement custom logic.
To perform the simulation, the MonteCarlo
class calls multiple objects each aiming to perform a specific task. These objects will be described in the following sections.
Move
Classes¶
Move
classes are responsible for defining actions that can be performed during the simulation. Each move when called perform geometric, exchange, or other types of operations on the system. The Move
class is defined as a Protocol
, allowing users to create their custom classes, and pass them in the Monte Carlo simulation, which take the Move
protocol as a Generic Type.
For a more focused approach, the package provide the BaseMove
class and their subclasses, such as DisplacementMove
, ExchangeMove
, and CellMove
.
Each of these move classes implements the __call__
method, which is invoked during the simulation to perform the move. In the case of DisplacementMove
, it will attempt to displace a particle selected randomly from the system. These behaviors are heavily tunable and can be customized by passing parameters to the move classes.
from quansino.moves import DisplacementMove
move = DisplacementMove(
labels=[0, 1, 1, 2, 2, 2, -1, -1],
operation=Ball(0.1),
apply_constraints=True, # ASE constraints will be applied when performing the move.
) # This move will attempt to displace one particle each time it is called.
Warning
The word "particle" is used as a general term to refer to any entity in the simulation that is considered a single unit, such as a single atom, molecules, or other entities. In the above example, the labels represent which atoms are grouped together as a "particle". Atoms with the same label are considered part of the same particle, and the move will move the entire particle as a single unit, allowing molecular displacements.
Operations
will be detailed in the next section, but it is important to note that it defines the action that will be performed on the particle. In this case, Ball(0.1)
will displace the particle by a random distance of maximum 0.1 Å in a ball around the particle's current position.
If users want to move multiple particles at once (per criteria), or perform more complex actions, they can simply add moves together:
from quansino.moves import DisplacementMove, ExchangeMove
# Create a move that will attempt to displace one particle and add/remove another particle
weird_move = DisplacementMove(...) + ExchangeMove(...)
# Multiplication is also supported, allowing to repeat the same move multiple times
multi_displacement_move = (
DisplacementMove(...) * 10
) # Will attempt to displace 10 particles per criteria
Operation
Classes¶
Operations
based classes define the specific actions that can be performed on the system during the simulation. The Operation
class is also defined as a Protocol
, allowing users to easily create custom operations. Sensible defaults are provided in the package, such as Ball
, Box
, Sphere
. Most of the time these classes only define a single calculate
method, which takes a Context
as an argument and returns the displacement/deformation vector to be applied to the particle/box.
from quansino.operations import Ball, Translation
operation = Ball(
0.1
) # Displaces particles by a random distance of maximum 0.1 Angstroms in a ball around the particle's current position
displacement_vector = operation.calculate(
...
) # Returns a random displacement vector used in the move.
operation = Translation() # Translates particles randomly in the unit cell.
Most of the time, operations are used in the __call__
method of the Move
class, and is checked against a user-defined criteria check_move
Callable
attribute. This can be used to define constraints, such as ensuring that particles do not overlap or go outside of a defined box. By default, operations are attempted a limited amount times, and the first successful one is used. This can be customized by modifying the max_attempts
attribute of the BaseMove
class.
Criteria
Classes¶
Criteria
based classes are used to evaluate whether a move is successful or not. They define the conditions that must be met for a move to be accepted. The Criteria
class is also defined as a Protocol
, allowing users to create custom criteria. When adding a move to a simulation, the criteria can be passed as well. The package provides several built-in criteria, such as CanonicalCriteria
, GrandCanonicalCriteria
, and IsobaricCriteria
. These classes implement the evaluate
method, which takes the context as argument.
from quansino.mc.criteria import CanonicalCriteria
criteria = CanonicalCriteria()
criteria.evaluate(context) # Returns True if the move is accepted, False otherwise
Context
Class¶
The Context
class provides the necessary information about the current state of the simulation. It encapsulates all relevant data that may be needed by moves, operations, and criteria to make decisions. This includes information about the system's configuration, (Atoms object), temperature, pressure, and other simulation parameters. The Context
class lives both in the MonteCarlo
class and in the Move
class, allowing to access the context from both places. The context is passed to Operations
and Criteria
classes when they are called, allowing them to access the necessary information to perform their actions.
Each kind of simulation (e.g., canonical, grand canonical) have its own context (see DisplacementContext
, DeformationContext
, etc.) which is initialized with the relevant parameters.
Danger
Context
based classes are not meant to be modified by users. It is used internally by the package to manage the state of the simulation. Unless you are implementing a custom move, operation, or criteria, that requires additional information to be passed, you should not need to interact with the Context
class directly. Instead, you can rely on the methods provided by the MonteCarlo
class to access the necessary information.
Find below a diagram summarizing the relationships between the main classes in the quansino
package:
classDiagram
Criteria <-- MonteCarlo
Move <-- MonteCarlo
Context <-- MonteCarlo
class MonteCarlo{
context
moves[MoveType, CriteriaType]
add_move(move, criteria, name, interval, probability)
run(max_steps)
step()
yield_moves()
}
class Context{
atoms
rng
}
class Move{
__call__(context)
}
class Criteria{
evaluate(context)
}
class Operation{
calculate(context)
}
Operation <-- Move
Summary¶
Gathering the information from the previous sections, a typical simulation setup in quansino
would look like this:
import numpy as np
from quansino.mc import Canonical
from quansino.moves import DisplacementMove, ExchangeMove
from quansino.operations import Ball
from quansino.mc.criteria import CanonicalCriteria
from ase.build import bulk
atoms = bulk("Cu", "fcc", cubic=True)
atoms.calc = ...
mc = Canonical(atoms, temperature=300, max_cycles=len(atoms), seed=42)
move = DisplacementMove(
labels=np.arange(len(atoms)),
operation=Ball(0.1),
)
mc.add_move(
move,
criteria=CanonicalCriteria(),
name="displacement_move",
interval=1,
probability=1.0,
)
for steps in mc.irun(1000):
for move_name in steps: # <-- len(atoms) cycles.
print(f"Performed move: {move_name}")
# Alternatively:
mc.run(1000)