หมุนโหมดโบซอน
ModeRotation ผสมโหมดโบซอนด้วยเมทริกซ์ยูนิทารี เหมือนที่เครือข่ายบีมสปลิตเตอร์และตัวเลื่อนเฟสทำ เรียกว่า passive เพราะมันรักษาจำนวนโบซอนทั้งหมด: มันย้ายโบซอนระหว่างโหมดและไม่เคยสร้างหรือทำลายโบซอน (การบีบและการเลื่อนตำแหน่งคือการดำเนินการ active และการหมุนโหมดแสดงมันไม่ได้) มันแมปตัวดำเนินการสร้างเป็น
ซึ่งบนปริภูมิ Fock เต็มคือยูนิทารีหลายวัตถุ
นี่คือเกตที่อยู่เบื้องหลังอินเทอร์เฟอโรมิเตอร์ทัศนศาสตร์เชิงเส้น: ส่งสถานะ Fock เข้าไป หมุนโหมด แล้วอ่านว่าโบซอนกระจายใหม่อย่างไร เหมือนเกตทุกตัวของ BosonicCircuit (ดู ทำงานกับ Circuit แบบโบซอน) มันไม่มีการตัดทอนและไม่มีการเข้ารหัส ทั้งสองเข้ามาตอน transpile และคู่มือนี้แสดงว่าทำไมการตัดทอนจึงต้องระวังเป็นพิเศษสำหรับการหมุน
สร้างการหมุน
ตัวสร้างรับเมทริกซ์ยูนิทารี เกตกระทำกับ โหมด คลาสไม่ตรวจว่าเมทริกซ์เป็นยูนิทารี จึงเป็นหน้าที่ของคุณ
>>> import numpy as np
>>> from qiskit_bosons.circuit.library import ModeRotation
>>>
>>> unitary = np.array([[1, -1], [1, 1]]) / np.sqrt(2)
>>> rotation = ModeRotation(unitary)
>>> rotation.num_modes # the gate acts on two modes
2
การหมุนที่พบบ่อยมีตัวสร้างเฉพาะ ModeRotation.beamsplitter() รับมุมผสม และเฟส แบบไม่บังคับ และ ให้บีมสปลิตเตอร์สมดุล 50:50 ModeRotation.phase_shift() รับหนึ่งเฟสต่อโหมด
>>> beamsplitter = ModeRotation.beamsplitter(np.pi / 4)
>>> np.round(beamsplitter.rotation_unitary.real, 4)
array([[ 0.7071, -0.7071],
[ 0.7071, 0.7071]])
>>> bool(np.allclose(beamsplitter.rotation_unitary, unitary))
True
>>> shift = ModeRotation.phase_shift([0.0, np.pi / 2])
>>> np.round(np.diag(shift.rotation_unitary), 4)
array([1.+0.j, 0.+1.j])
คุณยังเริ่มจากตัวกำเนิดแอนติ-Hermitian แล้วให้เกตทำเอกซ์โพเนนเชียลมัน ซึ่งให้การหมุน ได้ ตัวกำเนิดกู้คืนได้ด้วย generator()
>>> generator = np.array([[0, -0.3], [0.3, 0]], dtype=complex)
>>> small = ModeRotation.from_generator(generator)
>>> np.round(small.rotation_unitary.real, 4)
array([[ 0.9553, -0.2955],
[ 0.2955, 0.9553]])
>>> bool(np.allclose(small.generator(), generator))
True
สะพานสู่พีชคณิตตัวดำเนินการคือ to_operator() ซึ่งคืนตัวกำเนิด Hermitian ในรูป BosonOperator ดังนั้นเกตคือ สำหรับบีมสปลิตเตอร์ 50:50 นั่นคือคู่เทอมการกระโดดที่มีสัมประสิทธิ์จินตภาพตรงข้ามกัน ตัวดำเนินการเป็น Hermitian ซึ่งเป็นสิ่งที่เกต Evolution ต้องการ และมันรักษาจำนวนอนุภาค
>>> operator = beamsplitter.to_operator()
>>> # The stored order of the terms is not specified, so sort them before printing.
>>> sorted((term[0], round(term[1].imag, 4)) for term in operator.iter_terms())
[([(True, 0), (False, 1)], -0.7854), ([(True, 1), (False, 0)], 0.7854)]
>>> operator.is_hermitian()
True
>>> operator.conserves_particle_number()
True
แต่ละเทอมอ่านเป็น (is_creation, mode) จากซ้ายไปขวา ดังนั้นเทอมที่สองคือ
กระทำกับสถานะ Fock
การหมุนย้ายโบซอนระหว่างโหมด ผลของมันจึงเห็นง่ายที่สุดบนสถานะ Fock ปรากฏการณ์ Hong-Ou-Mandel เป็นตัวอย่างมาตรฐาน ส่งโบซอนหนึ่งตัวเข้าแต่ละโหมดอินพุตของบีมสปลิตเตอร์สมดุล สองเส้นทางที่ทิ้งโบซอนไว้ในโหมดเอาต์พุตต่างกันแทรกสอดแบบหักล้างกัน ดังนั้นสถานะ หายไปและโบซอนออกไปด้วยกันเสมอ ใน หรือ แต่ละแบบด้วยความน่าจะเป็นครึ่งหนึ่ง
สร้าง Circuit บนโหมดโบซอนและลดระดับด้วย preset pass manager LevelSpec กำหนดการตัดทอนและการเข้ารหัส ณ จุดนี้ ค่าตัดขาด max_occupation=2 เก็บระดับ 0, 1 และ 2 ของทุกโหมด
>>> from qiskit_bosons.circuit import BosonicCircuit
>>> from qiskit_bosons.circuit.library import InitializeModes
>>> from qiskit_bosons.mappers import LevelEncoding, LevelSpec
>>> from qiskit_bosons.transpiler.presets import generate_preset_occupation_pass_manager
>>>
>>> circuit = BosonicCircuit(2)
>>> circuit.append(InitializeModes([1, 1]), circuit.modes)
>>> circuit.append(ModeRotation.beamsplitter(np.pi / 4), circuit.modes)
ไปป์ไลน์คืน Circuit Qubit ธรรมดา อ่านความน่าจะเป็นของผลลัพธ์จากสถานะสุดท้าย ในที่นี้ด้วย Statevector ของ Qiskit ซึ่งแม่นยำที่ขนาดนี้ ตัวจำลองใดๆ ที่คืนแอมพลิจูดหรือจำนวนนับก็ใช้ได้ สถานะ Fock คือสถานะฐานการคำนวณเดียว ซึ่งดัชนีของมันกำหนดโดยรูปแบบบิตของ LevelEncoding.level_to_bits() โดย Qubit ของโหมดที่สองต่อจากโหมดแรก
>>> from qiskit.quantum_info import Statevector
>>> from qiskit.transpiler import PassManager
>>>
>>> def fock_probabilities(circuit, max_occupation):
... """Maps each pair of occupations to the probability of that outcome."""
... encoding = LevelEncoding.UNARY_UNPROJECTED
... spec = LevelSpec(max_occupation, encoding)
... pass_manager = generate_preset_occupation_pass_manager(spec)
... pass_manager.qubit = PassManager() # keep the evolutions as the pipeline emitted them
... qubit_circuit = pass_manager.run(circuit)
... state = Statevector(qubit_circuit)
... width = encoding.num_qubits(max_occupation + 1)
... probabilities = {}
... for n0 in range(max_occupation + 1):
... for n1 in range(max_occupation + 1):
... index = encoding.level_to_bits(n0) | (encoding.level_to_bits(n1) << width)
... probability = round(float(abs(state.data[index]) ** 2), 4)
... if probability > 0:
... probabilities[(n0, n1)] = probability
... return probabilities
>>>
>>> fock_probabilities(circuit, max_occupation=2)
{(0, 2): 0.5, (2, 0): 0.5}
ผลลัพธ์ มีความน่าจะเป็นเป็นศูนย์ และผลลัพธ์แบบจับกลุ่มสองแบบแบ่งความน่าจะเป็นเท่าๆ กัน
เลือกค่าตัดขาด
ผลลัพธ์ก่อนหน้าต้องใช้ max_occupation=2 รัน Circuit เดิมด้วยค่าตัดขาด 1 ซึ่งพอสำหรับเก็บสถานะอินพุต แต่ไม่พอสำหรับสถานะที่การหมุนสร้างจากมัน ซึ่งมีน้ำหนักบน และ :
>>> fock_probabilities(circuit, max_occupation=1)
{(1, 1): 1.0}
Circuit ทำงานโดยไม่มีข้อผิดพลาดใดๆ และคำตอบผิด: สถานะยังอยู่ที่ ด้วยความน่าจะเป็นหนึ่ง การหมุนผสมเซกเตอร์การครอบครอง ภายใน โหมด โบซอนสองตัวไปอยู่ในโหมดเอาต์พุตเดียวกันได้ ดังนั้นสถานะที่แน่นอนมีน้ำหนักบนการครอบครอง 2 ในโหมดที่เริ่มด้วยโบซอนหนึ่งตัว เมื่อค่าตัดขาดเป็น 1 ระดับนั้นไม่มีอยู่ และตัวกำเนิดที่ถูกตัดทอนย้ายสถานะไปที่นั่นไม่ได้
ขอบเขตของการครอบครองของสถานะเริ่มต้นจึงไม่เพียงพอ ค่าตัดขาดต้องครอบคลุมจำนวนโบซอนทั้งหมดที่เข้าถึง โหมดใดโหมดหนึ่ง ของการหมุนได้ สำหรับโบซอนรวม ตัว ตัวเลือกที่ปลอดภัยคือค่าตัดขาด
เหตุผลเดียวกันใช้กับทุกโหมดแยกกัน ดังนั้นการผสมค่าตัดขาดต่างกันต่อโหมด (ดู อธิบายการตัดทอนด้วย LevelSpec) ทำให้แมปการหมุนผิดได้ง่าย โหมดที่รับโบซอนได้ต้องมีค่าตัดขาดที่ครอบคลุมสิ่งที่มันรับได้ ไม่ใช่แค่สิ่งที่มันมีตอนเริ่ม การตรวจ LevelSpec.truncation() เปรียบเทียบ spec กับขอบเขตล่างของการครอบครองของตัวดำเนินการ มันจับกรณีข้างต้นไม่ได้ แต่ละเทอมของตัวกำเนิดบีมสปลิตเตอร์มีตัวดำเนินการสร้างเพียงตัวเดียว ขอบเขตจึงเป็นหนึ่ง และการตรวจไม่รายงานว่าขาดเมื่อค่าตัดขาดเป็น 1 ทั้งที่ผลลัพธ์ตรงนั้นผิด:
>>> LevelSpec(1, "unary_unprojected").truncation(beamsplitter.to_operator())
[0, 0]
ข้อควรระวังเพิ่มเติมเกิดจากวิธีที่ Transpiler สร้างการหมุน (ส่วนถัดไป) มันใช้การหมุนเป็นลำดับของตัวประกอบสองโหมด และเอกสารปลั๊กอินเตือนว่าตัวประกอบเดียวอาจทำให้เกิดระดับที่การหมุนทั้งหมดไม่เคยไปถึง ลำดับที่ถูกตัดทอนกับวิวัฒนาการเดี่ยวที่ถูกตัดทอนจึงเป็นการประมาณที่ต่างกันของยูนิทารีเดียวกัน การค้นหาการหมุนสามโหมดแบบสุ่ม 1,080 แบบ โดยมีโบซอนหนึ่งตัวในแต่ละสองโหมดอินพุตและไม่มีการครอบครองซ้อนในเอาต์พุต ไม่พบกรณีเช่นนั้น ดังนั้นให้ถือเป็นข้อควรระวังและไม่ใช่ความล้มเหลวที่พบบ่อย ให้ทำให้ค่าตัดขาดลู่เข้าเหมือน Circuit อื่นๆ โดยเพิ่มมันจนผลลัพธ์หยุดเปลี่ยน
Transpiler สังเคราะห์การหมุนอย่างไร
สเตจ optimization ของไปป์ไลน์ preset แยก ModeRotation ในปริภูมิโบซอนก่อน ด้วยพาส B2BSynthesis และเมธอด BosonicClementsDecomposition (ลงทะเบียนเป็น ModeRotation.Clements) นั่นเป็นทางเลือกของ preset ไม่ใช่ข้อกำหนดของเฟรมเวิร์ก: ส่วนหลังแสดงการลดระดับเกตเดียวกันโดยไม่ใช้การแยกส่วนใดๆ การแยกส่วนไม่ได้ทำเอกซ์โพเนนเชียลตัวกำเนิดเต็มในขั้นเดียว มันแยกตัวประกอบยูนิทารีเป็นการหมุนระหว่างโหมด ข้างเคียง ไม่เกิน ครั้ง บวกเฟสโหมดเดี่ยว ตัว ตามเมชสี่เหลี่ยมของ Clements et al. [1] แต่ละตัวประกอบกลายเป็นเกต Evolution อะตอมิก ซึ่งสเตจ boson-to-qubit แมปเหมือนตัวอื่นๆ
การแยกตัวประกอบนั้นทำโดย givens_decomposition() ซึ่งการแยกส่วนประกอบเรียกใช้ การแมปจากยูนิทารีไปเป็นการหมุนหลายวัตถุเป็นโฮโมมอร์ฟิซึมของกรุ๊ป ดังนั้นผลคูณของการหมุนแต่ละตัวจึงเท่ากับการหมุนเต็มบนปริภูมิที่ยังไม่ถูกตัดทอนพอดี เมชจึงไม่เพิ่มความคลาดเคลื่อนของตัวเอง ข้อดีของมันคือความเป็นท้องถิ่น แต่ละ evolution ที่ปล่อยออกมาเกี่ยวข้องกับเฉพาะพจน์ Pauli ของสองโหมดที่มันผสมกัน (หรือของหนึ่งโหมดสำหรับเฟส) แทนที่จะเป็นพจน์ข้ามทุกโหมด
ทรานspile การหมุนสามโหมดแบบสุ่ม แล้วตรวจดูว่าไปป์ไลน์ปล่อยอะไรออกมา ยูนิทารีขนาด ทั่วไปต้องใช้การหมุนสูงสุดสามครั้ง บวกเฟสสามตัว และแต่ละปัจจัยเป็น PauliEvolutionGate ของมันเอง ทั้งหมดถูกประกาศบนรีจิสเตอร์ทั้งชุด แต่ตัวดำเนินการข้างในทำงานกับคิวบิตเพียงไม่กี่ตัว
>>> from qiskit.quantum_info import random_unitary
>>>
>>> random = np.asarray(random_unitary(3, seed=7).data)
>>> spec = LevelSpec(2, "unary_unprojected")
>>> rotation_circuit = BosonicCircuit(3)
>>> rotation_circuit.append(InitializeModes([1, 0, 0]), rotation_circuit.modes)
>>> rotation_circuit.append(ModeRotation(random), rotation_circuit.modes)
>>> pass_manager = generate_preset_occupation_pass_manager(spec)
>>> pass_manager.qubit = PassManager()
>>> qubit_circuit = pass_manager.run(rotation_circuit)
>>> dict(qubit_circuit.count_ops())
{'PauliEvolution': 6, 'x': 3}
>>> for instruction in qubit_circuit.data:
... if instruction.operation.name == "PauliEvolution":
... terms = instruction.operation.operator.to_sparse_list()
... print(len(terms), sorted({qubit for _, indices, _ in terms for qubit in indices}))
64 [0, 1, 2, 3, 4, 5]
64 [3, 4, 5, 6, 7, 8]
64 [0, 1, 2, 3, 4, 5]
2 [1, 2]
2 [4, 5]
2 [7, 8]
เมื่อมีสามระดับต่อโหมด แต่ละโหมดจะใช้สามคิวบิต บีมสปลิตเตอร์ระหว่างสองโหมดจะแตะหกคิวบิตของโหมดเหล่านั้น และเฟสจะแตะคิวบิตของหนึ่งโหมด
ปัจจัยจะแม่นยำก็ต่อเมื่อแต่ละ evolution ถูกสังเคราะห์ได้ดีเท่านั้น พจน์ของตัวกำเนิดบีมสปลิตเตอร์ไม่สลับที่กัน ดังนั้นสูตรผลคูณแบบขั้นเดียวตามค่าเริ่มต้นจึงทิ้งความคลาดเคลื่อนไว้ในทุกปัจจัย ส่งสูตรผลคูณที่มีจำนวนการทำซ้ำมากขึ้นให้ปลั๊กอิน Evolution ที่แมปปัจจัยเหล่านั้น เพื่อลดความคลาดเคลื่อน
>>> from qiskit.passmanager import MultiStagePassManager
>>> from qiskit.synthesis import LieTrotter
>>> from qiskit_bosons.circuit.library.synthesis import BosonicClementsDecomposition
>>> from qiskit_bosons.mappers.library import occupation_encoding
>>> from qiskit_bosons.transpiler import (
... BosonicCircuitToDAG,
... BosonicPassManager,
... QuantumDAGToCircuit,
... )
>>> from qiskit_bosons.transpiler.passes import (
... B2BSynthesis,
... B2QSynthesis,
... MapperFnEvolutionSynthesis,
... OccupationB2QLayout,
... OccupationInitializeModesSynthesis,
... )
>>>
>>> def with_product_formula(product_formula):
... synthesis = B2QSynthesis()
... synthesis.methods["Evolution"] = MapperFnEvolutionSynthesis(
... lambda operator, num_qubits: occupation_encoding(operator, spec, num_qubits),
... product_formula,
... )
... synthesis.methods["InitializeModes"] = OccupationInitializeModesSynthesis(spec)
... return MultiStagePassManager(
... input=BosonicCircuitToDAG(),
... optimization=BosonicPassManager(
... B2BSynthesis({"ModeRotation": BosonicClementsDecomposition()})
... ),
... layout=OccupationB2QLayout(spec),
... synthesis=synthesis,
... output=QuantumDAGToCircuit(),
... )
โบซอนหนึ่งตัวที่เข้าโหมด 0 จะออกที่โหมด ด้วยความน่าจะเป็น เปรียบเทียบ Circuit ที่สังเคราะห์แล้วกับผลลัพธ์ที่แน่นอนนั้นสำหรับจำนวนการทำซ้ำที่เพิ่มขึ้น ให้แยกส่วน Circuit ก่อน เพื่อให้สูตรผลคูณถูกนำไปใช้ หากไม่ทำ ตัวจำลองที่แม่นยำอย่าง Statevector จะใช้เกตเป็นเอกซ์โพเนนเชียลเต็มรูปแบบและซ่อนความคลาดเคลื่อนไว้
>>> encoding = LevelEncoding.UNARY_UNPROJECTED
>>> width = encoding.num_qubits(3)
>>> exact = np.abs(random[:, 0]) ** 2
>>>
>>> def one_boson_probabilities(qubit_circuit):
... """Probability that the single boson sits in mode j, for each j."""
... state = Statevector(qubit_circuit)
... probabilities = []
... for j in range(3):
... occupations = [1 if mode == j else 0 for mode in range(3)]
... index = sum(
... encoding.level_to_bits(n) << (width * mode) for mode, n in enumerate(occupations)
... )
... probabilities.append(abs(state.data[index]) ** 2)
... return np.array(probabilities)
>>>
>>> for repetitions in (1, 4, 16):
... lowered = with_product_formula(LieTrotter(reps=repetitions)).run(rotation_circuit)
... error = np.max(np.abs(one_boson_probabilities(lowered.decompose(reps=2)) - exact))
... print(f"{repetitions:2d} repetitions: error {error:.1e}")
1 repetitions: error 1.2e-03
4 repetitions: error 7.6e-05
16 repetitions: error 4.7e-06
ความคลาดเคลื่อนลดลงประมาณสี่เท่าทุกครั้งที่จำนวนการทำซ้ำเพิ่มขึ้นสี่เท่า ตามที่คาดไว้สำหรับสูตรอันดับหนึ่ง ต้นทุนเพิ่มขึ้นตามสัดส่วน เพราะทุกปัจจัยถูกทำซ้ำ
หมุนโดยไม่แยกส่วนประกอบ
การแยกส่วนประกอบในปริภูมิโบซอนเป็นวิธีหนึ่งในการลดระดับการหมุน (ไลบรารีไม่มีวิธีอื่นรวมอยู่ แต่ขั้นตอนโบซอนเป็นคิวบิตไม่ขึ้นกับมัน) B2QSynthesis ค้นหาปลั๊กอินจากชื่อของเกต และ ModeRotation ที่ถูกปล่อยไว้ทั้งก้อนจะมาถึงมันเหมือนเกตอื่น ๆ ปลั๊กอินสำหรับมันแมปตัวกำเนิดของ ModeRotation.to_operator() ในครั้งเดียว และปล่อย PauliEvolutionGate หนึ่งตัวที่เวลาหนึ่งหน่วย แค่นี้เอง:
>>> from qiskit.circuit.library import PauliEvolutionGate
>>> from qiskit_bosons.transpiler.passes.utils import map_node_single_register
>>>
>>> class DirectModeRotationSynthesis:
... """Maps the generator of the whole rotation, with no bosonic decomposition."""
...
... def __init__(self, mapper_fn):
... self.mapper_fn = mapper_fn
...
... def run(self, in_node, out_dag, *, b2q_layout):
... modes, qreg = map_node_single_register(in_node, b2q_layout)
... generator = in_node.op.to_operator().relabel_modes(list(modes))
... observable = self.mapper_fn(generator, len(qreg))
... out_dag.apply_operation_back(PauliEvolutionGate(observable, time=1.0), tuple(qreg))
ลงทะเบียนมันกับชื่อของเกตในพาสแมเนเจอร์ที่ไม่แยกส่วนประกอบมัน:
>>> def mapper_fn(operator, num_qubits):
... return occupation_encoding(operator, spec, num_qubits)
>>>
>>> synthesis = B2QSynthesis()
>>> synthesis.methods["Evolution"] = MapperFnEvolutionSynthesis(mapper_fn)
>>> synthesis.methods["InitializeModes"] = OccupationInitializeModesSynthesis(spec)
>>> synthesis.methods["ModeRotation"] = DirectModeRotationSynthesis(mapper_fn)
>>> direct = MultiStagePassManager(
... input=BosonicCircuitToDAG(),
... layout=OccupationB2QLayout(spec),
... synthesis=synthesis,
... output=QuantumDAGToCircuit(),
... )
>>> direct_circuit = direct.run(rotation_circuit)
>>> dict(direct_circuit.count_ops())
{'x': 3, 'PauliEvolution': 1}
>>> terms = direct_circuit.data[-1].operation.operator.to_sparse_list()
>>> len(terms), sorted({qubit for _, indices, _ in terms for qubit in indices})
(198, [0, 1, 2, 3, 4, 5, 6, 7, 8])
ตอนนี้การหมุนเป็น evolution เดียวบนทั้งเก้าคิวบิต ขณะที่เมชมีหกตัว ซึ่งไม่มีตัวใดครอบคลุมเกินหกคิวบิต ทั้งสองไม่ใช่การประมาณเดียวกัน การตัดทอนถูกใช้ครั้งเดียวกับตัวกำเนิดของการหมุนทั้งหมดที่นี่ และใช้กับแต่ละปัจจัยแยกกันในเมช (จึงมีคำเตือนเรื่องค่าตัดด้านบน) เส้นทางตรงเท่ากับ occupation_encoding() ของตัวดำเนินการที่ค่าตัดใดก็ได้ จึงเป็นเกณฑ์อ้างอิงสำหรับเปรียบเทียบเมช สิ่งที่ต้องจ่ายคือความเป็นท้องถิ่น พจน์ของมันครอบคลุมทุกโหมด และสูตรผลคูณมีพจน์ให้แบ่งย่อยมากกว่า
ไม่มีแบบใดดีกว่าโดยทั่วไป ซึ่งเป็นเหตุผลที่ควรเปิดตัวเลือกไว้ เขียนปลั๊กอินแบบนี้เพื่อเปรียบเทียบการลดระดับ เพื่อใช้การหมุนเฉพาะตามการเข้ารหัส หรือเพื่อส่งเกตให้สิ่งที่เข้าใจมันโดยกำเนิด
ขั้นตอนถัดไป
-
ทรานสไพล์ Circuit โบซอน อธิบายขั้นตอนที่พาสแมเนเจอร์พรีเซ็ตรัน และวิธีแทนที่ปลั๊กอินที่จัดการเกต
-
แมปการแทนตัวดำเนินการ ครอบคลุมการตัดทอน การเข้ารหัส และ
LevelSpecที่กำหนดเลย์เอาต์ของทุกโหมด -
ทำงานกับ Circuit โบซอน แสดงวิธีประกอบและรวม Circuit โบซอน
-
เอกสาร API แสดงรายการ
qiskit_bosons.circuit.libraryสำหรับเกตqiskit_bosons.linalgสำหรับการแยกส่วนประกอบ และqiskit_bosons.transpiler.passesสำหรับปลั๊กอินการสังเคราะห์
[1]
W. R. Clements, P. C. Humphreys, B. J. Metcalf, W. S. Kolthammer, and I. A. Walmsley, Optimal design for universal multiport interferometers, Optica 3, 1460 (2016), arXiv:1603.08788.