Instance a rozšíření
Tato kapitola pokryje několik kvantových variačních algoritmů, včetně
- Variační kvantový řešič vlastních čísel (VQE)
- Subspace Search VQE (SSVQE)
- Variational Quantum Deflation (VQD)
- Quantum Sampling Regression (QSR)
Pomocí těchto algoritmů se seznámíme s několika návrhovými myšlenkami, které lze začlenit do vlastních variačních algoritmů, jako jsou váhy, penalizace, převzorkování a podvzorkování. Doporučujeme ti, abys s těmito koncepty experimentoval a podělil se o svá zjištění s komunitou.
Framework Qiskit patterns se vztahuje na všechny tyto algoritmy – jednotlivé kroky však explicitně vyjmenujeme pouze v prvním příkladu.
Variační kvantový řešič vlastních čísel (VQE)
VQE je jedním z nejpoužívanějších variačních kvantových algoritmů a tvoří šablonu, na které mohou stavět další algoritmy.
Krok 1: Mapování klasických vstupů na kvantový problém
Teoretické uspořádání
Uspořádání VQE je jednoduché:
- Příprava referenčních operátorů
- Začínáme ze stavu a přecházíme do referenčního stavu
- Aplikace variační formy k vytvoření ansatzu
- Přecházíme ze stavu do
- Bootstrap při , pokud máme podobný problém (obvykle zjištěný pomocí klasické simulace nebo vzorkování)
- Každý optimalizátor bude bootstrapován odlišně, což vede k počáteční sadě vektorů parametrů (například z počátečního bodu ).
- Vyhodnocení nákladové funkce pro všechny připravené stavy na kvantovém počítači.
- Použití klasického optimalizátoru k výběru další sady parametrů .
- Opakování procesu, dokud není dosaženo konvergence.
Jedná se o jednoduchou klasickou optimalizační smyčku, ve které vyhodnocujeme nákladovou funkci. Některé optimalizátory mohou vyžadovat více vyhodnocení k výpočtu gradientu, určení další iterace nebo posouzení konvergence.
Zde je příklad pro následující pozorovatelnou:
Implementace
# Added by doQumentation — required packages for this notebook
!pip install -q numpy qiskit qiskit-ibm-runtime scipy
from qiskit import QuantumCircuit
from qiskit.quantum_info import SparsePauliOp
from qiskit.circuit.library import TwoLocal
import numpy as np
theta_list = (2 * np.pi * np.random.rand(1, 8)).tolist()
observable = SparsePauliOp.from_list([("II", 2), ("XX", -2), ("YY", 3), ("ZZ", -3)])
reference_circuit = QuantumCircuit(2)
reference_circuit.x(0)
variational_form = TwoLocal(
2,
rotation_blocks=["rz", "ry"],
entanglement_blocks="cx",
entanglement="linear",
reps=1,
)
ansatz = reference_circuit.compose(variational_form)
ansatz.decompose().draw("mpl")
def cost_func_vqe(parameters, ansatz, hamiltonian, estimator):
"""Return estimate of energy from estimator
Parameters:
params (ndarray): Array of ansatz parameters
ansatz (QuantumCircuit): Parameterized ansatz circuit
hamiltonian (SparsePauliOp): Operator representation of Hamiltonian
estimator (Estimator): Estimator primitive instance
Returns:
float: Energy estimate
"""
estimator_job = estimator.run([(ansatz, hamiltonian, [parameters])])
estimator_result = estimator_job.result()[0]
cost = estimator_result.data.evs[0]
return cost
from qiskit.primitives import StatevectorEstimator
estimator = StatevectorEstimator()
Tuto nákladovou funkci můžeme použít k výpočtu optimálních parametrů
# SciPy minimizer routine
from scipy.optimize import minimize
x0 = np.ones(8)
result = minimize(
cost_func_vqe, x0, args=(ansatz, observable, estimator), method="COBYLA"
)
result
message: Optimization terminated successfully.
success: True
status: 1
fun: -5.999999982445723
x: [ 1.741e+00 9.606e-01 1.571e+00 2.115e-05 1.899e+00
1.243e+00 6.063e-01 6.063e-01]
nfev: 136
maxcv: 0.0
Krok 2: Optimalizace problému pro kvantové spouštění
Vybereme nejméně vytížený backend a naimportujeme potřebné komponenty z qiskit_ibm_runtime.
from qiskit_ibm_runtime import SamplerV2 as Sampler
from qiskit_ibm_runtime import EstimatorV2 as Estimator
from qiskit_ibm_runtime import Session, EstimatorOptions
from qiskit_ibm_runtime import QiskitRuntimeService
service = QiskitRuntimeService()
backend = service.least_busy(operational=True, simulator=False)
print(backend)
<IBMBackend('ibm_brisbane')>
Obvod transpilujeme pomocí přednastaveného pass manageru s úrovní optimalizace 3 a na pozorovatelnou veličinu použijeme odpovídající rozložení.
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
pm = generate_preset_pass_manager(backend=backend, optimization_level=3)
isa_ansatz = pm.run(ansatz)
isa_observable = observable.apply_layout(layout=isa_ansatz.layout)
Krok 3: Spuštění pomocí primitiv Qiskit Runtime
Nyní jsme připraveni spustit náš výpočet na hardwaru IBM Quantum®. Protože minimalizace nákladové funkce je vysoce iterativní, spustíme Runtime session. Díky tomu budeme muset čekat ve frontě pouze jednou. Jakmile úloha začne běžet, každá iterace s aktualizovanými parametry proběhne okamžitě.
x0 = np.ones(8)
estimator_options = EstimatorOptions(resilience_level=1, default_shots=10_000)
with Session(backend=backend) as session:
estimator = Estimator(mode=session, options=estimator_options)
result = minimize(
cost_func_vqe,
x0,
args=(isa_ansatz, isa_observable, estimator),
method="COBYLA",
options={"maxiter": 200, "disp": True},
)
session.close()
print(result)
Krok 4: Zpracování výsledků do klasického formátu
Vidíme, že minimalizační rutina úspěšně skončila, což znamená, že jsme dosáhli výchozí tolerance klasického optimalizátoru COBYLA. Pokud potřebujeme přesnější výsledek, můžeme specifikovat menší toleranci. To může být skutečně nutné, protože výsledek se lišil o několik procent oproti výsledku získanému výše simulátorem.
Získaná hodnota x je aktuální nejlepší odhad parametrů, které minimalizují nákladovou funkci. Pokud iterujeme pro získání vyšší přesnosti, měly by se tyto hodnoty použít místo původně použitého x0 (vektoru jedniček).
Nakonec poznamenejme, že funkce byla v procesu optimalizace vyhodnocena 96krát. To se může lišit od počtu optimalizačních kroků, protože některé optimalizátory vyžadují více vyhodnocení funkce v jednom kroku, například při odhadu gradientu.
VQE hledající podprostory (SSVQE)
SSVQE je varianta VQE, která umožňuje získat prvních vlastních hodnot pozorovatelné veličiny s vlastními hodnotami , kde . Bez újmy na obecnosti předpokládáme, že . SSVQE zavádí novou myšlenku tím, že přidává váhy, které pomáhají upřednostnit optimalizaci členu s největší váhou.
Abychom tento algoritmus implementovali, potřebujeme vzájemně ortogonálních referenčních stavů , což znamená pro . Tyto stavy lze sestrojit pomocí Pauliho operátorů. Nákladová funkce tohoto algoritmu pak je:
kde je libovolné kladné číslo takové, že pokud , pak , a je uživatelem definovaná variační forma.
Algoritmus SSVQE se opírá o skutečnost, že vlastní stavy odpovídající různým vlastním hodnotám jsou vzájemně ortogonální. Konkrétně lze skalární součin a vyjádřit jako:
První rovnost platí, protože je kvantový operátor, a je tedy unitární. Poslední rovnost platí díky ortogonalitě referenčních stavů . Skutečnost, že ortogonalita je zachována při unitárních transformacích, hluboce souvisí s principem zachování informace, jak je vyjádřen v kvantové informační vědě. Z tohoto pohledu neunitární transformace představují procesy, při nichž je informace buď ztracena, nebo vnesena.
Váhy pomáhají zajistit, aby všechny stavy byly vlastními stavy. Pokud se váhy dostatečně liší, bude během optimalizace upřednostněn člen s největší vahou (tj. ) před ostatními. V důsledku toho se výsledný stav stane vlastním stavem odpovídajícím . Protože jsou vzájemně ortogonální, zbývající stavy k němu budou ortogonální, a tedy budou obsaženy v podprostoru odpovídajícím vlastním hodnotám .
Aplikací stejného argumentu na zbývající členy bude další prioritou člen s vahou , takže bude vlastním stavem odpovídajícím a ostatní členy budou obsaženy ve vlastním prostoru .
Induktivním uvažováním odvodíme, že bude přibližným vlastním stavem pro
Teoretické uspořádání
SSVQE lze shrnout následovně:
- Připrav několik referenčních stavů aplikací unitárního operátoru U_R na k různých stavů výpočetní báze
- Tento algoritmus vyžaduje použití vzájemně ortogonálních referenčních stavů , takže pro .
- Aplikuj variační formu na každý referenční stav, což vede k následujícímu ansatzu .
- Použij bootstrap při , pokud je k dispozici podobný problém (obvykle nalezený prostřednictvím klasické simulace nebo vzorkování).
- Vyhodnoť nákladovou funkci pro všechny připravené stavy na kvantovém počítači.
- To lze rozdělit na výpočet střední hodnoty pro pozorovatelnou veličinu a vynásobení tohoto výsledku hodnotou .
- Poté nákladová funkce vrátí součet všech vážených středních hodnot.
- Použij klasický optimalizátor k určení další sady parametrů .
- Opakuj výše uvedené kroky, dokud nebude dosaženo konvergence.
V testu budeš rekonstruovat nákladovou funkci SSVQE, ale máme následující úryvek, který ti pomůže s řešením:
import numpy as np
def cost_func_ssvqe(
params, initialized_anastz_list, weights, ansatz, hamiltonian, estimator
):
# """Return estimate of energy from estimator
# Parameters:
# params (ndarray): Array of ansatz parameters
# initialized_anastz_list (list QuantumCircuit): Array of initialised ansatz with reference
# weights (list): List of weights
# ansatz (QuantumCircuit): Parameterized ansatz circuit
# hamiltonian (SparsePauliOp): Operator representation of Hamiltonian
# estimator (Estimator): Estimator primitive instance
# Returns:
# float: Weighted energy estimate
# """
energies = []
# Define SSVQE
weighted_energy_sum = np.dot(energies, weights)
return weighted_energy_sum
Variační kvantová deflace (VQD)
VQD je iterativní metoda, která rozšiřuje VQE tak, aby místo pouze nejnižší vlastní hodnoty získala prvních vlastních hodnot pozorovatelné s vlastními hodnotami , kde . Ve zbytku této sekce budeme bez ztráty obecnosti předpokládat, že . VQD zavádí pojem penalizační ceny, která vede optimalizační proces.
VQD zavádí penalizační člen, označený jako , který vyvažuje příspěvek každého překryvového členu k ceně. Tento penalizační člen slouží k penalizaci optimalizačního procesu, pokud není dosaženo ortogonality. Tuto podmínku zavádíme proto, že vlastní stavy pozorovatelné, nebo hermitovského operátoru, odpovídající různým vlastním hodnotám jsou vždy vzájemně ortogonální, nebo je lze takovými učinit v případě degenerace či opakovaných vlastních hodnot. Vynucením ortogonality s vlastním stavem odpovídajícím tedy efektivně optimalizujeme nad podprostorem, který odpovídá zbývajícím vlastním hodnotám . Zde je nejnižší vlastní hodnotou ze zbývajících vlastních hodnot, a proto lze optimální řešení nového problému získat pomocí variačního teorému.
Obecná myšlenka VQD spočívá v tom, že se běžně použije VQE k získání nejnižší vlastní hodnoty spolu s odpovídajícím (přibližným) vlastním stavem pro nějaký optimální vektor parametrů . Poté, abychom získali další vlastní hodnotu , místo minimalizace nákladové funkce optimalizujeme:
Kladná hodnota by ideálně měla být větší než .
Tím se zavádí nová nákladová funkce, kterou lze chápat jako omezenou úlohu, v níž minimalizujeme s podmínkou, že stav musí být ortogonální k dříve získanému , přičemž funguje jako penalizační člen, pokud podmínka není splněna.
Alternativně lze tuto novou úlohu interpretovat jako spuštění VQE nad novou pozorovatelnou:
Za předpokladu, že řešením nové úlohy je , střední hodnota (nikoli ) by měla být . Pro získání třetí vlastní hodnoty je nákladovou funkcí, kterou je třeba optimalizovat:
kde je kladná konstanta dostatečně velká na to, aby vynutila ortogonalitu řešícího stavu vůči i . To penalizuje stavy v prohledávaném prostoru, které tento požadavek nesplňují, čímž se prohledávaný prostor efektivně zužuje. Optimálním řešením nové úlohy by tedy měl být vlastní stav odpovídající .
Stejně jako v předchozím případě lze i tuto novou úlohu interpretovat jako VQE s pozorovatelnou:
Pokud je řešením této nové úlohy , střední hodnota (nikoli ) by měla být . Analogicky pro získání -té vlastní hodnoty bys minimalizoval nákladovou funkci:
Připomeňme, že jsme definovali tak, že . Tato úloha je ekvivalentní minimalizaci s podmínkou, že stav musí být ortogonální k , čímž se prohledávaný prostor omezuje na podprostor odpovídající vlastním hodnotám .
Tato úloha je ekvivalentní VQE s pozorovatelnou:
Jak je z postupu patrné, pro získání -té vlastní hodnoty potřebuješ (přibližné) vlastní stavy předchozích vlastních hodnot, takže VQE bys musel spustit celkem -krát. Nákladová funkce VQD tedy vypadá následovně:
Teoretické uspořádání
Uspořádání VQD lze shrnout takto:
- Připrav referenční operátor
- Aplikuj variační formu na referenční stav, čímž vytvoříš následující ansatze
- Pokud máme podobnou úlohu (typicky nalezenou pomocí klasické simulace nebo vzorkování), proveď bootstrap při .
- Vyhodnoť nákladovou funkci , která zahrnuje výpočet excitovaných stavů a pole hodnot definujících překryvovou penalizaci pro každý překryvový člen.
- Vypočítej střední hodnotu pozorovatelné pro každé
- Vypočítej penalizaci .
- Nákladová funkce by pak měla vrátit součet těchto dvou členů
- Pomocí klasického optimalizátoru zvol další sadu parametrů .
- Tento proces opakuj, dokud nedojde ke konvergenci.
Implementace
Pro tuto implementaci vytvoříme funkci pro penalizaci překryvu. Tato penalizace bude použita v nákladové funkci v každé iteraci. Tento proces se zopakuje pro každý excitovaný stav.
from qiskit.circuit.library import TwoLocal
ansatz = TwoLocal(2, rotation_blocks=["ry", "rz"], entanglement_blocks="cz", reps=1)
ansatz.decompose().draw("mpl")
Nejprve si připravíme funkci, která spočítá fidelitu stavu – procentuální překryv mezi dvěma stavy, který použijeme jako penalizaci pro VQD:
import numpy as np
def calculate_overlaps(ansatz, prev_circuits, parameters, sampler):
def create_fidelity_circuit(circuit_1, circuit_2):
"""
Constructs the list of fidelity circuits to be evaluated.
These circuits represent the state overlap between pairs of input circuits,
and their construction depends on the fidelity method implementations.
"""
if len(circuit_1.clbits) > 0:
circuit_1.remove_final_measurements()
if len(circuit_2.clbits) > 0:
circuit_2.remove_final_measurements()
circuit = circuit_1.compose(circuit_2.inverse())
circuit.measure_all()
return circuit
overlaps = []
for prev_circuit in prev_circuits:
fidelity_circuit = create_fidelity_circuit(ansatz, prev_circuit)
sampler_job = sampler.run([(fidelity_circuit, parameters)])
meas_data = sampler_job.result()[0].data.meas
counts_0 = meas_data.get_int_counts().get(0, 0)
shots = meas_data.num_shots
overlap = counts_0 / shots
overlaps.append(overlap)
return np.array(overlaps)
Je čas napsat nákladovou funkci pro VQD. Stejně jako dříve, když jsme počítali pouze základní stav, určíme stav s nejnižší energií pomocí primitivy Estimator. Nyní však, jak bylo popsáno výše, přidáme penalizační člen, který zajistí ortogonalitu stavů s vyšší energií. To znamená, že pro každý nový excitovaný stav je přidána penalizace za jakýkoli překryv mezi aktuálním variačním stavem a již nalezenými vlastními stavy s nižší energií.
def cost_func_vqd(
parameters, ansatz, prev_states, step, betas, estimator, sampler, hamiltonian
):
estimator_job = estimator.run([(ansatz, hamiltonian, [parameters])])
total_cost = 0
if step > 1:
overlaps = calculate_overlaps(ansatz, prev_states, parameters, sampler)
total_cost = np.sum(
[np.real(betas[state] * overlap) for state, overlap in enumerate(overlaps)]
)
estimator_result = estimator_job.result()[0]
value = estimator_result.data.evs[0] + total_cost
return value
Všimni si zejména, že nákladová funkce výše odkazuje na funkci calculate_overlaps, která ve skutečnosti vytváří nový kvantový obvod. Pokud chceme spouštět na reálném hardwaru, musí být i tento nový obvod transpilován, nejlépe optimálním způsobem, aby mohl běžet na námi zvoleném backendu. Všimni si, že transpilace byla zabudována do funkcí calculate_overlaps nebo cost_func_vqd. Klidně si zkus sám upravit kód tak, abys do něj zabudoval tuto dodatečnou (a podmíněnou) transpilaci – ale také to za tebe bude uděláno v další lekci.
V této lekci spustíme algoritmus VQD s využitím Statevector Sampleru a Statevector Estimatoru:
from qiskit.primitives import StatevectorEstimator as Estimator
sampler = Sampler()
estimator = Estimator()
Zavedeme pozorovatelnou, kterou budeme odhadovat. V další lekci k tomu přidáme nějaký fyzikální kontext, například excitovaný stav molekuly. Může být užitečné si tuto pozorovatelnou představit jako Hamiltonián systému, který může mít excitované stavy, i když tato pozorovatelná nebyla zvolena tak, aby odpovídala nějaké konkrétní molekule nebo atomu.
from qiskit.quantum_info import SparsePauliOp
observable = SparsePauliOp.from_list([("II", 2), ("XX", -2), ("YY", 3), ("ZZ", -3)])
Zde nastavíme celkový počet stavů, které chceme spočítat (základní stav a excitované stavy, k), a penalizace (betas) za překryv mezi stavovými vektory, které by měly být ortogonální. Důsledky volby příliš vysokých nebo příliš nízkých hodnot bet budou trochu prozkoumány v další lekci. Prozatím jednoduše použijeme hodnoty uvedené níže. Začneme tím, že jako počáteční parametry použijeme samé nuly. Ve vlastních výpočtech možná budeš chtít použít chytřejší počáteční parametry založené na znalosti systému nebo na předchozích výpočtech.
k = 3
betas = [33, 33, 33]
x0 = np.zeros(8)
Nyní můžeme spustit výpočet:
from scipy.optimize import minimize
prev_states = []
prev_opt_parameters = []
eigenvalues = []
for step in range(1, k + 1):
if step > 1:
prev_states.append(ansatz.assign_parameters(prev_opt_parameters))
result = minimize(
cost_func_vqd,
x0,
args=(ansatz, prev_states, step, betas, estimator, sampler, observable),
method="COBYLA",
options={
"maxiter": 200,
},
)
print(result)
prev_opt_parameters = result.x
eigenvalues.append(result.fun)
message: Optimization terminated successfully.
success: True
status: 1
fun: -5.999999979545955
x: [-5.150e-01 -5.452e-02 -1.571e+00 -2.853e-05 2.671e-01
-2.672e-01 -8.509e-01 -8.510e-01]
nfev: 131
maxcv: 0.0
message: Optimization terminated successfully.
success: True
status: 1
fun: 4.024550284767612
x: [-3.745e-01 1.041e+00 8.637e-01 1.202e+00 -8.847e-02
1.181e-02 7.611e-01 -3.006e-01]
nfev: 110
maxcv: 0.0
message: Optimization terminated successfully.
success: True
status: 1
fun: 5.608925562838559
x: [-2.670e-01 1.280e+00 1.070e+00 -8.031e-01 -1.524e-01
-6.956e-02 7.018e-01 1.514e+00]
nfev: 90
maxcv: 0.0
Hodnoty, které jsme získali z nákladové funkce, jsou přibližně -6,00, 4,02 a 5,61. Důležité na těchto výsledcích je, že hodnoty funkce rostou. Pokud bychom získali první excitovaný stav s nižší energií, než byl náš prvotní neomezený výpočet základního stavu, naznačovalo by to chybu někde v našem kódu.
Hodnoty x jsou parametry, které daly stavový vektor odpovídající každé z těchto cen (energií).
Na závěr podotýkáme, že všechny tři minimalizace konvergovaly v rámci výchozí tolerance klasického optimalizátoru (zde COBYLA). Vyžadovaly 131, 110 a 90 vyhodnocení funkce.
Kvantová vzorkovací regrese (QSR)
Jedním z hlavních problémů VQE je potřeba vícenásobných volání kvantového počítače k získání parametrů pro každý krok, například , a tak dále. To je obzvláště problematické, když je přístup ke kvantovým zařízením ve frontě. Zatímco Session lze použít ke seskupení více iterativních volání, alternativním přístupem je použití vzorkování. Využitím více klasických zdrojů můžeme dokončit celý optimalizační proces v jediném volání. Zde přichází na řadu kvantová vzorkovací regrese. Jelikož je přístup ke kvantovým počítačům stále komoditou s nízkou nabídkou a vysokou poptávkou, považujeme tento kompromis za možný a výhodný pro mnoho současných studií. Tento přístup využívá všechny dostupné klasické schopnosti a přitom zachycuje mnoho vnitřních mechanismů a vlastních vlastností kvantových výpočtů, které se v simulaci neobjevují.
Myšlenka za QSR spočívá v tom, že nákladovou funkci lze vyjádřit jako Fourierovu řadu následujícím způsobem:
V závislosti na periodicitě a šířce pásma původní funkce může být množina konečná nebo nekonečná. Pro účely této diskuse budeme předpokládat, že je nekonečná. Dalším krokem je několikanásobné vzorkování nákladové funkce , abychom získali Fourierovy koeficienty . Konkrétně, jelikož máme neznámých, budeme muset nákladovou funkci vzorkovat -krát.
Pokud pak vzorkujeme nákladovou funkci pro hodnot parametrů , můžeme získat následující soustavu:
kterou přepíšeme jako
V praxi tato soustava obvykle není konzistentní, protože hodnoty nákladové funkce nejsou přesné. Proto je obvykle dobrým nápadem je normalizovat vynásobením zleva , což vede k:
Tato nová soustava je vždy konzistentní a jejím řešením je řešení původní úlohy metodou nejmenších čtverců. Pokud máme parametrů místo jednoho a každý parametr má své vlastní pro , pak je celkový počet požadovaných vzorků:
kde . Navíc nastavení jako laditelného parametru (místo jeho odvozování) otevírá nové možnosti, jako například:
- Nadvzorkování pro zlepšení přesnosti.
- Podvzorkování pro zvýšení výkonu snížením režie doby běhu nebo odstraněním lokálních minim.
Teoretické uspořádání
Uspořádání QSR lze shrnout následovně:
- Připrav referenční operátory .
- Přejdeme ze stavu do referenčního stavu
- Aplikuj variační formu k vytvoření ansatzu .
- Urči šířku pásma spojenou s každým parametrem v ansatzu. Horní mez postačuje.
- Proveď bootstrap při , pokud máme podobný problém (typicky nalezený klasickou simulací nebo vzorkováním).
- Vzorkuj nákladovou funkci alespoň -krát.
- Rozhodni, zda nadvzorkovat/podvzorkovat pro vyvážení rychlosti vs. přesnosti úpravou .
- Spočítej Fourierovy koeficienty ze vzorků (tj. vyřeš normalizovanou lineární soustavu rovnic).
- Vyřeš globální minimum výsledné regresní funkce na klasickém stroji.
Shrnutí
V této lekci ses seznámil/a s vícero dostupnými variačními instancemi:
- Obecné uspořádání
- Zavedení vah a penalizací pro úpravu nákladové funkce
- Prozkoumání podvzorkování vs. nadvzorkování pro kompromis mezi rychlostí a přesností
Tyto myšlenky lze upravit tak, aby vytvořily vlastní variační algoritmus, který odpovídá tvému problému. Povzbuzujeme tě, abys své výsledky sdílel/a s komunitou. Další lekce prozkoumá, jak použít variační algoritmus k řešení aplikace.