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

  1. Minimize Code Duplication: significant functionality should be implemented once and then reused.

  2. Maximize Interoperability: any System should be useable with any Method (if enough information is present).

  3. Base Classes as Templates: base classes show what quantities Systems and Methods should have defined.

  4. System Objects are Static: systems cannot have their defining traits modified after creation.

  5. 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.