Skip to content

Ensembles and energy comparisons

A Space is a container for State objects. It supports loading trajectories and calculation folders, collecting energies, sorting states, generating calculation inputs, and delegating to conformer comparison tools. This tutorial focuses on metadata and energy workflows; conformer clustering covers structural similarity.

Build an ensemble

Space(states=None, theory=None, input_path=None, output_path=None) creates an empty list when states is omitted. If you provide a list, the container retains that list and its State objects. Use copy.deepcopy when changes must be isolated.

from matmalib import Space, State

# Synthetic hartree values for learning the arithmetic.
space = Space(states=[
    State(id="A", E=-100.000, F=-99.980, H=-99.970),
    State(id="B", E=-100.003, F=-99.981, H=-99.971),
    State(id="C", E=-99.998, F=-99.982, H=-99.968),
])
print("Electronic energies:", space.energies)
print("Free energies:", space.free_energies)
print("Enthalpies:", space.enthalpies)
print("Relative E (kcal/mol):", [round(x, 3) for x in space.diff(quant="E")])
print("Relative F (kcal/mol):", [round(x, 3) for x in space.diff(quant="F")])

Electronic energies rank B lowest, while free energies rank C lowest. diff subtracts the minimum within the selected states, then multiplies by 627.5095 to convert a hartree difference to kcal/mol. The E differences above are approximately [1.883, 0.0, 3.138], in the original state order. These are illustrative numbers, not computed molecular results.

Try changing: lower A's F to −99.984 and see how the free-energy reference changes. Choose the appropriate quantity consistently for your physical comparison; E, F, and H are not interchangeable.

The three collection properties skip states without the requested attribute. As a result, len(space.energies) can be smaller than len(space.states) and its positions need not match the original state positions. Check completeness before associating values with names. diff, in contrast, raises AttributeError when a selected state lacks the requested field.

Select, sort, and sum

from matmalib import Space, State

space = Space(states=[
    State(id="A", E=-100.000),
    State(id="B", E=-100.003),
    State(id="C", E=-99.998),
])
print("A/C differences:", space.diff(states=[0, 2], quant="E"))
print("Sum for A + C (hartree):", space.sum([space.states[0], space.states[2]], quant="E"))

space.sort(val="E")
print("Sorted names:", [s.id for s in space.states])
for state, relative in zip(space.states, space.diff(quant="E")):
    print(f"{state.id}: {state.E:.6f} hartree; relative {relative:.3f} kcal/mol")

diff(states=[0, 2]) accepts a list of indices, while diff(states=[state_a, state_b]) accepts a list of State objects. Omitting states uses the whole ensemble. An empty list also falls back to the whole ensemble; an empty Space has no minimum and cannot be compared. Use quant="E", "F", or "H"; other names raise ValueError.

sum(states, quant="E") adds raw values and returns their original units. Pass State objects, not a list of indices. As a historical quirk, any integer passed as states means “sum every state”; use space.sum(space.states) for clearer code. An unknown quantity or missing attribute prints an error and returns None rather than raising.

sort(val="E") sorts space.states in place by any existing sortable attribute; it returns None. On failure it catches the error and prints a message. Index selections therefore refer to the order after any prior sorting.

Try changing: request diff(states=[space.states[0], space.states[1]]) after sorting and identify which names those positions now refer to. For a reaction energy, sum explicitly selected reactant and product states separately and subtract those totals; diff is a minimum-relative conformer comparison, not a stoichiometric reaction calculator.

Add and remove states

space.add(state) appends one state; space.add([a, b]) appends a list. It retains references and does not reject duplicate objects or IDs. Use unique IDs before exporting files.

from matmalib import Space, State

space = Space()
space.add(State(id="A", E=-100.0))
space.add([State(id="B", E=-100.1), State(id="C", E=-99.9)])
print("After adding:", [state.id for state in space.states])

# The current Space.remove implementation has a local-variable bug.
# Use ordinary list operations on the public states collection.
removed = space.states.pop(1)
print("Removed:", removed.id)
print("Remaining:", [state.id for state in space.states])

Space.remove(state) currently raises UnboundLocalError before removing anything because it shadows Python's type function. Use space.states.remove(state_object) or space.states.pop(index). When deleting several indices, delete in descending order to avoid shifting the remaining positions.

space.table(sort="E") sorts the states in place, prints a tabulate table, and returns None. It is intended for states with id and a consistent set of E, F, and H fields. Its fixed headers can mislabel rows when energy attributes are missing. It also requires the optional tabulate package, which is not declared in the package's current installation dependencies.

from matmalib import Space

# Install tabulate separately if your environment does not already provide it.
space = Space.from_dir("calculations", software="gaussian")
space.table(sort="F")

For partial metadata, the explicit loop in the previous runnable example gives clearer control over column labels and missing values.

Compare geometry and reduce duplicates

The Space convenience methods delegate to the dedicated conformer functions. They return analysis results without replacing space.states:

from matmalib import Space

space = Space.from_xyz("data/glucose_puckers.xyz")
matrix = space.rmsd_matrix()
result = space.cluster(threshold=0.25)
unique = space.remove_duplicates(threshold=0.10)

print("RMSD matrix shape:", matrix.shape)
print("Cluster representatives:", len(result.representatives.states))
print("States before deduplication:", len(space.states))
print("Representatives after deduplication:", len(unique.states))

rmsd_matrix(**kwargs) uses aligned heavy-atom RMSD by default, returning a symmetric (n_states, n_states) array in coordinate units. cluster(threshold=1.0, **kwargs) returns a ClusterResult. remove_duplicates(threshold=0.1, **kwargs) returns a new Space using complete linkage, so every pair within a duplicate group is at most the threshold apart. It keeps the original container intact; representative State objects are shared. Do not pass another method argument to remove_duplicates, because it fixes that argument internally.

Try changing: increase the duplicate threshold to 0.25 Å and compare the representative count. All frames must describe corresponding atoms in the same order. See conformer tutorials for atom selections, alignment, linkage, representative selection, and the fields on ClusterResult.

Load calculations and write the next batch

Use Space.from_dir(path, software="gaussian") for immediate subdirectories containing input.log; use Space.from_xyz(path, glycans=False) for XYZ frames. input_path is stored metadata and is not automatically read by the constructor. output_path and theory are used by to_file(software="gaussian").

The default theory dictionary contains historical settings including mem="60B" and jobtype="opfreq"; provide explicit settings appropriate to your calculation rather than treating these as validated defaults. The I/O tutorial includes a complete writable Gaussian example, output layout, all required theory keys, and the working XYZ export path.