Conversation
NumPy 1.24 removed np.complex and np.float, which is already the pinned version here, so IO.load_np_array raised AttributeError on any call. Use np.complex128/np.float64 instead.
…ates OneQubitGateWrapper concatenates each gate's openqasm name with no separator (e.g. Hadamard+PhaseDagger+SigmaY -> "hsdgy"), but from_openqasm decoded that one character at a time, silently mis-parsing any multi-character name like PhaseDagger's "sdg". Decode via greedy longest-match instead.
Tests exercising draw_circuit()/plt.show() popped up plot windows on every full test-suite run since draw_circuit defaults to show=True. Set the Agg backend in a top-level tests/conftest.py, loaded before any test module imports matplotlib, so plotting still runs and is still assertable but never opens a window.
`from correlation_module import *` only resolved when run from inside graphiq/data_collection/, so importing this module normally raised ModuleNotFoundError. Use the fully-qualified package path instead.
… unseen label Both methods indexed node_dict[label] directly, raising KeyError whenever no node in the circuit had ever carried that label (e.g. querying for CNOT nodes on a circuit with no CNOT gates) - a real crash hit by several metrics.py/correlation_module.py/ user_interface.py call sites. Use node_dict.get(label, []) so an absent label just contributes nothing, matching "no nodes have this label".
AmplitudeDampingNoise.apply built Kraus operators with itertools.combinations instead of itertools.product. Since amplitude_damping_operators always returns exactly 2 single-qubit operators, combinations silently dropped terms: the channel was non-trace-preserving for 2 qubits and applied zero Kraus operators (no noise at all) for 3+ qubits. Use product, matching the pattern already used by DepolarizingNoise in the same file.
CircuitCnotCount.__init__ set self.n_emitter_penalty instead of self.n_cnot_penalty in the default-penalty branch, so evaluate() crashed with AttributeError on any default-constructed instance. CircuitUnitaryCount.evaluate's gate label list was ["SigmaX", "SigmaX", "SigmaX", ...] instead of ["SigmaX", "SigmaY", "SigmaZ", ...], so SigmaY gates were never counted and SigmaX gates were triple-counted.
…er states The MixedStabilizer branch checked isinstance(state.rep_data, ...) instead of isinstance(rep_data, ...). rep_data holds the converted stabilizer representation (set a few lines above); state.rep_data is the original, pre-conversion representation. Whenever state was a mixed QuantumState not already in stabilizer form, conversion produces a MixedStabilizer, but the check inspected the untouched original, so neither branch matched, fid was never assigned, and evaluate() crashed with UnboundLocalError.
Metrics._all only listed Infidelity, TraceDistance, and CircuitDepth, so Metrics rejected every other metric class defined in this file - even when passed as an already-constructed instance. Register the ones that actually fit Metrics' evaluate(state, circuit) calling convention: CircuitEmitterCount, CircuitCnotCount, CircuitUnitaryCount, CircuitMaxEmitDepth, CircuitMaxEmitResetDepth, CircuitMaxEmitEffDepth, and CircuitMeasureCount. GraphMetric is deliberately left out: its evaluate(graph_metric: str) signature doesn't match that convention, so Metrics could never actually call it - registering it would just turn a clear "not a recognized metric" warning at construction time into a confusing TypeError at evaluation time. Move the Metrics class itself to after all the metric classes it references, so _all can be one plain dict literal again instead of needing a separate post-definition update().
…abilizer classical_registers is a numpy array, but MixedStabilizer.apply_measurement returns a list of per-mixture-branch outcomes, not a scalar. ClassicalCNOT and ClassicalCZ both stored the raw list into classical_registers[op.c_register] via an unconditional/trailing assignment, raising "setting an array element with a sequence" whenever the stabilizer state was mixed. Store outcome[0] in the MixedStabilizer branch and outcome in the plain-Stabilizer branch, matching the already-correct MeasurementZ handling just below. Also drops a leftover debug print() in the ClassicalCNOT branch. Also documents, at each outcome[0]/outcomes[0] site, that this only records one mixture branch's measurement result even though branches can disagree - a pre-existing simplification (see the TODO on MixedStabilizer.apply_measurement), not something this fix resolves.
CompilerBase.compile sends a gate with replacement noise to compile_one_noisy_gate, which applies only the noise's own gate list. OneQubitGateReplacement's Stabilizer branch built an empty gate_list, in both its Stabilizer and MixedStabilizer paths (the latter dead code, since MixedStabilizer isn't a Stabilizer subclass). So on the stabilizer backend HadamardPerturbedError/PhasePerturbedError/ SigmaXPerturbedError (all subclasses of OneQubitGateReplacement) applied neither the noise nor the gate they replace: the state's tableau was unchanged before and after. Since a general one-qubit unitary isn't generally Clifford and can't always be represented exactly in the stabilizer formalism, raise NotImplementedError instead, matching the pattern already used by LocalCliffordError and TwoQubitControlledGateReplacement in this same file.
…rixCompiler compile_one_noisy_gate referenced state.dm at four call sites, but QuantumState has no .dm attribute - compile_one_gate in the same file correctly unwraps via state = state.rep_data, but compile_one_noisy_gate never did that. compile() sends a gate to compile_one_noisy_gate only when it carries replacement noise, so attaching a replacement noise model to a MeasurementZ and compiling with noise_simulation enabled crashed with AttributeError. Use state.rep_data throughout, matching compile_one_gate's convention. (Three of the four occurrences, in the ClassicalControlledPairOperationBase/MeasurementCNOTandReset branches, are currently dead code per compile()'s dispatch, but fixed alongside the live one for consistency.)
…red candidates self.hof is initialized with placeholder entries (np.inf, None). A candidate whose score is np.isclose to an unfilled slot's placeholder took the tie-breaking branch, which dereferenced .dag on the placeholder's None circuit. Treat an unfilled slot as always losing the tie-break (any real circuit fills it) instead of crashing.
… < n_hof self.hof can contain unfilled (np.inf, None) placeholder entries - update_hof fills at most one slot per population member, so whenever n_pop < n_hof (an ordinary solver setting, not an exotic input) some slots are still unfilled after the first iteration. update_logs computed depth_hof over every hof entry unconditionally, calling .depth on the None placeholder circuit and crashing with AttributeError on the very first iteration. Filter out unfilled entries before computing depth_hof. Same root cause as the update_hof fix in solver_base.py, surfacing at a second call site.
replace_photon_one_qubit_op called gate.add_labels("Fixed") on the
gate it put in place of the old one, and remove_op never removes a
"Fixed" node. Remove that line.
On a HybridEvolutionarySolver circuit, whose photon gates carry no
"Fixed" label, a replaced photon gate used to become permanently
unremovable. It now stays removable, like the gates the other mutation
operators add.
In an EvolutionarySolver search that starts from initialization and
uses the default transformations, every photon gate this operator
replaces is one that initialization labelled "Fixed". There the line
was what kept the label, so once replaced such a gate now becomes
removable.
This changes seeded EvolutionarySolver and HybridEvolutionarySolver
searches.
For a pure input, density_to_stabilizer called the unvalidated
_density_to_graph_pure helper, so a pure state that isn't a graph
state was silently converted to the wrong stabilizer instead of
raising, even though the docstring limits the function to graph
states. Use density_to_graph (which validates by default) to match
the mixed-state branch, which already does.
As a result, QuantumState.convert_representation("s") on a pure
density matrix that is not a graph state now raises instead of
returning a wrong stabilizer. That includes the conversion
Infidelity.evaluate makes when the target is a stabilizer and the
state a density matrix.
- trace_distance (density_matrix/functions.py): the docstring was not raw, so \rho, \frac/\right and \text were silently turned into a carriage return, form feed and tab, and \s raised an invalid-escape warning. Add the r""" prefix the module's other LaTeX docstrings already use. - binary_symplectic_product (stabilizer/functions/utils.py): the LaTeX row break was written as three backslashes plus a space, an invalid escape; use four. The runtime docstring text is unchanged. Both are SyntaxWarnings on Python 3.12. Add a test that compiles every .py file in the repo and fails on any invalid escape sequence.
…edError MixedStabilizer is a sibling of Stabilizer, not a subclass, so the "not implemented for stabilizer" arms of OneQubitGateReplacement, TwoQubitControlledGateReplacement, LocalCliffordError, AmplitudeDampingNoise, MixedUnitaryError, CoherentUnitaryError, GeneralKrausError, MeasurementError and ResetError never matched a mixture - which is what a noisy StabilizerCompiler run produces. A mixture fell through to the generic "Backend type is not supported." TypeError instead. Match both classes so the refusal is the same for either representation. Also correct the Graph-arm messages of TwoQubitControlledGateReplacement and LocalCliffordError, which said "stabilizer" instead of "graph".
DepolarizingNoise.apply converts a pure Stabilizer into a one-branch MixedStabilizer before branching, but it assigned the mixture to state.rep_data while state.mixed was still False, so the setter raised "Cannot initialize the stabilizer representation with datatype MixedStabilizer". Past that, the local state_rep still pointed at the discarded pure tableau while the rest of the method branches over the mixture. Rebind the local and set the mixed flag before assigning, as PhotonLoss already does. This path is reached when Monte Carlo noise simulation (which compiles to a pure Stabilizer) is combined with a noise map containing DepolarizingNoise.
GeneralKrausError.apply's DensityMatrix arm - the only backend the class claims to support - called self.get_backend_dependent_noise, which is not defined anywhere, so every application raised AttributeError. Embed the stored single-qubit Kraus operators on each register in reg_list (as the other per-register channels such as DepolarizingNoise do) and apply the resulting channel. Operators that are not 2x2 are refused with a ValueError naming the single-qubit restriction, instead of failing later with a dimension mismatch.
…rs dict
ResetError.__init__ defaulted noise_parameters to a literal {}, so
every default-constructed instance held the same dict object. Default
to None; NoiseBase.__init__ already turns None into a fresh empty
dict, matching the other noise base classes.
stabilizer_to_density's mixed-state branch (a list of (probability, StabilizerTableau) pairs) summed the branches and then fell off the end without returning, so it returned None - and converting a mixed stabilizer QuantumState to "dm" failed with "TypeError: Input must be a numpy.ndarray or an integer". The accumulator was also the integer 0, so a real-valued first branch followed by a Y-containing (complex) branch raised a numpy casting error on the in-place add. Start from a complex zero matrix and return the sum.
scipy's linalg.eigh returns eigenvectors as columns, but the mixed-state branches of both functions read eigenvectors[i] (a row) and computed eigenvectors[i] @ eigenvectors[i].conj().T, which collapses to a scalar instead of the rank-1 projector |v_i><v_i|, so any mixed density matrix crashed with "TypeError: Input density matrix must be a numpy.ndarray". density_to_graph also stored the whole eigenvalues array as each branch's weight instead of eigenvalues[i].
networkx 3.4 removed nx.random_tree (replaced by random_labeled_tree, added in 3.2), and pyproject.toml allows any networkx 3.x, so a fresh install hits AttributeError in data_collection/ui_functions.py's "tree" graph type and in test_random_state_converter. Use random_tree where it still exists, so older networkx versions keep generating the same graphs, and random_labeled_tree otherwise.
Stabilizer.trace_out_qubits and MixedStabilizer.trace_out_qubits passed the qubits to trace out as partial_trace's keep= argument, so they kept exactly the qubits they were asked to discard. Pass the complement instead.
MixedStabilizer.tableau returned a TypeError instance instead of raising it, so reading the property on a mixture silently yielded an exception object that failed later and far from the cause. Raise it.
…thods - MixedStabilizer.__eq__ called self.sort() and other.sort(), so comparing two mixtures reordered both of them in place. Compare sorted copies instead. - Stabilizer.__eq__ and MixedStabilizer.__eq__ reached straight for other.data / other.mixture, so comparing against anything else (an int, None, or a pure vs. mixed stabilizer) raised AttributeError. Return NotImplemented for a foreign operand so Python falls back to the reflected comparison / identity, which gives False.
…x.__eq__ DensityMatrix.__eq__ was a bare np.allclose(self._data, other.data): comparing against a non-DensityMatrix (an int, None, ...) raised AttributeError, comparing matrices of different sizes raised a broadcasting ValueError, and a (1, 1) matrix could compare equal to a larger one by broadcasting. Return NotImplemented for a foreign operand and False when the shapes differ.
- one_pauli_type_finder and pauli_type_finder bounded their row loop by the number of columns (np.shape(x_matrix)[1]) instead of rows, so on a non-square pair they silently skipped the lower rows or indexed past the end. Every production caller passes a square matrix, so results there are unchanged. - one_pauli_type_finder mapped any unrecognised pauli_type to the identity pair (0, 0), silently returning the identity rows instead of failing. Raise ValueError for anything but "x", "y" or "z".
StabilizerTableau's list branch checked that the X and Z matrices had matching shapes but not that they were square, and took n_qubits from the column axis, so e.g. a 4x2 pair built a self-contradictory tableau. This is reachable from stabilizer_to_density's list[str] input: stabilizer_to_density(["XZI", "ZXI"]) (two generators on three qubits) returned an 8x8 "density matrix" of trace 2. Raise ValueError for a non-square or non-2-D pair, and take n_qubits from the row axis like the other constructor branches.
benchmark_data's docstring typed solver_class as SolverBase and said nothing more. It builds each solver as solver_class(target=..., metric=..., compiler=..., n_emitter=..., n_photon=...) and then reads solver.hof[0], so TimeReversedSolver (which takes neither n_emitter nor n_photon, and has no hof) and HybridEvolutionarySolver (which works out n_emitter and n_photon itself) cannot be passed as they are. The docstring now states that call and the hof requirement, that EvolutionarySolver qualifies (through functools.partial for a solver_setting), that HybridEvolutionarySolver can be passed through a callable dropping the two keywords, for targets whose density matrix is itself a graph state, and that TimeReversedSolver cannot be benchmarked. It says the compiler must be a density-matrix one, since circuit_measurement_independent compares density matrices and a StabilizerCompiler raises TypeError there. The targets entry now describes the (target_circuit, target) pairs it takes: the circuit is not read, and target is a dict with "name", "n_emitters", "n_photons" and the state under the key target_type. Docstrings only; no behaviour changes.
CircuitMaxEmitDepth, CircuitMaxEmitResetDepth and CircuitMaxEmitEffDepth build one value per emitter and take the max. On a circuit with no emitters the dict is empty, so evaluate raised ValueError: max() arg is an empty sequence, directly and through Metrics([...]). The other circuit metrics return a value on the same circuit; CircuitEmitterCount gives 0. Pass default=0 to the three max calls. A circuit with no emitters now scores depth_penalty(0) on each metric, which is 0 by default, and the log records that value; a Metrics wrapper holding them returns 0.0. A circuit with at least one emitter gets the same value as before, since max with a default returns the same result on a non-empty dict. Nothing in the package evaluates these metrics on a circuit without emitters, so no solver, benchmark or example result changes.
bar_plot passed each entry to the diverging colour map as entry * 0.8. The map covers [0, 1] and clips below 0, so every negative entry and zero got the deep-blue end, told apart only by alpha, and a positive entry of at most 1 stayed in the blue half: 0.5 was a pale blue. The imaginary panel of any real density matrix was drawn all deep blue. Map each entry through 0.5 + 0.5 * entry / max_height, the band bar_plot already computes for the z-limits and the alpha. max_height is always 0.25, 0.5 or 1.0, never 0, and no entry of a density matrix exceeds it, so the position stays in [0, 1]. No numeric result changes; nothing reads the colours back. Every bar figure changes colour: zero bars become the neutral grey-white cmap_div(0.5) (so the imaginary panel of a real density matrix is all neutral instead of all deep blue), negatives run from neutral to blue, and positives from neutral to red. Any saved bar figure regenerated after the fix looks different. Bar heights, z-limits, ticks and alpha are unchanged. Three consequences of normalising by the panel's own band: - colour is relative to each panel's band (max_height 0.25, 0.5 or 1.0), so the same value can get a different colour in two panels whose bands differ (for example 0.2 is at 0.9 in a 0.25 panel and at 0.6 in a 1.0 panel), including the real and imaginary panels of one matrix; - the bars still disagree with density_matrix_heatmap (fixed vmin=-1, vmax=1) whenever max_height < 1; their colour positions agree only in the 1.0 band (the bars also keep their own alpha); - the end colours are reached only in the 1.0 band, by an entry of exactly +1 or -1; in the 0.25 and 0.5 bands every entry is strictly below max_height, so the extremes stay just short of the ends.
result_maker builds one value per emitter for max_emit_depth, max_emit_reset_depth and max_emit_eff_depth, and reads one register depth per emitter for depth, then takes the max of each. On a circuit with no emitters all four sequences are empty, so result_maker raised ValueError: max() arg is an empty sequence, including with its default metric list. n_emitters, n_cnots, n_measurements and n_unitary give 0 on the same circuit. Pass default=0 to the four max calls, as the CircuitMaxEmitDepth, CircuitMaxEmitResetDepth and CircuitMaxEmitEffDepth metrics already do; each of those evaluates depth_penalty(0), which is 0 with the default penalty. The depth column has no metric counterpart; 0 matches what an emitter with no gates already gets there. Nothing changes for any circuit with at least one emitter: every column keeps its value. That covers every circuit the package's own data-collection functions produce, so no orbit_analyzer, find_best, graph_analyzer, benchmark, example or test result changes. A direct result_maker call on a circuit with no emitters now returns 0 in max_emit_depth, max_emit_reset_depth, max_emit_eff_depth and depth instead of raising. (For comparison, an emitter with no gates gives 1 for the reset and eff depths, since Input to Output is one interval; a circuit with no emitters has no interval, hence 0, as the library metrics give with their default penalty.)
IO.default_path is data/ in the parent folder of the graphiq package. The comment above it said it pointed to the repository's data/ wherever the repository was, which holds only for a source checkout or an editable install (pip install -e). For a regular install (pip install graphiq, or pip install . from a checkout) io.py sits in site-packages, so the default is <site-packages>/data. Nothing in the docstring said where a default IO writes. The class docstring now says so, and the comment matches it. The usage example called IO.create_new_save_folder with include_uuid= and then io.save_df, none of which exist; it now calls IO.new_directory with include_id= and io.save_dataframe, the current names for the same calls. Docstring and comment only; where IO saves is unchanged.
The initial_graph setter's assert accepted None, but the setter then passed the value to _graph_list_maker, which calls nx.to_numpy_array on it, so assigning None always raised TypeError: 'NoneType' object is not iterable. It raised after storing the value, leaving initial_graph None while graph_list still held the old graph's relabelings. No version of the setter ever handled None. Accept only a networkx graph, as the assert's message already says. Assigning None now raises AssertionError before anything changes, so initial_graph and graph_list keep their previous values. Assigning a graph behaves as before, and GraphCorr(initial_graph=None) still works: the constructor handles None on its own path. Under python -O the assert is stripped and assigning None fails as it did before. Nothing in the package assigns initial_graph, so no result changes.
test_one_benchmarking_run built a solver, a run dict and an IO, and then stopped: its benchmark(...) call has been commented out since 2023, and benchmark is no longer imported in the file, so the call could not run as written. The test could fail only if a constructor raised, which the other tests already cover. Delete it and the __main__ block that called it. Every import stays in use by the remaining tests. No source changes.
draw_dag documents fig as the figure on which to draw the DAG, but a call with fig and no ax ignored it: the DAG was drawn on a new pyplot figure, that new figure was returned, and the caller's figure was left unchanged. When fig is given without ax, draw on fig's current axes, fig.gca(), which adds one subplot if fig has none, and return fig with that axes. No new figure is opened. Calls with no arguments, with ax only, or with both fig and ax behave as before. The docstring now says which axes a fig-only call uses. No numbers are computed, so no result changes.
local_comp_graph built its adjacency matrix with nx.to_numpy_array(input_graph), which fills each entry with the edge's weight attribute. The matrix product then ran in float64 on those weights with a single mod 2 at the end, so a weighted graph could give a result that is not a local complementation of the graph: an edge with weight 2.0, for example, was treated as absent. Read the adjacency with weight=None, and say in the docstring that edge weights are ignored. local_comp_graph now ignores edge weights. Its result is that of the same graph with its weights removed: every edge counts once, and between two nodes of a MultiGraph the parallel edges count by the parity of their number (an even number cancels). For a graph whose edges all have weight 1 or no weight, which covers every graph the package builds (including graphs from nx.from_numpy_array of a 0/1 matrix and graphs returned by local_comp_graph), the result is unchanged. Before, any other weight could change the result, for example an even, fractional, very large odd, nan or inf weight, and a non-numeric string or a complex weight raised. No in-tree call passes such a weight, so no result, test or benchmark in the package changes, and no random numbers are drawn.
density_matrix_heatmap documents its return value as fig (figure handle), axs, but a call with axs returned None in place of the figure. The heatmaps were drawn on the given axes as documented. When axs is given, return axs[0]'s root figure: the figure holding axs[0], or the root figure when the axes sit in a SubFigure. If the two axes come from different figures, it is the figure of the real-part axes. The docstring now says that axs is two axes, real part first, and which figure is returned. Calls without axs behave as before. No numbers are computed, so no result changes.
_partial_orbit gives fixed local-complementation sequences for the path 0-1-...-(n-1): an index x means the x-th node along the path. linear_partial_orbit passed each x straight to local_comp_graph, which reads it as the x-th node in the graph's iteration order. The two agree only when the path runs through the nodes in iteration order. For any other path, the wrong nodes were complemented: the first graph could differ from the input, and the list could repeat graphs. Walk the path from the end that comes first in iteration order and map each index to that node's position. Also require the graph to be connected: the existing check (n - 1 edges, maximum degree 2) accepted a shorter path plus disjoint cycles, which is not a linear cluster state, and now raises the same "input graph is not a linear graph" AssertionError. The docstring states both. For a path already in iteration order, including nx.path_graph(n), the output is unchanged, graphs and labels alike. For a path in another order, the output is the in-order orbit carried along the path, labelled 0..n-1 by position as before, with the input first and no repeats. The only caller is AlternateTargetSolver with lc_method="linear", which is not the default, was used nowhere in the package, its benchmarks or examples, and had no test, so no default or in-tree result changes. With that setting, candidate graphs change for iso graphs or targets not in path order; with allow_relabel=True that is typically every relabelling after the first.
CI's lint job runs black 24 --check on graphiq/. It rejects the parenthesised n_cnot_penalty default in CircuitCnotCount.__init__: the line fits on one line since the attribute was renamed from n_emitter_penalty, so black joins it. Join the line as black 24 does. Formatting only: the code, the comment and every result are unchanged, and black 24 --check now passes on all of graphiq/.
test_benchmark_run_graph_search_solver built its IO with no path, so IO fell back to its default folder: data/ in the checkout for a source or editable install, or <site-packages>/data for a regular install. Every run left a dated benchmarks folder there, holding solver_result.csv and a circuits folder, and nothing removed it. data/ is gitignored, so the folders piled up unseen. Pass path=tmp_path, as the neighbouring benchmark_run tests do, and assert that solver_result.csv is saved under tmp_path. That assertion fails if the IO falls back to the default folder again. No source changes.
QuantumState.n_qubits was stored once in __init__ and never updated. After partial_trace took a 4-qubit state to 3 it still reported 4, while rep_data reported 3. The same happened after resizing the live representation directly, e.g. state.rep_data.remove_qubit(0) or state.rep_data.add_node(...). The stale count broke two things: the rep_data setter's width asserts compared incoming data against it, so assigning a traced state's own data back raised AssertionError while data of the old, pre-trace width was accepted; and compile(circuit, initial_state=state) rejected a traced state whose data was exactly the circuit's width. n_qubits is now a read-only property returning rep_data.n_qubits, so it cannot go stale. DensityMatrix gains the n_qubits property the other representations already had. __init__ keeps the count from validate_data in a local, and the representation helpers take the expected width as an argument: __init__ passes that count, and the rep_data setter passes the state's current width. The width asserts themselves, their messages and their exception types are unchanged. Behaviour changes: - QuantumState.n_qubits reports the representation's current width after partial_trace or any resize made through rep_data. - QuantumState.n_qubits is read-only: assigning to it raises AttributeError. Before, the assignment was accepted and desynchronised the count from the data. Nothing in graphiq assigned it outside __init__. - After a resize, the rep_data setter accepts data of the new width and rejects data of the old width with AssertionError (the reverse of before). A state that was never resized is checked as before. - An existing Stabilizer or MixedStabilizer object assigned through rep_data is still adopted without a width check; the state's n_qubits now follows its width. - compile(circuit, initial_state=state) accepts a traced state for a circuit of its new width, and rejects it with "the number of qubits in initial state must match the circuit" for its old width. Before, the old width was accepted: an empty circuit returned a state narrower than the circuit, and a gate on a missing qubit failed later. - The "Density matrix is not recommended" warnings, in the rep_data setter and in convert_representation, are judged on the current width rather than the construction-time count. - Pickles and copies report the current width; n_qubits is no longer an instance attribute. A pickle made before this change still loads and reports the current width. - DensityMatrix.n_qubits is new. A 9x9 density matrix is still rejected with AssertionError, at construction and through the setter, and malformed setter input fails as before. Results of code paths that never resize a state are unchanged.
…graph gets With alternate_order=True, crazy relabels its graph with relabel(adj, nodes), which sends old node i to new label nodes[i]. The position dict was built the other way round, giving new label i the position of old node nodes[i]. The two agree only when every swapped pair of columns has the same size. Otherwise pos=True placed some nodes in the wrong column: for n_list [2, 3], three of the six edges were drawn within a single column. Give node nodes[i] the position that node i has in the plain layout. The graph itself is unchanged for every input, so its labelling and emission order stay as they were. Only the positions returned with pos=True and alternate_order=True change, and only when a swapped column pair has unequal sizes. Nothing in the package calls crazy with pos=True, so no in-tree result changes.
section_finder documents a ValueError for an invalid criteria, but it raised one only when criteria was neither a string nor a list. A string that named none of its options, such as the typo "reg_as_ctrl", matched no branch and silently left the path unsliced, as a single section. A NoisyEnsemble given that criteria in its noise parameters then built its event trees from that one section, with no error, and could score a different infidelity than the intended criteria would have. Raise the same ValueError for an unknown string. The five valid strings and a list of op classes behave as before, so no result from a valid criteria changes.
Fifteen docstring lines in the backends described behaviour the code does not have: - bipartite_partial_transpose raises ValueError when subsys is neither 0 nor 1; it never checks whether the matrix is bipartite. - project_and_remove's rho was described as the matrix whose negativity is evaluated, copied from negativity. - DensityMatrixCompiler.compile_one_noisy_gate takes the QuantumState, as compile_one_gate does, not a DensityMatrix. - graph_to_density raises TypeError for a graphiq Graph; it accepts a networkx graph, an adjacency matrix or a list of (probability, graph) pairs. - is_lc_equivalent and Graph.lc_equivalent return a (found, solution) pair, not an array, and Graph.lc_equivalent takes one other Graph. In 'random' mode False only means no solution was found, which Graph.lc_equivalent's return line now says as is_lc_equivalent's does. - _solution_basis_finder returns the solution-space basis vectors, _col_finder a list, and lc_graph_operations a list of node indices, not operation names. - Graph.add_edge adds a missing node before adding the edge. - create_n_ket1_state returns |1>^n, and add_columns adds columns. Docstrings only; no behaviour or result changes.
Seventeen docstring lines in the circuit package and the openQASM, drawing and comparison utilities described behaviour the code does not have: - CircuitDAG.from_json returns the circuit it builds, and calculate_reg_depth returns a list. - OperationBase.openqasm_info and find_local_clifford_by_matrix never return None; both raise ValueError instead (find_local_clifford_by_matrix's :raises line already says so). - The q_registers setter and _update_q_reg replace the whole quantum-register tuple and check its length against q_registers; operations have no emitter_registers or photon_registers. The c_registers setter checks c_reg against c_registers. - MeasurementZ's noise type is graphiq.noise.noise_models.NoiseBase; there is no src package. - OpenQASMParser.parse is a generator of per-node info dicts. - Columns.set_all_col_element sets one row position in every column, and only where the entry is still 0. - ged_adaptive returns whether the GED is zero, not the distance. Docstrings only; no behaviour or result changes.
Fourteen docstring lines described behaviour the code does not have: - TwoQubitControlledGateReplacement applies the target gate when the control is |1>, not |0>. - CircuitUnitaryCount counts unitary gates; CircuitMaxEmitDepth, CircuitMaxEmitResetDepth and CircuitMaxEmitEffDepth score the maximum emitter depth, reset depth and effective depth, not the circuit depth. - NoisyEnsemble.all_branches keeps each event tree built for a noisy register above the cut-off probability; the combined tree is their product and can fall below it. - multi_reg_nodes selects nodes by their operation's base class, not by their number of edges. - IO.load_np_array returns the loaded array. Docstrings only; no behaviour or result changes.
Eleven docstring lines described behaviour the code does not have: - SolverBase._identify_noise returns a list of two noise models for a two-legged operation, which every solver caller passes. - GradientDescentSolver.solve runs n_step updates (there is no n_steps), and AlternateTargetSolver's constructor named a class that does not exist. - lattice_cluster_state takes a tuple of sizes, one per dimension, and two_d_cluster and three_d_cluster build grid cluster states, not the crazy graph. - search_for_alternative_circuits and the exemplary_* helpers need an EvolutionarySolverSetting; a RandomSearchSolverSetting raises AttributeError in solve. - report_alternate_circuits takes the list of (score, probability, circuit) tuples the search returns. Docstrings only; no behaviour or result changes.
…hat contradict the code Nineteen docstring lines described behaviour the code does not have: - GraphCorr._graph_list_maker returns the initial graph followed by up to count labelings, the first of which is the initial one again. - GraphCorr.finder returns no standard deviations (they are only drawn as error bars), and its (x, y) tuple is (unique circuit metric values, average graph metric values). - corr_n_dependence's constant_np is a number, the n*p kept constant, not a flag. - met_met, NodeCorr.met_order_error, NodeCorr.next_node_corr and _rep_counter return the repetition counts as a numpy array. - NodeCorr.order_corr averages the correlations, not the graph metric. - correlation_checker prints and returns nothing; corr_with_mean also returns the unique values, means and standard deviations. - _label_finder samples from all permutations when n_node is below 8 as well as when exhaustive is set, and then ignores new_label_set. - get_relabel_map adds -1: "self" when the two graphs are equal. - density_matrix_heatmap and density_matrix_bars return two axes. Docstrings only; no behaviour or result changes.
…it saves 6_solvers.ipynb listed self.save_openqasm among the solver's attributes. It is a field of EvolutionarySolverSetting, a string: "hof", "pop" or "both" write those circuits out as openQASM after each generation, through the solver's io, and the default "none" saves nothing. Without an io nothing is written. One markdown line; no code or saved output changes.
8_variational_circuits.ipynb kept a TracerArrayConversionError traceback, with local paths from an old run, as the output of its optimisation-loop cell. Clear that cell's outputs and execution count. Only the saved output changes; the cell's source and every other cell are untouched.
…version GradientDescentSolver and 8_variational_circuits.ipynb present the jax array library as usable, and nothing said that it is not. In this version: - setting graphiq.DENSITY_MATRIX_ARRAY_LIBRARY after import graphiq does not switch the density-matrix backend to jax: importing graphiq already imports that backend, which reads the setting once and keeps numpy. It only lifts GradientDescentSolver's own check, which reads the setting each time a solver is made. The backend uses jax only if the constant is edited in graphiq/__init__.py; - with jax selected, no density-matrix QuantumState can be built from a matrix: QuantumState accepts only numpy arrays, while DensityMatrix accepts only jax arrays; - the density-matrix arm of Infidelity cannot be differentiated, since it takes the trace with numpy and branches on its value. What still works: with jax selected by editing the constant, GradientDescentSolver runs with states built from a number of qubits and a jax-differentiable metric of the user's own. GradientDescentSolver's class docstring now says this, and the notebook's title cell says the notebook does not run as written and that its saved outputs come from an older version. Text only: no code, cell source other than the title cell, or saved output changes.
…aphs
For graphs under 8 nodes, or whenever it is asked for an exhaustive
search, _label_finder drew its permutations with replacement
(rng.choice(perm[1:], n_label - 1)), so it could return the same
permutation more than once. automorph_check removes the duplicate
graphs, so callers saw missing relabellings rather than duplicate ones:
iso_finder could stop short of the graphs that exist and warn "Maximum
of N possible isomorphic graphs exist" for a count below the real one.
For example, iso_finder on a 4-node path, which has 12 distinct
labellings, returned 8 to 11 of them for 169 of seeds 0-199.
_label_finder now keeps the same draw, drops the repeats, and tops up
without replacement from the permutations that were not drawn. A draw
with no repeat gives exactly the rows it gave before, in the same order.
It also returns [[0]] for a 1-node graph, where it raised ValueError, so
iso_finder on a 1-node graph returns that graph.
This changes seeded results. A seeded iso_finder or AlternateTargetSolver
run changes only if one of its rounds drew a repeated permutation; it may
then return different, and sometimes more, relabelled graphs. On a grid
of 1480 seeded iso_finder calls on 4- to 7-node graphs, 350 drew a repeat
and 236 of those changed. Graphs of 8 or more nodes take the other branch
unless the search goes exhaustive.
The data_collection helpers find_best, graph_analyzer and
iso_scaling_test run with seed 1 by default, so their default outputs
change. find_best now finds every reordering: 12 instead of 11 for a
4-node path, 60 instead of 51 for a 5-node path, 360 instead of 311 for a
6-node path. The CSV and JSON files it saves, and graph_analyzer's
bests.txt ("number of iso found", "total number of cases" and the
best/worst picks when they depend on the new cases), change with it.
orbit_analyzer and lcs ask for one ordering and are unchanged.
new_label_set is still ignored in this branch, so each iso_finder round
still redraws its labellings instead of extending the previous round's.
test_label_map_has_one_map_per_returned_matrix_after_a_longer_search
moves from seed 5 to seed 0: seed 5 needed a longer search only because
its first draw repeated a permutation, while seed 0 needs one because of
the graph's own symmetry.
…ise_score noise_score reads the target's adjacency matrix in the graph's node order, but it built the relabelling permutation by looking the relabel map up with the integers 0..n-1. The map is keyed by the target's own node labels. So with noise simulation on (a noise_model_mapping given, not Monte Carlo), a target whose nodes are not labelled 0..n-1 raised KeyError. That includes linear_cluster_state(n).data, which is labelled 1..n, graphs with string labels, and QuantumState graph targets built on such graphs. A target labelled 0..n-1 but with its nodes inserted out of order raised nothing, but was scored against a relabelled target that was not the graph its circuit makes: with depolarizing noise and seed 1, the path 2-0-1 inserted as [2, 0, 1] scored 0.762727 for its first candidate instead of 0.076624. Look the map up by each node's label, in the order the adjacency matrix is read, and drop the node count that only the old lookup used. Every target labelled 0..n-1 in insertion order builds the same permutation as before, so its scores are bit-identical. That covers every target the tests passed to the solver before this change and every graph t_graph builds itself; a graph drawn with t_graph's "draw" or passed in with "nx" can be labelled either way. A target labelled otherwise now gets a score instead of KeyError, and a 0..n-1 target inserted out of order now gets the correct score (unless the old permutation happened to be an automorphism of the target), so its candidates can rank differently. Noise-free and Monte Carlo solves do not call noise_score and are unchanged.
The pytest workflow installs the package with its all extra (jax, optax, ray) on Python 3.8, 3.9 and 3.10. On 3.8 that install now fails with a pip ResolutionImpossible between optax and jaxlib: PyPI no longer has any jaxlib release with a Python 3.8 wheel, optax 0.1.8 and earlier depend on jaxlib, and later optax releases require Python 3.9 or newer. So the 3.8 job stopped before running any test. On 3.8 the job now installs ray alone, which test_solver_monte_carlo needs for its parallel Monte Carlo run; 3.9 and 3.10 still install the whole extra, so jax and optax are tested there. The package, its dependencies and its results do not change.
…a tiny rotation test_a_rounding_error_probability_is_not_forced built |0> as Hadamard twice on one emitter and required p(1) to be nonzero and below 1e-20. Whether H·H leaves any p(1) at all depends on the BLAS kernel numpy uses: with numpy 1.24.4, kernels that fuse multiply-add leave 6e-34, while the kernels without FMA that were tried (older x86 cores, and Rosetta, which does not advertise AVX) leave exactly 0, so the precondition failed there even though the measurement code is correct. The circuit now applies one rotation by 2e-17, which leaves p(1) = sin^2(1e-17), about 1e-34. That is a single product with nothing to cancel, so it is the same nonzero value on every kernel, and far below the rounding floor. The test still compiles the circuit, and still fails on the code before the floor (outcome 1 instead of 0). The library does not change.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This is a maintenance release of GraphiQ (version 0.1.2). It contains bug fixes, compatibility and packaging fixes, documentation and notebook fixes, and more tests. It adds no features. Some fixes change default or seeded results. Every one of them is listed in the next section, so results from 0.1.1 can be compared with results from this release.
Changes to default or seeded results
Solver searches and seeded runs
replace_photon_one_qubit_opnow labels the new gate "Fixed" only when the gate it replaces was "Fixed". Before, every new gate got the label. SeededHybridEvolutionarySolversearches change, because a photon gate it replaces is no longer made permanently unremovable. AnEvolutionarySolversearch that starts frominitializationwith the default transformations is unchanged by this fix: every photon gate it replaces was labelled "Fixed" byinitialization.PYTHONHASHSEED. Seeded searches that reach them can follow a different path than before, and that path is now the same in every process. That covers every search with more than one emitter, and everyHybridEvolutionarySolversearch, including single-emitter ones, since its default transformations addMeasurementCNOTandReset(sosearch_for_alternative_circuitsingraphiq/benchmarks/alternate_circuits.pyis among them).EvolutionarySolversearches with one emitter and the default transformations are unchanged.iso_findernow appliessort_emitandlabel_mapon every return path. For seededAlternateTargetSolverruns withsort_emit=True(the default) andn_iso_graphs > 1, a run whose isomorph search returns early can order its candidates differently, keep a different circuit and relabel map for a repeated graph, and put a different candidate first. The set of candidate graphs is unchanged, and so are runs withn_iso_graphs=1._label_finderno longer returns repeated permutations for graphs with fewer than 8 nodes, or when the search is exhaustive. This affects seedediso_finderandAlternateTargetSolverruns that drew a repeat: they can now return different graphs, and sometimes more of them. On a grid of 1480 seedediso_findercalls, 236 changed.find_best,graph_analyzerandiso_scaling_testrun with seed 1 by default, so their default outputs change. For example,find_bestfinds 12/60/360 relabellings for 4/5/6-node paths (11/51/311 before), which changes its CSV/JSON files andbests.txt.noise_model_mappingand Monte Carlo off,AlternateTargetSolver.noise_scorelooks up the relabel map by node label. A target labelled 0..n-1 but inserted out of order is now scored against the right graph, so its candidates can rank differently. Before, a target with other labels raisedKeyError(see below). Targets labelled 0..n-1 in insertion order are bit-identical.assign_noiseandMonteCarloNoisenow pair aOneQubitGateWrapper's noise with its gates in constructor order. With Monte Carlo on, seeded scores change while their expectation does not (e.g.MonteCarloNoiseonpath_graph(5), 40 samples, seed 7, rate 0.3: 0.925 -> 0.875). Mappings that give different gate classes different models now reach the intended gates. Runs without noise, and "depolarizing" runs with Monte Carlo off, are unchanged.MixedStabilizer.reduceno longer skips entries (6d874cd), and now merges branches that represent the same stabilizer state, so the mixture stays bounded. Expected values are unchanged up to rounding. With "probabilistic" measurement determinism on aStabilizerCompilerwith noise simulation on, fewer branches draw from the global numpy stream. A seeded compile can then give a different state, score and classical-register value, and later draws from that stream change too. With determinism 0 or 1, no outcome changes, and scores and summed probabilities change only by rounding in the last digits. From a3ada66 alone,search_for_alternative_circuitsscores a ring4TimeReversedSolvercircuit undernoise_model_loss_and_depolarizing(0.01, 0.01)as 0.104373358769414 instead of 0.10437335876941412, and reports its photon survival probability as 0.9801000000000005 instead of 0.9800999999999997. A seeded search can therefore rank two candidates whose exact scores tie differently.diag([-1e-3, 1e-19])set in place) now reaches NaN: the probabilistic mode raises numpy'sValueError, determinism 1 returns outcome 0 with a NaN state, and determinism 0 returns outcome 1 with a NaN state. Before this fix, all three returned outcome 1 without NaN.benchmarks/solvers.pyruns now (it crashed before) and uses its stated settings.examples/example_solve.pyalso uses its stated settings, where before it ran on the defaults.Noise, states and scores
AmplitudeDampingNoiseusesitertools.product. On 2 qubits it was not trace-preserving, and on 3 or more it applied no noise. Only noise maps that apply it to two or more qubits change; nothing in the package uses it.partial_tracenow contracts each traced qubit, so a Bell pair traces to I/2 (it gave |+><+|). Results change wherever a density matrix is traced over qubits still entangled with the rest, including the solvers' scoring after emitters are traced out.Infidelity,TraceDistance) gets a different score, and seeded searches that compile onStabilizerCompilerbut score against a density-matrix target follow a different path.remove_qubit, and so stabilizerpartial_trace, now carries the measurement sign to the surviving qubits. No score measured on the solvers' own circuits changed. A circuit that traces out a qubit left in 1 and correlated with a kept qubit now gets the correct state.MeasurementCNOTandReset, and the stabilizer compiler records its outcome in the classical register. This changes density-matrix circuits that reuse an emitter after outcome 1, and stabilizer circuits that read that register.ClassicalCNOT,ClassicalCZandMeasurementCNOTandResetis now applied. The stabilizer compiler used to drop it, and the density-matrix compiler raised. Noisy runs with such noise change.SolverBase._identify_noisehonours the per-leg<Op>_control/<Op>_targetkeys. Noisy runs that use them change, includingnoise_model_loss_and_depolarizing,examples/example_results.pyand two noisy test fixtures.AlternateTargetSolverwith noise simulation on and Monte Carlo off. It scores candidates throughassign_noise. That now applies per-leg CNOT noise (d84039d) and a section'sOneQubitGateWrappermodel as the solvers do (fcac0a3). A density-matrix measurement now keeps photon-loss trace, andInfidelityscores it (0281091). So scores under the loss maps ingraphiq/benchmarks/alternate_circuits.pychange on the stabilizer compiler and on the default density-matrix noise compiler. ForTimeReversedSolvercircuits undernoise_model_loss_and_depolarizing, the density-matrix score goes from 0.0 to 0.0457 (linear4) and 0.1044 (ring4) at (0.01, 0.01), and to 0.3826 and 0.6718 at (0.1, 0.1); the stabilizer score goes from 0.0100/0.0199 and 0.1000/0.1900 to the same values. Undernoise_model_pure_loss(0.1)the density-matrix score goes from 0.0 to 0.1000/0.1900, and the stabilizer score is unchanged.AssertionError. Against a stabilizer target their infidelity matches the stabilizer compiler's up to rounding (from 0281091 alone: linear4 under loss 0.1, 0.0 -> 0.1). The examples, benchmarks and notebooks that use the density-matrix compiler apply no photon loss, and the "depolarizing" mapping the data-collection helpers use is trace-preserving, so their results do not change from this.inverse_circuitfirst tries its original reduction and keeps it only if the result is exactly the standard all-zero tableau. Otherwise it uses a rewritten reduction, or raisesValueError.TimeReversedSolvercircuits on the 53 random graphs tested are identical to before. States the old reduction left short of |0...0> now reduce correctly. Stabilizer fidelity no longer returns 1 for orthogonal states in the case where the old reduction ended at |0...0> written with other Z generators.trace_out_qubitsdiscarded the wrong qubits (9135b33).reset_zoverwrote the measured row's sign (42d5a3b).swap_gatemoved row signs (a18d020).insert_qubitmisplaced the new phase (b034771).measure_x/measure_ydid not rotate back (e4deaa9).Metrics and data collection
CircuitUnitaryCountcounts SigmaY and SigmaZ, and counts SigmaX once. Before, SigmaY and SigmaZ were never counted and SigmaX counted three times. 43ee392 makes the same fix toresult_maker's "n_unitary", which also skipped PhaseDagger.CircuitUnitaryCountis not used by any solver, benchmark or example, butresult_makerrecords "n_unitary" by default, so that column changes inorbit_analyzer's result and infind_best's CSV/JSON (and so in whatgraph_analyzer,LC_scaling_testandiso_scaling_testwrite through it) for circuits with SigmaX, SigmaY, SigmaZ or PhaseDagger. From 43ee392 alone,find_beston a 5-cycle with 4 reorderings gives [17, 39, 56, 40] -> [17, 31, 44, 32].find_best's cost does not use "n_unitary", sograph_analyzer's best and worst picks do not change.height_dict(graph=...), and with itheight_max(graph=...), the minimum emitter count, order nodes by label. For a graph whose nodes were not inserted in label order, the emitter count changes (e.g. the path 0-1-2-3 inserted as [2, 0, 1, 3]: 2 -> 1), and so doGraphCorr/NodeCorr's "num_emit", "num_emit_per_photon" and "cnot_per_emitter" andgraph_to_cnot. Graphs the package builds itself, andsort_emit, are unaffected.GraphCorr/NodeCorr"cnot_per_photon" is now CNOTs / n (it was the raw count). Correlations change for samples that mix graph sizes.corr_n_dependencecurves shift relative to each other, and_rep_counterlists reorder.GraphCorr/NodeCorrdrops by exactly 2. Correlations are unchanged, andmet_distribution's mean drops by 2.graph_analyzer's worst-fidelity case (thebests.txtline and[4]) is now the lowest-fidelity case, and ties keep the first. TheLC_scaling_test/iso_scaling_testbw lists and per-runbests.txtfollow. The CSVs are unchanged.rgs_analysis's saved JSON loses its empty "n_cnot" column. The values are unchanged.photon_survival_ratereads the control leg's noise for a photon that is the control. Rates change only when the two legs carry different noise.Saved files, exports and figures
benchmark_run's summary records the metric's class name (it recorded the compiler's).solver_info["seed"]reports the seed (it was always None), andbenchmark_datarecords the seed actually used (i + seed_offset). Search results are unchanged.benchmark_runnow writes pop/ and hof/ openQASM every iteration, as it meant to. That is about 2750 files per run for the pipeline example. The time column includes the writes, and the caller's setting is left at "both". Scores, circuits, logs and hall of fame are unchanged.cost_mean/cost_variance/cost_maxskip unfilled hall-of-fame slots (they were inf/nan while n_pop < n_hof).log_hof.csvandlog.pdfchange for such runs. Default settings are not affected.OneQubitGateWrapperof two or more non-Identity gates that is not a palindrome lists its body in application order. Earlier exports described a different circuit to other tools. No score changes.rzdefinition usesU(0,0,phi). The old one was invalid OpenQASM 2.0.density_matrix_barscolours are centred on zero: zero is neutral, negatives run neutral to blue, positives neutral to red. Colour is relative to each panel's band. Every bar figure looks different, and no number changes.Other functions that returned wrong results without an error
from_openqasmdecodes multi-character gate names in wrappers (e.g. "sdg")._select_graphsno longer evicts the best candidate inget_lc_graph_by_max_edge/get_lc_graph_by_max_neighbor_edge.benchmarks/lc_equivalence.pydemos run and report distinct graphs.is_lc_equivalentchecks each connected component, so equivalent disconnected graphs are no longer reported non-equivalent. On connected graphs the boolean and the returned solution are as before. An unknownmodenow always raisesValueError; before, it raised only when the solution space had dimension 5 or more, and otherwise returned a result.find_lc_operationsbuilds its sequence from the first graph, the one the sequence starts from.bipartite_partial_transposeandnegativityare correct for unequal dimensions. Equal dimensions, which covers every in-package call, are bitwise identical.check_equivalent_unitariestakes the phase from the largest entry, so round-off no longer rejects unitaries that are equal up to phase. In-package answers are unchanged.circuit_is_isomorphictells control from target in classical pair ops, andremove_redundant_circuitskeeps both such circuits.OpenQASMParserreports the classical-bit index ofmeasure q[i] -> c[j];as j (it reported i).Painterdraws an indexedreset/measureon its one qubit (it drew the whole register).local_comp_graphignores edge weights. Weight-1 and unweighted graphs are unchanged.linear_partial_orbitwalks the path in any node order. This matters only forAlternateTargetSolver(lc_method="linear"), which is not the default.crazy(pos=True, alternate_order=True)places nodes by their new labels. The graphs are unchanged.Behaviour and API changes
QuantumStateand conversions (graphiq/state.py,graphiq/backends)QuantumState.n_qubitsis a read-only property that follows the representation, and assigning to it raisesAttributeError. After a resize,rep_dataandcompile(initial_state=)check the new width.DensityMatrix.n_qubitsis new.rep_typerecords the type it detects, soshow, conversions, metrics and solvers work on it.mixed=FalseraisesValueErrorfor a mixed matrix.mixed=FalseraisesValueErrorwhen the trace is not within about 1e-5 of 1 (0.1.1 raisedTypeErrorfor such a state with either setting). Withmixed=Trueit converts to aMixedStabilizerthat keeps the weight.stabilizer_to_graph(validate=True)accepts any generator set of a graph state (e.g. XZZ, ZXI, IXX).Graph, andmixed=TruegivesMixedGraph.mixed=Falseand not pure raisesValueError.MixedGraph-> s (95cbc14)ValueError.convert_representation("g")on a lossy density matrix raisesAssertionErrorinstead ofTypeError.validate_datarejects a list of (p, nx.Graph), and the constructor raisesTypeErrorfor it. It used to raiseAttributeError.partial_traceon a graph state raisesNotImplementedErroreven whenrep_typewas omitted.showand drawing:Graph.drawreturns(fig, ax)(283e7af).draw_graphreturns the given axes and its root figure (4ea9577).showon a mixed graph state raisesNotImplementedError(8df0e0d).str()of aMixedGraphworks. 809a3ca:state_to_graphreturns its tuple in the documented order for matrix input.dmf.fidelitycompares two mixed states instead of raising, andsqrtm_psdis correct for complex input.Stabilizer backend
is_stabilizer.StabilizerTableau.validateworks.ValueErrorinstead of being misread:measurement_determinisminz_measurement_gate, raised before the tableau is touched (2341674, 806f801)rref's rank check rejects a single identity generator withAssertionError.DensityMatrix__eq__no longer reorder operands, raise on foreign operands, or match by broadcasting.apply_x_measurement(167e7ab),tensor(3b4f9a9),binary_symplectic_producton Clifford tables (b7385d1), and_position_finder, which also no longer drops a Hadamard (3c41561).measurement_determinismparameter briefly added to thepartial_tracemethods is removed again, so their signatures match 0.1.1.MixedStabilizer.tableauraises itsTypeErrorinstead of returning it. f5d33ff:run_circuit(reverse=True)leaves the caller's list intact. 42b9c87:clifford_from_stabilizerno longer reduces the caller's tableau.Noise (
graphiq/noise)OneQubitGateReplacementand its subclasses raiseNotImplementedErroron the stabilizer backend. Before, they silently applied neither the noise nor the gate.MixedStabilizerraisesNotImplementedError(it raisedTypeError). b7be7b0: replacement noise onMeasurementZraisesNotImplementedErroron both compilers.MeasurementZraisesValueErroron the stabilizer compiler. Before, it was dropped.DepolarizingNoiseon a pureStabilizer(1f634a4)GeneralKrausErroron a density matrix (28ca290); non-2x2 operators raiseValueErrorPauliErroron aMixedStabilizer(baa7dfb)ResetErrors no longer share one parameter dict.section_finderraisesValueErrorfor an unknown criteria string.assign_noiseno longer changes the circuit it copies.MonteCarloNoisescores are unchanged, butAlternateTargetSolver's serial Monte Carlo circuits no longer carry the last trial's noise.ValueError. A per-legNoNoise()key takes precedence over a bare key.OneQubitGateWrapperkey raisesKeyErrorat compile.Circuits and compilers (
graphiq/circuit,graphiq/backends/*/compiler.py)param_infoof RX/RY/RZ is per-parameter (((-pi, pi),),("theta",)), soinitialize_parametersandGradientDescentSolverwork with it.circuit.parameters = ...works for ops missing from the id map (it raisedKeyError).compile(initial_state=)no longer changes the caller's initial state.get_node_by_labels/get_node_exclude_labelson an unseen label (63e39c0)ClassicalCNOT/ClassicalCZon aMixedStabilizer(db658e1)MeasurementZonDensityMatrixCompiler(09febbd)to_json/from_jsonround trip (092892d)add_*_registerreturn the new index. 5d37d89: theOneQubitGateWrappernesting guard fires. bd0ac5b:name_to_class_mapreturns None for unknown names.draw_dag: it uses a givenax(de734c9), and draws on a givenfigand returns it (5e67661).Metrics (
graphiq/metrics.py)MetricsacceptsCircuitEmitterCount,CircuitCnotCount,CircuitUnitaryCount, the threeCircuitMaxEmit*Depthmetrics andCircuitMeasureCount.CircuitCnotCountevaluates (it raisedAttributeError). 76447e9:Infidelityon a mixed state works (it raisedUnboundLocalError).MetricsraisesTypeErrorat construction for an unsupportedmetric_weighttype.depth_penalty(0)on a circuit with no emitters (they raisedValueError).Solvers (
graphiq/solvers)AlternateTargetSolverSetting.lc_methoddefault changes from "max edge" to None. The solver's own default raised before.allow_relabelandallow_lcare honoured. Both default to True.solve()withlabel_map=Trueworks.linear_cluster_state(n).data) are scored instead of raisingKeyError.graph_to_circraisesValueErrorfor a graph with an isolated vertex (it raisedIndexError).HybridEvolutionarySolverwith a density-matrix target and compiler no longer converts the caller's target, andsolve()completes.HybridEvolutionarySolverwithout a noise mapping leavesnoise_simulationFalse, so compiled states are pure. Scores are unchanged.update_hof,update_logsor the "hof"/"both" openQASM save.use_adapt_probabilityadapts only the transformations intrans_probs. Before, it raisedKeyError._change_pauli_typeraisesValueErrorfor an unknown basis.Utilities, IO and benchmarks
iso_graph_finder/iso_equal_checkaccept labels other than 0..n-1. Before, they raisedIndexError, and labels -n..-1 wrapped silently. Output for 0..n-1 labels is unchanged.crazy(alternate_order=True)imports correctly._compare_graphs_visual's label check can pass.benchmark_data(save_directory=...)saves, and each per-circuit JSON now holds that circuit's data. The module's__main__runs.load_np_arrayworks on current numpy (5687ebe);new_directorydates its fallback folder (a20e5e5);SolverResult.load_jsonworks and__setitem__type-checks (8ce498c);load_txtcloses its file (9da87be).density_matrix_heatmap(axs=...)returns the figure (it returned None).Data collection (
graphiq/data_collection)user_interfaceimports.GraphCorrandNodeCorr:GraphCorrworks without an initial graph (304f0ae).GraphCorrworks withrelabel_trialsaftergraph_circ_dict(957999d).NodeCorr's graph setter updates its adjacency, so its analyses use the new graph (7fe86f1).initial_graphsetter rejects None (2fbbbc9).rnd_graphwith an integer seed returns a connected graph. It looped forever when the first draw was disconnected; seeds whose first draw was connected give the same graph.find_bestwithout adir_path(bc0ca07),plot_map_based, which gains a requiredresultparameter (dc75888), andresult_makeron zero-emitter circuits (a49cfa6).rgs_analysisandLC_scaling_knowncreate their save directories.Packaging and compatibility
graphiq. Thetests,examplesandvisualizationpackages are no longer installed.requests, whichgraphiq/utils/draw.pyimports, is declared. The unusedautograd,genbadgeanddefusedxmland the duplicatenetworkx/matplotliblines are gone fromrequirements.txt.random_labeled_treewhererandom_treeis gone. 4b2b427:math.factorialreplacesnp.math, which numpy 2.0 removed. 5687ebe:np.complex/np.floatare replaced.metrics.pyis formatted with black 24, so CI's lint check passes ongraphiq/.rayfrom theallextra. PyPI no longer has a jaxlib for Python 3.8, sooptaxcannot be installed there and the job stopped at its install step. jax and optax are still tested on 3.9 and 3.10. The package does not change.Documentation and notebooks
GradientDescentSolverand8_variational_circuits.ipynbsay what does not work on the jax density-matrix path in this version, and what still works.graphiqpackage andTimeReversedSolver(bdc8fd6), and configure solvers throughEvolutionarySolverSetting(1c60f09).6_solvers.ipynb: its failing cells are fixed (8976fc1), and it says whatsave_openqasmsaves (8e527d3).simulate_circuit.ipynbruns on the current API, with its outputs cleared (9dfd7f3).8_variational_circuits.ipynbno longer contains a saved traceback (0c3c886).Tests
tmp_path. 30078da: a test that ran nothing is removed.graphiq/data_collectionis left out of coverage measurement. The package is unchanged.