Analyzing APE output

Use soda_analyzer to inspect the states, elementary processes, and local environments recorded by APE. The same functions apply to the EAM and MACE examples.

Run the Python snippets after the calculation has finished, from its run directory, with the matching SODA environment activated. If you only have a copied database, follow the standalone analyzer setup and replace output/output.h5 with its path.

What the database contains

Item

Meaning

State

A registered structure, identified by stateID.

Process

A transition rooted at a state, identified by that state’s procID, with reactant, saddle, and product structures.

Environment record

One local atomic environment in the descriptor registry, linked to its source structure and atom.

Environment cluster

A group of similar local environments used by the DECAF classifier to guide APE sampling.

Process structure selectors use rsp=0 for the reactant, rsp=1 for the saddle, and rsp=2 for the product. A process ID is local to its state, so retain both IDs when reporting a transition.

Results at each phase

Function signatures show key arguments; ... marks additional options in the linked API reference.

APE accumulates states, processes, and environment information throughout exploration; these queries let you inspect each result as saved.

Result to inspect

Helper or property

Registered minima

db.state_list with state_load=True, or db.read_state(stateID, proc_load, structure_load), then state.atoms.

Search attempts, when summaries were saved

Read each state’s summary_details table; see APE search summaries.

Discovered processes and their three structures

state.get_proc_list(structure_load=None), then process.get_rsp_atoms().

Process-to-state connections

db.read_process_product_states().

Learned environment clusters

envlib.summarize_clusters(stateID=None, …) and envlib.get_cluster_members(cluster_id, …).

db.state_list holds the registered-state catalogue when state loading is enabled. db.read_process_product_states() returns the resolved connections, mapping (state_id, process_id) to product_state_id; it is empty if the connection table is absent. Several processes can lead to the same registered state. Use the state and process objects for structures and barrier data, and db.environment_lib for environment queries as shown below.

Read states and barriers

This opens the database read-only and lists the recorded processes:

import soda_analyzer as soda

db = soda.SodaLibrary(
    "output/output.h5",
    workflow="ape",
    state_load=True,
    proc_load=True,
    structure_load=False,
    env_load=True,
    storesummariesinfile=True,
)
try:
    print("States:", db.state_num)
    for state in db.state_list:
        print("State:", state.stateID, "Processes:", state.proc_num)
        for process in state.proc_list:
            print("  Process:", process.procID,
                  "Barriers [forward, reverse]:", process.get_barrier_list())

    if db.environment_lib is not None:
        print("Environment clusters:", db.environment_lib.get_cluster_ids())
finally:
    db.close()

Forward and reverse barriers are measured from the reactant and product to the saddle, respectively, and are reported in eV. Counts and ordering can vary with the parallel layout; a short run may have no accepted processes.

The loading switches control how much data is prepared when opening the file. See the soda_analyzer.soda_lib_class reference for their defaults.

State and process functions

Here, state is an entry in db.state_list, and process is an entry in that state’s proc_list.

get_proc_list(structure_load=None)

Load and cache process objects belonging to this state.

get_atoms()

Build an ASE structure from stored coordinates and fixed-atom flags.

get_barrier_list()

Read forward and reverse process activation barriers.

get_energies(rsp=0)

Read energies of the reactant, saddle, and product.

get_prefactor_list()

Read forward and reverse process attempt frequencies.

get_rates(T=300)

Calculate forward and reverse rates from this process summary.

get_stat_repeats()

Read the number of times this process was rediscovered.

get_atoms(rsp=0)

Build an ASE structure from stored coordinates and fixed-atom flags.

get_rsp_atoms()

Build ASE structures for the reactant, saddle, and product.

Keep analysis light for large databases

Use proc_load=False and call state.get_proc_list() only for states you need. For environment-only queries, use state_load=False with env_load=True. Keep the database open while calling helpers, and close it when finished.

Read APE search summaries

Search summaries require --storesummariesinfile=1 when running SODA. Currently, state.search_stats and state.termination_list filter for random and recycled searches, excluding APE’s descriptor-selected searches. Read the stored table directly to count APE attempts and their termination codes:

import numpy as np
import soda_analyzer as soda

db = soda.SodaLibrary("output/output.h5", workflow="ape",
                      state_load=False, structure_load=False)
try:
    total = 0
    for state_id in db.state_id_list:
        table = db.h5.get(f"States/{state_id}/summary_details")
        if table is None:
            continue
        rows = table[()].reshape(-1)
        # Process-search job (1), with an APE-selected search center (5).
        ape_searches = (rows["job"]["type"] == 1) & (rows["job"]["disp_type"] == 5)
        terminations = rows["resultinfo"]["job_termination"][ape_searches]
        codes, counts = np.unique(terminations, return_counts=True)
        total += len(terminations)
        print("State:", state_id, "APE searches:", len(terminations),
              "Termination counts:", dict(zip(codes.tolist(), counts.tolist())))
    if total == 0:
        print("No APE search summaries are stored.")
finally:
    db.close()

These counts include unsuccessful attempts and rediscoveries. They count saved searches, not unique processes. See the termination-code definitions when interpreting the codes.

Export a process trajectory

This selects the first state that has a process and exports its three structures. It does not assume that state 0 or process 0 is available.

from ase.io import write
import soda_analyzer as soda

db = soda.SodaLibrary("output/output.h5", workflow="ape",
                      state_load=True, proc_load=True, structure_load=False)
try:
    for state in db.state_list:
        if not state.proc_list:
            continue
        process = state.proc_list[0]
        images = [
            soda.get_atoms(db.h5, stateID=state.stateID,
                           procID=process.procID, rsp=rsp, delta=True)
            for rsp in (0, 1, 2)
        ]
        write("first_process.xyz", images)
        print("Wrote reactant, saddle, product to first_process.xyz")
        break
    else:
        print("No accepted process is available yet.")
finally:
    db.close()

Open first_process.xyz in an atomistic viewer to inspect the moving atoms. The low-level soda.get_atoms(...) call uses delta=True to reconstruct a process structure from the stored changes. process.get_rsp_atoms() is a shorter way to obtain the same three images.

Inspect environment clusters

Start by listing cluster summaries, then trace one cluster member back to its structure and atom:

import soda_analyzer as soda

db = soda.SodaLibrary("output/output.h5", workflow="ape",
                      state_load=False, structure_load=False, env_load=True)
try:
    envlib = db.environment_lib
    if envlib is None:
        print("No environment data is present.")
    else:
        for summary in envlib.summarize_clusters():
            print("Cluster:", summary["cluster_id"],
                  "Records:", summary["registry_row_count"],
                  "Unique descriptors:", summary["unique_full_descriptor_count"])

        cluster_ids = envlib.get_cluster_ids()
        if cluster_ids:
            cluster_id = cluster_ids[0]
            rows = envlib.get_cluster_members(cluster_id)
            print("First member:", envlib.get_trace(rows[0]))
            print("Atom:", envlib.get_atom(rows[0]))
            representative = envlib.get_representative_environment(cluster_id)
            print("Representative registry row:", representative.registry_row_id)
        else:
            print("No assigned environment clusters are present yet.")
finally:
    db.close()

A registry row identifies an environment occurrence, while full_descriptor_id points to its descriptor vector. Several records may reference the same vector, so registry-row and unique-descriptor counts need not match. A trace’s negative process_id denotes a state-level environment rather than a process structure.

Environment functions

These methods belong to envlib = db.environment_lib:

get_cluster_ids(stateID=None, ...)

Read cluster IDs present in the selected source structures.

summarize_clusters(stateID=None, ...)

Summarize cluster membership, source structures, and provenance.

get_cluster_members(cluster_id, ...)

Select registry indices assigned to one cluster.

get_registry_members(cluster_id=None, ...)

Select descriptor registry indices using structure and cluster filters.

get_trace(registry_row_id)

Read source-structure identifiers and provenance for a registry row.

get_cluster_member_traces(cluster_id, ...)

Trace cluster members back to their source atoms and structures.

group_cluster_members_by_structure(cluster_id, ...)

Group cluster-member traces by their source structure.

get_clusters_for_structure(stateID, procID=None, rsp=None, ...)

Read cluster IDs appearing in a state or process structure.

get_representative_environment(cluster_id, ...)

Select a representative environment from one cluster.

get_atoms(registry_row_id)

Read the full source structure associated with a registry row.

get_atom(registry_row_id)

Read the central atom associated with a registry row.

export_cluster_members(cluster_id, path, ...)

Write source structures containing members of one cluster.

get_embedded_descriptors_by_registry_rows(registry_row_ids)

Read embedded descriptor vectors for selected registry indices.

get_full_descriptors_by_registry_rows(registry_row_ids)

Read full descriptor vectors referenced by registry indices.

get_cluster_birth_cycles(temporary=False, ...)

Read the earliest known update cycle for each cluster.

get_model_meta()

Copy the descriptor model metadata.

Compare physical environments across runs

Cluster numbers are labels within a run. Changing the descriptor or its embedding can change the labels and groupings, so compare representative structures and atoms when comparing EAM and MACE explorations. More clusters or processes alone does not establish convergence.

The detailed APE analyzer reference provides additional examples for cluster exports, descriptor statistics, and restoring the saved DECAF model. Basic database queries above do not require loading the force potential or MACE model.

See the generated API reference for full signatures, parameters, and return values. The query summaries above come directly from the source docstrings.