Návrhové principy reprezentací operátorů
Tento průvodce vysvětluje společné návrhové principy a základní koncepty sdílené všemi reprezentacemi operátorů v modulu operators.
Přehled
Reprezentace operátorů poskytované tímto modulem sdílejí několik základních návrhových principů:
-
Řídká datová struktura: Operátory kódují pouze neidentitní operace. Vnitřní uspořádání dat je obecně inspirováno formáty řídkých matic, což umožňuje efektivní uložení a výpočet pro systémy s mnoha módy, ale poměrně málo významnými příspěvky.
-
Iterace a rekonstrukce členů: Bez ohledu na vnitřní řídké uložení poskytují operátory konzistentní iterační rozhraní, takže můžeš členy prohlížet, filtrovat a transformovat, aniž bys rozuměl podkladové datové struktuře, a poté z upravených členů rekonstruovat nové operátory.
-
Indexování podle módů: Operátory používají abstraktní indexy módů k označení fermionických stupňů volnosti, což umožňuje flexibilní mapování z fyzikálních systémů na reprezentaci operátoru.
-
Seskupování členů a komutační vztahy: Operátory nativně podporují informace o seskupení, které přiřazují členům indexy skupin. To umožňuje optimalizace a zachování fyzikální struktury bez potřeby samostatných datových struktur. Praktické použití najdeš v průvodci seskupováním.
-
Aritmetické a matematické operace: Všechny operátory implementují konzistentní sadu aritmetických operací (jako sčítání, násobení a skládání) a matematických funkcí pomocí protokolu
OperatorTrait, což umožňuje jednotný kód napříč různými typy operátorů. -
Řazení členů operátoru a normální formy: Matematicky ekvivalentní operátory mohou mít v kvantových algoritmech velmi odlišné reprezentace a chování. Všechny reprezentace operátorů podporují různé normální formy (založené na komutačních vztazích specifických pro danou algebru), aby bylo dosaženo kanonických, předvídatelných a optimalizovatelných reprezentací operátorů.
Řídká datová struktura
Všechny operátory používají řídkou reprezentaci, v níž se sledují pouze neidentitní operace. Každá neidentitní operace se skládá z koeficientu (komplexní číslo) a posloupnosti akcí na konkrétních módech.
Tento přístup dramaticky snižuje spotřebu paměti i dobu výpočtu, zejména u systémů s mnoha módy, ale poměrně málo významnými příspěvky. Kódováním pouze neidentitních operací se operace soustředí jen na to, na čem záleží, což umožňuje pracovat s velkými systémy, které by s hustými reprezentacemi byly neproveditelné. Operátory se navíc přirozeně škálují na libovolný počet módů; operátor působící na módy {0, 1} funguje beze změny v systémech s mnohem více módy, protože nedotčené módy jsou implicitně identita.
Shodné členy se při aritmetických operacích uchovávají samostatně a v případě potřeby je nutné je výslovně sloučit.
Vnitřní formát uložení
Operátory jsou interně uloženy v polích inspirovaných formáty řídkých matic:
-
Pole koeficientů: Komplexní koeficient každého členu
-
Pole indexů módů: Fermionické módy, na které každá akce působí
-
Pole hranic: Indexy označující, kde v poli módů začínají a končí módy každého členu
V závislosti na typu operátoru mohou být přítomna další pole. Například instance FermionOperator obsahují pole akcí booleovských hodnot, které určuje typ fermionické akce působící na příslušný index módu. Naproti tomu třída MajoranaOperator toto rozlišení nepotřebuje, protože tuto informaci kóduje v paritě indexu módu. Úplný formát uložení najdeš v dokumentaci API pro svůj typ operátoru.
Následující příklady ukazují, jak jsou tato pole organizována. Nejprve přímá konstrukce pomocí řídkých polí:
- PYTHON
- C
>>> from qiskit_fermions.operators import FermionOperator
>>>
>>> # Construct operators directly using sparse arrays
>>> # First operator: 1.0 * +0 -1
>>> op1 = FermionOperator(
... coeffs=[1.0],
... actions=[True, False],
... modes=[0, 1],
... boundaries=[0, 2],
... )
>>>
>>> # Second operator: 1.0 * +2 -3
>>> op2 = FermionOperator(
... coeffs=[1.0],
... actions=[True, False],
... modes=[2, 3],
... boundaries=[0, 2],
... )
>>>
>>> # Combine the sparse operators
>>> op1 += op2
>>> print(format(op1))
1.000000e0 +0.000000e0j * (+0 -1)
1.000000e0 +0.000000e0j * (+2 -3)
#include <qiskit_fermions.h>
// Construct first operator: 1.0 * +0 -1
QkComplex64 coeff1[1] = {{1.0, 0.0}};
bool actions1[2] = {true, false};
uint32_t modes1[2] = {0, 1};
uint32_t boundaries1[2] = {0, 2};
QfFermionOperator *op1 = qf_ferm_op_new(1, 2, coeff1, actions1, modes1, boundaries1);
// Construct second operator: 1.0 * +2 -3
QkComplex64 coeff2[1] = {{1.0, 0.0}};
bool actions2[2] = {true, false};
uint32_t modes2[2] = {2, 3};
uint32_t boundaries2[2] = {0, 2};
QfFermionOperator *op2 = qf_ferm_op_new(1, 2, coeff2, actions2, modes2, boundaries2);
// Combine the sparse operators. There is no in-place add in the C API, so
// the sum is a new operator that the caller owns.
QfFermionOperator *sum = qf_ferm_op_add(op1, op2);
qf_ferm_op_free(op1);
qf_ferm_op_free(op2);
qf_ferm_op_free(sum);
Praktické metody konstrukce
Pro vývojáře v Pythonu je k dispozici několik praktických metod konstrukce, které skrývají detaily řídkého uložení. Usnadňují sestavování operátorů, aniž by ses musel starat o správu polí koeficientů, módů a hranic:
- PYTHON
- C
>>> from qiskit_fermions.operators import FermionOperator, cre, ann
>>>
>>> # Construct operators using operator algebra notation
>>> op1 = FermionOperator.from_dict({(cre(0), ann(1)): 1.0})
>>> op2 = FermionOperator.from_dict({(cre(2), ann(3)): 1.0})
>>>
>>> # The result is sparse even when combining them
>>> op1 += op2
>>> print(format(op1))
1.000000e0 +0.000000e0j * (+0 -1)
1.000000e0 +0.000000e0j * (+2 -3)
// The C API uses direct array construction; convenience methods are not available.
Jednotlivé implementace operátorů mohou podporovat další metody konstrukce přizpůsobené jejich konkrétnímu použití. Všechny dostupné možnosti konstrukce pro svůj typ operátoru najdeš v dokumentaci API.
Iterace přes členy a rekonstrukce
Operátory poskytují jednotné iterační rozhraní pomocí OperatorTrait.iter_terms() bez ohledu na jejich vnitřní řídkou reprezentaci. Členy tak můžeš prohlížet, filtrovat nebo transformovat, aniž bys musel rozumět podkladové datové struktuře. Z transformovaných členů pak můžeš pomocí OperatorTrait.from_terms() znovu sestavit nový operátor.
- PYTHON
- C
>>> from qiskit_fermions.operators import FermionOperator, cre, ann
>>>
>>> # Construct an operator with terms of different orders
>>> op = FermionOperator.from_dict({
... (): 0.5, # constant term (order 0)
... (cre(0), ann(1)): 1.0, # two-body term (order 2)
... (cre(0), cre(1), ann(1), ann(0)): 0.25 # four-body term (order 4)
... })
>>>
>>> # Filter to keep only terms of order 2
>>> order_two_terms = [
... (term, coeff) for term, coeff in op.iter_terms()
... if len(term) == 2
... ]
>>>
>>> # Reconstruct operator from filtered terms
>>> filtered_op = FermionOperator.from_terms(order_two_terms)
>>> print(f"Original operator has {len(op)} terms")
Original operator has 3 terms
>>> print(f"Filtered operator has {len(filtered_op)} term")
Filtered operator has 1 term
// WARNING: Term iteration and filtering are not yet available in the C API.
Indexování podle módů
Všechny reprezentace operátorů označují indexy, na které jejich členy působí, jako módy. Mód je prostě index identifikující fermionový stupeň volnosti ve tvém systému. Mapování fyzikálních stupňů volnosti (například prostorových orbitalů, spinových stavů nebo jiných kvantových čísel) na indexy módů je ponecháno na uživateli, což umožňuje maximální flexibilitu.
Tato abstrakce je přítomná také v modulu qiskit_fermions.circuit, kde FermionicCircuit pracuje s registrem fermionových módů. V reprezentaci operátorů i obvodů poskytují módy jednotný způsob nezávislý na algebře, jak určit, které stupně volnosti se dané operace účastní.
Současné implementace používají módy bez spinu: Všechny reprezentace operátorů, které tento modul aktuálně nabízí, zacházejí s módy jako s fermionovými stupni volnosti bez spinu. To znamená, že pokud tvůj systém obsahuje elektrony nebo fermiony se spinem nahoru i dolů, musíš je explicitně namapovat na různé módy (například módy 0-3 pro spin nahoru čtyř prostorových orbitalů, módy 4-7 pro spin dolů, nebo jakoukoli jinou konvenci, kterou zvolíš).
Tento návrh udržuje základní reprezentace jednoduché a obecné a zároveň nevnucuje konkrétní konvenci řazení spinů. Pomocné moduly jako qiskit_fermions.operators.library nabízejí praktické funkce (například FCIDump.from_file()), které při načítání dat o elektronové struktuře taková mapování vyřeší za tebe.
S vývojem balíčku mohou přibýt reprezentace operátorů se spinem, které budou spinové stupně volnosti podporovat přímo ve svém datovém modelu. Budou jasně odlišeny od současných implementací bez spinu a budou s nimi v modulu koexistovat.
Seskupování členů a komutační vztahy
Stejně jako koeficienty a indexy módů mohou operátory volitelně ukládat pole skupin, které každému členu přiřazuje index skupiny. Tím, že je seskupování zabudováno přímo do reprezentace operátoru jako součást řídké datové struktury, putuje informace o seskupení přirozeně s operátorem při transformacích. To umožňuje systematické využití struktury, ať už pochází z fyzikálních vlastností, algebraických vztahů nebo symetrií specifických pro daný problém. Strukturované informace pak lze využít v navazujících operacích, jako je syntéza a dekompozice obvodů, pomocí metod jako OperatorTrait.split_out_groups().
Podrobný návod, jak ve svých pracovních postupech seskupovat členy operátorů, najdeš v průvodci seskupování.
- PYTHON
- C
>>> from qiskit_fermions.operators import MajoranaOperator, gamma
>>> op = MajoranaOperator.from_dict({
... (gamma(0, False),): 1.0,
... (gamma(1, False),): 1.0,
... (gamma(2, False), gamma(3, False)): 1.0
... })
>>> # Assign group indices to terms
>>> op.groups = [0, 0, 1]
>>> # Partition operator by groups
>>> grouped_ops = op.split_out_groups()
#include <qiskit_fermions.h>
// Create operator with 3 terms
QkComplex64 coeffs[3] = {{1.0, 0.0}, {1.0, 0.0}, {1.0, 0.0}};
uint32_t modes[4] = {0, 1, 2, 3};
uint32_t boundaries[4] = {0, 1, 2, 4};
QfMajoranaOperator *op = qf_maj_op_new(3, 4, coeffs, modes, boundaries);
// Assign grouping information
uint32_t groups[3] = {0, 0, 1};
qf_maj_op_set_groups(op, groups, 3);
// Partition operator by groups
QfMajoranaOperator *grouped_ops[2];
qf_maj_op_split_out_groups(op, NULL, 0, grouped_ops);
Aritmetické a matematické operace
Všechny operátory implementují protokol OperatorTrait, který poskytuje jednotnou sadu operací napříč různými typy operátorů. Díky tomu kód napsaný pro jednu reprezentaci operátoru funguje stejně i s ostatními. Jak tento protokol souvisí s dalšími protokoly definovanými v tomto balíčku, se dozvíš v qiskit_fermions.protocols.
Protokol zahrnuje aritmetické operace (například sčítání, násobení a skládání), strukturální operace (iterace přes členy, analýza podpory módů, přeznačování), matematické funkce (normální uspořádání, zjednodušování, kontrola ekvivalence) a další. Úplný přehled všech dostupných operací najdeš v dokumentaci OperatorTrait.
- PYTHON
- C
>>> from qiskit_fermions.operators import FermionOperator, cre, ann
>>>
>>> # Construct a Hermitian operator: H = +0 -1 + +1 -0
>>> op = FermionOperator.from_dict({
... (cre(0), ann(1)): 1.0,
... (cre(1), ann(0)): 1.0
... })
>>>
>>> # Check if the operator is Hermitian
>>> print(f"Operator is Hermitian: {op.is_hermitian(atol=1e-10)}")
Operator is Hermitian: True
#include <qiskit_fermions.h>
// Construct a Hermitian operator: H = +0 -1 + +1 -0
QkComplex64 coeffs[2] = {{1.0, 0.0}, {1.0, 0.0}};
bool actions[4] = {true, false, true, false};
uint32_t modes[4] = {0, 1, 1, 0};
uint32_t boundaries[3] = {0, 2, 4};
QfFermionOperator *op = qf_ferm_op_new(2, 4, coeffs, actions, modes, boundaries);
// Check if the operator is Hermitian
bool is_hermitian = qf_ferm_op_is_hermitian(op, 1e-10);
printf("Operator is Hermitian: %s\n", is_hermitian ? "true" : "false");
// Clean up
qf_ferm_op_free(op);
is_hermitian() ověřuje, že , tak, že tento rozdíl převede na normální tvar a porovná ho s zero(). Rozepsáno pomocí ostatních operací protokolu provádí následující:
>>> difference = (op - op.adjoint()).normal_ordered().simplify(atol=1e-10)
>>> difference.equiv(FermionOperator.zero(), atol=1e-10)
True
Síla této kontroly závisí na typu operátoru, protože závisí na tom, jak úplně normální tvar daného typu kontrahuje sousední generátory. Výsledek True je vždy spolehlivý, ale u některých typů je výsledek False konzervativní: podrobnosti viz is_hermitian().
Příklad používá atol=1e-10. Parametr atol (absolutní tolerance) určuje práh. Koeficienty s velikostí menší než atol se považují za nulové a zahazují se. To je zásadní pro numerickou stabilitu při porovnávání operátorů, protože aritmetika s pohyblivou řádovou čárkou může způsobit drobné zaokrouhlovací chyby, které by jinak bránily rozpoznání ekvivalentních operátorů.
Protokol OperatorTrait poskytuje společné rozhraní, ale jednotlivé implementace operátorů mohou nabízet další praktické metody, které součástí protokolu nejsou. Například FermionOperator a MajoranaOperator nabízejí metodu max_rank(), která vrací délku nejdelšího členu. Všechny dostupné funkce pro svůj typ operátoru zjistíš v dokumentaci API.
Uspořádání členů operátorů a normální tvary
Základní výzvou v algebře kvantových operátorů je, že matematicky ekvivalentní operátory lze reprezentovat mnoha různými způsoby, z nichž každý má jiné důsledky pro kvantové algoritmy. Stejný operátor lze zapsat v algebraicky ekvivalentních tvarech (například lze vyjádřit jako ), a přesto tyto reprezentace vedou k odlišnému chování při syntéze obvodů, zjednodušování a v numerických algoritmech.
Reprezentace operátorů podporují operace normálního uspořádání, které pomocí komutačních relací transformují operátory do kanonických tvarů specifických pro danou algebru. To umožňuje spolehlivé porovnávání (dva ekvivalentní operátory mají shodné normálně uspořádané tvary), odhaluje zjednodušení (komutační relace způsobují, že se členy vyruší nebo sloučí) a podporuje algoritmy, které pro správnost a efektivitu vyžadují konkrétní tvary operátorů.
Různé reprezentace operátorů mohou implementovat normální uspořádání na základě různých komutačních relací odpovídajících jejich algebře. Například fermionové normální uspořádání využívá antikomutační relace (), zatímco majoranovské normální uspořádání používá jiné algebraické konvence (). Jak je normální uspořádání implementováno, zjistíš v dokumentaci pro svůj typ operátoru.
- PYTHON
- C
>>> from qiskit_fermions.operators import FermionOperator, cre, ann
>>>
>>> # Two different representations of the same operator
>>> op1 = FermionOperator.from_dict({(ann(0), cre(0)): 1.0})
>>> op2 = FermionOperator.from_dict({(): 1.0, (cre(0), ann(0)): -1.0})
>>>
>>> # Direct comparison fails due to different forms
>>> op1.equiv(op2, atol=1e-10)
False
>>>
>>> # Normal-order both and compare again
>>> op1_normal = op1.normal_ordered()
>>> op2_normal = op2.normal_ordered()
>>> op1_normal.equiv(op2_normal, atol=1e-10)
True
#include <qiskit_fermions.h>
#include <stdbool.h>
// Two different representations of the same operator
QkComplex64 coeff1[1] = {{1.0, 0.0}};
bool actions1[2] = {false, true};
uint32_t modes1[2] = {0, 0};
uint32_t boundaries1[2] = {0, 2};
QfFermionOperator *op1 = qf_ferm_op_new(1, 2, coeff1, actions1, modes1, boundaries1);
QkComplex64 coeff2[2] = {{1.0, 0.0}, {-1.0, 0.0}};
bool actions2[2] = {true, false};
uint32_t modes2[2] = {0, 0};
uint32_t boundaries2[3] = {0, 0, 2};
QfFermionOperator *op2 = qf_ferm_op_new(2, 2, coeff2, actions2, modes2, boundaries2);
// Direct comparison fails due to different forms
bool equiv_before = qf_ferm_op_equiv(op1, op2, 1e-10);
printf("Equivalent before normal ordering: %s\n", equiv_before ? "true" : "false");
// Normal-order both and compare again
QfFermionOperator *op1_normal = qf_ferm_op_normal_ordered(op1, NULL);
QfFermionOperator *op2_normal = qf_ferm_op_normal_ordered(op2, NULL);
bool equiv_after = qf_ferm_op_equiv(op1_normal, op2_normal, 1e-10);
printf("Equivalent after normal ordering: %s\n", equiv_after ? "true" : "false");
// Clean up
qf_ferm_op_free(op1);
qf_ferm_op_free(op2);
qf_ferm_op_free(op1_normal);
qf_ferm_op_free(op2_normal);
Metoda protokolu OperatorTrait.normal_ordered() záměrně nespecifikuje své poziční a pojmenované argumenty, takže konkrétní implementace operátorů mohou definovat nastavitelné parametry, které řídí přesný vytvářený kanonický tvar. To umožňuje optimalizace specifické pro operátor a varianty normálních tvarů přizpůsobené jejich algebře nebo případu užití. Dostupné parametry zjistíš v dokumentaci API pro svůj typ operátoru.