1.3 Entanglement Entropy by Classical Shadow#
Basic Usage#
a. Import the instances#
from qurry import ShadowUnveil
experiment_shadow = ShadowUnveil()
b. Preparing quantum circuit#
from qiskit import QuantumCircuit
from qurry.recipe import TrivialParamagnet, GHZ
sample01 = TrivialParamagnet(8)
print("| trivial paramagnet in 8 qubits:")
print(sample01)
| trivial paramagnet in 8 qubits:
┌───┐
q_0: ┤ H ├
├───┤
q_1: ┤ H ├
├───┤
q_2: ┤ H ├
├───┤
q_3: ┤ H ├
├───┤
q_4: ┤ H ├
├───┤
q_5: ┤ H ├
├───┤
q_6: ┤ H ├
├───┤
q_7: ┤ H ├
└───┘
sample02 = GHZ(8)
print("| GHZ in 8 qubits:")
print(sample02)
| GHZ in 8 qubits:
┌───┐
q_0: ┤ H ├──■────────────────────────────────
└───┘┌─┴─┐
q_1: ─────┤ X ├──■───────────────────────────
└───┘┌─┴─┐
q_2: ──────────┤ X ├──■──────────────────────
└───┘┌─┴─┐
q_3: ───────────────┤ X ├──■─────────────────
└───┘┌─┴─┐
q_4: ────────────────────┤ X ├──■────────────
└───┘┌─┴─┐
q_5: ─────────────────────────┤ X ├──■───────
└───┘┌─┴─┐
q_6: ──────────────────────────────┤ X ├──■──
└───┘┌─┴─┐
q_7: ───────────────────────────────────┤ X ├
└───┘
sample03 = QuantumCircuit(8)
sample03.x(range(0, 8, 2))
print("| Custom circuit:")
print(sample03)
| Custom circuit:
┌───┐
q_0: ┤ X ├
└───┘
q_1: ─────
┌───┐
q_2: ┤ X ├
└───┘
q_3: ─────
┌───┐
q_4: ┤ X ├
└───┘
q_5: ─────
┌───┐
q_6: ┤ X ├
└───┘
q_7: ─────
c. Execute the circuit#
i. Directly input the circuit#
After executing, it will return a uuid of experiment. You can use this uuid to get the result of the experiment.
exp1 = experiment_shadow.measure(sample01, times=100, shots=4096)
exp1
'925b20f3-4e68-4756-82e8-acad19048789'
Each experiment result will be stored in a container .exps
.
experiment_shadow.exps[exp1]
<ShadowUnveilExperiment(exp_id=925b20f3-4e68-4756-82e8-acad19048789,
ShadowUnveilArguments(exp_name='experiment.N_U_100.qurshady_entropy', times=100, qubits_measured=[0, 1, 2, 3, 4, 5, 6, 7], registers_mapping={0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7}, actual_num_qubits=8, unitary_located=[0, 1, 2, 3, 4, 5, 6, 7], random_unitary_seeds=None),
Commonparams(exp_id='925b20f3-4e68-4756-82e8-acad19048789', target_keys=[0], shots=4096, backend=<AerSimulator('aer_simulator')>, run_args={}, transpile_args={}, tags=(), save_location=PosixPath('.'), serial=None, summoner_id=None, summoner_name=None, datetimes=DatetimeDict({'build': '2025-06-26 11:46:59', 'run.001': '2025-06-26 11:46:59'})),
unused_args_num=0,
analysis_num=0))>
report01 = experiment_shadow.exps[exp1].analyze(
selected_qubits=[0, 1, 2, 3],
)
report01
<SUAnalysis(
serial=0,
SUAnalysisInput(shots=4096, num_qubits=8, selected_qubits=[0, 1, 2, 3], registers_mapping={0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7}, bitstring_mapping={0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7}, unitary_located=[0, 1, 2, 3, 4, 5, 6, 7]),
SUAnalysisContent(purity=1.0224932396682826, entropy=-0.03209130465427579, and others)),
unused_args_num=3
)>
The analysis result will be content following the structure of the experiment result. The analysis fields in the dictionary main01
.
classical_registers_actually: list[int]
"""The list of the selected_classical_registers."""
taking_time: float
"""The time taken for the calculation."""
# The mean of Rho
mean_of_rho: np.ndarray[tuple[int, int], np.dtype[np.complex128]]
"""The expectation value of Rho."""
# The trace of Rho square
purity: float
"""The purity calculated by classical shadow."""
entropy: float
"""The entropy calculated by classical shadow."""
# esitimation of given operators
estimate_of_given_operators: list[np.ndarray[tuple[int, int], np.dtype[np.complex128]]]
r"""The result of measurement primitive :math:`\mathcal{U}`."""
accuracy_prob_comp_delta: float
r"""The probabiltiy complement of accuracy, which used the notation :math:`\delta`
and mentioned in Theorem S1 in the supplementary material,
the equation (S13) in the supplementary material.
The probabiltiy of accuracy is :math:`1 - \delta`.
The number of given operators and the accuracy parameters will
be used to decide the number of estimators K
from the equation (S13) in the supplementary material.
.. math::
K = 2 \log(2M / \delta)
where :math:`\delta` is the probabiltiy complement of accuracy,
and :math:`M` is the number of given operators.
But we can see :math:`K` will be not the integer value of the result of the equation.
So, we will use the ceil value of the result of the equation.
And recalculate the probabiltiy complement of accuracy from this new value of :math:`K`.
"""
num_of_estimators_k: int
r"""The number of esitmators, which used the notation K
and mentioned in Algorithm 1 in the paper,
Theorem S1 in the supplementary material,
the equation (S13) in the supplementary material.
We can calculate the number of esitmator K from the equation (S13)
in the supplementary material, the equation (S13) is as follows,
.. math::
K = 2 \log(2M / \delta)
where :math:`\delta` is the probabiltiy complement of accuracy,
and :math:`M` is the number of given operators.
But we can see :math:`K` will be not the integer value of the result of the equation.
So, we will use the ceil value of the result of the equation.
And recalculate the probabiltiy complement of accuracy from this new value of :math:`K`.
"""
accuracy_predict_epsilon: float
r"""The prediction of accuracy, which used the notation :math:`\epsilon`
and mentioned in Theorem S1 in the supplementary material,
the equation (S13) in the supplementary material.
We can calculate the prediction of accuracy :math:`\epsilon` from the equation (S13)
in the supplementary material, the equation (S13) is as follows,
.. math::
N = \frac{34}{\epsilon^2} \max_{1 \leq i \leq M}
|| O_i - \frac{\text{tr}(O_i)}{2^n} ||_{\text{shadow}}^2
where :math:`\epsilon` is the prediction of accuracy,
and :math:`M` is the number of given operatorsm
and :math:`N` is the number of classical snapshots.
The :math:`|| O_i - \frac{\text{tr}(O_i)}{2^n} ||_{\text{shadow}}^2` is maximum shadow norm,
which is defined in the supplementary material with value between 0 and 1.
"""
maximum_shadow_norm: float
r"""The maximum shadow norm, which is defined in the supplementary material
with value between 0 and 1.
The maximum shadow norm is used to calculate the prediction of accuracy :math:`\epsilon`
from the equation (S13) in the supplementary material.
We can calculate the prediction of accuracy :math:`\epsilon` from the equation (S13)
in the supplementary material, the equation (S13) is as follows,
.. math::
N = \frac{34}{\epsilon^2} \max_{1 \leq i \leq M}
|| O_i - \frac{\text{tr}(O_i)}{2^n} ||_{\text{shadow}}^2
where :math:`\epsilon` is the prediction of accuracy,
and :math:`M` is the number of given operatorsm
and :math:`N` is the number of classical snapshots.
The :math:`|| O_i - \frac{\text{tr}(O_i)}{2^n} ||_{\text{shadow}}^2` is maximum shadow norm,
which is defined in the supplementary material with value between 0 and 1.
Due to maximum shadow norm is complex and it is a norm,
we suppose we have the worst case scenario,
where the maximum shadow norm is 1 as default.
Thus, we can simplify the equation to:
.. math::
N = \frac{34}{\epsilon^2}
"""
main01, side_product01 = report01.export(jsonable=False)
# you need to set jsonable=False to get the raw data
# Otherwise, the data will be converted to JSON format,
# which 'mean_of_rho' will be a string instead of a numpy array
for k, v in main01.items():
if k == "mean_of_rho":
continue
print(f"| {k}: {v}")
print("| shape of mean_of_rho:")
print(main01["mean_of_rho"].shape)
| classical_registers_actually: [3, 2, 1, 0]
| taking_time: 0.031109094619750977
| purity: 1.0224932396682826
| entropy: -0.03209130465427579
| estimate_of_given_operators: []
| accuracy_prob_comp_delta: nan
| num_of_estimators_k: 0
| accuracy_predict_epsilon: nan
| maximum_shadow_norm: nan
| input: {'shots': 4096, 'num_qubits': 8, 'selected_qubits': [0, 1, 2, 3], 'registers_mapping': {0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7}, 'bitstring_mapping': {0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7}, 'unitary_located': [0, 1, 2, 3, 4, 5, 6, 7]}
| header: {'serial': 0, 'datetime': '2025-06-26 11:47:00', 'log': {}}
| shape of mean_of_rho:
(16, 16)
Also, side_product
will contain a dictionary of rho
from each group of random unitary,
called average_classical_snapshots_rho
, which is where mean_of_rho
is calculated from.
If you give the operators in in .analyze
to get the estimatiton. corresponding_rhos
will contain the rho
used for calculation.
average_classical_snapshots_rho: dict[int, np.ndarray[tuple[int, int], np.dtype[np.complex128]]]
"""The dictionary of Rho M."""
corresponding_rhos: list[np.ndarray[tuple[int, ...], np.dtype[np.complex128]]]
r"""The corresponding rho of measurement primitive :math:`\mathcal{U}`."""
for k, v in side_product01.items():
print(f"| {k}")
print(
"| length of average_classical_snapshots_rho:",
len(side_product01["average_classical_snapshots_rho"]),
)
print(
"| keys of average_classical_snapshots_rho:",
side_product01["average_classical_snapshots_rho"].keys(),
)
print("| shape of average_classical_snapshots_rho[0]:")
print(side_product01["average_classical_snapshots_rho"][0].shape)
| average_classical_snapshots_rho
| corresponding_rhos
| length of average_classical_snapshots_rho: 100
| keys of average_classical_snapshots_rho: dict_keys([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99])
| shape of average_classical_snapshots_rho[0]:
(16, 16)
ii. Add the circuits to container .waves
, then call them later.#
Since we have executed an experiment, the circuit we input in exp1
is stored in the container .waves
with serial number 0
.
experiment_shadow.waves
WaveContainer({
0: <qurry.recipe.simple.paramagnet.TrivialParamagnet object at 0x7a29040f8590>})
But we can also add the circuit to the container .waves
with a custom name.
The name should be unique, otherwise it will be overwritten.
The method add
will return the actual name of the circuit in the container.
print(experiment_shadow.add(sample02, "ghz_8"))
print(experiment_shadow.waves["ghz_8"])
ghz_8
┌───┐
q_0: ┤ H ├──■────────────────────────────────
└───┘┌─┴─┐
q_1: ─────┤ X ├──■───────────────────────────
└───┘┌─┴─┐
q_2: ──────────┤ X ├──■──────────────────────
└───┘┌─┴─┐
q_3: ───────────────┤ X ├──■─────────────────
└───┘┌─┴─┐
q_4: ────────────────────┤ X ├──■────────────
└───┘┌─┴─┐
q_5: ─────────────────────────┤ X ├──■───────
└───┘┌─┴─┐
q_6: ──────────────────────────────┤ X ├──■──
└───┘┌─┴─┐
q_7: ───────────────────────────────────┤ X ├
└───┘
If there is a circuit with the same name, it will be replaced by the new one.
print(experiment_shadow.add(sample03, "ghz_8"))
print(experiment_shadow.waves["ghz_8"])
ghz_8
┌───┐
q_0: ┤ X ├
└───┘
q_1: ─────
┌───┐
q_2: ┤ X ├
└───┘
q_3: ─────
┌───┐
q_4: ┤ X ├
└───┘
q_5: ─────
┌───┐
q_6: ┤ X ├
└───┘
q_7: ─────
Otherwise, you will need to use replace="duplicate"
to prevent it from being replaced.
duplicated_case01 = experiment_shadow.add(sample02, "ghz_8", replace="duplicate")
print(duplicated_case01)
print(experiment_shadow.waves[duplicated_case01])
ghz_8.2
┌───┐
q_0: ┤ H ├──■────────────────────────────────
└───┘┌─┴─┐
q_1: ─────┤ X ├──■───────────────────────────
└───┘┌─┴─┐
q_2: ──────────┤ X ├──■──────────────────────
└───┘┌─┴─┐
q_3: ───────────────┤ X ├──■─────────────────
└───┘┌─┴─┐
q_4: ────────────────────┤ X ├──■────────────
└───┘┌─┴─┐
q_5: ─────────────────────────┤ X ├──■───────
└───┘┌─┴─┐
q_6: ──────────────────────────────┤ X ├──■──
└───┘┌─┴─┐
q_7: ───────────────────────────────────┤ X ├
└───┘
Now we have prepared the circuit and stored it in the container .waves
.
experiment_shadow.waves
WaveContainer({
0: <qurry.recipe.simple.paramagnet.TrivialParamagnet object at 0x7a29040f8590>,
'ghz_8': <qiskit.circuit.quantumcircuit.QuantumCircuit object at 0x7a29040f50f0>,
'ghz_8.2': <qurry.recipe.simple.cat.GHZ object at 0x7a29040fa3c0>})
Finally, we can execute the circuit and get the result.
exp2 = experiment_shadow.measure("ghz_8.2", times=100, shots=4096)
exp2
/root/.local/share/uv/python/cpython-3.13.5-linux-x86_64-gnu/lib/python3.13/multiprocessing/popen_fork.py:67: RuntimeWarning: os.fork() was called. os.fork() is incompatible with multithreaded code, and JAX is multithreaded, so this will likely lead to a deadlock.
self.pid = os.fork()
/root/.local/share/uv/python/cpython-3.13.5-linux-x86_64-gnu/lib/python3.13/multiprocessing/popen_fork.py:67: RuntimeWarning: os.fork() was called. os.fork() is incompatible with multithreaded code, and JAX is multithreaded, so this will likely lead to a deadlock.
self.pid = os.fork()
'82d784b1-199f-4cd1-a90d-8e22ecf8103e'
experiment_shadow.exps[exp2]
<ShadowUnveilExperiment(exp_id=82d784b1-199f-4cd1-a90d-8e22ecf8103e,
ShadowUnveilArguments(exp_name='experiment.N_U_100.qurshady_entropy', times=100, qubits_measured=[0, 1, 2, 3, 4, 5, 6, 7], registers_mapping={0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7}, actual_num_qubits=8, unitary_located=[0, 1, 2, 3, 4, 5, 6, 7], random_unitary_seeds=None),
Commonparams(exp_id='82d784b1-199f-4cd1-a90d-8e22ecf8103e', target_keys=['ghz_8.2'], shots=4096, backend=<AerSimulator('aer_simulator')>, run_args={}, transpile_args={}, tags=(), save_location=PosixPath('.'), serial=None, summoner_id=None, summoner_name=None, datetimes=DatetimeDict({'build': '2025-06-26 11:47:07', 'run.001': '2025-06-26 11:47:07'})),
unused_args_num=0,
analysis_num=0))>
report02 = experiment_shadow.exps[exp2].analyze(
selected_qubits=[0, 1, 2, 3],
)
report02
<SUAnalysis(
serial=0,
SUAnalysisInput(shots=4096, num_qubits=8, selected_qubits=[0, 1, 2, 3], registers_mapping={0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7}, bitstring_mapping={0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7}, unitary_located=[0, 1, 2, 3, 4, 5, 6, 7]),
SUAnalysisContent(purity=0.47988934820348567, entropy=1.0592263040557093, and others)),
unused_args_num=3
)>
main02, side_product02 = report02.export(jsonable=False)
# you need to set jsonable=False to get the raw data
# Otherwise, the data will be converted to JSON format,
# which 'mean_of_rho' will be a string instead of a numpy array
for k, v in main02.items():
if k == "mean_of_rho":
continue
print(f"| {k}: {v}")
print("| the shape of mean_of_rho:")
# For its shape, it should be (2**n, 2**n) where n is the number of selected qubits
print(main02["mean_of_rho"].shape)
| classical_registers_actually: [3, 2, 1, 0]
| taking_time: 0.04737687110900879
| purity: 0.47988934820348567
| entropy: 1.0592263040557093
| estimate_of_given_operators: []
| accuracy_prob_comp_delta: nan
| num_of_estimators_k: 0
| accuracy_predict_epsilon: nan
| maximum_shadow_norm: nan
| input: {'shots': 4096, 'num_qubits': 8, 'selected_qubits': [0, 1, 2, 3], 'registers_mapping': {0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7}, 'bitstring_mapping': {0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7}, 'unitary_located': [0, 1, 2, 3, 4, 5, 6, 7]}
| header: {'serial': 0, 'datetime': '2025-06-26 11:47:08', 'log': {}}
| the shape of mean_of_rho:
(16, 16)
d. Export them after all#
exp1_id, exp1_files_info = experiment_shadow.exps[exp1].write(
save_location=".", # where to save files
)
exp1_files_info
{'folder': 'experiment.N_U_100.qurshady_entropy.001',
'qurryinfo': 'experiment.N_U_100.qurshady_entropy.001/qurryinfo.json',
'args': 'experiment.N_U_100.qurshady_entropy.001/args/experiment.N_U_100.qurshady_entropy.001.id=925b20f3-4e68-4756-82e8-acad19048789.args.json',
'advent': 'experiment.N_U_100.qurshady_entropy.001/advent/experiment.N_U_100.qurshady_entropy.001.id=925b20f3-4e68-4756-82e8-acad19048789.advent.json',
'legacy': 'experiment.N_U_100.qurshady_entropy.001/legacy/experiment.N_U_100.qurshady_entropy.001.id=925b20f3-4e68-4756-82e8-acad19048789.legacy.json',
'tales.random_unitary_ids': 'experiment.N_U_100.qurshady_entropy.001/tales/experiment.N_U_100.qurshady_entropy.001.id=925b20f3-4e68-4756-82e8-acad19048789.random_unitary_ids.json',
'reports': 'experiment.N_U_100.qurshady_entropy.001/reports/experiment.N_U_100.qurshady_entropy.001.id=925b20f3-4e68-4756-82e8-acad19048789.reports.json',
'reports.tales.average_classical_snapshots_rho': 'experiment.N_U_100.qurshady_entropy.001/tales/experiment.N_U_100.qurshady_entropy.001.id=925b20f3-4e68-4756-82e8-acad19048789.average_classical_snapshots_rho.reports.json',
'reports.tales.corresponding_rhos': 'experiment.N_U_100.qurshady_entropy.001/tales/experiment.N_U_100.qurshady_entropy.001.id=925b20f3-4e68-4756-82e8-acad19048789.corresponding_rhos.reports.json'}
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>