-
Notifications
You must be signed in to change notification settings - Fork 1
Description
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:
-
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. -
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 formImplementation Plan
- Define
NodeT = TypeVar('NodeT', bound=Hashable)at module level - Implement
from_graph()classmethod with error handling - Implement
from_base_graph_state()classmethod - Write comprehensive tests
- Add docstring examples
- 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()andfrom_base_graph_state()for clarity - Optional LocalClifford copying enables canonical form extraction