Analyzing EZGA output

Use soda_analyzer to inspect EZGA agents, generations, populations, and evaluated structures. For a quick result from the FCC example, run its supplied analyze.py; the examples below explain the underlying queries.

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

What the database contains

Item

Meaning

Agent

An evolutionary search with its own configuration and population.

Generation

An iteration within an agent’s search; generation 0 holds the initial-population evaluation in the FCC example.

Structure

A stored candidate with identity, provenance, and, when available, evaluated energy and atomic data.

Population membership

A link between a structure and a population view at a particular agent, generation, and stage.

Generation commit

A save record identifying an agent, generation, stage, completion status, and saved-data boundaries.

Results at each phase

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

Optional stages depend on the EZGA configuration. Queries accept agent, generation, or stage filters as described in their API pages.

Result to inspect

Helper

Initial and newly created candidates

get_structures(agent_id=None, generation_created=None, …); generation 0 is the initial evaluation in the FCC example.

Evaluations and objective values

get_rows(table_name, …) with "simulation_result", "structure_metric", or "objective".

Population after a generation

get_population_memberships(agent_id=None, generation=None, …), then read_structure(structure_id=None, agent_id=None, …).

Rejected candidates

get_rejected_structures(agent_id=None, generation=None, …).

Optional HISE stages

get_hise_stages(agent_id=None, stage_index=None, …).

Optional reactivity paths and refinement

get_reactivity_irc_paths(agent_id=None, generation=None, …), then the path’s get_frames_by_kind().

Saved-generation completion

get_generation_commits(agent_id=None, generation=None, …).

Latest completely saved generation

get_latest_complete_commit(agent_id=None, stage_index=None, …).

Rows within a saved generation’s data boundary

get_committed_table_prefix(table_name, commit=None), then filter by agent, generation, and stage.

Recorded convergence checks

get_rows(table_name, …) with "convergence_history".

These methods belong to ezga = db.ezga.

Inspect the latest saved generation

An EZGA search can stop at an arbitrary generation. Use get_latest_complete_commit(agent_id=None, stage_index=None, …) to identify the latest completely saved generation for each agent:

import soda_analyzer as soda

db = soda.SodaLibrary("output/output.h5", workflow="ezga")
try:
    ezga = db.ezga
    for agent_id in ezga.get_agent_ids():
        commit = ezga.get_latest_complete_commit(agent_id=agent_id)
        if commit is None:
            print("No complete generation for agent", agent_id)
            continue
        print("Agent:", agent_id, "Generation:", commit.generation,
              "Stage:", commit.stage_index)
finally:
    db.close()

The returned record contains save metadata, including row counts that delimit saved tables. It does not contain the population structures. Without agent_id, the method selects one record across all agents. “Latest” means the greatest commit ID, with row position breaking ties.

To select an earlier generation or a particular stage, pass those filters: ezga.get_latest_complete_commit(agent_id=0, generation=5, stage_index=0). The result is None if no matching complete save exists.

Read the saved population

get_population_memberships(agent_id=None, generation=None, …) selects membership rows by agent, generation, and stage, but does not exclude rows beyond a commit’s saved-data boundaries. The following example first uses get_committed_table_prefix(table_name, commit=None) to exclude later incomplete writes, then selects active members and loads their unique structures. Change agent_id to inspect another agent.

import soda_analyzer as soda

db = soda.SodaLibrary("output/output.h5", workflow="ezga")
try:
    ezga = db.ezga
    agent_id = 0
    commit = ezga.get_latest_complete_commit(agent_id=agent_id)
    if commit is None:
        print("No complete generation for agent", agent_id)
    else:
        table = ezga.get_committed_table_prefix("population_membership", commit)
        members = [dict(zip(table.dtype.names, row)) for row in table]
        members = [
            row for row in members
            if row.get("agent_id", 0) == agent_id
            and row["generation"] == commit.generation
            and row["stage_index"] == commit.stage_index
            and row["active"] == 1
        ]

        table = ezga.get_committed_table_prefix("structure", commit)
        rows_by_id = {}
        for row_index, row in enumerate(table):
            row = dict(zip(table.dtype.names, row))
            if row.get("agent_id", 0) == agent_id:
                rows_by_id[int(row["structure_id"])] = row_index

        # Several population views can refer to the same structure.
        structure_ids = dict.fromkeys(int(row["structure_id"]) for row in members)
        for structure_id in structure_ids:
            if structure_id not in rows_by_id:
                raise ValueError(f"Structure {structure_id} is outside this save")
            structure = ezga.read_structure(row_index=rows_by_id[structure_id])
            print(structure.structure_id, structure.potential_energy)
            # structure.atoms contains the ASE Atoms object.
finally:
    db.close()

members retains all active population views; inspect view_kind and view_rank to distinguish them. Structures are loaded once per ID. A structure may have been created in an earlier generation and remain in this population; get_structures(generation_created=...) selects creation history instead. Legacy rows without an agent_id belong to agent 0.

Check convergence separately

A complete save records successful persistence, not search convergence. Inspect ezga.get_rows("convergence_history", agent_id=..., generation=..., stage_index=..., as_dict=True) for the selected generation’s recorded checks, including objective_converged, information_converged, and final_converged when present. Interpret them against the configured convergence criteria; missing records do not establish convergence. To stay within a selected save, apply the same table-prefix filtering shown above to "convergence_history".

Inspect rejected candidates

Population constraints can reject a candidate before dispatch or after its calculation. Their failures share reason code 6 (native_validation_failed) with native energy-bound (ef_bounds) failures. The records do not identify which predicate failed or report constraint rejections as a separate count. Observable blacklist hits use code 3 (blacklisted).

To inspect rejection records within a complete generation save:

import soda_analyzer as soda

db = soda.SodaLibrary("output/output.h5", workflow="ezga")
try:
    ezga = db.ezga
    agent_id = 0
    commit = ezga.get_latest_complete_commit(agent_id=agent_id)
    if commit is None:
        print("No complete generation for agent", agent_id)
    else:
        table = ezga.get_committed_table_prefix("rejected_structure", commit)
        for row in table:
            if (row["agent_id"] == agent_id
                    and row["generation"] == commit.generation
                    and row["stage_index"] == commit.stage_index):
                print("Reason:", row["rejection_reason"],
                      "Candidate hash:", row["candidate_hash"])
finally:
    db.close()

For a direct query, ezga.get_rejected_structures(..., as_dict=True) also decodes rejection_reason_name, but does not restrict rows to a commit’s saved-data boundary. Use the table-prefix approach above when a database may contain incomplete writes.

These are compact metadata records. Constraint rejection rows have structure_id = -1; they do not save the rejected geometry or feature row for read_structure(). rejection_value is an energy-like value when available, not the violated feature or bound; before calculation it may still reflect a parent’s energy. A generation can have rejection records and a complete save even when every proposal was rejected before dispatch and no worker calculation ran.

Inspect agents and generations

import soda_analyzer as soda

db = soda.SodaLibrary("output/output.h5", workflow="ezga")
try:
    ezga = db.ezga
    print(ezga.summarize())
    for agent_id in ezga.get_agent_ids():
        print("Agent:", agent_id)
        generations = ezga.get_generations(agent_id=agent_id)
        print("Generations with complete saves:", generations)
        for generation in generations:
            commit = ezga.get_latest_complete_commit(
                agent_id=agent_id, generation=generation,
            )
            print("Generation:", generation, "Latest complete stage:", commit.stage_index,
                  "Commit:", commit.commit_id)
finally:
    db.close()

get_generations() lists generation numbers with at least one complete save; other stages in the same generation may still be incomplete. The loop reports the latest complete stage for each generation. Pass stage_index to both queries to restrict the listing to a particular stage.

To read a population, use the commit boundaries in Read the saved population. Membership rows can include several population views and are not a count of unique candidates; use view_kind and active to select a view. The detailed EZGA analyzer guide describes them.

Export the lowest-energy FCC structure

For this fixed-composition example, compare finite evaluated energies and exclude the seed placeholder before choosing a structure:

import numpy as np
from ase.io import write
import soda_analyzer as soda

db = soda.SodaLibrary("output/output.h5", workflow="ezga")
try:
    structures = db.ezga.get_structures(payload_load=True)
    evaluated = [
        structure for structure in structures
        if structure.potential_energy is not None
        and np.isfinite(structure.potential_energy)
        and structure.potential_energy != 9999.0
    ]
    if evaluated:
        best = min(evaluated, key=lambda structure: structure.potential_energy)
        lattice = abs(np.linalg.det(best.cell)) ** (1 / 3) / 2
        print("Structure:", best.structure_id)
        print("Energy per atom [eV]:", best.potential_energy / best.atom_count)
        print("Effective FCC lattice constant [angstrom]:", lattice)
        write("best_fcc.xyz", best.atoms, format="extxyz")
    else:
        print("No evaluated structures are available yet.")
finally:
    db.close()

The division by two assumes the supplied 2 × 2 × 2 conventional supercell. For a distorted cell this is an effective volume-based lattice constant; also inspect the cell shape and atomic positions.

Match the comparison to the search objective

The lowest total energy is meaningful for the FCC example because every candidate contains the same 32 Ar atoms. For variable composition, different sizes, or multiple objectives, use the configured objectives and appropriate references. A low-energy structure alone does not establish that the search or relaxation has converged.

Query functions

These methods belong to ezga = db.ezga. Keep the database open while loading structures and their atomic data.

summarize()

Summarize EZGA agents, table counts, commits, and optional payload coverage.

get_agent_ids()

Return the agent IDs present in metadata, commits, or structures.

get_generations(agent_id=None, complete_only=True, ...)

Return generation numbers, restricted to complete commits by default.

get_generation_commits(agent_id=None, generation=None, ...)

Return generation commit records matching the supplied filters.

get_latest_complete_commit(agent_id=None, stage_index=None, ...)

Return the complete commit with the greatest commit ID and row index.

get_committed_table_prefix(table_name, commit=None)

Read the table prefix covered by a generation commit.

get_structures(agent_id=None, generation_created=None, ...)

Return structure records filtered by their creation metadata.

get_population_memberships(agent_id=None, generation=None, ...)

Return generation population memberships and view flags.

read_structure(structure_id=None, agent_id=None, ...)

Read a structure by row index or agent and structure ID.

get_rows(table_name, ...)

Select EZGA table rows using column filters.

get_rejected_structures(agent_id=None, generation=None, ...)

Return structure rejection records for matching generations.

get_hise_stages(agent_id=None, stage_index=None, ...)

Return HISE stage records with optional status filtering.

A structure’s .atoms property returns an ASE Atoms object; .potential_energy, .cell, and .atom_count support numerical analysis. Filter by agent and stage when combining results from larger searches. Use the EZGA analyzer reference for population views, optional SAGE fields, and advanced export recipes.

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