Design Philosophy
If you want to extend the functionality of pydmqmc, perhaps by adding support for new Systems or Methods, there are a few key design choices that you should keep in mind. These elements of pydmqmc’s design philosophy are summarized in the following section and explained in more detail below. Implementation discussions are kept at a high level; for concrete details on how to implement your own classes see the Writing New Systems and Writing New Methods documentation.
Note that pydmqmc uses the object oriented programming design paradigm. If you are unfamiliar with object oriented programming, check out the Primer on Object Oriented Programming.
Overview: Design Principles of pydmqmc
Minimize Code Duplication: significant functionality should be implemented once and then reused.
Maximize Interoperability: any System should be useable with any Method (if enough information is present).
Base Classes as Templates: base classes show what quantities Systems and Methods should have defined.
System Objects are Static: systems cannot have their defining traits modified after creation.
Calculations Only Run Once: once a Method’s calculation has been completed, the data can’t be modified.
Minimize Code Duplication
If you find yourself writing a piece of code over and over again, maybe it’s time to write a new function instead. This ethos is otherwise known as abstraction: significant pieces of functionality should be implemented only once.
The reasons for this are twofold: reliability and maintainability. If a piece of functionality is only written once, it’s easier to test since we only need to write one test to trust that it works everywhere. The more you duplicate functionality, the more tests you have to write to make sure every unique instance works as intended and doesn’t break. This leads into maintainability: if someone uncovers a bug or wants to implement a new feature, good abstraction means we only have to update the source code once. We don’t have to track down every instance of the same functionality and make sure they’re all updated.
In order to achieve good abstraction, pydmqmc makes heavy use of class inheritance.
For instance, the Method class implements a very minimal
run() method. This method ensures that
run() is only called once per Method object.
Child classes are free to add more functionality (in fact, they should) but
checking whether or not run() has been called before
does not need to be rewritten for every child class individually.
What does the principle of abstraction mean for you as a developer? You may need to
write your own base classes (like DensityMatrixQMC)
and inherit from them if you’re adding, say, a family of methods. Alternatively,
if you find yourself duplicating functionality already present in an existing class,
consider abstracting it to a shared base class or making your new class the child of
the existing one. Note that Python classes can inherit from multiple classes!
You’ll see more detailed examples of how to work with pydmqmc’s class inheritance in the Writing New Systems and Writing New Methods documentation.
Maximize Interoperability
The Systems and Methods in pydmqmc should be written such that any System can be used with any Method, provided the user has supplied enough information. Any restrictions should be handled with errors.
Take for example the MatrixHamiltonian system and the
InteractionPictureDMQMC method. If we want to use the
"random-grand-canonical" initialization method with IP-DMQMC’s
setup(), the
MatrixHamiltonian system must have some notion
of how many electrons are in the system as well as the system’s eigenvalues.
Neither of these are required for a MatrixHamiltonian system
to be defined, so InteractionPictureDMQMC must
check for them before attempting the "random-grand-canonical" initialization.
If the required values are not found, an error is thrown to alert the user accordingly.
How do you know which values are expected to be in an object? This brings us to the next design principle.
Base Classes as Templates
The base classes of pydmqmc don’t just allow us to Minimize Code Duplication; the attributes of a base class serve as a template showing what quantities a System or Method object can expect to have defined.
Take a look at the base System class. Even though this base
class is not intended to hold actual data or be used directly in scripts
(both characteristics of base classes), we see that it has a number of attributes defined.
Within the source code, all of these attributes are set to None as they should not have a value.
Instead, these attributes are defined within the System class
to show what attributes a child class should have values for.
A system may not be particularly useful if it has no way of setting the
hamiltonian attribute to anything other than None,
for instance. Put another way, child classes are expected to overwrite the None attributes
of the System class with their own values.
That doesn’t mean child classes have to have every attribute of
System set to something other than None.
One example is the MatrixHamiltonian
system as explained in Maximize Interoperability.
Another is the hamiltonian
attribute of the Integral system.
It may be computationally expensive to compute the Hamiltonian from the class’s integrals,
so the generate_hamiltonian() method exists instead.
The user may elect to call this method (or use a Method object
that calls it for them, such as DensityMatrixQMC)
whenever the Hamiltonian is actually necessary. Until this function is called,
the Integral system’s hamiltonian
will remain None.
Making base classes like System contain the attributes
a System object can be expected to have (or be able to generate)
is important for the Maximize Interoperability principle outlined above.
The Method base classes (including Analytic
and Iterative) also follow this principle,
though they dictate the presence of fewer attributes.
More information on how to follow this principle is contained in the Writing New Systems
and Writing New Methods documentation.
System Objects are Static
The pydmqmc library is written so that every unique physical system is represented
by its own System object. If you want to work with a new
or slightly modified system, you will need to create a new object.
This behavior can be relied on by Methods, which do not allow users to accidentally
erase data once a calculation has been completed.
Say, for example, you have a MatrixHamiltonian system
with 10 electrons. You’ve run a calculation using this object and now you want to
perform the same calculation using basically the same system but with 20 electrons.
If you’ve worked with other Python libraries, you might expect to be able to
do something like:
from pydmqmc.systems import MatrixHamiltonian
# Start with a 10 electron system
sys = MatrixHamiltonian("tests/inputs/hamiltonians/EQUILIBRIUM-H6-STO3G.hamil",
n_electrons = 10)
# Do something with our sys object, like include it in a Method object
# Try to update our system to use 20 electrons
sys.n_electrons = 20
This will fail. In particular, it will throw the following error:
AttributeError: property 'n_electrons' of 'MatrixHamiltonian' object has no setter
The appropriate way to handle this is to make a new object:
sys2 = MatrixHamiltonian("tests/inputs/hamiltonians/EQUILIBRIUM-H6-STO3G.hamil",
n_electrons = 20)
What does this mean for developers? Take a look at that error message again:
“property ‘n_electrons’ of ‘MatrixHamiltonian’ object has no setter.” Objects
will often have methods known as getters and setters. These names
refer to the method’s purpose: getters will retrieve the current value of an
attribute while setters will update it. The pydmqmc library does not
define setter methods for individual attributes. Attributes can instead
only be set at initialization or through methods like the
Iterative class’s setup()
which perform multiple tasks.
This design choice means that pydmqmc uses code to enforce a particular philosophy
about the physical systems it works with: if a system changes one of its attributes
(such as the total number of electrons) it is now a different system. In pydmqmc,
this new, different system must be represented by a new, different object
(sys2 in our example).
In more detail, pydmqmc is able to enforce a “no setters” design by making all
attributes private internally. Access to these private attributes is mediated
by public getters. Using the n_electrons
example again, if you look at the source code for MatrixHamiltonian
you’ll see that the private attribute _nel is used throughout most of the code.
Public access is defined through a special method in the base
System class:
@property
def n_electrons(self) -> int | None:
"""Total number of electrons."""
return self._nel
The special @property decorator means that the n_electrons method is a
getter. To a pydmqmc user, this method functions like an attribute named n_electrons
(and will even show up in the API Reference as attribute).
Note that these @property getter methods enforce what private attribute names
should be used by child classes as part of the Base Classes as Templates philosophy!
Why go through this process? Why not just give System classes an attribute
called n_electrons instead of a private _nel attribute and a getter method?
For example, let’s say we have a class like:
class MySystem():
def __init__(self, initial_electrons):
self.num_electrons = initial_electrons
In Python, this public num_electrons attribute doesn’t need a getter or setter.
It can be retrieved and set via dot notation:
my_sys = MySystem(initial_electrons = 10)
print("Initial electrons:", my_sys.num_electrons)
my_sys.num_electrons = 20
print("Current electrons:", my_sys.num_electrons)
This will produce the following output:
Initial electrons: 10
Current electrons: 20
This produces a situation that pydmqmc is explicitly trying to avoid; a system that can be modified after creation.
Confused? Overwhelmed? Don’t know what this means for writing your own classes? Don’t worry; explicit steps and guidelines are laid out in the Writing New Systems documentation. If you didn’t understand this design guideline on the first pass, try coming back after following along with that guide.
Calculations Only Run Once
Much like how System Objects are Static, Methods cannot have their data
modified after their run() methods
have been invoked. This prevents data from being erased.
This is controlled via the ran_calculation
attribute. This attribute is set via the base Method
class’s run() method. This is why using class
inheritance as shown in Writing New Methods is so important; properly
calling the base run() method will ensure
that data cannot be erased after a calculation has been run.
This is also why calculation data must only be accessible via special getter methods for private attributes, just like the static System attributes. For more details, see Writing New Methods.