Magnetic resonance benefits from a variety of computational tools designed to simulate phenomena in both liquid and solid-state systems. Popular simulation packages include SpinDynamica, Spinach, SPINEVOLUTION, GAMMA, EasySpin, and SIMPSON. However, many of these tools rely on commercial platforms such as MATLAB or Mathematica, which may not be accessible to all users. Additionally, even when open-source, the complexity of the code can make it challenging to understand the underlying spin physics.
In this first post, I introduce PyOR (Python On Resonance) — a versatile NMR simulator designed to address a wide range of magnetic resonance problems. PyOR is particularly useful for learning, teaching, and simulating NMR, making it an excellent resource for both students and educators. One of its key strengths is helping spin physics enthusiasts visualize how textbook equations are translated into Python code. In this post, I’ll highlight some of PyOR’s main features and the “quantum object”.
In future posts, I will demonstrate how to define a spin system, create spin operators, define simulation parameters, construct Hamiltonians, evolve the system, plot and visualize data, and explore many other powerful functionalities offered by PyOR.
In early 2024, I began developing PyOR (Python On Resonance), a compact and object-oriented Python package designed to simulate nonlinear nuclear magnetic resonance (NMR) phenomena. Initially focused on modeling effects such as radiation damping and maser (or “raser”), PyOR has since evolved into a versatile and extensible tool capable of simulating a wide range of magnetic resonance dynamics, encompassing both liquid and solid-state NMR.
Choosing Python as the development platform was an intentional decision: Python is freely available, easy to learn, and widely supported, making it an ideal environment for rapid scientific development. PyOR is particularly suited for beginners who have a basic understanding of matrices, spin operators, and Python programming. It provides an approachable way to code magnetic resonance pulse sequences and relaxation mechanics, serving both as a research tool and an educational platform.
With its modular structure and focus on transparency, PyOR also offers an opportunity for undergraduate and graduate students to explore the rich physics of magnetic resonance through direct simulation and hands-on coding. By blending intuitive design with scientific rigor, PyOR aims to lower the barrier to entry for magnetic resonance simulation and foster deeper engagement with the theory and practice of NMR.
A standout feature of PyOR is its ability to generate any type of spin system, from a single isolated spin to complex multispin networks (tested maximum of 6 spins), each with arbitrary spin quantum numbers. In PyOR, spin systems are defined using a dictionary that maps each spin label to a spin type. For example:Spin_list = {"A" : "H1", "B" : "H2"}
defines a simple two-spin system where the labels A and B correspond to proton and deuterium with spin half and one, respectively.
Once the user defines the spin dictionary, PyOR can construct all the spin operators not just for the whole system, but also for individual subsystems (i.e., for each spin separately). This allows for a lot of flexibility when building Hamiltonians, applying pulses, calculating observables, etc.
Here is an example of how to access the spin operators for both the full system and the subsystem of the previously defined spin-½ particle “A” in PyOR.
More about spin operators will be in my next post.
PyOR provides robust support for simulating relaxation dynamics using both the Redfield and Lindblad master equations, and this can be done in Hilbert or Liouville space, depending on the simulation context.
Currently, PyOR includes implementations for several common relaxation mechanisms, including:
PyOR offers a flexible framework, and users can implement custom relaxation mechanisms to suit their specific needs.
Users can propagate the initial density matrix using two distinct approaches:
One of the key features that distinguishes PyOR from other magnetic resonance simulation tools is its strong focus on code readability and modifiability. PyOR is built to ensure that users can clearly trace how textbook equations are translated into code. With its clean, well-documented Python architecture, even users new to scientific programming can easily follow and understand the implementation.
This level of transparency empowers users not just to run simulations, but to actively extend and adapt the code to meet their specific research needs. Whether it’s introducing a new interaction, tweaking solver behavior, or experimenting with custom relaxation models, PyOR is designed to be a flexible foundation, not a closed black box.
This philosophy makes PyOR especially appealing to students, educators, and researchers who want full control over their simulations and a deeper understanding of the underlying physics.
PyOR is built as a modular Python package, organized into a collection of focused modules. Each module defines a specific class with a well-defined purpose, such as generating spin operators, constructing quantum states, handling time evolution, or implementing relaxation dynamics.
Here are the various modules in PyOR
At the core of PyOR’s design is its object-oriented approach. Key elements of magnetic resonance simulations, such as spin operators, density matrices, quantum states, and Hamiltonians, are treated as objects. We will see this in the following sections.
Python heavily embraces the object-oriented programming paradigm, where objects—instances of classes—encapsulate both data (attributes) and behavior (methods). This approach is central to many scientific computing libraries, including NumPy. In NumPy, arrays are instances of the ndarray
class, which comes with a rich set of methods for efficient numerical operations. An example is shown below:
import numpy as np
data = [1, 2, 3]
# Object: array
array = np.asarray(data)
# Attribute of object 'array'
print(array.shape)
# Output: (3,) -> shape gives the dimensions of the array
# Methode of object 'array'
print(array.mean())
# Output: 2.0 -> mean() computes the average of the array elements
The object, array
has attributes like .shape
, and methods like .mean()
. The function np.asarray()
wraps raw data into a powerful object that carries both data and operations. This is a basic example of object-oriented programming in Python: objects with attributes and methods, derived from a class.
In the previous section, I mentioned that PyOR treats spin operators, quantum states, density matrices, Hamiltonians, and relaxation operators as objects of the Python class QunObj
. This class is inspired by QuTiP. The full implementation of the QunObj
class is available on GitHub:
Each QunObj
instance comes with the following key attributes:
.data
– Returns the object as a NumPy array. .shape
– Returns the dimensions of the object..datatype
-Returns the data type of the NumPy array..matrix
-Returns a symbolic representation of the matrix..type
– Return object type: ‘ket’, ‘bra’, or ‘operator’.And the following are a few methods of the class:
.Adjoint(
) – Return the Hermitian conjugate of the quantum object.Conjugate()
– Return the complex conjugate of the quantum object.Transpose()
– Return the transpose of the quantum object.Trace()
– Compute the trace of the objectUsers can also create their own quantum objects using the QunObj
class. Below are a few examples to illustrate how easily this can be done:
# Define the source path
SourcePath = 'The path to/Source_Doc'
# Add source path
import sys
sys.path.append(SourcePath)
# Import PyOR package
from PyOR_QuantumObject import QunObj
# Vector (ket)
ket1 = QunObj([[1], [0]],PrintDefault=True)
ket2 = QunObj([[0], [1]],PrintDefault=True)
# Matrix (Spin operator operator)
Sx = QunObj([[0.0, 0.5],[0.5,0.0]],PrintDefault=True)
Sy = QunObj([[0.0, -0.5j],[0.5j, 0.0]],PrintDefault=True)
Sz = QunObj([[0.5, 0.0],[0.0, -0.5]],PrintDefault=True)
Id = QunObj([[1., 0.0],[0.0, 1]],PrintDefault=True)
The above example created six objects ket1
, ket2
, Sx
, Sy
, Sz
and Id
; two quantum states (“kets”), three spin operator (“operators”), and identity matrix (“operators”) respectively. Now lets see some attributes and methods of these quantum objects.
.matrix
To obtain the symbolic representation of objects, use the .matrix
attribute as shown below:
ket1.matrix
ket2.matrix
Sx.matrix
Sy.matrix
Sz.matrix
Id.matrix
ket1.matrix
gives the symbolic form of the object ket1
: ket2.matrix
gives the symbolic form of the object ket2
:
Similarly, the Sx.matrix
returns,
.type
ket1.type
ket2.type
Sx.type
Sy.type
Sz.type
Id.type
The ket1.type
and Sx.type
return, 'ket
‘ and 'operator
‘ respectively.
.Rotate()
The method, Rotate
can rotate any objects. It implements either or
for a quantum state or operator, respectively. Where
is a quantum state,
is a density matrix,
is a spin operator and
To rotate ket1
by 180 degrees about the x axis (J = Sx)
ket3 = ket1.Rotate(180,Sx)
ket3.matrix
returns,
To add two objects, simply type:
ket4 = 2 * ket1 + 5 * ket2
ket4.matrix
returns,
For multiplying any two objects:
ket5 = Sx * ket1
ket5.matrix
returns,
.Adjoint()
This method gives the adjoint (conjugate transpose) of any quantum object.
bra1 = ket1.Adjoint()
bra1.matrixcode
return,
To explore more examples of the QunObj
class, click.
In this post, we explored some features of PyOR and introduced its quantum object system. The QunObj
class provides a convenient way to manipulate these objects. In the next post, I’ll show how to create spin operators from a defined spin system. See you then!