1.3 Entanglement Entropy by Classical Shadow¶
Multiple Experiments¶
Consider a scenario, you have multiple circuits that you want to run at once.
Call .measure()
one by one will be inefficient,
no to mention that you also need to call .anlyze()
for their post-processing.
Here we provide a more efficient way solve this problem, where the true power of Qurrium as experiment manage toolkit.
a. Import the instances¶
from qurry import ShadowUnveil
experiment_shadow = ShadowUnveil()
b. Preparing quantum circuit¶
Prepare and add circuits to the .wave
for later usage.
from qiskit import QuantumCircuit
from qurry.recipe import TrivialParamagnet, GHZ
def make_neel_circuit(n):
qc = QuantumCircuit(n)
for i in range(0, n, 2):
qc.x(i)
return qc
for i in range(2, 13, 2):
experiment_shadow.add(TrivialParamagnet(i), f"trivial_paramagnet_{i}")
experiment_shadow.add(GHZ(i), f"ghz_{i}")
experiment_shadow.add(make_neel_circuit(i), f"neel_{i}")
experiment_shadow.waves
WaveContainer({
'trivial_paramagnet_2': <qurry.recipe.simple.paramagnet.TrivialParamagnet object at 0x73df51502d20>,
'ghz_2': <qurry.recipe.simple.cat.GHZ object at 0x73df51407350>,
'neel_2': <qiskit.circuit.quantumcircuit.QuantumCircuit object at 0x73df5130c680>,
'trivial_paramagnet_4': <qurry.recipe.simple.paramagnet.TrivialParamagnet object at 0x73df5142e810>,
'ghz_4': <qurry.recipe.simple.cat.GHZ object at 0x73df5130d700>,
'neel_4': <qiskit.circuit.quantumcircuit.QuantumCircuit object at 0x73df5130d8b0>,
'trivial_paramagnet_6': <qurry.recipe.simple.paramagnet.TrivialParamagnet object at 0x73df5130da00>,
'ghz_6': <qurry.recipe.simple.cat.GHZ object at 0x73df5130daf0>,
'neel_6': <qiskit.circuit.quantumcircuit.QuantumCircuit object at 0x73df52090980>,
'trivial_paramagnet_8': <qurry.recipe.simple.paramagnet.TrivialParamagnet object at 0x73df51406960>,
'ghz_8': <qurry.recipe.simple.cat.GHZ object at 0x73df5f4a33e0>,
'neel_8': <qiskit.circuit.quantumcircuit.QuantumCircuit object at 0x73dffa5017f0>,
'trivial_paramagnet_10': <qurry.recipe.simple.paramagnet.TrivialParamagnet object at 0x73df5130e390>,
'ghz_10': <qurry.recipe.simple.cat.GHZ object at 0x73df5130e420>,
'neel_10': <qiskit.circuit.quantumcircuit.QuantumCircuit object at 0x73df5130e810>,
'trivial_paramagnet_12': <qurry.recipe.simple.paramagnet.TrivialParamagnet object at 0x73df514ab290>,
'ghz_12': <qurry.recipe.simple.cat.GHZ object at 0x73df5130eb10>,
'neel_12': <qiskit.circuit.quantumcircuit.QuantumCircuit object at 0x73df51d93260>})
c. Execute multiple experiments at once¶
Let’s demonstrate the true power of Qurrium.
from qurry.qurrent import ShadowUnveilMeasureArgs
Preparing a configuration list for multiple experiments with following parameters:
class ShadowUnveilMeasureArgs(BasicArgs, total=False):
"""Output arguments for :meth:`output`."""
wave: Optional[Union[QuantumCircuit, Hashable]]
"""The key or the circuit to execute."""
times: int
"""The number of random unitary operator.
It will denote as `N_U` in the experiment name."""
measure: Optional[Union[tuple[int, int], int, list[int]]]
"""The measure range."""
unitary_loc: Optional[Union[tuple[int, int], int, list[int]]]
"""The range of the unitary operator."""
unitary_loc_not_cover_measure: bool
"""Whether the range of the unitary operator is not cover the measure range."""
random_unitary_seeds: Optional[dict[int, dict[int, int]]]
"""The seeds for all random unitary operator.
This argument only takes input as type of `dict[int, dict[int, int]]`.
The first key is the index for the random unitary operator.
The second key is the index for the qubit.
.. code-block:: python
{
0: {0: 1234, 1: 5678},
1: {0: 2345, 1: 6789},
2: {0: 3456, 1: 7890},
}
If you want to generate the seeds for all random unitary operator,
you can use the function `generate_random_unitary_seeds`
in `qurry.qurrium.utils.random_unitary`.
.. code-block:: python
from qurry.qurrium.utils.random_unitary import generate_random_unitary_seeds
random_unitary_seeds = generate_random_unitary_seeds(100, 2)
"""
config_list: list[ShadowUnveilMeasureArgs] = [
{
"shots": 1024,
"times": 100,
"wave": f"{wave_names}_{i}",
"tags": (wave_names, f"size_{i}"),
}
for _ in range(10)
for i in range(2, 13, 2)
for wave_names in ["trivial_paramagnet", "ghz", "neel"]
]
print(len(config_list))
180
The .multiOutput
will return an id of this multimanager
instance,
which can be used to get the results and post-process them.
Each multimanager
will export the experiments in a folder you can specify
by setting save_location
parameter with default location for current directory
where Python executed.
It will create a folder with the name of the multimanager
instance,
and inside it will create a folder for storing each experiment data.
It will do firstly in the building process, but you can skip it by setting skip_build_write=True
to save time.
After all experiments are executed, it will export secondly,
which can also be skipped by setting skip_output_write=True
for no files output.
multi_exps1 = experiment_shadow.multiOutput(
config_list,
summoner_name="qurshady", # you can name it whatever you want
multiprocess_build=True,
# Using multiprocessing to build the experiments,
# it will be faster but take all the CPU
skip_build_write=True,
# Skip the writing of the experiment as files during the build,
save_location=".",
# Save the experiment as files in the current directory
multiprocess_write=True,
# Writing the experiment as files using multiprocessing,
)
multi_exps1
| MultiManager building...
| Write "qurshady.001", at location "qurshady.001"
| MultiOutput running...
| Export multimanager...
| No quantity to export.
| Export multi.config.json for 433d109c-c501-4c1d-b80c-3f057fd9aa57
| Exporting qurshady.001/qurryinfo.json...
| Exporting qurshady.001/qurryinfo.json done.
'433d109c-c501-4c1d-b80c-3f057fd9aa57'
You can check the result of multiOutput
that we just executed by accessing the .multimanagers
experiment_shadow.multimanagers
MultiManagerContainer(num=1, {
"433d109c-c501-4c1d-b80c-3f057fd9aa57":
<MultiManager(name="qurshady.001", jobstype="local", ..., exps_num=180)>,
})
experiment_shadow.multimanagers[multi_exps1]
<MultiManager(id="433d109c-c501-4c1d-b80c-3f057fd9aa57",
name="qurshady.001",
tags=(),
jobstype="local",
pending_strategy="tags",
last_events={
'output.001': '2025-07-08 17:04:33',},
exps_num=180)>
d. Run post-processing at once¶
experiment_shadow.multiAnalysis(
summoner_id=multi_exps1,
skip_write=True,
multiprocess_write=False,
selected_qubits=[0, 1],
)
WARNING:2025-07-08 17:04:45,867:jax._src.xla_bridge:909: An NVIDIA GPU may be present on this machine, but a CUDA-enabled jaxlib is not installed. Falling back to cpu.
| "report.001" has been completed.
'433d109c-c501-4c1d-b80c-3f057fd9aa57'
print("| Available results:")
for k, v in (
experiment_shadow.multimanagers[multi_exps1]
.quantity_container["report.001"]
.items()
):
print("| -", k, "with length", len(v))
| Available results:
| - ('trivial_paramagnet', 'size_2') with length 10
| - ('ghz', 'size_2') with length 10
| - ('neel', 'size_2') with length 10
| - ('trivial_paramagnet', 'size_4') with length 10
| - ('ghz', 'size_4') with length 10
| - ('neel', 'size_4') with length 10
| - ('trivial_paramagnet', 'size_6') with length 10
| - ('ghz', 'size_6') with length 10
| - ('neel', 'size_6') with length 10
| - ('trivial_paramagnet', 'size_8') with length 10
| - ('ghz', 'size_8') with length 10
| - ('neel', 'size_8') with length 10
| - ('trivial_paramagnet', 'size_10') with length 10
| - ('ghz', 'size_10') with length 10
| - ('neel', 'size_10') with length 10
| - ('trivial_paramagnet', 'size_12') with length 10
| - ('ghz', 'size_12') with length 10
| - ('neel', 'size_12') with length 10
Example of the content of
quantity_container
experiment_shadow.multimanagers[multi_exps1].quantity_container["report.001"][
("trivial_paramagnet", "size_10")
][:2]
[{'classical_registers_actually': [1, 0],
'taking_time': 0.005743980407714844,
'mean_of_rho': array([[0.24980957+0.j , 0.23140137+0.00307617j,
0.23192871+0.00114258j, 0.24741211+0.0023291j ],
[0.23140137-0.00307617j, 0.25118652+0.j ,
0.24758789-0.00180176j, 0.23307129-0.00140625j],
[0.23192871-0.00114258j, 0.24758789+0.00180176j,
0.2523291 +0.j , 0.23359863-0.00219727j],
[0.24741211-0.0023291j , 0.23307129+0.00140625j,
0.23359863+0.00219727j, 0.2466748 +0.j ]]),
'purity': np.float64(0.8976201092113149),
'entropy': np.float64(0.1558230981580489),
'estimate_of_given_operators': [],
'accuracy_prob_comp_delta': nan,
'num_of_estimators_k': 0,
'accuracy_predict_epsilon': nan,
'maximum_shadow_norm': nan,
'input': {'shots': 1024,
'num_qubits': 10,
'selected_qubits': [0, 1],
'registers_mapping': {0: 0,
1: 1,
2: 2,
3: 3,
4: 4,
5: 5,
6: 6,
7: 7,
8: 8,
9: 9},
'bitstring_mapping': {0: 0,
1: 1,
2: 2,
3: 3,
4: 4,
5: 5,
6: 6,
7: 7,
8: 8,
9: 9},
'unitary_located': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]},
'header': {'serial': 0, 'datetime': '2025-07-08 17:04:46', 'log': {}}},
{'classical_registers_actually': [1, 0],
'taking_time': 0.005501508712768555,
'mean_of_rho': array([[0.25134766+0.j , 0.21464355+0.00087891j,
0.27903809+0.00375j , 0.22420898+0.00026367j],
[0.21464355-0.00087891j, 0.24821289+0.j ,
0.22579102+0.00246094j, 0.27596191-0.00169922j],
[0.27903809-0.00375j , 0.22579102-0.00246094j,
0.24677734+0.j , 0.22035645-0.00175781j],
[0.22420898-0.00026367j, 0.27596191+0.00169922j,
0.22035645+0.00175781j, 0.25366211+0.j ]]),
'purity': np.float64(0.9213573577187278),
'entropy': np.float64(0.11816726624340655),
'estimate_of_given_operators': [],
'accuracy_prob_comp_delta': nan,
'num_of_estimators_k': 0,
'accuracy_predict_epsilon': nan,
'maximum_shadow_norm': nan,
'input': {'shots': 1024,
'num_qubits': 10,
'selected_qubits': [0, 1],
'registers_mapping': {0: 0,
1: 1,
2: 2,
3: 3,
4: 4,
5: 5,
6: 6,
7: 7,
8: 8,
9: 9},
'bitstring_mapping': {0: 0,
1: 1,
2: 2,
3: 3,
4: 4,
5: 5,
6: 6,
7: 7,
8: 8,
9: 9},
'unitary_located': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]},
'header': {'serial': 0, 'datetime': '2025-07-08 17:04:46', 'log': {}}}]
e. Run post-processing at once with specific analysis arguments¶
At first, we need to get the each experiment’s id in the multimanager
instance.
expkeys_of_multi_exps1 = list(experiment_shadow.multimanagers[multi_exps1].exps.keys())
print(len(expkeys_of_multi_exps1))
print("| The number of exp_id:", len(expkeys_of_multi_exps1))
print("| First 3 experiment keys:")
expkeys_of_multi_exps1[:3]
180
| The number of exp_id: 180
| First 3 experiment keys:
['4ed3c66d-d6f1-4ac6-b2c0-e3c803790aea',
'da6ee756-f5ab-44fe-bd0e-32811a87f3fb',
'cbd44496-0c19-408b-a073-a5e635baf285']
If you want to run the post-processing for some specific experiments, for example, the first 3 experiments we get for the
multimanager
instance.
experiment_shadow.multiAnalysis(
summoner_id=multi_exps1,
analysis_name="first_3",
skip_write=True,
multiprocess_write=False,
specific_analysis_args={
k: (
{
"selected_qubits": [0, 1],
}
if idx < 3
else False # Give False to skip analysis for this experiment
)
for idx, k in enumerate(expkeys_of_multi_exps1)
},
)
| "first_3.001" has been completed.
'433d109c-c501-4c1d-b80c-3f057fd9aa57'
print("| Available results:")
print(
"| length:",
sum(
len(v)
for v in experiment_shadow.multimanagers[multi_exps1]
.quantity_container["first_3.001"]
.values()
),
)
| Available results:
| length: 3
Or manually specify all the analysis arguments for each experiment.
experiment_shadow.multiAnalysis(
summoner_id=multi_exps1,
skip_write=False,
analysis_name="all_manual",
multiprocess_write=True,
specific_analysis_args={
k: {
"selected_qubits": [0, 1], # selected qubits for the analysis
}
for idx, k in enumerate(expkeys_of_multi_exps1)
},
)
| "all_manual.001" has been completed.
| Export multimanager...
| Export multi.config.json for 433d109c-c501-4c1d-b80c-3f057fd9aa57
| Exporting qurshady.001/qurryinfo.json...
| Exporting qurshady.001/qurryinfo.json done.
'433d109c-c501-4c1d-b80c-3f057fd9aa57'
print("| Available results:")
print(
"| length:",
sum(
len(v)
for v in experiment_shadow.multimanagers[multi_exps1]
.quantity_container["all_manual.001"]
.values()
),
)
| Available results:
| length: 180
All multiAnalysis
results¶
experiment_shadow.multimanagers[multi_exps1].quantity_container.keys()
dict_keys(['report.001', 'first_3.001', 'all_manual.001'])
f. Read exported multimanager data¶
multi_exps1_reades = experiment_shadow.multiRead(
save_location=".",
summoner_name="qurshady.001",
)
| Retrieve qurshady.001...
| at: qurshady.001
Post-Process Availablities and Version Info¶
from qurry.process import AVAIBILITY_STATESHEET
AVAIBILITY_STATESHEET
| Qurrium version: 0.13.0
---------------------------------------------------------------------------
### Qurrium Post-Processing
- Backend Availability ................... Python Cython Rust JAX
- randomized_measure
- entangled_entropy.entropy_core_2 ....... Yes Depr. Yes No
- entangle_entropy.purity_cell_2 ......... Yes Depr. Yes No
- entangled_entropy_v1.entropy_core ...... Yes Depr. Yes No
- entangle_entropy_v1.purity_cell ........ Yes Depr. Yes No
- wavefunction_overlap.echo_core_2 ....... Yes Depr. Yes No
- wavefunction_overlap.echo_cell_2 ....... Yes Depr. Yes No
- wavefunction_overlap_v1.echo_core ...... Yes Depr. Yes No
- wavefunction_overlap_v1.echo_cell ...... Yes Depr. Yes No
- hadamard_test
- purity_echo_core ....................... Yes No Yes No
- magnet_square
- magnsq_core ............................ Yes No Yes No
- string_operator
- strop_core ............................. Yes No Yes No
- classical_shadow
- rho_m_core ............................. Yes No No Yes
- utils
- randomized ............................. Yes Depr. Yes No
- counts_process ......................... Yes No Yes No
- bit_slice .............................. Yes No Yes No
- dummy .................................. Yes No Yes No
- test ................................... Yes No Yes No
---------------------------------------------------------------------------
+ Yes ...... Working normally.
+ Error .... Exception occurred.
+ No ....... Not supported.
+ Depr. .... Deprecated.
---------------------------------------------------------------------------
by <Hoshi>