Skip to content

Add bulk initialization methods for GraphState: from_graph() and from_base_graph_state() #120

@masa10-f

Description

@masa10-f

Motivation

Currently, constructing a GraphState requires adding nodes and edges one at a time using add_physical_node(), add_physical_edge(), register_input(), etc. This is cumbersome when building larger graphs and forces users to maintain node indices manually.

Proposal

Add two class methods for bulk initialization:

  1. GraphState.from_graph(): Create a GraphState from nodes and edges with arbitrary hashable node identifiers (strings, tuples, etc.). The method returns the GraphState and a mapping from external node IDs to internal indices.

  2. GraphState.from_base_graph_state(): Create a new GraphState by copying from an existing BaseGraphState instance, with optional control over LocalClifford operator copying.

These methods improve usability and enable cleaner graph construction patterns.


API Specification

1. GraphState.from_graph()

@classmethod
def from_graph(
    cls,
    nodes: Iterable[NodeT],
    edges: Iterable[tuple[NodeT, NodeT]],
    inputs: Sequence[NodeT] | None = None,
    outputs: Sequence[NodeT] | None = None,
    meas_bases: Mapping[NodeT, MeasBasis] | None = None,
) -> tuple[GraphState, dict[NodeT, int]]:
    """Create a graph state from nodes and edges with arbitrary node types.
    
    This factory method allows creating a graph state from any hashable node type
    (e.g., strings, tuples, custom objects). The method internally maps external
    node identifiers to integer indices used by GraphState.
    
    Parameters
    ----------
    nodes : Iterable[NodeT]
        Nodes to add to the graph. Can be any hashable type.
    edges : Iterable[tuple[NodeT, NodeT]]
        Edges as pairs of node identifiers.
    inputs : Sequence[NodeT] | None, optional
        Input nodes in order. Qubit indices are assigned sequentially (0, 1, 2, ...).
        Default is None (no inputs).
    outputs : Sequence[NodeT] | None, optional
        Output nodes in order. Qubit indices are assigned sequentially (0, 1, 2, ...).
        Default is None (no outputs).
    meas_bases : Mapping[NodeT, MeasBasis] | None, optional
        Measurement bases for nodes. Nodes not specified can be set later.
        Default is None (no bases assigned initially).
    
    Returns
    -------
    tuple[GraphState, dict[NodeT, int]]
        - Created GraphState instance
        - Mapping from external node IDs to internal integer indices
    
    Raises
    ------
    ValueError
        - Duplicate nodes in input
        - Edge references non-existent node
        - Input/output node not in nodes collection
        - Self-loop edge
        - Duplicate edge
    """

Implementation Notes:

  • Preserve node order by converting to list
  • Check for duplicate nodes
  • Validate all edge endpoints exist
  • Validate all input/output nodes exist
  • Assign sequential qubit indices for inputs/outputs

2. GraphState.from_base_graph_state()

@classmethod
def from_base_graph_state(
    cls,
    base: BaseGraphState,
    copy_local_cliffords: bool = True,
) -> tuple[GraphState, dict[int, int]]:
    """Create a new GraphState from an existing BaseGraphState instance.
    
    This method creates a complete copy of the graph structure, including nodes,
    edges, input/output registrations, and measurement bases. Useful for creating
    mutable copies or converting between GraphState implementations.
    
    Parameters
    ----------
    base : BaseGraphState
        The source graph state to copy from.
    copy_local_cliffords : bool, optional
        Whether to copy local Clifford operators if the source is a GraphState.
        If True and the source has local Cliffords, they are copied.
        If False, local Cliffords are not copied (canonical form only).
        Default is True.
    
    Returns
    -------
    tuple[GraphState, dict[int, int]]
        - Created GraphState instance
        - Mapping from source node indices to new node indices
    """

Implementation Notes:

  • Copy all nodes preserving the node_counter state where possible
  • Copy edges using the node mapping
  • Copy input/output registrations with same qubit indices
  • Copy measurement bases
  • Optionally copy LocalClifford operators (only if source is GraphState)

Usage Examples

Example 1: String Node Identifiers

gs, node_map = GraphState.from_graph(
    nodes=['start', 'middle', 'end'],
    edges=[('start', 'middle'), ('middle', 'end')],
    inputs=['start'],
    outputs=['end'],
    meas_bases={'middle': PlannerMeasBasis(Plane.XY, 0.0)},
)
# node_map = {'start': 0, 'middle': 1, 'end': 2}

Example 2: 2D Grid Graph with Tuple Coordinates

grid_nodes = [(i, j) for i in range(3) for j in range(3)]
grid_edges = [
    ((i, j), (i+1, j)) for i in range(2) for j in range(3)
] + [
    ((i, j), (i, j+1)) for i in range(3) for j in range(2)
]
gs, node_map = GraphState.from_graph(
    nodes=grid_nodes,
    edges=grid_edges,
    inputs=[(0, 0)],
    outputs=[(2, 2)],
)

Example 3: Copy and Modify

# Create initial graph
original = GraphState.from_graph(
    nodes=['a', 'b', 'c'],
    edges=[('a', 'b'), ('b', 'c')],
)[0]

# Create independent copy for modifications
copied, node_map = GraphState.from_base_graph_state(original)

# node_map allows tracking corresponding nodes
# Modifications to 'copied' do not affect 'original'

Example 4: Copy in Canonical Form

# Copy without local Cliffords
canonical_gs, _ = GraphState.from_base_graph_state(
    gs_with_lc, 
    copy_local_cliffords=False
)
# canonical_gs is ready for operations requiring canonical form

Implementation Plan

  1. Define NodeT = TypeVar('NodeT', bound=Hashable) at module level
  2. Implement from_graph() classmethod with error handling
  3. Implement from_base_graph_state() classmethod
  4. Write comprehensive tests
  5. Add docstring examples
  6. Update module documentation if needed

Test Plan

  • test_from_graph_with_string_nodes(): String identifiers
  • test_from_graph_with_tuple_nodes(): Tuple identifiers for grid graphs
  • test_from_graph_with_int_nodes(): Integer nodes (backward compatibility)
  • test_from_graph_with_edges(): Verify edges are correctly added
  • test_from_graph_with_inputs_outputs(): Input/output registration
  • test_from_graph_with_meas_bases(): Full measurement basis specification
  • test_from_graph_partial_meas_bases(): Partial measurement basis specification
  • test_from_graph_errors_duplicate_nodes(): Error on duplicate nodes
  • test_from_graph_errors_invalid_edges(): Error on non-existent node in edge
  • test_from_base_graph_state_simple(): Basic copying
  • test_from_base_graph_state_with_local_cliffords(): Copy with LocalCliffords
  • test_from_base_graph_state_without_local_cliffords(): Copy in canonical form
  • test_from_base_graph_state_preserves_indices(): Qubit indices preserved
  • test_from_base_graph_state_independence(): Modifications don't affect original
  • test_from_base_graph_state_custom_implementation(): Custom BaseGraphState implementations

Related Issues/PRs

This addresses the general issue of cumbersome graph construction. It complements the existing compose() function for combining graph states.

Discussion Notes

  • Using TypeVar for generic node types (Python 3.9+ compatible)
  • Node order preserved for predictable internal index assignment
  • Separated from_graph() and from_base_graph_state() for clarity
  • Optional LocalClifford copying enables canonical form extraction

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions