From 0cf446bdd476936ba6078cc517382052cf1957b3 Mon Sep 17 00:00:00 2001 From: Simon Cross Date: Fri, 4 Aug 2023 14:28:08 +0000 Subject: [PATCH] Release 2.12.0 --- docs/README.md | 97 +++ docs/images/Logo_LabOneQ.png | Bin 50598 -> 0 bytes docs/images/flowchart_QCCS.png | Bin 127840 -> 0 bytes .../00_reference/00_session_reference.ipynb | 20 +- .../01_calibration_reference.ipynb | 4 +- .../02_experiment_definition_reference.ipynb | 34 +- .../00_reference/03_section_tutorial.ipynb | 2 +- .../05_pulse_inspector_plotter.ipynb | 6 +- examples/00_reference/06_user_functions.ipynb | 74 +- .../07_waveform_replacement.ipynb | 22 +- .../00_reference/09_output_simulator.ipynb | 32 +- .../00_reference/10_database_interface.ipynb | 42 +- .../00_readout_raw_data.ipynb | 56 +- ..._cw_resonator_spec_shfsg_shfqa_shfqc.ipynb | 26 +- .../01_cw_resonator_spec_uhfqa_hdawg.ipynb | 114 ++-- ...sed_resonator_spec_shfsg_shfqa_shfqc.ipynb | 28 +- ...ator_spec_vs_power_shfsg_shfqa_shfqc.ipynb | 30 +- .../04_propagation_delay.ipynb | 28 +- .../05_qubit_spectroscopy.ipynb | 30 +- .../06_amplitude_rabi.ipynb | 32 +- .../01_qubit_characterization/07_ramsey.ipynb | 34 +- ..._transition_spectroscopy_shfsg_shfqc.ipynb | 40 +- .../09_e-f_gate_tuneup_shfsg_shfqc.ipynb | 34 +- .../10_CR_gate_tuneup_shfsg_shfqc.ipynb | 34 +- ..._experiments_shfsg_shfqa_shfqc_hdawg.ipynb | 222 +++--- ...active_qubit_reset_shfsg_shfqa_shfqc.ipynb | 161 +++-- .../01_randomized_benchmarking.ipynb | 26 +- .../00_qubit_tuneup_shfsg_shfqa_shfqc.ipynb | 203 +++--- .../01_single_qubit_tuneup_uhfqa_hdawg.ipynb | 143 ++-- ...02_two_qubit_experiments_uhfqa_hdawg.ipynb | 40 +- .../03_qubit_tuneup_shfqc_ext_dc_source.ipynb | 50 +- ...rallel_qubit_tuneup_shfqc_hdawg_pqsc.ipynb | 72 +- .../00_user_function_sweeps.ipynb | 22 +- .../04_spin_qubits/01_QCoDeS_sweeps.ipynb | 72 +- .../02_MFLI_cw_acquisition.ipynb | 67 +- .../03_UHFLI_pulsed_acquisition.ipynb | 61 +- .../04_HDAWG_pulse_sequences.ipynb | 205 ++++-- .../00_shfsg_basic_experiments.ipynb | 50 +- examples/06_qasm/01_VQE_Qiskit.ipynb | 2 +- examples/06_qasm/02_Two_Qubit_RB_Qiskit.ipynb | 20 +- .../03_Two_Qubit_RB_pyGSTi_OpenQASM.ipynb | 18 +- laboneq/VERSION.txt | 2 +- laboneq/_utils.py | 28 +- laboneq/compiler/code_generator/__init__.py | 6 +- .../compiler/code_generator/analyze_events.py | 21 +- .../code_generator/analyze_playback.py | 4 +- .../compiler/code_generator/code_generator.py | 210 +++--- .../code_generator/measurement_calculator.py | 37 +- .../code_generator/sampled_event_handler.py | 28 +- .../code_generator/seq_c_generator.py | 43 +- laboneq/compiler/code_generator/signatures.py | 2 +- laboneq/compiler/common/awg_signal_type.py | 1 + laboneq/compiler/common/device_type.py | 6 + .../compiler/experiment_access/dsl_loader.py | 644 ++++++++---------- .../experiment_access/experiment_dao.py | 344 ++++------ .../experiment_info_loader.py | 61 ++ .../compiler/experiment_access/json_dumper.py | 170 +++-- .../compiler/experiment_access/json_loader.py | 241 ++++--- .../compiler/experiment_access/loader_base.py | 148 ++-- laboneq/compiler/experiment_access/marker.py | 15 - .../compiler/experiment_access/pulse_def.py | 37 - .../experiment_access/section_info.py | 31 - .../experiment_access/section_signal_pulse.py | 32 - .../compiler/experiment_access/signal_info.py | 19 - laboneq/compiler/qccs-schema_2_5_0.json | 32 +- .../scheduler/acquire_group_schedule.py | 172 +++++ laboneq/compiler/scheduler/match_schedule.py | 2 +- .../compiler/scheduler/oscillator_schedule.py | 1 + laboneq/compiler/scheduler/pulse_phase.py | 82 ++- laboneq/compiler/scheduler/pulse_schedule.py | 23 +- .../scheduler/sampling_rate_tracker.py | 4 +- laboneq/compiler/scheduler/scheduler.py | 198 ++++-- laboneq/compiler/workflow/compiler.py | 222 +++--- .../compiler/workflow/neartime_execution.py | 5 +- .../workflow/precompensation_helpers.py | 123 ++-- .../compiler/workflow/realtime_compiler.py | 11 +- laboneq/compiler/workflow/recipe_generator.py | 378 +++++----- laboneq/compiler/workflow/rt_linker.py | 32 +- .../example_helpers/generate_descriptor.py | 40 +- .../contrib/example_helpers/qubit_helper.py | 100 +-- laboneq/controller/communication.py | 8 +- laboneq/controller/controller.py | 56 +- laboneq/controller/devices/device_hdawg.py | 9 +- laboneq/controller/devices/device_pqsc.py | 33 +- .../controller/devices/device_setup_dao.py | 2 +- laboneq/controller/devices/device_shfqa.py | 222 +++++- laboneq/controller/devices/device_shfsg.py | 8 +- laboneq/controller/devices/device_uhfqa.py | 2 +- laboneq/controller/devices/device_zi.py | 13 +- laboneq/controller/near_time_runner.py | 2 +- laboneq/controller/recipe_processor.py | 57 +- laboneq/controller/versioning.py | 25 +- laboneq/core/path.py | 2 - laboneq/core/types/compiled_experiment.py | 8 +- laboneq/core/types/enums/acquisition_type.py | 9 +- laboneq/core/utilities/string_sanitize.py | 32 + laboneq/core/utilities/validate_path.py | 46 ++ laboneq/data/calibration/__init__.py | 23 +- .../__init__.py => compilation_job.py} | 65 +- .../__init__.py => execution_payload.py} | 13 +- .../data/experiment_description/__init__.py | 43 +- laboneq/data/experiment_schedule/__init__.py | 39 -- laboneq/data/path.py | 5 + laboneq/data/recipe.py | 23 +- laboneq/data/scheduled_experiment.py | 13 +- laboneq/data/setup_description/__init__.py | 82 ++- .../data/setup_description/setup_helper.py | 4 - laboneq/data/utils/__init__.py | 2 + laboneq/data/utils/calibration_helper.py | 31 + laboneq/dsl/_inspect.py | 2 +- laboneq/dsl/calibration/calibration.py | 28 +- laboneq/dsl/calibration/calibration_item.py | 2 +- laboneq/dsl/calibration/precompensation.py | 5 +- laboneq/dsl/calibration/signal_calibration.py | 7 +- laboneq/dsl/device/_device_setup_generator.py | 31 - laboneq/dsl/device/device_setup.py | 25 +- laboneq/dsl/device/device_setup_helper.py | 9 +- laboneq/dsl/device/instruments/shfqa.py | 10 - laboneq/dsl/device/instruments/uhfqa.py | 21 - laboneq/dsl/experiment/__init__.py | 2 +- laboneq/dsl/experiment/acquire.py | 18 +- laboneq/dsl/experiment/builtins.py | 16 +- laboneq/dsl/experiment/experiment.py | 327 +++++---- laboneq/dsl/experiment/pulse_library.py | 68 +- laboneq/dsl/experiment/section.py | 55 +- laboneq/dsl/experiment/section_context.py | 2 +- .../dsl/experiment/{set.py => set_node.py} | 14 +- laboneq/dsl/experiment/utils.py | 13 - laboneq/dsl/quantum/quantum_element.py | 12 +- laboneq/dsl/quantum/quantum_operation.py | 2 +- laboneq/dsl/quantum/transmon.py | 6 +- laboneq/dsl/result/results.py | 6 +- laboneq/dsl/session.py | 86 +-- laboneq/executor/execution_from_experiment.py | 42 +- laboneq/executor/executor.py | 56 +- .../compilation_service_legacy.py | 176 +---- .../device_setup_generator.py | 439 +----------- .../experiment_workflow.py | 6 +- .../legacy_adapters/calibration_converter.py | 196 ++++++ .../legacy_adapters/converters_calibration.py | 215 ------ .../converters_experiment_description.py | 6 +- .../converters_setup_description.py | 401 ----------- .../converters_target_setup.py | 4 +- .../legacy_adapters/device_setup_converter.py | 405 ++++++++++- .../post_process_setup_description.py | 158 ----- .../implementation/legacy_adapters/utils.py | 38 ++ .../convert_from_legacy_json_recipe.py | 322 --------- .../payload_builder/payload_builder.py | 167 +---- .../payload_builder/target_setup_generator.py | 166 +++++ .../payload_builder/payload_builder_api.py | 2 - laboneq/openqasm3/gate_store.py | 2 +- laboneq/openqasm3/openqasm3_importer.py | 12 +- laboneq/openqasm3/reset_gate_factory.py | 2 +- .../pulse_sheet_viewer/pulse_sheet_viewer.py | 38 +- laboneq/simple.py | 2 + laboneq/simulator/output_simulator.py | 85 ++- laboneq/simulator/seqc_parser.py | 162 +++-- laboneq/simulator/wave_scroller.py | 4 +- pyproject.toml | 6 +- 159 files changed, 5229 insertions(+), 5335 deletions(-) create mode 100644 docs/README.md delete mode 100644 docs/images/Logo_LabOneQ.png delete mode 100644 docs/images/flowchart_QCCS.png create mode 100644 laboneq/compiler/experiment_access/experiment_info_loader.py delete mode 100644 laboneq/compiler/experiment_access/marker.py delete mode 100644 laboneq/compiler/experiment_access/pulse_def.py delete mode 100644 laboneq/compiler/experiment_access/section_info.py delete mode 100644 laboneq/compiler/experiment_access/section_signal_pulse.py delete mode 100644 laboneq/compiler/experiment_access/signal_info.py create mode 100644 laboneq/compiler/scheduler/acquire_group_schedule.py create mode 100644 laboneq/core/utilities/string_sanitize.py create mode 100644 laboneq/core/utilities/validate_path.py rename laboneq/data/{compilation_job/__init__.py => compilation_job.py} (81%) rename laboneq/data/{execution_payload/__init__.py => execution_payload.py} (89%) delete mode 100644 laboneq/data/experiment_schedule/__init__.py create mode 100644 laboneq/data/path.py create mode 100644 laboneq/data/utils/__init__.py create mode 100644 laboneq/data/utils/calibration_helper.py rename laboneq/dsl/experiment/{set.py => set_node.py} (67%) delete mode 100644 laboneq/dsl/experiment/utils.py create mode 100644 laboneq/implementation/legacy_adapters/calibration_converter.py delete mode 100644 laboneq/implementation/legacy_adapters/converters_calibration.py delete mode 100644 laboneq/implementation/legacy_adapters/converters_setup_description.py delete mode 100644 laboneq/implementation/legacy_adapters/post_process_setup_description.py create mode 100644 laboneq/implementation/legacy_adapters/utils.py delete mode 100644 laboneq/implementation/payload_builder/convert_from_legacy_json_recipe.py create mode 100644 laboneq/implementation/payload_builder/target_setup_generator.py diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..b200f09 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,97 @@ +# LabOne Q documentation + +This subdirectory contains the documentation for LabOneQ. + +The last released version can be found here: https://docs.zhinst.com/labone_q_user_manual + +## Development + +The Documentation is generated with [MkDocs](https://www.mkdocs.org/) and +[Material for MkDocs](https://squidfunk.github.io/mkdocs-material/) theme +to generate the documentation. + +### Build the documentation locally + +MkDocs is build with python, making it easy to install all requirement with `pip`. +The documentation therefore has its own `requirements.txt` + +Inside the `docs` directory, execute: + +``` +pip install requirements.txt +``` + +After this you should be able to build the documentation with: + +``` +mkdocs build +``` + +MkDocs also comes with a handy live preview that automatically adapts to your +local changes. + +``` +mkdocs serve +``` + +### Structure +The documentation is structured in four parts: + +* `content/`: contains all markdown source files +* `mkdocs.yml`: Configuration file for MKDocs. __INCLUDING__ the navigation content +* `overrides`: Theme customization +* `gen_file/`: Python script that are used in combination with the + [gen-files](https://oprypin.github.io/mkdocs-gen-files/) plugin + +### Automatic package reference documentation + +The package documentation is currently divided into two parts. + +[mkdocstrings](https://mkdocstrings.github.io/) is used to automatically +generate the documentation for each element. + +[gen-files](https://oprypin.github.io/mkdocs-gen-files/) is used to automatically +create the markdown files with the mkdocstrings identifiers. +`docs/gen-files/reference_doc.py` is used as a prebuild script for gen-files. + +To keep the live preview responsive mkdocstrings is disabled by default. +The environment variable MKDOCSTRINGS, which takes a boolean value, can +be used to enable the plugin. + +Linux/Mac + +``` +(export MKDOCSTRINGS=true; mkdocs serve) +``` + +Windows + +``` +set "MKDOCSTRINGS=true" && mkdocs serve +``` + +### Jupyter notebook integration + +[mkdocs-jupyter](https://github.com/danielfrg/mkdocs-jupyter) is used to +include jupyter notebooks into the documentation. + +In order to avoid copying all notebooks into the content directory, a simple +prebuild script `docs/gen-files/external_link.py` is used. It allows having +placeholders for additional files that automatically get added during the +build step without the need of a symbolic link. + +### Additional notes + +Currently, we use +[`use_directory_urls= True`](https://www.mkdocs.org/user-guide/configuration/#use_directory_urls)m +which is the default. This means that if one views the static build sources in +a browser, links will not work properly. +To view the static build sources locally, one needs to spin up a web server. +Thankfully, this is quite easy in python: + +In the terminal, simply execute +``` +python -m http.server -d path/to/static/sources +``` + + \ No newline at end of file diff --git a/docs/images/Logo_LabOneQ.png b/docs/images/Logo_LabOneQ.png deleted file mode 100644 index 951369e1cdbb21f7f7e3326a43766fe0a454beb7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 50598 zcmeFZWmH>h*ER~27AS=xZ7ELi;>E2i zSkM#p-rdjpooDa&{61rRKNw>X?v*v~Y4+uA`K^ybDcClAq1#K!VO6)K9E;lM=+;x?vl2@%4%$9^%Fln# z91%o^tE9=UBvt1TByFu><0lvKhJhzf^PxC0{>bw|^u6fl$B(6c5^7wE5K(1DTL6eG za6^dF8NPHbb|i*|uHJt!n3oc29!hxY*0uJ#)U+qJ-iAor`qnPemoIDm%JMNdn*1^D zKIV>~k~oDDs|Q+h!q)nVHma%^Z0KuT49qY)3~cljCi;g0{eyvlm5ufLliQH&TfeU{ zHE#ZRly(|{fgy#VC?l=yi@6QK$uO1o+LNE2Pxr1bwTH7ca2Pnd_jC*Hrekx+(l0tx_`R;uP>D{!*&(L`4Io+-yU%)zQlW};6aBGT10%h z*hPSt=CjYy<=njWTZsQ@Elp_*V2rjWU(7##=^yU37>1z+kj!Yh`#OWMp3^#A^xAJXVtCwd0feevH3>Mw2odVx_`OEze9S#fii$(F>g-ItX#DtC)tx_?;UX z2?hYYtz!}v$Kkn6su_dr+)Oft;H|>OpL1yUYR;3^Czcv#pD-AQ;j|Tx12yk zg(>d*rCLQt(J3(3W_ez^Hs6bN+17byf6om1@Khu0FdOK+uvl7H!r-T<6=5LQGFFyh z=!}TcqImnyF^Pe_jkoC807xm}(_ypB6?VuXl}C05-eM+HLpX{mP>(qH5Liicdp`Dh zQJJd6Ot{YLku3blVP)Z8W&HRY9enUwr8!Vw{QBO;+$}_3${8Qwnug-96i+Rd!!+tZ za9jc7816d0S}{*4nJ!%y=nz^Mc#}0qR2C^6rU!jv`yP0nz*N~CNLAh!NYnXa$;VPX zyTwq+)Ja2?ls8$)+>s2hLRBuJuVoLeEfp(El>li6OJEzsy?A4QdOE`&M*y7@wrA!} z77vV2l@FHu2+EU`e>=X#p`iF9(EAS3kumI^+M0s5@)cV?@zADKkPKBNY>)OH6WsLb zb;?ZAcvt&s=PLxVoMn2*u2yM~sZcjfS-q5hTt;f`PD1XVPpHK9lopumTCTO4ysx&_ z>P<;@q9S!IckC0VK(eh-+vxC}YI~e>wQCTbGp}wk)_TjyE2Y=^-o|! z!$go*Iy+t^ewSj*Uv3(&W@fcr1)qGL)`!_p=%9TNLJhIJuP9Mffe8k1Vit86_>^y5 zq=5m}?c*q&TdauQ07Kyf-~>rI(oPrxA!g3S7`F*>HLL_$0J6v_YA?0a??m;zu3jFl z8iDytG^BeVQqtLzb-}f3N$FV5W(iN0{r~o4A_J^V{dsR!eTkRgxOUvN&rj$|8#WS- zMrP@k!CR{0inI(zUHTr;98PQk%GN2y9$q8gU!IcxnQMr|uqc{Z^{58GA`Lt1My1HI z9jsk#jJiQJx7S;M(!niK)k&|lUhx*~H3P7vR|RB9T#Q(wNOIYe@S#f`K5b$V0;l0P ztwqR(MJ^YP%5a3igIj}vc^sTP}T zZItjZI!#Q-3qS>x5okHGf(@H{44iQxYYB`IUuBaJXMCkYOWm4{FGMJ@NO5Ot^LsoG z<5W+XTWF80bp-mJ{V9M{8s0kouW92;1Rf}E<^k!(XTpfFW>c(}hq=VB*;n&Vb7UVP z0{WiioDKgKb98Lj%{V>ytH#PsAC;3gd=FNlWtLI@d>C6 z&NTya7YKaXwUNOt5F2)rH$oYShV#HMCH~sob$`Ta=qpCP0Jk3i6{o16yEJwK!kLvM z5seQ$`W4bvN=ZKx{*k^~V>?p^YhmH0ie&PWdk?QQqp4Ns`J6J70koGe#eM&`iNba{UZ@dSdW15VqkM|<>B7#%brE?EAIdz~;rZ)>WWW{k8uVpC!^ z?U#9I&3W2T!GwCnOJ5J`J7d!&Q&#Fzm7no11ebghZ*Aj#bfoIXWNJ_QESLcV12iP2 zotO@rJrYc0Z0oDL|M$uNPE86jBng@eh>Q4c5bbj;>PA5$M&I<>Z}2$@umHN&81dck zt&FtgUyw}?Yt!)x#NoV2m|i!%xK8XbgPcQf!43AeT-s9#jeHA zpGLkR3gd@&$zWcTbB{t~qdJl2VS`!o&67s$c|R@=fJd`MIpfdA?))WAXe!b0P8)^U z{&2&gXnNcpTi~Id0SJeY?42`CP!HUzfCL`1{A}OlkGSZ4Bh|89fXW=KjoYUCxc``I zPSGoc<5x-SkrywlT@_&ve@>Ijhq`tXRfjU`b^wj&#oLc$S~f^H#Wo;?iHaBmSMwAu?8m_Q9pbpyi)~g+)pV}puAC(%9ayYCHQ|s*9&6(4nXHcx;V0= zn_lpF=oRQ2IOIW0?+?Hbld8CbntBYwO_`CG(y7&QKGY#O;jCTpFg;VMl2LPPjYPHN z6f{6FR9yQ}<2kuN29zb2(d}twI_+I!_h!a4+@;M$ETNN;xymVJfHY*$dmnJ z?VpF9#!dPeyeZixb}1CJTLJ`ZQcgW3E^FJ!`(k{uOkI@jf&nZwJsaX6IhCOKh36LS z$7bnpH(Oz5fr2zG;^CoBK2d_hd@VSG^4u=`1BU$tq8SVS2_kKx;i6KPG7+n>{b)Gp zFkr~P;jkCli;0w6F+d*LWD4xqxijBnr4=IIrF;qGkUSxH-h9O7YZ)FDDB_q}a1@)LOXK}zGEkWhJ1A0q_^ht!sc{5vYb zzlKfyRGc8qN7yF18UsU%Su^eWm2J{ABW&0nQ1@Wr*{3~;bFy5bq)iZl znfG}vNb)+LtunP69+Vf2$v!AkC3zXR+i-~(Vvmz>$T^~VZ}N%oCde*GU+vh$IO{gS zSi!p&6`bidnMuai-wraeJ<6b!kXy?(uck?FJM572WudwxkPSNZ2uQpwQ8~cURaD;G z%0?rMtMA9|^hk)Zov1 zA@EUwv{9fmbs6Cbu)fO%WXlIFf6B{wV3({O2>kT4KCxgp<(v?h?6N~6aK4i3Gz-5$ ztB3Df#ry@@GMFuA-OYM0=96kwU*4{KYa$jBbz`qyKFlQJA3)u;+<;VQE&g|yo6^e& zu$G|}V(dXWeEdBa)vKHKV$EPY$9V??)Xk(l?iD|eht?hh$GkYU^J~izpRsPJBLrVR zm4Lhk1}W6$w;Vm&K&hVN4C&Ddr$!`(D<%~eFknJ(C5}eh5R=?s>k56%Reg%Wm%r=K z&~;MZ06-S%?3)wGmz6z&>O9vEA}?2%p|)H@0f$V&>vJ}nWfLj(mlJ_vLJWqh4iy@)qOg*8 z>(xN-Pte*LWmbhzpmznw*ojf;K5m-`5=33j0ewwSd=Qc!#KK!A&J*a>TRgxBKQ1<`PXlxgc>_vx1?0GNJ7@L-hK z=;7~t`3mjNRdlMcmzELt4$K8=yiUGqu?>nK2l%+ZqWrqq9sy&mYnA!38q0aXJ6~Zt zl>F+-nQ`?kXR4;X6zL^DQC%&shbm2`%qk2`mTx_u{G%=p6(mGptO!QOssli+7#-$rNd{aRLmFcSV=2qXN7_~rwDEJ!l#EYCzU4GF?dAuO>SEM6 z-_pHwOgcOL*!n;oq@23^rsl@M86hEZ=RrqTZ7@U}{Gb_vCKFCVPi}m(dq&*_;wLwB{!^Gg8T|gqiAtP@YQj?{krk>b5lvKF*uZ zAeYMsADJ}~Q?asx5g}m+QaUiwp(OEZ%ATD-^Blq3P8qD`t^<6ZZJ`b?gWM*S98aD< zktQ0+5$R!3$NyTZ+@0lCJo<`PGi)`O_T0ppe~G58)Sb>HsbRLRrdymI1e2T@eyD_1 z1Yo?o%WX~hE6ZqxiXA7===x5P&`sv$!K*upN$Y6N6-Y4Rj^0A!p$u@kZq%~i?nsMQ z{Px}sU=l1;StdY%D&uV|e{LCSIcjYDd}$Pw>yT!-n(c!bItqVp(zzyaY>|Z`8~Jtg zbd$!Fl$Fa0Yq{ppetp>VVCeiaB-CT`MLR{;e`0+Rk7|>WFRLRkON151CvE8A1p-bs zc_A!7KT?Uml(%v1jIRS!dCE0r92!onJ*#HkS$}M3bD9dcNyQ11jj8r--hW~MB1x=f z|0`ro_!Jj@tMp7p8a+~ZZ5)jM9iCAK)MfZs`C1nOcA%Y}NbeIM=Ksb?VtbZyaPJTg z3|OwaHQ-J4)3bqz>=&@HVvqm&QX9~jFIOoXcs#j7rkbNFuG}Cds7-Zw(}19z9UP>E zIQI=32R6@1(^lx<*=l2Zcgx(9=ZhhXJZ*9A*`^ddyY`0XxGL!G6ZR3*tD2a+UcEz^ zOrx$HZeK$UQ3yg3#DoHZVw5P0asGBfhHQ}D=9qSm$46aV{%@@>a}p~^pArNHsnpoX zz*m<7+E>PuJkHVRjRoeN_URdM%)MTdM}cdwKK%VcXWM52)~#H3-X&SwU<=3uNq7SD>(%{RxEQ(wdjKnnJS}o z=PYaoSMZL;M9Tf9(ul0}dcxt}uDz|S6ms7%C8BZe_SaMi)U?%@lBfl#)Sd93i`@Ae zE9IzU`@3=^bUn8XL*AzUwErcFLZ#d>PBp9E5V_S7m3aF@9J@5SUz1!JNzK$en*a?sf${ofTn{1GE8dld zb%wK@Es6YmjKG70vmR8zQkyL&FSSYq@*G)6?MPew2PlKH#n$bzrFn6?7mwN_Xj3u4bmW_w^8t zit=f&q0a&yL@1$y3dIOg1n-;pW1(^;Tt1$z?H45)PUgDBoHypppaigF6TX!|@pG&${nsk4B%6P`e3uB$K- zPcVmKTzor3l*sJCO@E&5KDL}p*x|rQ-JLWm@2(79q?jIm#Gq!)Dy{oQMU?D>QkEn? zY{KLG4H(c!LnU~9*eQ+dsK}%~e;3VS*{3z#SM=@zf}S*J+!;^czTJ46YlW?&;`Hvw zL6c0K6IXIke15I61uE>*>)-(<3PZ^bKl8WnC-=wVI;OazFxm=OvUN%)rSVa;GmINi z%qtqI*^wm7??1X}iT@5l5u(%XG{IU$Z}4qv7ek$N#YaWc-BVk5wg-}4L<`Imi3q7IT(K=W8CDB-R#MbZl|Tzd;QuiD0ISP=F%`D3efJ~BApaS zHx%*&(<+!WU=$c26MMY_w)VY|((tw<{%;%GP4aPbL`w+Q3Hh^HM3QxlZ)RimSEUp< z-)0$iA-NuzP_m80p0P1E=*cYvIw>OTKouX4LZ?!lhfbG_%gKBA4CyRKowOhPc`|>r z#p>{_7!YrLo(c(MTRqlL%qauOO|4)b!jB7*>cP}O*S^=7ik&;{1gAkjLEVk!EhY_y zKcjNwiE9*NlfO8f>UI2AazrDyOaHc2oEm9Vjh7bI=lYa19ovaKVh3SDf!{LHwe zI!AHYl)jc$P-3R%KzN+cP<=kJ@b%0=_M?=%&~da>fh0%g>U##Z zK|47HHr`$u*tloqFJ4-I0`6_zF|U$p5aCZfi}dZ)wT=H?8ScC^P$KX=IkB<(J)}&4Nc<7qsOIrV!dVom+P>_gPD!~KTa2S4%%0WqyUT7nlbJ)$ zRtP)>?P7MJ*h7H_u=_L*@I*78<&O1?-NqtPAj2FYJ%%m0?$Wv~{4BeQQC}Fdcx^um zDcIP2Kl!>;F-9`bGRi=I4_f76HhU4p$fgp>jZ=tpSZ1utqVcbWT{PlG%fq$hmgmq% zcE@!-3i08gKmocvf)2h1k+F;8g;8VSJZB+{w9(Dm-Ino>pKUJh9=$`6{f=BkI#=}B zXk&?CFemVMNo$Eibvd>ryC?Kce85ukBahb@8jk2zFt6mo<4d!&Y;sMS8_ZKgbh^>a zRl6F}a_{Xf;wn*CnUa09vG)0Tdl|>b?RhgQSQ=m(s`^}7?-}>%PlJ2p=ekYA7g=G1 z9|9hpF9~d%j1QCyE<^Cj0bM+F2#bDD`(685pToiyMSU_ceuAI=NJT z+|wK1xf@H2%mvK8r4R_;U5;-oEsg{VDj$y06vVE@nNCtEaJ}EF*y1@~1+(owGWRi< z9ii~Z28;!uGIdGEGn^?le!XuT6rgRd8(D7<;}F7n=>8Jep#Zu#npp$$rRA1BE9zqx z_=54Dv-vAywxM(fm*ER}YNS#xqDDn??LEP+JCS*VP^`6{;{2l+-w9TCGOam9g0d0L z`9G2G>67e}*kZ^Dc@meG002!YP^)&&Ikkgbb82~uaD*koEo~=@+ZX#Jvy1r(0|j8 zAE0xX=}Ca-2;I`4lxor0~`UGEdXI)&u<~KOrGZmOA@>u9HK{wWX zT+HPm_%EUF(YW~|eyIOSX6cEE-uYrz?O+XjgdoGi$+R7&XHd1!;@+TPjY&2NC^>ID zZhind@o*Cj8@t_nRwwyJt$mdQZ5BVrkQSb^G0Su8J;`>b(Aon5ySKLBN%? z>?k_ZrjrnIikNwNg%vZ6C%+Bv&YTUSSV$<(uJGQlOG1bO)w;eiRcU7sboh;MtVIQw z9!z-Nj}=RKiTMYb;1LMT{N#pwTR3MeP+|1iy5hC&^P{fQa=uW$wM4L~NXL9r#&t5F zwEgEPv=%)m2^@WS{m#Qx&=UK6J?_s6n5S`dJB@P4QRv`A2oqu8@~DjS`fAOxgX358R55*Zi3T*47E7qSOn{8L#I-rKgk>Ge#fYHLqw{&A z-L)B=2930?a>Dxnle%;lBMtVSmm}`)?!cj}N%`zyrU5hJwXaMvbFAWawuKs>Pd?0N z(;)k;HVZW=vWC6+ZRXlnY*9zo%8axM_B%d20SX!Skz2EwdbIQN0Dhq8pU(S}$ajCZ z3fQS(BTb64DPD>hkt&Hr+f+7E7e4zTYL2~zP0ae<_YpMjY0AO)mL-CAPtuk#Ov!5d zrW9ZM{&V|`ha7yTtBAg28A;}n$w17g11hPvk&f9SVArH8zV|!jkYgRL@LlSKlTD~y@EhkQ}6JXW_0&hF_o&^5lN=SI-{$^yW>?_WD_3q z#;(9nl_R{}(Z41pOR8pDU!K&O?`3N-@UQib59HJ6*{OH@Z0_Uej9V`^Lq#9YS_S4h&+$p{^#Q$)_GkWSy8SHmSrDZ@8BiW>@ z=uSSF#5*IgDE`}cZ&-dU17Q+$-)F~a#-ATYbsQ~L!9V+SZl0k;@d|4hP;SIk=hJVUY^-K=%zLWo-Ot1$)Lva8 zZXYr2zSnrcik&7~`idGBMVUJyzYtJ2bVwlol7#ue;sbOY^M@s+Y_W|P31vgP z@#6bC+#6`sCvwiiOJKX3Oo6~Kj1FM0{nk%I_Ki1HF z*TK#A&X_r(uim?d0eylOY*+vr6ZTk8Vi)7^$ zkM?rFdKGG7c_jP?ObEZ?{%>)PvAuoni8+XL_;|HFbsaKG2}}~<#mDLh2!cF~F9I0O zj6Yiv=W?tZ0^Vegcp0;nZRY7&(Uq~hlRj}VEz%%!(34-8NXqDM{0qqC`pqDro*F!MTA@(Uk9#yB&T?uK}Klw7s$p4^(B7%Sj3SABPyR?di){mi^X;CPEVsKsM8ic9%})Tta7ZCzXKz>5~MykKE0euQbiZ2 z7fMcb0|Z;iN5=c_6v}@$hkq$8esOBE@g0=N>T9Z5Y*)y%thOaV&yZ4V0o3F^E)nVv zbou>5U!(D!9kc88gb5#lh}8Sb(}y-D#%^iEa?Lw8@ORVtxFwRYKBMX6z3*VHFFWUl z9JzNvTM-wVi_jFxa5>?W2$frpT(BtiFTzOnTkD>7aGTzg8+9Z}Bzs?EcK(v)3F$DS zn~#{1!)$=AQ*V8quO8MUSEbtz@1R#y%nV#Kq?31CC2j&NmE@***%piM<6_V!PIx?S6Ep zTP58{TH{ei{wV);M|2hiRBqM$ z=e+*r_)GdP2P?{#6KfvyH{#Th_&P6Si>%N=sw~vVTOurYd^h{?hpY>AUQgBmY-h!f z+z;0bGN2;f)%`^17RoLa?C!4Xcp%EsO>GmSD_&NLjF`X4%**cTiX)4dKqCGwvxCtH z#a|Kr5t}A(WaoAm>l&Ru!)%RnA}h!E?fQConOLGf;7sT&uJ~pu-%oU9)MWfI7b&+` z@X(ofqr$RF{u^>_?qH;usAB)JP;}5X^Tm|x)#Oe8e&~%~9q`5aw88_pwoamfHY=N} z?^hqkKJ=98@!XI9Fl9m`u)Aete&L0Emm3;?FL#tt?MswuY-|Mu0~SmvPY5YuzJL9# zt4)U=jBX8rl?}aR84ke<4Q*U&aN<{UPBAKt0t6IeD^LwVv(-u{>SS~xJVH?ehp?dxCBTSPL<+0pjxm|F-9Dw2EQ z7o27CNe+oyN8S0yB$R3E>uzDPk*pq{oNv~DrTbMTOpGJHbE~t$Q$dYP&Y#u84~q*~ zR-oo;keAx-y(_=y2@U;ulC|6WbKUHIo3F81b1|3_h|))iO@ENt^5)^LPh&S^ zlyEn&G^rSpManN;k_o0V#V`SjJHMctpdnV&J%1?rX5tY-^2#p8tL09ac?M_8QLmy! z47Ur*qh9+jl#gb(AK5PTK|`xs$FuNnnlRz`p~QHGfv@S&(gnDg=!$TVOq--2XK{<_ zHkkag|z}HiBS?{Ad&byhl zk_+2ARt<6>xspmnPyP&(&ij~NG=#C!$_}3$X^%!cdMROsP?O81we)-HQuXh=D+G>O_q!#0d zG~#qHtQ_6iLya8`$)`6VBCxq6>VFt*R?RJaC#k(gtWmZM5*v$t`~7?cflvUo6$1Lm zc<}|iEvzdg#CfJcKvhbyg2>P>0|$-a-iHhb4LojXXL$Av@Y^GxX7OD$C=D4eArXO%ikjF@YZ;%MM7ry`n*{la!Bb^ zIe zD03Dexz^mRmmEi5iqSN+$fiKMrfs-}Nh`??;6!`Tiv^nYQ;cI<7m z#hsh{%E&d!xr1AL2$GPrTi8M-LW4{4gSNqJ{YQn)@_4o#^~Y0N3#P7-mtW}ui3qUL zUJXz9h22jloB80Re*Z}P{B-A4=|jlZz1cCRgZ=6AdE}O20|~_Z<4Kx!@`F^<4y$C; z`8lC)Wt5y)r40!Z3c-Itw~IYC(%51tyj*0zmqPXgh}uX@d6Kj{vJexiF4+S87!usQjdp6o)B&=r`sq8qH5Skq$peLEgTGl%npF@@IJP(|~fr;H?#t z*{3J2l%u%qtB#iq(kX6Kn>nxnzlAM|XJKct^T;~xGwV-i>T}|GX1l!Y!nodpG&y+t zoTgR=_$ApG86Ze#9Cg8Ujkd%r8)09Hm^>fh>aC?$x;Xg~%18I8p^QZ7&JdHHQ0COp z>S4NNskIh4)F7xjds(XE+ktW?Q-4#P>j#4;Ei2W&r1}%u_S(#{)V+cYCYj@pl*u*t zcDb|;!z&(3RMq=LGJHji|0p2w80T$#NBwgwS`(=#rN<33edRm>jBQ1 z{A2jT!jP?zmu9t%x_k*fe=mwH(A+)lnUdQ6Y75{pxvm&h`Vk_1_Lbk1YFK$ydTUT5 zzn~zxCq3b7>x9$}q-P|mTIdBtgGIUkr)$a^&W6te(9fIwLw?Cd#rA&tv z8f!x;xnp=q{jV27)MFvRm4VI<%~hrNt@^EaMdFeD zpy!TYJ%^}4^6oKNLM#_+EzbzijIi#4*fj8xXQ}B%+maBMtnRnFCU@1&LV6w7@9GK? zzmZ=0?5`Y$8XBN@%Mo95gFZ;7*hh4?ypJfyH)s7^r_KX69!q#BI5fCi&ek`vAnWC% zvz`)r0U~Q1#%Fo8qL+U?uF$_QZM-dcrP%?z=xR?8Rs3rIPRV2aYebNNAdfn8%sMXb^A(AUvYb>1c<<#eG2D=Ma2SULo|bh6x{y zO@1g#G2Np{AR85MS0A=T6s5ISZOKE1n^#{kz2SdSYIDTgr^N5C3emW!-%tAK3nlpF zzZ~XYPbr_?5HBJyM%_Fk7*EHG*6Z}+WE7g$RhB%}pbBoFp5YD_FYzocd=UwiRb1^i zEM%kAn|y4h0zNmhI=F#a3W9gP%Uvabhxa)p^9X&WayB|0cvyF*rM* zJwq}e8R{Le+x+qXu)fP1Qx{od_aHu~{nE{&lMvWUbw&tIJb&QAsA^Zwn?fAk})4=ZGkm^c0YT-%-@ZpjX9o5Ung?h(i)6 zvSbI;HRB=)HxK%yUD#wq1v?}B5q|N(!)IeD3K8RH^yqcqo=JWAMKx+qGRqdm(Q4Az zI-T)@h_ffzX(q=tQ#nT`=0z4K>S>h~k?qGTZ8(FWNE4aW8jIRmYV>=YM(R0>mD*+( zQ=NhSPwI=$+LE1>FY-OMh9+s9k<(aSH$?x)j3{ZkO6+bc|71CV>j2Bf`nOLi355H) zbOuhy4st5hpaW{9ye`RPwyJbtD3)v}!jLIVjZ7oZg~8jYTrPf=rMcm3q`b#PtVJ+4 zV)M1LHp~Y|?j11|!4_9HsazjyoB`E92$lVWJOMMm^;bHmv3=mC?d@STqfH>uB=7xc z_WFf-H|jzSFW%y(prKzS^+~IO-g>K#%Gag(h3{)DUh$HwBdD{HSsWM1mRM|WOwX)= zC9akTto4xaho4|-IQ}~a=)Tnm0ej9vR)tiU;+@pU6Upk)%25NpZW9(_zQ@=#PqION zXiDI!4pP;Whkrkwc^eI!L#;?(0gygSFSbAS+A)rd^#Ql@H?T%30xElPh?$lO3(v!P zhVIMKD^Wjo^AOC69vM^Qud0}LD zy!2^yh0C3D;pi0f(H&Kkpy8t+W01^7>AmEV`2OpB^i^&DWgF0%vu~gVqtVzq%66Z^aGW{y+tI3%5ZO85vA{G5s z6ctUD#X=Vi(I?i&=vNMKZP(rbhOUI9A<`U{bQK{ z%KZw%b2O5^&7}KIf|5_#-!sk#_I>+>nmGZ)?_Nc6v^R_q zWhW{J(eyip>cwBP7!f#*0k1z%iowWwa#)}Vl9S95s=B`_r z6ZO9D+=op=&%I@vPvORt@6M>h4IzxLj?``mYR3JD* zW{(K`iM*B8s@czIrO!FIedgj76_QOg{*e|c6<|$#U0(C+Y(mFN<_iG6kHff8ywg@# zW?Nt<4HHrRu`l8nd+MviNZh>hd20L_$AQp@A{RJwY{nqAS^|Dt+(#XdCik6abvwbA zw<7<(H^q~^QBkJS-ABZQ=wjt&`#YJm9oFjh)KF6Y8dWOCU=N>C6t`PvX@LMjZINpP zcJ-appLEYPQ$Z2+=m;v4?Mps^ zB^gr357V%76I}Z=v1~fiKk|Xwn0*}hT6aC59iaOF#;?c0 z)Q|e?yqIM`^4Urwe~`N$Qnm`jF>f@~%IG|>*-uOa!cZgxENes04E)-a za5vP#V7;@|jSh$Upkh>qQh30@L>nR>_rzri;ua<@R`d5k?`EJ z;?&+OWYW!zC{qyME(W88UQ@P7!JrBI-aAapLZq9v4h&vsTwkX2Vr}D2)?k8XIhaJ+ zbBBD~B)!ggGGubg4c%s<^TlE-+@JKR?>}DP0%{PpUoa*XQM{)pZ)h6d+bv|Q(sFtc zxNnlhL#>tGE;1~Z3=g{M)NT3hAUJvNfNDIizWzNUO@k~M3i(L5w_rP>>gB}5Mp+cv z&L^12HZN+Ew<$S-uRI%v0bLuJb?<PRtsB<9D^!hDVv^G8SBqW zlf}H6ku=}AR^~bO0L<{)Ygd+{VhH;92@b5Cui~79%SXA`* zgACbul+UhEu)C8-9>46jvw`9FP23Bk_MV~7XbaTUN~%qmVA|F-Z<`gU?y=1j_iboz zzCbfpQabc9^Y6$bNv*S^h=SQPeg(T8IV$?~5T-8o;32p7WlkGOUKs-nJ;=2hCNnn0 zGoYmFMc)ieJpqc()J?PZv1$`~*h77k}GJ@IFH zHs`D;w`z%BD?xTq&T~GcQ%eeZ%=LN1(?ap2W0*m-J1jbmrI-!a=bZS?CI;;wtjlHS z*QOo}K&v$sTXU`_u{rEZhMv=_5e07Srr6dI_NivqJjL;oS1o1ya&S780{5%LdwbQh z8Uq&1RN(PjD9W9kINYOFW)6L{VQ4HqrqFGqx>rnnxeN-xp@5D&Ed;8K{j^PYfxk3nLWSNJt*C@K%IEK#l=Ef z;C1R?P<6TNkTwMN$VnJvTW^r8Z&|n)y%qGxKtNyBCvN>b<;pax*pq|5SHD+R%#?Z< zU2$B^Fu*jqBd+Qs$Toe<(?qba26(t;_seJd*6A#lMXwvO*zBJ__bOU_J%sY5+*!HtM5IzCJnyU!zcUb3l zmuIjQ9EiFeJvboK%>&)(xSVUl8SJUZaMI1v+<$%hbVCOVJuI`n+UkqdyQW^bzo{^> zUc{uzWaG9xa=rDYw6wUQNv_xl^ehq8D}2{q8vRO&+i1$(HK#^Me!Q1N{{+JkRh`Pa zq9{xa7t=(ze4F#;N(Lt(_lzuF%-2YZMO7{k)M3>Hm1U(T{}@xb-=}zXFS#+X?|CKT zvhBUEz-)YHzsgadmL%Kz2g!CM&sDij!9p-at8673(2<6LRlUA%Uvd){?>gUjNs~42 zb&}|UmY1}yPxkr^)pk6`*t^yC_S2}Slvtl0FR_o$q78e~w(4EXhB)Q-R&$S?WtdwqrFi@I$JhSL62Y(Q z1r82c?P(0XEX9rJb-V(JM#AY;rx(9@_m+H?Q%)QZRf1R?!cPZrflO>3Z z8$S0N(Lwc7Rx|}49GP!kynhIW;cz+fv@nK(M2{x6*YN{NYn1{nYK0$5XeAAj*-~Eh zKvD2+F63PO7tsu_mR~@8Hr{3Xe3||Y@1NPKx!)eykEI4w7zSzYsxLOGV~k;GMSdr^mXTaxp^bi)vmG$WI> z+Hw4WAtjK9D6_~zE0@yT>mU97gM^BldOjxWZEBL{+f$#eCEs|$GA2i<^)c974>2n(z*#*qW3z$h|Eg$B#ycOIg=eh5+#i!mjn+00OZT{3O zReoUI5nvKHEZb8dj`nje@Z{aC*blLoEuH$*hMNqLZ+?0P<&#Z&rsscq^wmfVNx%!X zYI(>!StB9=!JenPe-}-jtm3{*0FCJcs#fF^V%{iP5HrRM^@h<6PxOt)dtt0fk+#>n zw^rp6(&WCxiV!V$sX>7DCm~IiT0YEoMT?p@@DCUIb?pN~E1oVBD>X^b{ZNw@&44*R z6+rWHEuo!>)g}lRFzT4( zoN5qkN$a?8=%QiXo; z0fj*8j58IwqLkLhoVivDPY#BgqtwfPAv`A#&siwe(Olr+5hD^T5hCFN`7%7Iw>7m^ z>4kEIr?K|X{Hx6}ZSs4Uj$*=A=u)x;?&m6;LeKnMw_c%xUXwDb-CT`zq%pxn_)W*K zZHJ5a4}=k)aqVX##vbPf0FT6Y7=&4q<_uWKVKI9Z(AcDG()xDRyUg-nuZujlxlrSt z%qsw>TtFvh&2fN>YfV7((Cwp#=Ev58D9?E=WW@Zn4s+)?myx#ld`-;+N1D#kYi+u< zt&FSl_;pnu{~|4&+^n~}##ZOBo|&OAF7QKb5b)`mJk0aXuB(J8{mepp2^a6O8NL1- zxE#4K#O}ZgUCVM_Ct35rczc}`WedzX1sSYBeCPNtXMgg*>`m}F;Mg`IE5RPX5BO

A}lw27D~hI%@{0cbu}(G?;%5{dd~`?`5r(Ctx>x_2^~J8@?+ z$HKqeKJbNE(CB;}8)I z{<)p~78gfe^lOp4*Xp~f=eHu2It=M;yq@U$D0Yo#%PB#i(qMbQXzq zsp;o%x0F0Nqg@IQ_HFUUp4?gT6*tY3T|&I`p&`-?u|IRLkWHeju73odxcsRMuqLsF zC;vPbAF}*4KTZHH+73>@)=bB`tMB`2MeJ}@;~JV{<;?v}#!R-D`&&`ez}FXv^i|9? zoDr>^w8HM&v6B7D;QMDVbC`}uH2MY4_~U%${Ci)omaDzBSmLeEGLi*rRY?jn z_!sXf3u3DFJ34T|T+z^LyVZAYpT*(*@(KFQQ(!`GL#pY+H6YOzjvveE^h=zfUBdH@>WHJLQ5aXOw5n(X+IN|Q>jfZyTnk}HK1)L* zs^yEF_IiA`t#e^ye|8?tF}5~-ZF^2_cxNzA#Cag`*J6^IlxkL?6CRr>Si9*NBz#UJ{sQhoMLfq2w4dlsO+dkrDplEHYJ zjfHj#Moh9&^8O#1&cZLM=Y9KvtCX~W(%oIs-6`E6T}v(9Atepc-QBsgA`MH&(o)Nk z(gFg{e!jog^B>GPXU@z$*L_{@NdY;A$M8SEP_Vyg4R2;q@Twd6VtW2MM#fHeLQcbL zViHqiAt5qYp=|q7$LyAp8mWc{@A+~ba#LS~elPra>K5B<^#k;45yKDHpL$2EFDFwV z9${{^zt#z6>-PcbPGP#Y6Qm7_IG#OSnL_6}Qt-QmY+yBMfw1ur?jD$j7FtCAwCIK_ zBPASY^JD~7ouT;=V$W?fQYbe`w7MBG4-hy2>X>*Mk&uew*^%R|`5pR-;G%y$B`@RS zW$_nlZt8xA2Z<*N)d5ZP>a4)R;ydKrs8E094hW5j^ao8BYO!p3__f{B_-d#%?<%Cy z2i_hCLO7H=qRGE(xeA%4T<;(D%Mh&%AH-PRf-c>==5|S~wWf`yXDe+zMa;Yq#;}Tf z*MR=f^nZhaC^WdD!TQCYJsAq)!9BE4<3!M)6H>7v|uxLYMG5l8L2-kckMQNrN&2wdZy;j z|H=hFDxzn&7yF$t6|2@K*e+Ij@lAm*IPfk=hfHwlfx^&x=CzUpNfOKawj#oQkVi%p zyXmT-p3Eh~O2hdLDrH_a0w5E6@uIh9H$${K@lg+7 zpLAG}G{&0H%%SwNe>|}LCQSt|_wy;}>>Fa*F=#st?rRc;S8|_fky$Q+Ad9>X&m4PY zyj59CItAAq4R+ivxEmlnEogOt2#Iks^V-8THU&f=$_diI(y@i*01}0aQG^jzit>W@U#$>J5eVO6E$|HUfU=a^Sv*%F?P5`F_;;){Zlha!7ToQ2gr z+>D6mZeA4m<2RqReA6}WarFFk!{~VpT!<(4$@M2haw4fx>`Nw)rM1H>T!obN9He;7 zyvk{<2PkpqL#?Ntn<*WsKX`T`49^n0sFhb*G_p>o<7GMr($s-T^jafVBeIu2WE(E) zRX21YD*|q&h)ikiT3&bU@KA+rrnsLLh&ct~5LSk5JhQ!ChmeZKA8$b!L=cdaFq)pBReEbx>7g-aiPU!E|_LwT}S=VrSADG@9bM42D z5!8OO-}4X06@FYf*)o=k3HpHDA%cH6zxyJ`0_Pr9hcdqNGs*rK#uE`eDeD%kD8i3z=6Y2z&%;a zE49?x6-*X~Nf*u~;7mUGSkN|Czq z7qaRFJt#18sf2Vx=oI^5Z42*}g>&4)LF=u9lNZCeJMnNK~8>^HtC)?5*q}FP)e<_>=K7%urFb>1C49|R5H%Y9M zr-sf}-`5STa$R@@-5_AxcUp0h;|1(N($P!Xg2P~beeMS93x-y*t9 zH)ntkwt!P^U#bwK`SRbCcqW$UYOzZPHVNc$q5h?894+z6tl7-MCO3YKtjJI}OL=tN z$`?Ob%_JRl6Hd)$QKM7UUC!&yg0L!11!wN^AGXj24{LOdARn64VYYuV1Bd^s4x5owHqTn|`6II;55KYrsQ zw%%WNUuLG=2?%WTzw`B60{YtdpFI>T5nXxgv~XZ&zV3xiB(XU#rdRr!cD4Bj84RaP zDBnfPY&$%?qhgf%-kL@Nt47X6GWv951{t&E^u=iIdYyo})?efX1%aCA;}wnO zk*Tc9o$o@UXt-U@j>)+?>O0hTg!=1O0#v-?(C{a%sC#vG4ZtheN-BZJ7yl+h+dU!s zA{qXavEAULgu5no;cv$I`(dT&eibboqPC~4zHPUkHYkPjnp4yVD(0cEtjS)Gt!BO)tN8Zi?4zESyHTpF{t6+B%dM(~9~6D|?M%s5ek+au4=TB` zHDf$JaW)JIl3Sa8)a%v7l9&0U(~#71umE+5dL*$L?dLO`#j{J+Pfb|$;uX)3L&r6X z2*~kw+H2-zO@0Ww^H0G zmb?kiW@Hh>MGlrvDU)t3cA@k`x1NdFuv_b8^0bBp;1DL>O;oR(L<#H#Mk2d4Pxbe- zzrbDD;l>cy{9^CQYt*q8W1Ze|=t}+lYmkts-o@K-0gIM00Fi_f={gAy&egm?TMt88cXt6^KI;D)O4AUCc!e%Ue=RUt%@=mek7 zQMivKhK#SsZEn)~cb!T;TcZaKBPiRpK0cu%-DK-pVmaWvQ(fdBQl|Zf$VQY4 zS&eU1?M+zTc={-7J@ z*lN{Q8s+oj*#e?IJh-@JzFs!#W;Q;Ey=oa@D4Qi-;0M!=G^QRM*Y5(XH^j&zqbFFXio0;n*TurdZJ#-G}e4`3`9cR~%jJ)81?yd9?dZ&PV4WB|{Zr z31Xdpxx_kOsI2uaG9i@(1U;n~=vpe_!qZ>a>Y$2wfKY9G0ciFFMhZf`<`4%MKdf5E z?N_ulnCQHu&)4q$BaT@P(#%RuOULaO-g{0@^hP5K7bn1UAJ z?=@S?nkaU`nfAq?gC#f?pJ8eADJPlhz`*<47{?E5w`J?NH;Vj=YOua9!5k4S%qQ*e zw@~Q!GQYKg|8#_t_Ph8>*goQ~EG=56Oxp+_{OIyP6ZD(u zHs*>NnvA4#gsFVeS$WJxF}6d#vg#g1y!YE+vTDM z(qDTeC$g``;yMH!>IlyBJFw8H!3d!mpm}sFKKGB&{T|M}P4QSdOnFZmG+<-MgZ_r> z7sod;y=%p>8@}v;z!r1jHxkVFA$hq7XBczsxl7L78r`L^uWI>=t~Gyf`z-soZ$9h# z7Hf*4{uuy~aS3N?o~-R|3H;ULI&dy9)*AsW_{}}-P%4^^h0Gp!$~_!bnsfAfReXm- zs0Dpvl5&L2+FG&@Vq4lW@+Oxx8$yMnfQhsgz1nh+a!sEj>29NJ&mWcNI+?E~p6~;o z{p3E@EG=V1Y9d2^S|@&Tzj^|FGM?pxPXgiM+AU*a$NC|K0O~z3q7y0poN#3)58dO+TExRr%iszWjEL%9LYb zwu?$no9G+2EL-4+{>)U9{|;ssp)d0^ouys9&Mo}aWScW-feNxU9&mf2m|F9^rWa8* zXi##pno{L3k}amu;c<17Yftsa&3Zu=4gFItq{7OF zl~cwUyl>Pm*S7n-+@4*7(dsFRO;eQ3MkZlS>}JOHNy@oFGWB*%AfaQaj0O{_F3ukC zqFVWHQ5a1F_Y-60L$pwGNb8hK;axmm^WSVhi^;kt_?$k}Xm$1T_V(OIY;ifNnlo2B zDcybf2g-u{;J_aU98_wEkg~C}CU{{{enE%*=C?j-Y3M*`zDA8r{_xUvZYQEd-uLJs zuk98OV0VHUOOD`cds$Vdj2qV#U2+;(w$XHev*GJ%4G=8PGH~UgKoi3r42R zd|8uBXXsfju;++Ptzd@jH)m_f@g)K$Gky(+ubrF~F{^rYom~D{N9#c(QjP`x{60lQ z6eyZ{P>LN6#GZGwr9ub8)1=i7C2$2rm;r`~J?{_jhLRm4pbnWVTnX4=5>=-pzyuK zqqX~BcfO*VNS^ra4nbqgVCjwdR{!pG4{&+RKWB)&ZgTACWkzfG|hay!20vf08hcm=YK5U>HQusnGw}J7-w^#M@f@2<%L{WB2e66ng_LwFw5K zjOX^ibd0Xvsv!Q>4MgIT;ZPzA&TZ)4OF1IFO@Pdf%Ltb6P(*Ckp8svvKJgM2W#uN) zZ`5GRjA}1hskFUKP5D&8W8Z*Bw*C;~7<1_fX!_>ybhlqt-ml2?%ddxqk@oGgTq0+Y zP?lL1|48Ymkums5%0C{zUc#XGgXJ~B!$8)hqhga~^RufKc8S!lf-6m+ea*+OVb!Ok z-YvZ-UF+Vv5Z3q`Z^QL?)J6~CJW*NO;%@xp+5DdlolYYPRP5RBLiKSEn|$o&-a?vZ z7!wFGLs*P#m=gIYSGyHk9a-C=Q%8TcsWNMDk4W{&Haz}haiz;lxDpk$IUqO~s#txAKWN+W?eVdnRnam^e>9#IIHnJpg?AieWwB4Adb2hYsLLNufZ>nEdm^w z5sD0ZO;yNIT#}z+oo*C2R>aKKW*Ej z7O|2_?>=egq6HNdGbzv|lz&%%8CXsZ#&?|s7ub7~KzdW=b8(X$RJ10Nf=+opCa9yB zD|9mQ+W-RCJ?wtsAf)2|Vq?nYZ{~=Lyz1*b#xPLa6W1roso1-b0u}wO=dlvdGD2bl z3MW2bbm+({n{8Tjy#67-vEz!FR@T_mct+6<8AGJ~fZ0C;1@ZK4#-QzHpjuPLsI|@4 zgAXKVWy$Z4R1o0=wnh`EYyHhJhC*9KW$`bwt4Ar7Z+{-x_swtwlnR%3Gim0sG!Pzt z3B)@>V6Hyqlztwg!wZ;Db7=>;0ttNMIaBVyv}@!v?^ymK;mzWiQ0K*-ggy2q>pQ~% zSGqr<%_=(w&P3O&Ra7H>$$e8O*&tzwEoV;OdLx6#D&~QLVMTw(9p4+UPaY-|a~?c} zbfs7z({H=0v!(~IYH)T#Dbe|`3$es#@`T?eEu$7rFjAu+lRZ5T8|xTUyU+<@xs=Q8ZQk2E8vHe!SEX2&2Y;y+6J8ROwq zVr&IJL6tWi%*?E&#$$P(hhq9Zn=7 zhh)1;2W8BKJVMBKcoMkxUDQ5*cKy=7%}=3RQGegA)$NO|R^ryT)Wo=YejzJ+K26>z z$Rk8DpU8YPj+9V?RoAzuy7oZ$IlPB?R-x9vr-pE|v_@QAH_1_3v&~h*<090}lk4UMzbe4+qKMLxxv297DcEK^!{#R%%krxd z)2BN;g6=CxmPhjj`kcEXz`k+e>tBR(XPv;^5_J4Kc8G$#NaFx1a0^z=Gs&+iZtl!M z6xd!5wMyd6ulz-YUIE|oz-SW(#MCX-Tx`T{W>c57TK~JpK*9rUvWAVHhdSj+sOx)J z6%;|R)G%$=2+&&WY8@3rW~qP3*1FjCW?5rWC{{E31DZ`fezgc7Sbsm~JZ-C$H;qzS> z)jjF6?myIbW6B{Ms<3iqag0wF$8Q||i;Yu00}d9Uy@28V{bwUiJ<@;tOTqyw*Oom9Vj%m6dnw=+jZ-1;%%Mt(J0BNV&l(R<+3i9@&dczEyg<%c(l zrN4fYAMQlIYsUOeAcmCT+rIuK?hTZ?yZ?mJF$vaeqw`m~vBL4!f`5-`{;c~wg6G+E zFkxdtU+SU=E)gd+HU zrXsw#^B3$WYez0%<5bXpc}f*obM1DrTjjk9p#&a?r5p7fRu%sJiuYx(Z#1HeGP_p` z{uC{@iKcI$FM_0KXJ%KQEC&s>#c%UOd@Ex84P(-<2k(foa6})vaboF`6TbAFFthT| zJQ51wO#`+(w$zK4J|CcNagyD?%N#9vxX;iY`nNk3EiGSd_ z8kjde<)GK!$}1~9;pMAO)dNwUWDDMkerDt_1$g-wZ)fhh{iZkVl>kz`3bh1i+xFcN zME&6!aid4R*O9+;(-_S{-FKrAvt^wI1kf2nKs+=c-!%n}9EH9rViO&UmrKaRA8o7D z8{v$;`tQ*@jKsM08YcvgLiX=#<8|ac0#;k;*kPRlqYu{dM(eqd$riVdS@(6YkT{?b z=su^p_h1a7+&d4}B(HX~)~TIvmlFE8FxEHk*m@VA^;UfhgP{gxiyP9U%~OBFNFF|K%uHLu89+l2PdUBd9gkQMaP3_eOtifun$_C zpvYNR0&Z-ST!!6DKvRQ9bh-R6vsv%KOBZ{f2|@Bu%Yty8=Zak`XnDIZ1GlxxbEXG@ z`pa;1OZLE9SwB)AF6%3P6(u7S^|4Nk3Ms8?h05>3Xpg+58}#cWIZ-H=Z4*;dfg>N$ zFofzZ`e+6P)&a>(yGHR#!9A7N?^XqU!)nE?@2O@)t;ElahxIe-Xyyb}x}GPN%t=XD z+-BC=5W^&;D@@L!A^Z^%j@8w1Z*h(XU*B~-pVIRO*OMXse17KH7etf#oJgVc0QC4z zAn()J5ZvhwHf#XJTj|-F)?a#Ht>qYNHzY-D_P^pn=&dyjDsTMX#!Wuv2Fa27h1auTsCifm<#d`so9qr(Cw0F(AdXOEl?%{iSk!N{ur&@RoS!q1T!*!ziellsYfm)DElJRA>|hid)79JH-h<9T@b|CmV0(l zFn|0vPGYTfG0u9YAPO?%E|T))FR>j53o*q6a0iU^ z56J9(l`)}7VUclbZxmo@aSr;&3D`NZmIu%tue^>}>RAPjcV}5MWwQc# z5&87DS7|Drg&p%`601s4-i8*UiCkSu%*d)4Y^Kl$cLU06QTq{9N-e5WvdHWIAiztf zXP>y<;Wat%Fj%WgQ0Q~@1I9^nhqD!!6e>W{zC~&C1|2PlF)RL55aIstk*h^N+N5Fr z_RpyiIx@V9RZE!4SfK*+naWs!!E6Y|#qtNkK-nUJJbCweuuz`oZ-hV_|)$WQ?)5;s}>t|LKL z(6w8Vyi`{Kly?GDLump668K>Q{ya*g#hPJW^$TMYjdf%x8~FOo2wq~#xus{T%wn`h zg2VL6qGKEisjzGAenbdxM^kktzP=@cef(I~_yB5vr%yrnjs zW)A1J!oU4shG)nJ`&PViSk6PsUC;~*i%{?v0GzS&?O2uG&A)udU z=O`k0b{pF>yc8~NV~>yb=Hdg{Z2dmD8$Hm4Bm+ z5h%<5Fl7_py(nXU#L6ywz^e=U!%`5}@|OVRlFPQ4zQ*t7&fU0vw%f=s%3@~zw{{a{ zIfvWtnLm2ffu*3cgrh3Med@$n=4tSMGxI$ffswSuOgbrSU&(yk0*PZ8ae2@)+Akf# z5$k*%ur9*EXPjSVqa!CjPE>LA)ve1N(amcyy+t7H5dP^uFnNFEJK$-PICN)H=jz}1 ztiE=#7G;tH@?i1lLj&R#2}_O#W%g`LS2XURMWlb(H2TG{3~rRR#R-c=tjQw;L&Omx zgOik6-u#t<^bvqCNXGuohxN6g3D>!(LS>3>0mBqX+U*-l8m(JF5#_1+goK(;LVPh2cLj7^5Mo&Zr`*v!O#f>Hz?r4y*qX$} zBh_BT)ISKH0GZ~Jx6P*8%pUW}=m(k<$ z7*~rnRD7>vq-)2#_~qX>V_9P}^kV4z?`3AY`0YQAxp6EjLWw|{QihW9Sh4mw&?B8a z{EPkQWFZTmf%W|+ODKZRUT#tvA;Nl(S7c@ZVum4bGgSC?8G9!GL$%^GL)K2Jr9gh5 zs(|rHJTL`sY{^HM=4f6{)K2&kDnuIJ!dFrCpS>kmX~YVI>4~+vCp>#7)r;M>AYZwHd8DRFp_7QG%P5HT^ltv4p^=^dR-zYa;O3OdC{BYrT%SSO^0 zjQMwv;~uoHU`V6FwJNSK(tmvLr$fi6Xt|@>+Ye*M^^y_vhf}j(q9d)X0}zZ0!grQ3 zl#m3yUWQeHyf*)9Q&Lzvir_B~@5PQ~uMaoztd$)61%X$jTD3-8mJaNAx(KD2X3-%R zGU6E}w16PoKe}y2-l)9?VgU)u4=2Ijgalu z*^zTuPQQJb-`YqX(_RI?T%$x=8E21+qu{cpIw41E27XU_hm1jb7Q@kge`O$>)-Gh* zPkk;4#G4eHCC5e6yg4@Es77|<&9G&p&a<4^MT9-zi|~I4Qi(x_|A5_hzPF)HXd)dh zGkz0}?aKns0%Rq%4;1PV!P1odfM?Gss;ieZVQhi!YKM3i&5FAe#q-|euM&8av_&+G zmD6EwDD*XOofy?G;DeVZJ6b0zvY+9iwBK=*Lf$K2+w)|lt2Eg6+-&^L9NSYn$nRAb z-+FKZrwZTYw%i352-+63Ek7(_MbYPF6~}nrpNM#FemCYz5{ixq6{W?_cWdJp$`hsV z%d?QurSZ&TS-KtDlvf0#*9&@@%!!J1fTCW}ntOeeF^}igA9byeUw8bDc4$h}bokA} zm>17ACuBB|{~)$|x_7hK*}qHoOEP2D(|yEihd60$Oif;#ym_m zYo_Ip*D8Lq-^yKiE({&6#~--^QN(ivBweI_O4wK_zxxx-`sW8b!Q@AsN{*<;z~GF& z;w@v}02pd!DX3(dfRNknf6sMQSEhAtc%swqlWT`--U>R}g)#@{Yz5XTZFGzAWzD|L zn)rDUclf(8Gv@1}0MkEm-jaE62JQt-F+L<*KwkTX`~AO86eMk%r-|52g6>wV^O3Eu zo=$zKuh2-6u-u~)kt9lZ; zH1y1-l%WUK)0aTalK z^7|t*!<3PTLZXk4r6mX3&Is*{>~>7uRCV$1qzVDGWYp@GVOw8674kTMLZLTuqKh*> z#OnhG%kQmDk4kjIrkG9t(`gqKUau$nCXhT3EK^VVyNw=L6G6#oun3)obaf`XS*a z0v$BffsRmH%k%G~|23}Zr@em?h6B+Vl)B=AfCU?xFL&F9-o z#t1-MgX_#|#ys*9nSijsv(}h!Z1>-%(m~eIwS_kFbd;c{fv6ZNmV@_uP3CKwG)Wa7 z;l^vV!TA+S+(EH`1YAxq|JnDS2qA;-O-Ldi)q8Q1mP&mBt0QIJrvvDdT!d?Hh&I&c zvm~C(2&Ya-Pb8KgMTek^QUdJF8(fW>D4dArM*)#0k!v%vTCFLu_g-DD)pr=2O{C^t z*#J$-Kb5r$IRb8Sd)S(vOg|4-pD;BJ>+R%Q7!9JFzn^|@+owBUd+gz*o+t`57w20o z{JZA(*1j0>dEYdbx^dQ=6-6=amh0`qw%SeTV+;|JoluEagvop_;hHE`zdABy$d!lP znFQO1;?4?{`YA({8~c*n*5{*#vNENPuWw47f;)Q*-x#z6mPqpBqTd`Ht#k{PZXU^Z zLh*@i<3}O$BR(8Vf7T(ovsLz>BKdXeOf;uD_-7Uw!$C*sFX3a-bG>4$iSo#_Nnc+P z(w{3gxyq$X>9lK)Jg6eB_vJoY)AfCfYNohfzRmzL^Cwl+~)0TXnuZNtDd|D=mQSHv)(9Eo@P~q|GkF8sE zy`{>MKT9Yx{Mv^TDXJ_O<fIjIJ`&tjad11#0c4%czNWBEouRmX;X~` zsR31mt8==qRsLo5MCT<}1k1{mNPPwy%Y-9gquZ1}mD!_Ne5ns7rng=yn6)AuKD}7W z-+7@{0iw_znNS)By#8`0=CyU)(1d&iBl@ZJrDZwc4^aZn-)`ggcYJ;gZ0Q-BW^AN2 z!IIvkSLQdicH}{LB5fEq zC>QCO6u@m=^CZzC-y8w$iyq8zW%W}j)TfA79v-4r+VbmB;kVc3%fAl|o#gXY2|*Iq zw`N-u4UZ>Lw5uOKLc6@~y58QTuzo`sqfDLi)pu#aX!{hForf4bx-E3S?fegn~I{kcbc zYRu8IHcyG`)fZ$?HU9lRrvB+%;w%ZEAdxtbN?ljv=#rC0v=S5T zT+p*pzfO6%5PR$E_U?UyEC6#V5V;F0NW|4wL%`mk$~j0_F{;71Lsj5Wif zI9Z9&{z{m)+8|AI$|gK;@`_ktBmWVG8<5ZsDM@ zq>dhIVM53@Kf3NT&{snU!AGZwCQgcbU;5Vl{*@4LWH=7g0P?xetBc}|hcHTR*lNptG**15M zNf~maX16suP%NPu@@Oj;6SG6JIj)+8lV=iwDun&n{`Elp9n-gS1bp zZ>t`j67|9uJn~zc{>=~|n+oT*&|zNoFnMT#xii<9(ClyqXq(kQ)b7s<#^ZZ-oFnx6 z3m}!GHD<$lUT-vyZy}7N{zClYCHG9N_!`HO%lW)BrOTr99iYe%#d1vJK%Oet$)toU zhao^MnbAMb&UUi*^Gd87vMKHdGZB66jB67QmJUAO#RUm66r^JM*qmu)^XVrN;($~9 zZ$rn1F|4(|1R;g1nqiMH)AIbpuMkggGoBQK+xk3~nfD-y!jmE|s$9-*J`TwT|5 zx#YxC4xr0M3zFKf;)rhpU);JxyGvxdr?e*KwAHeoEh271!Bl7-G@MNfee|OLrgHdT zW*b;j6pEDA$VpV$;)X9`-46b-Kly~6Bk#u;2s0{oYLQk=UXHVcZnEEBUJc^-yc+4K zpGtcLr6lQ9;+I1aVw$(w*~^f$3D@y>DyE3r_51BJtONzexnm87v=Gq#p$}!tR_p&r zJ&{^aB#sn&^{Q17Niy}Pvxdc8m%x^h(2dq}@vonCeNO4EN+$C$+w-DJKbsz6^=H(0 zeJkgHJRLkVf95uI-vGT$cm!=d630#z3K>EbiPdaREP-HgwaWZcSpTl{z+QHYV_OWn zYbH}k`x|r&tP>YAC6p&ZMeCbXg2khd(NJGt(&rPA+J1dbm+yUqokW3U^HBQzyD5$Z zLgo(NO8mYAl!TsO;r5by+M}ts`Xr)({ceC+dMlG;RhN zszoyv4V*bYNu82!$npU{^M5KujJr|8ow`6&QhXf2a8&#I5M>B(fAL&)8;4FJbw3lYa5#mDwipkRTywVjt*O> zn%0JTC>)rxGN0W29fJQ&S`o0U?KA%Q@XbrdG?-IuG~xIdir>80tOy+mw1H7DQu;j} zJ$dnSJ+LQK?x!7j{|Oc z|78VL;M8Zh?5O$;-WbK7SlyN3m{U3=Eo4`LW5Bx2yQ2!ZJ%1u3`_<_}U?THet0f?* z-uHXa)<}rJ-}%Tu;Hn=aI{I=|MH8dzsQmx40D@7z8VI|2*T}vX?F>n1*5Gw> zzpgT_HZhuy;;ZSs&gCQGZ~f$UixVjeqBcqGB$Qo{0t3TpSBg148R7Fv&rh%3SYH2A zdqNJww_?{#uL;gSL?qzyo*7p}HGlO5&$b$uV1(ie=uV(L9P2#Ff;1W4LShJADI_Vk zzwQe9yNch``@WGp#9EU;rkx3w2+T1>>fm-0n!iTv>FiGPgDB24p&H{cMpBTyzt7dX zJ(QiLI(F_+j~#-MyeA!Qy+M2i%rQ4m2bxH8)2za>``9ni(=avXyZFaM2BcBkGVIp# z=M}^GDrS;h7CoHlmZ2BZoORp8(ZJiH%q?4tmbaDad0Na!0)CcE^DSTdc#t*Lo1;t} z7a$(eIh1sGZ!H!5r!WDcDDYr=O2Gp zaV+ECB;8_h|1M@#nm3Wc>mif%W(0;^d7$RYJm1LB!B;b3bHHR;!Mp9-h;}BN2%J-A z8>AYXo^6(QPF3$$=tdn0**VV1zF#CMq z^Az{cJ}7r*z*@*g3Nxz{6qeZaf4f?=FyXE7o7JK9uAk-dCC>xP z0`yz+7jo?0&8SaDg8RZl;vTSxo1;NnLivwc1%ch#q~~j8`d7qLwaxKqWJe9$W?;=Q z_T1KPD<@8(IllMG3uEguqWNQ9w^zz434}j@P41fC;r-`9X)+W~e`6fn#Eu0Ss0DbN zPEz)LB54E(lqr0^J=jgU$W*!RcqK)QqI)Vju7zY)moz(z0C!0a#Gaf`9d2Os6ZTx$HZ#5|MY$i2e} zEGfJbaNaEstsjZ-=ndtr@-ohLaOmh$NK((9em#$?} z-wE0J04DK<~#(N1I0waRPZEzlb@Mq!*WbFzv)b&5C7+frQbC*oiDGmrDzvhcA^~n5M?YrD7 zK?cXIqEJ(>UStkF*SS(6&9;*={R_{jEXF&E@af{m>8>r0iVdFiX3xY(&VFfy&YV{`3I{%s?#cHJ{|zKW%%MnP(d%Jtz^d^g(8AaNNJ9?0?osp zkwvK}t3#D}tUH_g+T#u#nICK4sjt7jh$CU>{>d3$T7Me-fWKdhS25`?YwDp{6;%39 zncBWa7^P3kiqE6uan@(D?XwoU6JbNYZic*VDeCGA>?d_bV#>o=)3f`Cvggrbq8?Wm zt*2A+qmBB=l{L@RLfjXdnzH2ZUs%Z!A!t_EXAO&8hEv~V>%wKtOQK2@q9t+Rs5p(w z5e)6TRjW2UC~3Vka8*1Q6(s!R7x)H17?O$P7CS2-f)&4mQpfEsy+?84!81I2vE<~9m z=wqd&Rc!{l`}zvqcykQJWexUI13}ibRLwg*t;KYodr;J%;t|AfG*j#A_csl7Yp2ju zBUv+ew>IXcXl)i{)pp9Cyg0+=U{ALDx`N11{4x)Kto{mA^Cmmuv)SfaIl`b-uD%^K zRUG#@H-}=tzfde@p+n|ty>lMXF=IboR(!hJrmaTwy^x!TBl8DlfcLY9b$qoV*I#6z znU4BOLIW&E+LM0EldUfwE}UN~Ax@GNEj*-&YZE#c-xe}|n%7AzC@T%IVe3@)D;UZq zez-hlPqu~emgb+*`acSjlk;q{_Y11L4$%pDd9zeORc zLzK<>A-fSai32?D>QK$8VkNK2-(pVGUtA$FF%W#kjzR;sC5Z?XX8iEMdxp!;k<_ug z=cwy_e*LSKIKGML?D%qIV;?Kovz)yO>KsL>A=AWS6r+V|$QzyA!Y z`n&X*qv-#&_f=7GZOxy!ySp^*4vkx5!8O4(xYKxWcL?s9V8PuXgaE-kxVyUrn7;FU z^ECI~r&()W{=LrAdFa}wcFC@)U)65ct%;9tf6W!3lf$>dYVY^**)b71A%6?a2ff3+w4ljv6eqyGuW~ zcf9vkuWu+P$+zd_CmY!XwRGRr5U%Qz7Y!5N)>~a?(&taeHXYsd9@|v5E=p zKmQQcy1AmUjfWJGo%)-+XubQjS1gSAt5OXoLo66s1RQYN?Pu$RiKvQ2Hj&m(d>Yz^ zEi>5g*y@G}sU-w82EAyxq!~3{hok&rzU5O1@U9zYT&=2kk$bt1O#GxqKik-iymI@l zOKLB(nvE=(ZXtL>woT&K@yq8|J6lXAdyR<`PPfpkW@?OOrA+dWHK|)h?Z=1M@++53Vj7-+xwH>mCTxUC+i=@ATXWNQSFxrHsxL>_2 zxn;BJy9cV=8wOa!mA21^ApmEW7Mbn(S-df~X5o(IybA3OnIqaW1i9DSk$2ilP%m!B z2@{lR&YYet(cvLS9SPdRgiQGiVLV(dC=FAGCszl#kT|tVv^RL2Emsbg3ECKEIPU!i zEb83`==h!NvvRc_EWbRh=(UGUud}gDd-u=rRxxnv3RDkp^tQGi0ou95Y{P?u295#a zB2QG1Kpk?y>fS3F=GyFliqOTzRYPe(VaCk@lOOOsg^=tHVx^J16Z+dlky)yk+S?}j z^8ndy0A-a%YFrA^QC!2>ewFL#^Bqm9tol8WKbpkvk3G{55JTdbnsY}WmISqy;j;R& zjTV{CeXHrp*_k_fg_JGY;cu!W4$**$ZiJR`u8>ajA~!@*pu$wkc)wGb(}-W%)YCik zG4c{I`gu6bp?Ee#fa;Vaj5uSbdX+U&Ja5 zP0er;X;pO~V1Q>6gvL_}=I1t=yr?rb63Tma1?r!E-ChLrdEy}PLxZ*sh+co@CpB^` zMZx*AC8d!%@u#|1q<^35+eyJ+6%dW1n5Ow11yAp5JSM9GuXZtwk@a~(J+T7l*fkii6LkMhlpjsu^!=gwO%;}1fL%5 zKa%%*I>l8z0Wdb4C#hEHKS;KQqrKHIqal7yf!kJH3i62dh=tnIxOd{5>6G30o>9~{ z=s^;kpQiLn{hEAoZG{8#`?^;xMA8s{l40zh%WEw+lU##shp-KVG~YH?g3g=?X*U?5 zLBfihRn4pPi>#53CAsoY3wimXx|O-7A(3k><7gA#kE7sL9^sPK8zQow(``!_=|72s zMObdn+TfR5g^51GG5X(8?%Em`Kbk%2Za2(sA@{LSIP8B@c~SU2ZuGc{izE?n&`w8Z zYny4*xP&YWeuM?)_uR^-K3$3KMj^@r>9;K>_ob*wIUn*PnB#KBRu|h2y_%1mo0lLa zQ6~7D7E1|6-Z<}zU>%4amBu+fj-|-KF@3A1d`V{JhKxmh(rc7t#h5METD1mWyHmX& z_A5^OwEp|~8@+34d|B85)GKyxB$Q%9H+leglxF5GhGzd5Q|p~xa~E0zC%0SW*ciDS zP9Lrp6+a&<$j~=_$Tr7pqmah|u7v?Q0y#y&=S61b_Uzsog`21Q#hQ+8%qJGrmg+_8 zEF={Ho{%HS#k6jk!esr={vd7b6f*zZmY3{?X=)#k&dIb@Y~XsnG%ckf811D^v4gcf zV`*|F=tvqnajq6Pd1#+>u|clgCGL5K#a9RvnPTq1Ak%7za=c_X|6TRk{FR-65O6?J zSd-svu4<-DfR@8AKsM2iVO8k_7SX4DP=V>Ab^b~80>EkgT4(YN2VIjXuc=cn`n81r zr#W3aIC{^_1@9&f+Yo$~LgvE$cUs3gtmQh9JE%`tc1%KIk%l)Jst_Ll`tD9pUN zJzL`j#;SJ*H6Z&8tf$(;7`)jS!kr~P?vEh27jOLO09#(f96a%@2_C-8mD1Htjf+&Z zGy}FC?e5<*KHzQHJ8v1c%x1R5V8|V$2gpL&#Uj+}4sK{^X@ZmYQTH*`%kIeN?aqJ| zXNKzqj;(f1TV|oNJB;|HQpP&+`LA+157p^nMKzPVE;Bh`k!+!yD*x$A`y@zi4lV%- zkn2vzj|FLqblB%-+6$V!YHyycIVDsXNs?mo(WEm_?RO0MacIDJ1wb8Ks#Hvx+)A}) z0zPhA%h75c4XV!mu6~(J#?Ki`kB}c}vwFUCvi{<)#*3WGiMP`x!#%;c`B`XeO#AeR z)A?o4kLv91;H#{AeqrbAvnt1IUc^Ay zTMG5z(XSa4@`{rf2(TzqKE3ATJtkU~X-QYknB*FJmJStYfza zk>ML*O=zb!qzqfEiv?#mANXx#334%fh{oETtE8MSwy=swb@Zw`9HIC|**h&nESW}-c z=n zdsr?RXi2s?UK~k7On8VAT%Sd}!St{;N||+T)HOo$ZfSYQG`QN( z0B1PDL8NijOENPDHB!xk{;6yGf)p8X&~_?6G`WaXKu;*LJnwr3=G$Qy>`^B9p8DNt zf}qykiSg%Ar54K84$5TlWC<*!4I}p;JXb$ZJ+qR1_K?PFI-IzlPEJrU3S#yN^W^f@ z;vXw+aWB*KM?Wm=eDxd_FRnlx{+BTgk@<4g29IF^IS=3`?Zg);D5V^L19ph1_m1!F6+Q~p$Nc(I!C}-z{Hf~j zN)3iw;UOQJION^p3fD{~kinU^4!&W@c2SIoZrR zPCC(^V$UTNmysec9?QHtoX6*j8c^s+TywDvBAF@W??wbc?VL8MgSqcB<@+ zUio4bHBP1x?tT+3JRtZ0n^|iBX<{WI zb{_Vk`beTO>Ll{v@HZ@1JnUl*$DalRLteJ`hW@tQQv9Z5XxH0v0(|8GofZNW^0j0MPnMmcSpIO{Li0Q4KO#C2uIP0%WVL zh4k8PS_Jc45YmB&`KPzzHIzZJ@yRj22%u82i*D@t_H0{sVTh?+(A4_4ypuX z3-&_XRYd;i6|W$Ukh=g0EivTfi9-dYN4K`_g@ft3T`wm=I&H}gK{W^(Ul!Jz-itC1 z*kWT5eU#pLB?Q%@B(13k=cw;*p#qvn5%)!N$DyBDB-?p(gfo5;)COI)3J#M^_zUJ# zRnPqASo5G-AVI$u*~`@=-lbOR-%z1Lr@zC!+On`*>;Z)mW6J%F2mYtuJubj5k+nl~CJF2tk$P*&dGmJ(?$ zx1nlE6}Y9iOvm>O79{jgT*5)&6(S!tOAj#!nY*svim;aWR)El_#*2vYc*d`f5*WvV z;q=b;zWZCE0uy-TbP0_~O^EA<&5Oq~>EL++8%arXv<_7@K0U(eOZr_S9kWq{OFd?l zAV}a%D!TRXEB2P|6~dH0+4_h(>}RNZhuZT|l_9C&FgHsm-pj(>IJ;KzkZQ9e(3d;j zQO(5Ek1Z1B_b(>YZgDxD$IMH(sePNH810h3Ou~kiHmEHI{Tsl?XMQp}56XlJAPpoU zuv5$7#D@c2PI|>Iokd4|9??{D>3H;gLKO67#XV`8aj2+i%u;Ds8TF}z{AiDdce>wP zjvt{0#(KQ`Gk>KBv6!|v@R8ewl!Pod4d6PjqQ2e^IrenEiqSH0w$$1=|3pL2;1?zs zM&W+8yJgUQ;}U=(HOLQ-$sq)M>Njg8c-b8gx>bz&1AfOtV#Fy*ptoh`Ih2FiEC255jMKyVTuM0=6O4D@N=)!b(An)0R{f;5fUL!j24K+>E%2 zs3xZ*QYn3gK$67Dz(NfBIA2t$M|frE4_I3GiRCg)ikhVEU{?)UnG5G7Ceasu%2~KR z&`izvRn<$gv@b%6D3EiyRZMZA04(MK(J?DjVWgVP9hA$rCw&R|!NX5$DJw=Z#|$Gy z1mM_H0^5^eB2@Sf29-Dt7I?tJ1TI%_jZ-} z5{2k15P~_LwjZy`e%O)Wo|x(GM4CIkYUB0hmXb4GAfOiwwdE7nqfm-=FAfsfZVv^N zNv1XQiCh}nkBzz3;~+&LI?M{aJaqcP{t!p7StcRfcteOr(aFaNk7)T*q0~cgzvL4 zeE2cgbq2?la1ZU5t~$KqWZ)9=RQZij=plC7Mb!&P4&#N_Zf}|AQ;KI842z8qX*I`# zBW1xHr18aDLXqnD9c~VHmQ*GSl9>vYhH5udkvpd%IX1USEu)O)8J$g}@UD^96k%if z=tO9^SfCQV0fqqlm9g?qFBD=CZ+1!jyqwR|srRNevrN5_pCV?$cGgsYwi zNkzKTaaS=Ij|$XXcqpJO^yMvItdu1Ab&ro&Tu2oo@;X8F>!1sQS zbhxBDN5rz?nK^x6m%(B@Y`_$b9%%F7{<-l%XfL*;e^H@&_%7@OAMf&|>LA*@u%AtO zEF*m&DhAT21Oc!O`SEL>H5>*_v4`&v0D6pDX`My~X0CBCjU8=T=){@DZz!;CvxKje%NM_F#XxLui$zzyIb_A0`ZW>9R4=^qLmD-; zS;LYD`k9cU`z?ILj{t{h$jD=cGv7zgg8AZzV;i0FyVmezj$YbVtlqS5ADT!{`=q8Y z1QU`jL#uZnaSH@MIsdasN6+7(zAV#S8v+B?$v zEPYLrfNVl{VQz(AnFntWyy2Difja}kDq#YVA|6hII`DvWh)je@JriPjA~%O#pA{jq zu|WBGOyvuaQn$8!$mP3{tVT5PJ%l^E;3H8mjW}zv6X@it*+;+&)FJk&wG~-0%R7C5 zl?>r;ZlxcWRDpQnO2OEMe>yN@>-oT8K0%jG-KulP?jws;S?fDClV~a8Hh9>Fo z3xC+%D&E3EV%|A=1-_K-nX*5FH%h&d*ao430~J%u*6$8tuH zebMCXO;BkW1WK4v_C3d_W4m;l7-OH5gYkNFbCw0}uSzHtQ&1%#ez{JRilhra13E4r zi#7(vG_r<0$0k%tM1B%Z)$uGW7Qk*JG>CvKM_j*Rbw<0x#5Uds5@~TCDnp72mz^J` zjkpv%r#Z1B`UNA?qGyOt#|aoXnM(rRE-OO2n`iujJa~;TyXG?>H-Cs^m|#O;GBVo6 zz__$cBa|c*jYU!n4pF37>l~l~e!))_-#SFhK{MHQ8RrCetqk`P$FQOot#$DO^}o4Q@7Hnpd*g&f9vglA6Wb<85cdt{rqu{w z4!v(sIlCqC6Pw^-1BCL~5XdIirmXZ@1f|fbtXvwhQAGT%b z)mIA@ey~4P&lvt}qHxz6>fy)?4gFzMj$hwlgxY&pR4vCP5^9wRwZ`$T@cBd0PxvFE zf>Yi%qyp>hmNkBZm-S@6_@k84B^o7PRge6zZb5DrpEWBkjxi{!q zU001!sZg((GTurkr#~RR4snVBY7lYycC*Y|hn8`L6DVJGqiXjCPyB*Aw@}CtrvP|G z-={=5L-Uz|jzlB}U7Hp~DwsA{?@DnHYT!hgRHq$?D-o^o0|7NT#syfrh33~XupW^> zU}aX$!u_00!t(O(DE>a!bHZIo986AmV(<*}?x#K$`|yai^Vo*$A=BUZL=YysG5&4^XG8Fql;_UWpTrM6@fybb;E82#0c8PBK>9!p{8fbx@5fWlStqIWaT=m|y4dW#$sjvJxU zSn3nbVbphzW!oDR%Vl9(-X4ctm7`I=B$o@{q~kUpbr;%A`65=fQMdxchgFvF$`Q~6 zuFXWFGghyRO(x#{)2p!TZx5ipXd~IghcS&Iu)~2hziywS zHLnkyF)pY8u*Y`kXxeXWwUKW4aW3E8ehRRYrU57T_#sy+_;9FKt^Sm^T5SMGEK`5E zUA;!Z86OcB3y|mnz(ZO0(FMj#Yonsf0jFg6_#iMz(WrJeA-Zr3tC^|IJK7COXyEIb z5DSO;!`Xc5TwN73718X#jO$#-4n>rfX6Z>W4wv8d>MpVasPvAw@dd3JWXneyF5_|l zzjg;!e!odH_7dvNBVr%0GYW{Jihc!z5pIXd8{*BjYer{_QD>4l60~0xi91r#80KMO z>3SuB>SXvnE;xj*h$H`|uGKoaQc4Xr=`pjEg<00s2z-Lo5QqJ2*SLY(+W=c1qe7Zm zc;;1ZBpUT>f02D^hgydfPnA!HqZC;{XAN$ zSa$t*9bU#UWamXO@EF41NLRG`0I8OMp6$wLAW4pZwC%;=NQkjuYljs4oN)sk_XX%q zafN(b8q_s*E=Y@-kOa{f$j3L3M8A`!rB?8cS#NO0hT%@>O+I!Xe&=?dBMz&Jd6i0H zvM8F`JULm+qY3bp7k`OowH_r=G2F5M*L5Qd_F>XxpG^}(;&FzM9~1JubTD2N3BHyH zf3SwCA08Izz`zc0UR&Gp3Yay6P;sNT7PRRskbpn$N5j7TDRbZN?UZJgyKP-BZ1~c@ zO1|9mwbC33@Mr4k2y{vcDGIj7QN$J8g`d8|%3!nU;rR%Nm(rNj0wvLB1;7<7$7*f6 z?S9{B-0nY0-SzSSY-M6t#V|wa+$3-)v`be#?97TK_JwNlm3UV%_0;X>uAqB`P#)gu zRc?#Q0>+g$F+KbYfR)PK9Pjj^Eh@ljJY8N9B=)9&^1Z8(VQKM0BKJeB$Twx?xOJ%n zzCEK(OhR&Pyy_K=)xy`ycU?a2#ux6pt9!vC8mC$dI?^CBr(G=dly%3Ze^mH=2qIRl)gghx7fqh~u zS?SIguaePUQUu73UXLf=a`LB)4XIH_oq1c^eq;NnVR%A~`-$N7KYm(&3J?7O0e%G~ z9eOkavpJH=EJ3={@_}%{l2G+H=eXX;BB-yLc?0=LZSV0u25}k#Rf|N|0Sg5=;CD7~ z+b3?-_=&N7sN?(30=#8SyO8-m13W(gmsQkdUuZkdpILXfs`b=L{JU&><|Rz3|9B<= zgF+qgVpaATW49C_mvrlu{NAKK$UJ3>LLKP-X)St?Tk_ofhgsFJS>DP(NKk(|YSok9 z^SmT#I}>C7jP{WS8I%k~fsl9ypKA_6>?R8$feQJtS??v~QB>gQq>ktqzbBt`*(wtq z@yA^`SC3joqG;>F6*4L@t`D5_>9JX!W@Ga?4+3teXIgQ?Ie0A0;F)Gc^!_x_l5u57 z2N+?9>NH5rQN`?;FUwn+|0aYXw_C&Yh-6-9p2S_PNg zRbQn=+mtyb_l|BtorNwYZw*Eap^s+So(m zIK={e5Mjg^pA%-@kdh2RdQ#M;8y^56j`0khUYtHC5{HO}X@oD?+jlNPYm*&=<(4U$psqeBn8wexZo~XaCos^&G{?HO01_r z+tjS9v&w_Be@@rw%fo^T@ZM(}$9 zPOe86>ocm`eAJj(-(9iimGLx?&dYq(!9yyopq6s-(Ah^E37{%BNV$GNjP^Ro5D~+6 z!o|tALKZ!}H;vnr!cuq_ovTKuwyna@@1^h8Him%i^j5de@Z`4GwsMzpoym>9G{ib`_KRM?E`tc5}Y#%G5zYt3?Nk%-mJKg(A4m4mw?XO8dL7*om` zR?aBH?)K&Q`72)7WjVR zZJKdogBubavcp1$ecCXb@FUW5_3b4s18!$L<6Jwv1Cr%WXyi0|7R*gyV#!Kq`ZV(5lm{{ZbeQSsW(C`m#=)_|%zgft(mD~1>r)EU?Y9JGbJgfmU9Ua5 z<}JtBis=T}Szx%%h~h8txrZy5s^H0Dx&^}Kjxz{Ln_YFxkJEe(XROrv9NpR?_K6T# zJiJ^P1cxy# zV1=gm-i@{$6H~-rj&GSfCSF<&-=Zop{=*FTL(@plB{6|-Qdo*uS-#XPKkrufKBiVA zRaAqiO0c{ONW$s&!k+Kt7bRj!JnKARZW?to>@a4VO}aoCK7-}?5%)8`DJ3*pQ|(#! zOu=u*IsLZ@Q=yQV+P_^P#~q1mJnx@PHe+Y=p_c4>DE|6Sy_z`pq9EUKlDz7~IT@Ou zZX~J;v#Okt3+c}G8XB(iQ#(>||Ac2&T4}21a5R8`pHNL?&ZZ}2<@DL!Vg1IZTs#J} z%Be%L`6RdURbSe!TjHsGT{@PTTZc`>dlzIyvzAGR(61Oe8w}xP%zjSs`;5RGE>1{i zruV$tx(j_Eh_NKpU72i|&b4p`_mtn7SGwO`N~h4ShHBUA{CyYn424M!RFZJjVR*%@aM8uf87=c_gwlyP4z9sdef_w*opIW#q1q(F;5N2w1Z|KkG_F2>Nu95z+Gy=!C+0e2C2L5%2&6ahzQdYXh{)qb# z#IH7DdVd+pEHH5nya%<6|)X%YzyG7riudui!BoVz@ za{`J{cMVcJ#Vk6wd^UeG6rr;0bV}_R%k)!w1JByWH}w`)YzVv~H{f&J2D}t3`#Iar z;u4R^$GbR7$HL7m7T@6@aX`aCYbkuiJ94IyK!ew;r2E+OGrxen69mqj z&wh^Ef=9v$S_%Zl9m-~g#&U0t?X|3%$)|0&LxLqV2a|q0MnvG}_yQ4K&Tf-6?E^=r zpGVr#sZ}iHGbPwzF_#?)-hqo$SL7}rLgZEw@Du35Gns--)nG%XhDcok4hMJ zp~K!AD!c*W%%?N!usgXv#>i8hz&TI!i`KRkAGgC9T6a1de&23)7yX`tdM_@2D>2Q& zJkerGDc=0%1p{=--hEwSzSr&aq-~o3aKe*pdsX)avyGqD<}5tuhjlkNzL3cWuUyS< z;xyupl1L6F?9~N?RW=PSjpd8bbLwe0T=)!SY-x+m23YSS`=n zpQqhab7m>Pi6k+&u1RE^VY22TE&AB+B{GoqFGocfNb2kVek7?mtAWaX7qLa5pfZ(g zkI0@=2*&&Udk*IL#g%l21MnTZZzBeZAtBMP(mZwQj&W4&81j$^xuj~8*d_PT z;*9)yuX!c@h}o`n2zz;yo*3CH9Q{H!O)itqtP03H{2&G`C=TKG-#!qBBC7gGC zAQ!dVuaNfB*~QWSesd>g7Sn*%MNtwIa^l}U5(Y>G1-G|~igW2-Y75Bepe%0tP5tgd zok4=;BDz9i>{fiw3V*}!|1xER1zNt(-^}G`X|zn$;qa?}3o*(p_Gtp$wg>#T=_Ft5}!xv*jq_7wl{%@9UNG$P#`dqzLa3VuPkB3`ZP z@v^JY)hekr(jwD~;CYfRb-P#Jo%oj(&~88n<=ng;2{ExE$?>qNzP_Z!L{xl9c5N3! zx_o1*!QSdKpMj_no1c0_Op@-lqmP6YD{*jx1*;Z@_L(O@ApD_O$!B%C6jch@7 zd;8ZNq=D?I8`Xassj;W7VJkg&;&44!|DSoRG}kzbu{$ zAW`ZTi;!(d_xED>9~QPCPadB9k6`^j6|7gvk%aw}ro??!D9BGiMg>$YWfJ_q0G{K3 Axc~qF diff --git a/docs/images/flowchart_QCCS.png b/docs/images/flowchart_QCCS.png deleted file mode 100644 index 5dd8ec1da1d30b4551395d0f7c674975524789f8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 127840 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 E9vhHc|`iC;2f7{o(hM_3-%=| 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 diff --git a/examples/00_reference/00_session_reference.ipynb b/examples/00_reference/00_session_reference.ipynb index f58194e..4012d18 100644 --- a/examples/00_reference/00_session_reference.ipynb +++ b/examples/00_reference/00_session_reference.ipynb @@ -254,7 +254,7 @@ "id": "c6460f9b", "metadata": {}, "source": [ - "# Session instantiation" + "## Session instantiation" ] }, { @@ -279,7 +279,7 @@ "id": "c6ec7b08", "metadata": {}, "source": [ - "# Experiment compilation" + "## Experiment compilation" ] }, { @@ -327,7 +327,7 @@ "id": "edf40dd9", "metadata": {}, "source": [ - "# Experiment execution\n", + "## Experiment execution\n", "\n", "Returns a reference to the results object obtained by running this experiment" ] @@ -353,7 +353,7 @@ "id": "defb6204", "metadata": {}, "source": [ - "# Result Handling" + "## Result Handling" ] }, { @@ -371,7 +371,7 @@ "id": "fcb9cf95", "metadata": {}, "source": [ - "## Get a results object\n", + "### Get a results object\n", "containing all relevant data from the last execution" ] }, @@ -451,7 +451,7 @@ "id": "fa85988c", "metadata": {}, "source": [ - "## Compiled Experiment" + "### Compiled Experiment" ] }, { @@ -477,7 +477,7 @@ "id": "d943cf47", "metadata": {}, "source": [ - "## Acquired Results\n", + "### Acquired Results\n", "The structure of the acquired results will change. For now, it is just adapted to the current controller implementation." ] }, @@ -502,7 +502,7 @@ "id": "b3a15441", "metadata": {}, "source": [ - "## Convenience getters on Results object" + "### Convenience getters on Results object" ] }, { @@ -538,7 +538,7 @@ "id": "bceaaaf3", "metadata": {}, "source": [ - "# Serialization" + "## Serialization" ] }, { @@ -683,7 +683,7 @@ "id": "7ae9cab0", "metadata": {}, "source": [ - "# Plotting" + "## Plotting" ] }, { diff --git a/examples/00_reference/01_calibration_reference.ipynb b/examples/00_reference/01_calibration_reference.ipynb index 5e0c098..a76dc37 100644 --- a/examples/00_reference/01_calibration_reference.ipynb +++ b/examples/00_reference/01_calibration_reference.ipynb @@ -52,7 +52,7 @@ "\n", "This baseline calibration can be overridden non-destructively with calibration on an `Experiment`. If a `SignalCalibration` is defined i.e. not `None` on an `ExperimentSignal`, then actual values from that `SignalCalibration` are considered while the corresponding values in the `SignalCalibration` on the `LogicalSignal` are ignored and left unmodified. If there are values in the `SignalCalibration` on the `ExperimentSignal` that are set to `None`, these values are not considered and the corresponding values in the baseline `SignalCalibration` on the corresponding `LogicalSignal` remain effective. \n", "\n", - "For example, the oscillator defined for a `LogicalSignal` can be overriden by an oscillator in the corresponding `ExperimentSignals`'s `SignalCalibration`. If this `SignalCalibration` only defines an oscillator but leaves all other values to `None`, only this oscillator will override the baseline oscillator. All values set to `None` e.g. like the mixer calibration values will leave the baseline values effective." + "For example, the oscillator defined for a `LogicalSignal` can be overridden by an oscillator in the corresponding `ExperimentSignals`'s `SignalCalibration`. If this `SignalCalibration` only defines an oscillator but leaves all other values to `None`, only this oscillator will override the baseline oscillator. All values set to `None` e.g. like the mixer calibration values will leave the baseline values effective." ] }, { @@ -175,7 +175,7 @@ "source": [ "## Calibrate `DeviceSetup`\n", "\n", - "Below, you'll use different methods to use calibrations, either by setting the calibration directly to the `DeviceSetup` or by ceating a `Calibration` object." + "Below, you'll use different methods to use calibrations, either by setting the calibration directly to the `DeviceSetup` or by creating a `Calibration` object." ] }, { diff --git a/examples/00_reference/02_experiment_definition_reference.ipynb b/examples/00_reference/02_experiment_definition_reference.ipynb index df5cb5c..65b46a1 100644 --- a/examples/00_reference/02_experiment_definition_reference.ipynb +++ b/examples/00_reference/02_experiment_definition_reference.ipynb @@ -16,7 +16,7 @@ "id": "5c0205b5", "metadata": {}, "source": [ - "# Imports, device setup, and example experiment\n", + "## Imports, device setup, and example experiment\n", "\n", "You'll start by importing LabOne Q, defining your descriptor, providing a calibration, and creating a device setup." ] @@ -203,7 +203,7 @@ "id": "9a712bcc", "metadata": {}, "source": [ - "## Example experiment\n", + "### Example experiment\n", "\n", "After creating your device setup, you can run an example experiment, e.g., this resonator spectroscopy." ] @@ -300,7 +300,7 @@ "id": "c6460f9b", "metadata": {}, "source": [ - "# Experiment Instantiation\n", + "## Experiment Instantiation\n", "\n", "You'll now create an experiment from scratch, going through each step." ] @@ -419,7 +419,7 @@ "id": "91e39f06", "metadata": {}, "source": [ - "# Sections" + "## Sections" ] }, { @@ -428,7 +428,7 @@ "id": "7134963f", "metadata": {}, "source": [ - "## Simple Section" + "### Simple Section" ] }, { @@ -457,7 +457,7 @@ "id": "7a33f47f", "metadata": {}, "source": [ - "## Access Current Section" + "### Access Current Section" ] }, { @@ -487,7 +487,7 @@ "id": "7f6d8fb3", "metadata": {}, "source": [ - "## Sweep Section" + "### Sweep Section" ] }, { @@ -566,7 +566,7 @@ "id": "8fc40e0b", "metadata": {}, "source": [ - "## Averaging Acquire Loop in near time" + "### Averaging Acquire Loop in near time" ] }, { @@ -594,7 +594,7 @@ "id": "fd8bb93d", "metadata": {}, "source": [ - "## Averaging Acquire Loop in real time" + "### Averaging Acquire Loop in real time" ] }, { @@ -626,7 +626,7 @@ "id": "eef26c14", "metadata": {}, "source": [ - "# Operations in near time" + "## Operations in near time" ] }, { @@ -635,7 +635,7 @@ "id": "feeffe27", "metadata": {}, "source": [ - "## Callback Function" + "### Callback Function" ] }, { @@ -659,7 +659,7 @@ "id": "54826045", "metadata": {}, "source": [ - "## Set node parameter in Zurich Instruments devices" + "### Set node parameter in Zurich Instruments devices" ] }, { @@ -683,7 +683,7 @@ "id": "b05e9379", "metadata": {}, "source": [ - "# Operations in real time" + "## Operations in real time" ] }, { @@ -692,7 +692,7 @@ "id": "0888e471", "metadata": {}, "source": [ - "## Play Pulses" + "### Play Pulses" ] }, { @@ -720,7 +720,7 @@ "id": "bfdea064", "metadata": {}, "source": [ - "## Acquire Signals" + "### Acquire Signals" ] }, { @@ -760,7 +760,7 @@ "id": "94b521ca", "metadata": {}, "source": [ - "## Delay" + "### Delay" ] }, { @@ -785,7 +785,7 @@ "id": "1f40cceb", "metadata": {}, "source": [ - "## Signal Reservation" + "### Signal Reservation" ] }, { diff --git a/examples/00_reference/03_section_tutorial.ipynb b/examples/00_reference/03_section_tutorial.ipynb index 21ae541..1380baa 100644 --- a/examples/00_reference/03_section_tutorial.ipynb +++ b/examples/00_reference/03_section_tutorial.ipynb @@ -1071,7 +1071,7 @@ "source": [ "Now we'll implement a full Ramsey sequence using one drive line, a measurement line, and a acquisition line.\n", "\n", - "We'll first define some new pulses along with sweeping and averaging paramters." + "We'll first define some new pulses along with sweeping and averaging parameters." ] }, { diff --git a/examples/00_reference/05_pulse_inspector_plotter.ipynb b/examples/00_reference/05_pulse_inspector_plotter.ipynb index 9aa5f16..20c3643 100644 --- a/examples/00_reference/05_pulse_inspector_plotter.ipynb +++ b/examples/00_reference/05_pulse_inspector_plotter.ipynb @@ -28,7 +28,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Inversion with gaussian pulse" + "## Inversion with gaussian pulse" ] }, { @@ -89,7 +89,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Inversion with flat-top Gaussian pulse" + "## Inversion with flat-top Gaussian pulse" ] }, { @@ -151,7 +151,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Inversion with DRAG pulse" + "## Inversion with DRAG pulse" ] }, { diff --git a/examples/00_reference/06_user_functions.ipynb b/examples/00_reference/06_user_functions.ipynb index fc28105..50bc3e6 100644 --- a/examples/00_reference/06_user_functions.ipynb +++ b/examples/00_reference/06_user_functions.ipynb @@ -24,7 +24,7 @@ "id": "7cc49c84", "metadata": {}, "source": [ - "# 0. General Imports and Definitions" + "## 0. General Imports and Definitions" ] }, { @@ -33,7 +33,7 @@ "id": "9a007a23", "metadata": {}, "source": [ - "## 0.1 Python Imports" + "### 0.1 Python Imports" ] }, { @@ -50,7 +50,7 @@ "from laboneq.contrib.example_helpers.plotting.plot_helpers import plot_simulation\n", "\n", "import numpy as np\n", - "import matplotlib.pyplot as plt\n" + "import matplotlib.pyplot as plt" ] }, { @@ -59,7 +59,7 @@ "id": "26c88141", "metadata": {}, "source": [ - "# 1. Define Device Setup and Calibration" + "## 1. Define Device Setup and Calibration" ] }, { @@ -68,7 +68,7 @@ "id": "2a0751ec", "metadata": {}, "source": [ - "## 1.1 Define a Device Setup\n", + "### 1.1 Define a Device Setup\n", "\n", "The descriptor contains all information on instruments used, internal connections between instruments as well as wiring to the experiment" ] @@ -113,7 +113,7 @@ " device_pqsc:\n", " - to: device_hdawg\n", " port: ZSYNCS/0\n", - "\"\"\"\n" + "\"\"\"" ] }, { @@ -122,7 +122,7 @@ "id": "9bd1917c", "metadata": {}, "source": [ - "## 1.2 Define Calibration Settings\n", + "### 1.2 Define Calibration Settings\n", "\n", "Modify the calibration on the device setup with known parameters for qubit control and readout - qubit control and readout frequencies, mixer calibration corrections" ] @@ -177,7 +177,7 @@ " oscillator=Oscillator(\n", " uid=\"acquire_osc\", frequency=1e8, modulation_type=ModulationType.SOFTWARE\n", " ),\n", - " )\n" + " )" ] }, { @@ -186,7 +186,7 @@ "id": "ed80e075", "metadata": {}, "source": [ - "## 1.3 Create Device Setup and Apply Calibration Settings" + "### 1.3 Create Device Setup and Apply Calibration Settings" ] }, { @@ -213,7 +213,7 @@ "\n", "\n", "# create device setup\n", - "device_setup = create_device_setup()\n" + "device_setup = create_device_setup()" ] }, { @@ -222,7 +222,7 @@ "id": "28d93015", "metadata": {}, "source": [ - "# 2. User functions\n", + "## 2. User functions\n", "\n", "First, the user functions are defined. User functions can contain name arguments and return values. Then, the user functions are used in a near-time sweep in an example experiment." ] @@ -233,7 +233,7 @@ "id": "e13deef6-9cd4-486b-bd58-46f54c8c5c35", "metadata": {}, "source": [ - "## 2.1 Define pulses" + "### 2.1 Define pulses" ] }, { @@ -251,7 +251,7 @@ "# readout weights for integration\n", "readout_weighting_function = pulse_library.const(\n", " uid=\"readout_weighting_function\", length=200e-9, amplitude=1.0\n", - ")\n" + ")" ] }, { @@ -260,7 +260,7 @@ "id": "6d973230", "metadata": {}, "source": [ - "## 2.2 Define user functions\n", + "### 2.2 Define user functions\n", "User functions are normal Python functions, but their first argument must be the `session` object. This enables access to all QCCS instruments and the results that have already been collected. The return values will be stored in the `session.results` object." ] }, @@ -292,7 +292,7 @@ " mylogger.info(\n", " f\"Called 'my_power_func' with params: amplitude={amplitude:.1f}, gain={gain}\"\n", " )\n", - " return amplitude, (amplitude * gain) ** 2\n" + " return amplitude, (amplitude * gain) ** 2" ] }, { @@ -301,7 +301,7 @@ "id": "05eba49d", "metadata": {}, "source": [ - "### 2.2.1 Controlling individual devices\n", + "#### 2.2.1 Controlling individual devices\n", "\n", "Configured devices can be controlled via `zhinst-toolkit` API." ] @@ -322,7 +322,7 @@ " amplitudes = device_hdawg.awgs[\"*\"].outputs[\"*\"].amplitude()\n", " gains = device_hdawg.awgs[\"*\"].outputs[\"*\"].gains[\"*\"]()\n", " awg_osc_freq = device_hdawg.oscs[\"*\"].freqawg()\n", - " return amplitudes, gains, awg_osc_freq\n" + " return amplitudes, gains, awg_osc_freq" ] }, { @@ -359,7 +359,7 @@ " res = inner_results.copy()\n", " inner_results.clear()\n", " multiplier[0] = multiplier[0] * 2\n", - " return res\n" + " return res" ] }, { @@ -391,7 +391,7 @@ " if np.abs(m) > 0.5:\n", " session.replace_pulse(\"x90\", x90_rect)\n", " else:\n", - " session.replace_pulse(\"x90\", x90)\n" + " session.replace_pulse(\"x90\", x90)" ] }, { @@ -400,7 +400,7 @@ "id": "833a8491", "metadata": {}, "source": [ - "## 2.3 Experiment definition" + "### 2.3 Experiment definition" ] }, { @@ -416,7 +416,7 @@ "lsg[\"drive_line\"].calibration.oscillator.frequency = 100e6\n", "lsg[\"measure_line\"].calibration.oscillator.frequency = 100e6\n", "lsg[\"drive_line\"].calibration.amplitude = 0.5\n", - "lsg[\"acquire_line\"].calibration.port_delay = 100e-9\n" + "lsg[\"acquire_line\"].calibration.port_delay = 100e-9" ] }, { @@ -440,7 +440,7 @@ "inner_arbitrary_sweep = SweepParameter(uid=\"inner\", values=[1, 1.1, 3.5, 7])\n", "\n", "# define number of averages\n", - "average_exponent = 1 # used for 2^n averages, n=average_exponent, maximum: n = 17\n" + "average_exponent = 1 # used for 2^n averages, n=average_exponent, maximum: n = 17" ] }, { @@ -523,7 +523,7 @@ " exp.delay(signal=\"q0_measure\", time=1e-6)\n", "\n", " # The call order of user functions is preserved relative to the nested sections\n", - " exp.call(\"after_inner_user_func\")\n" + " exp.call(\"after_inner_user_func\")" ] }, { @@ -545,7 +545,7 @@ "}\n", "\n", "# set signal map\n", - "exp.set_signal_map(map_q0)\n" + "exp.set_signal_map(map_q0)" ] }, { @@ -554,7 +554,7 @@ "id": "a1b00c22", "metadata": {}, "source": [ - "## 2.3 Run the Experiment" + "### 2.3 Run the Experiment" ] }, { @@ -565,7 +565,7 @@ "outputs": [], "source": [ "# create a session\n", - "session = Session(device_setup)\n" + "session = Session(device_setup)" ] }, { @@ -597,7 +597,7 @@ "session.register_user_function(inner_user_func, \"inner_user_func\")\n", "session.register_user_function(after_inner_user_func, \"after_inner_user_func\")\n", "\n", - "session.register_user_function(process_partial_result)\n" + "session.register_user_function(process_partial_result)" ] }, { @@ -619,7 +619,7 @@ "# connect to session\n", "session.connect(do_emulation=do_emulation)\n", "# run experiment\n", - "my_results = session.run(exp)\n" + "my_results = session.run(exp)" ] }, { @@ -630,7 +630,7 @@ "outputs": [], "source": [ "# Plot simulated output signals\n", - "plot_simulation(session.compiled_experiment, 0, 3e-6)\n" + "plot_simulation(session.compiled_experiment, 0, 3e-6)" ] }, { @@ -639,7 +639,7 @@ "id": "c9ef5231", "metadata": {}, "source": [ - "## 2.4 Results\n", + "### 2.4 Results\n", "\n", "Investigate the results returned from the calls of user functions" ] @@ -652,7 +652,7 @@ "outputs": [], "source": [ "# Return values of user functions upon execution are available per function, use function name as a key.\n", - "my_results.user_func_results[\"user_func\"]\n" + "my_results.user_func_results[\"user_func\"]" ] }, { @@ -663,7 +663,7 @@ "outputs": [], "source": [ "# Two calls per iteration to `calc_power` result in two adjacent entries in the results\n", - "my_results.user_func_results[\"calc_power\"]\n" + "my_results.user_func_results[\"calc_power\"]" ] }, { @@ -673,7 +673,7 @@ "metadata": {}, "outputs": [], "source": [ - "plt.scatter(*zip(*my_results.user_func_results[\"calc_power\"]))\n" + "plt.scatter(*zip(*my_results.user_func_results[\"calc_power\"]))" ] }, { @@ -683,7 +683,7 @@ "metadata": {}, "outputs": [], "source": [ - "my_results.user_func_results[\"calc_power_alt\"]\n" + "my_results.user_func_results[\"calc_power_alt\"]" ] }, { @@ -693,7 +693,7 @@ "metadata": {}, "outputs": [], "source": [ - "plt.scatter(*zip(*my_results.user_func_results[\"calc_power_alt\"]))\n" + "plt.scatter(*zip(*my_results.user_func_results[\"calc_power_alt\"]))" ] }, { @@ -703,7 +703,7 @@ "metadata": {}, "outputs": [], "source": [ - "my_results.user_func_results[\"after_inner_user_func\"]\n" + "my_results.user_func_results[\"after_inner_user_func\"]" ] }, { @@ -713,7 +713,7 @@ "metadata": {}, "outputs": [], "source": [ - "my_results.user_func_results[\"query_hdawg_device_info\"]\n" + "my_results.user_func_results[\"query_hdawg_device_info\"]" ] } ], diff --git a/examples/00_reference/07_waveform_replacement.ipynb b/examples/00_reference/07_waveform_replacement.ipynb index 289073e..9031cb7 100644 --- a/examples/00_reference/07_waveform_replacement.ipynb +++ b/examples/00_reference/07_waveform_replacement.ipynb @@ -41,7 +41,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# 1. Create Device Setup" + "## 1. Create Device Setup" ] }, { @@ -72,7 +72,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# 2. Create and Connect to a QCCS Session " + "## 2. Create and Connect to a QCCS Session " ] }, { @@ -94,7 +94,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# 3. Pulse Exchange Experiment" + "## 3. Pulse Exchange Experiment" ] }, { @@ -102,9 +102,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 3.1 Pulse Definitions\n", + "### 3.1 Pulse Definitions\n", "\n", - "Below, we define the pulse defnitions to be used in the experiment. The only restriction is that pulses must be of the same length of those that they are replacing." + "Below, we define the pulse definitions to be used in the experiment. The only restriction is that pulses must be of the same length of those that they are replacing." ] }, { @@ -155,7 +155,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 3.2 Replace pulse function\n", + "### 3.2 Replace pulse function\n", "\n", "Below, we define the function to replace our pulse with another." ] @@ -192,7 +192,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 3.3 Experiment definition\n", + "### 3.3 Experiment definition\n", "\n", "In our experiment, we increase a index (`instance_idx`) where, once the index increases over the threshold set in the above user function, once pulse is replaced with another." ] @@ -252,7 +252,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 3.4 Compilation" + "### 3.4 Compilation" ] }, { @@ -273,7 +273,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 3.5 Simulation\n", + "### 3.5 Simulation\n", "\n", "Here, the simulation shows the first real-time pulse sequence, before the pulses are replaced using our user function." ] @@ -292,7 +292,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 3.6 Create pulse sheet" + "### 3.6 Create pulse sheet" ] }, { @@ -310,7 +310,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 3.7 Run experiment" + "### 3.7 Run experiment" ] }, { diff --git a/examples/00_reference/09_output_simulator.ipynb b/examples/00_reference/09_output_simulator.ipynb index fbcf8c9..01688cb 100644 --- a/examples/00_reference/09_output_simulator.ipynb +++ b/examples/00_reference/09_output_simulator.ipynb @@ -32,7 +32,7 @@ "from laboneq.contrib.example_helpers.example_notebook_helper import create_device_setup\n", "\n", "import numpy as np\n", - "import matplotlib.pyplot as plt\n" + "import matplotlib.pyplot as plt" ] }, { @@ -47,7 +47,7 @@ "# create device setup\n", "generation = 2\n", "device_setup = create_device_setup(generation=generation)\n", - "use_emulation = True\n" + "use_emulation = True" ] }, { @@ -81,7 +81,7 @@ "# readout weights for integration\n", "readout_weighting_function = pulse_library.const(\n", " uid=\"readout_weighting_function\", length=400e-9, amplitude=1.0\n", - ")\n" + ")" ] }, { @@ -105,7 +105,7 @@ "amp_sweep = LinearSweepParameter(uid=\"amp\", start=1.0, stop=0.1, count=11)\n", "\n", "# define number of averages\n", - "average_exponent = 2 # used for 2^n averages, n=average_exponent, maximum: n = 17\n" + "average_exponent = 2 # used for 2^n averages, n=average_exponent, maximum: n = 17" ] }, { @@ -154,7 +154,7 @@ " signal=\"q0_acquire\",\n", " handle=\"ac_0\",\n", " kernel=readout_weighting_function,\n", - " )\n" + " )" ] }, { @@ -177,7 +177,7 @@ "}\n", "\n", "# set signal map\n", - "exp.set_signal_map(map_q0)\n" + "exp.set_signal_map(map_q0)" ] }, { @@ -195,7 +195,7 @@ "session.connect(do_emulation=use_emulation)\n", "\n", "# Compile experiment\n", - "compiled_experiment = session.compile(exp)\n" + "compiled_experiment = session.compile(exp)" ] }, { @@ -216,7 +216,7 @@ }, "outputs": [], "source": [ - "show_pulse_sheet(\"Amplitude Rabi\", compiled_experiment)\n" + "show_pulse_sheet(\"Amplitude Rabi\", compiled_experiment)" ] }, { @@ -236,7 +236,7 @@ "# interactive_psv(compiled_experiment)\n", "\n", "# Can also be used without inlining in Jupyter Notebook\n", - "# interactive_psv(compiled_experiment, inline=False)\n" + "# interactive_psv(compiled_experiment, inline=False)" ] }, { @@ -249,7 +249,7 @@ "\n", "You can easily plot the signals with the aid of one of our helper functions. \n", "\n", - "For more customized ploting, follow four simple steps to retrieve and plot the simulated output signals:\n", + "For more customized plotting, follow four simple steps to retrieve and plot the simulated output signals:\n", "\n", "1. Initialize the output simulator with the `CompiledExperiment` object\n", "2. Retrieve the physical channels that you are interested in.\n", @@ -263,7 +263,7 @@ "id": "f9379d28", "metadata": {}, "source": [ - "### Initalize the `OutputSimulator` and plot everything with a helper function" + "### Initialize the `OutputSimulator` and plot everything with a helper function" ] }, { @@ -284,7 +284,7 @@ " length=5e-6,\n", " plot_width=10,\n", " plot_height=3,\n", - ")\n" + ")" ] }, { @@ -309,7 +309,7 @@ "drive_iq_port = qb[\"drive_line\"].physical_channel\n", "flux_rf_port = qb[\"flux_line\"].physical_channel\n", "measure_iq_port = qb[\"measure_line\"].physical_channel\n", - "acquire_port = qb[\"acquire_line\"].physical_channel\n" + "acquire_port = qb[\"acquire_line\"].physical_channel" ] }, { @@ -343,7 +343,7 @@ "\n", "acquire_snippet = simulation.get_snippet(\n", " acquire_port, start=200e-9, output_length=400e-9\n", - ")\n" + ")" ] }, { @@ -373,7 +373,7 @@ "plt.plot(measure_snippet.time, measure_snippet.wave.real, label=\"measure I\")\n", "plt.plot(measure_snippet.time, measure_snippet.wave.imag, label=\"measure Q\")\n", "plt.plot(acquire_snippet.time, acquire_snippet.wave.real, label=\"acquire start\")\n", - "plt.legend()\n" + "plt.legend()" ] }, { @@ -407,7 +407,7 @@ " plt.plot(drive_snippet.time - start, drive_snippet.wave.real, color=\"tab:blue\")\n", " plt.plot(\n", " measure_snippet.time - start, measure_snippet.wave.real, color=\"tab:orange\"\n", - " )\n" + " )" ] }, { diff --git a/examples/00_reference/10_database_interface.ipynb b/examples/00_reference/10_database_interface.ipynb index bf6722a..621daf5 100755 --- a/examples/00_reference/10_database_interface.ipynb +++ b/examples/00_reference/10_database_interface.ipynb @@ -5,7 +5,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Database use with LabOne Q\n", + "# Database use with LabOne Q\n", "\n", "LabOne Q comes with a database interface to make it easier to store and retrieve any data or experiments. It currently supports the SQLite implementation native to Python.\n", "\n", @@ -22,7 +22,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# 0. Python imports" + "## 0. Python imports" ] }, { @@ -39,7 +39,7 @@ "from PIL import Image as PILImage\n", "\n", "# convenience import for all LabOne Q software functionality\n", - "from laboneq.simple import *\n" + "from laboneq.simple import *" ] }, { @@ -47,7 +47,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# 1. Creating and connecting to a database" + "## 1. Creating and connecting to a database" ] }, { @@ -64,7 +64,7 @@ "metadata": {}, "outputs": [], "source": [ - "my_db = DataStore()\n" + "my_db = DataStore()" ] }, { @@ -83,7 +83,7 @@ "source": [ "custom_db_path = \"laboneq_data/custom_database.db\"\n", "\n", - "my_custom_db = DataStore(custom_db_path)\n" + "my_custom_db = DataStore(custom_db_path)" ] }, { @@ -91,7 +91,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# 2. Saving a LabOne Q data object in the database" + "## 2. Saving a LabOne Q data object in the database" ] }, { @@ -108,7 +108,7 @@ "metadata": {}, "outputs": [], "source": [ - "[my_db.get(k, with_metadata=True) for k in my_db.keys()]\n" + "[my_db.get(k, with_metadata=True) for k in my_db.keys()]" ] }, { @@ -127,7 +127,7 @@ "source": [ "exp = Experiment(\"my_experiment\", signals=[ExperimentSignal(uid=\"signal_1\")])\n", "\n", - "my_db.store(exp, key=\"my_experiment\")\n" + "my_db.store(exp, key=\"my_experiment\")" ] }, { @@ -152,7 +152,7 @@ " \"creation_date\": datetime.datetime.now(),\n", " \"setup\": \"CountZero\",\n", " },\n", - ")\n" + ")" ] }, { @@ -177,7 +177,7 @@ " \"creation_date\": datetime.datetime(year=2021, month=4, day=20),\n", " \"setup\": \"CountZero\",\n", " },\n", - ")\n" + ")" ] }, { @@ -209,7 +209,7 @@ " \"result_plot_png_bytes\": image_bytes,\n", " \"creation_date\": datetime.datetime.now(),\n", " },\n", - ")\n" + ")" ] }, { @@ -217,7 +217,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# 3. Accessing the stored data by key and through metadata" + "## 3. Accessing the stored data by key and through metadata" ] }, { @@ -234,7 +234,7 @@ "metadata": {}, "outputs": [], "source": [ - "[my_db.get(k, with_metadata=True) for k in my_db.keys()]\n" + "[my_db.get(k, with_metadata=True) for k in my_db.keys()]" ] }, { @@ -251,7 +251,7 @@ "metadata": {}, "outputs": [], "source": [ - "list(my_db.keys())\n" + "list(my_db.keys())" ] }, { @@ -269,7 +269,7 @@ "outputs": [], "source": [ "for k in my_db.keys():\n", - " print(k, my_db.get_metadata(k))\n" + " print(k, my_db.get_metadata(k))" ] }, { @@ -288,7 +288,7 @@ "source": [ "count_zero_keys = my_db.find(metadata={\"setup_name\": \"CountZero\"})\n", "for k in count_zero_keys:\n", - " print(k, my_db.get(k, with_metadata=True))\n" + " print(k, my_db.get(k, with_metadata=True))" ] }, { @@ -311,7 +311,7 @@ ")\n", "\n", "for k in new_data:\n", - " print(k, my_db.get(k, with_metadata=True))\n" + " print(k, my_db.get(k, with_metadata=True))" ] }, { @@ -332,7 +332,7 @@ " io.BytesIO(my_db.get_metadata(\"experiment_with_image\")[\"result_plot_png_bytes\"])\n", ")\n", "\n", - "im.show()\n" + "im.show()" ] }, { @@ -340,7 +340,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# 4. Deleting data from the database" + "## 4. Deleting data from the database" ] }, { @@ -359,7 +359,7 @@ "source": [ "my_db.delete(\"experiment_with_image\")\n", "my_db.delete(\"my_experiment\")\n", - "my_db.delete(\"my_old_experiment\")\n" + "my_db.delete(\"my_old_experiment\")" ] }, { diff --git a/examples/01_qubit_characterization/00_readout_raw_data.ipynb b/examples/01_qubit_characterization/00_readout_raw_data.ipynb index 95541e7..53178d6 100644 --- a/examples/01_qubit_characterization/00_readout_raw_data.ipynb +++ b/examples/01_qubit_characterization/00_readout_raw_data.ipynb @@ -17,7 +17,7 @@ "id": "2091d81c", "metadata": {}, "source": [ - "# 0. Python Imports" + "## 0. Python Imports" ] }, { @@ -36,7 +36,7 @@ "from laboneq.simple import *\n", "\n", "import numpy as np\n", - "import matplotlib.pyplot as plt\n" + "import matplotlib.pyplot as plt" ] }, { @@ -45,16 +45,9 @@ "id": "91e842db", "metadata": {}, "source": [ - "# 1. Define Device Setup and Calibration" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "4e01d7e4", - "metadata": {}, - "source": [ - "## 1.1 Device Setup\n", + "## 1. Define Device Setup and Calibration\n", + "\n", + "### 1.1 Device Setup\n", "\n", "This device setup contains both an UHFQA and a SHFQA in order to demonstrate raw readout trace access for both device types" ] @@ -111,7 +104,7 @@ " port: ZSYNCS/0\n", " - to: device_shfqa\n", " port: ZSYNCS/1\n", - "\"\"\"\n" + "\"\"\"" ] }, { @@ -120,7 +113,7 @@ "id": "8129d20c", "metadata": {}, "source": [ - "## 1.2 Calibration" + "### 1.2 Calibration" ] }, { @@ -199,7 +192,7 @@ " ),\n", " # delay between readout pulse and start of signal integration\n", " port_delay=150e-9,\n", - " )\n" + " )" ] }, { @@ -208,7 +201,7 @@ "id": "0cd675b0", "metadata": {}, "source": [ - "## 1.3 Create device setup" + "### 1.3 Create device setup" ] }, { @@ -231,7 +224,7 @@ " setup_name=\"my_QCCS_setup\",\n", " )\n", " calibrate_devices(device_setup)\n", - " return device_setup\n" + " return device_setup" ] }, { @@ -240,20 +233,13 @@ "id": "5f9e26d9", "metadata": {}, "source": [ - "# 2. Readout raw time traces with a UHFQA or an SHFQA\n", + "## 2. Readout raw time traces with a UHFQA or an SHFQA\n", "\n", "readout raw integrsation traces for two situations - qubit in groundstate and qubit in excited state\n", "\n", - "difference in raw traces can be used for readout weight optimisation" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "aae294c3", - "metadata": {}, - "source": [ - "## 2.1 Define the Experiment" + "difference in raw traces can be used for readout weight optimisation\n", + "\n", + "### 2.1 Define the Experiment" ] }, { @@ -278,7 +264,7 @@ "# readout integration weights - here simple square pulse, i.e. same weights at all times\n", "readout_weighting_function = pulse_library.const(\n", " uid=\"readout_weighting_function\", length=200e-9, amplitude=1.0\n", - ")\n" + ")" ] }, { @@ -317,7 +303,7 @@ " )\n", " # delay section - to facilitate signal processing\n", " with exp_0.section(uid=\"relax\"):\n", - " exp_0.delay(signal=\"measure\", time=1e-6)\n" + " exp_0.delay(signal=\"measure\", time=1e-6)" ] }, { @@ -355,7 +341,7 @@ " )\n", " # delay section - to facilitate signal processing\n", " with exp_1.section(uid=\"relax\"):\n", - " exp_1.delay(signal=\"measure\", time=1e-6)\n" + " exp_1.delay(signal=\"measure\", time=1e-6)" ] }, { @@ -370,7 +356,7 @@ " \"drive\": \"/logical_signal_groups/q0/drive_line\",\n", " \"measure\": \"/logical_signal_groups/q0/measure_line\",\n", " \"acquire\": \"/logical_signal_groups/q0/acquire_line\",\n", - "}\n" + "}" ] }, { @@ -394,7 +380,7 @@ "\n", "# run the second experiment and access the data\n", "results_1 = session.run(exp_1)\n", - "raw_1 = results_1.get_data(\"ac_1\")\n" + "raw_1 = results_1.get_data(\"ac_1\")" ] }, { @@ -403,7 +389,7 @@ "id": "13d24ca1", "metadata": {}, "source": [ - "## 2.2 Plot the results" + "### 2.2 Plot the results" ] }, { @@ -427,7 +413,7 @@ "plt.plot(time, np.imag(raw_1), \"-r\")\n", "\n", "plt.xlabel(\"Time (ns)\")\n", - "plt.ylabel(\"Amplitude (a.u.)\")\n" + "plt.ylabel(\"Amplitude (a.u.)\")" ] }, { diff --git a/examples/01_qubit_characterization/01_cw_resonator_spec_shfsg_shfqa_shfqc.ipynb b/examples/01_qubit_characterization/01_cw_resonator_spec_shfsg_shfqa_shfqc.ipynb index 1303917..db635be 100644 --- a/examples/01_qubit_characterization/01_cw_resonator_spec_shfsg_shfqa_shfqc.ipynb +++ b/examples/01_qubit_characterization/01_cw_resonator_spec_shfsg_shfqa_shfqc.ipynb @@ -36,7 +36,7 @@ "from laboneq.contrib.example_helpers.example_notebook_helper import create_device_setup\n", "\n", "from pathlib import Path\n", - "import time\n" + "import time" ] }, { @@ -58,7 +58,7 @@ "outputs": [], "source": [ "device_setup = create_device_setup(generation=2)\n", - "emulate = True\n" + "emulate = True" ] }, { @@ -69,7 +69,7 @@ "source": [ "# create and connect to a session\n", "session = Session(device_setup=device_setup)\n", - "session.connect(do_emulation=emulate)\n" + "session.connect(do_emulation=emulate)" ] }, { @@ -107,7 +107,7 @@ " stop=stop_freq,\n", " count=num_points,\n", " axis_name=\"Frequency [Hz]\",\n", - " )\n" + " )" ] }, { @@ -157,7 +157,7 @@ " # holdoff time after signal acquisition\n", " exp_spec.reserve(signal=\"measure\")\n", "\n", - " return exp_spec\n" + " return exp_spec" ] }, { @@ -165,7 +165,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 3.1 Experiment Calibration and Signal Map\n", + "### 3.1 Experiment Calibration and Signal Map\n", "\n", "Before running the experiment, you'll need to set an [experiment calibration](https://docs.zhinst.com/labone_q_user_manual/concepts/experiment_calibration.html). The sweep parameter is assigned to the hardware oscillator modulating the readout resonator drive line. You'll also define and set the mapping between the experimental and logical lines." ] @@ -213,7 +213,7 @@ "\n", "# set signal calibration and signal map for experiment to qubit 0\n", "exp_spec.set_calibration(res_spec_calib(freq_sweep))\n", - "exp_spec.set_signal_map(res_spec_map(\"q0\"))\n" + "exp_spec.set_signal_map(res_spec_map(\"q0\"))" ] }, { @@ -221,7 +221,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 3.2 Compile and Generate Pulse Sheet\n", + "### 3.2 Compile and Generate Pulse Sheet\n", "\n", "Now you'll compile the experiment and generate a pulse sheet." ] @@ -236,8 +236,8 @@ "compiled_res_spec = session.compile(exp_spec)\n", "\n", "Path(\"Pulse_Sheets\").mkdir(parents=True, exist_ok=True)\n", - "# generate a pulse sheet to inspect experiment befor runtime\n", - "show_pulse_sheet(\"Pulse_Sheets/Resonator_Spectroscopy_Pulse_Sheet\", compiled_res_spec)\n" + "# generate a pulse sheet to inspect experiment before runtime\n", + "show_pulse_sheet(\"Pulse_Sheets/Resonator_Spectroscopy_Pulse_Sheet\", compiled_res_spec)" ] }, { @@ -245,7 +245,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 3.3 Run, Save, and Plot Results\n", + "### 3.3 Run, Save, and Plot Results\n", "\n", "Finally, you'll run the experiment, save, and plot the results." ] @@ -261,7 +261,7 @@ "timestamp = time.strftime(\"%Y%m%dT%H%M%S\")\n", "Path(\"Results\").mkdir(parents=True, exist_ok=True)\n", "session.save_results(f\"Results/{timestamp}_results.json\")\n", - "print(f\"File saved as Results/{timestamp}_results.json\")\n" + "print(f\"File saved as Results/{timestamp}_results.json\")" ] }, { @@ -271,7 +271,7 @@ "outputs": [], "source": [ "# plot the results\n", - "plot_results(res_spec_results, phase=True)\n" + "plot_results(res_spec_results, phase=True)" ] }, { diff --git a/examples/01_qubit_characterization/01_cw_resonator_spec_uhfqa_hdawg.ipynb b/examples/01_qubit_characterization/01_cw_resonator_spec_uhfqa_hdawg.ipynb index 28c7096..d29fd06 100644 --- a/examples/01_qubit_characterization/01_cw_resonator_spec_uhfqa_hdawg.ipynb +++ b/examples/01_qubit_characterization/01_cw_resonator_spec_uhfqa_hdawg.ipynb @@ -11,6 +11,15 @@ "In contrast to the SHF instruments, which can sweep their oscillator frequencies in real time (see [this notebook](https://github.com/zhinst/laboneq/blob/main/examples/basic_experiments.ipynb)), HDAWG and UHFQA require sweeps in near time." ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 0. LabOne Q Imports\n", + "\n", + "You'll begin by importing `laboneq.simple` and some extra helper functions to run the examples. " + ] + }, { "cell_type": "code", "execution_count": null, @@ -25,7 +34,18 @@ " plot_results,\n", " plot_simulation,\n", ")\n", - "from laboneq.contrib.example_helpers.example_notebook_helper import create_device_setup\n" + "from laboneq.contrib.example_helpers.example_notebook_helper import create_device_setup" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Device Setup\n", + "\n", + "Below, you'll create a device setup and specify to run in an emulated mode or on hardware, `emulate = True/False` respectively.\n", + "\n", + "If you run on your hardware, the [descriptor](https://docs.zhinst.com/labone_q_user_manual/concepts/set_up_equipment.html) called by `create_device_setup` should be replaced by one of your own, generally stored as a [YAML file](https://docs.zhinst.com/labone_q_user_manual/concepts/set_up_equipment.html#labone_q.func_concepts.setting_up.set_up_equipment.descriptor). Once you have this descriptor, it can be reused for all your experiments." ] }, { @@ -36,7 +56,7 @@ "source": [ "# create device setup\n", "device_setup = create_device_setup(generation=1)\n", - "use_emulation = True\n" + "use_emulation = True" ] }, { @@ -44,7 +64,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# 1. Pulsed Resonator Spectroscopy\n", + "## 2. Pulsed Resonator Spectroscopy\n", "\n", "Find the resonance frequency of the qubit readout resonator by looking at the transmission or reflection of a probe signal applied through the readout line." ] @@ -54,7 +74,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 1.1 Define the Experiment" + "### 2.1 Experiment Definition" ] }, { @@ -76,7 +96,7 @@ "average_exponent = 4 # used for 2^n averages, n=average_exponent, maximum: n = 17\n", "\n", "# Create Experiment - uses only a readout pulse and a data acquisition line\n", - "exp = Experiment(\n", + "exp_spec = Experiment(\n", " uid=\"Resonator Spectroscopy\",\n", " signals=[\n", " ExperimentSignal(\"measure\"),\n", @@ -86,26 +106,26 @@ "\n", "## experimental pulse sequence\n", "# Define an acquisition loop of type SPECTROSCOPY\n", - "with exp.sweep(uid=\"sweep\", parameter=frequency_sweep_parameter):\n", - " with exp.acquire_loop_rt(\n", + "with exp_spec.sweep(uid=\"sweep\", parameter=frequency_sweep_parameter):\n", + " with exp_spec.acquire_loop_rt(\n", " uid=\"shots\",\n", " count=pow(2, average_exponent),\n", " averaging_mode=AveragingMode.SEQUENTIAL,\n", " acquisition_type=AcquisitionType.SPECTROSCOPY,\n", " ):\n", " # readout pulse and data acquisition\n", - " with exp.section(uid=\"spectroscopy\"):\n", - " exp.play(\n", + " with exp_spec.section(uid=\"spectroscopy\"):\n", + " exp_spec.play(\n", " signal=\"measure\", pulse=pulse_library.const(length=1e-6, amplitude=1.0)\n", " )\n", - " exp.acquire(\n", + " exp_spec.acquire(\n", " signal=\"acquire\",\n", " handle=\"ac_0\",\n", " length=1e-6,\n", " )\n", " # relax time after readout - for signal processing and qubit relaxation to ground state\n", - " with exp.section(uid=\"relax\"):\n", - " exp.delay(signal=\"measure\", time=1e-6)\n" + " with exp_spec.section(uid=\"relax\"):\n", + " exp_spec.delay(signal=\"measure\", time=1e-6)" ] }, { @@ -121,7 +141,7 @@ " frequency=frequency_sweep_parameter,\n", " modulation_type=ModulationType.HARDWARE,\n", " )\n", - ")\n" + ")" ] }, { @@ -134,7 +154,7 @@ "map_q0 = {\n", " \"measure\": \"/logical_signal_groups/q0/measure_line\",\n", " \"acquire\": \"/logical_signal_groups/q0/acquire_line\",\n", - "}\n" + "}" ] }, { @@ -142,7 +162,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 1.2 Run the Experiment and Plot the Measurement Results and Pulse Sequence" + "### 2.2 Run the Experiment and Plot the Measurement Results and Pulse Sequence" ] }, { @@ -156,14 +176,14 @@ "session.connect(do_emulation=use_emulation)\n", "\n", "# set experiment calibration and signal map\n", - "exp.set_calibration(calib_q0)\n", - "exp.set_signal_map(map_q0)\n", + "exp_spec.set_calibration(calib_q0)\n", + "exp_spec.set_signal_map(map_q0)\n", "\n", "# run experiment\n", - "my_results = session.run(exp)\n", + "spec_results = session.run(exp_spec)\n", "\n", "# plot measurement results\n", - "plot_results(my_results)\n" + "plot_results(spec_results)" ] }, { @@ -173,7 +193,7 @@ "outputs": [], "source": [ "# use pulse sheet viewer to display the pulse sequence - only recommended for small number of averages and sweep steps to avoid performance issues\n", - "show_pulse_sheet(\"Resonator Spectroscopy\", session.compiled_experiment)\n" + "show_pulse_sheet(\"Resonator Spectroscopy\", session.compiled_experiment)" ] }, { @@ -181,11 +201,18 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# 2. Pulsed Qubit Spectroscopy\n", + "## 3. Pulsed Qubit Spectroscopy\n", "\n", "Find the resonance frequency of the qubit by looking at the change in resonator transmission when sweeping the frequency of a qubit excitation pulse." ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 3.1 Pulse Definitions" + ] + }, { "cell_type": "code", "execution_count": null, @@ -201,7 +228,14 @@ "# readout weights for integration\n", "readout_weighting_function = pulse_library.const(\n", " uid=\"readout_weighting_function\", length=200e-9, amplitude=1.0\n", - ")\n" + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 3.2 Experiment Definition" ] }, { @@ -223,7 +257,7 @@ "average_exponent = 4 # used for 2^n averages, n=average_exponent, maximum: n = 17\n", "\n", "# Create Experiment - no explicit mapping to qubit lines\n", - "exp = Experiment(\n", + "exp_qspec = Experiment(\n", " uid=\"Qubit Spectroscopy\",\n", " signals=[\n", " ExperimentSignal(\"drive\"),\n", @@ -232,29 +266,31 @@ " ],\n", ")\n", "## experimental pulse sequence\n", - "with exp.sweep(uid=\"sweep\", parameter=drive_frequency_sweep):\n", - " with exp.acquire_loop_rt(\n", + "with exp_qspec.sweep(uid=\"sweep\", parameter=drive_frequency_sweep):\n", + " with exp_qspec.acquire_loop_rt(\n", " uid=\"shots\",\n", " count=pow(2, average_exponent),\n", " averaging_mode=AveragingMode.SEQUENTIAL,\n", " acquisition_type=AcquisitionType.INTEGRATION,\n", " ):\n", " # qubit excitation pulse - frequency will be swept\n", - " with exp.section(uid=\"qubit_excitation\", alignment=SectionAlignment.RIGHT):\n", - " exp.play(signal=\"drive\", pulse=const_iq_100ns)\n", + " with exp_qspec.section(\n", + " uid=\"qubit_excitation\", alignment=SectionAlignment.RIGHT\n", + " ):\n", + " exp_qspec.play(signal=\"drive\", pulse=const_iq_100ns)\n", " # readout and data acquisition\n", - " with exp.section(uid=\"qubit_readout\", play_after=\"qubit_excitation\"):\n", + " with exp_qspec.section(uid=\"qubit_readout\", play_after=\"qubit_excitation\"):\n", " # play readout pulse\n", - " exp.play(signal=\"measure\", pulse=readout_pulse)\n", + " exp_qspec.play(signal=\"measure\", pulse=readout_pulse)\n", " # signal data acquisition\n", - " exp.acquire(\n", + " exp_qspec.acquire(\n", " signal=\"acquire\",\n", " handle=\"ac_0\",\n", " kernel=readout_weighting_function,\n", " )\n", " # relax time after readout - for signal processing and qubit relaxation to ground state\n", - " with exp.section(uid=\"relax\"):\n", - " exp.delay(signal=\"measure\", time=1e-6)\n" + " with exp_qspec.section(uid=\"relax\"):\n", + " exp_qspec.delay(signal=\"measure\", time=1e-6)" ] }, { @@ -277,7 +313,7 @@ " \"drive\": device_setup.logical_signal_groups[\"q0\"].logical_signals[\"drive_line\"],\n", " \"measure\": device_setup.logical_signal_groups[\"q0\"].logical_signals[\"measure_line\"],\n", " \"acquire\": device_setup.logical_signal_groups[\"q0\"].logical_signals[\"acquire_line\"],\n", - "}\n" + "}" ] }, { @@ -285,7 +321,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 2.2 Run the Experiment and Plot the Measurement Results and Pulse Sequence" + "### 3.3 Run the Experiment and Plot the Measurement Results and Pulse Sequence" ] }, { @@ -295,18 +331,18 @@ "outputs": [], "source": [ "# set calibration and signal map for qubit 0\n", - "exp.set_calibration(exp_calib)\n", - "exp.set_signal_map(map_q0)\n", + "exp_qspec.set_calibration(exp_calib)\n", + "exp_qspec.set_signal_map(map_q0)\n", "\n", "# create a session and connect to it\n", "session = Session(device_setup=device_setup)\n", "session.connect(do_emulation=use_emulation)\n", "\n", "# run experiment on qubit 0\n", - "my_results = session.run(exp)\n", + "qspec_results = session.run(exp_qspec)\n", "\n", "# plot measurement results\n", - "plot_results(my_results, phase=True)\n" + "plot_results(qspec_results, phase=True)" ] }, { @@ -316,7 +352,7 @@ "outputs": [], "source": [ "# Plot simulated output signals\n", - "plot_simulation(session.compiled_experiment, 0, 10e-6)\n" + "plot_simulation(session.compiled_experiment, 0, 10e-6)" ] } ], diff --git a/examples/01_qubit_characterization/02_pulsed_resonator_spec_shfsg_shfqa_shfqc.ipynb b/examples/01_qubit_characterization/02_pulsed_resonator_spec_shfsg_shfqa_shfqc.ipynb index 8ae1c53..69f43ae 100644 --- a/examples/01_qubit_characterization/02_pulsed_resonator_spec_shfsg_shfqa_shfqc.ipynb +++ b/examples/01_qubit_characterization/02_pulsed_resonator_spec_shfsg_shfqa_shfqc.ipynb @@ -34,7 +34,7 @@ "from laboneq.contrib.example_helpers.example_notebook_helper import create_device_setup\n", "\n", "from pathlib import Path\n", - "import time\n" + "import time" ] }, { @@ -56,7 +56,7 @@ "outputs": [], "source": [ "device_setup = create_device_setup(generation=2)\n", - "emulate = True\n" + "emulate = True" ] }, { @@ -67,7 +67,7 @@ "source": [ "# create and connect to a session\n", "session = Session(device_setup=device_setup)\n", - "session.connect(do_emulation=emulate)\n" + "session.connect(do_emulation=emulate)" ] }, { @@ -108,12 +108,12 @@ "# used for 2^num_averages, maximum: num_averages = 17\n", "num_averages = 4\n", "\n", - "# readout pulse parameters and definiation\n", + "# readout pulse parameters and definition\n", "envelope_duration = 2.048e-6\n", "envelope_rise_fall_time = 0.05e-6\n", "readout_pulse = pulse_library.gaussian_square(\n", " uid=\"readout_pulse\", length=envelope_duration, amplitude=0.9\n", - ")\n" + ")" ] }, { @@ -166,7 +166,7 @@ " # holdoff time after signal acquisition - minimum 1us required for data processing on UHFQA\n", " exp_spec_pulsed.reserve(signal=\"measure\")\n", "\n", - " return exp_spec_pulsed\n" + " return exp_spec_pulsed" ] }, { @@ -174,7 +174,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 3.1 Experiment Calibration and Signal Map\n", + "### 3.1 Experiment Calibration and Signal Map\n", "\n", "Before running the experiment, you'll need to set an [experiment calibration](https://docs.zhinst.com/labone_q_user_manual/concepts/experiment_calibration.html). The sweep parameter is assigned to the hardware oscillator modulating the readout resonator drive line. You'll also define and set the mapping between the experimental and logical lines." ] @@ -222,7 +222,7 @@ "\n", "# set signal calibration and signal map for experiment to qubit 0\n", "exp_spec_pulsed.set_calibration(res_spec_calib(freq_sweep))\n", - "exp_spec_pulsed.set_signal_map(res_spec_map(\"q0\"))\n" + "exp_spec_pulsed.set_signal_map(res_spec_map(\"q0\"))" ] }, { @@ -230,7 +230,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 3.2 Compile and Generate Pulse Sheet\n", + "### 3.2 Compile and Generate Pulse Sheet\n", "\n", "Now you'll compile the experiment and generate a pulse sheet." ] @@ -245,10 +245,10 @@ "compiled_spec_pulsed = session.compile(exp_spec_pulsed)\n", "\n", "Path(\"Pulse_Sheets\").mkdir(parents=True, exist_ok=True)\n", - "# generate a pulse sheet to inspect experiment befor runtime\n", + "# generate a pulse sheet to inspect experiment before runtime\n", "show_pulse_sheet(\n", " \"Pulse_Sheets/Resonator_Spectroscopy_Pulse_Sheet\", compiled_spec_pulsed\n", - ")\n" + ")" ] }, { @@ -256,7 +256,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 3.3 Run, Save, and Plot Results\n", + "### 3.3 Run, Save, and Plot Results\n", "\n", "Finally, you'll run the experiment, save, and plot the results." ] @@ -272,7 +272,7 @@ "timestamp = time.strftime(\"%Y%m%dT%H%M%S\")\n", "Path(\"Results\").mkdir(parents=True, exist_ok=True)\n", "session.save_results(f\"Results/{timestamp}_spec_pulsed_results.json\")\n", - "print(f\"File saved as Results/{timestamp}_spec_pulsed_results.json\")\n" + "print(f\"File saved as Results/{timestamp}_spec_pulsed_results.json\")" ] }, { @@ -282,7 +282,7 @@ "outputs": [], "source": [ "# plot the results\n", - "plot_results(spec_pulsed_results, phase=True)\n" + "plot_results(spec_pulsed_results, phase=True)" ] }, { diff --git a/examples/01_qubit_characterization/03_resonator_spec_vs_power_shfsg_shfqa_shfqc.ipynb b/examples/01_qubit_characterization/03_resonator_spec_vs_power_shfsg_shfqa_shfqc.ipynb index 5840d8f..188e3f4 100644 --- a/examples/01_qubit_characterization/03_resonator_spec_vs_power_shfsg_shfqa_shfqc.ipynb +++ b/examples/01_qubit_characterization/03_resonator_spec_vs_power_shfsg_shfqa_shfqc.ipynb @@ -38,7 +38,7 @@ "import time\n", "\n", "import matplotlib.pyplot as plt\n", - "import numpy as np\n" + "import numpy as np" ] }, { @@ -60,7 +60,7 @@ "outputs": [], "source": [ "device_setup = create_device_setup(generation=2)\n", - "emulate = True\n" + "emulate = True" ] }, { @@ -71,7 +71,7 @@ "source": [ "# create and connect to a session\n", "session = Session(device_setup=device_setup)\n", - "session.connect(do_emulation=emulate)\n" + "session.connect(do_emulation=emulate)" ] }, { @@ -115,12 +115,12 @@ "# used for 2^num_averages, maximum: num_averages = 17\n", "num_averages = 4\n", "\n", - "# readout pulse parameters and definiation\n", + "# readout pulse parameters and definition\n", "envelope_duration = 2.048e-6\n", "envelope_rise_fall_time = 0.05e-6\n", "readout_pulse = pulse_library.gaussian_square(\n", " uid=\"readout_pulse\", length=envelope_duration, amplitude=0.9\n", - ")\n" + ")" ] }, { @@ -142,7 +142,7 @@ "for i in range(len(device_setup.instruments)):\n", " if \"QA\" in str(device_setup.instruments[i]):\n", " # print(device_setup.instruments[i].address)\n", - " shfqa_address.append(device_setup.instruments[i].address)\n" + " shfqa_address.append(device_setup.instruments[i].address)" ] }, { @@ -203,7 +203,7 @@ " # holdoff time after signal acquisition - minimum 1us required for data processing on UHFQA\n", " exp_spec_amp.delay(signal=\"measure\", time=1e-6)\n", "\n", - " return exp_spec_amp\n" + " return exp_spec_amp" ] }, { @@ -211,7 +211,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 3.1 Experiment Calibration and Signal Map\n", + "### 3.1 Experiment Calibration and Signal Map\n", "\n", "Before running the experiment, you'll need to set an [experiment calibration](https://docs.zhinst.com/labone_q_user_manual/concepts/experiment_calibration.html). The sweep parameter is assigned to the hardware oscillator modulating the readout resonator drive line. You'll also define and set the mapping between the experimental and logical lines." ] @@ -262,7 +262,7 @@ ")\n", "\n", "exp_spec_amp.set_calibration(res_spec_calib(freq_sweep))\n", - "exp_spec_amp.set_signal_map(res_spec_map(\"q0\"))\n" + "exp_spec_amp.set_signal_map(res_spec_map(\"q0\"))" ] }, { @@ -270,7 +270,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 3.2 Compile and Generate Pulse Sheet\n", + "### 3.2 Compile and Generate Pulse Sheet\n", "\n", "Now you'll compile the experiment and generate a pulse sheet." ] @@ -285,8 +285,8 @@ "compiled_spec_amp = session.compile(exp_spec_amp)\n", "\n", "Path(\"Pulse_Sheets\").mkdir(parents=True, exist_ok=True)\n", - "# generate a pulse sheet to inspect experiment befor runtime\n", - "show_pulse_sheet(\"Pulse_Sheets/Spectroscopy_vs_Amplitude\", compiled_spec_amp)\n" + "# generate a pulse sheet to inspect experiment before runtime\n", + "show_pulse_sheet(\"Pulse_Sheets/Spectroscopy_vs_Amplitude\", compiled_spec_amp)" ] }, { @@ -294,7 +294,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 3.3 Run, Save, and Plot Results\n", + "### 3.3 Run, Save, and Plot Results\n", "\n", "Finally, you'll run the experiment, save, and plot the results." ] @@ -310,7 +310,7 @@ "timestamp = time.strftime(\"%Y%m%dT%H%M%S\")\n", "Path(\"Results\").mkdir(parents=True, exist_ok=True)\n", "session.save_results(f\"Results/{timestamp}_spec_amp_results.json\")\n", - "print(f\"File saved as Results/{timestamp}_spec_amp_results.json\")\n" + "print(f\"File saved as Results/{timestamp}_spec_amp_results.json\")" ] }, { @@ -319,7 +319,7 @@ "metadata": {}, "outputs": [], "source": [ - "plot_results(spec_amp_results, phase=True)\n" + "plot_results(spec_amp_results, phase=True)" ] }, { diff --git a/examples/01_qubit_characterization/04_propagation_delay.ipynb b/examples/01_qubit_characterization/04_propagation_delay.ipynb index f7aebe6..d100e4a 100644 --- a/examples/01_qubit_characterization/04_propagation_delay.ipynb +++ b/examples/01_qubit_characterization/04_propagation_delay.ipynb @@ -34,7 +34,7 @@ "from laboneq.contrib.example_helpers.example_notebook_helper import create_device_setup\n", "\n", "from pathlib import Path\n", - "import time\n" + "import time" ] }, { @@ -56,7 +56,7 @@ "outputs": [], "source": [ "device_setup = create_device_setup(generation=2)\n", - "emulate = True\n" + "emulate = True" ] }, { @@ -67,7 +67,7 @@ "source": [ "# create and connect to a session\n", "session = Session(device_setup=device_setup)\n", - "session.connect(do_emulation=emulate)\n" + "session.connect(do_emulation=emulate)" ] }, { @@ -94,12 +94,12 @@ "# used for 2^num_averages, maximum: num_averages = 17\n", "num_averages = 4\n", "\n", - "# readout pulse parameters and definiation\n", + "# readout pulse parameters and definition\n", "envelope_duration = 2.048e-6\n", "envelope_rise_fall_time = 0.05e-6\n", "readout_pulse = pulse_library.gaussian_square(\n", " uid=\"readout_pulse\", length=envelope_duration, amplitude=0.9\n", - ")\n" + ")" ] }, { @@ -109,7 +109,7 @@ "source": [ "## 3. Experiment Definition\n", "\n", - "You'll now create a function to generate your propagation delay [experiment](https://docs.zhinst.com/labone_q_user_manual/concepts/experiment.html). In this experiment, you'll pass the `delay_sweep` defined previously as an argument to the near-time [sweep section](https://docs.zhinst.com/labone_q_user_manual/concepts/averaging_sweeping.html#labone_q.func_concepts.experiment.averaging_sweeping.parameters_sweeps). Within the real-time aquisition section, you'll set use `INTEGRATION` as your acquisition type, and you'll create a [section](https://docs.zhinst.com/labone_q_user_manual/concepts/sections_and_pulses.html) containing a `play` and an `acquire` command." + "You'll now create a function to generate your propagation delay [experiment](https://docs.zhinst.com/labone_q_user_manual/concepts/experiment.html). In this experiment, you'll pass the `delay_sweep` defined previously as an argument to the near-time [sweep section](https://docs.zhinst.com/labone_q_user_manual/concepts/averaging_sweeping.html#labone_q.func_concepts.experiment.averaging_sweeping.parameters_sweeps). Within the real-time acquisition section, you'll set use `INTEGRATION` as your acquisition type, and you'll create a [section](https://docs.zhinst.com/labone_q_user_manual/concepts/sections_and_pulses.html) containing a `play` and an `acquire` command." ] }, { @@ -161,7 +161,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 3.1 Signal Map\n", + "### 3.1 Signal Map\n", "\n", "Before running the experiment, you'll define and set the mapping between the experimental and logical lines." ] @@ -187,7 +187,7 @@ "\n", "# pass sweep and pulse to experiment and apply signal map\n", "exp_prop_delay = propagation_delay(readout_pulse, delay_sweep)\n", - "exp_prop_delay.set_signal_map(res_spec_map(\"q0\"))\n" + "exp_prop_delay.set_signal_map(res_spec_map(\"q0\"))" ] }, { @@ -195,7 +195,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 3.2 Compile and Generate Pulse Sheet\n", + "### 3.2 Compile and Generate Pulse Sheet\n", "\n", "Now, you'll compile the experiment and generate a pulse sheet." ] @@ -210,8 +210,8 @@ "compiled_prop_delay = session.compile(exp_prop_delay)\n", "\n", "Path(\"Pulse_Sheets\").mkdir(parents=True, exist_ok=True)\n", - "# generate a pulse sheet to inspect experiment befor runtime\n", - "show_pulse_sheet(\"Pulse_Sheets/Propagation_Delay\", compiled_prop_delay)\n" + "# generate a pulse sheet to inspect experiment before runtime\n", + "show_pulse_sheet(\"Pulse_Sheets/Propagation_Delay\", compiled_prop_delay)" ] }, { @@ -219,7 +219,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 3.3 Run, Save, and Plot Results\n", + "### 3.3 Run, Save, and Plot Results\n", "\n", "Finally, you'll run the experiment, save, and plot the results." ] @@ -235,7 +235,7 @@ "timestamp = time.strftime(\"%Y%m%dT%H%M%S\")\n", "Path(\"Results\").mkdir(parents=True, exist_ok=True)\n", "session.save_results(f\"Results/{timestamp}_prop_delay_results.json\")\n", - "print(f\"File saved as Results/{timestamp}_prop_delay_results.json\")\n" + "print(f\"File saved as Results/{timestamp}_prop_delay_results.json\")" ] }, { @@ -244,7 +244,7 @@ "metadata": {}, "outputs": [], "source": [ - "plot_results(prop_delay_results, phase=True)\n" + "plot_results(prop_delay_results, phase=True)" ] }, { diff --git a/examples/01_qubit_characterization/05_qubit_spectroscopy.ipynb b/examples/01_qubit_characterization/05_qubit_spectroscopy.ipynb index f810a46..764628d 100644 --- a/examples/01_qubit_characterization/05_qubit_spectroscopy.ipynb +++ b/examples/01_qubit_characterization/05_qubit_spectroscopy.ipynb @@ -37,7 +37,7 @@ "from laboneq.contrib.example_helpers.example_notebook_helper import create_device_setup\n", "\n", "from pathlib import Path\n", - "import time\n" + "import time" ] }, { @@ -59,7 +59,7 @@ "outputs": [], "source": [ "device_setup = create_device_setup(generation=2)\n", - "emulate = True\n" + "emulate = True" ] }, { @@ -70,7 +70,7 @@ "source": [ "# create and connect to a session\n", "session = Session(device_setup=device_setup)\n", - "session.connect(do_emulation=emulate)\n" + "session.connect(do_emulation=emulate)" ] }, { @@ -125,7 +125,7 @@ "def create_drive_freq_sweep(qubit, start_freq, stop_freq, num_points):\n", " return LinearSweepParameter(\n", " uid=f\"drive_freq_{qubit}\", start=start_freq, stop=stop_freq, count=num_points\n", - " )\n" + " )" ] }, { @@ -135,7 +135,7 @@ "source": [ "## 3. Experiment Definition\n", "\n", - "To perform qubit spectroscopy, you'll create a function which generates an [experiment](https://docs.zhinst.com/labone_q_user_manual/concepts/experiment.html). In this experiment, you'll pass a freqency sweep parameter as an argument to the [sweep section](https://docs.zhinst.com/labone_q_user_manual/concepts/averaging_sweeping.html#labone_q.func_concepts.experiment.averaging_sweeping.parameters_sweeps). Within the sweeep section, you'll create another [section](https://docs.zhinst.com/labone_q_user_manual/concepts/sections_and_pulses.html) containing a `play` command to drive the qubit and an `play` and `acquire` commands to perform readout. Within the real-time aquisition section, you'll set use `INTEGRATION` as your acquisition type." + "To perform qubit spectroscopy, you'll create a function which generates an [experiment](https://docs.zhinst.com/labone_q_user_manual/concepts/experiment.html). In this experiment, you'll pass a frequency sweep parameter as an argument to the [sweep section](https://docs.zhinst.com/labone_q_user_manual/concepts/averaging_sweeping.html#labone_q.func_concepts.experiment.averaging_sweeping.parameters_sweeps). Within the sweeep section, you'll create another [section](https://docs.zhinst.com/labone_q_user_manual/concepts/sections_and_pulses.html) containing a `play` command to drive the qubit and an `play` and `acquire` commands to perform readout. Within the real-time acquisition section, you'll set use `INTEGRATION` as your acquisition type." ] }, { @@ -181,7 +181,7 @@ " # relax time after readout - for qubit relaxation to groundstate and signal processing\n", " exp_qspec.delay(signal=\"measure\", time=1e-6)\n", "\n", - " return exp_qspec\n" + " return exp_qspec" ] }, { @@ -189,7 +189,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 3.1 Signal Map\n", + "### 3.1 Signal Map\n", "\n", "Before running the experiment, you'll define and set the mapping between the experimental and logical lines." ] @@ -240,7 +240,7 @@ "\n", "# apply calibration and signal map for qubit 0\n", "exp_qspec.set_calibration(exp_calibration_q0)\n", - "exp_qspec.set_signal_map(signal_map_default(\"q0\"))\n" + "exp_qspec.set_signal_map(signal_map_default(\"q0\"))" ] }, { @@ -248,7 +248,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 3.2 Compile, Generate Pulse Sheet, and Plot Simulated Signals\n", + "### 3.2 Compile, Generate Pulse Sheet, and Plot Simulated Signals\n", "\n", "Now, you'll compile the experiment and generate a pulse sheet." ] @@ -263,8 +263,8 @@ "compiled_qspec = session.compile(exp_qspec)\n", "\n", "Path(\"Pulse_Sheets\").mkdir(parents=True, exist_ok=True)\n", - "# generate a pulse sheet to inspect experiment befor runtime\n", - "show_pulse_sheet(\"Pulse_Sheets/Qubit_Spectroscopy\", compiled_qspec)\n" + "# generate a pulse sheet to inspect experiment before runtime\n", + "show_pulse_sheet(\"Pulse_Sheets/Qubit_Spectroscopy\", compiled_qspec)" ] }, { @@ -281,7 +281,7 @@ "metadata": {}, "outputs": [], "source": [ - "plot_simulation(compiled_qspec)\n" + "plot_simulation(compiled_qspec)" ] }, { @@ -289,7 +289,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 3.3 Run, Save, and Plot Results\n", + "### 3.3 Run, Save, and Plot Results\n", "\n", "Finally, you'll run the experiment, save, and plot the results." ] @@ -305,7 +305,7 @@ "timestamp = time.strftime(\"%Y%m%dT%H%M%S\")\n", "Path(\"Results\").mkdir(parents=True, exist_ok=True)\n", "session.save_results(f\"Results/{timestamp}_qspec_results.json\")\n", - "print(f\"File saved as Results/{timestamp}_qspec_results.json\")\n" + "print(f\"File saved as Results/{timestamp}_qspec_results.json\")" ] }, { @@ -314,7 +314,7 @@ "metadata": {}, "outputs": [], "source": [ - "plot_results(qspec_results, phase=True)\n" + "plot_results(qspec_results, phase=True)" ] }, { diff --git a/examples/01_qubit_characterization/06_amplitude_rabi.ipynb b/examples/01_qubit_characterization/06_amplitude_rabi.ipynb index 3c16d3d..afb171c 100644 --- a/examples/01_qubit_characterization/06_amplitude_rabi.ipynb +++ b/examples/01_qubit_characterization/06_amplitude_rabi.ipynb @@ -44,7 +44,7 @@ "import time\n", "\n", "import numpy as np\n", - "import matplotlib.pyplot as plt\n" + "import matplotlib.pyplot as plt" ] }, { @@ -66,7 +66,7 @@ "outputs": [], "source": [ "device_setup = create_device_setup(generation=2)\n", - "emulate = True\n" + "emulate = True" ] }, { @@ -77,7 +77,7 @@ "source": [ "# create and connect to a session\n", "session = Session(device_setup=device_setup)\n", - "session.connect(do_emulation=emulate)\n" + "session.connect(do_emulation=emulate)" ] }, { @@ -130,7 +130,7 @@ "def create_rabi_drive_pulse(qubit, length=1e-6, amplitude=0.9):\n", " return pulse_library.gaussian(\n", " uid=f\"gaussian_drive_q{qubit}\", length=length, amplitude=amplitude\n", - " )\n" + " )" ] }, { @@ -140,7 +140,7 @@ "source": [ "## 3. Experiment Definition\n", "\n", - "You'll now create a function which generates an [experiment](https://docs.zhinst.com/labone_q_user_manual/concepts/experiment.html). In this experiment, you'll pass an amplitude sweep parameter as an argument to the [sweep section](https://docs.zhinst.com/labone_q_user_manual/concepts/averaging_sweeping.html#labone_q.func_concepts.experiment.averaging_sweeping.parameters_sweeps). Within the sweeep section, you'll create another [section](https://docs.zhinst.com/labone_q_user_manual/concepts/sections_and_pulses.html) containing a `play` command to drive the qubit, where the amplitude of this command takes the sweep parameter. You'll also make a readout section containing `play` and `acquire` commands to perform readout. Within the real-time aquisition section, you'll set use `INTEGRATION` as your acquisition type." + "You'll now create a function which generates an [experiment](https://docs.zhinst.com/labone_q_user_manual/concepts/experiment.html). In this experiment, you'll pass an amplitude sweep parameter as an argument to the [sweep section](https://docs.zhinst.com/labone_q_user_manual/concepts/averaging_sweeping.html#labone_q.func_concepts.experiment.averaging_sweeping.parameters_sweeps). Within the sweeep section, you'll create another [section](https://docs.zhinst.com/labone_q_user_manual/concepts/sections_and_pulses.html) containing a `play` command to drive the qubit, where the amplitude of this command takes the sweep parameter. You'll also make a readout section containing `play` and `acquire` commands to perform readout. Within the real-time acquisition section, you'll set use `INTEGRATION` as your acquisition type." ] }, { @@ -190,7 +190,7 @@ " with exp_rabi.section(uid=\"delay\", length=1e-6):\n", " # relax time after readout - for qubit relaxation to groundstate and signal processing\n", " exp_rabi.reserve(signal=\"measure\")\n", - " return exp_rabi\n" + " return exp_rabi" ] }, { @@ -198,7 +198,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 3.1 Create Experiment and Signal Map\n", + "### 3.1 Create Experiment and Signal Map\n", "\n", "Before running the experiment, you'll define and set the mapping between the experimental and logical lines." ] @@ -232,7 +232,7 @@ "\n", "\n", "# run the experiment on qubit 0\n", - "exp_rabi.set_signal_map(signal_map_default(\"q0\"))\n" + "exp_rabi.set_signal_map(signal_map_default(\"q0\"))" ] }, { @@ -240,7 +240,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 3.2 Compile, Generate Pulse Sheet, and Plot Simulated Signals\n", + "### 3.2 Compile, Generate Pulse Sheet, and Plot Simulated Signals\n", "\n", "Now, you'll compile the experiment and generate a pulse sheet." ] @@ -255,8 +255,8 @@ "compiled_rabi = session.compile(exp_rabi)\n", "\n", "Path(\"Pulse_Sheets\").mkdir(parents=True, exist_ok=True)\n", - "# generate a pulse sheet to inspect experiment befor runtime\n", - "show_pulse_sheet(\"Pulse_Sheets/Rabi\", compiled_rabi)\n" + "# generate a pulse sheet to inspect experiment before runtime\n", + "show_pulse_sheet(\"Pulse_Sheets/Rabi\", compiled_rabi)" ] }, { @@ -273,7 +273,7 @@ "metadata": {}, "outputs": [], "source": [ - "plot_simulation(compiled_rabi, start_time=0, length=100e-6)\n" + "plot_simulation(compiled_rabi, start_time=0, length=100e-6)" ] }, { @@ -281,7 +281,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 3.3 Run, Save, and Plot Results\n", + "### 3.3 Run, Save, and Plot Results\n", "\n", "Finally, you'll run the experiment, save, and plot the results." ] @@ -297,7 +297,7 @@ "timestamp = time.strftime(\"%Y%m%dT%H%M%S\")\n", "Path(\"Results\").mkdir(parents=True, exist_ok=True)\n", "session.save_results(f\"Results/{timestamp}_rabi_results.json\")\n", - "print(f\"File saved as Results/{timestamp}_rabi_results.json\")\n" + "print(f\"File saved as Results/{timestamp}_rabi_results.json\")" ] }, { @@ -306,7 +306,7 @@ "metadata": {}, "outputs": [], "source": [ - "plot_results(rabi_results)\n" + "plot_results(rabi_results)" ] }, { @@ -349,7 +349,7 @@ "print(popt)\n", "\n", "# plot fit results together with measurement data\n", - "plt.plot(amp_plot, func_osc(amp_plot, *popt), \"-r\")\n" + "plt.plot(amp_plot, func_osc(amp_plot, *popt), \"-r\")" ] } ], diff --git a/examples/01_qubit_characterization/07_ramsey.ipynb b/examples/01_qubit_characterization/07_ramsey.ipynb index 248b20e..a624974 100644 --- a/examples/01_qubit_characterization/07_ramsey.ipynb +++ b/examples/01_qubit_characterization/07_ramsey.ipynb @@ -42,7 +42,7 @@ "from pathlib import Path\n", "import time\n", "import numpy as np\n", - "import matplotlib.pyplot as plt\n" + "import matplotlib.pyplot as plt" ] }, { @@ -64,7 +64,7 @@ "outputs": [], "source": [ "device_setup = create_device_setup(generation=2)\n", - "emulate = True\n" + "emulate = True" ] }, { @@ -75,7 +75,7 @@ "source": [ "# create and connect to a session\n", "session = Session(device_setup=device_setup)\n", - "session.connect(do_emulation=emulate)\n" + "session.connect(do_emulation=emulate)" ] }, { @@ -135,7 +135,7 @@ " time_sweep = LinearSweepParameter(\n", " uid=\"time_sweep_param\", start=start, stop=stop, count=count, axis_name=axis_name\n", " )\n", - " return time_sweep\n" + " return time_sweep" ] }, { @@ -151,7 +151,7 @@ "\n", "You'll also make a readout section containing `play` and `acquire` commands to perform readout. \n", "\n", - "Within the real-time aquisition section, you'll set use `INTEGRATION` as your acquisition type and set the repetition mode to `AUTO`." + "Within the real-time acquisition section, you'll set use `INTEGRATION` as your acquisition type and set the repetition mode to `AUTO`." ] }, { @@ -206,7 +206,7 @@ " with exp_ramsey.section(uid=\"delay\", length=1e-6):\n", " # relax time after readout - for qubit relaxation to groundstate and signal processing\n", " exp_ramsey.reserve(signal=\"measure\")\n", - " return exp_ramsey\n" + " return exp_ramsey" ] }, { @@ -214,7 +214,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 3.1 Create Experiment and Signal Map\n", + "### 3.1 Create Experiment and Signal Map\n", "\n", "Before running the experiment, you'll define and set the mapping between the experimental and logical lines." ] @@ -252,7 +252,7 @@ "\n", "\n", "# run the experiment on qubit 0\n", - "ramsey_exp.set_signal_map(signal_map_default(\"q0\"))\n" + "ramsey_exp.set_signal_map(signal_map_default(\"q0\"))" ] }, { @@ -260,7 +260,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 3.2 Compile, Generate Pulse Sheet, and Plot Simulated Signals\n", + "### 3.2 Compile, Generate Pulse Sheet, and Plot Simulated Signals\n", "\n", "Now, you'll compile the experiment and generate a pulse sheet." ] @@ -275,8 +275,8 @@ "compiled_ramsey = session.compile(ramsey_exp)\n", "\n", "Path(\"Pulse_Sheets\").mkdir(parents=True, exist_ok=True)\n", - "# generate a pulse sheet to inspect experiment befor runtime\n", - "show_pulse_sheet(\"Pulse_Sheets/Ramsey\", compiled_ramsey)\n" + "# generate a pulse sheet to inspect experiment before runtime\n", + "show_pulse_sheet(\"Pulse_Sheets/Ramsey\", compiled_ramsey)" ] }, { @@ -293,7 +293,7 @@ "metadata": {}, "outputs": [], "source": [ - "plot_simulation(compiled_ramsey, start_time=0, length=200e-6, plot_width=10)\n" + "plot_simulation(compiled_ramsey, start_time=0, length=200e-6, plot_width=10)" ] }, { @@ -301,7 +301,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 3.3 Run, Save, and Plot Results\n", + "### 3.3 Run, Save, and Plot Results\n", "\n", "Finally, you'll run the experiment, save, and plot the results." ] @@ -317,7 +317,7 @@ "timestamp = time.strftime(\"%Y%m%dT%H%M%S\")\n", "Path(\"Results\").mkdir(parents=True, exist_ok=True)\n", "session.save_results(f\"Results/{timestamp}_ramsey_results.json\")\n", - "print(f\"File saved as Results/{timestamp}_ramsey_results.json\")\n" + "print(f\"File saved as Results/{timestamp}_ramsey_results.json\")" ] }, { @@ -327,7 +327,7 @@ "outputs": [], "source": [ "# plot_result_2d(ramsey_results, list(ramsey_results.acquired_results.keys())[0])\n", - "plot_results(ramsey_results)\n" + "plot_results(ramsey_results)" ] }, { @@ -335,7 +335,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# 4. Fitting Results\n", + "## 4. Fitting Results\n", "\n", "You can also fit your results. The below script fits some emulated Rabi data when running in emulation mode." ] @@ -385,7 +385,7 @@ "print(popt)\n", "\n", "# plot fit results together with experimental data\n", - "plt.plot(delay_plot, func_decayOsc(delay_plot, *popt), \"-r\")\n" + "plt.plot(delay_plot, func_decayOsc(delay_plot, *popt), \"-r\")" ] } ], diff --git a/examples/01_qubit_characterization/08_e-f_transition_spectroscopy_shfsg_shfqc.ipynb b/examples/01_qubit_characterization/08_e-f_transition_spectroscopy_shfsg_shfqc.ipynb index a8b6037..c5719fb 100644 --- a/examples/01_qubit_characterization/08_e-f_transition_spectroscopy_shfsg_shfqc.ipynb +++ b/examples/01_qubit_characterization/08_e-f_transition_spectroscopy_shfsg_shfqc.ipynb @@ -17,7 +17,7 @@ "id": "961a420e-7dc7-46fd-aea8-12af1cea8aa2", "metadata": {}, "source": [ - "# 0. General Imports and Definitions" + "## 0. General Imports and Definitions" ] }, { @@ -26,7 +26,7 @@ "id": "2a01d7d5-527b-4324-aa74-95d768f9a2ef", "metadata": {}, "source": [ - "## 0.1 Python Imports" + "### 0.1 Python Imports" ] }, { @@ -53,7 +53,7 @@ ")\n", "from laboneq.contrib.example_helpers.descriptors.shfsg_shfqa_pqsc import (\n", " descriptor_shfsg_shfqa_pqsc,\n", - ")\n" + ")" ] }, { @@ -62,7 +62,7 @@ "id": "c8aa3c8e-12ce-4f86-a5bb-7f76e0c0f5d7", "metadata": {}, "source": [ - "# 1. Define Device Setup and Calibration" + "## 1. Define Device Setup and Calibration" ] }, { @@ -71,7 +71,7 @@ "id": "f44d74bf-d663-4421-b826-bd156e65415c", "metadata": {}, "source": [ - "## 1.1 Define a Device Setup\n", + "### 1.1 Define a Device Setup\n", "\n", "We'll load a descriptor file to define our device setup and logical signal lines. We could, instead, explicitly include the descriptor here as a string and then use `DeviceSetup.from_descriptor()` below. Choose the best method that works for you!" ] @@ -92,7 +92,7 @@ " setup_name=\"my_QCCS_setup\", # setup name\n", ")\n", "\n", - "use_emulation = True # set to False to run on real hardware\n" + "use_emulation = True # set to False to run on real hardware" ] }, { @@ -101,7 +101,7 @@ "id": "81eae8d4-aaac-486e-ae41-0c0bc01c706e", "metadata": {}, "source": [ - "## 1.2 Define Calibration Settings\n", + "### 1.2 Define Calibration Settings\n", "\n", "Modify the calibration on the device setup with known parameters for qubit control and readout - qubit control and readout frequencies, mixer calibration corrections" ] @@ -201,7 +201,7 @@ " # delays the start of integration in relation to the start of the readout pulse to compensate for signal propagation time\n", " port_delay=10e-9,\n", " local_oscillator=local_oscillator_shfqa, # will be ignored if the instrument is not an SHF*\n", - " )\n" + " )" ] }, { @@ -211,7 +211,7 @@ "metadata": {}, "outputs": [], "source": [ - "calibrate_devices(device_setup)\n" + "calibrate_devices(device_setup)" ] }, { @@ -221,7 +221,7 @@ "metadata": {}, "outputs": [], "source": [ - "device_setup\n" + "device_setup" ] }, { @@ -230,7 +230,7 @@ "id": "38438dd2-6905-4f99-a556-bb27363c3a1f", "metadata": {}, "source": [ - "# 2. e-f Transition Spectroscopy\n", + "## 2. e-f Transition Spectroscopy\n", "\n", "Sweep the pulse frequency of a qubit drive pulse to determine the frequency of the e-f transition. This assumes that a pi-pulse for the first excited state is already calibrated." ] @@ -241,7 +241,7 @@ "id": "d068797e-1673-4a5b-93c2-c450e8c061ab", "metadata": {}, "source": [ - "## 2.1 Define the Experiment" + "### 2.1 Define the Experiment" ] }, { @@ -264,7 +264,7 @@ "# readout integration weights\n", "readout_weighting_function = pulse_library.const(\n", " uid=\"readout_weighting_function\", length=400e-9, amplitude=0.8\n", - ")\n" + ")" ] }, { @@ -336,7 +336,7 @@ " )\n", " # relax time after readout - for signal processing and qubit relaxation to groundstate\n", " with exp_ef_spec.section(uid=\"relax\"):\n", - " exp_ef_spec.delay(signal=\"measure\", time=100e-9)\n" + " exp_ef_spec.delay(signal=\"measure\", time=100e-9)" ] }, { @@ -363,7 +363,7 @@ " ],\n", " \"measure\": device_setup.logical_signal_groups[\"q1\"].logical_signals[\"measure_line\"],\n", " \"acquire\": device_setup.logical_signal_groups[\"q1\"].logical_signals[\"acquire_line\"],\n", - "}\n" + "}" ] }, { @@ -385,7 +385,7 @@ " )\n", " )\n", " }\n", - ")\n" + ")" ] }, { @@ -394,7 +394,7 @@ "id": "7e485382-ccd1-4c32-8253-1f5e9e2ad127", "metadata": {}, "source": [ - "## 2.2 Run the Experiment and Plot the Measurement Results and Pulse Sequence" + "### 2.2 Run the Experiment and Plot the Measurement Results and Pulse Sequence" ] }, { @@ -415,7 +415,7 @@ "# run experiment on qubit 0\n", "compiled_exp_ef_spec = session.compile(exp_ef_spec)\n", "\n", - "ef_espec_results = session.run(compiled_exp_ef_spec)\n" + "ef_espec_results = session.run(compiled_exp_ef_spec)" ] }, { @@ -425,7 +425,7 @@ "metadata": {}, "outputs": [], "source": [ - "# show_pulse_sheet(\"ef_spectroscopy\", compiled_exp_ef_spec)\n" + "# show_pulse_sheet(\"ef_spectroscopy\", compiled_exp_ef_spec)" ] }, { @@ -436,7 +436,7 @@ "outputs": [], "source": [ "# Plot simulated output signals\n", - "plot_simulation(compiled_exp_ef_spec, 0, 0.5e-6)\n" + "plot_simulation(compiled_exp_ef_spec, 0, 0.5e-6)" ] }, { diff --git a/examples/01_qubit_characterization/09_e-f_gate_tuneup_shfsg_shfqc.ipynb b/examples/01_qubit_characterization/09_e-f_gate_tuneup_shfsg_shfqc.ipynb index 57cbf0d..ae8f34b 100644 --- a/examples/01_qubit_characterization/09_e-f_gate_tuneup_shfsg_shfqc.ipynb +++ b/examples/01_qubit_characterization/09_e-f_gate_tuneup_shfsg_shfqc.ipynb @@ -17,7 +17,7 @@ "id": "961a420e-7dc7-46fd-aea8-12af1cea8aa2", "metadata": {}, "source": [ - "# 0. General Imports and Definitions" + "## 0. General Imports and Definitions" ] }, { @@ -26,7 +26,7 @@ "id": "2a01d7d5-527b-4324-aa74-95d768f9a2ef", "metadata": {}, "source": [ - "## 0.1 Python Imports" + "### 0.1 Python Imports" ] }, { @@ -53,7 +53,7 @@ ")\n", "from laboneq.contrib.example_helpers.descriptors.shfsg_shfqa_pqsc import (\n", " descriptor_shfsg_shfqa_pqsc,\n", - ")\n" + ")" ] }, { @@ -62,7 +62,7 @@ "id": "c8aa3c8e-12ce-4f86-a5bb-7f76e0c0f5d7", "metadata": {}, "source": [ - "# 1. Define Device Setup and Calibration" + "## 1. Define Device Setup and Calibration" ] }, { @@ -71,7 +71,7 @@ "id": "f44d74bf-d663-4421-b826-bd156e65415c", "metadata": {}, "source": [ - "## 1.1 Define a Device Setup\n", + "### 1.1 Define a Device Setup\n", "\n", "We'll load a descriptor file to define our device setup and logical signal lines. We could, instead, explicitly include the descriptor here as a string and then use `DeviceSetup.from_descriptor()` below. Choose the best method that works for you!" ] @@ -92,7 +92,7 @@ " setup_name=\"my_QCCS_setup\", # setup name\n", ")\n", "\n", - "use_emulation = True # set to False to run on real hardware\n" + "use_emulation = True # set to False to run on real hardware" ] }, { @@ -101,7 +101,7 @@ "id": "81eae8d4-aaac-486e-ae41-0c0bc01c706e", "metadata": {}, "source": [ - "## 1.2 Define Calibration Settings\n", + "### 1.2 Define Calibration Settings\n", "\n", "Modify the calibration on the device setup with known parameters for qubit control and readout - qubit control and readout frequencies, mixer calibration corrections" ] @@ -201,7 +201,7 @@ " # delays the start of integration in relation to the start of the readout pulse to compensate for signal propagation time\n", " port_delay=10e-9,\n", " local_oscillator=local_oscillator_shfqa, # will be ignored if the instrument is not an SHF*\n", - " )\n" + " )" ] }, { @@ -211,7 +211,7 @@ "metadata": {}, "outputs": [], "source": [ - "calibrate_devices(device_setup)\n" + "calibrate_devices(device_setup)" ] }, { @@ -220,7 +220,7 @@ "id": "38438dd2-6905-4f99-a556-bb27363c3a1f", "metadata": {}, "source": [ - "# 2. e-f Gate Tune-up\n", + "## 2. e-f Gate Tune-up\n", "\n", "Sweep the pulse amplitude of a qubit drive pulse to determine the ideal amplitudes to drive qubit from excited to second excited state \n", "- assumes that a pi-pulse to reach the e state is already calibrated" @@ -232,7 +232,7 @@ "id": "d068797e-1673-4a5b-93c2-c450e8c061ab", "metadata": {}, "source": [ - "## 2.1 Define the Experiment" + "### 2.1 Define the Experiment" ] }, { @@ -257,7 +257,7 @@ "# readout integration weights\n", "readout_weighting_function = pulse_library.const(\n", " uid=\"readout_weighting_function\", length=400e-9, amplitude=0.8\n", - ")\n" + ")" ] }, { @@ -331,7 +331,7 @@ " )\n", " # relax time after readout - for signal processing and qubit relaxation to groundstate\n", " with exp_ef_gate.section(uid=\"relax\"):\n", - " exp_ef_gate.delay(signal=\"measure\", time=100e-9)\n" + " exp_ef_gate.delay(signal=\"measure\", time=100e-9)" ] }, { @@ -358,7 +358,7 @@ " ],\n", " \"measure\": device_setup.logical_signal_groups[\"q1\"].logical_signals[\"measure_line\"],\n", " \"acquire\": device_setup.logical_signal_groups[\"q1\"].logical_signals[\"acquire_line\"],\n", - "}\n" + "}" ] }, { @@ -367,7 +367,7 @@ "id": "7e485382-ccd1-4c32-8253-1f5e9e2ad127", "metadata": {}, "source": [ - "## 2.2 Run the Experiment and Plot the Pulse Sequence" + "### 2.2 Run the Experiment and Plot the Pulse Sequence" ] }, { @@ -386,7 +386,7 @@ "\n", "# run experiment on qubit 0\n", "compiled_exp_ef_gate = session.compile(exp_ef_gate)\n", - "ef_gate_results = session.run(compiled_exp_ef_gate)\n" + "ef_gate_results = session.run(compiled_exp_ef_gate)" ] }, { @@ -397,7 +397,7 @@ "outputs": [], "source": [ "# Plot simulated output signals\n", - "plot_simulation(compiled_exp_ef_gate, 0, 10e-6)\n" + "plot_simulation(compiled_exp_ef_gate, 0, 10e-6)" ] } ], diff --git a/examples/01_qubit_characterization/10_CR_gate_tuneup_shfsg_shfqc.ipynb b/examples/01_qubit_characterization/10_CR_gate_tuneup_shfsg_shfqc.ipynb index ed56c6a..2f95f79 100644 --- a/examples/01_qubit_characterization/10_CR_gate_tuneup_shfsg_shfqc.ipynb +++ b/examples/01_qubit_characterization/10_CR_gate_tuneup_shfsg_shfqc.ipynb @@ -17,7 +17,7 @@ "id": "961a420e-7dc7-46fd-aea8-12af1cea8aa2", "metadata": {}, "source": [ - "# 0. General Imports and Definitions" + "## 0. General Imports and Definitions" ] }, { @@ -26,7 +26,7 @@ "id": "2a01d7d5-527b-4324-aa74-95d768f9a2ef", "metadata": {}, "source": [ - "## 0.1 Python Imports" + "### 0.1 Python Imports" ] }, { @@ -53,7 +53,7 @@ ")\n", "from laboneq.contrib.example_helpers.descriptors.shfsg_shfqa_pqsc import (\n", " descriptor_shfsg_shfqa_pqsc,\n", - ")\n" + ")" ] }, { @@ -62,7 +62,7 @@ "id": "c8aa3c8e-12ce-4f86-a5bb-7f76e0c0f5d7", "metadata": {}, "source": [ - "# 1. Define Device Setup and Calibration" + "## 1. Define Device Setup and Calibration" ] }, { @@ -71,7 +71,7 @@ "id": "f44d74bf-d663-4421-b826-bd156e65415c", "metadata": {}, "source": [ - "## 1.1 Define a Device Setup\n", + "### 1.1 Define a Device Setup\n", "\n", "We'll load a descriptor file to define our device setup and logical signal lines. We could, instead, explicitly include the descriptor here as a string and then use `DeviceSetup.from_descriptor()` below. Choose the best method that works for you!" ] @@ -92,7 +92,7 @@ " setup_name=\"my_QCCS_setup\", # setup name\n", ")\n", "\n", - "use_emulation = True # set to False to run on real hardware\n" + "use_emulation = True # set to False to run on real hardware" ] }, { @@ -101,7 +101,7 @@ "id": "81eae8d4-aaac-486e-ae41-0c0bc01c706e", "metadata": {}, "source": [ - "## 1.2 Define Calibration Settings\n", + "### 1.2 Define Calibration Settings\n", "\n", "Modify the calibration on the device setup with known parameters for qubit control and readout - qubit control and readout frequencies, mixer calibration corrections" ] @@ -201,7 +201,7 @@ " # delays the start of integration in relation to the start of the readout pulse to compensate for signal propagation time\n", " port_delay=10e-9,\n", " local_oscillator=local_oscillator_shfqa, # will be ignored if the instrument is not an SHF*\n", - " )\n" + " )" ] }, { @@ -211,7 +211,7 @@ "metadata": {}, "outputs": [], "source": [ - "calibrate_devices(device_setup)\n" + "calibrate_devices(device_setup)" ] }, { @@ -220,7 +220,7 @@ "id": "38438dd2-6905-4f99-a556-bb27363c3a1f", "metadata": {}, "source": [ - "# 2. CR Gate Tune-up\n", + "## 2. CR Gate Tune-up\n", "\n", "Sweep the pulse length of a qubit drive pulse at the difference frequency of two qubits to determine the ideal parameters for a CR gate." ] @@ -231,7 +231,7 @@ "id": "d068797e-1673-4a5b-93c2-c450e8c061ab", "metadata": {}, "source": [ - "## 2.1 Define the Experiment" + "### 2.1 Define the Experiment" ] }, { @@ -256,7 +256,7 @@ "# readout integration weights\n", "readout_weighting_function = pulse_library.const(\n", " uid=\"readout_weighting_function\", length=400e-9, amplitude=0.8\n", - ")\n" + ")" ] }, { @@ -341,7 +341,7 @@ " )\n", " # relax time after readout - for signal processing and qubit relaxation to ground state\n", " with exp_cr_gate.section(uid=\"relax\", play_after=\"qubit_readout\"):\n", - " exp_cr_gate.delay(signal=\"measure_0\", time=100e-9)\n" + " exp_cr_gate.delay(signal=\"measure_0\", time=100e-9)" ] }, { @@ -386,7 +386,7 @@ " \"acquire_1\": device_setup.logical_signal_groups[\"q1\"].logical_signals[\n", " \"acquire_line\"\n", " ],\n", - "}\n" + "}" ] }, { @@ -395,7 +395,7 @@ "id": "7e485382-ccd1-4c32-8253-1f5e9e2ad127", "metadata": {}, "source": [ - "## 2.2 Run the Experiment and Plot the Pulse Sequence" + "### 2.2 Run the Experiment and Plot the Pulse Sequence" ] }, { @@ -414,7 +414,7 @@ "\n", "# run experiment on qubit 0\n", "compiled_exp_cr_gate = session.compile(exp_cr_gate)\n", - "cr_gate_results = session.run(compiled_exp_cr_gate)\n" + "cr_gate_results = session.run(compiled_exp_cr_gate)" ] }, { @@ -425,7 +425,7 @@ "outputs": [], "source": [ "# Plot simulated output signals\n", - "plot_simulation(compiled_exp_cr_gate, 0, 10e-6)\n" + "plot_simulation(compiled_exp_cr_gate, 0, 10e-6)" ] }, { diff --git a/examples/01_qubit_characterization/99_basic_experiments_shfsg_shfqa_shfqc_hdawg.ipynb b/examples/01_qubit_characterization/99_basic_experiments_shfsg_shfqa_shfqc_hdawg.ipynb index 99b9e91..16a0003 100644 --- a/examples/01_qubit_characterization/99_basic_experiments_shfsg_shfqa_shfqc_hdawg.ipynb +++ b/examples/01_qubit_characterization/99_basic_experiments_shfsg_shfqa_shfqc_hdawg.ipynb @@ -29,7 +29,7 @@ "# helper import\n", "from laboneq.contrib.example_helpers.data_analysis.data_analysis import *\n", "from laboneq.contrib.example_helpers.plotting.plot_helpers import *\n", - "from laboneq.contrib.example_helpers.example_notebook_helper import create_device_setup\n" + "from laboneq.contrib.example_helpers.example_notebook_helper import create_device_setup" ] }, { @@ -46,7 +46,7 @@ "source": [ "# create device setup\n", "device_setup = create_device_setup(generation=2)\n", - "use_emulation = True\n" + "use_emulation = True" ] }, { @@ -63,7 +63,7 @@ "id": "cfdea1e4-0d6e-4b6d-b9fb-5f3bdc8f84c6", "metadata": {}, "source": [ - "# 1. CW Resonator Spectroscopy\n", + "## 1. CW Resonator Spectroscopy\n", "\n", "Find the resonance frequency of the qubit readout resonator by looking at the transmission or reflection of a probe signal applied through the readout line." ] @@ -73,7 +73,7 @@ "id": "d7caa8f4-9cd8-46b4-8ff4-543e4e56670d", "metadata": {}, "source": [ - "## 1.1 Define the Experiment" + "### 1.1 Define the Experiment" ] }, { @@ -127,7 +127,7 @@ " )\n", " # relax time after readout - for signal processing and qubit relaxation to ground state\n", " with exp.section(uid=\"relax\", play_after=\"spectroscopy\"):\n", - " exp.delay(signal=\"acquire\", time=1e-6)\n" + " exp.delay(signal=\"acquire\", time=1e-6)" ] }, { @@ -146,7 +146,7 @@ " modulation_type=ModulationType.HARDWARE,\n", " ),\n", " port_delay=250e-9,\n", - ")\n" + ")" ] }, { @@ -161,7 +161,7 @@ " return {\n", " \"measure\": f\"/logical_signal_groups/q{qubit_id}/measure_line\",\n", " \"acquire\": f\"/logical_signal_groups/q{qubit_id}/acquire_line\",\n", - " }\n" + " }" ] }, { @@ -169,7 +169,7 @@ "id": "db6ec116-41db-4c23-8657-c2f3d2b27b3a", "metadata": {}, "source": [ - "## 1.2 Run the Experiment and Plot the Measurement Results" + "### 1.2 Run the Experiment and Plot the Measurement Results" ] }, { @@ -191,7 +191,7 @@ "my_results = session.run(exp)\n", "\n", "# plot measurement results\n", - "plot_result_2d(my_results, \"ac_0\")\n" + "plot_result_2d(my_results, \"ac_0\")" ] }, { @@ -202,7 +202,7 @@ "outputs": [], "source": [ "# use pulse sheet viewer to display the pulse sequence - only recommended for small number of averages and sweep steps to avoid performance issues\n", - "show_pulse_sheet(\"Resonator Spectroscopy\", session.compiled_experiment)\n" + "show_pulse_sheet(\"Resonator Spectroscopy\", session.compiled_experiment)" ] }, { @@ -215,7 +215,7 @@ "# Run the same experiment on qubit 1\n", "exp.set_calibration(exp_calib)\n", "exp.set_signal_map(map_qubit(1))\n", - "my_results = session.run(exp)\n" + "my_results = session.run(exp)" ] }, { @@ -223,7 +223,7 @@ "id": "4ecbf581-b2e9-4f44-a6da-4f16f7a13291", "metadata": {}, "source": [ - "# 2. Pulsed Qubit Spectroscopy\n", + "## 2. Pulsed Qubit Spectroscopy\n", "\n", "Find the resonance frequency of the qubit by looking at the change in resonator transmission when sweeping the frequency of a qubit excitation pulse." ] @@ -233,7 +233,7 @@ "id": "55540be2-0163-4eb4-8929-068fed757795", "metadata": {}, "source": [ - "## 2.1 Define the Experiment" + "### 2.1 Define the Experiment" ] }, { @@ -252,7 +252,7 @@ "# readout weights for integration\n", "readout_weighting_function = pulse_library.const(\n", " uid=\"readout_weighting_function\", length=200e-9, amplitude=1.0\n", - ")\n" + ")" ] }, { @@ -311,7 +311,7 @@ " )\n", " # relax time after readout - for signal processing and qubit relaxation to ground state\n", " with exp.section(uid=\"relax\", play_after=\"qubit_readout\"):\n", - " exp.delay(signal=\"measure\", time=1e-6)\n" + " exp.delay(signal=\"measure\", time=1e-6)" ] }, { @@ -328,7 +328,7 @@ " frequency=drive_frequency_sweep,\n", " modulation_type=ModulationType.HARDWARE,\n", " )\n", - ")\n" + ")" ] }, { @@ -349,7 +349,7 @@ " \"acquire\": device_setup.logical_signal_groups[f\"q{qubit_id}\"].logical_signals[\n", " \"acquire_line\"\n", " ],\n", - " }\n" + " }" ] }, { @@ -357,7 +357,7 @@ "id": "192639da-1b74-43dd-a72d-923c5a0227f4", "metadata": {}, "source": [ - "## 2.2 Run the Experiment and Plot the Measurement Results and Pulse Sequence" + "### 2.2 Run the Experiment and Plot the Measurement Results and Pulse Sequence" ] }, { @@ -376,7 +376,7 @@ "session.connect(do_emulation=use_emulation)\n", "\n", "# run experiment on qubit 0\n", - "my_results = session.run(exp)\n" + "my_results = session.run(exp)" ] }, { @@ -387,7 +387,7 @@ "outputs": [], "source": [ "# Plot simulated output signals\n", - "plot_simulation(session.compiled_experiment, 0, 10e-6)\n" + "plot_simulation(session.compiled_experiment, 0, 10e-6)" ] }, { @@ -403,7 +403,7 @@ "outputs": [], "source": [ "# plot measurement results\n", - "plot_result_2d(my_results, \"ac_0\")\n" + "plot_result_2d(my_results, \"ac_0\")" ] }, { @@ -414,7 +414,7 @@ "outputs": [], "source": [ "# use pulse sheet viewer to display the pulse sequence - only recommended for small number of averages and sweep steps to avoid performance issues\n", - "show_pulse_sheet(\"Qubit Spectroscopy\", session.compiled_experiment)\n" + "show_pulse_sheet(\"Qubit Spectroscopy\", session.compiled_experiment)" ] }, { @@ -422,7 +422,7 @@ "id": "38438dd2-6905-4f99-a556-bb27363c3a1f", "metadata": {}, "source": [ - "# 3. Amplitude Rabi Experiment\n", + "## 3. Amplitude Rabi Experiment\n", "\n", "Sweep the pulse amplitude of a qubit drive pulse to determine the ideal amplitudes for specific qubit rotation angles." ] @@ -432,7 +432,7 @@ "id": "d068797e-1673-4a5b-93c2-c450e8c061ab", "metadata": {}, "source": [ - "## 3.1 Define the Experiment" + "### 3.1 Define the Experiment" ] }, { @@ -452,7 +452,7 @@ "# readout integration weights\n", "readout_weighting_function = pulse_library.const(\n", " uid=\"readout_weighting_function\", length=400e-9, amplitude=1.0\n", - ")\n" + ")" ] }, { @@ -514,7 +514,7 @@ " )\n", " # relax time after readout - for signal processing and qubit relaxation to ground state\n", " with exp.section(uid=\"relax\", play_after=\"qubit_readout\"):\n", - " exp.delay(signal=\"measure\", time=1e-6)\n" + " exp.delay(signal=\"measure\", time=1e-6)" ] }, { @@ -535,7 +535,7 @@ " \"acquire\": device_setup.logical_signal_groups[f\"q{qubit_id}\"].logical_signals[\n", " \"acquire_line\"\n", " ],\n", - " }\n" + " }" ] }, { @@ -543,7 +543,7 @@ "id": "7e485382-ccd1-4c32-8253-1f5e9e2ad127", "metadata": {}, "source": [ - "## 3.2 Run the Experiment and Plot the Measurement Results and Pulse Sequence" + "### 3.2 Run the Experiment and Plot the Measurement Results and Pulse Sequence" ] }, { @@ -561,7 +561,7 @@ "session.connect(do_emulation=use_emulation)\n", "\n", "# run experiment on qubit 0\n", - "my_results = session.run(exp)\n" + "my_results = session.run(exp)" ] }, { @@ -572,7 +572,7 @@ "outputs": [], "source": [ "# Plot simulated output signals\n", - "plot_simulation(session.compiled_experiment, 0, 10e-6)\n" + "plot_simulation(session.compiled_experiment, 0, 10e-6)" ] }, { @@ -588,7 +588,7 @@ "outputs": [], "source": [ "# plot measurement results\n", - "plot_result_2d(my_results, \"ac_0\")\n" + "plot_result_2d(my_results, \"ac_0\")" ] }, { @@ -599,7 +599,7 @@ "outputs": [], "source": [ "# use pulse sheet viewer to display the pulse sequence - only recommended for small number of averages and sweep steps to avoid performance issues\n", - "show_pulse_sheet(\"Amplitude Rabi\", session.compiled_experiment)\n" + "show_pulse_sheet(\"Amplitude Rabi\", session.compiled_experiment)" ] }, { @@ -617,7 +617,7 @@ "session.connect(do_emulation=use_emulation)\n", "\n", "# run experiment on qubit 1\n", - "my_results = session.run(exp)\n" + "my_results = session.run(exp)" ] }, { @@ -625,7 +625,7 @@ "id": "fe1dc6dc", "metadata": {}, "source": [ - "# 4. Length Rabi Experiment\n", + "## 4. Length Rabi Experiment\n", "\n", "Instead of sweeping the amplitude of the drive pulse as above, we can also sweep its length." ] @@ -635,7 +635,7 @@ "id": "c6ce2a16", "metadata": {}, "source": [ - "## 4.1 Define the Experiment" + "### 4.1 Define the Experiment" ] }, { @@ -654,7 +654,7 @@ "# readout integration weights\n", "readout_weighting_function = pulse_library.const(\n", " uid=\"readout_weighting_function\", length=400e-9, amplitude=1.0\n", - ")\n" + ")" ] }, { @@ -710,7 +710,7 @@ " )\n", " # relax time after readout - for signal processing and qubit relaxation to ground state\n", " with exp.section(uid=\"relax\", play_after=\"qubit_readout\"):\n", - " exp.delay(signal=\"measure\", time=1e-6)\n" + " exp.delay(signal=\"measure\", time=1e-6)" ] }, { @@ -718,7 +718,7 @@ "id": "c1155568", "metadata": {}, "source": [ - "## 4.2 Run the Experiment and Plot the Measurement Results and Pulse Sequence" + "### 4.2 Run the Experiment and Plot the Measurement Results and Pulse Sequence" ] }, { @@ -736,7 +736,7 @@ "session.connect(do_emulation=use_emulation)\n", "\n", "# run experiment on qubit 0\n", - "my_results = session.run(exp)\n" + "my_results = session.run(exp)" ] }, { @@ -747,7 +747,7 @@ "outputs": [], "source": [ "# Plot simulated output signals\n", - "plot_simulation(session.compiled_experiment, 0, 10e-6)\n" + "plot_simulation(session.compiled_experiment, 0, 10e-6)" ] }, { @@ -758,7 +758,7 @@ "outputs": [], "source": [ "# plot measurement results\n", - "plot_result_2d(my_results, \"ac_0\")\n" + "plot_result_2d(my_results, \"ac_0\")" ] }, { @@ -769,7 +769,7 @@ "outputs": [], "source": [ "# use pulse sheet viewer to display the pulse sequence - only recommended for small number of averages and sweep steps to avoid performance issues\n", - "show_pulse_sheet(\"Length Rabi\", session.compiled_experiment)\n" + "show_pulse_sheet(\"Length Rabi\", session.compiled_experiment)" ] }, { @@ -787,7 +787,7 @@ "session.connect(do_emulation=use_emulation)\n", "\n", "# run experiment on qubit 1\n", - "my_results = session.run(exp)\n" + "my_results = session.run(exp)" ] }, { @@ -797,7 +797,7 @@ "tags": [] }, "source": [ - "# 5. Ramsey Experiment\n", + "## 5. Ramsey Experiment\n", "\n", "Sweep the delay between two slightly detuned pi/2 pulses to determine the qubit dephasing time as well as fine calibration its excited state frequency" ] @@ -807,7 +807,7 @@ "id": "ac0d5da7-7416-4f32-b3e8-0114efd383f7", "metadata": {}, "source": [ - "## 5.1 Define the Experiment" + "### 5.1 Define the Experiment" ] }, { @@ -826,7 +826,7 @@ "# readout integration weights\n", "readout_weighting_function = pulse_library.const(\n", " uid=\"readout_weighting_function\", length=400e-9, amplitude=1.0\n", - ")\n" + ")" ] }, { @@ -895,7 +895,7 @@ " )\n", " # relax time after readout - for signal processing and qubit relaxation to ground state\n", " with exp.section(uid=\"relax\", play_after=\"qubit_readout\"):\n", - " exp.delay(signal=\"measure\", time=1e-6)\n" + " exp.delay(signal=\"measure\", time=1e-6)" ] }, { @@ -910,7 +910,7 @@ " \"drive\": device_setup.logical_signal_groups[\"q0\"].logical_signals[\"drive_line\"],\n", " \"measure\": device_setup.logical_signal_groups[\"q0\"].logical_signals[\"measure_line\"],\n", " \"acquire\": device_setup.logical_signal_groups[\"q0\"].logical_signals[\"acquire_line\"],\n", - "}\n" + "}" ] }, { @@ -918,7 +918,7 @@ "id": "cd97d37a-c3d9-480a-94ed-568bc237bd64", "metadata": {}, "source": [ - "## 5.2 Run the Experiment and Plot the Measurement Results and Pulse Sequence" + "### 5.2 Run the Experiment and Plot the Measurement Results and Pulse Sequence" ] }, { @@ -936,7 +936,7 @@ "session.connect(do_emulation=use_emulation)\n", "\n", "# run on qubit 0\n", - "my_results = session.run(exp)\n" + "my_results = session.run(exp)" ] }, { @@ -947,7 +947,7 @@ "outputs": [], "source": [ "# Plot simulated output signals\n", - "plot_simulation(session.compiled_experiment, 0, 10e-6)\n" + "plot_simulation(session.compiled_experiment, 0, 10e-6)" ] }, { @@ -963,7 +963,7 @@ "outputs": [], "source": [ "# plot measurement results\n", - "plot_result_2d(my_results, \"ac_0\")\n" + "plot_result_2d(my_results, \"ac_0\")" ] }, { @@ -974,7 +974,7 @@ "outputs": [], "source": [ "# use pulse sheet viewer to display the pulse sequence - only recommended for small number of averages and sweep steps to avoid performance issues\n", - "show_pulse_sheet(\"Ramsey\", session.compiled_experiment)\n" + "show_pulse_sheet(\"Ramsey\", session.compiled_experiment)" ] }, { @@ -992,7 +992,7 @@ "session.connect(do_emulation=use_emulation)\n", "\n", "# run on qubit 1\n", - "my_results = session.run(exp)\n" + "my_results = session.run(exp)" ] }, { @@ -1002,7 +1002,7 @@ "tags": [] }, "source": [ - "# 6. T1 Experiment\n", + "## 6. T1 Experiment\n", "\n", "Sweep the delay between a qubit excitation pulse and the readout to measure the energy relaxation time of the qubit" ] @@ -1012,7 +1012,7 @@ "id": "76f8bebd-da55-487e-b290-9dc15f495678", "metadata": {}, "source": [ - "## 6.1 Define the Experiment" + "### 6.1 Define the Experiment" ] }, { @@ -1031,7 +1031,7 @@ "# readout integration weights\n", "readout_weighting_function = pulse_library.const(\n", " uid=\"readout_weighting_function\", length=400e-9, amplitude=1.0\n", - ")\n" + ")" ] }, { @@ -1089,7 +1089,7 @@ " )\n", " # relax time after readout - for signal processing and qubit relaxation to ground state\n", " with exp.section(uid=\"relax\", play_after=\"qubit_readout\"):\n", - " exp.delay(signal=\"measure\", time=1e-6)\n" + " exp.delay(signal=\"measure\", time=1e-6)" ] }, { @@ -1104,7 +1104,7 @@ " \"drive\": device_setup.logical_signal_groups[\"q0\"].logical_signals[\"drive_line\"],\n", " \"measure\": device_setup.logical_signal_groups[\"q0\"].logical_signals[\"measure_line\"],\n", " \"acquire\": device_setup.logical_signal_groups[\"q0\"].logical_signals[\"acquire_line\"],\n", - "}\n" + "}" ] }, { @@ -1112,7 +1112,7 @@ "id": "62b0e602-da60-468b-ae92-64ecc8fcc4c0", "metadata": {}, "source": [ - "## 6.2 Run the Experiment and Plot the Measurement Results and Pulse Sequence" + "### 6.2 Run the Experiment and Plot the Measurement Results and Pulse Sequence" ] }, { @@ -1130,7 +1130,7 @@ "session.connect(do_emulation=use_emulation)\n", "\n", "# run on qubit 0\n", - "my_results = session.run(exp)\n" + "my_results = session.run(exp)" ] }, { @@ -1141,7 +1141,7 @@ "outputs": [], "source": [ "# Plot simulated output signals\n", - "plot_simulation(session.compiled_experiment, 0, 10e-6)\n" + "plot_simulation(session.compiled_experiment, 0, 10e-6)" ] }, { @@ -1157,7 +1157,7 @@ "outputs": [], "source": [ "# plot measurement results\n", - "plot_result_2d(my_results, \"ac_0\")\n" + "plot_result_2d(my_results, \"ac_0\")" ] }, { @@ -1168,7 +1168,7 @@ "outputs": [], "source": [ "# use pulse sheet viewer to display the pulse sequence - only recommended for small number of averages and sweep steps to avoid performance issues\n", - "show_pulse_sheet(\"T1\", session.compiled_experiment)\n" + "show_pulse_sheet(\"T1\", session.compiled_experiment)" ] }, { @@ -1186,7 +1186,7 @@ "session.connect(do_emulation=use_emulation)\n", "\n", "# run on qubit 1\n", - "my_results = session.run(exp)\n" + "my_results = session.run(exp)" ] }, { @@ -1196,7 +1196,7 @@ "tags": [] }, "source": [ - "# 7. Ramsey with Sampled Pulse Definition\n", + "## 7. Ramsey with Sampled Pulse Definition\n", "\n", "Sweep the delay between two slightly detuned pi/2 pulses to determine the qubit dephasing time as well as fine calibration its excited state frequency.\n", "\n", @@ -1208,7 +1208,7 @@ "id": "f5982a0b-1c39-40bc-993e-7df9af3eb013", "metadata": {}, "source": [ - "## 7.1 Pulse definition" + "### 7.1 Pulse definition" ] }, { @@ -1244,7 +1244,7 @@ "# readout integration weights\n", "readout_weighting_function = pulse_library.const(\n", " uid=\"readout_weighting_function\", length=200e-9, amplitude=1.0\n", - ")\n" + ")" ] }, { @@ -1252,7 +1252,7 @@ "id": "0dcbc56d-20e2-4622-b7df-d7886e187cc8", "metadata": {}, "source": [ - "## 7.2 Define the Experiment" + "### 7.2 Define the Experiment" ] }, { @@ -1319,7 +1319,7 @@ " )\n", " # relax time after readout - for signal processing and qubit relaxation to ground state\n", " with exp.section(uid=\"relax\", play_after=\"qubit_readout\"):\n", - " exp.delay(signal=\"measure\", time=1e-6)\n" + " exp.delay(signal=\"measure\", time=1e-6)" ] }, { @@ -1327,7 +1327,7 @@ "id": "07957303-15c0-4251-9783-ea3ccd31dfb6", "metadata": {}, "source": [ - "## 7.3 Run the Experiment and Plot the Measurement Results and Pulse Sequence" + "### 7.3 Run the Experiment and Plot the Measurement Results and Pulse Sequence" ] }, { @@ -1345,7 +1345,7 @@ "session.connect(do_emulation=use_emulation)\n", "\n", "# run on qubit 0\n", - "my_results = session.run(exp)\n" + "my_results = session.run(exp)" ] }, { @@ -1356,7 +1356,7 @@ "outputs": [], "source": [ "# Plot simulated output signals\n", - "plot_simulation(session.compiled_experiment, 0, 10e-6)\n" + "plot_simulation(session.compiled_experiment, 0, 10e-6)" ] }, { @@ -1372,7 +1372,7 @@ "outputs": [], "source": [ "# plot measurement results\n", - "plot_result_2d(my_results, \"ac_0\")\n" + "plot_result_2d(my_results, \"ac_0\")" ] }, { @@ -1383,7 +1383,7 @@ "outputs": [], "source": [ "# use pulse sheet viewer to display the pulse sequence - only recommended for small number of averages and sweep steps to avoid performance issues\n", - "show_pulse_sheet(\"T1\", session.compiled_experiment)\n" + "show_pulse_sheet(\"T1\", session.compiled_experiment)" ] }, { @@ -1401,7 +1401,7 @@ "session.connect(do_emulation=use_emulation)\n", "\n", "# run on qubit 1\n", - "my_results = session.run(exp)\n" + "my_results = session.run(exp)" ] }, { @@ -1409,7 +1409,7 @@ "id": "e3e6f040-090c-48ed-9822-8897636ff850", "metadata": {}, "source": [ - "# 8. Pulsed Qubit Spectroscopy for Flux-Dependent Qubit\n", + "## 8. Pulsed Qubit Spectroscopy for Flux-Dependent Qubit\n", "\n", "Determine the flux-dependent resonance frequency of a qubit by investigating the change in resonator transmission when sweeping the frequency of a qubit excitation pulse" ] @@ -1419,7 +1419,7 @@ "id": "be8430c9-7c2b-4ef5-ba50-805b66c197b2", "metadata": {}, "source": [ - "## 8.1 Define the Experiment" + "### 8.1 Define the Experiment" ] }, { @@ -1440,7 +1440,7 @@ "# readout weights for integration\n", "readout_weighting_function = pulse_library.const(\n", " uid=\"readout_weighting_function\", length=400e-9, amplitude=1.0\n", - ")\n" + ")" ] }, { @@ -1468,7 +1468,7 @@ "flux_sweep = LinearSweepParameter(uid=\"flux_qubit\", start=0, stop=1, count=flux_count)\n", "\n", "# define number of averages\n", - "average_exponent = 4 # used for 2^n averages, n=average_exponent, maximum: n = 17\n" + "average_exponent = 4 # used for 2^n averages, n=average_exponent, maximum: n = 17" ] }, { @@ -1519,7 +1519,7 @@ " )\n", " # relax time after readout - for signal processing and qubit relaxation to ground state\n", " with exp.section(uid=\"relax\", play_after=\"qubit_readout\"):\n", - " exp.delay(signal=\"measure\", time=1e-6)\n" + " exp.delay(signal=\"measure\", time=1e-6)" ] }, { @@ -1553,7 +1553,7 @@ " \"acquire\": device_setup.logical_signal_groups[f\"q{qubit_id}\"].logical_signals[\n", " \"acquire_line\"\n", " ],\n", - " }\n" + " }" ] }, { @@ -1561,7 +1561,7 @@ "id": "3a6f57de-d25c-4c12-9c29-072c3b75b88d", "metadata": {}, "source": [ - "## 8.2 Run the Experiment and Plot the Measurement Results and Pulse Sequence" + "### 8.2 Run the Experiment and Plot the Measurement Results and Pulse Sequence" ] }, { @@ -1580,7 +1580,7 @@ "session.connect(do_emulation=use_emulation)\n", "\n", "# run experiment on qubit 0\n", - "my_results = session.run(exp)\n" + "my_results = session.run(exp)" ] }, { @@ -1591,7 +1591,7 @@ "outputs": [], "source": [ "# Plot simulated output signals\n", - "plot_simulation(session.compiled_experiment, 0, 10e-6)\n" + "plot_simulation(session.compiled_experiment, 0, 10e-6)" ] }, { @@ -1607,7 +1607,7 @@ "outputs": [], "source": [ "# plot measurement results\n", - "plot_result_3d(my_results, \"ac_0\")\n" + "plot_result_3d(my_results, \"ac_0\")" ] }, { @@ -1618,7 +1618,7 @@ "outputs": [], "source": [ "# use pulse sheet viewer to display the pulse sequence - only recommended for small number of averages and sweep steps to avoid performance issues\n", - "show_pulse_sheet(\"Qubit Flux Spectroscopy\", session.compiled_experiment)\n" + "show_pulse_sheet(\"Qubit Flux Spectroscopy\", session.compiled_experiment)" ] }, { @@ -1637,7 +1637,7 @@ "session.connect(do_emulation=use_emulation)\n", "\n", "# run experiment on qubit 1\n", - "my_results = session.run(exp)\n" + "my_results = session.run(exp)" ] }, { @@ -1645,7 +1645,7 @@ "id": "f6ead121", "metadata": {}, "source": [ - "# 9. Flux-Scope Experiment\n", + "## 9. Flux-Scope Experiment\n", "\n", "Experiment to characterise the distortions of flux pulses due to the imperfect signal lines, following chapter 4.4.3 in https://www.research-collection.ethz.ch/handle/20.500.11850/153681" ] @@ -1655,7 +1655,7 @@ "id": "7afbdca5", "metadata": {}, "source": [ - "## 9.1 Define the Experiment" + "### 9.1 Define the Experiment" ] }, { @@ -1680,7 +1680,7 @@ " uid=\"readout_weighting_function\", length=200e-9, amplitude=1.0\n", ")\n", "\n", - "# assuming all calibration settings are already correct\n" + "# assuming all calibration settings are already correct" ] }, { @@ -1711,7 +1711,7 @@ ")\n", "\n", "# define number of averages\n", - "average_exponent = 10 # used for 2^n averages, n=average_exponent, maximum: n = 19\n" + "average_exponent = 10 # used for 2^n averages, n=average_exponent, maximum: n = 19" ] }, { @@ -1760,7 +1760,7 @@ " )\n", " # relax time after readout - for signal processing and qubit relaxation to ground state\n", " with exp.section(uid=\"relax\", play_after=\"qubit_readout\"):\n", - " exp.delay(signal=\"measure\", time=1e-6)\n" + " exp.delay(signal=\"measure\", time=1e-6)" ] }, { @@ -1768,7 +1768,7 @@ "id": "2c4a02bc-a500-4acd-a5b0-d8ed4302bec0", "metadata": {}, "source": [ - "## 9.2 Run the Experiment and Plot the Measurement Results and Pulse Sequence" + "### 9.2 Run the Experiment and Plot the Measurement Results and Pulse Sequence" ] }, { @@ -1787,7 +1787,7 @@ "session.connect(do_emulation=use_emulation)\n", "\n", "# run experiment on qubit 0\n", - "my_results = session.run(exp)\n" + "my_results = session.run(exp)" ] }, { @@ -1798,7 +1798,7 @@ "outputs": [], "source": [ "# Plot simulated output signals\n", - "plot_simulation(session.compiled_experiment, 0, 10e-6)\n" + "plot_simulation(session.compiled_experiment, 0, 10e-6)" ] }, { @@ -1814,7 +1814,7 @@ "outputs": [], "source": [ "# plot measurement results\n", - "plot_result_3d(my_results, \"ac_0\")\n" + "plot_result_3d(my_results, \"ac_0\")" ] }, { @@ -1825,7 +1825,7 @@ "outputs": [], "source": [ "# use pulse sheet viewer to display the pulse sequence - only recommended for small number of averages and sweep steps to avoid performance issues\n", - "show_pulse_sheet(\"Flux Scope Experiment\", session.compiled_experiment)\n" + "show_pulse_sheet(\"Flux Scope Experiment\", session.compiled_experiment)" ] }, { @@ -1844,7 +1844,7 @@ "session.connect(do_emulation=use_emulation)\n", "\n", "# run experiment on qubit 1\n", - "my_results = session.run(exp)\n" + "my_results = session.run(exp)" ] }, { @@ -1852,7 +1852,7 @@ "id": "4cff8208-5413-4449-982e-d68f532f5fd2", "metadata": {}, "source": [ - "# 10. Cryoscope experiment\n", + "## 10. Cryoscope experiment\n", "\n", "Alternate experiment to characterise the pulse distortions from line impedance, following https://arxiv.org/pdf/1907.04818.pdf - Ramsey sequence with fixed timing and variable flux pulse in between - sweeping flux pulse length and amplitude" ] @@ -1862,7 +1862,7 @@ "id": "9543bc09-aa41-4af1-bec1-f050d61b901e", "metadata": {}, "source": [ - "## 10.1 Define the Experiment" + "### 10.1 Define the Experiment" ] }, { @@ -1881,7 +1881,7 @@ "# readout weights for integration\n", "readout_weighting_function = pulse_library.const(\n", " uid=\"readout_weighting_function\", length=200e-9, amplitude=1.0\n", - ")\n" + ")" ] }, { @@ -1905,7 +1905,7 @@ " start=length_start, stop=length_stop, count=length_count\n", ")\n", "\n", - "flux_pulse = pulse_library.const(uid=\"flux_pulse\", length=length_start, amplitude=1.0)\n" + "flux_pulse = pulse_library.const(uid=\"flux_pulse\", length=length_start, amplitude=1.0)" ] }, { @@ -1923,7 +1923,7 @@ ")\n", "\n", "# define number of averages\n", - "average_exponent = 1 # used for 2^n averages, n=average_exponent, maximum: n = 17\n" + "average_exponent = 1 # used for 2^n averages, n=average_exponent, maximum: n = 17" ] }, { @@ -1984,7 +1984,7 @@ " )\n", " # relax time after readout - for signal processing and qubit relaxation to ground state\n", " with exp.section(uid=\"relax\", play_after=\"qubit_readout\"):\n", - " exp.delay(signal=\"measure\", time=1e-6)\n" + " exp.delay(signal=\"measure\", time=1e-6)" ] }, { @@ -1992,7 +1992,7 @@ "id": "cccae36a-a47f-4328-8f8a-062b8836f574", "metadata": {}, "source": [ - "## 10.2 Run the Experiment and Plot the Measurement Results and Pulse Sequence" + "### 10.2 Run the Experiment and Plot the Measurement Results and Pulse Sequence" ] }, { @@ -2009,7 +2009,7 @@ "session = Session(device_setup=device_setup)\n", "session.connect(do_emulation=use_emulation)\n", "\n", - "my_results = session.run(exp)\n" + "my_results = session.run(exp)" ] }, { @@ -2020,7 +2020,7 @@ "outputs": [], "source": [ "# Plot simulated output signals\n", - "plot_simulation(session.compiled_experiment, 0, 10e-6)\n" + "plot_simulation(session.compiled_experiment, 0, 10e-6)" ] }, { @@ -2036,7 +2036,7 @@ "outputs": [], "source": [ "# plot measurement results\n", - "plot_result_3d(my_results, \"ac_0\")\n" + "plot_result_3d(my_results, \"ac_0\")" ] }, { @@ -2047,7 +2047,7 @@ "outputs": [], "source": [ "# use pulse sheet viewer to display the pulse sequence - only recommended for small number of averages and sweep steps to avoid performance issues\n", - "show_pulse_sheet(\"Cryoscope\", session.compiled_experiment)\n" + "show_pulse_sheet(\"Cryoscope\", session.compiled_experiment)" ] }, { @@ -2064,7 +2064,7 @@ "session = Session(device_setup=device_setup)\n", "session.connect(do_emulation=use_emulation)\n", "\n", - "my_results = session.run(exp)\n" + "my_results = session.run(exp)" ] }, { diff --git a/examples/02_advanced_qubit_experiments/00_active_qubit_reset_shfsg_shfqa_shfqc.ipynb b/examples/02_advanced_qubit_experiments/00_active_qubit_reset_shfsg_shfqa_shfqc.ipynb index 458a0da..f5c347a 100644 --- a/examples/02_advanced_qubit_experiments/00_active_qubit_reset_shfsg_shfqa_shfqc.ipynb +++ b/examples/02_advanced_qubit_experiments/00_active_qubit_reset_shfsg_shfqa_shfqc.ipynb @@ -14,7 +14,7 @@ "We require either a SHFQC instrument for this notebook or a combination of SHFSG and SHFQA connected via a PQSC. \n", "\n", "This demonstration runs without real qubits, assuming a loopback on the readout drive line directly into the reaoud acquisition line. We emulate the different qubit states by two different readout measurement pulses, differing by a phase. \n", - "To demonstrate real-time feedback, we first calibrate the state discrimintation unit for the two measurement pulsese we choose to emulate the qubit response. The we use this calibration to playback an arbitrary simualted pattern of qubit states and demonstrate the real-time feedback capabilities of the instrument. " + "To demonstrate real-time feedback, we first calibrate the state discrimintation unit for the two measurement pulsese we choose to emulate the qubit response. The we use this calibration to play an arbitrary simulated pattern of qubit states and demonstrate the real-time feedback capabilities of the instrument. " ] }, { @@ -23,7 +23,7 @@ "id": "4d4e7d0b-b53a-40e4-831c-236ed9d97c42", "metadata": {}, "source": [ - "# 0. General Imports and Definitions" + "## 0. General Imports and Definitions" ] }, { @@ -35,7 +35,7 @@ "tags": [] }, "source": [ - "## 0.1 Python Imports " + "### 0.1 Python Imports " ] }, { @@ -51,7 +51,6 @@ "from laboneq.simple import *\n", "\n", "# helper import\n", - "from laboneq.contrib.example_helpers.qubit_helper import QubitParameters, Qubit\n", "from laboneq.contrib.example_helpers.feedback_helper import (\n", " complex_freq_phase,\n", " exp_raw,\n", @@ -92,7 +91,7 @@ "id": "ce9078f7", "metadata": {}, "source": [ - "# 1. Define the Device Setup and apply baseline calibration\n", + "## 1. Define the Device Setup and apply baseline calibration\n", "\n", "We'll load a descriptor file to define our device setup and logical signal lines and then apply a baseline calibration to the signal lines based on a dictionary of qubit parameters" ] @@ -103,7 +102,7 @@ "id": "610a3cb7", "metadata": {}, "source": [ - "## 1.1 DeviceSetup from descriptor" + "### 1.1 DeviceSetup from descriptor" ] }, { @@ -136,7 +135,7 @@ "id": "98dc2fac", "metadata": {}, "source": [ - "## 1.2 Baseline calibration parameters as dictionary" + "### 1.2 Baseline calibration parameters as dictionary" ] }, { @@ -147,8 +146,8 @@ "outputs": [], "source": [ "base_qubit_parameters = {\n", - " \"frequency\": 100e6, # qubit drive frequency in [Hz] - relative to local oscillator for qubit drive upconversion\n", - " \"readout_frequency\": -100e6,\n", + " \"resonance_frequency\": 1100e6, # qubit drive frequency in [Hz] - relative to local oscillator for qubit drive upconversion\n", + " \"readout_resonator_frequency\": 900e6,\n", " \"readout_length\": 400e-9,\n", " \"readout_amplitude\": 0.4,\n", " \"readout_integration_delay\": 20e-9,\n", @@ -173,8 +172,38 @@ "outputs": [], "source": [ "# define qubit object, containing all relevant information for the tuneup experiments\n", - "my_parameters = QubitParameters(base_qubit_parameters)\n", - "my_qubit = Qubit(0, base_qubit_parameters)" + "my_parameters = QubitParameters(\n", + " resonance_frequency=base_qubit_parameters[\"resonance_frequency\"],\n", + " drive_lo_frequency=base_qubit_parameters[\"drive_lo_frequency\"],\n", + " readout_resonator_frequency=base_qubit_parameters[\"readout_resonator_frequency\"],\n", + " readout_lo_frequency=base_qubit_parameters[\"readout_lo_frequency\"],\n", + " readout_integration_delay=base_qubit_parameters[\"readout_integration_delay\"],\n", + " drive_range=base_qubit_parameters[\"drive_range\"],\n", + " readout_range_out=base_qubit_parameters[\"readout_range_out\"],\n", + " readout_range_in=base_qubit_parameters[\"readout_range_in\"],\n", + " user_defined={\n", + " \"readout_length\": base_qubit_parameters[\"readout_length\"],\n", + " \"readout_amplitude\": base_qubit_parameters[\"readout_amplitude\"],\n", + " \"pi_amplitude\": base_qubit_parameters[\"pi_amplitude\"],\n", + " \"pi_2_amplitude\": base_qubit_parameters[\"pi_2_amplitude\"],\n", + " \"pulse_length\": base_qubit_parameters[\"pulse_length\"],\n", + " \"readout_data_delay\": base_qubit_parameters[\"readout_data_delay\"],\n", + " },\n", + ")\n", + "# my_qubit = Qubit(0, base_qubit_parameters)\n", + "my_qubit = Qubit.from_logical_signal_group(\n", + " \"q0\", my_setup.logical_signal_groups[\"q0\"], my_parameters\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "069468dd", + "metadata": {}, + "source": [ + "### 1.3 Calibration with Dummy Emulation state\n", + "\n", + "Below, we define a calibration using an extra measure line to emulate the excited state of the qubit. In a real experiment, the calibration can be generated from your qubit object with `my_setup.set_calibration(my_qubit.calibration())`, without the need to sepcify and set the calibration as we do here." ] }, { @@ -187,10 +216,10 @@ "my_base_calibration = Calibration()\n", "# qubit drive line\n", "my_base_calibration[\n", - " f\"/logical_signal_groups/q{my_qubit.id}/drive_line\"\n", + " f\"/logical_signal_groups/{my_qubit.uid}/drive_line\"\n", "] = SignalCalibration(\n", " oscillator=Oscillator(\n", - " frequency=my_qubit.parameters.frequency,\n", + " frequency=my_qubit.parameters.drive_frequency,\n", " modulation_type=ModulationType.HARDWARE,\n", " ),\n", " local_oscillator=Oscillator(\n", @@ -200,7 +229,7 @@ ")\n", "# qubit measure line - for pulse emulating state 0\n", "my_base_calibration[\n", - " f\"/logical_signal_groups/q{my_qubit.id}/measure_line\"\n", + " f\"/logical_signal_groups/{my_qubit.uid}/measure_line\"\n", "] = SignalCalibration(\n", " oscillator=Oscillator(\n", " frequency=my_qubit.parameters.readout_frequency,\n", @@ -212,9 +241,7 @@ " range=my_qubit.parameters.readout_range_out,\n", ")\n", "# qubit measure line - for pulse emulating state 1\n", - "my_base_calibration[\n", - " f\"/logical_signal_groups/q{my_qubit.id+1}/measure_line\"\n", - "] = SignalCalibration(\n", + "my_base_calibration[f\"/logical_signal_groups/q1/measure_line\"] = SignalCalibration(\n", " oscillator=Oscillator(\n", " frequency=my_qubit.parameters.readout_frequency,\n", " modulation_type=ModulationType.SOFTWARE,\n", @@ -226,7 +253,7 @@ ")\n", "# qubit acquire line - no baseband modulation applied\n", "my_base_calibration[\n", - " f\"/logical_signal_groups/q{my_qubit.id}/acquire_line\"\n", + " f\"/logical_signal_groups/{my_qubit.uid}/acquire_line\"\n", "] = SignalCalibration(\n", " oscillator=None,\n", " local_oscillator=Oscillator(\n", @@ -249,9 +276,13 @@ }, "outputs": [], "source": [ - "# apply calibration to device setup\n", + "# apply dummy calibration to device setup\n", + "# comment out if not emulating\n", "my_setup.set_calibration(my_base_calibration)\n", "\n", + "# apply calibration from qubit object\n", + "# my_setup.set_calibration(my_qubit.calibration())\n", + "\n", "q0 = my_setup.logical_signal_groups[\"q0\"].logical_signals\n", "q1 = my_setup.logical_signal_groups[\"q1\"].logical_signals" ] @@ -273,7 +304,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# 2. Calibration of state discrimination\n", + "## 2. Calibration of state discrimination\n", "\n", "We determine the optimal integration weights by subtracting and conjugating the raw response corresponding to the two different qubit states. We then additionall rotate these integration weights to result in maximum separation of the resulting IQ valuebs on the real axis and set the threshold to the setup calibration." ] @@ -283,7 +314,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 2.1 Define measurement pulse waveforms to simulate measurement of |0> and |1> qubit states" + "### 2.1 Define measurement pulse waveforms to simulate measurement of |0> and |1> qubit states" ] }, { @@ -293,7 +324,7 @@ "outputs": [], "source": [ "# measure pulse parameters\n", - "pulse_len = my_qubit.parameters.readout_length\n", + "pulse_len = my_qubit.parameters.user_defined[\"readout_length\"]\n", "pulse_phase = np.pi / 4\n", "\n", "# sampling rate of SHFQC\n", @@ -302,7 +333,11 @@ "pulse_freq = 0.0\n", "measure0_gen2 = pulse_library.sampled_pulse_complex(\n", " complex_freq_phase(\n", - " sampling_rate, pulse_len, pulse_freq, my_qubit.parameters.readout_amplitude, 0\n", + " sampling_rate,\n", + " pulse_len,\n", + " pulse_freq,\n", + " my_qubit.parameters.user_defined[\"readout_amplitude\"],\n", + " 0,\n", " )\n", ")\n", "measure1_gen2 = pulse_library.sampled_pulse_complex(\n", @@ -310,7 +345,7 @@ " sampling_rate,\n", " pulse_len,\n", " pulse_freq,\n", - " my_qubit.parameters.readout_amplitude,\n", + " my_qubit.parameters.user_defined[\"readout_amplitude\"],\n", " pulse_phase,\n", " )\n", ")" @@ -321,7 +356,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 2.2 Determine optimal integration weights based on raw readout results of two measurement pulses" + "### 2.2 Determine optimal integration weights based on raw readout results of two measurement pulses" ] }, { @@ -352,7 +387,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 2.3 Determine optimal rotation of integration weights and discrimination threshold" + "### 2.3 Determine optimal rotation of integration weights and discrimination threshold" ] }, { @@ -425,15 +460,11 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 2.4 Checks status of state discrimination calibration" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 2.4.1 check for proper rotation of kernel - IQ values should be maximally separate on the real axis\n" + "### 2.4 Check status of state discrimination calibration\n", + "\n", + "#### 2.4.1 Check for proper rotation of kernel\n", + "\n", + "IQ values should be maximally separate on the real axis" ] }, { @@ -477,7 +508,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### 2.4.2 Check correct state discrimination when including rotation of integration weights" + "#### 2.4.2 Check correct state discrimination when including rotation of integration weights" ] }, { @@ -509,9 +540,40 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# 3. Feedback experiment\n", + "## 3. Feedback experiment\n", "\n", - "Here we create a real-time feedback demonstration that plays back a user defined sequence of \"qubit states\" i.e. a sequences of different measurment pulses emulating different qubit states. The measured qubit state after state discrimination is used in a real-time feedback section to playback either of two pulses: x90 for the qubit in its ground state and x180 for the qubit in the excited state. " + "Here we create a real-time feedback demonstration that plays back a user defined sequence of \"qubit states\" i.e. a sequences of different measurement pulses emulating different qubit states. The measured qubit state after state discrimination is used in a real-time feedback section to playback either of two pulses: x90 for the qubit in its ground state and x180 for the qubit in the excited state. " + ] + }, + { + "cell_type": "markdown", + "id": "85b9cc35", + "metadata": {}, + "source": [ + "### 3.0 Define Pulses" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5dd4faf4", + "metadata": {}, + "outputs": [], + "source": [ + "x90 = pulse_library.drag(\n", + " uid=f\"x90_q{id}\",\n", + " length=my_qubit.parameters.user_defined[\"pulse_length\"],\n", + " amplitude=my_qubit.parameters.user_defined[\"pi_2_amplitude\"],\n", + " sigma=0.3,\n", + " beta=0.4,\n", + ")\n", + "x180 = pulse_library.drag(\n", + " uid=f\"x180_q{id}\",\n", + " length=my_qubit.parameters.user_defined[\"pulse_length\"],\n", + " amplitude=my_qubit.parameters.user_defined[\"pi_amplitude\"],\n", + " sigma=0.3,\n", + " beta=0.4,\n", + ")" ] }, { @@ -519,7 +581,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 3.1 Define Experiment" + "### 3.1 Define Experiment" ] }, { @@ -539,8 +601,8 @@ " integration_weights=my_integration_weights,\n", " acquisition_type=AcquisitionType.DISCRIMINATION,\n", " # parameters that determine the type of pulse sequence to be played\n", - " x90=my_qubit.pulses.qubit_x90,\n", - " x180=my_qubit.pulses.qubit_x180,\n", + " x90=x90,\n", + " x180=x180,\n", " pattern_delay=1e-6,\n", "):\n", " exp = Experiment(\n", @@ -575,7 +637,7 @@ " exp.delay(signal=\"drive\", time=5 * x90.length)\n", " # qubit state readout\n", " with exp.section(uid=f\"measure_{id}\", play_after=f\"drive_{id}\"):\n", - " # emulate qubit state by playing different measurment pulses based on pattern\n", + " # emulate qubit state by playing different measurement pulses based on pattern\n", " if letter == \"0\":\n", " exp.play(signal=\"measure0\", pulse=measure_pulse0)\n", " else:\n", @@ -629,7 +691,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 3.3 Run experiment" + "### 3.3 Run experiment" ] }, { @@ -717,9 +779,9 @@ ], "metadata": { "kernelspec": { - "display_name": "develop", + "display_name": "ZI_LabOneQ_2p11", "language": "python", - "name": "develop" + "name": "python3" }, "language_info": { "codemirror_mode": { @@ -731,14 +793,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.0" + "version": "3.11.4" }, - "orig_nbformat": 4, - "vscode": { - "interpreter": { - "hash": "13449c5f825fe7a87315d4df6bc389ef4adf2b2262bad717199e4a7c71a2a192" - } - } + "orig_nbformat": 4 }, "nbformat": 4, "nbformat_minor": 5 diff --git a/examples/02_advanced_qubit_experiments/01_randomized_benchmarking.ipynb b/examples/02_advanced_qubit_experiments/01_randomized_benchmarking.ipynb index 0e94916..e899ba5 100644 --- a/examples/02_advanced_qubit_experiments/01_randomized_benchmarking.ipynb +++ b/examples/02_advanced_qubit_experiments/01_randomized_benchmarking.ipynb @@ -19,7 +19,7 @@ "id": "fad0c51b", "metadata": {}, "source": [ - "# 0. General Imports and Definitions" + "## 0. General Imports and Definitions" ] }, { @@ -28,7 +28,7 @@ "id": "7b01c7db", "metadata": {}, "source": [ - "## 0.1 Python Imports" + "### 0.1 Python Imports" ] }, { @@ -80,7 +80,7 @@ "id": "f6f2a1fa", "metadata": {}, "source": [ - "# 1. Setting up the LabOne Q Software\n", + "## 1. Setting up the LabOne Q Software\n", "\n", "Define the device setup, experimental parameters and baseline calibration\n", "\n", @@ -93,7 +93,7 @@ "id": "c463c14f", "metadata": {}, "source": [ - "## 1.1 Setup Descriptor" + "### 1.1 Setup Descriptor" ] }, { @@ -150,7 +150,7 @@ "id": "f1744463", "metadata": {}, "source": [ - "## 1.2 Define Qubit / Experiment Parameters" + "### 1.2 Define Qubit / Experiment Parameters" ] }, { @@ -195,7 +195,7 @@ "id": "c2dfa493", "metadata": {}, "source": [ - "## 1.3 Baseline Calibration for Device Setup" + "### 1.3 Baseline Calibration for Device Setup" ] }, { @@ -272,7 +272,7 @@ "id": "001f2e70", "metadata": {}, "source": [ - "## 1.4 Create Device Setup and Apply Baseline Calibration" + "### 1.4 Create Device Setup and Apply Baseline Calibration" ] }, { @@ -321,7 +321,7 @@ "id": "84a4ef0a", "metadata": {}, "source": [ - "## 1.5 Create a Session and Connect to it" + "### 1.5 Create a Session and Connect to it" ] }, { @@ -343,7 +343,7 @@ "id": "989af646", "metadata": {}, "source": [ - "# 2. Randomized Benchmarking\n", + "## 2. Randomized Benchmarking\n", "\n", "Perform a randomized benchmarking experiment on a qubit" ] @@ -354,7 +354,7 @@ "id": "441d7a4c", "metadata": {}, "source": [ - "## 2.1 Additional Experimental Parameters and Pulses\n", + "### 2.1 Additional Experimental Parameters and Pulses\n", "\n", "Define the number of averages and the pulses used in the experiment" ] @@ -387,7 +387,7 @@ "id": "6a79d209", "metadata": {}, "source": [ - "### 2.1.1 Adjust Pulse Parameters for Clifford Gates\n", + "#### 2.1.1 Adjust Pulse Parameters for Clifford Gates\n", "\n", "Calculate the basic gate set and the pulse objects corresponding to them" ] @@ -417,7 +417,7 @@ "id": "f04294e1", "metadata": {}, "source": [ - "## 2.2 Define and run the RB Experiment \n", + "### 2.2 Define and run the RB Experiment \n", "The RB experiment will consist of random sequences of different lengths, where each sequence length has a number of instances." ] }, @@ -539,7 +539,7 @@ "id": "352dbf2a", "metadata": {}, "source": [ - "# Process Results and Plot\n", + "## 3. Process Results and Plot\n", "For each sequence length, the acquired results are averaged and then plotted." ] }, diff --git a/examples/03_superconducting_qubits/00_qubit_tuneup_shfsg_shfqa_shfqc.ipynb b/examples/03_superconducting_qubits/00_qubit_tuneup_shfsg_shfqa_shfqc.ipynb index 5a60c7c..f838a64 100644 --- a/examples/03_superconducting_qubits/00_qubit_tuneup_shfsg_shfqa_shfqc.ipynb +++ b/examples/03_superconducting_qubits/00_qubit_tuneup_shfsg_shfqa_shfqc.ipynb @@ -23,18 +23,8 @@ "id": "4d4e7d0b-b53a-40e4-831c-236ed9d97c42", "metadata": {}, "source": [ - "# 0. General Imports and Definitions" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "f3ecf2ff-f304-472a-b6d2-a50792a39a0d", - "metadata": { - "tags": [] - }, - "source": [ - "## 0.1 Python Imports " + "## 0. General Imports and Definitions\n", + "### 0.1 Python Imports " ] }, { @@ -88,16 +78,8 @@ "tags": [] }, "source": [ - "# 1. Define the Instrument Setup and Required Experimental Parameters" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "2a4a933f", - "metadata": {}, - "source": [ - "## 1.1 Create device setup\n", + "## 1. Define the Instrument Setup and Required Experimental Parameters\n", + "### 1.1 Create device setup\n", "\n", "Create the device setup from the descriptor, and apply some convenient mapping to instruments and logical signals." ] @@ -151,7 +133,7 @@ "tags": [] }, "source": [ - "## 1.2 Qubit Parameters\n", + "### 1.2 Qubit Parameters\n", "\n", "A python dictionary containing all parameters needed to control and readout the qubits - frequencies, pulse lengths, timings\n", "\n", @@ -170,7 +152,7 @@ }, "outputs": [], "source": [ - "# a function to define a collection of signle qubit control and readout parameters as a python dictionary\n", + "# a function to define a collection of single qubit control and readout parameters as a python dictionary\n", "def single_qubit_parameters():\n", " return {\n", " \"freq\": 100e6, # qubit 0 drive frequency in [Hz] - relative to local oscillator for qubit drive upconversion\n", @@ -214,7 +196,7 @@ "tags": [] }, "source": [ - "## 1.3 Setup Calibration\n", + "### 1.3 Setup Calibration\n", "\n", "Generate a calibration object from the qubit control and readout parameters" ] @@ -308,16 +290,8 @@ "tags": [] }, "source": [ - "# 2. Apply Calibration Data, Connect to the Instruments" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "7a113bea-2b36-4f29-9eb7-2b76b061983d", - "metadata": {}, - "source": [ - "## 2.1 Set Calibration\n", + "## 2. Apply Calibration Data, Connect to the Instruments\n", + "### 2.1 Set Calibration\n", "\n", "Create qubit control and readout calibration, and apply it to the device setup." ] @@ -341,7 +315,7 @@ "id": "62ae58f8-4016-43e2-8c33-ee88645c7268", "metadata": {}, "source": [ - "## 2.2 Create and Connect to a QCCS Session \n", + "### 2.2 Create and Connect to a QCCS Session \n", "\n", "Establishes the connection to the instruments and readies them for experiments" ] @@ -369,7 +343,7 @@ "tags": [] }, "source": [ - "# 3. Qubit Tuneup - Experimental Sequence\n", + "## 3. Qubit Tuneup - Experimental Sequence\n", "\n", "Sequence of experiments for tuneup from scratch of a superconducting qubit in circuit QED architecture " ] @@ -382,7 +356,7 @@ "tags": [] }, "source": [ - "## 3.1 Resonator Spectroscopy: CW\n", + "### 3.1 Resonator Spectroscopy: CW\n", "\n", "Find the resonance frequency of the qubit readout resonator by looking at the transmission or reflection of a probe signal applied through the readout line" ] @@ -395,9 +369,9 @@ "tags": [] }, "source": [ - "### 3.1.1 Additional Experimental Parameters\n", + "#### 3.1.1 Additional Experimental Parameters\n", "\n", - "Define the frequency scan and the excitation pulse" + "Define the frequency scan" ] }, { @@ -433,7 +407,7 @@ "tags": [] }, "source": [ - "### 3.1.2 Experiment Definition\n", + "#### 3.1.2 Experiment Definition\n", "\n", "Define the experimental pulse and readout sequence - here without any explicit qubit reference\n", "\n", @@ -531,7 +505,7 @@ "tags": [] }, "source": [ - "### 3.1.3 Run and Evaluate Experiment\n", + "#### 3.1.3 Run and Evaluate Experiment\n", "Runs the experiment and evaluates the data returned by the measurement" ] }, @@ -626,7 +600,7 @@ "id": "d9a8dc1f", "metadata": {}, "source": [ - "#### Update Calibration" + "#### 3.1.4 Update Calibration" ] }, { @@ -653,7 +627,17 @@ "id": "253bbdd5", "metadata": {}, "source": [ - "## 3.2 Resonator Spectroscopy: Pulsed" + "### 3.2 Resonator Spectroscopy: Pulsed" + ] + }, + { + "cell_type": "markdown", + "id": "f6a129d9", + "metadata": {}, + "source": [ + "#### 3.2.1 Additional Experimental Parameters\n", + "\n", + "Define the frequency scan and the pulse" ] }, { @@ -707,6 +691,14 @@ "readout_pulse = create_readout_pulse(\"q0\")" ] }, + { + "cell_type": "markdown", + "id": "42123d67", + "metadata": {}, + "source": [ + "#### 3.2.2 Experiment Definition" + ] + }, { "cell_type": "code", "execution_count": null, @@ -751,6 +743,14 @@ " return exp_spec_pulsed" ] }, + { + "cell_type": "markdown", + "id": "355e6399", + "metadata": {}, + "source": [ + "#### 3.2.3 Apply Experiment Parameters and Compile" + ] + }, { "cell_type": "code", "execution_count": null, @@ -780,10 +780,18 @@ "compiled_spec_pulsed = session.compile(exp_spec_pulsed)\n", "\n", "Path(\"Pulse_Sheets\").mkdir(parents=True, exist_ok=True)\n", - "# generate a pulse sheet to inspect experiment befor runtime\n", + "# generate a pulse sheet to inspect experiment before runtime\n", "show_pulse_sheet(\"Pulse_Sheets/Pulsed_Spectroscopy\", compiled_spec_pulsed)" ] }, + { + "cell_type": "markdown", + "id": "edce904b", + "metadata": {}, + "source": [ + "#### 3.2.4 Run and Evaluate Experiment" + ] + }, { "cell_type": "code", "execution_count": null, @@ -835,7 +843,7 @@ "id": "49394dee", "metadata": {}, "source": [ - "### 3.2.1 Update calibration\n", + "#### 3.2.5 Update calibration\n", "Extract the resonance frequency and update the calibration" ] }, @@ -864,9 +872,10 @@ "id": "05c3da67", "metadata": {}, "source": [ - "## 3.3 Resonator Spectroscopy v/ Power\n", + "### 3.3 Resonator Spectroscopy v/ Power\n", "* _It is possible to define the spectroscopy experiments in a more general way, so that CW and pulsed spectroscopy can be chosen with an argument to the function. However, this is not done here for the sake of clarity_\n", - "* _One of the next releases will allow for real-time frequency sweep on the SHFQA, which will make the experiment much faster_" + "* _One of the next releases will allow for real-time frequency sweep on the SHFQA, which will make the experiment much faster_\n", + "#### 3.3.1 Additional experimental parameters" ] }, { @@ -886,6 +895,14 @@ "num_amp_points = 21" ] }, + { + "cell_type": "markdown", + "id": "6f2deecf", + "metadata": {}, + "source": [ + "#### 3.3.2 Experiment Definition" + ] + }, { "cell_type": "code", "execution_count": null, @@ -939,6 +956,14 @@ " return exp_spec" ] }, + { + "cell_type": "markdown", + "id": "d335eaa1", + "metadata": {}, + "source": [ + "#### 3.3.3 Apply Experiment Parameters and Compile" + ] + }, { "cell_type": "code", "execution_count": null, @@ -970,7 +995,7 @@ "compiled_spec_amp = session.compile(exp_spec_amp)\n", "\n", "Path(\"Pulse_Sheets\").mkdir(parents=True, exist_ok=True)\n", - "# generate a pulse sheet to inspect experiment befor runtime\n", + "# generate a pulse sheet to inspect experiment before runtime\n", "show_pulse_sheet(\n", " \"Pulse_Sheets/Spectroscopy_vs_Amplitude_Pulse_Sheet\", compiled_spec_amp\n", ")" @@ -991,6 +1016,14 @@ "print(f\"File saved as Results/{timestamp}_spec_amp_results.json\")" ] }, + { + "cell_type": "markdown", + "id": "e1e7e747", + "metadata": {}, + "source": [ + "#### 3.3.4 Run and Evaluate Experiment" + ] + }, { "cell_type": "code", "execution_count": null, @@ -1058,9 +1091,9 @@ "id": "aeaa0821", "metadata": {}, "source": [ - "## 3.4 Propagation Delay\n", - "\n", - "Sweep the delay of the integration, and then find the maximum result" + "### 3.4 Propagation Delay\n", + "Sweep the delay of the integration, and then find the maximum result\n", + "#### 3.4.1 Additional experimental parameters" ] }, { @@ -1080,6 +1113,14 @@ "num_averages = 4" ] }, + { + "cell_type": "markdown", + "id": "2610be3a", + "metadata": {}, + "source": [ + "#### 3.4.2 Experiment Definition" + ] + }, { "cell_type": "code", "execution_count": null, @@ -1129,13 +1170,11 @@ ] }, { - "cell_type": "code", - "execution_count": null, - "id": "31c8975b", + "cell_type": "markdown", + "id": "6d8e586d", "metadata": {}, - "outputs": [], "source": [ - "short_readout_pulse = create_readout_pulse(\"q0\", length=600e-9, width=200e-9, sigma=0.2)" + "#### 3.4.3 Apply Experiment Parameters and Compile" ] }, { @@ -1145,6 +1184,8 @@ "metadata": {}, "outputs": [], "source": [ + "short_readout_pulse = create_readout_pulse(\"q0\", length=600e-9, width=200e-9, sigma=0.2)\n", + "\n", "device_setup.set_calibration(\n", " define_calibration(device_setup, qubit_parameters, lo_settings)\n", ")\n", @@ -1165,10 +1206,18 @@ "compiled_prop_delay = session.compile(exp_prop_delay)\n", "\n", "Path(\"Pulse_Sheets\").mkdir(parents=True, exist_ok=True)\n", - "# generate a pulse sheet to inspect experiment befor runtime\n", + "# generate a pulse sheet to inspect experiment before runtime\n", "show_pulse_sheet(\"Pulse_Sheets/Propagation_delay\", compiled_prop_delay)" ] }, + { + "cell_type": "markdown", + "id": "6a649f52", + "metadata": {}, + "source": [ + "#### 3.4.4 Run and Evaluate Experiment" + ] + }, { "cell_type": "code", "execution_count": null, @@ -1208,7 +1257,7 @@ "tags": [] }, "source": [ - "## 3.5 Pulsed Qubit Spectroscopy\n", + "### 3.5 Pulsed Qubit Spectroscopy\n", "\n", "Find the resonance frequency of the qubit by looking at the change in resonator transmission when sweeping the frequency of a qubit excitation pulse" ] @@ -1222,7 +1271,7 @@ "tags": [] }, "source": [ - "### 3.5.1 Additional Experimental Parameters\n", + "#### 3.5.1 Additional Experimental Parameters\n", "\n", "Define the frequency scan and the pulses used in the experiment" ] @@ -1275,7 +1324,7 @@ "id": "fefd645d-ceee-41d3-a86c-403d63d5b4f1", "metadata": {}, "source": [ - "### 3.5.2 Experiment Definition\n", + "#### 3.5.2 Experiment Definition\n", "\n", "The frequency sweep of the drive line can now be done in real time (was: near time in older software releases)" ] @@ -1374,7 +1423,7 @@ "id": "46bf613c-2f03-4a02-8bc0-1201b845468a", "metadata": {}, "source": [ - "### 3.5.3 Run and Evaluate Experiment for Both Qubits\n", + "#### 3.5.3 Run and Evaluate Experiment for Both Qubits\n", "\n", "Runs the experiment and evaluates the data returned by the measurement" ] @@ -1413,7 +1462,7 @@ "compiled_qspec = session.compile(exp_qspec)\n", "\n", "Path(\"Pulse_Sheets\").mkdir(parents=True, exist_ok=True)\n", - "# generate a pulse sheet to inspect experiment befor runtime\n", + "# generate a pulse sheet to inspect experiment before runtime\n", "show_pulse_sheet(\"Pulse_Sheets/Qubit_Spectroscopy\", compiled_qspec)\n", "\n", "plot_simulation(compiled_qspec, 0, 100e-6)" @@ -1511,7 +1560,7 @@ "tags": [] }, "source": [ - "## 3.6 Amplitude Rabi Experiment\n", + "### 3.6 Amplitude Rabi Experiment\n", "\n", "Sweep the pulse amplitude of a qubit drive pulse to determine the ideal amplitudes for specific qubit rotation angles" ] @@ -1524,7 +1573,7 @@ "tags": [] }, "source": [ - "### 3.6.1 Additional Experimental Parameters\n", + "#### 3.6.1 Additional Experimental Parameters\n", "\n", "Define the amplitude sweep range and qubit excitation pulse" ] @@ -1577,7 +1626,7 @@ "tags": [] }, "source": [ - "### 3.6.2 Experiment Definition\n", + "#### 3.6.2 Experiment Definition\n", "\n", "Define the experimental pulse and readout sequence - here without any explicit qubit reference\n", "\n", @@ -1648,7 +1697,7 @@ "tags": [] }, "source": [ - "### 3.6.3 Set Experiment Parameters and Compile" + "#### 3.6.3 Set Experiment Parameters and Compile" ] }, { @@ -1681,7 +1730,7 @@ "id": "303ac382", "metadata": {}, "source": [ - "### 3.6.4 Show Pulse Sheet" + "#### 3.6.4 Show Pulse Sheet" ] }, { @@ -1700,7 +1749,7 @@ "id": "35e3b45e", "metadata": {}, "source": [ - "### 3.6.5 Plot Simulated Outputs" + "#### 3.6.5 Plot Simulated Outputs" ] }, { @@ -1720,7 +1769,7 @@ "id": "145af2b3", "metadata": {}, "source": [ - "### 3.6.6 Run, Save, and Plot Results\n", + "#### 3.6.6 Run, Save, and Plot Results\n", "\n", "Finally, you'll run the experiment, save, and plot the results." ] @@ -1788,7 +1837,7 @@ "id": "71848b75", "metadata": {}, "source": [ - "## 3.7 Ramsey Experiment\n", + "### 3.7 Ramsey Experiment\n", "The Ramsey experiment is different from the experiments above as the length of the drive section changes. Using a right-aligned sweep section and the automatic repetition time makes sure that the experiment is run as efficiently as possible on the Zurich Instruments hardware." ] }, @@ -1798,7 +1847,7 @@ "id": "81e8012b", "metadata": {}, "source": [ - "### 3.7.1 Experiment Parameters" + "#### 3.7.1 Experiment Parameters" ] }, { @@ -1842,7 +1891,7 @@ "id": "7fa365d3", "metadata": {}, "source": [ - "### 3.7.2 Experiment Definition" + "#### 3.7.2 Experiment Definition" ] }, { @@ -1907,7 +1956,7 @@ "id": "f9e51b3d", "metadata": {}, "source": [ - "### 3.7.3 Create Experiment and Signal Map" + "#### 3.7.3 Create Experiment and Signal Map" ] }, { @@ -1938,7 +1987,7 @@ "id": "9e091bd2", "metadata": {}, "source": [ - "### 3.7.4 Show Pulse Sheet" + "#### 3.7.4 Show Pulse Sheet" ] }, { @@ -1957,7 +2006,7 @@ "id": "2e8bd3b3", "metadata": {}, "source": [ - "### 3.7.5 Plot Simulated Outputs" + "#### 3.7.5 Plot Simulated Outputs" ] }, { @@ -1976,7 +2025,7 @@ "id": "ce23d659", "metadata": {}, "source": [ - "### 3.7.6 Run, Save, and Plot Results" + "#### 3.7.6 Run, Save, and Plot Results" ] }, { diff --git a/examples/03_superconducting_qubits/01_single_qubit_tuneup_uhfqa_hdawg.ipynb b/examples/03_superconducting_qubits/01_single_qubit_tuneup_uhfqa_hdawg.ipynb index de33f9b..57437fc 100644 --- a/examples/03_superconducting_qubits/01_single_qubit_tuneup_uhfqa_hdawg.ipynb +++ b/examples/03_superconducting_qubits/01_single_qubit_tuneup_uhfqa_hdawg.ipynb @@ -23,19 +23,8 @@ "id": "4d4e7d0b-b53a-40e4-831c-236ed9d97c42", "metadata": {}, "source": [ - "# 0. General Imports and Definitions" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "f3ecf2ff-f304-472a-b6d2-a50792a39a0d", - "metadata": { - "jp-MarkdownHeadingCollapsed": true, - "tags": [] - }, - "source": [ - "## 0.1 Python Imports " + "## 0. General Imports and Definitions\n", + "### 0.1 Python Imports" ] }, { @@ -78,19 +67,9 @@ "tags": [] }, "source": [ - "# 1. Define the Instrument Setup and Required Experimental Parameters" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "fd09ab91-8aee-4b32-8fb7-30dcc4f18139", - "metadata": { - "jp-MarkdownHeadingCollapsed": true, - "tags": [] - }, - "source": [ - "## 1.1 Setup Descriptor\n", + "## 1. Define the Instrument Setup and Required Experimental Parameters\n", + "\n", + "### 1.1 Setup Descriptor\n", "\n", "The descriptor defines the QCCS setup - it contains instrument identifiers, internal wiring between instruments as well as the definition of the logical signals and their names" ] @@ -147,7 +126,7 @@ "tags": [] }, "source": [ - "## 1.2 Qubit Parameters\n", + "### 1.2 Qubit Parameters\n", "\n", "A python dictionary containing all parameters needed to control and readout the qubits - frequencies, pulse lengths, timings\n", "\n", @@ -206,7 +185,7 @@ "tags": [] }, "source": [ - "## 1.3 Setup Calibration\n", + "### 1.3 Setup Calibration\n", "\n", "Generate a calibration object from the qubit control and readout parameters" ] @@ -315,16 +294,8 @@ "tags": [] }, "source": [ - "# 2. Create Device Setup and Apply Calibration Data, Connect to the Instruments" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "7a113bea-2b36-4f29-9eb7-2b76b061983d", - "metadata": {}, - "source": [ - "## 2.1 The Device Setup\n", + "## 2. Create Device Setup and Apply Calibration Data, Connect to the Instruments\n", + "### 2.1 The Device Setup\n", "\n", "Create the device setup from the descriptor and apply to qubit control and readout calibration to it" ] @@ -373,7 +344,7 @@ "id": "62ae58f8-4016-43e2-8c33-ee88645c7268", "metadata": {}, "source": [ - "## 2.2 Create and Connect to a LabOne Q Session \n", + "### 2.2 Create and Connect to a LabOne Q Session \n", "\n", "Establishes the connection to the instruments and readies them for experiments" ] @@ -401,7 +372,7 @@ "tags": [] }, "source": [ - "# 3. Single Qubit Tuneup - Experimental Sequence\n", + "## 3. Single Qubit Tuneup - Experimental Sequence\n", "\n", "Sequence of experiments for tuneup from scratch of a superconducting qubit in circuit QED architecture " ] @@ -415,7 +386,7 @@ "tags": [] }, "source": [ - "## 3.1 Resonator Spectroscopy\n", + "### 3.1 Resonator Spectroscopy\n", "\n", "Find the resonance frequency of the qubit readout resonator by looking at the transmission or reflection of a probe signal applied through the readout line" ] @@ -429,7 +400,7 @@ "tags": [] }, "source": [ - "### 3.1.1 Additional Experimental Parameters\n", + "#### 3.1.1 Additional Experimental Parameters\n", "\n", "Define the frequency scan and the excitation pulse" ] @@ -478,7 +449,7 @@ "tags": [] }, "source": [ - "### 3.1.2 Experiment Definition\n", + "#### 3.1.2 Experiment Definition\n", "\n", "Define the experimental pulse and readout sequence - here without any explicit qubit reference\n", "\n", @@ -573,7 +544,7 @@ "tags": [] }, "source": [ - "### 3.1.3 Run and Evaluate Experiment for Both Qubits\n", + "#### 3.1.3 Run and Evaluate Experiment for Both Qubits\n", "\n", "Runs the experiment and evaluates the data returned by the measurement" ] @@ -587,7 +558,7 @@ "tags": [] }, "source": [ - "#### - for qubit 0" + "##### - for qubit 0" ] }, { @@ -689,7 +660,7 @@ "tags": [] }, "source": [ - "#### - for qubit 1" + "##### - for qubit 1" ] }, { @@ -791,9 +762,9 @@ "tags": [] }, "source": [ - "## 3.2 Pulsed Qubit Spectroscopy\n", + "### 3.2 Pulsed Qubit Spectroscopy\n", "\n", - "Find the resonance frequency of the qubit bu looking at the change in resonator transmission when sweeping the frequency of a qubit excitation pulse" + "Find the resonance frequency of the qubit by looking at the change in resonator transmission when sweeping the frequency of a qubit excitation pulse" ] }, { @@ -805,7 +776,7 @@ "tags": [] }, "source": [ - "### 3.2.1 Additional Experimental Parameters\n", + "#### 3.2.1 Additional Experimental Parameters\n", "\n", "Define the frequency scan and the pulses used in the experiment" ] @@ -866,7 +837,7 @@ "tags": [] }, "source": [ - "### 3.2.2 Readout Pulse and Signal Acquisition \n", + "#### 3.2.2 Readout Pulse and Signal Acquisition \n", "\n", "Define a reusable code section containing the readout pulse and signal acquisition statements - will be used in all subsequent experiments" ] @@ -920,7 +891,7 @@ "tags": [] }, "source": [ - "### 3.2.3 Change Calibration of Readout and Acquire Line\n", + "#### 3.2.3 Change Calibration of Readout and Acquire Line\n", "\n", "Apply the readout frequency parameter as measured in the resonator spectroscopy experiment" ] @@ -954,7 +925,7 @@ "id": "fefd645d-ceee-41d3-a86c-403d63d5b4f1", "metadata": {}, "source": [ - "### 3.2.4 Experiment Definition\n", + "#### 3.2.4 Experiment Definition\n", "\n", "Define the experimental pulse and readout sequence - here without any explicit qubit reference\n", "\n", @@ -1059,7 +1030,7 @@ "id": "46bf613c-2f03-4a02-8bc0-1201b845468a", "metadata": {}, "source": [ - "### 3.2.5 Run and Evaluate Experiment for Both Qubits\n", + "#### 3.2.5 Run and Evaluate Experiment for Both Qubits\n", "\n", "Runs the experiment and evaluates the data returned by the measurement" ] @@ -1070,7 +1041,7 @@ "id": "adf615a4-6376-4a7d-8d06-80d7366f7122", "metadata": {}, "source": [ - "#### - for qubit 0" + "##### - for qubit 0" ] }, { @@ -1178,7 +1149,7 @@ "id": "d800ef7d-8c30-4e70-b2f8-09716663157f", "metadata": {}, "source": [ - "#### - for qubit 1" + "##### - for qubit 1" ] }, { @@ -1278,7 +1249,7 @@ "tags": [] }, "source": [ - "## 3.3 Amplitude Rabi Experiment\n", + "### 3.3 Amplitude Rabi Experiment\n", "\n", "Sweep the pulse amplitude of a qubit drive pulse to determine the ideal amplitudes for specific qubit rotation angles" ] @@ -1292,7 +1263,7 @@ "tags": [] }, "source": [ - "### 3.3.1 Additional Experimental Parameters\n", + "#### 3.3.1 Additional Experimental Parameters\n", "\n", "Define the amplitude sweep range and qubit excitation pulse" ] @@ -1333,7 +1304,7 @@ "tags": [] }, "source": [ - "### 3.3.2 Change Calibration of Qubit Drive Line\n", + "#### 3.3.2 Change Calibration of Qubit Drive Line\n", "\n", "Apply the qubit resonance frequency, as found in the qubit spectroscopy experiment" ] @@ -1363,7 +1334,7 @@ "tags": [] }, "source": [ - "### 3.3.3 Experiment Definition\n", + "#### 3.3.3 Experiment Definition\n", "\n", "Define the experimental pulse and readout sequence - here without any explicit qubit reference\n", "\n", @@ -1449,7 +1420,7 @@ "tags": [] }, "source": [ - "### 3.3.4 Run Experiments and Evaluate Measurement Results" + "#### 3.3.4 Run Experiments and Evaluate Measurement Results" ] }, { @@ -1461,7 +1432,7 @@ "tags": [] }, "source": [ - "#### - run for qubit 0 " + "##### - run for qubit 0 " ] }, { @@ -1498,7 +1469,7 @@ "tags": [] }, "source": [ - "#### - run for qubit 1" + "##### - run for qubit 1" ] }, { @@ -1521,7 +1492,7 @@ "id": "c39c5016-5f6d-42ea-9fe3-35cc574ddba2", "metadata": {}, "source": [ - "#### - plot and evaluate measurement results" + "##### - plot and evaluate measurement results" ] }, { @@ -1591,7 +1562,7 @@ "tags": [] }, "source": [ - "## 3.4. T1 Experiment\n", + "### 3.4. T1 Experiment\n", "\n", "Sweep the delay between a qubit excitation pulse and the readout to measure the energy releaxation time of the qubit" ] @@ -1605,7 +1576,7 @@ "tags": [] }, "source": [ - "### 3.4.1 Additional Experimental Parameters" + "#### 3.4.1 Additional Experimental Parameters" ] }, { @@ -1642,7 +1613,7 @@ "tags": [] }, "source": [ - "### 3.4.2 Experiment Definition\n", + "#### 3.4.2 Experiment Definition\n", "\n", "Define the experimental pulse and readout sequence - here without any explicit qubit reference\n", "\n", @@ -1711,7 +1682,7 @@ "tags": [] }, "source": [ - "### 3.4.3 Run and Evaluate Experiment for Both Qubits" + "#### 3.4.3 Run and Evaluate Experiment for Both Qubits" ] }, { @@ -1722,7 +1693,7 @@ "tags": [] }, "source": [ - "#### - run for qubit 0" + "##### - run for qubit 0" ] }, { @@ -1732,7 +1703,7 @@ "metadata": {}, "outputs": [], "source": [ - "# set signal map for qubit 0 - no other calibration necesary, taken from DeviceSetup / baseline\n", + "# set signal map for qubit 0 - no other calibration necessary, taken from DeviceSetup / baseline\n", "exp_t1.set_signal_map(q0_map)\n", "\n", "# run the experiment on qubit 0\n", @@ -1758,7 +1729,7 @@ "tags": [] }, "source": [ - "#### - run for qubit 1" + "##### - run for qubit 1" ] }, { @@ -1768,7 +1739,7 @@ "metadata": {}, "outputs": [], "source": [ - "# set signal map for qubit 1 - no other calibration necesary, taken from DeviceSetup / baseline\n", + "# set signal map for qubit 1 - no other calibration necessary, taken from DeviceSetup / baseline\n", "exp_t1.set_signal_map(q1_map)\n", "\n", "# run the experiment on qubit 1\n", @@ -1783,7 +1754,7 @@ "tags": [] }, "source": [ - "#### - plot and evaluate measurement results" + "##### - plot and evaluate measurement results" ] }, { @@ -1851,7 +1822,7 @@ "tags": [] }, "source": [ - "## 3.5 Ramsey Experiment\n", + "### 3.5 Ramsey Experiment\n", "\n", "Sweep the delay between two slightly detuned pi/2 pulses to determine the qubit dephasing time as well as fine calibration its excited state frequency" ] @@ -1862,7 +1833,7 @@ "id": "fcd07f1c-6abd-4c74-882e-83c5483dc02f", "metadata": {}, "source": [ - "### 3.5.1 Additional Experimental Parameters" + "#### 3.5.1 Additional Experimental Parameters" ] }, { @@ -1900,7 +1871,7 @@ "id": "9faf0399-7ffb-454c-bf13-98c2c8f1325a", "metadata": {}, "source": [ - "### 3.5.2 Experiment Definition\n", + "#### 3.5.2 Experiment Definition\n", "\n", "Define the experimental pulse and readout sequence - here without any explicit qubit reference\n", "\n", @@ -1994,7 +1965,7 @@ "id": "8b6502d1-162f-4f33-8bb4-9ab28f511f99", "metadata": {}, "source": [ - "### 3.5.3 Run and Evaluate Experiment for Both Qubits" + "#### 3.5.3 Run and Evaluate Experiment for Both Qubits" ] }, { @@ -2003,7 +1974,7 @@ "id": "735d236f-0481-403a-94a0-caeafdbcb6bc", "metadata": {}, "source": [ - "#### - run for qubit 0" + "##### - run for qubit 0" ] }, { @@ -2038,7 +2009,7 @@ "id": "ae21badd-8e62-4341-a567-ae6d52c2d274", "metadata": {}, "source": [ - "#### - run for qubit 1" + "##### - run for qubit 1" ] }, { @@ -2062,7 +2033,7 @@ "id": "fdf6c2d0-c598-46bd-8dca-75486e3c9df4", "metadata": {}, "source": [ - "#### - plot and evaluate measurements results" + "##### - plot and evaluate measurements results" ] }, { @@ -2148,7 +2119,7 @@ "tags": [] }, "source": [ - "## 3.6 Hahn Echo Experiment\n", + "### 3.6 Hahn Echo Experiment\n", "\n", "Sweep the delay between two pi/2 pulses and a pi pulse inserted at exactly half time between them, in order to determine the qubit dephasing time without noise components that are constant on the sequence time scale - gets rid of 1/f noise contributions" ] @@ -2159,7 +2130,7 @@ "id": "bae89d79-4c55-4d0c-96ad-1812668051c1", "metadata": {}, "source": [ - "### 3.5.1 Additional Experimental Parameters" + "#### 3.6.1 Additional Experimental Parameters" ] }, { @@ -2200,7 +2171,7 @@ "id": "ffbd9243-227a-4683-bcd8-368d50d52856", "metadata": {}, "source": [ - "### 3.5.2 Experiment Definition\n", + "#### 3.6.2 Experiment Definition\n", "\n", "Define the experimental pulse and readout sequence - here without any explicit qubit reference\n", "\n", @@ -2270,7 +2241,7 @@ "id": "30f92285-395b-4ac0-879c-576e25ad62a3", "metadata": {}, "source": [ - "### 3.5.3 Run and Evaluate Experiment for Both Qubits" + "#### 3.6.3 Run and Evaluate Experiment for Both Qubits" ] }, { @@ -2282,7 +2253,7 @@ "tags": [] }, "source": [ - "#### - run for qubit 0" + "##### - run for qubit 0" ] }, { @@ -2319,7 +2290,7 @@ "tags": [] }, "source": [ - "#### - run for qubit 1" + "##### - run for qubit 1" ] }, { @@ -2342,7 +2313,7 @@ "id": "db0cdb65-e17b-445c-a031-468f046d8266", "metadata": {}, "source": [ - "#### - plot and evaluate measurements results" + "##### - plot and evaluate measurements results" ] }, { diff --git a/examples/03_superconducting_qubits/02_two_qubit_experiments_uhfqa_hdawg.ipynb b/examples/03_superconducting_qubits/02_two_qubit_experiments_uhfqa_hdawg.ipynb index e9731e9..bc65bcb 100644 --- a/examples/03_superconducting_qubits/02_two_qubit_experiments_uhfqa_hdawg.ipynb +++ b/examples/03_superconducting_qubits/02_two_qubit_experiments_uhfqa_hdawg.ipynb @@ -17,16 +17,8 @@ "id": "961a420e-7dc7-46fd-aea8-12af1cea8aa2", "metadata": {}, "source": [ - "# 0. General Imports and Definitions" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "2a01d7d5-527b-4324-aa74-95d768f9a2ef", - "metadata": {}, - "source": [ - "## 0.1 Python Imports" + "## 0. General Imports and Definitions\n", + "### 0.1 Python Imports" ] }, { @@ -57,16 +49,8 @@ "id": "c8aa3c8e-12ce-4f86-a5bb-7f76e0c0f5d7", "metadata": {}, "source": [ - "# 1. Define Device Setup and Calibration" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "f44d74bf-d663-4421-b826-bd156e65415c", - "metadata": {}, - "source": [ - "## 1.1 Define a Device Setup\n", + "## 1. Define Device Setup and Calibration\n", + "### 1.1 Define a Device Setup\n", "\n", "Descriptor contains all information on instruments used, internal connections between instruments as well as wiring to the experiment" ] @@ -125,7 +109,7 @@ "id": "81eae8d4-aaac-486e-ae41-0c0bc01c706e", "metadata": {}, "source": [ - "## 1.2 Define Calibration Settings\n", + "### 1.2 Define Calibration Settings\n", "\n", "Modify the calibration on the device setup with known parameters for qubit control and readout - qubit control and readout frequencies, mixer calibration corrections" ] @@ -258,7 +242,7 @@ "id": "ce42775c-9f55-422d-8430-595a620cba87", "metadata": {}, "source": [ - "## 1.3 Create Device Setup and Apply Calibration Settings" + "### 1.3 Create Device Setup and Apply Calibration Settings" ] }, { @@ -306,7 +290,7 @@ "id": "38438dd2-6905-4f99-a556-bb27363c3a1f", "metadata": {}, "source": [ - "# 2. Simultaneous Two Qubit Amplitude Rabi Experiment\n", + "## 2. Simultaneous Two Qubit Amplitude Rabi Experiment\n", "\n", "Sweep the pulse amplitude of a qubit drive pulse to determine the ideal amplitudes for specific qubit rotation angles - done simultaneously on two qubits\n", "\n", @@ -319,7 +303,7 @@ "id": "d068797e-1673-4a5b-93c2-c450e8c061ab", "metadata": {}, "source": [ - "## 2.1 Define the Experiment" + "### 2.1 Define the Experiment" ] }, { @@ -505,7 +489,7 @@ "id": "7e485382-ccd1-4c32-8253-1f5e9e2ad127", "metadata": {}, "source": [ - "## 2.2 Run the Experiment and Plot the Measurement Results and Pulse Sequence" + "### 2.2 Run the Experiment and Plot the Measurement Results and Pulse Sequence" ] }, { @@ -584,7 +568,7 @@ "tags": [] }, "source": [ - "# 3. Simultaneous Two Qubit Ramsey Experiment\n", + "## 3. Simultaneous Two Qubit Ramsey Experiment\n", "\n", "Sweep the delay between two slightly detuned pi/2 pulses to determine the qubit dephasing time as well as for fine calibration its excited state frequency - for two qubits in parallel. Again, we need to scale the amplitude of the readout pulse, to avoid clipping of the signal when the readout pulses for both pulses are added." ] @@ -595,7 +579,7 @@ "id": "ac0d5da7-7416-4f32-b3e8-0114efd383f7", "metadata": {}, "source": [ - "## 3.1 Define the Experiment" + "### 3.1 Define the Experiment" ] }, { @@ -766,7 +750,7 @@ "id": "cd97d37a-c3d9-480a-94ed-568bc237bd64", "metadata": {}, "source": [ - "## 5.2 Run the Experiment and Plot the Measurement Results and Pulse Sequence" + "### 3.2 Run the Experiment and Plot the Measurement Results and Pulse Sequence" ] }, { diff --git a/examples/03_superconducting_qubits/03_qubit_tuneup_shfqc_ext_dc_source.ipynb b/examples/03_superconducting_qubits/03_qubit_tuneup_shfqc_ext_dc_source.ipynb index 68c581a..5948188 100644 --- a/examples/03_superconducting_qubits/03_qubit_tuneup_shfqc_ext_dc_source.ipynb +++ b/examples/03_superconducting_qubits/03_qubit_tuneup_shfqc_ext_dc_source.ipynb @@ -23,7 +23,7 @@ "id": "4d4e7d0b-b53a-40e4-831c-236ed9d97c42", "metadata": {}, "source": [ - "# 0. General Imports and Definitions" + "## 0. General Imports and Definitions" ] }, { @@ -34,7 +34,7 @@ "tags": [] }, "source": [ - "## 0.1 Python Imports " + "### 0.1 Python Imports " ] }, { @@ -68,7 +68,7 @@ "tags": [] }, "source": [ - "# 1. Define the Instrument Setup and Required Experimental Parameters" + "## 1. Define the Instrument Setup and Required Experimental Parameters" ] }, { @@ -77,7 +77,7 @@ "id": "2a4a933f", "metadata": {}, "source": [ - "## 1.1 Create device setup\n", + "### 1.1 Create device setup\n", "\n", "Create the device setup from the descriptor, and apply some convenient mapping to instruments and logical signals." ] @@ -151,7 +151,7 @@ "tags": [] }, "source": [ - "## 1.2 Qubit Parameters\n", + "### 1.2 Qubit Parameters\n", "\n", "A python dictionary containing all parameters needed to control and readout the qubits - frequencies, pulse lengths, timings\n", "\n", @@ -170,7 +170,7 @@ }, "outputs": [], "source": [ - "# a function to define a collection of signle qubit control and readout parameters as a python dictionary\n", + "# a function to define a collection of single qubit control and readout parameters as a python dictionary\n", "def single_qubit_parameters():\n", " return {\n", " \"freq\": 100e6, # qubit 0 drive frequency in [Hz] - relative to local oscillator for qubit drive upconversion\n", @@ -214,7 +214,7 @@ "tags": [] }, "source": [ - "## 1.3 Setup Calibration\n", + "### 1.3 Setup Calibration\n", "\n", "Generate a calibration object from the qubit control and readout parameters" ] @@ -306,7 +306,7 @@ "id": "7b2ff63f", "metadata": {}, "source": [ - "## 1.4 Define user functions" + "### 1.4 Define user functions" ] }, { @@ -340,7 +340,7 @@ "tags": [] }, "source": [ - "# 2. Apply Calibration Data, Connect to the Instruments" + "## 2. Apply Calibration Data, Connect to the Instruments" ] }, { @@ -349,7 +349,7 @@ "id": "7a113bea-2b36-4f29-9eb7-2b76b061983d", "metadata": {}, "source": [ - "## 2.1 Set Calibration\n", + "### 2.1 Set Calibration\n", "\n", "Create qubit control and readout calibration, and apply it to the device setup." ] @@ -373,7 +373,7 @@ "id": "62ae58f8-4016-43e2-8c33-ee88645c7268", "metadata": {}, "source": [ - "## 2.2 Create and Connect to a QCCS Session \n", + "### 2.2 Create and Connect to a QCCS Session \n", "\n", "Establishes a session and connection to the instruments.\n", "\n", @@ -407,7 +407,7 @@ "tags": [] }, "source": [ - "# 3. Experimental Sequences\n", + "## 3. Experimental Sequences\n", "\n", "Sequence of experiments for tune-up of a superconducting qubit with DC bias in circuit QED architecture " ] @@ -420,7 +420,7 @@ "tags": [] }, "source": [ - "## 3.1 Resonator Spectroscopy: CW with DC Bias\n", + "### 3.1 Resonator Spectroscopy: CW with DC Bias\n", "\n", "Find the resonance frequency of the qubit readout resonator by looking at the transmission or reflection of a probe signal applied through the readout line" ] @@ -433,7 +433,7 @@ "tags": [] }, "source": [ - "### 3.1.1 Additional Experimental Parameters\n", + "#### 3.1.1 Additional Experimental Parameters\n", "\n", "Define the frequency scan and the excitation pulse" ] @@ -481,7 +481,7 @@ "tags": [] }, "source": [ - "### 3.1.2 Experiment Definition\n", + "#### 3.1.2 Experiment Definition\n", "\n", "Define the experimental pulse and readout sequence - here without any explicit qubit reference\n", "\n", @@ -581,7 +581,7 @@ "tags": [] }, "source": [ - "### 3.1.3 Run and Evaluate Experiment\n", + "#### 3.1.3 Run and Evaluate Experiment\n", "Runs the experiment and evaluates the data returned by the measurement" ] }, @@ -635,7 +635,7 @@ "id": "d9a8dc1f", "metadata": {}, "source": [ - "#### Update Calibration" + "#### 3.1.4 Update Calibration" ] }, { @@ -667,7 +667,7 @@ "id": "253bbdd5", "metadata": {}, "source": [ - "## 3.2 Resonator Spectroscopy: Pulsed with DC Bias" + "### 3.2 Resonator Spectroscopy: Pulsed with DC Bias" ] }, { @@ -790,7 +790,7 @@ "compiled_spec_pulsed = session.compile(exp_spec_pulsed)\n", "\n", "Path(\"Pulse_Sheets\").mkdir(parents=True, exist_ok=True)\n", - "# generate a pulse sheet to inspect experiment befor runtime\n", + "# generate a pulse sheet to inspect experiment before runtime\n", "show_pulse_sheet(\"Pulse_Sheets/Pulsed_Spectroscopy\", compiled_spec_pulsed)" ] }, @@ -827,7 +827,7 @@ "id": "49394dee", "metadata": {}, "source": [ - "### 3.2.1 Update calibration\n", + "#### 3.2.1 Update calibration\n", "Extract the resonance frequency and update the calibration" ] }, @@ -854,7 +854,7 @@ "tags": [] }, "source": [ - "## 3.3 Pulsed Qubit Spectroscopy with DC Bias\n", + "### 3.3 Pulsed Qubit Spectroscopy with DC Bias\n", "\n", "Find the resonance frequency of the qubit by looking at the change in resonator transmission when sweeping the frequency of a qubit excitation pulse vs DC bias" ] @@ -868,7 +868,7 @@ "tags": [] }, "source": [ - "### 3.3.1 Additional Experimental Parameters\n", + "#### 3.3.1 Additional Experimental Parameters\n", "\n", "Define the frequency scan and the pulses used in the experiment" ] @@ -921,7 +921,7 @@ "id": "fefd645d-ceee-41d3-a86c-403d63d5b4f1", "metadata": {}, "source": [ - "### 3.3.2 Experiment Definition\n", + "#### 3.3.2 Experiment Definition\n", "\n", "The frequency sweep of the drive line can now be done in real time (was: near time in older software releases)" ] @@ -1026,7 +1026,7 @@ "id": "46bf613c-2f03-4a02-8bc0-1201b845468a", "metadata": {}, "source": [ - "### 3.3.3 Run and Evaluate Experiment for Both Qubits\n", + "#### 3.3.3 Run and Evaluate Experiment for Both Qubits\n", "\n", "Runs the experiment and evaluates the data returned by the measurement" ] @@ -1068,7 +1068,7 @@ "compiled_qspec = session.compile(exp_qspec)\n", "\n", "Path(\"Pulse_Sheets\").mkdir(parents=True, exist_ok=True)\n", - "# generate a pulse sheet to inspect experiment befor runtime\n", + "# generate a pulse sheet to inspect experiment before runtime\n", "show_pulse_sheet(\"Pulse_Sheets/Qubit_Spectroscopy\", compiled_qspec)\n", "\n", "plot_simulation(compiled_qspec, 0, 100e-6)" diff --git a/examples/03_superconducting_qubits/04_parallel_qubit_tuneup_shfqc_hdawg_pqsc.ipynb b/examples/03_superconducting_qubits/04_parallel_qubit_tuneup_shfqc_hdawg_pqsc.ipynb index a6f017c..bf810b8 100644 --- a/examples/03_superconducting_qubits/04_parallel_qubit_tuneup_shfqc_hdawg_pqsc.ipynb +++ b/examples/03_superconducting_qubits/04_parallel_qubit_tuneup_shfqc_hdawg_pqsc.ipynb @@ -23,18 +23,8 @@ "id": "4d4e7d0b-b53a-40e4-831c-236ed9d97c42", "metadata": {}, "source": [ - "# 0. General Imports and Definitions" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "f3ecf2ff-f304-472a-b6d2-a50792a39a0d", - "metadata": { - "tags": [] - }, - "source": [ - "## 0.1 Python Imports " + "## 0. General Imports and Definitions\n", + "### 0.1 Python Imports " ] }, { @@ -97,7 +87,7 @@ "tags": [] }, "source": [ - "# 1. Define the Instrument Setup - auto-define depending on number of qubits used" + "## 1. Define the Instrument Setup - auto-define depending on number of qubits used" ] }, { @@ -147,16 +137,8 @@ "tags": [] }, "source": [ - "# 2. Create Qubits, Apply Calibration Data, Connect to the Instruments" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "9815c045", - "metadata": {}, - "source": [ - "## 2.1 Create Qubits, use them to generate and set calibration to the device setup" + "## 2. Create Qubits, Apply Calibration Data, Connect to the Instruments\n", + "### 2.1 Create Qubits, use them to generate and set calibration to the device setup" ] }, { @@ -217,7 +199,7 @@ "id": "62ae58f8-4016-43e2-8c33-ee88645c7268", "metadata": {}, "source": [ - "## 2.2 Create and Connect to a LabOne Q Session \n", + "### 2.2 Create and Connect to a LabOne Q Session \n", "\n", "Establishes the connection to the instruments and readies them for experiments" ] @@ -245,7 +227,7 @@ "tags": [] }, "source": [ - "# 3. Qubit Tuneup - Experimental Sequence\n", + "## 3. Qubit Tuneup - Experimental Sequence\n", "\n", "Sequence of experiments for tuneup from scratch of a superconducting qubit in circuit QED architecture " ] @@ -258,7 +240,7 @@ "tags": [] }, "source": [ - "## 3.1 Resonator Spectroscopy: CW in parallel over full range of 0.5 - 8.5 GHz\n", + "### 3.1 Resonator Spectroscopy: CW in parallel over full range of 0.5 - 8.5 GHz\n", "\n", "Find the resonance frequency of the qubit readout resonator by looking at the transmission or reflection of a probe signal applied through the readout line" ] @@ -271,7 +253,7 @@ "tags": [] }, "source": [ - "### 3.1.1 Experiment Definition\n", + "#### 3.1.1 Experiment Definition\n", "\n", "Define the experimental pulse and readout sequence - here without any explicit qubit reference\n", "\n", @@ -375,7 +357,7 @@ "tags": [] }, "source": [ - "### 3.1.2 Run and Evaluate Experiment\n", + "#### 3.1.2 Run and Evaluate Experiment\n", "Runs the experiment and evaluates the data returned by the measurement" ] }, @@ -452,7 +434,7 @@ "id": "d9a8dc1f", "metadata": {}, "source": [ - "### 3.1.3 Update Calibration and save to database " + "#### 3.1.3 Update Calibration and save to database " ] }, { @@ -485,7 +467,7 @@ "tags": [] }, "source": [ - "## 3.2 Pulsed Qubit Spectroscopy: in parallel over 100MHz range for each qubit\n", + "### 3.2 Pulsed Qubit Spectroscopy: in parallel over 100MHz range for each qubit\n", "\n", "Find the resonance frequency of the qubit by looking at the change in resonator transmission when sweeping the frequency of a qubit excitation pulse" ] @@ -496,7 +478,7 @@ "id": "fefd645d-ceee-41d3-a86c-403d63d5b4f1", "metadata": {}, "source": [ - "### 3.2.1 Experiment Definition\n", + "#### 3.2.1 Experiment Definition\n", "\n", "The frequency sweep of the drive line can now be done in real time (was: near time in older software releases)" ] @@ -631,7 +613,7 @@ "id": "46bf613c-2f03-4a02-8bc0-1201b845468a", "metadata": {}, "source": [ - "### 3.2.2 Run and Evaluate Experiment for all Qubits in parallel\n", + "#### 3.2.2 Run and Evaluate Experiment for all Qubits in parallel\n", "\n", "Runs the experiment and evaluates the data returned by the measurement" ] @@ -700,7 +682,7 @@ "id": "a432798b", "metadata": {}, "source": [ - "### 3.2.3 Update Calibration and save to database " + "#### 3.2.3 Update Calibration and save to database " ] }, { @@ -733,7 +715,7 @@ "tags": [] }, "source": [ - "## 3.3 Amplitude Rabi Experiment - in parallel\n", + "### 3.3 Amplitude Rabi Experiment - in parallel\n", "\n", "Sweep the pulse amplitude of a qubit drive pulse to determine the ideal amplitudes for specific qubit rotation angles" ] @@ -746,7 +728,7 @@ "tags": [] }, "source": [ - "### 3.3.1 Experiment Definition\n" + "#### 3.3.1 Experiment Definition\n" ] }, { @@ -846,7 +828,7 @@ "tags": [] }, "source": [ - "### 3.3.2 Execute experiment and analyze results" + "#### 3.3.2 Execute experiment and analyze results" ] }, { @@ -924,7 +906,7 @@ "id": "ba9b96a6", "metadata": {}, "source": [ - "### 3.3.3 Update Calibration and save to database " + "#### 3.3.3 Update Calibration and save to database " ] }, { @@ -954,7 +936,7 @@ "id": "71848b75", "metadata": {}, "source": [ - "## 3.4 Ramsey Experiment - in parallel\n", + "### 3.4 Ramsey Experiment - in parallel\n", "The Ramsey experiment is different from the experiments above as the length of the drive section changes. Using a right-aligned sweep section and the automatic repetition time makes sure that the experiment is run as efficiently as possible on the Zurich Instruments hardware." ] }, @@ -964,7 +946,7 @@ "id": "7fa365d3", "metadata": {}, "source": [ - "### 3.4.1 Experiment Definition" + "#### 3.4.1 Experiment Definition" ] }, { @@ -1061,7 +1043,7 @@ "id": "45f3ecb7", "metadata": {}, "source": [ - "### 3.4.2 Execute experiment" + "#### 3.4.2 Execute experiment" ] }, { @@ -1144,7 +1126,7 @@ "id": "3d0ef303", "metadata": {}, "source": [ - "### 3.4.3 Update Qubit parameters and save to database " + "#### 3.4.3 Update Qubit parameters and save to database " ] }, { @@ -1174,7 +1156,7 @@ "id": "fbcaa9fc", "metadata": {}, "source": [ - "## 3.5 T1 Experiment - in parallel\n" + "### 3.5 T1 Experiment - in parallel\n" ] }, { @@ -1183,7 +1165,7 @@ "id": "23c1610f", "metadata": {}, "source": [ - "### 3.5.1 Experiment Definition" + "#### 3.5.1 Experiment Definition" ] }, { @@ -1280,7 +1262,7 @@ "id": "59992477", "metadata": {}, "source": [ - "### 3.5.2 Execute experiment" + "#### 3.5.2 Execute experiment" ] }, { @@ -1358,7 +1340,7 @@ "id": "688da0dc", "metadata": {}, "source": [ - "### 3.5.3 Update Qubit parameters and save to database " + "#### 3.5.3 Update Qubit parameters and save to database " ] }, { diff --git a/examples/04_spin_qubits/00_user_function_sweeps.ipynb b/examples/04_spin_qubits/00_user_function_sweeps.ipynb index 147d2a4..63973ff 100644 --- a/examples/04_spin_qubits/00_user_function_sweeps.ipynb +++ b/examples/04_spin_qubits/00_user_function_sweeps.ipynb @@ -23,7 +23,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# General Imports" + "## 0. General Imports" ] }, { @@ -53,7 +53,7 @@ "id": "e66d0291", "metadata": {}, "source": [ - "# Device Setup" + "## 1. Device Setup" ] }, { @@ -61,7 +61,7 @@ "id": "a36a3823", "metadata": {}, "source": [ - "## Create device setup" + "### 1.1 Create device setup" ] }, { @@ -92,7 +92,7 @@ "id": "476a49d2", "metadata": {}, "source": [ - "# MFLI example" + "## 2. MFLI example" ] }, { @@ -101,7 +101,7 @@ "id": "0cb835ce", "metadata": {}, "source": [ - "## Connect to session" + "### 2.1 Connect to session" ] }, { @@ -121,7 +121,7 @@ "id": "b2ed6e8f", "metadata": {}, "source": [ - "## Experiment definition" + "### 2.2 Experiment definition" ] }, { @@ -160,7 +160,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Configure MFLI and DAQ module" + "### 2.3 Configure MFLI and DAQ module" ] }, { @@ -234,7 +234,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Define user functions for arming MFLI and reading results" + "### 2.4 Define user functions for arming MFLI and reading results" ] }, { @@ -290,7 +290,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Register user functions" + "### 2.5 Register user functions" ] }, { @@ -309,7 +309,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Run experiment" + "### 2.6 Run experiment" ] }, { @@ -325,7 +325,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Plot results" + "## 3. Plot results" ] }, { diff --git a/examples/04_spin_qubits/01_QCoDeS_sweeps.ipynb b/examples/04_spin_qubits/01_QCoDeS_sweeps.ipynb index 04016cf..030a91b 100644 --- a/examples/04_spin_qubits/01_QCoDeS_sweeps.ipynb +++ b/examples/04_spin_qubits/01_QCoDeS_sweeps.ipynb @@ -6,7 +6,7 @@ "id": "46cfbe23", "metadata": {}, "source": [ - "# Sweeping paramters with QCoDeS in LabOne Q" + "# Sweeping parameters with QCoDeS in LabOne Q" ] }, { @@ -15,7 +15,7 @@ "id": "4a5e08b0", "metadata": {}, "source": [ - "This notebook shows you how to perform a very general 2D sweep. Here, the two sweep axes are set through a [QCoDeS](https://qcodes.github.io/Qcodes/) parameter, mimicing arbitrary instruments that can be controlled with a QCoDeS driver." + "This notebook shows you how to perform a very general 2D sweep. Here, the two sweep axes are set through a [QCoDeS](https://qcodes.github.io/Qcodes/) parameter, mimicking arbitrary instruments that can be controlled with a QCoDeS driver." ] }, { @@ -23,7 +23,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# General Imports" + "## 0. General Imports" ] }, { @@ -57,7 +57,7 @@ "outputs": [], "source": [ "import qcodes as qc\n", - "from qcodes.tests.instrument_mocks import DummyInstrument\n" + "from qcodes.tests.instrument_mocks import DummyInstrument" ] }, { @@ -69,7 +69,7 @@ "source": [ "# generate dummy instruments\n", "my_magnet = DummyInstrument(name=\"magnet\", gates=[\"Bx\", \"By\", \"Bz\"])\n", - "my_LO = DummyInstrument(name=\"RF_source\", gates=[\"P\", \"f\"])\n" + "my_LO = DummyInstrument(name=\"RF_source\", gates=[\"P\", \"f\"])" ] }, { @@ -77,7 +77,7 @@ "id": "e66d0291", "metadata": {}, "source": [ - "# Device Setup" + "## 1. Device Setup" ] }, { @@ -85,7 +85,7 @@ "id": "a36a3823", "metadata": {}, "source": [ - "## Create device setup" + "### 1.1 Create device setup" ] }, { @@ -107,34 +107,36 @@ " server_host=\"your_ip_address\",\n", " server_port=8004,\n", " setup_name=\"MySetup\",\n", - ")\n" + ")" ] }, { + "attachments": {}, "cell_type": "markdown", + "id": "476a49d2", "metadata": {}, "source": [ - "## Connect session" + "## 2. MFLI example" ] }, { - "cell_type": "code", - "execution_count": null, + "cell_type": "markdown", + "id": "ef196447", "metadata": {}, - "outputs": [], "source": [ - "# create and connect to session\n", - "session = Session(device_setup=device_setup)\n", - "session.connect(do_emulation=True)\n" + "### 2.1 Connect session" ] }, { - "attachments": {}, - "cell_type": "markdown", - "id": "476a49d2", + "cell_type": "code", + "execution_count": null, + "id": "70913a8b", "metadata": {}, + "outputs": [], "source": [ - "# MFLI example" + "# create and connect to session\n", + "session = Session(device_setup=device_setup)\n", + "session.connect(do_emulation=True)" ] }, { @@ -143,7 +145,7 @@ "id": "0cb835ce", "metadata": {}, "source": [ - "## Connect to instrument in session" + "Connect to the instrument in the session" ] }, { @@ -153,7 +155,7 @@ "metadata": {}, "outputs": [], "source": [ - "mfli = session.devices[\"device_mfli\"]\n" + "mfli = session.devices[\"device_mfli\"]" ] }, { @@ -162,7 +164,7 @@ "id": "b2ed6e8f", "metadata": {}, "source": [ - "## Experiment" + "### 2.2 Experiment Definition" ] }, { @@ -194,7 +196,7 @@ " with exp.sweep(uid=\"inner_sweep\", parameter=frequency_sweep):\n", " # use user function\n", " exp.call(\"set_frequency\", value=frequency_sweep)\n", - " exp.call(\"readMFLI\", settling_time=0.1)\n" + " exp.call(\"readMFLI\", settling_time=0.1)" ] }, { @@ -202,7 +204,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Configure MFLI and DAQ module" + "### 2.3 Configure MFLI and DAQ module" ] }, { @@ -264,7 +266,7 @@ "# print(f\"Columns: {daq_module.grid.cols()}\")\n", "# print(f\"Rows: {daq_module.grid.rows()}\")\n", "# print(f\"Repetitions: {daq_module.grid.repetitions()}\")\n", - "# print(f\"Holdoff: {daq_module.holdoff.time()}\")\n" + "# print(f\"Holdoff: {daq_module.holdoff.time()}\")" ] }, { @@ -272,7 +274,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Define user functions for arming MFLI and reading results" + "### 2.4 Define user functions for arming MFLI and reading results" ] }, { @@ -312,7 +314,7 @@ " # Get data\n", " daq_data = daq_module.read(raw=False, clk_rate=clockbase)\n", "\n", - " return daq_data\n" + " return daq_data" ] }, { @@ -333,7 +335,7 @@ " my_LO.f.set(value) # set new value in MHz\n", " print(f\"Set new frequency:{value}\")\n", " time.sleep(0.1) # settling time\n", - " return my_LO.f.get() # return new value\n" + " return my_LO.f.get() # return new value" ] }, { @@ -346,14 +348,14 @@ "# register user functions\n", "session.register_user_function(set_magnet, \"set_magnet\")\n", "session.register_user_function(set_frequency, \"set_frequency\")\n", - "session.register_user_function(readMFLI, \"readMFLI\")\n" + "session.register_user_function(readMFLI, \"readMFLI\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Run experiment" + "### 2.5 Run experiment" ] }, { @@ -362,14 +364,14 @@ "metadata": {}, "outputs": [], "source": [ - "my_results = session.run(exp)\n" + "my_results = session.run(exp)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "# Plot results" + "## 3. Plot results" ] }, { @@ -420,7 +422,7 @@ " ax.set_xlabel(sweep_axes[1].axis_name)\n", " ax.set_ylabel(sweep_axes[0].axis_name)\n", "else:\n", - " print(\"Emulation - nothing to plot\")\n" + " print(\"Emulation - nothing to plot\")" ] }, { @@ -428,7 +430,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Plot individual time traces" + "### 3.1 Plot individual time traces" ] }, { @@ -451,7 +453,7 @@ " # plt.legend(loc='best', fontsize=8)\n", " plt.title(\"MFLI time traces of demodulated data\")\n", "else:\n", - " print(\"Emulation - nothing to plot\")\n" + " print(\"Emulation - nothing to plot\")" ] } ], diff --git a/examples/04_spin_qubits/02_MFLI_cw_acquisition.ipynb b/examples/04_spin_qubits/02_MFLI_cw_acquisition.ipynb index 05dc88a..f6351aa 100644 --- a/examples/04_spin_qubits/02_MFLI_cw_acquisition.ipynb +++ b/examples/04_spin_qubits/02_MFLI_cw_acquisition.ipynb @@ -15,7 +15,7 @@ "id": "4a5e08b0", "metadata": {}, "source": [ - "This notebook shows you how to perform a CW experiment using a HDAWG and an MFLI. On the HDAWG, a Ramsey sequence is played in sequential mode. Each iteration of the sweep is repeated for a certain time (the integration time). A trigger from the HDAWG is sent to the MFLI DAQ module, to trigger the data acquisition." + "In this notebook, you'll perform a CW experiment using a HDAWG and an MFLI. On the HDAWG, you'll play a Ramsey sequence in sequential mode, where each iteration of the sweep will play for a certain time (the integration time). You'll send the trigger from the HDAWG to the MFLI DAQ module to trigger the data acquisition." ] }, { @@ -23,7 +23,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# General Imports" + "## 0. General Imports" ] }, { @@ -52,7 +52,7 @@ "id": "e66d0291", "metadata": {}, "source": [ - "# Device Setup" + "## 1. Device Setup" ] }, { @@ -60,7 +60,7 @@ "id": "332a893d", "metadata": {}, "source": [ - "## Calibration" + "### 1.1 Calibration" ] }, { @@ -76,14 +76,13 @@ "outputs": [], "source": [ "def calibrate_devices(device_setup):\n", - "\n", " device_setup.logical_signal_groups[\"q0\"].logical_signals[\n", " \"drive_line\"\n", " ].calibration = SignalCalibration(\n", " oscillator=Oscillator(\n", " uid=\"drive_q0_osc\", frequency=1e6, modulation_type=ModulationType.HARDWARE\n", " )\n", - " )\n" + " )" ] }, { @@ -91,7 +90,7 @@ "id": "a36a3823", "metadata": {}, "source": [ - "## Create device setup" + "### 1.2 Create device setup" ] }, { @@ -127,14 +126,14 @@ " server_port=8004,\n", " setup_name=\"MySetup\",\n", ")\n", - "calibrate_devices(device_setup)\n" + "calibrate_devices(device_setup)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Connect session" + "## 2. Connect session" ] }, { @@ -145,7 +144,7 @@ "source": [ "# create and connect to session\n", "session = Session(device_setup=device_setup)\n", - "session.connect(do_emulation=True)\n" + "session.connect(do_emulation=True)" ] }, { @@ -154,7 +153,7 @@ "id": "476a49d2", "metadata": {}, "source": [ - "# MFLI example" + "## 3. MFLI example" ] }, { @@ -163,7 +162,7 @@ "id": "0cb835ce", "metadata": {}, "source": [ - "## Connect to instrument in session" + "### 3.1 Connect to instrument in session" ] }, { @@ -173,7 +172,7 @@ "metadata": {}, "outputs": [], "source": [ - "mfli = session.devices[\"device_mfli\"]\n" + "mfli = session.devices[\"device_mfli\"]" ] }, { @@ -182,7 +181,7 @@ "id": "b2ed6e8f", "metadata": {}, "source": [ - "## Experiment (Ramsey with marker sent to MFLI)" + "### 3.2 Experiment (Ramsey with marker sent to MFLI)" ] }, { @@ -260,7 +259,7 @@ " exp.play(signal=\"drive\", pulse=drive_pulse)\n", "\n", " with exp.section(uid=\"relax\", play_after=\"triggersection\"):\n", - " exp.delay(signal=\"drive\", time=100e-6)\n" + " exp.delay(signal=\"drive\", time=100e-6)" ] }, { @@ -268,7 +267,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Configure MFLI and DAQ module" + "### 3.3 Configure MFLI and DAQ module" ] }, { @@ -334,7 +333,7 @@ "# print(f\"Rows: {daq_module.grid.rows()}\")\n", "# print(f\"Repetitions: {daq_module.grid.repetitions()}\")\n", "# print(f\"Holdoff: {daq_module.holdoff.time()}\")\n", - "# print(f\"Delay: {daq_module.delay()}\")\n" + "# print(f\"Delay: {daq_module.delay()}\")" ] }, { @@ -342,7 +341,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Define user functions for arming MFLI and reading results" + "### 3.4 Define user functions for arming MFLI and reading results" ] }, { @@ -389,14 +388,14 @@ "\n", "def clearDAQmodule():\n", " for node in sample_nodes:\n", - " daq_module.subscribe(node)\n" + " daq_module.subscribe(node)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Signal mapping" + "### 3.5 Signal mapping" ] }, { @@ -420,7 +419,7 @@ " frequency=111e6,\n", " modulation_type=ModulationType.SOFTWARE,\n", " )\n", - ")\n" + ")" ] }, { @@ -428,7 +427,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Set calibration and signal map" + "### 3.6 Set calibration and signal map" ] }, { @@ -440,14 +439,14 @@ "source": [ "# set experiment calibration and signal map\n", "exp.set_calibration(calib_q0)\n", - "exp.set_signal_map(map_q0)\n" + "exp.set_signal_map(map_q0)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Run experiment" + "### 3.7 Run experiment" ] }, { @@ -458,7 +457,7 @@ "source": [ "exp_compiled = session.compile(exp)\n", "# print(exp_compiled.src[1]['text'])\n", - "# exp_compiled.src[1]['text'] = session.compiled_experiment.src[1]['text'].replace('while(repeat_count_shots);\\n', 'while(repeat_count_shots);\\nsetTrigger(0b0);\\n')\n" + "# exp_compiled.src[1]['text'] = session.compiled_experiment.src[1]['text'].replace('while(repeat_count_shots);\\n', 'while(repeat_count_shots);\\nsetTrigger(0b0);\\n')" ] }, { @@ -469,7 +468,7 @@ "source": [ "armMFLI()\n", "time.sleep(0.1)\n", - "session.run(experiment=exp_compiled)\n" + "session.run(experiment=exp_compiled)" ] }, { @@ -479,14 +478,14 @@ "outputs": [], "source": [ "data = readMFLI(session)\n", - "clearDAQmodule()\n" + "clearDAQmodule()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "# Plot results" + "## 4. Plot results" ] }, { @@ -519,7 +518,15 @@ " # plt.legend(loc='upper right', fontsize=8)\n", " plt.title(\"Time traces MFLI\")\n", "else:\n", - " print(\"Emulation - nothing to plot\")\n" + " print(\"Emulation - nothing to plot\")" + ] + }, + { + "cell_type": "markdown", + "id": "43fc8a36", + "metadata": {}, + "source": [ + "## 5. Pulse sheet" ] }, { @@ -529,7 +536,7 @@ "metadata": {}, "outputs": [], "source": [ - "show_pulse_sheet(\"MFLI integration\", session.compiled_experiment)\n" + "show_pulse_sheet(\"MFLI integration\", session.compiled_experiment)" ] } ], diff --git a/examples/04_spin_qubits/03_UHFLI_pulsed_acquisition.ipynb b/examples/04_spin_qubits/03_UHFLI_pulsed_acquisition.ipynb index 40de297..b2fbdfb 100644 --- a/examples/04_spin_qubits/03_UHFLI_pulsed_acquisition.ipynb +++ b/examples/04_spin_qubits/03_UHFLI_pulsed_acquisition.ipynb @@ -15,7 +15,7 @@ "id": "4a5e08b0", "metadata": {}, "source": [ - "This notebook shows you how to perform a pulsed aquisition experiment using a HDAWG and an UHFLI. On the HDAWG, a Ramsey sequence is played. In each iteration a trigger from the HDAWG is sent to the UHFLI DAQ module, to trigger the data acquisition. A second trigger is sent to the UHFLI demodulator, to gate the data transfer and enable fast measurements. In each iteration (pulse/shot) of the experiment, a time trace is acquired with the UHFLI. To imitate the readout signal, a Gaussian pulse is played and acquired with the UHFLI. You might use this notebook if you are interested in acquiring short time traces with the UHFLI, e.g. for RF reflectometry.\n", + "This notebook shows you how to perform a pulsed acquisition experiment using a HDAWG and an UHFLI. On the HDAWG, a Ramsey sequence is played. In each iteration a trigger from the HDAWG is sent to the UHFLI DAQ module, to trigger the data acquisition. A second trigger is sent to the UHFLI demodulator, to gate the data transfer and enable fast measurements. In each iteration (pulse/shot) of the experiment, a time trace is acquired with the UHFLI. To imitate the readout signal, a Gaussian pulse is played and acquired with the UHFLI. You might use this notebook if you are interested in acquiring short time traces with the UHFLI, e.g. for RF reflectometry.\n", "\n", "Connections:\n", "* HDAWG SigOut 4 to UHFLI Input 1\n", @@ -24,6 +24,14 @@ "* synchronize RefClk of both instruments" ] }, + { + "cell_type": "markdown", + "id": "f762233a", + "metadata": {}, + "source": [ + "## 0. General Imports" + ] + }, { "cell_type": "code", "execution_count": null, @@ -54,7 +62,7 @@ "id": "e66d0291", "metadata": {}, "source": [ - "# Device Setup" + "## 1. Device Setup" ] }, { @@ -62,7 +70,7 @@ "id": "332a893d", "metadata": {}, "source": [ - "## Calibration" + "### 1.1 Calibration" ] }, { @@ -78,7 +86,6 @@ "outputs": [], "source": [ "def calibrate_devices(device_setup):\n", - "\n", " device_setup.logical_signal_groups[\"q0\"].logical_signals[\n", " \"drive_line\"\n", " ].calibration = SignalCalibration(\n", @@ -92,7 +99,7 @@ " [0.0, 1.0],\n", " ],\n", " ),\n", - " )\n" + " )" ] }, { @@ -100,7 +107,7 @@ "id": "a36a3823", "metadata": {}, "source": [ - "## Create device setup" + "### 1.2 Create device setup" ] }, { @@ -136,7 +143,7 @@ " server_port=8004,\n", " setup_name=\"MySetup\",\n", ")\n", - "calibrate_devices(device_setup)\n" + "calibrate_devices(device_setup)" ] }, { @@ -145,7 +152,7 @@ "id": "476a49d2", "metadata": {}, "source": [ - "# UHFLI example" + "## 2. UHFLI example" ] }, { @@ -154,7 +161,7 @@ "id": "0cb835ce", "metadata": {}, "source": [ - "## Connect to instrument in zhinst-toolkit session" + "### 2.1 Connect to instrument in zhinst-toolkit session" ] }, { @@ -165,7 +172,7 @@ "source": [ "# create and connect to session\n", "session = Session(device_setup=device_setup)\n", - "session.connect(do_emulation=True)\n" + "session.connect(do_emulation=True)" ] }, { @@ -176,7 +183,7 @@ "outputs": [], "source": [ "# shortcut for the used MFLI in the setup\n", - "uhfli = session.devices[\"device_uhfli\"]\n" + "uhfli = session.devices[\"device_uhfli\"]" ] }, { @@ -185,7 +192,7 @@ "id": "b2ed6e8f", "metadata": {}, "source": [ - "## Experiment (Ramsey with marker sent to UHFLI)" + "### 2.2 Experiment (Ramsey with marker sent to UHFLI)" ] }, { @@ -287,14 +294,14 @@ " exp.play(signal=\"coulomb_1\", pulse=coulomb_readout, amplitude=0.3)\n", " exp.play(signal=\"coulomb_2\", pulse=coulomb_readout, amplitude=0.3)\n", " # use user function to read out the results from the DAQ module\n", - " exp.call(\"read_UHFLI\")\n" + " exp.call(\"read_UHFLI\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Configure UHFLI and DAQ module" + "### 2.3 Configure UHFLI and DAQ module" ] }, { @@ -362,14 +369,14 @@ "# print(f\"Rows: {daq_module.grid.rows()}\")\n", "# print(f\"Repetitions: {daq_module.grid.repetitions()}\")\n", "# print(f\"Holdoff: {daq_module.holdoff.time()}\")\n", - "# print(f\"Delay: {daq_module.delay()}\")\n" + "# print(f\"Delay: {daq_module.delay()}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Define user functions for arming UHFLI and reading results" + "### 2.4 Define user functions for arming UHFLI and reading results" ] }, { @@ -404,14 +411,14 @@ " # Get data\n", " daq_data = daq_module.read(raw=False, clk_rate=clockbase)\n", "\n", - " return daq_data\n" + " return daq_data" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Signal mapping" + "### 2.5 Signal mapping" ] }, { @@ -435,7 +442,7 @@ " frequency=100e6,\n", " modulation_type=ModulationType.HARDWARE,\n", " )\n", - ")\n" + ")" ] }, { @@ -443,7 +450,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Register user functions" + "### 2.6 Register user functions" ] }, { @@ -459,7 +466,7 @@ "\n", "# register user functions\n", "session.register_user_function(armUHFLI, \"arm_UHFLI\")\n", - "session.register_user_function(readUHFLI, \"read_UHFLI\")\n" + "session.register_user_function(readUHFLI, \"read_UHFLI\")" ] }, { @@ -472,14 +479,14 @@ " instrument_serial = device_setup.instrument_by_uid(\"device_hdawg\").address\n", " device = session.devices[instrument_serial]\n", " device.triggers.out[2].delay(23.9e-9)\n", - " device.triggers.out[3].delay(23.9e-9)\n" + " device.triggers.out[3].delay(23.9e-9)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Run experiment" + "### 2.7 Run experiment" ] }, { @@ -488,14 +495,14 @@ "metadata": {}, "outputs": [], "source": [ - "session.run(exp)\n" + "session.run(exp)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "# Plot results" + "## 3. Plot results" ] }, { @@ -528,7 +535,7 @@ "plt.title(\"CYCLIC averaging\")\n", "# plt.xlim([0,10e-6])\n", "# if SEQUENTIAL: plt.title(\"Sequential averaging\")\n", - "# if not SEQUENTIAL: plt.title(\"Cyclic averaging\")\n" + "# if not SEQUENTIAL: plt.title(\"Cyclic averaging\")" ] }, { @@ -538,7 +545,7 @@ "metadata": {}, "outputs": [], "source": [ - "show_pulse_sheet(\"UHFLI integration\", session.compiled_experiment)\n" + "show_pulse_sheet(\"UHFLI integration\", session.compiled_experiment)" ] } ], diff --git a/examples/04_spin_qubits/04_HDAWG_pulse_sequences.ipynb b/examples/04_spin_qubits/04_HDAWG_pulse_sequences.ipynb index c33741f..4a05440 100644 --- a/examples/04_spin_qubits/04_HDAWG_pulse_sequences.ipynb +++ b/examples/04_spin_qubits/04_HDAWG_pulse_sequences.ipynb @@ -27,7 +27,7 @@ "id": "3647462f", "metadata": {}, "source": [ - "# General Imports" + "## 0. General Imports" ] }, { @@ -57,15 +57,8 @@ "id": "e66d0291", "metadata": {}, "source": [ - "# Device Setup" - ] - }, - { - "cell_type": "markdown", - "id": "332a893d", - "metadata": {}, - "source": [ - "## Calibration" + "## 1. Device Setup\n", + "### 1.1 Calibration" ] }, { @@ -81,7 +74,6 @@ "outputs": [], "source": [ "def calibrate_devices(device_setup):\n", - "\n", " device_setup.logical_signal_groups[\"q0\"].logical_signals[\n", " \"drive_line\"\n", " ].calibration = SignalCalibration(\n", @@ -95,7 +87,7 @@ " [0.0, 1.0],\n", " ],\n", " ),\n", - " )\n" + " )" ] }, { @@ -103,7 +95,7 @@ "id": "a36a3823", "metadata": {}, "source": [ - "## Create device setup" + "### 1.2 Create device setup" ] }, { @@ -134,7 +126,7 @@ " server_port=8004,\n", " setup_name=\"MySetup\",\n", ")\n", - "calibrate_devices(device_setup)\n" + "calibrate_devices(device_setup)" ] }, { @@ -142,7 +134,7 @@ "id": "04cd28fe", "metadata": {}, "source": [ - "# Rabi Experiment: sweep burst length" + "## 2. Rabi Experiment: sweep burst length" ] }, { @@ -150,7 +142,7 @@ "id": "bed6f007", "metadata": {}, "source": [ - "## Define parameters for experiment" + "### 2.1 Define parameters for experiment" ] }, { @@ -168,7 +160,7 @@ "START = 0\n", "STOP = LEN_COULOMB_CYCLE / 2\n", "STEPS = 5\n", - "NUM_REP = 2\n" + "NUM_REP = 2" ] }, { @@ -181,7 +173,7 @@ "## define length sweep parameter\n", "length_sweep_parameter = LinearSweepParameter(\n", " uid=\"length_sweep\", start=START, stop=STOP, count=STEPS\n", - ")\n" + ")" ] }, { @@ -189,7 +181,7 @@ "id": "687ff6a6", "metadata": {}, "source": [ - "## Experiment" + "### 2.2 Experiment" ] }, { @@ -208,7 +200,7 @@ ")\n", "drive_pulse = pulse_library.const(\n", " uid=\"rabi_drive_pulse\", length=X90_DURATION, amplitude=1\n", - ")\n" + ")" ] }, { @@ -267,7 +259,15 @@ " length=LEN_READOUT - 100e-9,\n", " trigger={\"drive\": {\"state\": 2}},\n", " ):\n", - " exp.reserve(signal=\"drive\")\n" + " exp.reserve(signal=\"drive\")" + ] + }, + { + "cell_type": "markdown", + "id": "6e8a6e1c", + "metadata": {}, + "source": [ + "### 2.3 Signal mapping" ] }, { @@ -291,7 +291,15 @@ " frequency=500e6,\n", " modulation_type=ModulationType.HARDWARE,\n", " )\n", - ")\n" + ")" + ] + }, + { + "cell_type": "markdown", + "id": "322dbc5d", + "metadata": {}, + "source": [ + "### 2.4 Session" ] }, { @@ -307,7 +315,7 @@ "\n", "# set experiment calibration and signal map\n", "exp.set_calibration(calib_q0)\n", - "exp.set_signal_map(map_q0)\n" + "exp.set_signal_map(map_q0)" ] }, { @@ -320,7 +328,15 @@ "if not session.connection_state.emulated:\n", " instrument_serial = device_setup.instrument_by_uid(\"device_hdawg\").address\n", " device = session.devices[instrument_serial]\n", - " device.triggers.out[2].delay(23.9e-9)\n" + " device.triggers.out[2].delay(23.9e-9)" + ] + }, + { + "cell_type": "markdown", + "id": "5bab2a0c", + "metadata": {}, + "source": [ + "### 2.5 Run" ] }, { @@ -330,7 +346,7 @@ "metadata": {}, "outputs": [], "source": [ - "session.run(exp)\n" + "session.run(exp)" ] }, { @@ -338,7 +354,7 @@ "id": "05562c77", "metadata": {}, "source": [ - "## View experiment in pulse sheet viewer" + "### 2.6 View experiment in pulse sheet viewer" ] }, { @@ -350,7 +366,7 @@ "source": [ "# use pulse sheet viewer to display the pulse sequence - only recommended for small number of averages and sweep steps to avoid performance issues\n", "compiled_exp = session.compiled_experiment\n", - "show_pulse_sheet(\"Spin qubit length Rabi\", compiled_exp)\n" + "show_pulse_sheet(\"Spin qubit length Rabi\", compiled_exp)" ] }, { @@ -358,15 +374,8 @@ "id": "bc2c27f8", "metadata": {}, "source": [ - "# Ramsey Experiment I: sweep wait time at constant burst length" - ] - }, - { - "cell_type": "markdown", - "id": "55f9133a", - "metadata": {}, - "source": [ - "## Experiment" + "## 3. Ramsey Experiment I: sweep wait time at constant burst length\n", + "### 3.1 Define parameters for experiment" ] }, { @@ -379,7 +388,7 @@ "START = 0\n", "STOP = LEN_COULOMB_CYCLE / 2 - 2 * X90_DURATION\n", "STEPS = 3\n", - "NUM_REP = 20\n" + "NUM_REP = 20" ] }, { @@ -392,7 +401,15 @@ "## Define sweep parameter\n", "sweep_delay = LinearSweepParameter(\n", " uid=\"Ramsey_delay\", start=START, stop=STOP, count=STEPS\n", - ")\n" + ")" + ] + }, + { + "cell_type": "markdown", + "id": "55f9133a", + "metadata": {}, + "source": [ + "### 3.2 Experiment" ] }, { @@ -446,7 +463,15 @@ " length=LEN_READOUT - 100e-9,\n", " trigger={\"drive\": {\"state\": 2}},\n", " ):\n", - " exp.reserve(signal=\"drive\")\n" + " exp.reserve(signal=\"drive\")" + ] + }, + { + "cell_type": "markdown", + "id": "44cc0461", + "metadata": {}, + "source": [ + "### 3.3 Signal mapping" ] }, { @@ -470,7 +495,15 @@ " frequency=500e6,\n", " modulation_type=ModulationType.HARDWARE,\n", " )\n", - ")\n" + ")" + ] + }, + { + "cell_type": "markdown", + "id": "190e4cdd", + "metadata": {}, + "source": [ + "### 3.4 Connect Session and Run" ] }, { @@ -488,7 +521,7 @@ "exp.set_calibration(calib_q0)\n", "exp.set_signal_map(map_q0)\n", "\n", - "session.run(exp)\n" + "session.run(exp)" ] }, { @@ -496,7 +529,7 @@ "id": "51ab74a9", "metadata": {}, "source": [ - "## View experiment in pulse sheet viewer" + "### 3.5 View experiment in pulse sheet viewer" ] }, { @@ -508,7 +541,7 @@ "source": [ "# use pulse sheet viewer to display the pulse sequence - only recommended for small number of averages and sweep steps to avoid performance issues\n", "compiled_exp = session.compiled_experiment\n", - "show_pulse_sheet(\"Ramsey variant I\", compiled_exp)\n" + "show_pulse_sheet(\"Ramsey variant I\", compiled_exp)" ] }, { @@ -516,7 +549,8 @@ "id": "4acd21e8", "metadata": {}, "source": [ - "# Ramsey Experiment II: sweep burst length at constant wait time" + "## 4. Ramsey Experiment II: sweep burst length at constant wait time\n", + "### 4.1 Define parameters for experiment" ] }, { @@ -527,7 +561,7 @@ "outputs": [], "source": [ "# define constant delay\n", - "T_DELAY = 40e-9\n" + "T_DELAY = 40e-9" ] }, { @@ -541,7 +575,7 @@ "START = 0\n", "STOP = (LEN_COULOMB_CYCLE / 2 - T_DELAY) / 2\n", "STEPS = 5\n", - "NUM_REP = 2\n" + "NUM_REP = 2" ] }, { @@ -549,7 +583,7 @@ "id": "b3e1bac3", "metadata": {}, "source": [ - "## Experiment" + "### 4.2 Experiment" ] }, { @@ -562,7 +596,7 @@ "## Define sweep parameter\n", "sweep_length = LinearSweepParameter(\n", " uid=\"pulse_length_sweep\", start=START, stop=STOP, count=STEPS\n", - ")\n" + ")" ] }, { @@ -619,7 +653,15 @@ " length=LEN_READOUT - 100e-9,\n", " trigger={\"drive\": {\"state\": 2}},\n", " ):\n", - " exp.reserve(signal=\"drive\")\n" + " exp.reserve(signal=\"drive\")" + ] + }, + { + "cell_type": "markdown", + "id": "6b5ee59a", + "metadata": {}, + "source": [ + "### 4.3 Signal Mapping" ] }, { @@ -643,7 +685,15 @@ " frequency=100e6,\n", " modulation_type=ModulationType.HARDWARE,\n", " )\n", - ")\n" + ")" + ] + }, + { + "cell_type": "markdown", + "id": "f04cba7d", + "metadata": {}, + "source": [ + "### 4.4 Connect Session and Run" ] }, { @@ -661,7 +711,7 @@ "exp.set_calibration(calib_q0)\n", "exp.set_signal_map(map_q0)\n", "\n", - "session.run(exp)\n" + "session.run(exp)" ] }, { @@ -669,7 +719,7 @@ "id": "ec6bf9fb", "metadata": {}, "source": [ - "## View experiment in pulse sheet viewer" + "### 4.5 View experiment in pulse sheet viewer" ] }, { @@ -681,7 +731,7 @@ "source": [ "# use pulse sheet viewer to display the pulse sequence - only recommended for small number of averages and sweep steps to avoid performance issues\n", "compiled_exp = session.compiled_experiment\n", - "show_pulse_sheet(\"Ramsey variant II\", compiled_exp)\n" + "show_pulse_sheet(\"Ramsey variant II\", compiled_exp)" ] }, { @@ -689,15 +739,8 @@ "id": "daa3a73a", "metadata": {}, "source": [ - "# Ramsey Experiment III: 2D sweep, wait time vs phase of second pulse" - ] - }, - { - "cell_type": "markdown", - "id": "ad40d7c9", - "metadata": {}, - "source": [ - "## Experiment" + "## 5. Ramsey Experiment III: 2D sweep, wait time vs phase of second pulse\n", + "### 5.1 Define parameters for experiment" ] }, { @@ -717,7 +760,7 @@ "\n", "START_DELAY = 0\n", "STOP_DELAY = LEN_COULOMB_CYCLE / 2 - 2 * X90_DURATION\n", - "STEPS_DELAY = 3\n" + "STEPS_DELAY = 3" ] }, { @@ -735,7 +778,15 @@ " uid=\"Ramsey_delay\", start=START_DELAY, stop=STOP_DELAY, count=STEPS_DELAY\n", ")\n", "\n", - "print(sweep_phase.values / np.pi)\n" + "print(sweep_phase.values / np.pi)" + ] + }, + { + "cell_type": "markdown", + "id": "ad40d7c9", + "metadata": {}, + "source": [ + "### 5.2 Experiment" ] }, { @@ -797,7 +848,15 @@ " length=LEN_READOUT - 100e-9,\n", " trigger={\"drive\": {\"state\": 2}},\n", " ):\n", - " exp.reserve(signal=\"drive\")\n" + " exp.reserve(signal=\"drive\")" + ] + }, + { + "cell_type": "markdown", + "id": "c10be024", + "metadata": {}, + "source": [ + "### 5.3 Signal Mapping" ] }, { @@ -823,7 +882,15 @@ " )\n", ")\n", "\n", - "print(\"Set modulation frequency to 0 Hz to better observe the phase sweep.\")\n" + "print(\"Set modulation frequency to 0 Hz to better observe the phase sweep.\")" + ] + }, + { + "cell_type": "markdown", + "id": "6878f3f7", + "metadata": {}, + "source": [ + "### 5.4 Connect Session and Run" ] }, { @@ -841,7 +908,7 @@ "exp.set_calibration(calib_q0)\n", "exp.set_signal_map(map_q0)\n", "\n", - "session.run(exp)\n" + "session.run(exp)" ] }, { @@ -849,7 +916,7 @@ "id": "519cb971", "metadata": {}, "source": [ - "## View experiment in pulse sheet viewer" + "### 5.5 View experiment in pulse sheet viewer" ] }, { @@ -861,7 +928,7 @@ "source": [ "# use pulse sheet viewer to display the pulse sequence - only recommended for small number of averages and sweep steps to avoid performance issues\n", "compiled_exp = session.compiled_experiment\n", - "show_pulse_sheet(\"Ramsey variant III\", compiled_exp)\n" + "show_pulse_sheet(\"Ramsey variant III\", compiled_exp)" ] } ], diff --git a/examples/05_color_centers/00_shfsg_basic_experiments.ipynb b/examples/05_color_centers/00_shfsg_basic_experiments.ipynb index d97dfa0..143fe15 100644 --- a/examples/05_color_centers/00_shfsg_basic_experiments.ipynb +++ b/examples/05_color_centers/00_shfsg_basic_experiments.ipynb @@ -27,7 +27,7 @@ "id": "46cfbe23", "metadata": {}, "source": [ - "# 0 - General Imports" + "## 0. General Imports" ] }, { @@ -60,7 +60,7 @@ "tags": [] }, "source": [ - "# 1 - Device Setup\n", + "## 1. Device Setup\n", "\n", "We first need to define a calibration and our device setup." ] @@ -73,7 +73,7 @@ "tags": [] }, "source": [ - "## 1.1 - Calibration" + "### 1.1 Calibration" ] }, { @@ -82,7 +82,7 @@ "id": "36612270", "metadata": {}, "source": [ - "Read about applying intrument settings through [calibration objects](https://docs.zhinst.com/labone_q_user_manual/concepts/instrument_calibration.html) and their [properties](https://docs.zhinst.com/labone_q_user_manual/concepts/calibration_properties.html).\n", + "Read about applying instrument settings through [calibration objects](https://docs.zhinst.com/labone_q_user_manual/concepts/instrument_calibration.html) and their [properties](https://docs.zhinst.com/labone_q_user_manual/concepts/calibration_properties.html).\n", "\n", "Here, we configure two lines of the SHFSG signal generator to play the pulses we need: The first line is used to drive the color center and is centered around 2.9 GHz. The second line will drive the AOM and uses the low-frequency output mode of the SHFSG channels." ] @@ -144,7 +144,7 @@ "tags": [] }, "source": [ - "## 1.2 - Create device setup" + "### 1.2 Create device setup" ] }, { @@ -205,7 +205,7 @@ "tags": [] }, "source": [ - "## 1.3 - Connect" + "### 1.3 Connect" ] }, { @@ -250,7 +250,7 @@ "id": "a96a532c", "metadata": {}, "source": [ - "# 2 - Pulse defintions - To be used throughout" + "## 2. Pulse definitions - To be used throughout" ] }, { @@ -274,7 +274,7 @@ "id": "63bf3d34", "metadata": {}, "source": [ - "# 3 - ODMR Spectroscopy" + "## 3. ODMR Spectroscopy" ] }, { @@ -283,7 +283,7 @@ "id": "7694a75b", "metadata": {}, "source": [ - "## 3.1 - More pulse parameters\n", + "### 3.1 More pulse parameters\n", "\n", "We define a frequency sweep to be used in the spectroscopy experiment, in which we'll use a rectangular excitation pulse of 500 ns length, which we also define here." ] @@ -383,7 +383,7 @@ "id": "21fa014a", "metadata": {}, "source": [ - "## 3.3 - Experiment Calibration and Signal Map" + "### 3.3 Experiment Calibration and Signal Map" ] }, { @@ -423,7 +423,7 @@ "id": "5cfe5462", "metadata": {}, "source": [ - "## 3.4 - Apply Experiment Settings and Run" + "### 3.4 Apply Experiment Settings and Run" ] }, { @@ -466,7 +466,7 @@ "id": "fc81f169", "metadata": {}, "source": [ - "## 3.5 - Using the Output Simulator\n", + "### 3.5 Using the Output Simulator\n", "\n", "The output simulator displays a true time-domain representation of the pulses played within the experiment." ] @@ -487,7 +487,7 @@ "id": "169b0a78", "metadata": {}, "source": [ - "## 3.6 - Pulse Sheet Viewer\n", + "### 3.6 Pulse Sheet Viewer\n", "\n", "The pulse sheet viewer displays the higher-level pulse sheet." ] @@ -509,7 +509,7 @@ "id": "edf6cb9e", "metadata": {}, "source": [ - "# 4.0 - Length Rabi\n", + "## 4. Length Rabi\n", "\n", "A sweep object is rather flexible, and can be used for many different purposes. For example, let's use it to sweep the length of a drive pulse to obtain a Rabi sequence." ] @@ -520,7 +520,7 @@ "id": "0a7b92ab", "metadata": {}, "source": [ - "## 4.1 - Define Pulse Parameters" + "### 4.1 Define Pulse Parameters" ] }, { @@ -551,7 +551,7 @@ "id": "e9f7d4dc", "metadata": {}, "source": [ - "## 4.2 - Experiment Definition and Sequence\n", + "### 4.2 Experiment Definition and Sequence\n", "\n", "An alternative to trigger are markers. Their functionality inside of an experiment is the same, but they are synchronized to the wave output of a signal line. Hence, they have an increased precision compared to triggers. Notice that because of this they don't live in the section, but they are rather connected to a specific play instruction." ] @@ -616,7 +616,7 @@ "id": "37f7f730", "metadata": {}, "source": [ - "## 4.3 - Set Map and Update Oscillator Freq" + "### 4.3 Set Map and Update Oscillator Freq" ] }, { @@ -639,7 +639,7 @@ "id": "7ab10805", "metadata": {}, "source": [ - "## 4.4 - Apply Settings and Run Experiment" + "### 4.4 Apply Settings and Run Experiment" ] }, { @@ -662,7 +662,7 @@ "id": "be0e02d3", "metadata": {}, "source": [ - "## 4.5 - Plot with Output Simulator" + "### 4.5 Plot with Output Simulator" ] }, { @@ -681,7 +681,7 @@ "id": "2dbc9e29", "metadata": {}, "source": [ - "## 4.6 - Show in Pulse Sheet Viewer" + "### 4.6 Show in Pulse Sheet Viewer" ] }, { @@ -703,7 +703,7 @@ "tags": [] }, "source": [ - "# Ramsey Experiment" + "## 5. Ramsey Experiment" ] }, { @@ -712,7 +712,7 @@ "id": "c85e6cc1", "metadata": {}, "source": [ - "## 5.1 - All-in-one experiment definitions and signal mapping\n", + "### 5.1 All-in-one experiment definitions and signal mapping\n", "\n", "Let's make our experiment customizable by creating a function that allow us to specify the parameters later on. This time, our sweep parameter is the time that we wait between two pi/2 pulses." ] @@ -798,7 +798,7 @@ "id": "3c95204b", "metadata": {}, "source": [ - "## 5.2 - Apply settings and run" + "### 5.2 Apply settings and run" ] }, { @@ -822,7 +822,7 @@ "id": "afe74a86", "metadata": {}, "source": [ - "## 5.3 - Plot in Output Simulator" + "### 5.3 Plot in Output Simulator" ] }, { @@ -841,7 +841,7 @@ "id": "d1fbc721", "metadata": {}, "source": [ - "## 5.4 - Show Pulse Sheet" + "### 5.4 Show Pulse Sheet" ] }, { diff --git a/examples/06_qasm/01_VQE_Qiskit.ipynb b/examples/06_qasm/01_VQE_Qiskit.ipynb index 15137a8..cc83fc9 100755 --- a/examples/06_qasm/01_VQE_Qiskit.ipynb +++ b/examples/06_qasm/01_VQE_Qiskit.ipynb @@ -44,9 +44,9 @@ "\n", "from laboneq.simple import *\n", "\n", + "from laboneq._utils import id_generator\n", "from laboneq.contrib.example_helpers.plotting.plot_helpers import plot_simulation\n", "from laboneq.pulse_sheet_viewer.interactive_psv import interactive_psv\n", - "from laboneq.dsl.experiment.utils import id_generator\n", "from laboneq.openqasm3.gate_store import GateStore\n", "from laboneq.contrib.example_helpers.descriptors.shfsg_shfqa_shfqc_hdawg_pqsc import (\n", " descriptor_shfsg_shfqa_shfqc_hdawg_pqsc,\n", diff --git a/examples/06_qasm/02_Two_Qubit_RB_Qiskit.ipynb b/examples/06_qasm/02_Two_Qubit_RB_Qiskit.ipynb index d6194ba..2f8782c 100644 --- a/examples/06_qasm/02_Two_Qubit_RB_Qiskit.ipynb +++ b/examples/06_qasm/02_Two_Qubit_RB_Qiskit.ipynb @@ -21,7 +21,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Python Imports" + "## Python Imports" ] }, { @@ -39,8 +39,8 @@ "from laboneq.pulse_sheet_viewer.interactive_psv import interactive_psv\n", "\n", "# device setup and descriptor\n", + "from laboneq._utils import id_generator\n", "from laboneq.dsl.utils import calibrate_devices\n", - "from laboneq.dsl.experiment.utils import id_generator\n", "from laboneq.contrib.example_helpers.generate_descriptor import generate_descriptor\n", "\n", "# open qasm importer\n", @@ -59,7 +59,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Set up Qiskit-generated RB" + "## Set up Qiskit-generated RB" ] }, { @@ -125,7 +125,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# LabOne Q Experiment" + "## LabOne Q Experiment" ] }, { @@ -423,7 +423,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Two Qubit RB" + "### Two Qubit RB" ] }, { @@ -439,7 +439,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Connect to Session" + "#### Connect to Session" ] }, { @@ -465,7 +465,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Define Gates, Load QASM 3 Program, and Go!" + "#### Define Gates, Load QASM 3 Program, and Go!" ] }, { @@ -529,7 +529,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Draw the circuit from above" + "#### Draw the circuit from above" ] }, { @@ -554,7 +554,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Compile and draw more circuits in the list" + "#### Compile and draw more circuits in the list" ] }, { @@ -640,7 +640,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.4" + "version": "3.11.3" }, "orig_nbformat": 4 }, diff --git a/examples/06_qasm/03_Two_Qubit_RB_pyGSTi_OpenQASM.ipynb b/examples/06_qasm/03_Two_Qubit_RB_pyGSTi_OpenQASM.ipynb index ad546a7..e2fb8a3 100644 --- a/examples/06_qasm/03_Two_Qubit_RB_pyGSTi_OpenQASM.ipynb +++ b/examples/06_qasm/03_Two_Qubit_RB_pyGSTi_OpenQASM.ipynb @@ -21,7 +21,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Python Imports" + "## Python Imports" ] }, { @@ -38,8 +38,8 @@ "from laboneq.contrib.example_helpers.data_analysis.data_analysis import *\n", "\n", "# device setup and descriptor\n", + "from laboneq._utils import id_generator\n", "from laboneq.dsl.utils import calibrate_devices\n", - "from laboneq.dsl.experiment.utils import id_generator\n", "from laboneq.contrib.example_helpers.generate_descriptor import generate_descriptor\n", "\n", "# LabOne Q OpenQASM Tools\n", @@ -62,7 +62,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# pyGSTi Experiment Generation" + "## pyGSTi Experiment Generation" ] }, { @@ -226,7 +226,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# LabOne Q Experiment" + "## LabOne Q Experiment" ] }, { @@ -500,7 +500,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Two Qubit RB" + "### Two Qubit RB" ] }, { @@ -516,7 +516,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Connect to Session" + "#### Connect to Session" ] }, { @@ -542,7 +542,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Define Gates, Load QASM 3 Program, and Go!" + "#### Define Gates, Load QASM 3 Program, and Go!" ] }, { @@ -608,7 +608,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Draw the circuit from above" + "#### Draw the circuit from above" ] }, { @@ -634,7 +634,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Compile and draw more circuits in the list" + "#### Compile and draw more circuits in the list" ] }, { diff --git a/laboneq/VERSION.txt b/laboneq/VERSION.txt index ed0edc8..3ca2c9b 100644 --- a/laboneq/VERSION.txt +++ b/laboneq/VERSION.txt @@ -1 +1 @@ -2.11.0 \ No newline at end of file +2.12.0 \ No newline at end of file diff --git a/laboneq/_utils.py b/laboneq/_utils.py index 6d110d8..27100cb 100644 --- a/laboneq/_utils.py +++ b/laboneq/_utils.py @@ -3,7 +3,9 @@ """General utility functions for development.""" import functools -from typing import Any +from collections import defaultdict +from itertools import count +from typing import Any, Iterable def cached_method(maxsize: int = 128, typed=False) -> Any: @@ -31,3 +33,27 @@ def cached_func(*args, **kwargs): return wrapper return outer_wrapper + + +def ensure_list(obj): + if not isinstance(obj, list): + return [obj] + return obj + + +def flatten(l: Iterable): + """Flatten an arbitrarily nested list.""" + for el in l: + if isinstance(el, Iterable) and not isinstance(el, (str, bytes)): + yield from flatten(el) + else: + yield el + + +_iid_map = defaultdict(count) + + +def id_generator(cat: str = "") -> str: + """Incremental IDs for each category.""" + global _iid_map + return f"_{cat}_{next(_iid_map[cat])}" diff --git a/laboneq/compiler/code_generator/__init__.py b/laboneq/compiler/code_generator/__init__.py index 05fb203..34aa406 100644 --- a/laboneq/compiler/code_generator/__init__.py +++ b/laboneq/compiler/code_generator/__init__.py @@ -10,9 +10,5 @@ IntegrationTimes, MeasurementCalculator, ) -from laboneq.compiler.code_generator.seq_c_generator import ( - SeqCGenerator, - string_sanitize, -) +from laboneq.compiler.code_generator.seq_c_generator import SeqCGenerator from laboneq.compiler.code_generator.wave_index_tracker import WaveIndexTracker -from laboneq.compiler.experiment_access.pulse_def import PulseDef diff --git a/laboneq/compiler/code_generator/analyze_events.py b/laboneq/compiler/code_generator/analyze_events.py index c4f3363..f2623de 100644 --- a/laboneq/compiler/code_generator/analyze_events.py +++ b/laboneq/compiler/code_generator/analyze_events.py @@ -11,6 +11,7 @@ from engineering_notation import EngNumber from sortedcontainers import SortedDict +from laboneq._utils import ensure_list from laboneq.compiler.code_generator.feedback_register_allocator import ( FeedbackRegisterAllocator, ) @@ -45,7 +46,7 @@ def analyze_loop_times( for e in events: if ( e["event_type"] in ["PLAY_START", "ACQUIRE_START"] - and e.get("signal") in signal_ids + and ensure_list(e.get("signal"))[0] in signal_ids ): plays_anything = True break @@ -300,7 +301,7 @@ def analyze_set_oscillator_times( for event in events if event["event_type"] == "SET_OSCILLATOR_FREQUENCY_START" and event.get("device_id") == device_id - and event.get("signal") == signal_id + and ensure_list(event.get("signal"))[0] == signal_id ] if len(set_oscillator_events) == 0: return AWGSampledEventSequence() @@ -339,6 +340,7 @@ def analyze_set_oscillator_times( "parameter_name": event["parameter"]["id"], "iteration": iteration, "iterations": len(iterations), + "oscillator_id": event["oscillator_id"], }, ) @@ -368,6 +370,7 @@ def analyze_acquire_times( @dataclass class IntervalStartEvent: + signals: str | list[str] event_type: str time: float play_wave_id: str @@ -375,6 +378,7 @@ class IntervalStartEvent: acquire_handle: str play_pulse_parameters: Optional[Dict[str, Any]] pulse_pulse_parameters: Optional[Dict[str, Any]] + channels: list[int | list[int]] @dataclass class IntervalEndEvent: @@ -386,6 +390,7 @@ class IntervalEndEvent: zip( [ IntervalStartEvent( + event["signal"], event["event_type"], event["time"] + delay, event["play_wave_id"], @@ -393,10 +398,11 @@ class IntervalEndEvent: event["acquire_handle"], event.get("play_pulse_parameters"), event.get("pulse_pulse_parameters"), + event.get("channel") or channels, ) for event in events if event["event_type"] in ["ACQUIRE_START"] - and event["signal"] == signal_id + and signal_id in ensure_list(event["signal"]) ], [ IntervalEndEvent( @@ -406,7 +412,7 @@ class IntervalEndEvent: ) for event in events if event["event_type"] in ["ACQUIRE_END"] - and event["signal"] == signal_id + and signal_id in ensure_list(event["signal"]) ], ) ) @@ -437,12 +443,13 @@ class IntervalEndEvent: start=start_samples, end=end_samples, params={ - "signal_id": signal_id, + "signal_id": ensure_list(interval_start.signals)[0], + "signals": interval_start.signals, # for multistate discrimination "play_wave_id": interval_start.play_wave_id, "acquisition_type": interval_start.acquisition_type, "acquire_handles": [interval_start.acquire_handle], "feedback_register": feedback_register, - "channels": channels, + "channels": interval_start.channels, "play_pulse_parameters": interval_start.play_pulse_parameters, "pulse_pulse_parameters": interval_start.pulse_pulse_parameters, }, @@ -460,7 +467,7 @@ def analyze_trigger_events( event for event in events if event["event_type"] == EventType.DIGITAL_SIGNAL_STATE_CHANGE - and signal.id == event["signal"] + and signal.id == ensure_list(event["signal"])[0] ] delay = signal.total_delay sampling_rate = signal.awg.sampling_rate diff --git a/laboneq/compiler/code_generator/analyze_playback.py b/laboneq/compiler/code_generator/analyze_playback.py index fb2fdbb..83ff93c 100644 --- a/laboneq/compiler/code_generator/analyze_playback.py +++ b/laboneq/compiler/code_generator/analyze_playback.py @@ -445,9 +445,7 @@ def _make_pulse_signature(pulse_iv: Interval, wave_iv: Interval, signal_ids: Lis pulse_parameters=None if combined_pulse_parameters is None else frozenset(combined_pulse_parameters.items()), - markers=None - if markers is None - else tuple(frozenset(m.items()) for m in markers), + markers=None if not markers else tuple(frozenset(m.items()) for m in markers), ) pulse_parameters = ( frozenset((data.play_pulse_parameters or {}).items()), diff --git a/laboneq/compiler/code_generator/code_generator.py b/laboneq/compiler/code_generator/code_generator.py index 2a052d7..5b9ca5f 100644 --- a/laboneq/compiler/code_generator/code_generator.py +++ b/laboneq/compiler/code_generator/code_generator.py @@ -10,11 +10,12 @@ from collections import namedtuple from contextlib import suppress from itertools import groupby -from typing import Any, Dict, List, NamedTuple, Tuple +from typing import Any, Dict, List, NamedTuple, Set, Tuple import numpy as np from engineering_notation import EngNumber +from laboneq._utils import ensure_list from laboneq.compiler.code_generator.analyze_events import ( analyze_acquire_times, analyze_init_times, @@ -71,7 +72,6 @@ from laboneq.compiler.common.pulse_parameters import decode_pulse_parameters from laboneq.compiler.common.signal_obj import SignalObj from laboneq.compiler.common.trigger_mode import TriggerMode -from laboneq.compiler.experiment_access.pulse_def import PulseDef from laboneq.core.exceptions import LabOneQException from laboneq.core.utilities.pulse_sampler import ( combine_pulse_parameters, @@ -79,6 +79,7 @@ sample_pulse, verify_amplitude_no_clipping, ) +from laboneq.data.compilation_job import ParameterInfo, PulseDef from laboneq.data.scheduled_experiment import ( PulseInstance, PulseMapEntry, @@ -170,74 +171,90 @@ def setup_sine_phase( def calculate_integration_weights( - acquire_events: AWGSampledEventSequence, signal_obj, pulse_defs, device_type + acquire_events: AWGSampledEventSequence, signal_obj, pulse_defs ): integration_weights = {} for event_list in acquire_events.sequence.values(): for event in event_list: _logger.debug("For weights, look at %s", event) - play_wave_id = event.params.get("play_wave_id") - if play_wave_id is not None: + signal_ids = ensure_list( + event.params.get("signals") or event.params.get("signal_id") or [] + ) + n = len(signal_ids) + play_wave_ids = ensure_list(event.params.get("play_wave_id") or [None] * n) + play_pars = ensure_list( + event.params.get("play_pulse_parameters") or [None] * n + ) + pulse_pars = ensure_list( + event.params.get("pulse_pulse_parameters") or [None] * n + ) + assert n == len(play_wave_ids) == len(play_pars) == len(pulse_pars) + for signal_id, play_wave_id, play_par, pulse_par in zip( + signal_ids, play_wave_ids, play_pars, pulse_pars + ): + if play_wave_id is None or signal_id != signal_obj.id: + continue _logger.debug("Event %s has play wave id %s", event, play_wave_id) if ( - play_wave_id in pulse_defs - and play_wave_id not in integration_weights + play_wave_id not in pulse_defs + or play_wave_id in integration_weights ): - pulse_def = pulse_defs[play_wave_id] - _logger.debug("Pulse def: %s", pulse_def) + continue + pulse_def = pulse_defs[play_wave_id] + _logger.debug("Pulse def: %s", pulse_def) - if None is pulse_def.samples is pulse_def.function: - # Not a real pulse, just a placeholder for the length - skip - continue + if None is pulse_def.samples and None is pulse_def.function: + # Not a real pulse, just a placeholder for the length - skip + continue - samples = pulse_def.samples - amplitude = pulse_def.effective_amplitude + samples = pulse_def.samples + pulse_amplitude = pulse_def.amplitude + if pulse_amplitude is None: + pulse_amplitude = 1.0 + elif isinstance(pulse_amplitude, ParameterInfo): + pulse_amplitude = event.params.get(pulse_def.amplitude.uid) - length = pulse_def.length - if length is None: - length = len(samples) / signal_obj.awg.sampling_rate + length = pulse_def.length + if length is None: + length = len(samples) / signal_obj.awg.sampling_rate - _logger.debug( - "Sampling integration weights for %s with modulation_frequency %s", - signal_obj.id, - str(signal_obj.oscillator_frequency), - ) + _logger.debug( + "Sampling integration weights for %s with modulation_frequency %s", + signal_obj.id, + signal_obj.oscillator_frequency, + ) - pulse_parameters = combine_pulse_parameters( - event.params.get("pulse_pulse_parameters"), - None, - event.params.get("play_pulse_parameters"), - ) - pulse_parameters = decode_pulse_parameters(pulse_parameters) - integration_weight = sample_pulse( - signal_type="iq", - sampling_rate=signal_obj.awg.sampling_rate, - length=length, - amplitude=amplitude, - pulse_function=pulse_def.function, - modulation_frequency=signal_obj.oscillator_frequency, - samples=samples, - mixer_type=signal_obj.mixer_type, - pulse_parameters=pulse_parameters, - ) + pulse_parameters = combine_pulse_parameters(pulse_par, None, play_par) + pulse_parameters = decode_pulse_parameters(pulse_parameters) + integration_weight = sample_pulse( + signal_type="iq", + sampling_rate=signal_obj.awg.sampling_rate, + length=length, + amplitude=pulse_amplitude, + pulse_function=pulse_def.function, + modulation_frequency=signal_obj.oscillator_frequency, + samples=samples, + mixer_type=signal_obj.mixer_type, + pulse_parameters=pulse_parameters, + ) - verify_amplitude_no_clipping( - integration_weight, - pulse_def.id, - signal_obj.mixer_type, - signal_obj.id, - ) + verify_amplitude_no_clipping( + integration_weight, + pulse_def.uid, + signal_obj.mixer_type, + signal_obj.id, + ) - integration_weight["basename"] = ( - signal_obj.awg.device_id - + "_" - + str(signal_obj.awg.awg_number) - + "_" - + str(min(signal_obj.channels)) - + "_" - + play_wave_id - ) - integration_weights[play_wave_id] = integration_weight + integration_weight["basename"] = ( + signal_obj.awg.device_id + + "_" + + str(signal_obj.awg.awg_number) + + "_" + + str(min(signal_obj.channels)) + + "_" + + play_wave_id + ) + integration_weights[play_wave_id] = integration_weight return integration_weights @@ -282,6 +299,7 @@ def __init__(self, settings: CompilerSettings = None): self._feedback_register_allocator: FeedbackRegisterAllocator = None self._total_execution_time = None self._wave_compressor = WaveCompressor() + self._multistate_signal_groups = set() self.EMIT_TIMING_COMMENTS = self._settings.EMIT_TIMING_COMMENTS self.PHASE_RESOLUTION_BITS = self._settings.PHASE_RESOLUTION_BITS @@ -333,8 +351,6 @@ def _save_wave_bin( self._append_to_pulse_map(signature_pulse_map, sig_string) def gen_waves(self): - if _logger.getEffectiveLevel() == logging.DEBUG: - _logger.debug("Sampled signatures: %s", self._sampled_signatures) for awg in self._awgs.values(): # Handle integration weights separately for signal_obj in awg.signals: @@ -488,17 +504,23 @@ def gen_waves(self): group = list(group) assert all(np.all(group[0][1] == g[1]) for g in group[1:]) - if _logger.getEffectiveLevel() == logging.DEBUG: - _logger.debug("Sampled signatures: %s", self._sampled_signatures) - - _logger.debug(self._waves) - def gen_acquire_map(self, events: EventList): # timestamp -> map[signal -> handle] self._simultaneous_acquires: Dict[float, Dict[str, str]] = {} for e in (e for e in events if e["event_type"] == "ACQUIRE_START"): time_events = self._simultaneous_acquires.setdefault(e["time"], {}) - time_events[e["signal"]] = e["acquire_handle"] + for s in ensure_list(e["signal"]): + time_events[s] = e["acquire_handle"] + + def find_multistate_signal_groups(self, events: EventList): + # find all signals that are used in a multi-state acquisition + for e in events: + if ( + e["event_type"] == "ACQUIRE_START" + and isinstance(e["signal"], list) + and len(e["signal"]) > 1 + ): + self._multistate_signal_groups.add(tuple(sorted(e["signal"]))) def gen_seq_c(self, events: List[Any], pulse_defs: Dict[str, PulseDef]): signal_info_map = { @@ -693,7 +715,10 @@ def _compress_waves( event_replacement = {} for i, event in enumerate(event_group): if event.type == AWGEventType.ACQUIRE: - if pulse_defs[event.params["play_wave_id"]].can_compress: + if any( + pulse_defs[id].can_compress + for id in ensure_list(event.params["play_wave_id"]) + ): _logger.warn( "Compression for integration pulses is not supported. %s, for which compression has been requested, will not be compressed.", event.params["play_wave_id"], @@ -867,7 +892,7 @@ def _gen_seq_c_per_awg( own_sections = set( event["section_name"] for event in events - if event.get("signal") in signal_ids + if ensure_list(event.get("signal"))[0] in signal_ids or event["event_type"] in ( EventType.PARAMETER_SET, @@ -876,7 +901,9 @@ def _gen_seq_c_per_awg( EventType.SET_OSCILLATOR_FREQUENCY_START, ) or set( - [to_item["signal_id"] for to_item in event.get("trigger_output", [])] + s + for to_item in event.get("trigger_output", []) + for s in ensure_list(to_item["signal_id"]) ).intersection(signal_ids) ) has_match_case = False @@ -940,6 +967,18 @@ def _gen_seq_c_per_awg( ) sampled_events.merge(loop_events) + # Retrieve the channel numbers for multistate discrimination + # events + for signal_obj in awg.signals: + for e in events: + if (sigs := e.get("signal")) and isinstance(sigs, list): + if not e.get("channel"): + e["channel"] = [None] * len(sigs) + for i, s in enumerate(sigs): + if s == signal_obj.id: + e["channel"][i] = signal_obj.channels + break + for signal_obj in awg.signals: set_oscillator_events = analyze_set_oscillator_times( events, signal_obj, global_delay @@ -975,9 +1014,19 @@ def _gen_seq_c_per_awg( self._integration_weights[ signal_obj.id ] = calculate_integration_weights( - acquire_events, signal_obj, pulse_defs, awg.device_type + acquire_events, signal_obj, pulse_defs ) + # We have created events for all signals of a multistate + # discrimination; later, we only need one of them. Let's choose + # the one where the first signal id matches. + for t in acquire_events.sequence: + acquire_events.sequence[t] = [ + e + for e in acquire_events.sequence[t] + if e.params["signal_id"] == signal_obj.id + ] + sampled_events.merge(acquire_events) if awg.signal_type == AWGSignalType.MULTI: @@ -1357,14 +1406,16 @@ def _sample_pulses( # that is used to reset the precompensation. It is all zeros, but we # use it to force a playWave command to be generated. pulse_def = PulseDef( - id="dummy_precomp_reset", + uid="dummy_precomp_reset", length=32, amplitude=1.0, samples=np.zeros(32), - play_mode="", function=None, - amplitude_param=None, ) + + if pulse_def.amplitude is None: + pulse_def = copy.deepcopy(pulse_def) + pulse_def.amplitude = 1.0 _logger.debug(" Pulse def: %s", pulse_def) sampling_signal_type = signal_type @@ -1375,8 +1426,7 @@ def _sample_pulses( if pulse_part.sub_channel is not None: sampling_signal_type = "iq" - amplitude = pulse_def.effective_amplitude - + amplitude = pulse_def.amplitude if pulse_part.amplitude is not None: amplitude *= pulse_part.amplitude @@ -1428,12 +1478,12 @@ def _sample_pulses( mixer_type=mixer_type, pulse_parameters=decoded_pulse_parameters, markers=None - if pulse_part.markers is None + if not pulse_part.markers else [{k: v for k, v in m} for m in pulse_part.markers], ) verify_amplitude_no_clipping( - sampled_pulse, pulse_def.id, mixer_type, signal_id + sampled_pulse, pulse_def.uid, mixer_type, signal_id ) if "samples_q" in sampled_pulse and len( @@ -1520,7 +1570,7 @@ def _sample_pulses( ) has_marker2 = True - pm = signature_pulse_map.get(pulse_def.id) + pm = signature_pulse_map.get(pulse_def.uid) if pm is None: pm = PulseWaveformMap( sampling_rate=sampling_rate, @@ -1528,11 +1578,10 @@ def _sample_pulses( signal_type=sampling_signal_type, mixer_type=mixer_type, ) - signature_pulse_map[pulse_def.id] = pm + signature_pulse_map[pulse_def.uid] = pm + pulse_amplitude = pulse_def.amplitude amplitude_multiplier = ( - amplitude / pulse_def.effective_amplitude - if pulse_def.effective_amplitude - else 0.0 + amplitude / pulse_amplitude if pulse_amplitude else 0.0 ) pm.instances.append( PulseInstance( @@ -1617,6 +1666,9 @@ def feedback_connections(self) -> Dict[str, FeedbackConnection]: def feedback_registers(self) -> Dict[AwgKey, int]: return self._feedback_register_allocator.feedback_registers + def multistate_signal_groups(self) -> Set[Tuple[str, ...]]: + return self._multistate_signal_groups + @staticmethod def stencil_samples(start, source, target): source_start = 0 diff --git a/laboneq/compiler/code_generator/measurement_calculator.py b/laboneq/compiler/code_generator/measurement_calculator.py index 39a6d48..8f653c7 100644 --- a/laboneq/compiler/code_generator/measurement_calculator.py +++ b/laboneq/compiler/code_generator/measurement_calculator.py @@ -8,8 +8,10 @@ from engineering_notation import EngNumber +from laboneq._utils import ensure_list from laboneq.compiler.common.device_type import DeviceType from laboneq.compiler.fastlogging import NullLogger +from laboneq.core.exceptions import LabOneQException from laboneq.core.types.enums import AcquisitionType _logger = logging.getLogger(__name__) @@ -98,10 +100,17 @@ def calc_awg_key(signal_id): return (acquire_signal_info["device_id"], acquire_signal_info["awg_number"]) def group_by_awg_key(event): - return (calc_awg_key(event.get("signal", "")),) + signal = event.get("signal", "") + # Multiple signals in the multistate case play on the same AWG, thus take the alphabetically + # lowest signal for disambiguation. + return (calc_awg_key(min(signal) if isinstance(signal, list) else signal),) def group_by_signal_and_section(event): - return (event.get("signal", ""), event.get("section_name", "")) + signal = event.get("signal", "") + return ( + min(signal) if isinstance(signal, list) else signal, + event.get("section_name", ""), + ) for awg_key, events_for_awg_iterator in groupby( sorted(acquire_and_play_events, key=group_by_awg_key), key=group_by_awg_key @@ -110,7 +119,7 @@ def group_by_signal_and_section(event): if awg_key not in signals_on_awg: signals_on_awg[awg_key] = set() - if "ACQUIRE_START" in {e["event_type"] for e in events_for_awg}: + if "ACQUIRE_START" in (e["event_type"] for e in events_for_awg): # there are acquire events on this AWG for k, events_for_signal_and_section in groupby( sorted(events_for_awg, key=group_by_signal_and_section), @@ -132,6 +141,14 @@ def group_by_signal_and_section(event): if signal_info.get("delay_signal") is not None: delay_signal = signal_info.get("delay_signal") + if any( + signal_info_map[sig].get("delay_signal") != delay_signal + for sig in ensure_list(k)[1:] + ): + raise LabOneQException( + f"All acquire/play signals in section {section_name} must have the same signal delay." + ) + for event in sorted( events_for_signal_and_section, key=lambda x: x["time"] ): @@ -196,7 +213,9 @@ def group_by_signal_and_section(event): for signal, signal_integration_info in section_info.items(): signal_info = signal_info_map[signal] - device_type = DeviceType(signal_info["device_type"]) + device_type = DeviceType.from_device_info_type( + signal_info["device_type"] + ) sampling_rate = device_type.sampling_rate signal_integration_info.awg = signal_info["awg_number"] @@ -228,9 +247,9 @@ def group_by_signal_and_section(event): delays_per_awg[awg_key]["delays"].add(round(delay_time * sampling_rate)) delays_per_awg[awg_key]["signals"].append(signal) delays_per_awg[awg_key]["sections"].append(section_name) - delays_per_awg[awg_key]["device_type"] = DeviceType( - signal_info["device_type"] - ) + delays_per_awg[awg_key][ + "device_type" + ] = DeviceType.from_device_info_type(signal_info["device_type"]) _dlogger.debug("Delays per awg: %s", delays_per_awg) for awg_key, delays in delays_per_awg.items(): @@ -255,7 +274,9 @@ def group_by_signal_and_section(event): def calc_and_round_delay(signal, delay_in_samples): signal_info = signal_info_map[signal] - device_type = DeviceType(signal_info["device_type"]) + device_type = DeviceType.from_device_info_type( + signal_info["device_type"] + ) sampling_rate = signal_info["sampling_rate"] rest = delay_in_samples % device_type.sample_multiple _dlogger.debug( diff --git a/laboneq/compiler/code_generator/sampled_event_handler.py b/laboneq/compiler/code_generator/sampled_event_handler.py index e07749a..20dde84 100644 --- a/laboneq/compiler/code_generator/sampled_event_handler.py +++ b/laboneq/compiler/code_generator/sampled_event_handler.py @@ -7,10 +7,10 @@ from dataclasses import dataclass, field from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Set +from laboneq._utils import flatten from laboneq.compiler.code_generator.seq_c_generator import ( SeqCGenerator, merge_generators, - string_sanitize, ) from laboneq.compiler.code_generator.signatures import PlaybackSignature from laboneq.compiler.common.awg_sampled_event import ( @@ -22,6 +22,7 @@ from laboneq.compiler.common.device_type import DeviceType from laboneq.core.exceptions import LabOneQException from laboneq.core.types.enums import AcquisitionType +from laboneq.core.utilities.string_sanitize import string_sanitize if TYPE_CHECKING: from laboneq.compiler.code_generator.command_table_tracker import ( @@ -387,16 +388,14 @@ def handle_qa_event(self, sampled_event: AWGEvent): current_signal_obj.channels[0], ) - integration_channels = [ - event.params["channels"] for event in sampled_event.params["acquire_events"] - ] - - integration_channels = [ - item for sublist in integration_channels for item in sublist - ] + integration_channels = list( + flatten( + event.params["channels"] + for event in sampled_event.params["acquire_events"] + ) + ) if len(integration_channels) > 0: - integrator_mask = "|".join( map(lambda x: "QA_INT_" + str(x), integration_channels) ) @@ -546,15 +545,21 @@ def _handle_set_oscillator_frequency_shf(self, sampled_event: AWGEvent): iteration = sampled_event.params["iteration"] parameter_name = sampled_event.params["parameter_name"] counter_variable_name = string_sanitize(f"index_{parameter_name}") + osc_id_symbol = string_sanitize(sampled_event.params["oscillator_id"]) if not self.declarations_generator.is_variable_declared(counter_variable_name): self.declarations_generator.add_variable_declaration( counter_variable_name, 0 ) + self.declarations_generator.add_constant_definition( + osc_id_symbol, + 0, + "preliminary! will be updated by controller", + ) self.declarations_generator.add_function_call_statement( "configFreqSweep", ( - 0, + osc_id_symbol, sampled_event.params["start_frequency"], sampled_event.params["step_frequency"], ), @@ -569,9 +574,10 @@ def _handle_set_oscillator_frequency_shf(self, sampled_event: AWGEvent): if iteration == 0: self.seqc_tracker.add_variable_assignment(counter_variable_name, 0) self.seqc_tracker.add_required_playzeros(sampled_event) + self.seqc_tracker.add_function_call_statement( "setSweepStep", - args=(0, f"{counter_variable_name}++"), + args=(osc_id_symbol, f"{counter_variable_name}++"), deferred=True, ) diff --git a/laboneq/compiler/code_generator/seq_c_generator.py b/laboneq/compiler/code_generator/seq_c_generator.py index cb977e5..cee0004 100644 --- a/laboneq/compiler/code_generator/seq_c_generator.py +++ b/laboneq/compiler/code_generator/seq_c_generator.py @@ -3,10 +3,7 @@ from __future__ import annotations -import functools -import hashlib import logging -import re import textwrap from enum import Enum from typing import Any, Dict, List, Optional, Sequence, Set @@ -23,31 +20,6 @@ MIN_PLAY_ZERO_HOLD = 512 + 128 - -@functools.lru_cache() -def string_sanitize(input): - """Sanitize the input string, so it can be safely used as (part of) an identifier - in seqC.""" - - # strip non-ascii characters - s = input.encode("ascii", "ignore").decode() - - if s == "": - s = "_" - - # only allowed characters are alphanumeric and underscore - s = re.sub(r"[^\w\d]", "_", s) - - # names must not start with a digit - if s[0].isdigit(): - s = "_" + s - - if s != input: - s = f"{s}_{hashlib.md5(input.encode()).hexdigest()[:4]}" - - return s - - SeqCStatement = Dict[str, Any] @@ -108,6 +80,12 @@ def add_wave_declaration( } ) + def add_constant_definition(self, name: str, value, comment: str | None = None): + statement = {"type": "constant", "name": name, "value": value} + if comment is not None: + statement["comment"] = comment + self.add_statement(statement) + def estimate_complexity(self): """Calculate a rough estimate for the complexity (~nr of instructions) @@ -442,6 +420,15 @@ def emit_statement(self, statement: SeqCStatement): elif statement["type"] == "playHold": return f"playHold({statement['num_samples']});\n" + elif statement["type"] == "constant": + if statement["comment"] is not None: + comment = " // " + statement["comment"] + else: + comment = "" + return f"const {statement['name']} = {statement['value']};{comment}\n" + + raise ValueError(f"Invalid statement type '{statement['type']}'") + def _gen_wave_declaration_placeholder(self, statement: SeqCStatement) -> str: dual_channel = statement["signal_type"] in ["iq", "double", "multi"] sig_string = statement["wave_id"] diff --git a/laboneq/compiler/code_generator/signatures.py b/laboneq/compiler/code_generator/signatures.py index 29aa288..83a4f4a 100644 --- a/laboneq/compiler/code_generator/signatures.py +++ b/laboneq/compiler/code_generator/signatures.py @@ -11,8 +11,8 @@ import numpy as np from orjson import orjson -from laboneq.compiler.code_generator.seq_c_generator import string_sanitize from laboneq.compiler.code_generator.utils import normalize_phase +from laboneq.core.utilities.string_sanitize import string_sanitize @dataclass(unsafe_hash=True) diff --git a/laboneq/compiler/common/awg_signal_type.py b/laboneq/compiler/common/awg_signal_type.py index 4ef9814..634e58f 100644 --- a/laboneq/compiler/common/awg_signal_type.py +++ b/laboneq/compiler/common/awg_signal_type.py @@ -11,6 +11,7 @@ class AWGSignalType(Enum): DOUBLE = "double" # Two independent channels IQ = "iq" # Two channels form an I/Q signal MULTI = "multi" # Multiple logical channels mixed + # todo: "integration" missing here? def __repr__(self): cls_name = self.__class__.__name__ diff --git a/laboneq/compiler/common/device_type.py b/laboneq/compiler/common/device_type.py index 73e45f6..54129ee 100644 --- a/laboneq/compiler/common/device_type.py +++ b/laboneq/compiler/common/device_type.py @@ -5,6 +5,8 @@ from enum import Enum from typing import Optional +from laboneq.data.compilation_job import DeviceInfoType + @dataclass(eq=True, frozen=True) class DeviceTraits: @@ -41,6 +43,10 @@ def __init__(self, value: DeviceTraits): # gets properly initialized with the original DeviceTraits value members super().__init__(**asdict(value)) + @classmethod + def from_device_info_type(cls, value: DeviceInfoType): + return cls(value.name.lower()) + HDAWG = DeviceTraits( str_value="hdawg", sampling_rate=2.4e9, diff --git a/laboneq/compiler/experiment_access/dsl_loader.py b/laboneq/compiler/experiment_access/dsl_loader.py index 4503038..7021890 100644 --- a/laboneq/compiler/experiment_access/dsl_loader.py +++ b/laboneq/compiler/experiment_access/dsl_loader.py @@ -11,19 +11,29 @@ from types import SimpleNamespace from typing import Any, Callable, Dict, Tuple +from laboneq._utils import ensure_list, id_generator from laboneq.compiler.experiment_access.acquire_info import AcquireInfo from laboneq.compiler.experiment_access.loader_base import LoaderBase -from laboneq.compiler.experiment_access.marker import Marker from laboneq.compiler.experiment_access.param_ref import ParamRef -from laboneq.compiler.experiment_access.pulse_def import PulseDef -from laboneq.compiler.experiment_access.section_info import SectionInfo -from laboneq.compiler.experiment_access.section_signal_pulse import SectionSignalPulse from laboneq.core.exceptions import LabOneQException from laboneq.core.types.enums import ( AcquisitionType, AveragingMode, + ExecutionType, IODirection, IOSignalType, + SectionAlignment, +) +from laboneq.data.compilation_job import ( + AmplifierPumpInfo, + FollowerInfo, + Marker, + MixerCalibrationInfo, + PrecompensationInfo, + PulseDef, + SectionInfo, + SectionSignalPulse, + SignalRange, ) if typing.TYPE_CHECKING: @@ -60,7 +70,6 @@ def load(self, experiment: Experiment, device_setup: DeviceSetup): for server in device_setup.servers.values(): if hasattr(server, "leader_uid"): self.global_leader_device_id = server.leader_uid - self.add_server(server.uid, server.host, server.port, server.api_level) dest_path_devices = {} @@ -269,8 +278,8 @@ def opt_param( self.add_device_oscillator(device_id, oscillator_uid) else: if ( - known_oscillator["frequency"], - known_oscillator["is_hardware"], + known_oscillator.frequency, + known_oscillator.is_hardware, ) != (frequency, is_hardware): raise Exception( f"Duplicate oscillator uid {oscillator_uid} found in {ls.path}" @@ -279,51 +288,22 @@ def opt_param( ls_voltage_offsets[ls.path] = calibration.voltage_offset except AttributeError: pass - try: - ls_mixer_calibrations[ls.path] = { - "voltage_offsets": calibration.mixer_calibration.voltage_offsets, - "correction_matrix": calibration.mixer_calibration.correction_matrix, - } - except (AttributeError, KeyError): - pass - try: - precomp = calibration.precompensation - if precomp is None: - raise AttributeError - except AttributeError: - pass - else: - precomp_dict = {} - - if precomp.exponential: - precomp_exp = [ - {"timeconstant": e.timeconstant, "amplitude": e.amplitude} - for e in precomp.exponential - ] - precomp_dict["exponential"] = precomp_exp - if precomp.high_pass is not None: - # Since we currently only support clearing the integrator - # inside a delay, the different modes are not relevant. - # Instead, we would like to merge subsequent pulses into the - # same waveform, so we restrict the choice to "rise", regardless - # of what the user may have specified. - clearing = "rise" - - precomp_dict["high_pass"] = { - "timeconstant": precomp.high_pass.timeconstant, - "clearing": clearing, - } - if precomp.bounce is not None: - precomp_dict["bounce"] = { - "delay": precomp.bounce.delay, - "amplitude": precomp.bounce.amplitude, - } - if precomp.FIR is not None: - precomp_dict["FIR"] = { - "coefficients": copy.deepcopy(precomp.FIR.coefficients), - } - if precomp_dict: - ls_precompensations[ls.path] = precomp_dict + + mixer_cal = getattr(calibration, "mixer_calibration", None) + if mixer_cal is not None: + ls_mixer_calibrations[ls.path] = MixerCalibrationInfo( + voltage_offsets=calibration.mixer_calibration.voltage_offsets, + correction_matrix=calibration.mixer_calibration.correction_matrix, + ) + + precomp = getattr(calibration, "precompensation", None) + if precomp is not None: + ls_precompensations[ls.path] = PrecompensationInfo( + precomp.exponential, + precomp.high_pass, + precomp.bounce, + precomp.FIR, + ) local_oscillator = calibration.local_oscillator if local_oscillator is not None: @@ -363,18 +343,16 @@ def opt_param( ls.path, ) else: - ls_amplifier_pumps[ls.path] = ( - ppc_connections[ls.path][0], - { - "channel": ppc_connections[ls.path][1], - "pump_freq": opt_param(amp_pump.pump_freq), - "pump_power": opt_param(amp_pump.pump_power), - "cancellation": amp_pump.cancellation, - "alc_engaged": amp_pump.alc_engaged, - "use_probe": amp_pump.use_probe, - "probe_frequency": opt_param(amp_pump.probe_frequency), - "probe_power": opt_param(amp_pump.probe_power), - }, + ls_amplifier_pumps[ls.path] = AmplifierPumpInfo( + channel=ppc_connections[ls.path][1], + device=self._devices[ppc_connections[ls.path][0]], + pump_freq=opt_param(amp_pump.pump_freq), + pump_power=opt_param(amp_pump.pump_power), + cancellation=amp_pump.cancellation, + alc_engaged=amp_pump.alc_engaged, + use_probe=amp_pump.use_probe, + probe_frequency=opt_param(amp_pump.probe_frequency), + probe_power=opt_param(amp_pump.probe_power), ) for signal in sorted(experiment.signals.values(), key=lambda x: x.uid): @@ -397,11 +375,7 @@ def opt_param( else: _logger.debug("exp signal %s ls %s IS AN OUTPUT", signal, ls) - self.add_signal( - signal.uid, - signal_type, - modulation=signal.mapped_logical_signal_path in modulated_paths, - ) + self.add_signal(signal.uid, signal_type) if signal.mapped_logical_signal_path in modulated_paths: oscillator_id = modulated_paths[signal.mapped_logical_signal_path][ @@ -465,8 +439,11 @@ def opt_param( "mixer_calibration": ls_mixer_calibrations.get(lsuid), "precompensation": ls_precompensations.get(lsuid), "lo_frequency": ls_lo_frequencies.get(lsuid), - "range": ls_ranges.get(lsuid), - "range_unit": ls_range_units.get(lsuid), + "range": SignalRange( + ls_ranges.get(lsuid), ls_range_units.get(lsuid) + ) + if ls_ranges.get(lsuid) is not None + else None, "port_delay": ls_port_delays.get(lsuid), "delay_signal": ls_delays_signal.get(lsuid), "port_mode": ls_port_modes.get(lsuid), @@ -504,11 +481,13 @@ class _SyncingConnection: for sc in syncing_connections: if sc.signal_type == IOSignalType.DIO: - self.dios.append((sc.leader_device_uid, sc.follower_device_uid)) + leader = self._devices[sc.leader_device_uid] + follower = self._devices[sc.follower_device_uid] + leader.followers.append(FollowerInfo(follower, 0)) elif sc.signal_type == IOSignalType.ZSYNC: port = int(sc.output.physical_port_ids[0]) - self.pqsc_ports.append( - (sc.leader_device_uid, sc.follower_device_uid, port) + self._devices[sc.leader_device_uid].followers.append( + FollowerInfo(self._devices[sc.follower_device_uid], port) ) seq_avg_section, sweep_sections = find_sequential_averaging(experiment) @@ -548,18 +527,19 @@ def exchanger_map(section): ) if seq_avg_section is not None and len(sweep_sections): - avg_children = self._section_tree.get(sweep_sections[0].uid, []) - sweep_children = self._section_tree.get(seq_avg_section.uid, []) - - self._section_tree[seq_avg_section.uid] = avg_children - self._section_tree[sweep_sections[0].uid] = sweep_children + avg_info = self._sections[seq_avg_section.uid] + sweep_info = self._sections[sweep_sections[0].uid] + avg_info.children, sweep_info.children = ( + sweep_info.children, + avg_info.children, + ) - for _parent, children in self._section_tree.items(): - for i, c in enumerate(children): - if c == sweep_sections[0].uid: - children[i] = seq_avg_section.uid - elif c == seq_avg_section.uid: - children[i] = sweep_sections[0].uid + for parent in self._sections.values(): + for i, c in enumerate(parent.children): + if c is avg_info: + parent.children[i] = sweep_info + elif c is sweep_info: + parent.children[i] = avg_info self.acquisition_type = AcquisitionType( next( @@ -618,9 +598,7 @@ def _process_section( ) def _extract_markers(self, operation): - markers_raw = getattr(operation, "marker", None) - if markers_raw is None: - return None + markers_raw = getattr(operation, "marker", None) or {} return [ Marker( @@ -663,16 +641,14 @@ def _sweep_derived_param(self, param: Parameter): def _insert_section( self, section, - acquisition_type, + exp_acquisition_type, exchanger_map: Callable[[Any], Any], instance_id: str, ): - has_repeat = False - count = 1 + count = None if hasattr(section, "count"): - has_repeat = True - count = section.count + count = int(section.count) if hasattr(section, "parameters"): for parameter in section.parameters: @@ -680,7 +656,6 @@ def _insert_section( if parameter.values is not None: values_list = list(parameter.values) axis_name = getattr(parameter, "axis_name", None) - has_repeat = True self.add_section_parameter( instance_id, parameter.uid, @@ -706,53 +681,21 @@ def _insert_section( f" bound to a value that can only be set in near-time" ) - execution_type = None - if section.execution_type is not None: - execution_type = section.execution_type.value - - align = "left" - if exchanger_map(section).alignment is not None: - align = exchanger_map(section).alignment.value - - on_system_grid = None - if exchanger_map(section).on_system_grid is not None: - on_system_grid = exchanger_map(section).on_system_grid - - length = None - if section.length is not None: - length = section.length - - averaging_mode = None - if hasattr(section, "averaging_mode"): - averaging_mode = section.averaging_mode.value - - repetition_mode = None - if hasattr(section, "repetition_mode"): - repetition_mode = section.repetition_mode.value - - repetition_time = None - if hasattr(section, "repetition_time"): - repetition_time = section.repetition_time - - reset_oscillator_phase = False - if hasattr(section, "reset_oscillator_phase"): - reset_oscillator_phase = section.reset_oscillator_phase - - handle = None - if hasattr(section, "handle"): - handle = section.handle - - user_register = None - if hasattr(section, "user_register"): - user_register = section.user_register - - state = None - if hasattr(section, "state"): - state = section.state - - local = None - if hasattr(section, "local"): - local = section.local + execution_type = ( + ExecutionType(section.execution_type) if section.execution_type else None + ) + align = SectionAlignment(exchanger_map(section).alignment) + on_system_grid = exchanger_map(section).on_system_grid + length = section.length + + averaging_mode = getattr(section, "averaging_mode", None) + repetition_mode = getattr(section, "repetition_mode", None) + repetition_time = getattr(section, "repetition_time", None) + reset_oscillator_phase = getattr(section, "reset_oscillator_phase", False) + handle = getattr(section, "handle", None) + user_register = getattr(section, "user_register", None) + state = getattr(section, "state", None) + local = getattr(section, "local", None) trigger = [ {"signal_id": k, "state": v["state"]} for k, v in section.trigger.items() @@ -760,11 +703,11 @@ def _insert_section( chunk_count = getattr(section, "chunk_count", 1) - acquisition_types = None + acquisition_type = None for operation in exchanger_map(section).operations: if hasattr(operation, "handle"): # an acquire event - add acquisition_types - acquisition_types = [acquisition_type.value] + acquisition_type = exp_acquisition_type play_after = getattr(section, "play_after", None) if play_after: @@ -776,14 +719,12 @@ def _insert_section( self.add_section( instance_id, SectionInfo( - section_id=instance_id, - section_display_name=section.uid, - has_repeat=has_repeat, + uid=instance_id, execution_type=execution_type, count=count, chunk_count=chunk_count, - acquisition_types=acquisition_types, - align=align, + acquisition_type=acquisition_type, + alignment=align, on_system_grid=on_system_grid, length=length, averaging_mode=averaging_mode, @@ -791,7 +732,7 @@ def _insert_section( repetition_time=repetition_time, play_after=play_after, reset_oscillator_phase=reset_oscillator_phase, - trigger_output=trigger, + triggers=trigger, handle=handle, user_register=user_register, state=state, @@ -810,7 +751,9 @@ def _insert_section( continue self.add_section_signal(instance_id, operation.signal) - self._section_operations_to_add.append((section, acquisition_type, instance_id)) + self._section_operations_to_add.append( + (section, exp_acquisition_type, instance_id) + ) def _insert_section_operations( self, @@ -824,7 +767,6 @@ def _insert_section_operations( for operation in exchanger_map(section).operations: if hasattr(operation, "signal"): pulse_offset = None - pulse_offset_param = None if hasattr(operation, "time"): # Delay operation pulse_offset = operation.time @@ -834,247 +776,255 @@ def _insert_section_operations( if not isinstance(operation.time, float) and not isinstance( operation.time, int ): - pulse_offset = None - pulse_offset_param = operation.time.uid - - if pulse_offset_param is not None: + pulse_offset = self._get_or_create_parameter(operation.time.uid) self._sweep_derived_param(operation.time) ssp = SectionSignalPulse( - signal_id=operation.signal, + signal=self._signals[operation.signal], offset=pulse_offset, - offset_param=pulse_offset_param, precompensation_clear=precompensation_clear, ) self.add_section_signal_pulse(instance_id, operation.signal, ssp) else: # All operations, except Delay - pulse = None - operation_length_param = None + pulses = None markers = self._extract_markers(operation) length = getattr(operation, "length", None) operation_length = length - if ( - operation_length is not None - and not isinstance(operation_length, float) - and not isinstance(operation_length, complex) - and not isinstance(operation_length, int) + if operation_length is not None and not isinstance( + operation_length, (float, complex, int) ): - operation_length_param = operation_length.uid self._sweep_derived_param(operation_length) - operation_length = None + operation_length = self._get_or_create_parameter( + operation_length.uid + ) if hasattr(operation, "pulse"): - pulse = getattr(operation, "pulse") + pulses = getattr(operation, "pulse") + if isinstance(pulses, list) and len(pulses) > 1: + raise RuntimeError( + f"Only one pulse can be provided for pulse play command in section {instance_id}." + ) if hasattr(operation, "kernel"): - pulse = getattr(operation, "kernel") - if pulse is None and length is not None: - pulse = SimpleNamespace() - setattr(pulse, "uid", next(_auto_pulse_id)) - setattr(pulse, "length", length) - if pulse is None and markers: + # We allow multiple kernels for multistate + pulses = getattr(operation, "kernel") + if not hasattr(operation, "handle"): + raise RuntimeError( + f"Kernels {pulses} in section {instance_id} do not have a handle." + ) + if pulses is None and length is not None: + pulses = SimpleNamespace() + setattr(pulses, "uid", next(_auto_pulse_id)) + setattr(pulses, "length", length) + if pulses is None and markers: # generate an zero amplitude pulse to play the markers - pulse = SimpleNamespace() - pulse.uid = next(_auto_pulse_id) - pulse.function = "const" - pulse.amplitude = 0.0 - pulse.length = max([m.start + m.length for m in markers]) - pulse.can_compress = False - pulse.pulse_parameters = None + pulses = SimpleNamespace() + pulses.uid = next(_auto_pulse_id) + pulses.function = "const" + pulses.amplitude = 0.0 + pulses.length = max([m.start + m.length for m in markers]) + pulses.can_compress = False + pulses.pulse_parameters = None + pulses = ensure_list(pulses) + pulse_group = ( + None if len(pulses) == 1 else id_generator("pulse_group") + ) if markers: for m in markers: if m.pulse_id is None: - m.pulse_id = pulse.uid + assert len(pulses) == 1 and pulses[0] is not None + m.pulse_id = pulses[0].uid - if hasattr(operation, "handle") and pulse is None: + if hasattr(operation, "handle") and pulses is None: raise RuntimeError( f"Either 'kernel' or 'length' must be provided for the" f" acquire operation with handle '{getattr(operation, 'handle')}'." ) - if pulse is not None: - function = None - length = None - - pulse_parameters = getattr(pulse, "pulse_parameters", None) - - if pulse.uid not in self._pulses: - samples = None - if hasattr(pulse, "function"): - function = pulse.function - if hasattr(pulse, "length"): - length = pulse.length + signals = ensure_list(operation.signal) + if len(signals) != len(pulses): + raise RuntimeError( + f"Number of pulses ({len(pulses)}) must be equal to number of signals ({len(signals)}) for section {instance_id}." + ) - if hasattr(pulse, "samples"): - samples = pulse.samples + # Play/acquire/measure command parameters + ( + pulse_amplitude, + pulse_amplitude_param, + ) = find_value_or_parameter_attr( + operation, "amplitude", (int, float, complex) + ) + if pulse_amplitude_param is not None: + self._sweep_derived_param(operation.amplitude) + pulse_amplitude = self._get_or_create_parameter( + pulse_amplitude_param + ) - amplitude, amplitude_param = find_value_or_parameter_attr( - pulse, "amplitude", (float, int, complex) - ) - if amplitude_param is not None: - self._sweep_derived_param(pulse.amplitude) - - can_compress = False - if hasattr(pulse, "can_compress"): - can_compress = pulse.can_compress - - self.add_pulse( - pulse.uid, - PulseDef( - id=pulse.uid, - function=function, - length=length, - amplitude=amplitude, - amplitude_param=amplitude_param, - play_mode=None, - can_compress=can_compress, - samples=samples, - ), + pulse_phase, pulse_phase_param = find_value_or_parameter_attr( + operation, "phase", (int, float) + ) + if pulse_phase_param is not None: + self._sweep_derived_param(operation.phase) + pulse_phase = self._get_or_create_parameter(pulse_phase_param) + + ( + pulse_increment_oscillator_phase, + pulse_increment_oscillator_phase_param, + ) = find_value_or_parameter_attr( + operation, "increment_oscillator_phase", (int, float) + ) + if pulse_increment_oscillator_phase_param is not None: + self._sweep_derived_param(operation.increment_oscillator_phase) + pulse_increment_oscillator_phase = ( + self._get_or_create_parameter( + pulse_increment_oscillator_phase_param ) - ( - pulse_amplitude, - pulse_amplitude_param, - ) = find_value_or_parameter_attr( - operation, "amplitude", (int, float, complex) - ) - if pulse_amplitude_param is not None: - self._sweep_derived_param(operation.amplitude) - pulse_phase, pulse_phase_param = find_value_or_parameter_attr( - operation, "phase", (int, float) - ) - if pulse_phase_param is not None: - self._sweep_derived_param(operation.phase) - ( - pulse_increment_oscillator_phase, - pulse_increment_oscillator_phase_param, - ) = find_value_or_parameter_attr( - operation, "increment_oscillator_phase", (int, float) ) - if pulse_increment_oscillator_phase_param is not None: - self._sweep_derived_param( - operation.increment_oscillator_phase - ) - ( - pulse_set_oscillator_phase, - pulse_set_oscillator_phase_param, - ) = find_value_or_parameter_attr( - operation, "set_oscillator_phase", (int, float) + ( + pulse_set_oscillator_phase, + pulse_set_oscillator_phase_param, + ) = find_value_or_parameter_attr( + operation, "set_oscillator_phase", (int, float) + ) + if pulse_set_oscillator_phase_param is not None: + self._sweep_derived_param(operation.set_oscillator_phase) + pulse_set_oscillator_phase = self._get_or_create_parameter( + pulse_set_oscillator_phase_param ) - if pulse_set_oscillator_phase_param is not None: - self._sweep_derived_param(operation.set_oscillator_phase) - - acquire_params = None - if hasattr(operation, "handle"): - acquire_params = AcquireInfo( - handle=operation.handle, - acquisition_type=acquisition_type.value, - ) - operation_pulse_parameters = copy.deepcopy( - getattr(operation, "pulse_parameters", None) + acquire_params = None + if hasattr(operation, "handle"): + acquire_params = AcquireInfo( + handle=operation.handle, + acquisition_type=acquisition_type.value, ) - # Replace sweep params with a ParamRef - if pulse_parameters is not None: - for param, val in pulse_parameters.items(): - if hasattr(val, "uid"): - # Take the presence of "uid" as a proxy for isinstance(val, SweepParameter) - pulse_parameters[param] = ParamRef(val.uid) - self._sweep_derived_param(val) - if operation_pulse_parameters is not None: - for param, val in operation_pulse_parameters.items(): + operation_pulse_parameters = copy.deepcopy( + getattr(operation, "pulse_parameters", None) + ) + + if operation_pulse_parameters is not None: + operation_pulse_parameters_list = ensure_list( + operation_pulse_parameters + ) + for p in operation_pulse_parameters_list: + for param, val in p.items(): if hasattr(val, "uid"): # Take the presence of "uid" as a proxy for isinstance(val, SweepParameter) - operation_pulse_parameters[param] = ParamRef( - val.uid - ) + p[param] = ParamRef(val.uid) self._sweep_derived_param(val) + else: + operation_pulse_parameters_list = [None] * len(pulses) - if markers: - for m in markers: - self.add_signal_marker( - operation.signal, m.marker_selector - ) + if markers: + for m in markers: + self.add_signal_marker(operation.signal, m.marker_selector) - ssp = SectionSignalPulse( - signal_id=operation.signal, - pulse_id=pulse.uid, - offset=pulse_offset, - offset_param=pulse_offset_param, - amplitude=pulse_amplitude, - amplitude_param=pulse_amplitude_param, - length=operation_length, - length_param=operation_length_param, - acquire_params=acquire_params, - phase=pulse_phase, - phase_param=pulse_phase_param, - increment_oscillator_phase=pulse_increment_oscillator_phase, - increment_oscillator_phase_param=pulse_increment_oscillator_phase_param, - set_oscillator_phase=pulse_set_oscillator_phase, - set_oscillator_phase_param=pulse_set_oscillator_phase_param, - play_pulse_parameters=operation_pulse_parameters, - pulse_pulse_parameters=pulse_parameters, - precompensation_clear=False, # not supported - markers=markers, - ) - self.add_section_signal_pulse( - instance_id, operation.signal, ssp - ) - elif ( - getattr(operation, "increment_oscillator_phase", None) - is not None - or getattr(operation, "set_oscillator_phase", None) is not None - or getattr(operation, "phase", None) is not None + for pulse, signal, op_pars in zip( + pulses, signals, operation_pulse_parameters_list ): - if getattr(operation, "phase", None) is not None: - raise LabOneQException( - "Phase argument has no effect for virtual Z gates." - ) - # virtual Z gate - ( - pulse_increment_oscillator_phase, - pulse_increment_oscillator_phase_param, - ) = find_value_or_parameter_attr( - operation, "increment_oscillator_phase", (int, float) - ) - if pulse_increment_oscillator_phase_param is not None: - self._sweep_derived_param( - operation.increment_oscillator_phase + if pulse is not None: + function = None + length = None + + pulse_parameters = getattr(pulse, "pulse_parameters", None) + + if pulse.uid not in self._pulses: + samples = None + if hasattr(pulse, "function"): + function = pulse.function + if hasattr(pulse, "length"): + length = pulse.length + if hasattr(pulse, "samples"): + samples = pulse.samples + + ( + amplitude, + amplitude_param, + ) = find_value_or_parameter_attr( + pulse, "amplitude", (float, int, complex) + ) + if amplitude_param is not None: + raise LabOneQException( + f"Amplitude of pulse '{pulse.uid}' cannot be a parameter." + f" To sweep the amplitude, pass the parameter in the" + f" corresponding `play()` command." + ) + + can_compress = False + if hasattr(pulse, "can_compress"): + can_compress = pulse.can_compress + + self.add_pulse( + pulse.uid, + PulseDef( + uid=pulse.uid, + function=function, + length=length, + amplitude=amplitude, + can_compress=can_compress, + samples=samples, + ), + ) + # Replace sweep params with a ParamRef + if pulse_parameters is not None: + for param, val in pulse_parameters.items(): + if hasattr(val, "uid"): + # Take the presence of "uid" as a proxy for isinstance(val, SweepParameter) + pulse_parameters[param] = ParamRef(val.uid) + self._sweep_derived_param(val) + + ssp = SectionSignalPulse( + signal=self._signals[signal], + pulse=self._pulses[pulse.uid], + offset=pulse_offset, + amplitude=pulse_amplitude, + length=operation_length, + acquire_params=acquire_params, + phase=pulse_phase, + increment_oscillator_phase=pulse_increment_oscillator_phase, + set_oscillator_phase=pulse_set_oscillator_phase, + play_pulse_parameters=op_pars, + pulse_pulse_parameters=pulse_parameters, + precompensation_clear=False, # only for delay + markers=markers, + pulse_group=pulse_group, ) - ( - pulse_set_oscillator_phase, - pulse_set_oscillator_phase_param, - ) = find_value_or_parameter_attr( - operation, "set_oscillator_phase", (int, float) - ) - if pulse_set_oscillator_phase_param is not None: - self._sweep_derived_param(operation.set_oscillator_phase) - for par in [ - "precompensation_clear", - "amplitude", - "phase", - "pulse_parameters", - "handle", - "length", - ]: - if getattr(operation, par, None) is not None: + self.add_section_signal_pulse(instance_id, signal, ssp) + elif ( + getattr(operation, "increment_oscillator_phase", None) + is not None + or getattr(operation, "set_oscillator_phase", None) + is not None + or getattr(operation, "phase", None) is not None + ): + # virtual Z gate + if getattr(operation, "phase", None) is not None: raise LabOneQException( - f"parameter {par} not supported for virtual Z gates" + "Phase argument has no effect for virtual Z gates." ) + for par in [ + "precompensation_clear", + "amplitude", + "phase", + "pulse_parameters", + "handle", + "length", + ]: + if getattr(operation, par, None) is not None: + raise LabOneQException( + f"parameter {par} not supported for virtual Z gates" + ) - ssp = SectionSignalPulse( - signal_id=operation.signal, - offset=pulse_offset, - offset_param=pulse_offset_param, - increment_oscillator_phase=pulse_increment_oscillator_phase, - increment_oscillator_phase_param=pulse_increment_oscillator_phase_param, - set_oscillator_phase=pulse_set_oscillator_phase, - set_oscillator_phase_param=pulse_set_oscillator_phase_param, - precompensation_clear=False, # not supported - ) - self.add_section_signal_pulse( - instance_id, operation.signal, ssp - ) + ssp = SectionSignalPulse( + signal=self._signals[operation.signal], + increment_oscillator_phase=pulse_increment_oscillator_phase, + set_oscillator_phase=pulse_set_oscillator_phase, + precompensation_clear=False, # only for delay + ) + self.add_section_signal_pulse( + instance_id, operation.signal, ssp + ) def find_sequential_averaging(section) -> Tuple[Any, Tuple]: diff --git a/laboneq/compiler/experiment_access/experiment_dao.py b/laboneq/compiler/experiment_access/experiment_dao.py index 6a6c50f..a051cff 100644 --- a/laboneq/compiler/experiment_access/experiment_dao.py +++ b/laboneq/compiler/experiment_access/experiment_dao.py @@ -3,7 +3,6 @@ from __future__ import annotations -import copy import logging from collections import deque from typing import Any, List, Optional @@ -13,24 +12,45 @@ from laboneq._utils import cached_method from laboneq.compiler.experiment_access import json_dumper from laboneq.compiler.experiment_access.dsl_loader import DSLLoader +from laboneq.compiler.experiment_access.experiment_info_loader import ( + ExperimentInfoLoader, +) from laboneq.compiler.experiment_access.json_loader import JsonLoader -from laboneq.compiler.experiment_access.section_info import SectionInfo -from laboneq.compiler.experiment_access.signal_info import SignalInfo from laboneq.core.exceptions import LabOneQException -from laboneq.core.types.enums import AcquisitionType +from laboneq.core.types.enums import AcquisitionType, ExecutionType from laboneq.core.validators import dicts_equal -from laboneq.data.compilation_job import DeviceInfo, OscillatorInfo, ParameterInfo +from laboneq.data.compilation_job import ( + AmplifierPumpInfo, + DeviceInfo, + DeviceInfoType, + ExperimentInfo, + OscillatorInfo, + ParameterInfo, + PulseDef, + SectionInfo, + SectionSignalPulse, + SignalInfo, + SignalInfoType, + SignalRange, +) _logger = logging.getLogger(__name__) class ExperimentDAO: - def __init__(self, experiment, core_device_setup=None, core_experiment=None): + def __init__( + self, + experiment, + core_device_setup=None, + core_experiment=None, + ): self._data: dict[str, Any] = {} self._acquisition_type: AcquisitionType = None # type: ignore if core_device_setup is not None and core_experiment is not None: self._loader = self._load_from_core(core_device_setup, core_experiment) + elif isinstance(experiment, ExperimentInfo): + self._loader = self._load_experiment_info(experiment) else: self._loader = self._load_experiment(experiment) self._data = self._loader.data() @@ -46,34 +66,20 @@ def __eq__(self, other): self._data, other._data ) - def add_signal( - self, device_id, channels, connection_type, signal_id, signal_type, modulation - ): - self._data["signals"][signal_id] = { - "signal_id": signal_id, - "signal_type": signal_type, - "modulation": modulation, - "offset": None, - } - - self._data["signal_connections"][signal_id] = { - "signal_id": signal_id, - "device_id": device_id, - "connection_type": connection_type, - "channels": channels, - "voltage_offset": None, - "mixer_calibration": None, - "precompensation": None, - "lo_frequency": None, - "range": None, - "range_unit": None, - "port_delay": None, - "delay_signal": None, - "port_mode": None, - "threshold": None, - "amplitude": None, - "amplifier_pump": None, - } + def add_signal(self, device_id, channels, signal_id, signal_type): + assert signal_id not in self._data["signals"] + + self._data["signals"][signal_id] = SignalInfo( + uid=signal_id, + type=SignalInfoType(signal_type), + device=self.device_info(device_id), + channels=channels, + ) + + def _load_experiment_info(self, experiment: ExperimentInfo) -> ExperimentInfoLoader: + loader = ExperimentInfoLoader() + loader.load(experiment) + return loader def _load_experiment(self, experiment) -> JsonLoader: loader = JsonLoader() @@ -100,111 +106,36 @@ def dump(experiment_dao: "ExperimentDAO"): def acquisition_type(self) -> AcquisitionType: return self._acquisition_type - def server_infos(self): - return copy.deepcopy(list(self._data["servers"].values())) - def signals(self) -> list[str]: - return sorted([s["signal_id"] for s in self._data["signals"].values()]) + return sorted([s.uid for s in self._data["signals"].values()]) def devices(self) -> List[str]: - return [d["uid"] for d in self._data["devices"].values()] + return [d.uid for d in self._data["devices"].values()] def global_leader_device(self) -> str: return self._data["global_leader_device_id"] - @classmethod - def _device_info_keys(cls): - return [ - "uid", - "device_type", - "reference_clock", - "reference_clock_source", - "is_qc", - ] - def device_info(self, device_id) -> Optional[DeviceInfo]: - device_info = self._data["devices"].get(device_id) - if device_info is not None: - return DeviceInfo(**{k: device_info[k] for k in self._device_info_keys()}) - return None + return self._data["devices"].get(device_id) def device_infos(self) -> List[DeviceInfo]: - return [ - DeviceInfo(**{k: device_info[k] for k in self._device_info_keys()}) - for device_info in self._data["devices"].values() - ] + return list(self._data["devices"].values()) def device_reference_clock(self, device_id): - return self._data["devices"][device_id].get("reference_clock") + return self._data["devices"][device_id].reference_clock def device_from_signal(self, signal_id): - signal_connection = self._data["signal_connections"][signal_id] - return signal_connection["device_id"] - - def devices_in_section_no_descend(self, section_id): - signals = self._data["section_signals"].get(section_id, []) - devices = {self.device_from_signal(s) for s in signals} - return devices - - def _device_types_in_section_no_descend(self, section_id): - devices = self.devices_in_section_no_descend(section_id) - return { - d["device_type"] - for d in self._data["devices"].values() - if d["uid"] in devices - } - - def device_types_in_section(self, section_id): - if self.is_branch(section_id): - return self.device_types_in_section(self.section_parent(section_id)) - retval = set() - section_with_children = self.all_section_children(section_id) - section_with_children.add(section_id) - for child in section_with_children: - retval = retval.union(self._device_types_in_section_no_descend(child)) - return retval - - @classmethod - def _signal_info_keys(cls): - return [ - "signal_id", - "signal_type", - "device_id", - "device_type", - "connection_type", - "channels", - "delay_signal", - "modulation", - "offset", - ] + return self.device_info(self.signal_info(signal_id).device.uid) @cached_method() - def signal_info(self, signal_id) -> SignalInfo: - signal_info = self._data["signals"].get(signal_id) - if signal_info is not None: - signal_info_copy = copy.deepcopy(signal_info) - signal_connection = self._data["signal_connections"][signal_id] - - for k in ["device_id", "connection_type", "channels", "delay_signal"]: - signal_info_copy[k] = signal_connection[k] - - device_info = self._data["devices"][signal_connection["device_id"]] - - signal_info_copy["device_type"] = device_info["device_type"] - return SignalInfo( - **{k: signal_info_copy[k] for k in self._signal_info_keys()} - ) - else: - raise Exception(f"Signal_id {signal_id} not found") + def signal_info(self, signal_id) -> SignalInfo | None: + return self._data["signals"].get(signal_id) def sections(self) -> List[str]: return list(self._data["sections"].keys()) def section_info(self, section_id) -> SectionInfo: retval = self._data["sections"][section_id] - - if retval.count is not None: - retval.count = int(retval.count) return retval def root_sections(self): @@ -230,8 +161,8 @@ def root_rt_sections(self): candidate = queue.popleft() info = self.section_info(candidate) nt_subsection = self._has_near_time_child(candidate) - if info.execution_type != "controller": - if info.execution_type == "hardware" and nt_subsection is not None: + if info.execution_type in (None, ExecutionType.REAL_TIME): + if nt_subsection is not None: raise LabOneQException( f"Real-time section {candidate} has near-time sub-section " f"{nt_subsection}." @@ -243,7 +174,7 @@ def root_rt_sections(self): @cached_method() def direct_section_children(self, section_id) -> List[str]: - return self._data["section_tree"].get(section_id, []) + return [child.uid for child in self.section_info(section_id).children] @cached_method() def all_section_children(self, section_id): @@ -257,43 +188,51 @@ def all_section_children(self, section_id): return set(retval) @cached_method() - def section_parent(self, section_id): - try: - return next( - parent - for parent, children in self._data["section_tree"].items() - if section_id in children - ) - except StopIteration: - return None - - def is_branch(self, section_id): - return self._data["sections"][section_id].state is not None + def section_parent(self, section_id) -> str | None: + for parent_id in self.sections(): + parent = self.section_info(parent_id) + if any(child.uid == section_id for child in parent.children): + return parent.uid + return None def pqscs(self) -> list[str]: - return list({p[0] for p in self._loader.pqsc_ports}) - - def pqsc_ports(self, pqsc_device_uid: str): return [ - {"device": p[1], "port": p[2]} - for p in self._loader.pqsc_ports - if p[0] == pqsc_device_uid + d.uid for d in self.device_infos() if d.device_type == DeviceInfoType.PQSC ] + def pqsc_ports(self, pqsc_device_uid: str): + assert pqsc_device_uid in self.pqscs() + leader = self.device_info(pqsc_device_uid) + return [{"device": p.device.uid, "port": p.port} for p in leader.followers] + def dio_followers(self) -> list[str]: - return [d[1] for d in self._loader.dios] + return [ + follower.device.uid + for leader in self.device_infos() + for follower in leader.followers + if leader.device_type != DeviceInfoType.PQSC + ] def dio_leader(self, device_id) -> str | None: - try: - return next(d[0] for d in self._loader.dios if d[1] == device_id) - except StopIteration: - return None + for d in self.device_infos(): + if d.device_type == DeviceInfoType.PQSC: + continue + for f in d.followers: + if f.device.uid == device_id: + return d.uid + + return None def dio_connections(self) -> list[tuple[str, str]]: - return self._loader.dios + return [ + (leader.uid, follower.device.uid) + for leader in self.device_infos() + for follower in leader.followers + if leader.device_type != DeviceInfoType.PQSC + ] def section_signals(self, section_id): - return self._data["section_signals"].get(section_id, set()) + return {s.uid for s in self.section_info(section_id).signals} @cached_method() def section_signals_with_children(self, section_id): @@ -301,27 +240,17 @@ def section_signals_with_children(self, section_id): section_with_children = self.all_section_children(section_id) section_with_children.add(section_id) for child in section_with_children: - retval = retval.union(self.section_signals(child)) + retval |= self.section_signals(child) return retval - def pulses(self): + def pulses(self) -> list[str]: return list(self._data["pulses"].keys()) - def pulse(self, pulse_id): - pulse = self._data["pulses"].get(pulse_id) - return pulse - - @classmethod - def _oscillator_info_fields(cls): - return ["uid", "frequency", "is_hardware"] + def pulse(self, pulse_id) -> PulseDef: + return self._data["pulses"].get(pulse_id) def oscillator_info(self, oscillator_id) -> OscillatorInfo | None: - oscillator = self._data["oscillators"].get(oscillator_id) - if oscillator is None: - return None - return OscillatorInfo( - **{k: oscillator[k] for k in self._oscillator_info_fields()} - ) + return self._data["oscillators"].get(oscillator_id) def hardware_oscillators(self) -> List[OscillatorInfo]: oscillator_infos: List[OscillatorInfo] = [] @@ -335,7 +264,7 @@ def hardware_oscillators(self) -> List[OscillatorInfo]: return sorted(oscillator_infos, key=lambda x: x.uid) - def device_oscillators(self, device_id): + def device_oscillators(self, device_id) -> list[OscillatorInfo]: return [ do["oscillator_id"] for do in self._data["device_oscillators"].get(device_id, []) @@ -345,63 +274,42 @@ def oscillators(self): return list(self._data["oscillators"].keys()) def signal_oscillator(self, signal_id): - oscillator = self._data["signal_oscillator"].get(signal_id) - if oscillator is None: - return None - return self.oscillator_info(oscillator) + return self._data["signals"][signal_id].oscillator def voltage_offset(self, signal_id): - return self._data["signal_connections"][signal_id]["voltage_offset"] + return self._data["signals"][signal_id].voltage_offset def mixer_calibration(self, signal_id): - return self._data["signal_connections"][signal_id]["mixer_calibration"] + return self._data["signals"][signal_id].mixer_calibration def precompensation(self, signal_id): - return self._data["signal_connections"][signal_id]["precompensation"] + return self._data["signals"][signal_id].precompensation def lo_frequency(self, signal_id): - return self._data["signal_connections"][signal_id]["lo_frequency"] + return self._data["signals"][signal_id].lo_frequency - def signal_range(self, signal_id): - sc = self._data["signal_connections"][signal_id] - return sc["range"], sc["range_unit"] + def signal_range(self, signal_id) -> SignalRange: + return self._data["signals"][signal_id].signal_range def port_delay(self, signal_id) -> float | str | None: - return self._data["signal_connections"][signal_id]["port_delay"] + return self._data["signals"][signal_id].port_delay def port_mode(self, signal_id): - return self._data["signal_connections"][signal_id]["port_mode"] + return self._data["signals"][signal_id].port_mode def threshold(self, signal_id): - return self._data["signal_connections"][signal_id]["threshold"] + return self._data["signals"][signal_id].threshold def amplitude(self, signal_id) -> float | str | None: - return self._data["signal_connections"][signal_id]["amplitude"] - - def amplifier_pump(self, signal_id) -> tuple[str, dict[str, Any]] | None: - return self._data["signal_connections"][signal_id]["amplifier_pump"] - - def section_pulses(self, section_id, signal_id): - retval = self._section_pulses_raw(section_id, signal_id) - for sp in retval: - pulse_id = sp.pulse_id - if pulse_id is not None: - pulse_def = self._data["pulses"].get(pulse_id) - if pulse_def is not None: - if sp.length is None and sp.length_param is None: - sp.length = pulse_def.length - # TODO(2K): pulse_def has no length_param! - # if pulse_def.length_param is not None: - # sp["length_param"] = pulse_def.length_param - # sp["length"] = None + return self._data["signals"][signal_id].amplitude - return retval + def amplifier_pump(self, signal_id) -> AmplifierPumpInfo | None: + return self._data["signals"][signal_id].amplifier_pump - def _section_pulses_raw(self, section_id, signal_id): - section_signal_pulses = ( + def section_pulses(self, section_id, signal_id) -> list[SectionSignalPulse]: + return ( self._data["section_signal_pulses"].get(section_id, {}).get(signal_id, []) ) - return [copy.copy(ssp) for ssp in section_signal_pulses] def markers_on_signal(self, signal_id: str): return self._data["signal_markers"].get(signal_id) @@ -421,31 +329,29 @@ def validate_experiment(self): for section_id in self.sections(): for signal_id in self.section_signals(section_id): for section_pulse in self.section_pulses(section_id, signal_id): - pulse_id = section_pulse.pulse_id - if pulse_id is not None and pulse_id not in self._data["pulses"]: - raise RuntimeError( - f"Pulse {pulse_id} referenced in section {section_id} by a pulse on signal {signal_id} is not known." - ) - - for k in ["length_param", "amplitude_param", "offset_param"]: - param_name = getattr(section_pulse, k) - if param_name is not None: - if param_name not in all_parameters: - raise RuntimeError( - f"Parameter {param_name} referenced in section {section_id} by a pulse on signal {signal_id} is not known." - ) + if section_pulse.pulse is None: + continue + pulse_id = section_pulse.pulse.uid if ( - self._data["signal_connections"][section_pulse.signal_id][ - "device_id" - ] - in ["device_shfqa", "device_uhfqa"] - ) and not ( - section_pulse.markers is None or len(section_pulse.markers) == 0 - ): + section_pulse.signal.device.device_type + in (DeviceInfoType.SHFQA, DeviceInfoType.UHFQA) + ) and not len(section_pulse.markers) == 0: raise RuntimeError( - f"Pulse {pulse_id} referenced in section {section_id} has markers but is to be played on a QA device. QA devices do not support markers." + f"Pulse {pulse_id} referenced in section {section_id}" + f" has markers but is to be played on a QA device. QA" + f" devices do not support markers." ) + for handle, acquisition_signals in self._data["handle_acquires"].items(): + if not acquisition_signals: + continue + dev0 = self.device_from_signal(acquisition_signals[0]) + for sig in acquisition_signals[1:]: + if self.device_from_signal(sig) != dev0: + raise LabOneQException( + f"Acquisition signals {acquisition_signals} for " + f"handle '{handle}' are not on the same device" + ) - def acquisition_signal(self, handle: str) -> Optional[str]: + def acquisition_signal(self, handle: str) -> Optional[list[str]]: return self._data["handle_acquires"][handle] diff --git a/laboneq/compiler/experiment_access/experiment_info_loader.py b/laboneq/compiler/experiment_access/experiment_info_loader.py new file mode 100644 index 0000000..4dafbeb --- /dev/null +++ b/laboneq/compiler/experiment_access/experiment_info_loader.py @@ -0,0 +1,61 @@ +# Copyright 2022 Zurich Instruments AG +# SPDX-License-Identifier: Apache-2.0 + + +from laboneq.compiler.experiment_access.loader_base import LoaderBase +from laboneq.core.exceptions import LabOneQException +from laboneq.data.compilation_job import ExperimentInfo, SectionInfo + + +class ExperimentInfoLoader(LoaderBase): + def load(self, job: ExperimentInfo): + self.global_leader_device_id = job.global_leader_device + + for s in job.signals: + self._signals[s.uid] = s + if s.device.uid not in self._devices: + self._devices[s.device.uid] = s.device + + if s.oscillator is not None: + o = s.oscillator + if o.uid in self._oscillators and self._oscillators[o.uid] != o: + raise LabOneQException( + f"Detected duplicate oscillator UID '{o.uid}'" + ) + self._oscillators[s.oscillator.uid] = s.oscillator + if s.oscillator.is_hardware: + self.add_device_oscillator(s.device.uid, s.oscillator.uid) + + for pulse in job.pulse_defs: + self._pulses[pulse.uid] = pulse + + for section in job.sections: + self._root_sections.append(section.uid) + self.walk_sections(section) + + def walk_sections(self, section: SectionInfo): + self.add_section(section.uid, section) + + if section.acquisition_type is not None: + self.acquisition_type = section.acquisition_type + + if section.pulses: + self._section_signal_pulses[section.uid] = { + ssp.signal.uid: ssp for ssp in section.pulses + } + + for ssp in section.pulses: + for marker in ssp.markers: + self.add_signal_marker(ssp.signal.uid, marker.marker_selector) + + if section.parameters: + self._section_parameters[section.uid] = section.parameters[:] + + for t in section.triggers: + if "signal_id" in t: + signal_id = t["signal_id"] + v = self._signal_trigger.get(signal_id, 0) + self._signal_trigger[signal_id] = v | t["state"] + + for child in section.children: + self.walk_sections(child) diff --git a/laboneq/compiler/experiment_access/json_dumper.py b/laboneq/compiler/experiment_access/json_dumper.py index 3836c8f..fae95d5 100644 --- a/laboneq/compiler/experiment_access/json_dumper.py +++ b/laboneq/compiler/experiment_access/json_dumper.py @@ -3,6 +3,8 @@ from __future__ import annotations +import copy +import dataclasses import typing from laboneq.data.compilation_job import ParameterInfo @@ -20,15 +22,6 @@ def dump(experiment_dao: ExperimentDAO): "epsilon": {"time": 1e-12}, "line_endings": "unix", }, - "servers": [ - { - "id": server_info["id"], - "host": server_info["host"], - "port": int(server_info["port"]), - "api_level": server_info["api_level"], - } - for server_info in experiment_dao.server_infos() - ], } device_entries = {} @@ -41,7 +34,7 @@ def dump(experiment_dao: ExperimentDAO): if getattr(device_info, key) is not None: device_entry[key] = getattr(device_info, key) device_entry["id"] = device_info.uid - device_entry["driver"] = device_info.device_type.lower() + device_entry["driver"] = device_info.device_type.name.lower() oscillator_ids = experiment_dao.device_oscillators(device) @@ -114,82 +107,65 @@ def dump(experiment_dao: ExperimentDAO): signal_connections = [] for signal_info in signal_infos: signal_entry = { - "id": signal_info.signal_id, - "signal_type": signal_info.signal_type, + "id": signal_info.uid, + "signal_type": signal_info.type.value, } - if signal_info.modulation: - signal_entry["modulation"] = signal_info.modulation - if signal_info.offset is not None: - signal_entry["offset"] = signal_info.offset - signal_oscillator = experiment_dao.signal_oscillator(signal_info.signal_id) + if signal_info.oscillator is not None: + signal_entry["modulation"] = True + signal_oscillator = experiment_dao.signal_oscillator(signal_info.uid) if signal_oscillator is not None: signal_entry["oscillators_list"] = [{"$ref": signal_oscillator.uid}] retval["signals"].append(signal_entry) - device_id = experiment_dao.device_from_signal(signal_info.signal_id) signal_connection = { - "signal": {"$ref": signal_info.signal_id}, - "device": {"$ref": device_id}, + "signal": {"$ref": signal_info.uid}, + "device": {"$ref": signal_info.device.uid}, "connection": { - "type": signal_info.connection_type, + "type": "in" if signal_info.type.value == "integration" else "out", "channels": signal_info.channels, }, } - voltage_offset = experiment_dao.voltage_offset(signal_info.signal_id) + voltage_offset = experiment_dao.voltage_offset(signal_info.uid) if voltage_offset is not None: signal_connection["voltage_offset"] = voltage_offset - mixer_calibration = experiment_dao.mixer_calibration(signal_info.signal_id) + mixer_calibration = experiment_dao.mixer_calibration(signal_info.uid) if mixer_calibration is not None: - mixer_calibration_object = {} - for key in ["voltage_offsets", "correction_matrix"]: - if mixer_calibration.get(key) is not None: - mixer_calibration_object[key] = mixer_calibration[key] - if len(mixer_calibration_object.keys()) > 0: - signal_connection["mixer_calibration"] = mixer_calibration_object - - precompensation = experiment_dao.precompensation(signal_info.signal_id) + signal_connection["mixer_calibration"] = dataclasses.asdict( + mixer_calibration + ) + + precompensation = experiment_dao.precompensation(signal_info.uid) if precompensation is not None: - precompensation_object = {} - for key in ["exponential", "high_pass", "bounce", "FIR"]: - if precompensation.get(key) is not None: - precompensation_object[key] = precompensation[key] - if precompensation_object: - signal_connection["precompensation"] = precompensation_object - - lo_frequency = experiment_dao.lo_frequency(signal_info.signal_id) + signal_connection["precompensation"] = dataclasses.asdict(precompensation) + + lo_frequency = experiment_dao.lo_frequency(signal_info.uid) if lo_frequency is not None: signal_connection["lo_frequency"] = lo_frequency - port_mode = experiment_dao.port_mode(signal_info.signal_id) + port_mode = experiment_dao.port_mode(signal_info.uid) if port_mode is not None: signal_connection["port_mode"] = port_mode - signal_range, signal_range_unit = experiment_dao.signal_range( - signal_info.signal_id - ) - if signal_range is not None: - signal_connection["range"] = signal_range - if signal_range_unit is not None: - signal_connection["range_unit"] = signal_range_unit + signal_range = experiment_dao.signal_range(signal_info.uid) + if signal_range is not None and signal_range.value is not None: + signal_connection["range"] = signal_range.value + if signal_range is not None and signal_range.unit is not None: + signal_connection["range_unit"] = signal_range.unit - port_delay = experiment_dao.port_delay(signal_info.signal_id) + port_delay = experiment_dao.port_delay(signal_info.uid) if port_delay is not None: signal_connection["port_delay"] = port_delay - threshold = experiment_dao.threshold(signal_info.signal_id) + threshold = experiment_dao.threshold(signal_info.uid) if threshold is not None: signal_connection["threshold"] = threshold - amplitude = experiment_dao.amplitude(signal_info.signal_id) + amplitude = experiment_dao.amplitude(signal_info.uid) if amplitude is not None: signal_connection["amplitude"] = amplitude - amplifier_pump = experiment_dao.amplifier_pump(signal_info.signal_id) - if amplifier_pump is not None: - signal_connection["amplifier_pump"] = list(amplifier_pump) - delay_signal = signal_info.delay_signal if delay_signal is not None: signal_connection["delay_signal"] = delay_signal @@ -202,17 +178,15 @@ def dump(experiment_dao: ExperimentDAO): for pulse_id in experiment_dao.pulses(): pulse = experiment_dao.pulse(pulse_id) - pulse_entry = {"id": pulse.id} - fields = ["function", "length", "samples", "amplitude", "play_mode"] - for field_ in fields: - val = getattr(pulse, field_, None) - if val is not None: - pulse_entry[field_] = val - - if pulse_entry.get("amplitude_param"): - pulse_entry["amplitude"] = {"$ref": pulse_entry["amplitude_param"]} - if pulse_entry.get("length_param"): - pulse_entry["length"] = {"$ref": pulse_entry["length_param"]} + pulse_entry = {"id": pulse.uid} + if pulse.function: + pulse_entry["function"] = pulse.function + if pulse.amplitude is not None: + pulse_entry["amplitude"] = pulse.amplitude + if pulse.length is not None: + pulse_entry["length"] = pulse.length + if pulse.samples is not None: + pulse_entry["samples"] = pulse.samples pulses_list.append(pulse_entry) retval["pulses"] = pulses_list @@ -220,22 +194,23 @@ def dump(experiment_dao: ExperimentDAO): sections = {} for section_id in experiment_dao.sections(): section_info = experiment_dao.section_info(section_id) - if section_info.section_display_name in sections: + if section_info.uid in sections: # Section is reused, has already been processed continue - out_section = {"id": section_info.section_display_name} + out_section = {"id": section_info.uid} direct_children = experiment_dao.direct_section_children(section_id) direct_children = [ - experiment_dao.section_info(child_id).section_display_name - for child_id in direct_children + experiment_dao.section_info(child_id).uid for child_id in direct_children ] - if section_info.has_repeat: + if section_info.count is not None: out_section["repeat"] = { - "execution_type": section_info.execution_type, + "execution_type": section_info.execution_type.value + if section_info.execution_type + else None, "count": section_info.count, } @@ -252,7 +227,7 @@ def dump(experiment_dao: ExperimentDAO): out_section["repeat"]["parameters"].append(param_object) if len(direct_children) > 0: - if section_info.has_repeat: + if section_info.count is not None: out_section["repeat"]["sections_list"] = [ {"$ref": child} for child in direct_children ] @@ -261,12 +236,8 @@ def dump(experiment_dao: ExperimentDAO): {"$ref": child} for child in direct_children ] keys = [ - "align", "length", - "acquisition_types", - "repetition_mode", "repetition_time", - "averaging_mode", "play_after", "handle", "user_register", @@ -278,26 +249,32 @@ def dump(experiment_dao: ExperimentDAO): out_section[key] = getattr(section_info, key) if section_info.reset_oscillator_phase: out_section["reset_oscillator_phase"] = section_info.reset_oscillator_phase - if section_info.trigger_output: + if section_info.alignment: + out_section["align"] = section_info.alignment.value + if section_info.repetition_mode: + out_section["repetition_mode"] = section_info.repetition_mode.value + if section_info.acquisition_type: + out_section["acquisition_types"] = [section_info.acquisition_type.value] + if section_info.averaging_mode: + out_section["averaging_mode"] = section_info.averaging_mode.value + if section_info.triggers: out_section["trigger_output"] = [ { "signal": {"$ref": to_item["signal_id"]}, "state": to_item["state"], } - for to_item in section_info.trigger_output + for to_item in section_info.triggers ] signals_list = [] for signal_id in sorted(experiment_dao.section_signals(section_id)): section_signal_object = {"signal": {"$ref": signal_id}} section_signal_pulses = [] - for section_pulse in experiment_dao._section_pulses_raw( - section_id, signal_id - ): + for section_pulse in experiment_dao.section_pulses(section_id, signal_id): section_signal_pulse_object = {} - if section_pulse.pulse_id is not None: + if section_pulse.pulse is not None: section_signal_pulse_object["pulse"] = { - "$ref": section_pulse.pulse_id + "$ref": section_pulse.pulse.uid } if section_pulse.precompensation_clear: section_signal_pulse_object[ @@ -311,16 +288,27 @@ def dump(experiment_dao: ExperimentDAO): "set_oscillator_phase", "length", ]: - if getattr(section_pulse, key) is not None: - section_signal_pulse_object[key] = getattr(section_pulse, key) - if getattr(section_pulse, key + "_param") is not None: - section_signal_pulse_object[key] = { - "$ref": getattr(section_pulse, key + "_param") - } + if (val := getattr(section_pulse, key)) is not None: + if isinstance(val, ParameterInfo): + section_signal_pulse_object[key] = {"$ref": val.uid} + else: + section_signal_pulse_object[key] = val if section_pulse.acquire_params is not None: handle = section_pulse.acquire_params.handle if handle is not None: section_signal_pulse_object["readout_handle"] = handle + if section_pulse.play_pulse_parameters: + section_signal_pulse_object[ + "play_pulse_parameters" + ] = copy.deepcopy(section_pulse.play_pulse_parameters) + if section_pulse.pulse_pulse_parameters: + section_signal_pulse_object[ + "pulse_pulse_parameters" + ] = copy.deepcopy(section_pulse.pulse_pulse_parameters) + if section_pulse.pulse_group is not None: + section_signal_pulse_object[ + "pulse_group" + ] = section_pulse.pulse_group markers = getattr(section_pulse, "markers") if markers is not None: markers_object = {} @@ -344,13 +332,13 @@ def dump(experiment_dao: ExperimentDAO): if len(signals_list) > 0: out_section["signals_list"] = signals_list - sections[section_info.section_display_name] = out_section + sections[section_info.uid] = out_section retval["sections"] = list(sorted(sections.values(), key=lambda x: x["id"])) retval["experiment"] = { "sections_list": [ - {"$ref": experiment_dao.section_info(section).section_display_name} + {"$ref": experiment_dao.section_info(section).uid} for section in experiment_dao.root_sections() ], "signals_list": [{"$ref": signal_id} for signal_id in experiment_dao.signals()], diff --git a/laboneq/compiler/experiment_access/json_loader.py b/laboneq/compiler/experiment_access/json_loader.py index d7fe4f2..c4b4c96 100644 --- a/laboneq/compiler/experiment_access/json_loader.py +++ b/laboneq/compiler/experiment_access/json_loader.py @@ -13,14 +13,27 @@ from jsonschema.validators import validator_for -from laboneq.compiler.experiment_access.acquire_info import AcquireInfo from laboneq.compiler.experiment_access.loader_base import LoaderBase -from laboneq.compiler.experiment_access.marker import Marker -from laboneq.compiler.experiment_access.pulse_def import PulseDef -from laboneq.compiler.experiment_access.section_info import SectionInfo -from laboneq.compiler.experiment_access.section_signal_pulse import SectionSignalPulse from laboneq.core.exceptions import LabOneQException -from laboneq.core.types.enums import AcquisitionType +from laboneq.data.calibration import ExponentialCompensation, HighPassCompensation +from laboneq.data.compilation_job import ( + AcquireInfo, + FollowerInfo, + Marker, + MixerCalibrationInfo, + PrecompensationInfo, + PulseDef, + SectionInfo, + SectionSignalPulse, + SignalRange, +) +from laboneq.data.experiment_description import ( + AcquisitionType, + AveragingMode, + ExecutionType, + RepetitionMode, + SectionAlignment, +) _logger = logging.getLogger(__name__) @@ -41,7 +54,6 @@ class JsonLoader(LoaderBase): _validator = None def load(self, experiment: Dict): - self._load_servers(experiment) self._load_devices(experiment) self._load_oscillator(experiment) self._load_connectivity(experiment) @@ -50,15 +62,6 @@ def load(self, experiment: Dict): self._load_pulses(experiment) self._load_sections(experiment) - def _load_servers(self, experiment): - for server in experiment["servers"]: - self.add_server( - server["id"], - server.get("host"), - server.get("port"), - server.get("api_level"), - ) - def _load_devices(self, experiment): for device in sorted(experiment["devices"], key=lambda x: x["id"]): if "driver" in device: @@ -99,37 +102,32 @@ def _load_oscillator(self, experiment): def _load_connectivity(self, experiment): if "connectivity" in experiment: - if "dios" in experiment["connectivity"]: - for dio in experiment["connectivity"]["dios"]: - self.dios.append((dio["leader"]["$ref"], dio["follower"]["$ref"])) + for dio in experiment["connectivity"].get("dios", {}): + leader = self._devices[dio["leader"]["$ref"]] + follower = self._devices[dio["follower"]["$ref"]] + leader.followers.append(FollowerInfo(follower, 0)) if "leader" in experiment["connectivity"]: - leader_device_id = experiment["connectivity"]["leader"]["$ref"] self.global_leader_device_id = leader_device_id if "reference_clock" in experiment["connectivity"]: reference_clock = experiment["connectivity"]["reference_clock"] for device in self._devices.values(): - if device["device_type"] in {"hdawg", "uhfqa", "pqsc"}: - device["reference_clock"] = reference_clock - - if "pqscs" in experiment["connectivity"]: - pqscs = experiment["connectivity"]["pqscs"] - for pqsc in pqscs: - pqsc_device_id = pqsc["device"]["$ref"] - if "ports" in pqsc: - for port in pqsc["ports"]: - self.pqsc_ports.append( - (pqsc_device_id, port["device"]["$ref"], port["port"]) - ) + if device.device_type.value in {"hdawg", "uhfqa", "pqsc"}: + device.reference_clock = reference_clock + + for pqsc in experiment["connectivity"].get("pqscs", {}): + pqsc_device_id = pqsc["device"]["$ref"] + for port in pqsc.get("ports", ()): + self._devices[pqsc_device_id].followers.append( + FollowerInfo( + self._devices[port["device"]["$ref"]], port["port"] + ) + ) def _load_signals(self, experiment): for signal in sorted(experiment["signals"], key=lambda s: s["id"]): - self.add_signal( - signal["id"], - signal["signal_type"], - modulation=bool(signal.get("modulation")), - ) + self.add_signal(signal["id"], signal["signal_type"]) if "oscillators_list" in signal: for oscillator_ref in signal["oscillators_list"]: oscillator_id = oscillator_ref["$ref"] @@ -137,20 +135,39 @@ def _load_signals(self, experiment): def _load_signal_connections(self, experiment): for connection in experiment["signal_connections"]: - try: - voltage_offset = copy.deepcopy(connection["voltage_offset"]) - except KeyError: - voltage_offset = None - try: - mixer_calibration = copy.deepcopy(connection["mixer_calibration"]) - except KeyError: + voltage_offset = copy.deepcopy(connection.get("voltage_offset")) + mixer_calibration_dict = connection.get("mixer_calibration") + if mixer_calibration_dict is not None: + mixer_calibration = MixerCalibrationInfo( + voltage_offsets=mixer_calibration_dict.get("voltage_offsets"), + correction_matrix=mixer_calibration_dict.get("correction_matrix"), + ) + else: mixer_calibration = None - try: - precompensation = copy.deepcopy(connection["precompensation"]) - except KeyError: + precompensation_dict = connection.get("precompensation") + if precompensation_dict is not None: + exponential = precompensation_dict.get("exponential") + if exponential: + exponential = [ExponentialCompensation(**e) for e in exponential] + high_pass = precompensation_dict.get("high_pass") + if high_pass: + high_pass = HighPassCompensation(**high_pass) + bounce = precompensation_dict.get("bounce") + if bounce: + bounce = HighPassCompensation(**bounce) + FIR = precompensation_dict.get("FIR") + if FIR: + FIR = HighPassCompensation(**FIR) + + precompensation = PrecompensationInfo( + exponential, high_pass, bounce, FIR + ) + else: precompensation = None range = connection.get("range") range_unit = connection.get("range_unit") + if range is not None or range_unit is not None: + range = SignalRange(range, range_unit) lo_frequency = connection.get("lo_frequency") port_delay = connection.get("port_delay") delay_signal = connection.get("delay_signal") @@ -184,16 +201,20 @@ def _load_pulses(self, experiment): amplitude, amplitude_param = find_value_or_parameter_dict( pulse, "amplitude", (int, float, complex) ) + if amplitude_param is not None: + raise LabOneQException( + f"Amplitude of pulse '{pulse.uid}' cannot be a parameter." + f" To sweep the amplitude, pass the parameter in the" + f" corresponding `play()` command." + ) self.add_pulse( pulse["id"], PulseDef( - id=pulse["id"], + uid=pulse["id"], function=pulse.get("function"), length=pulse.get("length"), amplitude=amplitude, - amplitude_param=amplitude_param, - play_mode=pulse.get("play_mode"), samples=samples, ), ) @@ -231,9 +252,6 @@ def _load_sections(self, experiment): section = sections_proto[section_name] - if parent_instance is not None: - self.add_section_child(parent_instance, instance_id) - sections_list = None if "repeat" in section and "sections_list" in section["repeat"]: sections_list = section["repeat"]["sections_list"] @@ -244,14 +262,12 @@ def _load_sections(self, experiment): sections_to_process.appendleft( (child_section_ref["$ref"], instance_id) ) - has_repeat = False execution_type = None length = None - count: int = 1 + count: int | None = None if "repeat" in section: - has_repeat = True - execution_type = section["repeat"]["execution_type"] + execution_type = ExecutionType(section["repeat"]["execution_type"]) count = int(section["repeat"]["count"]) if "parameters" in section["repeat"]: @@ -268,15 +284,18 @@ def _load_sections(self, experiment): values, ) - acquisition_types = section.get("acquisition_types") - # backwards-compatibility: "acquisition_types" field was previously named "trigger" - acquisition_types = acquisition_types or section.get("trigger") - if self.acquisition_type is None and acquisition_types is not None: - self.acquisition_type = AcquisitionType(acquisition_types[0]) + acquisition_type = None + for field in ["acquisition_types", "trigger"]: + # backwards-compatibility: "acquisition_types" field was previously named "trigger" + acquisition_type = section.get(field, [acquisition_type])[0] + if acquisition_type is not None: + acquisition_type = AcquisitionType(acquisition_type) + if self.acquisition_type is None and acquisition_type is not None: + self.acquisition_type = acquisition_type align = None if "align" in section: - align = section["align"] + align = SectionAlignment(section["align"]) on_system_grid = False if "on_system_grid" in section: @@ -307,7 +326,7 @@ def _load_sections(self, experiment): averaging_mode = None if "averaging_mode" in section: - averaging_mode = section["averaging_mode"] + averaging_mode = AveragingMode(section["averaging_mode"]) repetition_time = None if "repetition_time" in section: @@ -315,7 +334,7 @@ def _load_sections(self, experiment): repetition_mode = None if "repetition_mode" in section: - repetition_mode = section["repetition_mode"] + repetition_mode = RepetitionMode(section["repetition_mode"]) trigger_output = [] for to_item in section.get("trigger_output", ()): @@ -330,14 +349,12 @@ def _load_sections(self, experiment): self.add_section( instance_id, SectionInfo( - section_id=instance_id, - section_display_name=section["id"], - has_repeat=has_repeat, + uid=instance_id, execution_type=execution_type, count=count, chunk_count=1, - acquisition_types=acquisition_types, - align=align, + acquisition_type=acquisition_type, + alignment=align, on_system_grid=on_system_grid, length=length, averaging_mode=averaging_mode, @@ -345,7 +362,7 @@ def _load_sections(self, experiment): repetition_time=repetition_time, play_after=section.get("play_after"), reset_oscillator_phase=reset_oscillator_phase, - trigger_output=trigger_output, + triggers=trigger_output, handle=handle, user_register=user_register, state=state, @@ -353,6 +370,9 @@ def _load_sections(self, experiment): ), ) + if parent_instance is not None: + self.add_section_child(parent_instance, instance_id) + if "signals_list" in section: for signals_list_entry in section["signals_list"]: signal_id = signals_list_entry["signal"]["$ref"] @@ -366,36 +386,56 @@ def _load_sections(self, experiment): ) = find_value_or_parameter_dict( pulse_ref, "offset", (int, float) ) + if pulse_offset_param is not None: + pulse_offset = self._all_parameters[pulse_offset_param] ( pulse_amplitude, pulse_amplitude_param, ) = find_value_or_parameter_dict( pulse_ref, "amplitude", (int, float, complex) ) + if pulse_amplitude_param is not None: + pulse_amplitude = self._all_parameters[ + pulse_amplitude_param + ] ( pulse_increment, pulse_increment_oscillator_phase_param, ) = find_value_or_parameter_dict( pulse_ref, "increment_oscillator_phase", (int, float) ) + if pulse_increment_oscillator_phase_param is not None: + pulse_increment = self._all_parameters[ + pulse_increment_oscillator_phase_param + ] ( pulse_set_oscillator_phase, pulse_set_oscillator_phase_param, ) = find_value_or_parameter_dict( pulse_ref, "set_oscillator_phase", (int, float) ) + if pulse_set_oscillator_phase_param is not None: + pulse_set_oscillator_phase = self._all_parameters[ + pulse_set_oscillator_phase_param + ] ( pulse_phase, pulse_phase_param, ) = find_value_or_parameter_dict( pulse_ref, "phase", (int, float) ) + if pulse_phase_param is not None: + pulse_phase = self._all_parameters[pulse_phase_param] ( resulting_pulse_instance_length, resulting_pulse_instance_length_param, ) = find_value_or_parameter_dict( pulse_ref, "length", (int, float) ) + if resulting_pulse_instance_length_param is not None: + resulting_pulse_instance_length = self._all_parameters[ + resulting_pulse_instance_length_param + ] precompensation_clear = pulse_ref.get( "precompensation_clear", False @@ -406,7 +446,7 @@ def _load_sections(self, experiment): pulse_id = pulse_ref["pulse"]["$ref"] acquire_params = None - signal_type = self._signals[signal_id]["signal_type"] + signal_type = self._signals[signal_id].type.value if signal_type == "integration": acquire_params = AcquireInfo( handle=pulse_ref.get("readout_handle"), @@ -414,45 +454,50 @@ def _load_sections(self, experiment): self.acquisition_type, "value", None ), ) + + pulse_parameters = copy.deepcopy( + pulse_ref.get("pulse_pulse_parameters") + ) + operation_pulse_parameters = copy.deepcopy( + pulse_ref.get("play_pulse_parameters") + ) + + pulse_group = pulse_ref.get("pulse_group") markers = [] - if "markers" in pulse_ref: - for k, v in pulse_ref["markers"].items(): - marker_pulse_id = None - pulse_ref = v.get("waveform") - if pulse_ref is not None: - marker_pulse_id = pulse_ref["$ref"] - - markers.append( - Marker( - k, - enable=v.get("enable"), - start=v.get("start"), - length=v.get("length"), - pulse_id=marker_pulse_id, - ) + for k, v in pulse_ref.get("markers", {}).items(): + marker_pulse_id = None + pulse_ref = v.get("waveform") + if pulse_ref is not None: + marker_pulse_id = pulse_ref["$ref"] + + markers.append( + Marker( + k, + enable=v.get("enable"), + start=v.get("start"), + length=v.get("length"), + pulse_id=marker_pulse_id, ) - self.add_signal_marker(signal_id, k) + ) + self.add_signal_marker(signal_id, k) new_ssp = SectionSignalPulse( - pulse_id=pulse_id, - signal_id=signal_id, + pulse=self._pulses[pulse_id] + if pulse_id is not None + else None, + signal=self._signals[signal_id], offset=pulse_offset, - offset_param=pulse_offset_param, amplitude=pulse_amplitude, - amplitude_param=pulse_amplitude_param, length=resulting_pulse_instance_length, - length_param=resulting_pulse_instance_length_param, acquire_params=acquire_params, phase=pulse_phase, - phase_param=pulse_phase_param, increment_oscillator_phase=pulse_increment, - increment_oscillator_phase_param=pulse_increment_oscillator_phase_param, set_oscillator_phase=pulse_set_oscillator_phase, - set_oscillator_phase_param=pulse_set_oscillator_phase_param, - play_pulse_parameters=None, - pulse_pulse_parameters=None, + play_pulse_parameters=operation_pulse_parameters, + pulse_pulse_parameters=pulse_parameters, precompensation_clear=precompensation_clear, markers=markers, + pulse_group=pulse_group, ) self.add_section_signal_pulse( instance_id, signal_id, new_ssp diff --git a/laboneq/compiler/experiment_access/loader_base.py b/laboneq/compiler/experiment_access/loader_base.py index 3c7af3b..ecfe125 100644 --- a/laboneq/compiler/experiment_access/loader_base.py +++ b/laboneq/compiler/experiment_access/loader_base.py @@ -1,15 +1,24 @@ # Copyright 2022 Zurich Instruments AG # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import logging from typing import Any, Optional -from laboneq.compiler.experiment_access.pulse_def import PulseDef -from laboneq.compiler.experiment_access.section_info import SectionInfo -from laboneq.compiler.experiment_access.section_signal_pulse import SectionSignalPulse from laboneq.core.exceptions import LabOneQException from laboneq.core.types.enums import AcquisitionType -from laboneq.data.compilation_job import ParameterInfo +from laboneq.data.compilation_job import ( + DeviceInfo, + DeviceInfoType, + OscillatorInfo, + ParameterInfo, + PulseDef, + SectionInfo, + SectionSignalPulse, + SignalInfo, + SignalInfoType, +) logger = logging.getLogger(__name__) @@ -17,26 +26,26 @@ class LoaderBase: def __init__(self): self.acquisition_type: Optional[AcquisitionType] = None + self.global_leader_device_id: str | None = None - # leader_uid, follower_uid, port - self.pqsc_ports: list[tuple[str, str, int]] = [] - - # leader_uid, follower_uid - self.dios: list[tuple[str, str]] = [] - self.global_leader_device_id: str = None - - self._devices = {} + self._devices: dict[str, DeviceInfo] = {} self._device_oscillators = {} - self._oscillators = {} - self._pulses = {} - self._sections = {} + self._oscillators: dict[str, OscillatorInfo] = {} + self._pulses: dict[str, PulseDef] = {} + self._sections: dict[str, SectionInfo] = {} + + # Todo (PW): This could be dropped and replaced by a look-up of + # `SectionInfo.parameters`. The loaders will require updating though. self._section_parameters = {} - self._section_signals = {} - self._section_signal_pulses = {} - self._section_tree = {} - self._servers = {} - self._signals = {} - self._signal_connections = {} + + # Todo (PW): Unlike `SectionInfo.pulses`, `self._section_signal_pulses` + # is indexed by signal. We could drop `_section_signal_pulses` and instead + # do a linear search of the section's SSPs instead. + # The scheduler could indeed be refactored such that it does not need access by + # signal, so there is no performance down-side. + self._section_signal_pulses: dict[str, dict[str, SectionSignalPulse]] = {} + + self._signals: dict[str, SignalInfo] = {} self._signal_markers = {} self._signal_oscillator = {} self._signal_trigger = {} @@ -54,12 +63,8 @@ def data(self) -> dict[str, Any]: "root_sections": self._root_sections, "sections": self._sections, "section_parameters": self._section_parameters, - "section_signals": self._section_signals, "section_signal_pulses": self._section_signal_pulses, - "section_tree": self._section_tree, - "servers": self._servers, "signals": self._signals, - "signal_connections": self._signal_connections, "signal_markers": self._signal_markers, "signal_oscillator": self._signal_oscillator, "signal_trigger": self._signal_trigger, @@ -110,7 +115,18 @@ def add_section_parameter( self._section_parameters.setdefault(section_id, []).append(param) def add_section_signal(self, section_uid, signal_uid): - self._section_signals.setdefault(section_uid, set()).add(signal_uid) + if isinstance(signal_uid, list): + for signal in signal_uid: + self.add_section_signal(section_uid, signal) + else: + assert section_uid in self._sections, "use `add_section()` first" + section = self._sections[section_uid] + + assert signal_uid in self._signals, "use `add_signal()` first" + signal = self._signals[signal_uid] + + if signal not in section.signals: + section.signals.append(signal) def add_section_signal_pulse( self, section_id, signal_id, section_signal_pulse: SectionSignalPulse @@ -121,55 +137,56 @@ def add_section_signal_pulse( if section_signal_pulse.acquire_params is not None: handle = section_signal_pulse.acquire_params.handle if handle is not None: - self.add_handle_acquire(handle, signal_id) + self._handle_acquires.setdefault(handle, []).append(signal_id) def add_signal_marker(self, signal_id, marker: str): self._signal_markers.setdefault(signal_id, set()).add(marker) - def add_server(self, server_id, host, port, api_level): - self._servers[server_id] = { - "id": server_id, - "port": int(port) if port is not None else None, - "host": host, - "api_level": api_level, - } - def add_device( self, - device_id, - device_type, + device_id: str, + device_type: DeviceInfoType | str, reference_clock=None, reference_clock_source=None, is_qc=None, ): - self._devices[device_id] = { - "uid": device_id, - "device_type": device_type, - "reference_clock": reference_clock, - "reference_clock_source": reference_clock_source, - "is_qc": is_qc, - } + self._devices[device_id] = DeviceInfo( + uid=device_id, + device_type=DeviceInfoType(device_type), + reference_clock=reference_clock, + reference_clock_source=reference_clock_source, + is_qc=is_qc, + ) def add_oscillator(self, oscillator_id, frequency, is_hardware): - self._oscillators[oscillator_id] = { - "uid": oscillator_id, - "frequency": frequency, - "is_hardware": is_hardware, - } + self._oscillators[oscillator_id] = OscillatorInfo( + uid=oscillator_id, frequency=frequency, is_hardware=is_hardware + ) - def add_signal(self, signal_id, signal_type, modulation, offset=None): - self._signals[signal_id] = { - "signal_id": signal_id, - "signal_type": signal_type, - "modulation": modulation, - "offset": offset, - } + def add_signal(self, signal_id, signal_type: str | SignalInfoType): + signal_info = SignalInfo(uid=signal_id, type=SignalInfoType(signal_type)) + assert signal_id not in self._signals + self._signals[signal_id] = signal_info def add_signal_oscillator(self, signal_id, oscillator_id): - self._signal_oscillator[signal_id] = oscillator_id + signal_info: SignalInfo = self._signals[signal_id] + signal_info.oscillator = self._oscillators[oscillator_id] def add_signal_connection(self, signal_id, signal_connection): - self._signal_connections[signal_id] = signal_connection + signal_info: SignalInfo = self._signals[signal_id] + signal_info.device = self._devices[signal_connection["device_id"]] + signal_info.channels = signal_connection["channels"] + signal_info.voltage_offset = signal_connection["voltage_offset"] + signal_info.mixer_calibration = signal_connection["mixer_calibration"] + signal_info.precompensation = signal_connection["precompensation"] + signal_info.lo_frequency = signal_connection["lo_frequency"] + signal_info.signal_range = signal_connection["range"] + signal_info.port_delay = signal_connection["port_delay"] + signal_info.delay_signal = signal_connection["delay_signal"] + signal_info.port_mode = signal_connection["port_mode"] + signal_info.threshold = signal_connection["threshold"] + signal_info.amplitude = signal_connection["amplitude"] + signal_info.amplifier_pump = signal_connection["amplifier_pump"] def add_section(self, section_id, section_info: SectionInfo): if section_info.handle is not None and section_info.user_register is not None: @@ -182,13 +199,8 @@ def add_pulse(self, pulse_id, pulse_def: PulseDef): self._pulses[pulse_id] = pulse_def def add_section_child(self, parent_id, child_id): - self._section_tree.setdefault(parent_id, []).append(child_id) - - def add_handle_acquire(self, handle: str, signal: str): - if handle in self._handle_acquires: - other_signal = self._handle_acquires[handle] - if other_signal != signal: - raise LabOneQException( - f"Acquisition handle '{handle}' used on multiple signals: {other_signal}, {signal}" - ) - self._handle_acquires[handle] = signal + assert parent_id in self._sections, "use `add_section()` first" + assert child_id in self._sections, "use `add_section()` first" + + parent: SectionInfo = self._sections[parent_id] + parent.children.append(self._sections[child_id]) diff --git a/laboneq/compiler/experiment_access/marker.py b/laboneq/compiler/experiment_access/marker.py deleted file mode 100644 index 7d3b237..0000000 --- a/laboneq/compiler/experiment_access/marker.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright 2022 Zurich Instruments AG -# SPDX-License-Identifier: Apache-2.0 - -from __future__ import annotations - -from dataclasses import dataclass - - -@dataclass -class Marker: - marker_selector: str - enable: bool - start: float - length: float - pulse_id: str diff --git a/laboneq/compiler/experiment_access/pulse_def.py b/laboneq/compiler/experiment_access/pulse_def.py deleted file mode 100644 index 76c98cf..0000000 --- a/laboneq/compiler/experiment_access/pulse_def.py +++ /dev/null @@ -1,37 +0,0 @@ -# Copyright 2022 Zurich Instruments AG -# SPDX-License-Identifier: Apache-2.0 - -from __future__ import annotations - -from dataclasses import asdict, dataclass -from typing import Optional - -import numpy as np -from numpy.typing import ArrayLike - - -@dataclass -class PulseDef: - id: str - function: str - length: float - amplitude: float - amplitude_param: str - play_mode: str - can_compress: Optional[bool] = False - samples: Optional[ArrayLike] = None - - @property - def effective_amplitude(self) -> float: - return 1.0 if self.amplitude is None else self.amplitude - - def __eq__(self, other: PulseDef): - if isinstance(other, PulseDef): - for k, v in asdict(self).items(): - if k == "samples": - if not np.array_equal(self.samples, other.samples): - return False - elif not v == getattr(other, k): - return False - return True - return False diff --git a/laboneq/compiler/experiment_access/section_info.py b/laboneq/compiler/experiment_access/section_info.py deleted file mode 100644 index 326ccdc..0000000 --- a/laboneq/compiler/experiment_access/section_info.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright 2022 Zurich Instruments AG -# SPDX-License-Identifier: Apache-2.0 - -from __future__ import annotations - -from dataclasses import dataclass, field -from typing import Dict, List, Optional, Union - - -@dataclass -class SectionInfo: - section_id: str - has_repeat: bool - execution_type: Optional[str] - acquisition_types: Optional[List[str]] - count: int - chunk_count: int - align: Optional[str] - on_system_grid: bool - length: Optional[float] - averaging_mode: Optional[str] - repetition_mode: Optional[str] - repetition_time: Optional[float] - play_after: Optional[Union[str, List[str]]] - reset_oscillator_phase: bool - handle: Optional[str] - user_register: Optional[int] - state: Optional[int] - local: Optional[bool] - section_display_name: Optional[str] = None - trigger_output: List[Dict] = field(default_factory=list) diff --git a/laboneq/compiler/experiment_access/section_signal_pulse.py b/laboneq/compiler/experiment_access/section_signal_pulse.py deleted file mode 100644 index c908748..0000000 --- a/laboneq/compiler/experiment_access/section_signal_pulse.py +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright 2022 Zurich Instruments AG -# SPDX-License-Identifier: Apache-2.0 - -from __future__ import annotations - -from dataclasses import dataclass -from typing import Any, List, Optional - -from laboneq.compiler.experiment_access.marker import Marker - - -@dataclass -class SectionSignalPulse: - signal_id: str - pulse_id: Optional[str] = None - length: Optional[float] = None - length_param: Optional[str] = None - amplitude: Optional[float] = None - amplitude_param: Optional[str] = None - offset: Optional[float] = None - offset_param: Optional[str] = None - phase: Optional[float] = None - phase_param: Optional[str] = None - increment_oscillator_phase: Optional[float] = None - increment_oscillator_phase_param: Optional[str] = None - set_oscillator_phase: Optional[float] = None - set_oscillator_phase_param: Optional[str] = None - acquire_params: Any = None - play_pulse_parameters: Optional[Any] = None - pulse_pulse_parameters: Optional[Any] = None - precompensation_clear: bool = False - markers: Optional[List[Marker]] = None diff --git a/laboneq/compiler/experiment_access/signal_info.py b/laboneq/compiler/experiment_access/signal_info.py deleted file mode 100644 index 6595413..0000000 --- a/laboneq/compiler/experiment_access/signal_info.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright 2022 Zurich Instruments AG -# SPDX-License-Identifier: Apache-2.0 - -from __future__ import annotations - -from dataclasses import dataclass - - -@dataclass -class SignalInfo: - signal_id: str - signal_type: str - device_id: str - device_type: str - connection_type: str - channels: str - delay_signal: float - modulation: str - offset: float diff --git a/laboneq/compiler/qccs-schema_2_5_0.json b/laboneq/compiler/qccs-schema_2_5_0.json index 636f50b..55d049a 100644 --- a/laboneq/compiler/qccs-schema_2_5_0.json +++ b/laboneq/compiler/qccs-schema_2_5_0.json @@ -215,6 +215,9 @@ "port_delay": { "type": "number" }, + "threshold": { + "type": "number" + }, "delay_signal": { "type": "number" } @@ -402,10 +405,6 @@ "id": { "$ref": "#/definitions/id-def" }, - "play_mode": { - "type": "string", - "value": "FUNCTIONAL" - }, "function": { "type": "string" }, @@ -441,10 +440,6 @@ "id": { "$ref": "#/definitions/id-def" }, - "play_mode": { - "type": "string", - "value": "SAMPLED_REAL" - }, "samples": { "type": "array", "items": { @@ -455,7 +450,6 @@ "additionalProperties": false, "required": [ "id", - "play_mode", "samples" ] }, @@ -465,10 +459,6 @@ "id": { "$ref": "#/definitions/id-def" }, - "play_mode": { - "type": "string", - "value": "SAMPLED_COMPLEX" - }, "samples": { "type": "array", "items": { @@ -656,6 +646,17 @@ }, "readout_handle": { "type": "string" + }, + "pulse_group": { + "type": "string" + }, + "pulse_pulse_parameters": { + "type": "object", + "additionalProperties": true + }, + "play_pulse_parameters": { + "type": "object", + "additionalProperties": true } }, "additionalProperties": false @@ -751,7 +752,9 @@ "type": "string", "enum": [ "integration_trigger", - "spectroscopy" + "spectroscopy", + "discrimination", + "RAW" ] } }, @@ -975,7 +978,6 @@ "required": [ "$schema", "metadata", - "servers", "devices", "signals", "signal_connections", diff --git a/laboneq/compiler/scheduler/acquire_group_schedule.py b/laboneq/compiler/scheduler/acquire_group_schedule.py new file mode 100644 index 0000000..a0ed7cb --- /dev/null +++ b/laboneq/compiler/scheduler/acquire_group_schedule.py @@ -0,0 +1,172 @@ +# Copyright 2022 Zurich Instruments AG +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from typing import Any, Dict, Iterator, List, Optional + +from attrs import define + +from laboneq.compiler.common.compiler_settings import CompilerSettings +from laboneq.compiler.common.event_type import EventType +from laboneq.compiler.common.pulse_parameters import encode_pulse_parameters +from laboneq.compiler.scheduler.interval_schedule import IntervalSchedule +from laboneq.data.compilation_job import ParameterInfo, SectionSignalPulse + + +@define(kw_only=True, slots=True) +class AcquireGroupSchedule(IntervalSchedule): + pulses: list[SectionSignalPulse] + amplitudes: list[float] + phases: list[float] + offset: int + oscillator_frequencies: list[Optional[float]] + set_oscillator_phases: list[Optional[float]] + increment_oscillator_phases: list[Optional[float]] + section: str + play_pulse_params: list[Optional[Dict[str, Any]]] + pulse_pulse_params: list[Optional[Dict[str, Any]]] + + def generate_event_list( + self, + start: int, + max_events: int, + id_tracker: Iterator[int], + expand_loops, + settings: CompilerSettings, + ) -> List[Dict]: + assert self.length is not None + assert self.absolute_start is not None + assert ( + len(self.pulses) + == len(self.amplitudes) + == len(self.set_oscillator_phases) + == len(self.increment_oscillator_phases) + == len(self.phases) + == len(self.oscillator_frequencies) + == len(self.play_pulse_params) + == len(self.pulse_pulse_params) + ) + + amplitude_resolution = pow( + 2, getattr(settings, "AMPLITUDE_RESOLUTION_BITS", 24) + ) + amplitudes = [ + round(amplitude * amplitude_resolution) / amplitude_resolution + for amplitude in self.amplitudes + ] + + assert all( + self.pulses[0].acquire_params.handle == p.acquire_params.handle + for p in self.pulses + ) + assert all( + self.pulses[0].acquire_params.acquisition_type + == p.acquire_params.acquisition_type + for p in self.pulses + ) + start_id = next(id_tracker) + d = { + "section_name": self.section, + "signal": [p.signal.uid for p in self.pulses], + "play_wave_id": [p.pulse.uid for p in self.pulses], + "parametrized_with": [], + "phase": self.phases, + "amplitude": amplitudes, + "chain_element_id": start_id, + "acquisition_type": self.pulses[0].acquire_params.acquisition_type, + "acquire_handle": self.pulses[0].acquire_params.handle, + } + + if self.oscillator_frequencies is not None: + d["oscillator_frequency"] = self.oscillator_frequencies + + if self.pulse_pulse_params: + d["pulse_pulse_parameters"] = [ + encode_pulse_parameters(par) if par is not None else None + for par in self.pulse_pulse_params + ] + if self.play_pulse_params: + d["play_pulse_parameters"] = [ + encode_pulse_parameters(par) if par is not None else None + for par in self.play_pulse_params + ] + + osc_events = [] + for pulse, iop, sop in zip( + self.pulses, self.increment_oscillator_phases, self.set_oscillator_phases + ): + params_list = [] + for f in ("length", "amplitude", "phase", "offset"): + if isinstance(getattr(pulse, f), ParameterInfo): + params_list.append(getattr(pulse, f).uid) + d["parametrized_with"].append(params_list) + + osc_common = { + "time": start, + "section_name": self.section, + "signal": pulse.signal.uid, + } + if iop: + osc_events.append( + { + "event_type": EventType.INCREMENT_OSCILLATOR_PHASE, + "increment_oscillator_phase": iop, + "id": next(id_tracker), + **osc_common, + } + ) + if sop is not None: + osc_events.append( + { + "event_type": EventType.SET_OSCILLATOR_PHASE, + "set_oscillator_phase": sop, + "id": next(id_tracker), + **osc_common, + } + ) + + return osc_events + [ + { + "event_type": EventType.ACQUIRE_START, + "time": start + self.offset, + "id": start_id, + **d, + }, + { + "event_type": EventType.ACQUIRE_END, + "time": start + self.length, + "id": next(id_tracker), + **d, + }, + ] + + def _calculate_timing( + self, + schedule_data: ScheduleData, # type: ignore # noqa: F821 + start: int, + start_may_change: bool, + ) -> int: + # Length must be set via parameter, so nothing to do here + assert self.length is not None + + valid_pulse = next( + ( + p + for p in self.pulses + if p + and p.pulse is not None + and p.acquire_params is not None + and p.acquire_params.handle + ), + None, + ) + if valid_pulse is not None: + schedule_data.acquire_pulses.setdefault( + valid_pulse.acquire_params.handle, [] + ).append(self) + + return start + + def __hash__(self): + return super().__hash__() diff --git a/laboneq/compiler/scheduler/match_schedule.py b/laboneq/compiler/scheduler/match_schedule.py index 56bf29d..91a88c2 100644 --- a/laboneq/compiler/scheduler/match_schedule.py +++ b/laboneq/compiler/scheduler/match_schedule.py @@ -102,7 +102,7 @@ def _compute_start_with_latency( # - The sum of the settings of the port_delay parameter for the acquisition device # for measure and acquire pulse - qa_signal_obj = schedule_data.signal_objects[acquire_pulse.pulse.signal_id] + qa_signal_obj = schedule_data.signal_objects[acquire_pulse.pulse.signal.uid] qa_device_type = qa_signal_obj.awg.device_type qa_sampling_rate = qa_signal_obj.awg.sampling_rate diff --git a/laboneq/compiler/scheduler/oscillator_schedule.py b/laboneq/compiler/scheduler/oscillator_schedule.py index 928dc25..14819e7 100644 --- a/laboneq/compiler/scheduler/oscillator_schedule.py +++ b/laboneq/compiler/scheduler/oscillator_schedule.py @@ -49,6 +49,7 @@ def generate_event_list( "section_name": self.section, "device_id": osc.device, "signal": osc.signal, + "oscillator_id": osc.id, "id": start_id, "chain_element_id": start_id, }, diff --git a/laboneq/compiler/scheduler/pulse_phase.py b/laboneq/compiler/scheduler/pulse_phase.py index b0d4431..b0b42d5 100644 --- a/laboneq/compiler/scheduler/pulse_phase.py +++ b/laboneq/compiler/scheduler/pulse_phase.py @@ -7,6 +7,7 @@ from laboneq.compiler.common.event_type import EventType from laboneq.compiler.experiment_access import ExperimentDAO from laboneq.core.exceptions.laboneq_exception import LabOneQException +from laboneq.data.compilation_job import DeviceInfoType _logger = logging.getLogger(__name__) @@ -47,37 +48,50 @@ def calculate_osc_phase(event_list, experiment_dao: ExperimentDAO): oscillator_phase_sets[signal_id] = event["time"] elif event["event_type"] in [EventType.PLAY_START, EventType.ACQUIRE_START]: - oscillator_phase = None - baseband_phase = None - signal_id = event["signal"] - signal_info = experiment_dao.signal_info(signal_id) - oscillator_info = experiment_dao.signal_oscillator(signal_id) - if oscillator_info is not None: - if signal_info.modulation and signal_info.device_type in [ - "hdawg", - "shfsg", - ]: - incremented_phase = oscillator_phase_cumulative.get(signal_id, 0.0) - - if oscillator_info.is_hardware: - if signal_id in oscillator_phase_sets: - raise LabOneQException( - f"There are set_oscillator_phase entries for signal " - f"'{signal_id}', but oscillator '{oscillator_info.uid}' " - f"is a hardware oscillator. Setting absolute phase is " - f"not supported for hardware oscillators." - ) - baseband_phase = incremented_phase - else: - phase_reference_time = phase_reset_time - if signal_id in oscillator_phase_sets: - phase_reference_time = max( - phase_reset_time, oscillator_phase_sets[signal_id] - ) - oscillator_phase = ( - event["time"] - phase_reference_time - ) * 2.0 * math.pi * event.get( - "oscillator_frequency", 0.0 - ) + incremented_phase - event["oscillator_phase"] = oscillator_phase - event["baseband_phase"] = baseband_phase + signal_ids = event["signal"] + is_signal_list = isinstance(signal_ids, list) + if not is_signal_list: + signal_ids = [signal_ids] + oscillator_phases = [] + baseband_phases = [] + for signal_id in signal_ids: + oscillator_phase = None + baseband_phase = None + signal_info = experiment_dao.signal_info(signal_id) + oscillator_info = experiment_dao.signal_oscillator(signal_id) + if oscillator_info is not None: + if signal_info.device.device_type in [ + DeviceInfoType.HDAWG, + DeviceInfoType.SHFSG, + ]: + incremented_phase = oscillator_phase_cumulative.get( + signal_id, 0.0 + ) + if oscillator_info.is_hardware: + if signal_id in oscillator_phase_sets: + raise LabOneQException( + f"There are set_oscillator_phase entries for signal " + f"'{signal_id}', but oscillator '{oscillator_info.uid}' " + f"is a hardware oscillator. Setting absolute phase is " + f"not supported for hardware oscillators." + ) + baseband_phase = incremented_phase + else: + phase_reference_time = phase_reset_time + if signal_id in oscillator_phase_sets: + phase_reference_time = max( + phase_reset_time, oscillator_phase_sets[signal_id] + ) + oscillator_phase = ( + event["time"] - phase_reference_time + ) * 2.0 * math.pi * event.get( + "oscillator_frequency", 0.0 + ) + incremented_phase + oscillator_phases.append(oscillator_phase) + baseband_phases.append(baseband_phase) + event["oscillator_phase"] = ( + oscillator_phases if is_signal_list else oscillator_phases[0] + ) + event["baseband_phase"] = ( + baseband_phases if is_signal_list else baseband_phases[0] + ) diff --git a/laboneq/compiler/scheduler/pulse_schedule.py b/laboneq/compiler/scheduler/pulse_schedule.py index b2f71f8..86957a7 100644 --- a/laboneq/compiler/scheduler/pulse_schedule.py +++ b/laboneq/compiler/scheduler/pulse_schedule.py @@ -11,8 +11,8 @@ from laboneq.compiler.common.event_type import EventType from laboneq.compiler.common.play_wave_type import PlayWaveType from laboneq.compiler.common.pulse_parameters import encode_pulse_parameters -from laboneq.compiler.experiment_access.section_signal_pulse import SectionSignalPulse from laboneq.compiler.scheduler.interval_schedule import IntervalSchedule +from laboneq.data.compilation_job import ParameterInfo, SectionSignalPulse @define(kw_only=True, slots=True) @@ -41,11 +41,14 @@ def generate_event_list( assert self.length is not None assert self.absolute_start is not None params_list = [] - for f in ("length_param", "amplitude_param", "phase_param", "offset_param"): - if getattr(self.pulse, f) is not None: - params_list.append(getattr(self.pulse, f)) + for f in ("length", "amplitude", "phase", "offset"): + if isinstance(getattr(self.pulse, f), ParameterInfo): + params_list.append(getattr(self.pulse, f).uid) - play_wave_id = self.pulse.pulse_id or "delay" + if self.pulse.pulse is not None: + play_wave_id = self.pulse.pulse.uid + else: + play_wave_id = "delay" amplitude_resolution = pow( 2, getattr(settings, "AMPLITUDE_RESOLUTION_BITS", 24) @@ -55,9 +58,9 @@ def generate_event_list( start_id = next(id_tracker) d = { "section_name": self.section, - "signal": self.pulse.signal_id, + "signal": self.pulse.signal.uid, "play_wave_id": play_wave_id, - "parameterized_with": params_list, + "parametrized_with": params_list, "phase": self.phase, "amplitude": amplitude, "chain_element_id": start_id, @@ -80,7 +83,7 @@ def generate_event_list( osc_common = { "time": start, "section_name": self.section, - "signal": self.pulse.signal_id, + "signal": self.pulse.signal.uid, } if self.increment_oscillator_phase: osc_events.append( @@ -101,7 +104,7 @@ def generate_event_list( } ) - is_delay = self.pulse.pulse_id is None + is_delay = self.pulse.pulse is None if is_delay: return osc_events + [ @@ -200,7 +203,7 @@ def generate_event_list( { "event_type": EventType.RESET_PRECOMPENSATION_FILTERS, "time": start, - "signal_id": self.pulse.pulse.signal_id, + "signal_id": self.pulse.pulse.signal.uid, "section_name": self.pulse.section, "id": next(id_tracker), } diff --git a/laboneq/compiler/scheduler/sampling_rate_tracker.py b/laboneq/compiler/scheduler/sampling_rate_tracker.py index 61f4c49..d31e4ea 100644 --- a/laboneq/compiler/scheduler/sampling_rate_tracker.py +++ b/laboneq/compiler/scheduler/sampling_rate_tracker.py @@ -15,7 +15,7 @@ def __init__(self, experiment_dao: ExperimentDAO, clock_settings): def sampling_rate_for_device(self, device_id): if device_id not in self._sampling_rate_cache: - device_type = DeviceType( + device_type = DeviceType.from_device_info_type( self._experiment_dao.device_info(device_id).device_type ) if ( @@ -33,7 +33,7 @@ def sampling_rate_for_device(self, device_id): def sequencer_rate_for_device(self, device_id): if device_id not in self._sequencer_rate_cache: - device_type = DeviceType( + device_type = DeviceType.from_device_info_type( self._experiment_dao.device_info(device_id).device_type ) if ( diff --git a/laboneq/compiler/scheduler/scheduler.py b/laboneq/compiler/scheduler/scheduler.py index 69f6fae..80446f0 100644 --- a/laboneq/compiler/scheduler/scheduler.py +++ b/laboneq/compiler/scheduler/scheduler.py @@ -30,8 +30,7 @@ from laboneq.compiler.common.event_type import EventType from laboneq.compiler.experiment_access.experiment_dao import ExperimentDAO from laboneq.compiler.experiment_access.param_ref import ParamRef -from laboneq.compiler.experiment_access.section_info import SectionInfo -from laboneq.compiler.experiment_access.section_signal_pulse import SectionSignalPulse +from laboneq.compiler.scheduler.acquire_group_schedule import AcquireGroupSchedule from laboneq.compiler.scheduler.case_schedule import CaseSchedule, EmptyBranch from laboneq.compiler.scheduler.interval_schedule import IntervalSchedule from laboneq.compiler.scheduler.loop_iteration_schedule import LoopIterationSchedule @@ -62,8 +61,14 @@ to_tinysample, ) from laboneq.core.exceptions import LabOneQException -from laboneq.core.types.enums import RepetitionMode, SectionAlignment -from laboneq.data.compilation_job import ParameterInfo +from laboneq.core.types.enums import RepetitionMode +from laboneq.data.compilation_job import ( + ParameterInfo, + SectionAlignment, + SectionInfo, + SectionSignalPulse, + SignalInfoType, +) if TYPE_CHECKING: from laboneq.compiler.common.signal_obj import SignalObj @@ -154,7 +159,7 @@ def _start_events(self): # Todo (PW): Drop once system tests have been migrated from legacy behaviour. for device_info in self._experiment_dao.device_infos(): try: - device_type = DeviceType(device_info.device_type) + device_type = DeviceType.from_device_info_type(device_info.device_type) except ValueError: # Not every device has a corresponding DeviceType (e.g. PQSC) continue @@ -232,7 +237,7 @@ def _schedule_section( if param.values is None: param.values = param.start + np.arange(section_info.count) * param.step - is_loop = section_info.has_repeat + is_loop = section_info.count is not None if is_loop: schedule = self._schedule_loop( section_id, section_info, current_parameters, sweep_parameters @@ -282,7 +287,7 @@ def _swept_hw_oscillators( "Hardware frequency sweep may drive only a single oscillator" ) oscillator_param_lookup[param.uid] = SweptHardwareOscillator( - id=oscillator.uid, device=signal_info.device_id, signal=signal + id=oscillator.uid, device=signal_info.device.uid, signal=signal ) return oscillator_param_lookup @@ -408,7 +413,7 @@ def _schedule_oscillator_frequency_step( params.append(param.uid) device_id = osc.device device_info = self._experiment_dao.device_info(device_id) - device_type = DeviceType(device_info.device_type) + device_type = DeviceType.from_device_info_type(device_info.device_type) length = max( length, int(device_type.oscillator_set_latency / self._TINYSAMPLE), @@ -447,13 +452,11 @@ def _schedule_phase_reset( hw_osc_devices = {} for signal in hw_signals: device = self._experiment_dao.device_from_signal(signal) - device_type = DeviceType( - self._experiment_dao.device_info(device).device_type - ) + device_type = DeviceType.from_device_info_type(device.device_type) if not device_type.supports_reset_osc_phase: continue duration = device_type.reset_osc_duration / self._TINYSAMPLE - hw_osc_devices[device] = duration + hw_osc_devices[device.uid] = duration length = max(length, duration) if device_type.lo_frequency_granularity is not None: # The frequency of Grimsel's LO in RF mode is a multiple of 100 MHz. @@ -574,7 +577,11 @@ def _schedule_loop_iteration( ) def _schedule_children( - self, section_id, section_info, children: List[IntervalSchedule], grid=1 + self, + section_id, + section_info: SectionInfo, + children: List[IntervalSchedule], + grid=1, ) -> SectionSchedule: """Schedule the given children of a section, arranging them in the required order. @@ -585,10 +592,8 @@ def _schedule_children( restrictions beyond those imposed by the children. In addition, escalation to the system grid can be enforced via the DSL. """ - right_align = section_info.align == SectionAlignment.RIGHT.value - signals = set() - for c in children: - signals.update(c.signals) + right_align = section_info.alignment == SectionAlignment.RIGHT + signals = set(s for c in children for s in c.signals) signals.update(self._experiment_dao.section_signals(section_id)) play_after = section_info.play_after or [] @@ -633,16 +638,15 @@ def _schedule_pulse( # todo: add memoization - grid, _ = self.grid(pulse.signal_id) + grid, _ = self.grid(pulse.signal.uid) def resolve_value_or_parameter(name, default): - value = default - param_name = name + "_param" - if getattr(pulse, name) is not None: - value = getattr(pulse, name) - elif getattr(pulse, param_name) is not None: + if (value := getattr(pulse, name)) is None: + return default + + if isinstance(value, ParameterInfo): try: - value = current_parameters[getattr(pulse, param_name)] + value = current_parameters[value.uid] except KeyError as e: raise LabOneQException( f"Parameter '{name}' requested outside of sweep. " @@ -652,26 +656,26 @@ def resolve_value_or_parameter(name, default): offset = resolve_value_or_parameter("offset", 0.0) length = resolve_value_or_parameter("length", None) - if length is None: - pulse_def = self._experiment_dao.pulse(pulse.pulse_id) - if pulse_def is not None: - assert pulse_def.length is None - if pulse_def.samples is None: - raise LabOneQException( - f"Cannot determine length of pulse '{pulse.pulse_id}' in section " - f"'{section}'. Either specify the length at the pulse definition, " - f"when playing the pulse, or by specifying the samples." - ) + if length is None and (pulse_def := pulse.pulse) is not None: + if pulse_def.length is not None: + length = pulse_def.length + elif pulse_def.samples is not None: length = len(pulse_def.samples) * grid * self._TINYSAMPLE else: - assert offset is not None - length = 0.0 + raise LabOneQException( + f"Cannot determine length of pulse '{pulse_def.uid}' in section " + f"'{section}'. Either specify the length at the pulse definition, " + f"when playing the pulse, or by specifying the samples." + ) + elif length is None: + assert offset is not None + length = 0.0 amplitude = resolve_value_or_parameter("amplitude", 1.0) if abs(amplitude) > 1.0 + 1e-9: raise LabOneQException( f"Magnitude of amplitude {amplitude} exceeding unity for pulse " - f"'{pulse.pulse_id}' on signal '{pulse.signal_id}' in section '{section}'" + f"'{pulse.pulse.uid}' on signal '{pulse.signal.uid}' in section '{section}'" ) phase = resolve_value_or_parameter("phase", 0.0) set_oscillator_phase = resolve_value_or_parameter("set_oscillator_phase", None) @@ -686,7 +690,7 @@ def resolve_pulse_params(params: Dict[str, Any]): resolved = current_parameters[value.param_name] except KeyError as e: raise LabOneQException( - f"Pulse '{pulse.pulse_id}' in section '{section}' requires " + f"Pulse '{pulse.pulse.uid}' in section '{section}' requires " f"parameter '{param}' which is not available. " f"Note that only RT sweep parameters are currently supported here." ) from e @@ -707,7 +711,7 @@ def resolve_pulse_params(params: Dict[str, Any]): length_int = round_to_grid(scheduled_length / self._TINYSAMPLE, grid) offset_int = round_to_grid(offset / self._TINYSAMPLE, grid) - osc = self._experiment_dao.signal_oscillator(pulse.signal_id) + osc = self._experiment_dao.signal_oscillator(pulse.signal.uid) if osc is None: freq = None elif not osc.is_hardware and isinstance(osc.frequency, ParameterInfo): @@ -715,22 +719,22 @@ def resolve_pulse_params(params: Dict[str, Any]): freq = current_parameters[osc.frequency.uid] except KeyError as e: raise LabOneQException( - f"Playback of pulse '{pulse.pulse_id}' in section '{section} " - f"requires the parameter '{osc.frequency_param}' to set the frequency." + f"Playback of pulse '{pulse.pulse.uid}' in section '{section} " + f"requires the parameter '{osc.frequency.uid}' to set the frequency." ) from e elif osc is None or osc.is_hardware: freq = None else: freq = osc.frequency if osc is not None else None - signal_info = self._experiment_dao.signal_info(pulse.signal_id) - is_acquire = signal_info.signal_type == "integration" + signal_info = self._experiment_dao.signal_info(pulse.signal.uid) + is_acquire = signal_info.type == SignalInfoType.INTEGRATION markers = pulse.markers return PulseSchedule( grid=grid, length=length_int, - signals={pulse.signal_id}, + signals={pulse.signal.uid}, pulse=pulse, section=section, amplitude=amplitude, @@ -745,6 +749,63 @@ def resolve_pulse_params(params: Dict[str, Any]): markers=markers, ) + def _schedule_acquire_group( + self, + pulses: list[SectionSignalPulse], + section: str, + current_parameters: ParameterStore[str, float], + ) -> AcquireGroupSchedule: + # Take the first one, they all run on the same device + grid, _ = self.grid(pulses[0].signal.uid) + offsets_int = [] + lengths_int = [] + amplitudes = [] + phases = [] + set_oscillator_phases = [] + increment_oscillator_phases = [] + play_pulse_params = [] + pulse_pulse_params = [] + freqs = [] + + for pulse in pulses: + pulse_schedule = self._schedule_pulse(pulse, section, current_parameters) + + lengths_int.append(pulse_schedule.length) + offsets_int.append(pulse_schedule.offset) + amplitudes.append(pulse_schedule.amplitude) + phases.append(pulse_schedule.phase) + set_oscillator_phases.append(pulse_schedule.set_oscillator_phase) + increment_oscillator_phases.append( + pulse_schedule.increment_oscillator_phase + ) + pulse_pulse_params.append(pulse_schedule.pulse_pulse_params) + play_pulse_params.append(pulse_schedule.play_pulse_params) + freqs.append(pulse_schedule.oscillator_frequency) + + assert pulse_schedule.is_acquire + assert not pulse.markers + + if len(set(lengths_int)) != 1 or len(set(offsets_int)) != 1: + raise LabOneQException( + f"Cannot schedule pulses with different lengths or offsets in the multistate discrimination group in section '{section}'. " + ) + + return AcquireGroupSchedule( + grid=grid, + length=lengths_int[0], + signals={p.signal.uid for p in pulses}, + pulses=pulses, + section=section, + amplitudes=amplitudes, + phases=phases, + offset=offsets_int[0], + set_oscillator_phases=set_oscillator_phases, + increment_oscillator_phases=increment_oscillator_phases, + oscillator_frequencies=freqs, + play_pulse_params=play_pulse_params, + pulse_pulse_params=pulse_pulse_params, + ) + def _schedule_match( self, section_id: str, @@ -785,11 +846,12 @@ def _schedule_match( if handle: try: - acquire_signal = dao.acquisition_signal(handle) + acquire_signals = dao.acquisition_signal(handle) except KeyError as e: raise LabOneQException(f"No acquisition with handle '{handle}'") from e - acquire_device = dao.device_from_signal(acquire_signal) - match_devices = {dao.device_from_signal(s) for s in signals} + assert isinstance(acquire_signals, list) and len(acquire_signals) >= 1 + acquire_device = dao.device_from_signal(acquire_signals[0]).uid + match_devices = {dao.device_from_signal(s).uid for s in signals} # todo: this is a brittle check for SHFQC local_feedback_allowed = match_devices == {f"{acquire_device}_sg"} @@ -846,7 +908,7 @@ def _schedule_case( section_info = self._schedule_data.experiment_dao.section_info(section_id) - assert not section_info.has_repeat # case must not be a loop + assert section_info.count is None # case must not be a loop assert section_info.handle is None and section_info.user_register is None state = section_info.state assert state is not None @@ -872,7 +934,7 @@ def _schedule_case( return schedule def _schedule_precomp_clear(self, pulse: PulseSchedule): - signal = pulse.pulse.signal_id + signal = pulse.pulse.signal.uid _, grid = self.grid(signal) # The precompensation clearing overlaps with a 'pulse' on the same signal, # whereas regular scheduling rules disallow this. For this reason we do not @@ -898,25 +960,32 @@ def _collect_children_schedules( pulse_schedules = [] section_signals = self._schedule_data.experiment_dao.section_signals(section_id) + pulse_groups: dict[str | None, list[Any]] = {None: []} for signal_id in section_signals: pulses = self._schedule_data.experiment_dao.section_pulses( section_id, signal_id ) - for pulse in pulses: - pulse_schedules.append( - self._schedule_pulse(pulse, section_id, parameters) - ) - if pulse.precompensation_clear: - pulse_schedules.append( - self._schedule_precomp_clear(pulse_schedules[-1]) - ) - signal_grid, _ = self.grid(signal_id) if len(pulses) == 0: # the section occupies the signal via a reserve, so add a placeholder # to include this signal in the grid calculation + signal_grid, _ = self.grid(signal_id) pulse_schedules.append(ReserveSchedule.create(signal_id, signal_grid)) + else: + for p in pulses: + pulse_groups.setdefault(p.pulse_group, []).append(p) + for pulse in pulse_groups[None]: + pulse_schedules.append(self._schedule_pulse(pulse, section_id, parameters)) + if pulse.precompensation_clear: + pulse_schedules.append( + self._schedule_precomp_clear(pulse_schedules[-1]) + ) + for (group, group_pulses) in pulse_groups.items(): + if group is not None: + pulse_schedules.append( + self._schedule_acquire_group(group_pulses, section_id, parameters) + ) if len(pulse_schedules) and len(subsection_schedules): if any(not isinstance(ps, ReserveSchedule) for ps in pulse_schedules): raise LabOneQException( @@ -936,7 +1005,7 @@ def grid(self, *signal_ids: Iterable[str]) -> Tuple[int, int]: for signal_id in signal_ids: signal = self._schedule_data.experiment_dao.signal_info(signal_id) - device = self._schedule_data.experiment_dao.device_info(signal.device_id) + device = signal.device assert device is not None sample_rate = self._sampling_rate_tracker.sampling_rate_for_device( @@ -967,11 +1036,10 @@ def _compute_trigger_output( The return value is a set of `(signal_id, bit_index)` tuples. """ - if len(section_info.trigger_output) == 0: + if len(section_info.triggers) == 0: return set() - section_name = section_info.section_display_name or section_info.section_id parent_section_trigger_states = {} # signal -> state - parent_section_id = section_name + parent_section_id = section_info.uid while True: parent_section_id = self._schedule_data.experiment_dao.section_parent( parent_section_id @@ -981,7 +1049,7 @@ def _compute_trigger_output( parent_section_info = self._schedule_data.experiment_dao.section_info( parent_section_id ) - for trigger_info in parent_section_info.trigger_output: + for trigger_info in parent_section_info.triggers: state = trigger_info["state"] signal = trigger_info["signal_id"] parent_section_trigger_states[signal] = ( @@ -989,7 +1057,7 @@ def _compute_trigger_output( ) section_trigger_signals = set() - for trigger_info in section_info.trigger_output: + for trigger_info in section_info.triggers: signal = trigger_info["signal_id"] state = trigger_info["state"] parent_state = parent_section_trigger_states.get(signal, 0) @@ -1037,7 +1105,7 @@ def _resolve_repetition_time( def search_lowest_level_loop(section): loop: Optional[str] = None section_info = self._schedule_data.experiment_dao.section_info(section) - if section_info.has_repeat: + if section_info.count is not None: loop = section children = self._schedule_data.experiment_dao.direct_section_children( diff --git a/laboneq/compiler/workflow/compiler.py b/laboneq/compiler/workflow/compiler.py index 1d16a95..4a6976e 100644 --- a/laboneq/compiler/workflow/compiler.py +++ b/laboneq/compiler/workflow/compiler.py @@ -47,7 +47,12 @@ from laboneq.core.types.compiled_experiment import CompiledExperiment from laboneq.core.types.enums.acquisition_type import AcquisitionType, is_spectroscopy from laboneq.core.types.enums.mixer_type import MixerType -from laboneq.data.compilation_job import DeviceInfo, ParameterInfo +from laboneq.data.compilation_job import ( + DeviceInfo, + ParameterInfo, + PrecompensationInfo, + SignalInfoType, +) from laboneq.data.scheduled_experiment import ScheduledExperiment from laboneq.executor.execution_from_experiment import ExecutionFactoryFromExperiment from laboneq.executor.executor import Statement @@ -79,9 +84,7 @@ def __init__(self, settings: Optional[Dict] = None): self._clock_settings: Dict[str, Any] = {} self._integration_unit_allocation = None self._awgs: _AWGMapping = {} - self._precompensations: Dict[ - str, Dict[str, Union[Dict[str, Any], float]] - ] = None + self._precompensations: dict[str, PrecompensationInfo] | None = None self._signal_objects: Dict[str, SignalObj] = {} _logger.info("Starting LabOne Q Compiler run...") @@ -103,7 +106,11 @@ def _check_tinysamples(self): ) def use_experiment(self, experiment): - if "experiment" in experiment and "setup" in experiment: + if ( + isinstance(experiment, dict) + and "experiment" in experiment + and "setup" in experiment + ): _logger.debug("Processing DSLv3 setup and experiment") self._experiment_dao = ExperimentDAO( None, experiment["setup"], experiment["experiment"] @@ -112,15 +119,21 @@ def use_experiment(self, experiment): experiment["experiment"] ) else: + # todo (Pol): ExperimentInfo currently tumbles down this path. For current + # tests, the mock execution is fine, but it won't be in production. self._experiment_dao = ExperimentDAO(experiment) self._execution = legacy_execution_program() + @staticmethod + def _get_first_instr_of(device_infos: List[DeviceInfo], type: str) -> DeviceInfo: + return next( + (instr for instr in device_infos if instr.device_type.value == type) + ) + def _analyze_setup(self): - def get_first_instr_of(device_infos: List[DeviceInfo], type) -> DeviceInfo: - return next((instr for instr in device_infos if instr.device_type == type)) device_infos = self._experiment_dao.device_infos() - device_type_list = [i.device_type for i in device_infos] + device_type_list = [i.device_type.value for i in device_infos] type_counter = Counter(device_type_list) has_pqsc = type_counter["pqsc"] > 0 has_hdawg = type_counter["hdawg"] > 0 @@ -134,7 +147,7 @@ def get_first_instr_of(device_infos: List[DeviceInfo], type) -> DeviceInfo: self._experiment_dao.signal_info(signal_id) for signal_id in self._experiment_dao.signals() ] - used_devices = set(info.device_type for info in signal_infos) + used_devices = set(info.device.device_type.value for info in signal_infos) if ( "hdawg" in used_devices and "uhfqa" in used_devices @@ -171,15 +184,15 @@ def get_first_instr_of(device_infos: List[DeviceInfo], type) -> DeviceInfo: if self._leader_properties.is_desktop_setup: if leader is None: if has_hdawg: - leader = get_first_instr_of(device_infos, "hdawg").uid + leader = self._get_first_instr_of(device_infos, "hdawg").uid elif has_shfqa: - leader = get_first_instr_of(device_infos, "shfqa").uid + leader = self._get_first_instr_of(device_infos, "shfqa").uid if has_shfsg: # SHFQC self._leader_properties.internal_followers = [ - get_first_instr_of(device_infos, "shfsg").uid + self._get_first_instr_of(device_infos, "shfsg").uid ] elif has_shfsg: - leader = get_first_instr_of(device_infos, "shfsg").uid + leader = self._get_first_instr_of(device_infos, "shfsg").uid _logger.debug("Using desktop setup configuration with leader %s", leader) @@ -187,7 +200,7 @@ def get_first_instr_of(device_infos: List[DeviceInfo], type) -> DeviceInfo: has_signal_on_awg_0_of_leader = False for signal_id in self._experiment_dao.signals(): signal_info = self._experiment_dao.signal_info(signal_id) - if signal_info.device_id == leader and ( + if signal_info.device.uid == leader and ( 0 in signal_info.channels or 1 in signal_info.channels ): has_signal_on_awg_0_of_leader = True @@ -199,7 +212,7 @@ def get_first_instr_of(device_infos: List[DeviceInfo], type) -> DeviceInfo: signal_type = "iq" channels = [0, 1] self._experiment_dao.add_signal( - device_id, channels, "out", signal_id, signal_type, False + device_id, channels, signal_id, signal_type ) _logger.debug( "No pulses played on channels 1 or 2 of %s, adding dummy signal %s to ensure triggering of the setup", @@ -210,16 +223,16 @@ def get_first_instr_of(device_infos: List[DeviceInfo], type) -> DeviceInfo: has_qa = type_counter["shfqa"] > 0 or type_counter["uhfqa"] > 0 is_hdawg_solo = type_counter["hdawg"] == 1 and not has_shf and not has_qa if is_hdawg_solo: - first_hdawg = get_first_instr_of(device_infos, "hdawg") + first_hdawg = self._get_first_instr_of(device_infos, "hdawg") if first_hdawg.reference_clock_source is None: self._clock_settings[first_hdawg.uid] = "internal" else: if not has_hdawg and has_shfsg: # SHFSG or SHFQC solo - first_shfsg = get_first_instr_of(device_infos, "shfsg") + first_shfsg = self._get_first_instr_of(device_infos, "shfsg") if first_shfsg.reference_clock_source is None: self._clock_settings[first_shfsg.uid] = "internal" if not has_hdawg and has_shfqa: # SHFQA or SHFQC solo - first_shfqa = get_first_instr_of(device_infos, "shfqa") + first_shfqa = self._get_first_instr_of(device_infos, "shfqa") if first_shfqa.reference_clock_source is None: self._clock_settings[first_shfqa.uid] = "internal" @@ -258,6 +271,8 @@ def _process_experiment(self): rt_compiler_output, [0] ) + self._combine_multistate_integrator_allocations() + @staticmethod def _get_total_rounded_delay(delay, signal_id, device_type, sampling_rate): if delay < 0: @@ -287,9 +302,11 @@ def _calc_osc_numbering(self): for signal_id in self._experiment_dao.signals(): signal_info = self._experiment_dao.signal_info(signal_id) - device_type = DeviceType(signal_info.device_type) + device_type = DeviceType.from_device_info_type( + signal_info.device.device_type + ) - if signal_info.signal_type == "integration": + if signal_info.type == SignalInfoType.INTEGRATION: continue hw_osc_names = set() @@ -312,19 +329,19 @@ def _calc_integration_unit_allocation(self): for signal_id in self._experiment_dao.signals(): signal_info = self._experiment_dao.signal_info(signal_id) _logger.debug("_integration_unit_allocation considering %s", signal_info) - if signal_info.signal_type == "integration": + if signal_info.type == SignalInfoType.INTEGRATION: _logger.debug( "_integration_unit_allocation: found integration signal %s", signal_info, ) - device_id = signal_info.device_id - device_type = DeviceType(signal_info.device_type) + device_type = DeviceType.from_device_info_type( + signal_info.device.device_type + ) awg_nr = Compiler.calc_awg_number(signal_info.channels[0], device_type) - num_acquire_signals = len( list( filter( - lambda x: x["device_id"] == device_id + lambda x: x["device_id"] == signal_info.device.uid and x["awg_nr"] == awg_nr, self._integration_unit_allocation.values(), ) @@ -350,7 +367,7 @@ def _calc_integration_unit_allocation(self): ) self._integration_unit_allocation[signal_id] = { - "device_id": device_id, + "device_id": signal_info.device.uid, "awg_nr": awg_nr, "channels": [ integrators_per_signal * num_acquire_signals + i @@ -358,17 +375,36 @@ def _calc_integration_unit_allocation(self): ], } + def _combine_multistate_integrator_allocations(self): + msgroups = self._combined_compiler_output.multistate_signal_groups + for msg in msgroups: + if allocations := [self._integration_unit_allocation.pop(s) for s in msg]: + a0 = allocations[0] + assert all(a["device_id"] == a0["device_id"] for a in allocations) + assert all(a["awg_nr"] == a0["awg_nr"] for a in allocations) + self._integration_unit_allocation[msg[0]] = { + "device_id": a0["device_id"], + "awg_nr": a0["awg_nr"], + "channels": [a["channels"] for a in allocations], + "signals": msg, + } + def _calc_shfqa_generator_allocation(self): self._shfqa_generator_allocation = {} for signal_id in self._experiment_dao.signals(): signal_info = self._experiment_dao.signal_info(signal_id) - device_type = DeviceType(signal_info.device_type) + device_type = DeviceType.from_device_info_type( + signal_info.device.device_type + ) - if signal_info.signal_type == "iq" and device_type == DeviceType.SHFQA: + if ( + signal_info.type == SignalInfoType.IQ + and device_type == DeviceType.SHFQA + ): _logger.debug( "_shfqa_generator_allocation: found SHFQA iq signal %s", signal_info ) - device_id = signal_info.device_id + device_id = signal_info.device.uid awg_nr = Compiler.calc_awg_number(signal_info.channels[0], device_type) num_generator_signals = len( list( @@ -404,14 +440,16 @@ def _calc_awgs(self): ] = {} for signal_id in self._experiment_dao.signals(): signal_info = self._experiment_dao.signal_info(signal_id) - device_id = signal_info.device_id - device_type = DeviceType(signal_info.device_type) + device_id = signal_info.device.uid + device_type = DeviceType.from_device_info_type( + signal_info.device.device_type + ) for channel in sorted(signal_info.channels): awg_number = Compiler.calc_awg_number(channel, device_type) device_awgs = awgs.setdefault(device_id, SortedDict()) awg = device_awgs.get(awg_number) if awg is None: - signal_type = signal_info.signal_type + signal_type = signal_info.type.value # Treat "integration" signal type same as "iq" at AWG level if signal_type == "integration": signal_type = "iq" @@ -426,7 +464,7 @@ def _calc_awgs(self): awg.signal_channels.append((signal_id, channel)) - if signal_info.signal_type == "iq": + if signal_info.type == SignalInfoType.IQ: signal_channel_awg_key = (device_id, awg.awg_number, channel) if signal_channel_awg_key in signals_by_channel_and_awg: signals_by_channel_and_awg[signal_channel_awg_key][ @@ -474,13 +512,15 @@ def _calc_awgs(self): self._awgs = awgs def get_awg(self, signal_id) -> AWGInfo: - awg_number = None signal_info = self._experiment_dao.signal_info(signal_id) - device_id = signal_info.device_id - device_type = DeviceType(signal_info.device_type) + device_id = signal_info.device.uid + device_type = DeviceType.from_device_info_type(signal_info.device.device_type) awg_number = Compiler.calc_awg_number(signal_info.channels[0], device_type) - if signal_info.signal_type == "integration" and device_type != DeviceType.SHFQA: + if ( + signal_info.type == SignalInfoType.INTEGRATION + and device_type != DeviceType.SHFQA + ): awg_number = 0 return self._awgs[device_id][awg_number] @@ -508,8 +548,10 @@ class DelayInfo: signal_info = self._experiment_dao.signal_info(signal_id) delay_signal = signal_info.delay_signal - device_type = DeviceType(signal_info.device_type) - device_id = signal_info.device_id + device_type = DeviceType.from_device_info_type( + signal_info.device.device_type + ) + device_id = signal_info.device.uid sampling_rate = self._sampling_rate_tracker.sampling_rate_for_device( device_id @@ -520,7 +562,7 @@ class DelayInfo: self._leader_properties.is_desktop_setup, self._clock_settings["use_2GHz_for_HDAWG"], ) - start_delay += self._precompensations[signal_id]["computed_delay_signal"] + start_delay += self._precompensations[signal_id].computed_delay_signal if delay_signal is not None: delay_signal = self._get_total_rounded_delay( @@ -545,7 +587,7 @@ class DelayInfo: }.get(device_type, TriggerMode.NONE) awg.sampling_rate = sampling_rate - signal_type = signal_info.signal_type + signal_type = signal_info.type.value _logger.debug( "Adding signal %s with signal type %s", signal_id, signal_type @@ -554,11 +596,7 @@ class DelayInfo: oscillator_frequency = None oscillator_info = self._experiment_dao.signal_oscillator(signal_id) - if ( - oscillator_info is not None - and not oscillator_info.is_hardware - and signal_info.modulation - ): + if oscillator_info is not None and not oscillator_info.is_hardware: oscillator_frequency = oscillator_info.frequency channels = copy.deepcopy(signal_info.channels) if signal_id in self._integration_unit_allocation: @@ -624,7 +662,7 @@ def calc_outputs(self, signal_delays: SignalDelays): for signal_id in self._experiment_dao.signals(): signal_info = self._experiment_dao.signal_info(signal_id) - if signal_info.signal_type == "integration": + if signal_info.type == SignalInfoType.INTEGRATION: continue oscillator_frequency = None oscillator_number = None @@ -642,17 +680,17 @@ def calc_outputs(self, signal_delays: SignalDelays): mixer_calibration = self._experiment_dao.mixer_calibration(signal_id) lo_frequency = self._experiment_dao.lo_frequency(signal_id) port_mode = self._experiment_dao.port_mode(signal_id) - signal_range, signal_range_unit = self._experiment_dao.signal_range( - signal_id + signal_range = self._experiment_dao.signal_range(signal_id) + device_type = DeviceType.from_device_info_type( + signal_info.device.device_type ) - device_type = DeviceType(signal_info.device_type) port_delay = self._experiment_dao.port_delay(signal_id) scheduler_port_delay: float = 0.0 if signal_id in signal_delays: scheduler_port_delay += signal_delays[signal_id].on_device precompensation = self._precompensations[signal_id] - pc_port_delay = precompensation["computed_port_delay"] + pc_port_delay = precompensation.computed_port_delay if pc_port_delay: scheduler_port_delay += pc_port_delay @@ -677,17 +715,19 @@ def calc_outputs(self, signal_delays: SignalDelays): for channel in signal_info.channels: output = { - "device_id": signal_info.device_id, + "device_id": signal_info.device.uid, "channel": channel, "lo_frequency": lo_frequency, "port_mode": port_mode, - "range": signal_range, - "range_unit": signal_range_unit, + "range": signal_range.value if signal_range is not None else None, + "range_unit": signal_range.unit + if signal_range is not None + else None, "port_delay": port_delay, "scheduler_port_delay": scheduler_port_delay, "amplitude": self._experiment_dao.amplitude(signal_id), } - signal_is_modulated = signal_info.modulation + signal_is_modulated = signal_info.oscillator is not None if oscillator_is_hardware and signal_is_modulated: output["modulation"] = True @@ -708,41 +748,46 @@ def calc_outputs(self, signal_delays: SignalDelays): output["diagonal"] = None output["off_diagonal"] = None - if signal_info.signal_type == "single" and voltage_offset is not None: + if signal_info.type == SignalInfoType.RF and voltage_offset is not None: output["offset"] = voltage_offset - if signal_info.signal_type == "iq" and mixer_calibration is not None: - if mixer_calibration["voltage_offsets"] is not None: - output["offset"] = mixer_calibration["voltage_offsets"][ + if ( + signal_info.type == SignalInfoType.IQ + and mixer_calibration is not None + ): + if mixer_calibration.voltage_offsets is not None: + output["offset"] = mixer_calibration.voltage_offsets[ channel - base_channel ] - if mixer_calibration["correction_matrix"] is not None: - output["diagonal"] = mixer_calibration["correction_matrix"][ + if mixer_calibration.correction_matrix is not None: + output["diagonal"] = mixer_calibration.correction_matrix[ channel - base_channel ][channel - base_channel] - output["off_diagonal"] = mixer_calibration["correction_matrix"][ + output["off_diagonal"] = mixer_calibration.correction_matrix[ flipper[channel - base_channel] ][channel - base_channel] - device_type = DeviceType(signal_info.device_type) + device_type = DeviceType.from_device_info_type( + signal_info.device.device_type + ) if precompensation_is_nonzero(precompensation): if not device_type.supports_precompensation: raise RuntimeError( - f"Device {signal_info.device_id} does not" + f"Device {signal_info.device.uid} does not" + " support precompensation" ) warnings = verify_precompensation_parameters( precompensation, self._sampling_rate_tracker.sampling_rate_for_device( - signal_info.device_id + signal_info.device.uid ), signal_id, ) if warnings: - _logger.warn(warnings) + _logger.warning(warnings) output["precompensation"] = precompensation if markers is not None: - if signal_info.signal_type == "iq": + if signal_info.type == SignalInfoType.IQ: if device_type == DeviceType.HDAWG: marker_key = channel % 2 + 1 if f"marker{marker_key}" in markers: @@ -753,7 +798,7 @@ def calc_outputs(self, signal_delays: SignalDelays): if "marker2" in markers: raise RuntimeError("Only marker1 supported on SHFSG") if triggers is not None: - if signal_info.signal_type == "iq": + if signal_info.type == SignalInfoType.IQ: if device_type == DeviceType.HDAWG: trigger_bit = 2 ** (channel % 2) if triggers & trigger_bit: @@ -762,7 +807,7 @@ def calc_outputs(self, signal_delays: SignalDelays): and output["marker_mode"] == "MARKER" ): raise RuntimeError( - f"Trying to use marker and trigger on the same output channel {channel} with signal {signal_id} on device {signal_info.device_id}" + f"Trying to use marker and trigger on the same output channel {channel} with signal {signal_id} on device {signal_info.device.uid}" ) else: output["marker_mode"] = "TRIGGER" @@ -775,14 +820,14 @@ def calc_outputs(self, signal_delays: SignalDelays): and output["marker_mode"] == "MARKER" ): raise RuntimeError( - f"Trying to use marker and trigger on the same SG output channel {channel} with signal {signal_id} on device {signal_info.device_id}" + f"Trying to use marker and trigger on the same SG output channel {channel} with signal {signal_id} on device {signal_info.device.uid}" ) else: output["marker_mode"] = "TRIGGER" output["oscillator_frequency"] = oscillator_frequency output["oscillator"] = oscillator_number - channel_key = (signal_info.device_id, channel) + channel_key = (signal_info.device.uid, channel) # TODO(2K): check for conflicts if 'channel_key' already present in 'all_channels' all_channels[channel_key] = output retval = sorted( @@ -795,13 +840,11 @@ def calc_inputs(self, signal_delays: SignalDelays): all_channels = {} for signal_id in self._experiment_dao.signals(): signal_info = self._experiment_dao.signal_info(signal_id) - if signal_info.signal_type != "integration": + if signal_info.type != SignalInfoType.INTEGRATION: continue lo_frequency = self._experiment_dao.lo_frequency(signal_id) - signal_range, signal_range_unit = self._experiment_dao.signal_range( - signal_id - ) + signal_range = self._experiment_dao.signal_range(signal_id) port_delay = self._experiment_dao.port_delay(signal_id) @@ -811,15 +854,17 @@ def calc_inputs(self, signal_delays: SignalDelays): for channel in signal_info.channels: input = { - "device_id": signal_info.device_id, + "device_id": signal_info.device.uid, "channel": channel, "lo_frequency": lo_frequency, - "range": signal_range, - "range_unit": signal_range_unit, + "range": signal_range.value if signal_range is not None else None, + "range_unit": signal_range.unit + if signal_range is not None + else None, "port_delay": port_delay, "scheduler_port_delay": scheduler_port_delay, } - channel_key = (signal_info.device_id, channel) + channel_key = (signal_info.device.uid, channel) # TODO(2K): check for conflicts if 'channel_key' already present in 'all_channels' all_channels[channel_key] = input retval = sorted( @@ -833,7 +878,7 @@ def calc_measurement_map(self, integration_times: IntegrationTimes): for section_name in self._experiment_dao.sections(): section_info = self._experiment_dao.section_info(section_name) - if section_info.acquisition_types is not None: + if section_info.acquisition_type is not None: measurement_sections.append(section_name) section_measurement_infos = [] @@ -849,13 +894,15 @@ def empty_device(): infos_by_device_awg = {} for signal in section_signals: signal_info_for_section = self._experiment_dao.signal_info(signal) - device_type = DeviceType(signal_info_for_section.device_type) + device_type = DeviceType.from_device_info_type( + signal_info_for_section.device.device_type + ) awg_nr = Compiler.calc_awg_number( signal_info_for_section.channels[0], device_type ) - if signal_info_for_section.signal_type == "integration": - device_id = signal_info_for_section.device_id + if signal_info_for_section.type == SignalInfoType.INTEGRATION: + device_id = signal_info_for_section.device.uid device_awg_key = (device_id, awg_nr) if device_awg_key not in infos_by_device_awg: infos_by_device_awg[device_awg_key] = { @@ -871,7 +918,7 @@ def empty_device(): _logger.debug( "Added measurement device %s", - signal_info_for_section.device_id, + signal_info_for_section.device.uid, ) section_measurement_infos.extend(infos_by_device_awg.values()) @@ -880,8 +927,6 @@ def empty_device(): measurements = {} for info in section_measurement_infos: - section_name = info["section_name"] - for device_awg_nr, v in info["devices"].items(): device_id, awg_nr = device_awg_nr @@ -1056,7 +1101,7 @@ def compiler_output(self) -> CompiledExperiment: return CompiledExperiment( experiment_dict=ExperimentDAO.dump(self._experiment_dao), scheduled_experiment=ScheduledExperiment( - recipe=self._recipe, # TODO(2K): Build 'Recipe' instead of dict + recipe=self._recipe, src=self._combined_compiler_output.src, waves=list(self._combined_compiler_output.waves.values()), wave_indices=self._combined_compiler_output.wave_indices, @@ -1118,8 +1163,7 @@ def get_lead_delay( desktop_setup: bool, hdawg_uses_2GHz: bool, ): - if not isinstance(device_type, DeviceType): - raise RuntimeError(f"Device type {device_type} is not of type DeviceType") + assert isinstance(device_type, DeviceType) if device_type == DeviceType.HDAWG: if not desktop_setup: if hdawg_uses_2GHz: diff --git a/laboneq/compiler/workflow/neartime_execution.py b/laboneq/compiler/workflow/neartime_execution.py index e2ad12e..e17ded5 100644 --- a/laboneq/compiler/workflow/neartime_execution.py +++ b/laboneq/compiler/workflow/neartime_execution.py @@ -68,7 +68,7 @@ def legacy_execution_program(): class NtCompilerExecutor(ExecutorBase): def __init__(self, rt_compiler: RealtimeCompiler): - super().__init__(looping_mode=LoopingMode.EXECUTE) + super().__init__(looping_mode=LoopingMode.NEAR_TIME_ONLY) self._rt_compiler = rt_compiler self._iteration_stack = IterationStack() @@ -93,7 +93,8 @@ def set_sw_param_handler( @contextmanager def for_loop_handler(self, count: int, index: int, loop_flags: LoopFlags): self._iteration_stack.push(index, {}) - # the name & value will be set by the set_sw_param_handler + if loop_flags.is_pipeline: + self._iteration_stack.set_parameter_value("__pipeline_index", index) yield diff --git a/laboneq/compiler/workflow/precompensation_helpers.py b/laboneq/compiler/workflow/precompensation_helpers.py index 90091d1..2c5a119 100644 --- a/laboneq/compiler/workflow/precompensation_helpers.py +++ b/laboneq/compiler/workflow/precompensation_helpers.py @@ -6,44 +6,48 @@ import copy import math from math import ceil -from typing import TYPE_CHECKING, Any, Dict, NewType +from typing import TYPE_CHECKING import numpy as np from engineering_notation import EngNumber from laboneq.compiler.common.device_type import DeviceType from laboneq.core.exceptions import LabOneQException +from laboneq.data.calibration import ( + BounceCompensation, + ExponentialCompensation, + FIRCompensation, +) +from laboneq.data.compilation_job import DeviceInfoType, PrecompensationInfo if TYPE_CHECKING: from laboneq.compiler.experiment_access.experiment_dao import ExperimentDAO -PrecompensationType = NewType("PrecompensationType", Dict[str, Dict[str, Any]]) - -def precompensation_is_nonzero(precompensation: PrecompensationType): +def precompensation_is_nonzero(precompensation: PrecompensationInfo): """Check whether the precompensation has any effect""" return precompensation is not None and ( - precompensation.get("exponential") - or precompensation.get("high_pass") - or precompensation.get("bounce") - or precompensation.get("FIR") + precompensation.exponential + or precompensation.high_pass + or precompensation.bounce + or precompensation.FIR ) -def precompensation_delay_samples(precompensation: PrecompensationType): +def precompensation_delay_samples(precompensation: PrecompensationInfo): """Compute the additional delay (in samples) caused by the precompensation""" if not precompensation_is_nonzero(precompensation): return 0 delay = 72 try: - delay += 88 * len(precompensation["exponential"]) + delay += 88 * len(precompensation.exponential or []) except KeyError: pass - if precompensation.get("high_pass") is not None: + if precompensation.high_pass is not None: delay += 96 - if precompensation.get("bounce") is not None: + if precompensation.bounce is not None: delay += 32 - if precompensation.get("FIR") is not None: + if precompensation.FIR is not None: delay += 136 return delay @@ -55,8 +59,11 @@ def _adapt_precompensations_of_awg(signal_ids, precompensations): has_bounce = False has_FIR = False for signal_id in signal_ids: - precompensation = precompensations.get(signal_id) or {} - hp = bool(precompensation.get("high_pass")) + precompensation: PrecompensationInfo = ( + precompensations.get(signal_id) or PrecompensationInfo() + ) + + hp = bool(precompensation.high_pass) if has_high_pass is None: has_high_pass = hp else: @@ -66,36 +73,39 @@ def _adapt_precompensations_of_awg(signal_ids, precompensations): + "outputs of the same AWG must have the high pass " + f"filter enabled or disabled; see signal {signal_id}" ) - exp = precompensation.get("exponential") + exp = precompensation.exponential if exp is not None and number_of_exponentials < len(exp): number_of_exponentials = len(exp) - has_bounce = has_bounce or bool(precompensation.get("bounce")) - has_FIR = has_FIR or bool(precompensation.get("FIR")) + has_bounce = has_bounce or bool(precompensation.bounce) + has_FIR = has_FIR or bool(precompensation.FIR) # Add zero effect filters to get consistent timing if has_bounce or has_FIR or number_of_exponentials: for signal_id in signal_ids: - old_pc = precompensations.get(signal_id, {}) or {} + old_pc = precompensations.get(signal_id) or PrecompensationInfo() new_pc = copy.deepcopy(old_pc) if number_of_exponentials: - exp = new_pc.setdefault("exponential", []) - exp += [{"amplitude": 0, "timeconstant": 10e-9}] * ( - number_of_exponentials - len(exp) + exp = new_pc.exponential = new_pc.exponential or [] + exp.extend( + [ExponentialCompensation(amplitude=0, timeconstant=10e-9)] + * (number_of_exponentials - len(exp)) ) - if has_bounce and not new_pc.get("bounce"): - new_pc["bounce"] = {"delay": 10e-9, "amplitude": 0} - if has_FIR and not new_pc.get("FIR"): - new_pc["FIR"] = {"coefficients": [1.0]} + if has_bounce and not new_pc.bounce: + new_pc.bounce = BounceCompensation(delay=10e-9, amplitude=0) + if has_FIR and not new_pc.FIR: + new_pc.FIR = FIRCompensation(coefficients=[1.0]) precompensations[signal_id] = new_pc -def adapt_precompensations(precompensations: PrecompensationType, dao: ExperimentDAO): +def adapt_precompensations( + precompensations: dict[str, PrecompensationInfo], dao: ExperimentDAO +): """Make sure that we have the same timing for rf_signals on the same AWG""" signals_by_awg = {} # Group by AWG for signal_id in precompensations.keys(): signal_info = dao.signal_info(signal_id) - device_id = signal_info.device_id - device_type = DeviceType(signal_info.device_type) + device_id = signal_info.device.uid + device_type = DeviceType.from_device_info_type(signal_info.device.device_type) channel = signal_info.channels[0] awg = ( 0 @@ -117,16 +127,18 @@ def compute_precompensations_and_delays(dao: ExperimentDAO): adapt_precompensations(precompensations, dao) for signal_id, pc in precompensations.items(): delay = precompensation_delay_samples(pc) - pc = precompensations.setdefault(signal_id, {}) + pc = precompensations.setdefault(signal_id, PrecompensationInfo()) if pc is None: - precompensations[signal_id] = {"computed_delay_samples": delay} + precompensations[signal_id] = PrecompensationInfo( + computed_delay_samples=delay + ) else: - pc["computed_delay_samples"] = delay + pc.computed_delay_samples = delay return precompensations def compute_precompensation_delays_on_grid( - precompensations: PrecompensationType, dao: ExperimentDAO, use_2GHz: bool + precompensations: dict[str, PrecompensationInfo], dao: ExperimentDAO, use_2GHz: bool ): """Compute delay_signal and port_delay contributions for each signal so that delays are commensurable with the grid""" @@ -139,7 +151,9 @@ def compute_precompensation_delays_on_grid( unique_sequencer_rates = set() sampling_rates_and_multiples = {} for signal_id in signals: - devtype = DeviceType(signal_infos[signal_id].device_type) + devtype = DeviceType.from_device_info_type( + signal_infos[signal_id].device.device_type + ) sampling_rate = ( devtype.sampling_rate_2GHz if use_2GHz and devtype == DeviceType.HDAWG @@ -158,7 +172,7 @@ def compute_precompensation_delays_on_grid( max_delay = 0 for signal_id, pc in precompensations.items(): delay = ( - precompensations[signal_id]["computed_delay_samples"] + precompensations[signal_id].computed_delay_samples / sampling_rates_and_multiples[signal_id][0] ) if max_delay < delay: @@ -166,9 +180,9 @@ def compute_precompensation_delays_on_grid( max_delay = ceil(max_delay / system_grid) * system_grid for signal_id in signals: - pc = precompensations.setdefault(signal_id, {}) + pc = precompensations.setdefault(signal_id, PrecompensationInfo()) try: - delay_samples = pc["computed_delay_samples"] + delay_samples = pc.computed_delay_samples except KeyError: delay_samples = 0 sampling_rate, multiple = sampling_rates_and_multiples[signal_id] @@ -176,9 +190,12 @@ def compute_precompensation_delays_on_grid( compensation = max_delay_samples - delay_samples delay_signal = (compensation // multiple) / sampling_rate * multiple port_delay = (compensation % multiple) / sampling_rate - assert port_delay == 0 or signal_infos[signal_id].device_type != "uhfqa" - pc["computed_delay_signal"] = delay_signal if abs(delay_signal) > 1e-12 else 0 - pc["computed_port_delay"] = port_delay if abs(port_delay) > 1e-12 else 0 + assert ( + port_delay == 0 + or signal_infos[signal_id].device.device_type != DeviceInfoType.UHFQA + ) + pc.computed_delay_signal = delay_signal if abs(delay_signal) > 1e-12 else 0 + pc.computed_port_delay = port_delay if abs(port_delay) > 1e-12 else 0 def _round_to_FPGA(coef): @@ -286,49 +303,51 @@ def verify_exponential_filter_params( def verify_precompensation_parameters( - precompensation: Dict, sampling_rate: float, signal_id: str + precompensation: PrecompensationInfo | None, + sampling_rate: float, + signal_id: str, ) -> str: if not precompensation: return "" warnings = [] - pcexp = precompensation.get("exponential") + pcexp = precompensation.exponential if pcexp: - if len(precompensation["exponential"]) > 8: + if len(precompensation.exponential) > 8: raise LabOneQException( f"Too many exponential filters defined on '{signal_id}'. Maximum is 8 exponential filters." ) warnings += [ verify_exponential_filter_params( - e["timeconstant"], e["amplitude"], sampling_rate, signal_id + e.timeconstant, e.amplitude, sampling_rate, signal_id ) for e in pcexp ] - hp = precompensation.get("high_pass") - if hp and not (166e-3 >= hp["timeconstant"] >= 208e-12): + hp = precompensation.high_pass + if hp and not (166e-3 >= hp.timeconstant >= 208e-12): warnings.append( f"High pass precompensation timeconstant of signal '{signal_id}' out " "of range; will be clamped to [208 ps, 166 ms]." ) - bounce = precompensation.get("bounce") + bounce = precompensation.bounce if bounce: - if bounce["delay"] > 103.3e-9: + if bounce.delay > 103.3e-9: warnings.append( f"Bounce precompensation timeconstant of signal '{signal_id}' out " "of range; will be clamped to 103.3 ns." ) - if abs(bounce["amplitude"]) > 1: + if abs(bounce.amplitude) > 1: warnings.append( f"Bounce precompensation amplitude of signal '{signal_id}' out " "of range; will be clamped to +/- 1." ) - fir = precompensation.get("FIR") + fir = precompensation.FIR if fir: - if len(fir["coefficients"]) > 40: + if len(fir.coefficients) > 40: raise LabOneQException( "Too many coefficients in FIR filter defined on " f"'{signal_id}'. Maximum is 40 coefficients." ) - if any(abs(np.array(fir["coefficients"])) > 4): + if any(abs(np.array(fir.coefficients)) > 4): warnings.append( f"FIR precompensation coefficients of signal '{signal_id}' out " "of range; will be clamped to +/- 4." diff --git a/laboneq/compiler/workflow/realtime_compiler.py b/laboneq/compiler/workflow/realtime_compiler.py index 78d26b2..85c4375 100644 --- a/laboneq/compiler/workflow/realtime_compiler.py +++ b/laboneq/compiler/workflow/realtime_compiler.py @@ -6,7 +6,7 @@ import logging from dataclasses import dataclass -from typing import Any, Dict, Optional +from typing import Any, Dict, Optional, Set, Tuple from laboneq._observability.tracing import trace from laboneq.compiler import CodeGenerator, CompilerSettings @@ -40,6 +40,7 @@ class RealtimeCompilerOutput: command_tables: Dict[AwgKey, Dict[str, Any]] pulse_map: Dict[str, PulseMapEntry] schedule: Dict[str, Any] + multistate_signal_groups: Set[Tuple[str, ...]] class RealtimeCompiler: @@ -82,6 +83,7 @@ def _generate_code(self): events = self._scheduler.event_timing(expand_loops=False) code_generator.gen_acquire_map(events) + code_generator.find_multistate_signal_groups(events) code_generator.gen_seq_c( events, {k: self._experiment_dao.pulse(k) for k in self._experiment_dao.pulses()}, @@ -109,6 +111,7 @@ def run(self, near_time_parameters: Optional[ParameterStore] = None): command_tables=self._code_generator.command_tables(), pulse_map=self._code_generator.pulse_map(), schedule=self.prepare_schedule(), + multistate_signal_groups=self._code_generator.multistate_signal_groups(), ) return compiler_output @@ -146,7 +149,7 @@ def prepare_schedule(self): *self._experiment_dao.all_section_children(root_section), ]: section_info = self._experiment_dao.section_info(section) - section_display_name = section_info.section_display_name + section_display_name = section_info.uid section_signals_with_children[section] = list( self._experiment_dao.section_signals_with_children(section) ) @@ -158,8 +161,8 @@ def prepare_schedule(self): sampling_rate_tuples = [] for signal_id in self._experiment_dao.signals(): signal_info = self._experiment_dao.signal_info(signal_id) - device_id = signal_info.device_id - device_type = signal_info.device_type + device_id = signal_info.device.uid + device_type = signal_info.device.device_type.value sampling_rate_tuples.append( ( device_type, diff --git a/laboneq/compiler/workflow/recipe_generator.py b/laboneq/compiler/workflow/recipe_generator.py index ce24998..228cfed 100644 --- a/laboneq/compiler/workflow/recipe_generator.py +++ b/laboneq/compiler/workflow/recipe_generator.py @@ -3,14 +3,32 @@ from __future__ import annotations +import dataclasses import logging from typing import TYPE_CHECKING, Any, Dict, List, Optional from laboneq.compiler.code_generator.measurement_calculator import IntegrationTimes from laboneq.compiler.common.device_type import DeviceType from laboneq.compiler.experiment_access.experiment_dao import ExperimentDAO -from laboneq.core.types.enums.acquisition_type import AcquisitionType +from laboneq.core.exceptions import LabOneQException +from laboneq.core.types.enums.acquisition_type import is_spectroscopy from laboneq.data.compilation_job import ParameterInfo +from laboneq.data.recipe import ( + AWG, + IO, + AcquireLength, + Gains, + Initialization, + IntegratorAllocation, + Measurement, + NtStepKey, + OscillatorParam, + RealtimeExecutionInit, + Recipe, + RefClkType, + SignalType, + TriggeringMode, +) if TYPE_CHECKING: from laboneq.compiler.workflow.compiler import LeaderProperties @@ -20,20 +38,9 @@ class RecipeGenerator: def __init__(self): - self._recipe = {} - self._recipe[ - "$schema" - ] = "../../interface/qccs/interface/schemas/recipe-schema-1_4_0.json" - self._recipe["line_endings"] = "unix" - self._recipe["header"] = { - "version": "1.4.0", - "unit": {"time": "s", "frequency": "Hz", "phase": "rad"}, - "epsilon": {"time": 1e-12}, - } - self._recipe["experiment"] = {"realtime_execution_init": []} + self._recipe = Recipe() def add_oscillator_params(self, experiment_dao: ExperimentDAO): - oscillator_params = [] for signal_id in experiment_dao.signals(): signal_info = experiment_dao.signal_info(signal_id) oscillator_info = experiment_dao.signal_oscillator(signal_id) @@ -46,16 +53,15 @@ def add_oscillator_params(self, experiment_dao: ExperimentDAO): frequency, param = oscillator_info.frequency, None for ch in signal_info.channels: - oscillator_param = { - "id": oscillator_info.uid, - "device_id": signal_info.device_id, - "channel": ch, - "frequency": frequency, - "param": param, - } - oscillator_params.append(oscillator_param) - - self._recipe["experiment"]["oscillator_params"] = oscillator_params + self._recipe.oscillator_params.append( + OscillatorParam( + id=oscillator_info.uid, + device_id=signal_info.device.uid, + channel=ch, + frequency=frequency, + param=param, + ) + ) def add_integrator_allocations( self, @@ -63,62 +69,69 @@ def add_integrator_allocations( experiment_dao: ExperimentDAO, integration_weights, ): - def _make_integrator_allocation(signal_id: str, integrator): - weights = {} - if signal_id in integration_weights: - weights = next(iter(integration_weights[signal_id].values()), {}) - integrator_allocation = { - "signal_id": signal_id, - "device_id": integrator["device_id"], - "awg": integrator["awg_nr"], - "channels": integrator["channels"], - "weights": weights.get("basename"), - } - threshold = experiment_dao.threshold(signal_id) - if threshold is not None: - integrator_allocation["threshold"] = threshold + def _make_integrator_allocation( + signal_id: str, integrator + ) -> IntegratorAllocation: + is_multistate = "signals" in integrator and len(integrator["signals"]) > 1 + signals = integrator.get("signals") or [signal_id] + thresholds: list[float] = [ + experiment_dao.threshold(signal_id) for signal_id in signals + ] + weights: list[dict[str, Any]] = [ + next(iter(integration_weights[signal_id].values()), {}) + if signal_id in integration_weights + else {} + for signal_id in signals + ] + integrator_allocation = IntegratorAllocation( + signal_id=signals if is_multistate else signal_id, + device_id=integrator["device_id"], + awg=integrator["awg_nr"], + channels=integrator["channels"], + weights=[w.get("basename") for w in weights] + if is_multistate + else weights[0].get("basename"), + ) + if is_multistate or thresholds[0] is not None: + integrator_allocation.threshold = ( + thresholds if is_multistate else thresholds[0] + ) return integrator_allocation - self._recipe["experiment"]["integrator_allocations"] = [ - _make_integrator_allocation(signal_id, integrator) - for signal_id, integrator in integration_unit_allocation.items() - ] + for signal_id, integrator in integration_unit_allocation.items(): + self._recipe.integrator_allocations.append( + _make_integrator_allocation(signal_id, integrator) + ) def add_acquire_lengths(self, integration_times: IntegrationTimes): - self._recipe["experiment"]["acquire_lengths"] = [ - { - "section_id": section_id, - "signal_id": signal_id, - "acquire_length": integration_info.length_in_samples, - } - for section_id, section_integration_time in integration_times.items() - for signal_id, integration_info in section_integration_time.items() - if not integration_info.is_play - ] + self._recipe.acquire_lengths.extend( + [ + AcquireLength( + section_id=section_id, + signal_id=signal_id, + acquire_length=integration_info.length_in_samples, + ) + for section_id, section_integration_time in integration_times.items() + for signal_id, integration_info in section_integration_time.items() + if not integration_info.is_play + ] + ) def add_devices_from_experiment(self, experiment_dao: ExperimentDAO): - devices = [] - initializations = [] for device in experiment_dao.device_infos(): - devices.append( - {"device_uid": device.uid, "driver": device.device_type.upper()} + self._recipe.initializations.append( + Initialization( + device_uid=device.uid, device_type=device.device_type.name + ) ) - initializations.append({"device_uid": device.uid, "config": {}}) - self._recipe["devices"] = devices - self._recipe["experiment"]["initializations"] = initializations - - def add_acquisition_type_from_experiment(self, experiment_dao: ExperimentDAO): - self._recipe["experiment"]["acquisition_type"] = ( - AcquisitionType.INTEGRATION.value - if experiment_dao.acquisition_type is None - else experiment_dao.acquisition_type.value - ) - def _find_initialization(self, device_uid): - for initialization in self._recipe["experiment"]["initializations"]: - if initialization["device_uid"] == device_uid: + def _find_initialization(self, device_uid) -> Initialization: + for initialization in self._recipe.initializations: + if initialization.device_uid == device_uid: return initialization - return None + raise LabOneQException( + f"Internal error: missing initialization for device {device_uid}" + ) def add_connectivity_from_experiment( self, @@ -128,50 +141,81 @@ def add_connectivity_from_experiment( ): if leader_properties.global_leader is not None: initialization = self._find_initialization(leader_properties.global_leader) - initialization["config"]["repetitions"] = 1 - initialization["config"]["holdoff"] = 0 + initialization.config.repetitions = 1 + initialization.config.holdoff = 0 if leader_properties.is_desktop_setup: - initialization["config"]["triggering_mode"] = "desktop_leader" + initialization.config.triggering_mode = TriggeringMode.DESKTOP_LEADER if leader_properties.is_desktop_setup: # Internal followers are followers on the same device as the leader. This # is necessary for the standalone SHFQC, where the SHFSG part does neither # appear in the PQSC device connections nor the DIO connections. for f in leader_properties.internal_followers: initialization = self._find_initialization(f) - initialization["config"]["triggering_mode"] = "internal_follower" + initialization.config.triggering_mode = TriggeringMode.INTERNAL_FOLLOWER for device in experiment_dao.device_infos(): device_uid = device.uid initialization = self._find_initialization(device_uid) reference_clock = experiment_dao.device_reference_clock(device_uid) - if reference_clock is not None: - initialization["config"]["reference_clock"] = reference_clock + initialization.config.reference_clock = ( + RefClkType._10MHZ if reference_clock == 10e6 else RefClkType._100MHZ + ) - if device.device_type == "hdawg" and clock_settings["use_2GHz_for_HDAWG"]: - initialization["config"][ - "sampling_rate" - ] = DeviceType.HDAWG.sampling_rate_2GHz + if ( + device.device_type.value == "hdawg" + and clock_settings["use_2GHz_for_HDAWG"] + ): + initialization.config.sampling_rate = ( + DeviceType.HDAWG.sampling_rate_2GHz + ) - if device.device_type == "shfppc": + if device.device_type.value == "shfppc": ppchannels = [] for signal in experiment_dao.signals(): amplifier_pump = experiment_dao.amplifier_pump(signal) - if amplifier_pump is not None and amplifier_pump[0] == device_uid: - ppchannels.append(amplifier_pump[1]) - initialization["ppchannels"] = ppchannels + if ( + amplifier_pump is None + or amplifier_pump.device.uid != device_uid + ): + continue + amplifier_pump_dict: dict[str, str | float | bool | int] = { + "cancellation": amplifier_pump.cancellation, + "alc_engaged": amplifier_pump.alc_engaged, + "use_probe": amplifier_pump.use_probe, + "channel": amplifier_pump.channel, + } + for field in [ + "pump_freq", + "pump_power", + "probe_frequency", + "probe_power", + ]: + val = getattr(amplifier_pump, field) + if val is None: + continue + if isinstance(val, ParameterInfo): + amplifier_pump_dict[field] = val.uid + else: + amplifier_pump_dict[field] = val + + ppchannels.append(amplifier_pump_dict) + initialization.ppchannels = ppchannels for follower in experiment_dao.dio_followers(): initialization = self._find_initialization(follower) if leader_properties.is_desktop_setup: - initialization["config"]["triggering_mode"] = "desktop_dio_follower" + initialization.config.triggering_mode = ( + TriggeringMode.DESKTOP_DIO_FOLLOWER + ) else: - initialization["config"]["triggering_mode"] = "dio_follower" + initialization.config.triggering_mode = TriggeringMode.DIO_FOLLOWER for pqsc_device_id in experiment_dao.pqscs(): for port in experiment_dao.pqsc_ports(pqsc_device_id): - follower_device_id = port["device"] - follower_device_init = self._find_initialization(follower_device_id) - follower_device_init["config"]["triggering_mode"] = "zsync_follower" + follower_device_init = self._find_initialization(port["device"]) + follower_device_init.config.triggering_mode = ( + TriggeringMode.ZSYNC_FOLLOWER + ) def add_output( self, @@ -193,41 +237,36 @@ def add_output( marker_mode=None, amplitude=None, ): - output = {"channel": channel, "enable": True} - if offset is not None: - output.update({"offset": offset}) - if diagonal is not None and off_diagonal is not None: - output.update( - {"gains": {"diagonal": diagonal, "off_diagonal": off_diagonal}} - ) if precompensation is not None: - output["precompensation"] = precompensation - if lo_frequency is not None: - output["lo_frequency"] = lo_frequency - if port_mode is not None: - output["port_mode"] = port_mode - if output_range is not None: - output["range"] = output_range - if output_range_unit is not None: - output["range_unit"] = output_range_unit - if oscillator is not None: - output["oscillator"] = oscillator - output["modulation"] = modulation - if oscillator_frequency is not None and not isinstance( - oscillator_frequency, ParameterInfo - ): - output["oscillator_frequency"] = oscillator_frequency - if port_delay is not None: - output["port_delay"] = port_delay - output["scheduler_port_delay"] = scheduler_port_delay - if marker_mode is not None: - output["marker_mode"] = marker_mode - if amplitude is not None: - output["amplitude"] = amplitude - - initialization: dict = self._find_initialization(device_id) - outputs: list = initialization.setdefault("outputs", []) - outputs.append(output) + precomp_dict = { + k: v + for k, v in dataclasses.asdict(precompensation).items() + if k in ("exponential", "high_pass", "bounce", "FIR") + } + if "clearing" in (precomp_dict["high_pass"] or {}): + del precomp_dict["high_pass"]["clearing"] + else: + precomp_dict = None + output = IO( + channel=channel, + enable=True, + offset=offset, + precompensation=precomp_dict, + lo_frequency=lo_frequency, + port_mode=port_mode, + range=None if output_range is None else float(output_range), + range_unit=output_range_unit, + modulation=modulation, + port_delay=port_delay, + scheduler_port_delay=scheduler_port_delay, + marker_mode=marker_mode, + amplitude=amplitude, + ) + if diagonal is not None and off_diagonal is not None: + output.gains = Gains(diagonal=diagonal, off_diagonal=off_diagonal) + + initialization = self._find_initialization(device_id) + initialization.outputs.append(output) def add_input( self, @@ -239,20 +278,18 @@ def add_input( port_delay=None, scheduler_port_delay=0.0, ): - input = {"channel": channel, "enable": True} - if lo_frequency is not None: - input["lo_frequency"] = lo_frequency - if input_range is not None: - input["range"] = input_range - if input_range_unit is not None: - input["range_unit"] = input_range_unit - if port_delay is not None: - input["port_delay"] = port_delay - input["scheduler_port_delay"] = scheduler_port_delay - - initialization: dict = self._find_initialization(device_id) - inputs: list = initialization.setdefault("inputs", []) - inputs.append(input) + input = IO( + channel=channel, + enable=True, + lo_frequency=lo_frequency, + range=None if input_range is None else float(input_range), + range_unit=input_range_unit, + port_delay=port_delay, + scheduler_port_delay=scheduler_port_delay, + ) + + initialization = self._find_initialization(device_id) + initialization.inputs.append(input) def add_awg( self, @@ -263,18 +300,15 @@ def add_awg( command_table_match_offset: Optional[int], feedback_register: Optional[int], ): + awg = AWG( + awg=awg_number, + signal_type=SignalType(signal_type), + qa_signal_id=qa_signal_id, + command_table_match_offset=command_table_match_offset, + feedback_register=feedback_register, + ) initialization = self._find_initialization(device_id) - - if "awgs" not in initialization: - initialization["awgs"] = [] - awg = { - "awg": awg_number, - "signal_type": signal_type, - "qa_signal_id": qa_signal_id, - "command_table_match_offset": command_table_match_offset, - "feedback_register": feedback_register, - } - initialization["awgs"].append(awg) + initialization.awgs.append(awg) def add_realtime_step( self, @@ -284,14 +318,14 @@ def add_realtime_step( wave_indices_name: str, nt_loop_indices: List[int], ): - self._recipe["experiment"]["realtime_execution_init"].append( - { - "device_id": device_id, - "awg_id": awg_id, - "seqc_ref": seqc_filename, - "wave_indices_ref": wave_indices_name, - "nt_step": {"indices": nt_loop_indices}, - } + self._recipe.realtime_execution_init.append( + RealtimeExecutionInit( + device_id=device_id, + awg_id=awg_id, + seqc_ref=seqc_filename, + wave_indices_ref=wave_indices_name, + nt_step=NtStepKey(indices=tuple(nt_loop_indices)), + ) ) def from_experiment( @@ -304,29 +338,31 @@ def from_experiment( self.add_connectivity_from_experiment( experiment_dao, leader_properties, clock_settings ) - self.add_acquisition_type_from_experiment(experiment_dao) + self._recipe.is_spectroscopy = is_spectroscopy(experiment_dao.acquisition_type) def add_simultaneous_acquires( self, simultaneous_acquires: Dict[float, Dict[str, str]] ): # Keys are of no interest, only order and simultaneity is important - self._recipe["experiment"]["simultaneous_acquires"] = list( - simultaneous_acquires.values() - ) + self._recipe.simultaneous_acquires = list(simultaneous_acquires.values()) def add_total_execution_time(self, total_execution_time): - self._recipe["experiment"]["total_execution_time"] = total_execution_time + self._recipe.total_execution_time = total_execution_time - def add_max_step_execution_time(self, add_max_step_execution_time): - self._recipe["experiment"][ - "max_step_execution_time" - ] = add_max_step_execution_time + def add_max_step_execution_time(self, max_step_execution_time): + self._recipe.max_step_execution_time = max_step_execution_time - def recipe(self): - return self._recipe - - def add_measurements(self, measurement_map): - for initialization in self._recipe["experiment"]["initializations"]: - device_uid = initialization["device_uid"] + def add_measurements(self, measurement_map: dict[str, list[dict]]): + for initialization in self._recipe.initializations: + device_uid = initialization.device_uid if device_uid in measurement_map: - initialization["measurements"] = measurement_map[device_uid] + initialization.measurements = [ + Measurement( + length=m.get("length"), + channel=m.get("channel"), + ) + for m in measurement_map[device_uid] + ] + + def recipe(self) -> Recipe: + return self._recipe diff --git a/laboneq/compiler/workflow/rt_linker.py b/laboneq/compiler/workflow/rt_linker.py index b973c7f..174fb30 100644 --- a/laboneq/compiler/workflow/rt_linker.py +++ b/laboneq/compiler/workflow/rt_linker.py @@ -1,9 +1,13 @@ # Copyright 2022 Zurich Instruments AG # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + from dataclasses import dataclass, field from typing import Any +import numpy as np + from laboneq.compiler.code_generator import IntegrationTimes from laboneq.compiler.code_generator.measurement_calculator import SignalDelays from laboneq.compiler.code_generator.sampled_event_handler import FeedbackConnection @@ -13,6 +17,24 @@ from laboneq.data.scheduled_experiment import PulseMapEntry +def deep_compare(a: Any, b: Any) -> bool: + if type(a) != type(b): + return False + if isinstance(a, list): + if len(a) != len(b): + return False + return all([deep_compare(_a, _b) for _a, _b in zip(a, b)]) + if isinstance(a, dict): + if len(a) != len(b): + return False + if not deep_compare(list(a.keys()), list(b.keys())): + return False + return deep_compare(list(a.values()), list(b.values())) + if isinstance(a, np.ndarray): + return np.array_equal(a, b, equal_nan=True) + return a == b + + @dataclass class RealtimeStep: device_id: str @@ -40,6 +62,7 @@ class CombinedRealtimeCompilerOutput: command_tables: list[dict[str, Any]] = field(default_factory=dict) pulse_map: dict[str, PulseMapEntry] = field(default_factory=dict) schedule: dict[str, Any] = field(default_factory=dict) + multistate_signal_groups: set[tuple[str, ...]] = field(default_factory=set) def make_seqc_name(awg: AwgKey, step_indices: list[int]) -> str: @@ -90,6 +113,7 @@ def from_single_run( wave_indices=wave_indices, pulse_map=rt_compiler_output.pulse_map, schedule=rt_compiler_output.schedule, + multistate_signal_groups=rt_compiler_output.multistate_signal_groups, ) @@ -115,7 +139,7 @@ def merge_compiler_runs( raise LabOneQException( "Signal delays do not match between real-time iterations" ) - if this.integration_weights != new.integration_weights: + if not deep_compare(this.integration_weights, new.integration_weights): # todo: this we probably want to allow in the future raise LabOneQException( "Integration weights do not match between real-time iterations" @@ -128,6 +152,10 @@ def merge_compiler_runs( raise LabOneQException( "Simultaneous acquires do not match between real-time iterations" ) + if this.multistate_signal_groups != new.multistate_signal_groups: + raise LabOneQException( + "Multistate discrimination signal groups do not match between real-time iterations" + ) for awg, awg_src in new.src.items(): seqc_name = make_seqc_name(awg, step_indices) @@ -152,7 +180,7 @@ def merge_compiler_runs( previous_src == awg_src and previous_ct == new_ct and previous_wave_indices == new_wave_indices - and previous_waves == new_waves + and deep_compare(previous_waves, new_waves) ): # No change in this iteration continue diff --git a/laboneq/contrib/example_helpers/generate_descriptor.py b/laboneq/contrib/example_helpers/generate_descriptor.py index 335fba7..1204837 100644 --- a/laboneq/contrib/example_helpers/generate_descriptor.py +++ b/laboneq/contrib/example_helpers/generate_descriptor.py @@ -35,6 +35,7 @@ def generate_descriptor( filename="yaml_descriptor", get_zsync=False, get_dio=False, + dummy_dio: dict = None, # {"DEV8XX0":"DEV2XX0"} ip_address: str = "localhost", ): """A function to generate a descriptor given a list of devices based on wiring assumptions. @@ -87,6 +88,10 @@ def generate_descriptor( listed devices to determine the connections of the ZSync cables. get_dio: If True, starts a Session to determine the connections of HDAWG to UHFQA instruments via DIO cables. + dummy_dio: Allows the user to specify a dictionary with a DIO connection + without querying the instruments with the HDAWG as the key and UHFQA as + the value + (e.g. `{"DEV8XX0": "DEV2XX0"}`). ip_address: The IP address needed to connect to the instruments if using get_zsync or get_dio. @@ -289,6 +294,9 @@ def generate_descriptor( ): print("Get DIO not supported with SHF Instruments.") return + elif get_dio and dummy_dio: + print("Can't use get_dio and dummy_dio together!") + return # Create instrument dictionary def generate_instrument_list(instrument, instrument_name): @@ -481,7 +489,7 @@ def generate_instrument_list(instrument, instrument_name): sig_dict.append( { "iq_signal": f"q{i}/drive_line", - "ports": f"[SIGOUTS/{i_hd_ch_8}, SIGOUTS/{i_hd_ch_8+1}]", + "ports": [f"SIGOUTS/{i_hd_ch_8}", f"SIGOUTS/{i_hd_ch_8+1}"], } ) i_hd_ch_8 += 2 @@ -500,7 +508,7 @@ def generate_instrument_list(instrument, instrument_name): sig_dict.append( { "iq_signal": f"q{i}/drive_line", - "ports": f"[SIGOUTS/{i_hd_ch_4}, SIGOUTS/{i_hd_ch_4+1}]", + "ports": [f"SIGOUTS/{i_hd_ch_4}", f"SIGOUTS/{i_hd_ch_4+1}"], } ) i_hd_ch_4 += 2 @@ -779,13 +787,12 @@ def generate_instrument_list(instrument, instrument_name): sig_dict.append( { "iq_signal": f"q{i}/measure_line", - "ports": f"[SIGOUTS/{i_uhfqa_ch}, SIGOUTS/{i_uhfqa_ch+1}]", + "ports": [f"SIGOUTS/{i_uhfqa_ch}", f"SIGOUTS/{i_uhfqa_ch+1}"], } ) sig_dict.append( { "acquire_signal": f"q{i}/acquire_line", - "ports": f"[SIGINS/{i_uhfqa_ch},SIGINS/{i_uhfqa_ch+1}]", } ) i_uhfqa_ch += 2 @@ -952,13 +959,12 @@ def generate_instrument_list(instrument, instrument_name): sig_dict.append( { "iq_signal": f"q{i}/measure_line", - "ports": f"[SIGOUTS/{i_uhfqa_ch}, SIGOUTS/{i_uhfqa_ch+1}]", + "ports": [f"SIGOUTS/{i_uhfqa_ch}", f"SIGOUTS/{i_uhfqa_ch+1}"], } ) sig_dict.append( { "acquire_signal": f"q{i}/acquire_line", - "ports": f"[SIGINS/{i_uhfqa_ch},SIGINS/{i_uhfqa_ch+1}]", } ) current_qubit += 1 @@ -1059,6 +1065,28 @@ def generate_instrument_list(instrument, instrument_name): for device in all_list: session.disconnect_device(device) + if hdawg_8 is not None and uhfqa is not None and dummy_dio: + for hd in hdawg_8: + sig_dict = signal_and_port_dict.setdefault(f"HDAWG_{hd}", []) + if hd in str(dummy_dio): + sig_dict.append( + { + "to": f"UHFQA_{dummy_dio[hd]}", + "port": "DIOS/0", + } + ) + + if hdawg_4 is not None and uhfqa is not None and dummy_dio: + for hd in hdawg_4: + sig_dict = signal_and_port_dict.setdefault(f"HDAWG_{hd}", []) + if hd in str(dummy_dio): + sig_dict.append( + { + "to": f"UHFQA_{dummy_dio[hd]}", + "port": "DIOS/0", + } + ) + clean_connections_dict = { "connections": {k: v for k, v in signal_and_port_dict.items() if v is not None} } diff --git a/laboneq/contrib/example_helpers/qubit_helper.py b/laboneq/contrib/example_helpers/qubit_helper.py index c22cb03..dd10798 100755 --- a/laboneq/contrib/example_helpers/qubit_helper.py +++ b/laboneq/contrib/example_helpers/qubit_helper.py @@ -1,107 +1,9 @@ # Copyright 2020 Zurich Instruments AG # SPDX-License-Identifier: Apache-2.0 -"""Simple Qubit class for simplifying device setup calibration +"""Some helper functions for using qubits. Note: The Qubit and QubitParameters classes, as well as a calibration method to derive a Calibration from the qubit parematers, are now part of the main DSL and included in laboneq.simple. """ -from copy import deepcopy - -from laboneq.core.types.enums import ModulationType -from laboneq.dsl.calibration import Calibration, Oscillator, SignalCalibration -from laboneq.dsl.experiment import pulse_library - def flatten(l): return [item for sublist in l for item in sublist] - - -def get_qubit_parameters(base_parameters, id): - parameters = deepcopy(base_parameters) - parameters["frequency"] = base_parameters["frequency"][id] - parameters["readout_frequency"] = base_parameters["readout_frequency"][id] - parameters["drive_lo_frequency"] = base_parameters["drive_lo_frequency"][id // 2] - - return parameters - - -class QubitParameters: - def __init__(self, my_parameter_dict): - for key in my_parameter_dict.keys(): - setattr(self, key, my_parameter_dict[key]) - - -class QubitPulses: - def __init__(self, id, parameters: QubitParameters): - self.qubit_x90 = pulse_library.drag( - uid=f"x90_q{id}", - length=parameters.pulse_length, - amplitude=parameters.pi_2_amplitude, - sigma=0.3, - beta=0.4, - ) - self.qubit_x180 = pulse_library.drag( - uid=f"x180_q{id}", - length=parameters.pulse_length, - amplitude=parameters.pi_amplitude, - sigma=0.3, - beta=0.4, - ) - self.readout_pulse = pulse_library.const( - uid=f"readout_pulse_q{id}", - length=parameters.readout_length, - amplitude=parameters.readout_amplitude, - ) - self.readout_weights = pulse_library.const( - uid=f"readout_weights_q{id}", - length=parameters.readout_length, - amplitude=1, - ) - - -class Qubit: - def __init__(self, id, parameter_dict): - self.id = id - - self.parameters = QubitParameters(parameter_dict) - - self.pulses = QubitPulses(self.id, self.parameters) - - -# define baseline signal calibration for a list of qubits -def define_calibration(qubits): - calib = Calibration() - - for _, qubit in enumerate(qubits): - calib[f"/logical_signal_groups/q{qubit.id}/drive_line"] = SignalCalibration( - oscillator=Oscillator( - frequency=qubit.parameters.frequency, - modulation_type=ModulationType.HARDWARE, - ), - local_oscillator=Oscillator( - frequency=qubit.parameters.drive_lo_frequency, - ), - range=qubit.parameters.drive_range, - ) - calib[f"/logical_signal_groups/q{qubit.id}/measure_line"] = SignalCalibration( - oscillator=Oscillator( - frequency=qubit.parameters.readout_frequency, - modulation_type=ModulationType.SOFTWARE, - ), - local_oscillator=Oscillator( - frequency=qubit.parameters.readout_lo_frequency, - ), - range=qubit.parameters.readout_range_out, - ) - calib[f"/logical_signal_groups/q{qubit.id}/acquire_line"] = SignalCalibration( - oscillator=Oscillator( - frequency=qubit.parameters.readout_frequency, - modulation_type=ModulationType.SOFTWARE, - ), - local_oscillator=Oscillator( - frequency=qubit.parameters.readout_lo_frequency, - ), - range=qubit.parameters.readout_range_in, - port_delay=qubit.parameters.readout_integration_delay, - ) - - return calib diff --git a/laboneq/controller/communication.py b/laboneq/controller/communication.py index 755887a..a0ad671 100644 --- a/laboneq/controller/communication.py +++ b/laboneq/controller/communication.py @@ -220,14 +220,14 @@ def __init__(self, name, server_qualifier: ServerQualifier): path = "/zi/about/version" version_str = self.batch_get([DaqNodeGetAction(self, path)])[path] try: - self._dataserver_version = LabOneVersion(version_str) - except ValueError: - err_msg = f"Version {version_str} is not supported by LabOne Q." + self._dataserver_version = LabOneVersion.cast_if_supported(version_str) + except ValueError as e: + err_msg = e.args[0] if server_qualifier.ignore_version_mismatch: _logger.warning("Ignoring that %s", err_msg) self._dataserver_version = LabOneVersion.LATEST else: - raise LabOneQControllerException(err_msg) + raise LabOneQControllerException(err_msg) from e [major, minor] = zi.__version__.split(".")[0:2] zi_python_version = f"{major}.{minor}" diff --git a/laboneq/controller/controller.py b/laboneq/controller/controller.py index a4a8709..2345a02 100644 --- a/laboneq/controller/controller.py +++ b/laboneq/controller/controller.py @@ -4,7 +4,6 @@ from __future__ import annotations import concurrent.futures -import copy import itertools import logging import os @@ -38,6 +37,7 @@ ) from laboneq.controller.results import build_partial_result, make_acquired_result from laboneq.controller.util import LabOneQControllerException +from laboneq.controller.versioning import LabOneVersion from laboneq.core.types.enums.acquisition_type import AcquisitionType from laboneq.core.types.enums.averaging_mode import AveragingMode from laboneq.core.utilities.replace_pulse import ReplacementType, calc_wave_replacements @@ -46,9 +46,6 @@ from laboneq.data.recipe import NtStepKey from laboneq.executor.execution_from_experiment import ExecutionFactoryFromExperiment from laboneq.executor.executor import Statement -from laboneq.implementation.payload_builder.convert_from_legacy_json_recipe import ( - convert_from_legacy_json_recipe, -) if TYPE_CHECKING: from laboneq.controller.devices.device_zi import DeviceZI @@ -113,6 +110,8 @@ def __init__( self._run_parameters.reset_devices, ) + self._dataserver_version: LabOneVersion | None = None + self._last_connect_check_ts: float = None # Waves which are uploaded to the devices via pulse replacements @@ -379,6 +378,8 @@ def _execute_one_step_followers(self): _logger.warning( "Conditions to start RT on followers still not fulfilled after 2" " seconds, nonetheless trying to continue..." + "\nNot fulfilled:\n%s", + response_waiter.remaining_str(), ) # Standalone workaround: The device is triggering itself, @@ -421,9 +422,11 @@ def _wait_execution_to_stop(self, acquisition_type: AcquisitionType): ( "Stop conditions still not fulfilled after %f s, estimated" " execution time was %.2f s. Continuing to the next step." + "\nNot fulfilled:\n%s" ), guarded_wait_time, min_wait_time, + response_waiter.remaining_str(), ) def _execute_one_step(self, acquisition_type: AcquisitionType): @@ -449,6 +452,16 @@ def connect(self): or now - self._last_connect_check_ts > CONNECT_CHECK_HOLDOFF ): self._devices.connect() + + try: + self._dataserver_version = next(self._devices.leaders)[ + 1 + ].daq._dataserver_version + except StopIteration: + # It may happen in emulation mode, mainly for tests + # We use LATEST in emulation mode, keeping the consistency here. + self._dataserver_version = LabOneVersion.LATEST + self._last_connect_check_ts = now def disable_outputs( @@ -482,14 +495,11 @@ def execute_compiled_legacy( compiled_experiment.experiment ) - scheduled_experiment = copy.copy(compiled_experiment.scheduled_experiment) - if isinstance(scheduled_experiment.recipe, dict): - scheduled_experiment.recipe = convert_from_legacy_json_recipe( - scheduled_experiment.recipe - ) - self._recipe_data = pre_process_compiled( - scheduled_experiment, self._devices, execution + compiled_experiment.scheduled_experiment, + self._devices, + execution, + self._dataserver_version, ) self._session = session @@ -504,6 +514,7 @@ def execute_compiled(self, job: ExecutionPayload): job.scheduled_experiment, self._devices, job.scheduled_experiment.execution, + self._dataserver_version, ) self._session = None self._execute_compiled_impl() @@ -740,13 +751,28 @@ def _read_one_step_results(self, nt_step: NtStepKey, rt_section_uid: str): ) for signal in awg_config.acquire_signals: integrator_allocation = next( - i - for i in self._recipe_data.recipe.integrator_allocations - if i.signal_id == signal + ( + i + for i in self._recipe_data.recipe.integrator_allocations + if ( + i.signal_id + if isinstance(i.signal_id, str) + else i.signal_id[0] + ) + == signal + ), + None, ) + if not integrator_allocation: + continue + is_multistate = not isinstance(integrator_allocation.signal_id, str) assert integrator_allocation.device_id == awg_key.device_uid assert integrator_allocation.awg == awg_key.awg_index - result_indices = integrator_allocation.channels + result_indices = ( + integrator_allocation.channels[0] + if is_multistate + else integrator_allocation.channels + ) raw_results = device.get_measurement_data( awg_key.awg_index, rt_execution_info.acquisition_type, diff --git a/laboneq/controller/devices/device_hdawg.py b/laboneq/controller/devices/device_hdawg.py index c199c0d..f04b552 100644 --- a/laboneq/controller/devices/device_hdawg.py +++ b/laboneq/controller/devices/device_hdawg.py @@ -365,13 +365,10 @@ def collect_initialization_nodes( try: hp = precomp["high_pass"] timeconstant = hp["timeconstant"] - clearing = {"level": 0, "rise": 1, "fall": 2, "both": 3}[ - hp["clearing"] - ] nodes += [ (hp_p + "enable", 1), (hp_p + "timeconstant", timeconstant), - (hp_p + "clearing/slope", clearing), + (hp_p + "clearing/slope", 1), ] except (KeyError, TypeError): nodes.append((hp_p + "enable", 0)) @@ -528,12 +525,12 @@ def collect_trigger_configuration_nodes( DaqNodeSetAction( self._daq, f"{awg_path}/zsync/register/shift", - awg_config.zsync_bit, + awg_config.register_selector_shift, ), DaqNodeSetAction( self._daq, f"{awg_path}/zsync/register/mask", - 0b1, + awg_config.register_selector_bitmask, ), DaqNodeSetAction( self._daq, diff --git a/laboneq/controller/devices/device_pqsc.py b/laboneq/controller/devices/device_pqsc.py index 03db30c..22f61b7 100644 --- a/laboneq/controller/devices/device_pqsc.py +++ b/laboneq/controller/devices/device_pqsc.py @@ -20,6 +20,7 @@ Response, ) from laboneq.controller.recipe_processor import DeviceRecipeData, RecipeData +from laboneq.controller.versioning import SUPPORT_PRE_V23_06, LabOneVersion from laboneq.core.types.enums.acquisition_type import AcquisitionType from laboneq.data.recipe import Initialization @@ -93,23 +94,37 @@ def configure_feedback(self, recipe_data: RecipeData) -> list[DaqNodeAction]: or awg_config.source_feedback_register is None ): continue # Only consider devices receiving feedback from PQSC - zsync_base = f"/{self.serial}/zsyncs/{p_addr}/output/registerbank" - feedback_actions.append( - DaqNodeSetAction(self.daq, f"{zsync_base}/enable", 1) + + zsync_output = f"/{self.serial}/zsyncs/{p_addr}/output" + zsync_base = f"{zsync_output}/registerbank" + if SUPPORT_PRE_V23_06 and ( + self.daq._dataserver_version < LabOneVersion.V_23_06 + ): + actions_to_enable_feedback = [ + DaqNodeSetAction(self.daq, f"{zsync_base}/enable", 1) + ] + else: + actions_to_enable_feedback = [ + DaqNodeSetAction(self.daq, f"{zsync_output}/enable", 1), + DaqNodeSetAction(self.daq, f"{zsync_output}/source", 0), + ] + + feedback_actions.extend(actions_to_enable_feedback) + reg_selector_base = ( + f"{zsync_base}/sources/{awg_config.register_selector_index}" ) - bit_base = f"{zsync_base}/sources/{awg_config.zsync_bit}" feedback_actions.extend( [ - DaqNodeSetAction(self.daq, f"{bit_base}/enable", 1), + DaqNodeSetAction(self.daq, f"{reg_selector_base}/enable", 1), DaqNodeSetAction( self.daq, - f"{bit_base}/register", + f"{reg_selector_base}/register", awg_config.source_feedback_register, ), DaqNodeSetAction( self.daq, - f"{bit_base}/index", - awg_config.feedback_register_bit, + f"{reg_selector_base}/index", + awg_config.readout_result_index, ), ] ) @@ -164,7 +179,7 @@ def collect_trigger_configuration_nodes( DaqNodeSetAction( self._daq, f"/{self.serial}/system/clocks/referenceclock/out/freq", - initialization.config.reference_clock, + initialization.config.reference_clock.value, ) ] ) diff --git a/laboneq/controller/devices/device_setup_dao.py b/laboneq/controller/devices/device_setup_dao.py index 44f71b8..3ee2197 100644 --- a/laboneq/controller/devices/device_setup_dao.py +++ b/laboneq/controller/devices/device_setup_dao.py @@ -27,7 +27,7 @@ def _make_server_qualifier( ): return ServerQualifier( dry_run=dry_run, - host=server.address, + host=server.host, port=server.port, api_level=server.api_level, ignore_version_mismatch=ignore_version_mismatch, diff --git a/laboneq/controller/devices/device_shfqa.py b/laboneq/controller/devices/device_shfqa.py index 268a384..74ae8dc 100644 --- a/laboneq/controller/devices/device_shfqa.py +++ b/laboneq/controller/devices/device_shfqa.py @@ -171,6 +171,7 @@ def on_experiment_end(self): self._daq, f"/{self.serial}/qachannels/*/spectroscopy/envelope/enable", 1, + caching_strategy=CachingStrategy.NO_CACHE, ), ] @@ -281,16 +282,32 @@ def _configure_readout( or integrator.signal_id not in awg_config.acquire_signals ): continue - assert len(integrator.channels) == 1 - integrator_idx = integrator.channels[0] - nodes_to_initialize_readout.append( - DaqNodeSetAction( - self._daq, - f"/{self.serial}/qachannels/{channel}/readout/discriminators/" - f"{integrator_idx}/threshold", - integrator.threshold, + if self._integrator_uses_multistate_discrimation(integrator): + assert self._integrator_has_consistent_msd_num_state(integrator) + num_states = len(integrator.weights) + for state_i in range(0, num_states): + channel = integrator.channels[state_i] + integrator_idx = channel[0] + nodes_to_initialize_readout.append( + DaqNodeSetAction( + self._daq, + f"/{self.serial}/qachannels/{channel[0]}/readout/multistate/qudits/" + f"{integrator_idx}/thresholds/{state_i}/value", + integrator.threshold[state_i] or 0.0, + ) + ) + + else: + assert len(integrator.channels) == 1 + integrator_idx = integrator.channels[0] + nodes_to_initialize_readout.append( + DaqNodeSetAction( + self._daq, + f"/{self.serial}/qachannels/{channel}/readout/discriminators/" + f"{integrator_idx}/threshold", + integrator.threshold or 0.0, + ) ) - ) nodes_to_initialize_readout.append( DaqNodeSetAction( self._daq, @@ -757,44 +774,132 @@ def prepare_upload_all_binary_waves( ) return waves_upload - def _configure_readout_mode_nodes( + def _integrator_has_consistent_msd_num_state( + self, integrator_allocation: IntegratorAllocation.Data + ): + num_msd_states = [ + len(integrator_allocation.weights), + len(integrator_allocation.threshold), + len(integrator_allocation.channels), + ] + if len(set(num_msd_states)) != 1: + raise LabOneQControllerException( + f"Multi discrimination configuration of experiment is not consistent. " + f"Received num weights={len(integrator_allocation.weights)}, num thresholds={len(integrator_allocation.threshold)}, num channels={len(integrator_allocation.channels)}, " + f"these three quantities need to have the same number of entries." + ) + return True + + def _integrator_uses_multistate_discrimation( + self, integrator_allocation: IntegratorAllocation.Data + ): + weights_is_msd = isinstance(integrator_allocation.weights, list) + threshold_is_msd = isinstance(integrator_allocation.threshold, list) + channels_is_msd = all( + isinstance(item, list) for item in integrator_allocation.channels + ) + msd_state = [weights_is_msd, threshold_is_msd, channels_is_msd] + if len(set(msd_state)) != 1: + raise LabOneQControllerException( + f"Multi discrimination configuration of experiment is not consistent. " + f"Received weights={integrator_allocation.weights}, thresholds={integrator_allocation.threshold}, channels={integrator_allocation.channels}, " + f"these three quantities need to either all be lists, or all be singular items" + ) + return all(msd_state) + + def _configure_readout_mode_nodes_single_state( self, - dev_input: IO, - dev_output: IO, - measurement: Measurement | None, - device_uid: str, + integrator_allocation: IntegratorAllocation.Data, recipe_data: RecipeData, + measurement: Measurement.Data, + max_len: int, ): - _logger.debug("%s: Setting measurement mode to 'Readout'.", self.dev_repr) + if len(integrator_allocation.channels) != 1: + raise LabOneQControllerException( + f"{self.dev_repr}: Internal error - expected 1 integrator for " + f"signal '{integrator_allocation.signal_id}', " + f"got {len(integrator_allocation.channels)}" + ) + integration_unit_index = integrator_allocation.channels[0] + wave_name = integrator_allocation.weights + ".wave" + weight_vector = np.conjugate( + get_wave(wave_name, recipe_data.scheduled_experiment.waves) + ) + wave_len = len(weight_vector) + if wave_len > max_len: + max_pulse_len = max_len / SAMPLE_FREQUENCY_HZ + raise LabOneQControllerException( + f"{self.dev_repr}: Length {wave_len} of the integration weight " + f"'{integration_unit_index}' of channel {measurement.channel} exceeds " + f"maximum of {max_len} samples. Ensure length of acquire kernels don't " + f"exceed {max_pulse_len * 1e6:.3f} us." + ) + node_path = ( + f"/{self.serial}/qachannels/{measurement.channel}/readout/integration/" + f"weights/{integration_unit_index}/wave" + ) - nodes_to_set_for_readout_mode = [ + return [ DaqNodeSetAction( self._daq, - f"/{self.serial}/qachannels/{measurement.channel}/readout/integration/length", - measurement.length, - ), + node_path, + weight_vector, + filename=wave_name, + caching_strategy=CachingStrategy.CACHE, + ) ] - max_len = 4096 - for integrator_allocation in recipe_data.recipe.integrator_allocations: - if ( - integrator_allocation.device_id != device_uid - or integrator_allocation.awg != measurement.channel - ): - continue - if integrator_allocation.weights is None: - # Skip configuration if no integration weights provided to keep same behavior - # TODO(2K): Consider not emitting the integrator allocation in this case. - continue + def _configure_readout_mode_nodes_multi_state( + self, + integrator_allocation: IntegratorAllocation.Data, + recipe_data: RecipeData, + measurement: Measurement.Data, + max_len: int, + ): + ret_nodes = [] + num_states = len(integrator_allocation.weights) + assert self._integrator_has_consistent_msd_num_state(integrator_allocation) + + # Note: copying this from grimsel_multistate_demo jupyter notebook + ret_nodes.append( + DaqNodeSetAction( + self._daq, + f"/{self.serial}/qachannels/{measurement.channel}/readout/multistate/qudits/*/enable", + 0, + caching_strategy=CachingStrategy.NO_CACHE, + ) + ) + ret_nodes.append( + DaqNodeSetAction( + self._daq, + f"/{self.serial}/qachannels/{measurement.channel}/readout/multistate/enable", + 1, + caching_strategy=CachingStrategy.CACHE, + ) + ) - if len(integrator_allocation.channels) != 1: + for state_i in range(0, num_states): + channel = integrator_allocation.channels[state_i] + if len(channel) != 1: raise LabOneQControllerException( f"{self.dev_repr}: Internal error - expected 1 integrator for " f"signal '{integrator_allocation.signal_id}', " - f"got {len(integrator_allocation.channels)}" + f"got {len(channel)}" + ) + integration_unit_index = channel[0] + node_path = ( + f"/{self.serial}/qachannels/{measurement.channel}/readout/multistate/qudits/" + f"{integration_unit_index}/numstates" + ) + ret_nodes.append( + DaqNodeSetAction( + self._daq, + node_path, + num_states, + caching_strategy=CachingStrategy.CACHE, ) - integration_unit_index = integrator_allocation.channels[0] - wave_name = integrator_allocation.weights + ".wave" + ) + wave_name = integrator_allocation.weights[state_i] + ".wave" weight_vector = np.conjugate( get_wave(wave_name, recipe_data.scheduled_experiment.waves) ) @@ -808,10 +913,10 @@ def _configure_readout_mode_nodes( f"exceed {max_pulse_len * 1e6:.3f} us." ) node_path = ( - f"/{self.serial}/qachannels/{measurement.channel}/readout/integration/" - f"weights/{integration_unit_index}/wave" + f"/{self.serial}/qachannels/{measurement.channel}/readout/multistate/qudits/{integration_unit_index}" + f"/weights/{state_i}/wave" ) - nodes_to_set_for_readout_mode.append( + ret_nodes.append( DaqNodeSetAction( self._daq, node_path, @@ -820,6 +925,49 @@ def _configure_readout_mode_nodes( caching_strategy=CachingStrategy.CACHE, ) ) + return ret_nodes + + def _configure_readout_mode_nodes( + self, + dev_input: IO, + dev_output: IO, + measurement: Measurement | None, + device_uid: str, + recipe_data: RecipeData, + ): + _logger.debug("%s: Setting measurement mode to 'Readout'.", self.dev_repr) + + nodes_to_set_for_readout_mode = [ + DaqNodeSetAction( + self._daq, + f"/{self.serial}/qachannels/{measurement.channel}/readout/integration/length", + measurement.length, + ), + ] + + max_len = 4096 + for integrator_allocation in recipe_data.recipe.integrator_allocations: + if ( + integrator_allocation.device_id != device_uid + or integrator_allocation.awg != measurement.channel + ): + continue + if integrator_allocation.weights is None: + # Skip configuration if no integration weights provided to keep same behavior + # TODO(2K): Consider not emitting the integrator allocation in this case. + continue + + if self._integrator_uses_multistate_discrimation(integrator_allocation): + readout_nodes = self._configure_readout_mode_nodes_multi_state( + integrator_allocation, recipe_data, measurement, max_len + ) + else: + readout_nodes = self._configure_readout_mode_nodes_single_state( + integrator_allocation, recipe_data, measurement, max_len + ) + + nodes_to_set_for_readout_mode.extend(readout_nodes) + return nodes_to_set_for_readout_mode def _configure_spectroscopy_mode_nodes( diff --git a/laboneq/controller/devices/device_shfsg.py b/laboneq/controller/devices/device_shfsg.py index 7cdf6c9..ba8eb6c 100644 --- a/laboneq/controller/devices/device_shfsg.py +++ b/laboneq/controller/devices/device_shfsg.py @@ -453,11 +453,11 @@ def collect_trigger_configuration_nodes( [ ( f"sgchannels/{awg_key.awg_index}/awg/intfeedback/direct/shift", - awg_config.feedback_register_bit, + awg_config.readout_result_index, ), ( f"sgchannels/{awg_key.awg_index}/awg/intfeedback/direct/mask", - 0b1, + awg_config.register_selector_bitmask, ), ( f"sgchannels/{awg_key.awg_index}/awg/intfeedback/direct/offset", @@ -475,11 +475,11 @@ def collect_trigger_configuration_nodes( ), ( f"sgchannels/{awg_key.awg_index}/awg/zsync/register/shift", - awg_config.zsync_bit, + awg_config.register_selector_shift, ), ( f"sgchannels/{awg_key.awg_index}/awg/zsync/register/mask", - 0b1, + awg_config.register_selector_bitmask, ), ( f"sgchannels/{awg_key.awg_index}/awg/zsync/register/offset", diff --git a/laboneq/controller/devices/device_uhfqa.py b/laboneq/controller/devices/device_uhfqa.py index b2675c8..a010818 100644 --- a/laboneq/controller/devices/device_uhfqa.py +++ b/laboneq/controller/devices/device_uhfqa.py @@ -520,7 +520,7 @@ def _configure_standard_mode_nodes( DaqNodeSetAction( self._daq, f"/{self.serial}/qas/0/thresholds/{integration_unit_index}/level", - integrator_allocation.threshold, + integrator_allocation.threshold or 0.0, ), ] ) diff --git a/laboneq/controller/devices/device_zi.py b/laboneq/controller/devices/device_zi.py index 97770ee..e06bfbd 100644 --- a/laboneq/controller/devices/device_zi.py +++ b/laboneq/controller/devices/device_zi.py @@ -44,6 +44,7 @@ from laboneq.controller.util import LabOneQControllerException from laboneq.core.types.enums.acquisition_type import AcquisitionType from laboneq.core.types.enums.averaging_mode import AveragingMode +from laboneq.core.utilities.string_sanitize import string_sanitize from laboneq.data.recipe import Initialization, IntegratorAllocation, OscillatorParam from laboneq.data.scheduled_experiment import ScheduledExperiment @@ -737,7 +738,7 @@ def prepare_command_table( def prepare_seqc( self, scheduled_experiment: ScheduledExperiment, seqc_ref: str - ) -> str: + ) -> str | None: if seqc_ref is None: return None @@ -758,6 +759,16 @@ def prepare_seqc( seqc_lines[ i ] = f"{m.group(1)}{m.group(2)}{m.group(3)}{osc.index}{m.group(4)}" + + # Substitute oscillator index by actual assignment + for osc in self._allocated_oscs: + osc_index_symbol = string_sanitize(osc.id) + pattern = re.compile(rf"const {osc_index_symbol} = \w+;") + for i, l in enumerate(seqc_lines): + if not pattern.match(l): + continue + seqc_lines[i] = f"const {osc_index_symbol} = {osc.index}; // final" + seqc_text = "\n".join(seqc_lines) return seqc_text diff --git a/laboneq/controller/near_time_runner.py b/laboneq/controller/near_time_runner.py index 7eab3a0..2225c25 100644 --- a/laboneq/controller/near_time_runner.py +++ b/laboneq/controller/near_time_runner.py @@ -31,7 +31,7 @@ class NearTimeRunner(ExecutorBase): def __init__(self, controller: Controller): - super().__init__(looping_mode=LoopingMode.EXECUTE) + super().__init__(looping_mode=LoopingMode.NEAR_TIME_ONLY) self.controller = controller self.user_set_nodes = [] self.nt_loop_indices: list[int] = [] diff --git a/laboneq/controller/recipe_processor.py b/laboneq/controller/recipe_processor.py index 1ada7ac..80228a1 100644 --- a/laboneq/controller/recipe_processor.py +++ b/laboneq/controller/recipe_processor.py @@ -17,6 +17,7 @@ DeviceAttribute, ) from laboneq.controller.util import LabOneQControllerException +from laboneq.controller.versioning import SUPPORT_PRE_V23_06, LabOneVersion from laboneq.core.types.enums.acquisition_type import AcquisitionType from laboneq.core.types.enums.averaging_mode import AveragingMode from laboneq.data.recipe import IO, Initialization, Recipe, SignalType @@ -62,8 +63,14 @@ class AwgConfig: qa_signal_id: str | None = None command_table_match_offset: int | None = None source_feedback_register: int | None = None - zsync_bit: int | None = None - feedback_register_bit: int | None = None + readout_result_index: int | None = None + readout_result_nbits: int = 2 + register_selector_index: int | None = None + register_selector_bitmask: int = 0b1 + + @property + def register_selector_shift(self): + return self.readout_result_nbits * self.register_selector_index AwgConfigs = Dict[AwgKey, AwgConfig] @@ -239,7 +246,6 @@ def __init__(self): self._loop_stack: List[_LoopStackEntry] = [] self._current_rt_uid: str = None self._current_rt_info: RtExecutionInfo = None - self._pipeline_index: int | None = None def _single_shot_axis(self) -> npt.ArrayLike: return np.linspace( @@ -283,22 +289,21 @@ def acquire_handler(self, handle: str, signal: str, parent_uid: str): def set_sw_param_handler( self, name: str, index: int, value: float, axis_name: str, values: npt.ArrayLike ): - if name == "__pipeline_index": - self._pipeline_index = value - return self._loop_stack[-1].axis_names.append(name if axis_name is None else axis_name) self._loop_stack[-1].axis_points.append(values) @contextmanager def for_loop_handler(self, count: int, index: int, loop_flags: LoopFlags): - if loop_flags & LoopFlags.PIPELINE: + if loop_flags.is_pipeline: + self._chunk_count = count yield - self._pipeline_index = None + self._chunk_count = None return - is_averaging = bool(loop_flags & LoopFlags.AVERAGE) - self._loop_stack.append(_LoopStackEntry(count=count, is_averaging=is_averaging)) - if is_averaging: + self._loop_stack.append( + _LoopStackEntry(count=count, is_averaging=loop_flags.is_average) + ) + if loop_flags.is_average: single_shot_cyclic = ( self._current_rt_info.averaging_mode == AveragingMode.SINGLE_SHOT ) @@ -360,8 +365,7 @@ def _calculate_result_shapes( def _calculate_awg_configs( - rt_execution_infos: RtExecutionInfos, - recipe: Recipe, + rt_execution_infos: RtExecutionInfos, recipe: Recipe, labone_version: LabOneVersion ) -> AwgConfigs: awg_configs: AwgConfigs = defaultdict(AwgConfig) @@ -372,7 +376,7 @@ def awg_key_by_acquire_signal(signal_id: str) -> AwgKey: if signal_id in awg_config.acquire_signals ) - def integrator_index_by_acquire_signal(signal_id: str, is_local: bool) -> int: + def readout_result_index_by_acquire_signal(signal_id: str, is_local: bool) -> int: integrator = next( ia for ia in recipe.integrator_allocations if ia.signal_id == signal_id ) @@ -382,29 +386,40 @@ def integrator_index_by_acquire_signal(signal_id: str, is_local: bool) -> int: return integrator.channels[0] * (2 if is_local else 1) for a in recipe.integrator_allocations: - awg_configs[AwgKey(a.device_id, a.awg)].acquire_signals.add(a.signal_id) + if isinstance(a.signal_id, str): + awg_configs[AwgKey(a.device_id, a.awg)].acquire_signals.add(a.signal_id) + else: + assert isinstance(a.signal_id, list) or isinstance(a.signal_id, tuple) + awg_configs[AwgKey(a.device_id, a.awg)].acquire_signals.update(a.signal_id) for initialization in recipe.initializations: device_id = initialization.device_uid + for awg in initialization.awgs or []: awg_config = awg_configs[AwgKey(device_id, awg.awg)] + + if (labone_version < LabOneVersion.V_23_06) and SUPPORT_PRE_V23_06: + awg_config.readout_result_nbits = 1 + awg_config.qa_signal_id = awg.qa_signal_id awg_config.command_table_match_offset = awg.command_table_match_offset awg_config.target_feedback_register = awg.feedback_register - zsync_bits_allocation: Dict[str, int] = defaultdict(int) + zsync_reg_selector_allocation: Dict[str, int] = defaultdict(int) for awg_key, awg_config in awg_configs.items(): if awg_config.qa_signal_id is not None: qa_awg_key = awg_key_by_acquire_signal(awg_config.qa_signal_id) feedback_register = awg_configs[qa_awg_key].target_feedback_register is_local = feedback_register is None - awg_config.feedback_register_bit = integrator_index_by_acquire_signal( + awg_config.readout_result_index = readout_result_index_by_acquire_signal( awg_config.qa_signal_id, is_local ) if not is_local: awg_config.source_feedback_register = feedback_register - awg_config.zsync_bit = zsync_bits_allocation[awg_key.device_uid] - zsync_bits_allocation[awg_key.device_uid] += 1 + awg_config.register_selector_index = zsync_reg_selector_allocation[ + awg_key.device_uid + ] + zsync_reg_selector_allocation[awg_key.device_uid] += 1 # As currently just a single RT execution per experiment is supported, # AWG configs are not cloned per RT execution. May need to be changed in the future. @@ -508,6 +523,7 @@ def pre_process_compiled( scheduled_experiment: ScheduledExperiment, devices: DeviceCollection, execution: Statement = None, + labone_version: LabOneVersion = LabOneVersion.LATEST, ) -> RecipeData: recipe = scheduled_experiment.recipe @@ -518,7 +534,8 @@ def pre_process_compiled( ) result_shapes, rt_execution_infos = _calculate_result_shapes(execution) - awg_configs = _calculate_awg_configs(rt_execution_infos, recipe) + + awg_configs = _calculate_awg_configs(rt_execution_infos, recipe, labone_version) attribute_value_tracker, oscillator_ids = _pre_process_attributes(recipe, devices) recipe_data = RecipeData( diff --git a/laboneq/controller/versioning.py b/laboneq/controller/versioning.py index 149ad57..ddb75b2 100644 --- a/laboneq/controller/versioning.py +++ b/laboneq/controller/versioning.py @@ -2,12 +2,31 @@ # SPDX-License-Identifier: Apache-2.0 from enum import Enum +from functools import total_ordering +SUPPORT_PRE_V23_06 = True + +@total_ordering class LabOneVersion(Enum): UNKNOWN = "unknown" V_23_02 = "23.02" - LATEST = V_23_02 + V_23_06 = "23.06" + LATEST = V_23_06 + + def __eq__(self, other): + return float(self.value) == float(other.value) + + def __lt__(self, other): + return float(self.value) < float(other.value) - def __le__(self, other): - return float(self.value) <= float(other.value) + @classmethod + def cast_if_supported(cls, version: str) -> "LabOneVersion": + try: + labone_version = LabOneVersion(version) + if (labone_version < cls.V_23_06) and (not SUPPORT_PRE_V23_06): + raise ValueError + except ValueError: + err_msg = f"Version {version} is not supported by LabOne Q." + raise ValueError(err_msg) + return labone_version diff --git a/laboneq/core/path.py b/laboneq/core/path.py index 3ad6bb9..faa32d1 100644 --- a/laboneq/core/path.py +++ b/laboneq/core/path.py @@ -4,8 +4,6 @@ Separator = "/" InstrumentOutputs = "$OUTPUTS" InstrumentInputs = "$INPUTS" -Results = "$RESULTS" - Instruments_Path = "instruments" Instruments_Path_Abs = Separator + Instruments_Path diff --git a/laboneq/core/types/compiled_experiment.py b/laboneq/core/types/compiled_experiment.py index 8fb90e2..7440a6a 100644 --- a/laboneq/core/types/compiled_experiment.py +++ b/laboneq/core/types/compiled_experiment.py @@ -75,11 +75,13 @@ def replace_pulse(self, pulse_uid: str | Pulse, pulse_or_array: ArrayLike | Puls experiment. Previous pulse data is lost. Args: - pulse_uid: pulse to replace, can be :py:class:`~.dsl.experiment.pulse.Pulse` + pulse_uid: + Pulse to replace, can be [Pulse][laboneq.dsl.experiment.pulse.Pulse] object or uid of the pulse pulse_or_array: replacement pulse, can be - :py:class:`~.dsl.experiment.pulse.Pulse` object or value array (see - ``sampled_pulse_*`` from the :py:mod:`~.dsl.experiment.pulse_library`) + [Pulse][laboneq.dsl.experiment.pulse.Pulse] object or + value array (see `sampled_pulse_*` from + [pulse_library][laboneq.dsl.experiment.pulse_library]) """ from laboneq.core.utilities.replace_pulse import replace_pulse diff --git a/laboneq/core/types/enums/acquisition_type.py b/laboneq/core/types/enums/acquisition_type.py index ad89188..3c40b50 100644 --- a/laboneq/core/types/enums/acquisition_type.py +++ b/laboneq/core/types/enums/acquisition_type.py @@ -4,12 +4,13 @@ from __future__ import annotations from enum import Enum -from typing import Any, Union +from typing import Any # TODO: Move to laboneq.data. Note that moving the type will cause issues when deserialising # objects that referred to the class in its old module. Moving the class is therefore # not as straight-forward as one might naively hope. +# Note(2K): We do not support ser/des across versions, so it is straightforward. class AcquisitionType(Enum): """Acquisition type @@ -33,8 +34,7 @@ class AcquisitionType(Enum): RAW: Returns raw data after ADC up to 4096 samples. Only a single raw acquire event within an averaging loop per experiment is allowed. - .. versionchanged:: 2.9 - + !!! version-changed "Changed in version 2.9" Added `SPECTROSCOPY_IQ` (same as `SPECTROSCOPY`) Added `SPECTROSCOPY_PSD` for PSD Spectroscopy mode. @@ -48,7 +48,8 @@ class AcquisitionType(Enum): RAW = "RAW" -def is_spectroscopy(obj: Union[AcquisitionType, Any]) -> bool: +# TODO(2K): Why do we need optional 'Any' here? +def is_spectroscopy(obj: AcquisitionType | Any) -> bool: return obj in ( AcquisitionType.SPECTROSCOPY, AcquisitionType.SPECTROSCOPY_IQ, diff --git a/laboneq/core/utilities/string_sanitize.py b/laboneq/core/utilities/string_sanitize.py new file mode 100644 index 0000000..0913bd2 --- /dev/null +++ b/laboneq/core/utilities/string_sanitize.py @@ -0,0 +1,32 @@ +# Copyright 2022 Zurich Instruments AG +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import functools +import hashlib +import re + + +@functools.lru_cache() +def string_sanitize(input): + """Sanitize the input string, so it can be safely used as (part of) an identifier + in seqC.""" + + # strip non-ascii characters + s = input.encode("ascii", "ignore").decode() + + if s == "": + s = "_" + + # only allowed characters are alphanumeric and underscore + s = re.sub(r"\W", "_", s) + + # names must not start with a digit + if s[0].isdigit(): + s = "_" + s + + if s != input: + s = f"{s}_{hashlib.md5(input.encode()).hexdigest()[:4]}" + + return s diff --git a/laboneq/core/utilities/validate_path.py b/laboneq/core/utilities/validate_path.py new file mode 100644 index 0000000..f815962 --- /dev/null +++ b/laboneq/core/utilities/validate_path.py @@ -0,0 +1,46 @@ +# Copyright 2019 Zurich Instruments AG +# SPDX-License-Identifier: Apache-2.0 + +import logging +import re + +controlled_nodes = [ + (r"/dev[^/]+/qachannels/\d+/oscs/0/gain", "amplitude"), + (r"/dev[^/]+/sigouts/\d+/delay", "port_delay"), + (r"/dev[^/]+/qachannels/\d+/spectroscopy/envelope/delay", "port_delay"), + (r"/dev[^/]+/qachannels/\d+/generator/delay", "port_delay"), + (r"/dev[^/]+/qachannels/\d+/spectroscopy/delay", "port_delay"), + (r"/dev[^/]+/qachannels/\d+/readout/integration/delay", "port_delay"), + (r"/dev[^/]+/qas/0/delay", "port_delay"), + (r"/dev[^/]+/sigouts/\d+/range", "range"), + (r"/dev[^/]+/sigins/\d+/range", "range"), + (r"/dev[^/]+/sgchannels/\d+/output/range", "range"), + (r"/dev[^/]+/qachannels/\d+/output/range", "range"), + (r"/dev[^/]+/qachannels/\d+/input/range", "range"), + (r"/dev[^/]+/qachannels/\d+/oscs/\d+/freq", "oscillator"), + (r"/dev[^/]+/sgchannels/\d+/oscs/\d+/freq", "oscillator"), + (r"/dev[^/]+/oscs/\d+/freq", "oscillator"), + (r"/dev[^/]+/sigouts/\d+/offset", "voltage_offset"), + (r"/dev[^/]+/sgchannels/\d+/output/rflfpath", "port_mode"), + (r"/dev[^/]+/awgs/\d+/outputs/\d+/modulation/mode", "mixer_calibration"), + (r"/dev[^/]+/awgs/\d+/outputs/\d+/gains/\d+", "mixer_calibration"), + (r"/dev[^/]+/sigouts/\d+/precompensation/.*", "precompensation"), + (r"/dev[^/]+/synthesizers/\d+/centerfreq", "local_oscillator"), + (r"/dev[^/]+/sgchannels/\d+/digitalmixer/centerfreq", "local_oscillator"), +] + +_logger = logging.getLogger(__name__) + + +def validate_path(path: str): + for pattern, calib_param in controlled_nodes: + if re.match(pattern, path.lower()): + _logger.warning( + "The instrument node '%s' you are trying to access is also controlled " + "through a calibration setting in LabOne Q - to avoid conflicts, it is " + "recommended to use the '%s' calibration property instead of a direct " + "node setting.", + path, + calib_param, + ) + break diff --git a/laboneq/data/calibration/__init__.py b/laboneq/data/calibration/__init__.py index ee3d219..d7f4355 100644 --- a/laboneq/data/calibration/__init__.py +++ b/laboneq/data/calibration/__init__.py @@ -5,7 +5,7 @@ from __future__ import annotations from dataclasses import dataclass, field -from enum import Enum, auto +from enum import Enum from numpy.typing import ArrayLike @@ -14,19 +14,19 @@ class CarrierType(EnumReprMixin, Enum): - IF = auto() - RF = auto() + IF = "if" + RF = "rf" class ModulationType(EnumReprMixin, Enum): - AUTO = auto() - HARDWARE = auto() - SOFTWARE = auto() + AUTO = "auto" + HARDWARE = "hardware" + SOFTWARE = "software" class PortMode(EnumReprMixin, Enum): - LF = auto() - RF = auto() + LF = "lf" + RF = "rf" @dataclass @@ -37,7 +37,7 @@ class BounceCompensation: @dataclass class Calibration: - calibration_items: dict[str, SignalCalibration] = field(default_factory=dict) + items: dict[str, SignalCalibration] = field(default_factory=dict) @dataclass @@ -47,11 +47,6 @@ class MixerCalibration: correction_matrix: list[list[float]] | None = None -@dataclass -class Signal: - uid: str = None - - @dataclass class ExponentialCompensation: timeconstant: float = None diff --git a/laboneq/data/compilation_job/__init__.py b/laboneq/data/compilation_job.py similarity index 81% rename from laboneq/data/compilation_job/__init__.py rename to laboneq/data/compilation_job.py index d1cba38..8f2a808 100644 --- a/laboneq/data/compilation_job/__init__.py +++ b/laboneq/data/compilation_job.py @@ -6,7 +6,6 @@ from dataclasses import dataclass, field from enum import Enum, auto -from typing import Optional, Union from numpy.typing import ArrayLike @@ -35,8 +34,9 @@ class DeviceInfoType(EnumReprMixin, Enum): HDAWG = "hdawg" SHFQA = "shfqa" SHFSG = "shfsg" - SHFQC = "shfqc" PQSC = "pqsc" + SHFPPC = "shfppc" + NONQC = "nonqc" class ReferenceClockSourceInfo(EnumReprMixin, Enum): @@ -45,9 +45,9 @@ class ReferenceClockSourceInfo(EnumReprMixin, Enum): class SignalInfoType(EnumReprMixin, Enum): - IQ = auto() - RF = auto() - INTEGRATION = auto() + IQ = "iq" + RF = "single" + INTEGRATION = "integration" # @@ -64,13 +64,20 @@ class ParameterInfo: axis_name: str | None = None +@dataclass +class FollowerInfo: + device: DeviceInfo + port: int + + @dataclass class DeviceInfo: uid: str = None device_type: DeviceInfoType = None reference_clock: float = None - reference_clock_source: ReferenceClockSourceInfo = None + reference_clock_source: ReferenceClockSourceInfo | None = None is_qc: bool | None = None + followers: list[FollowerInfo] = field(default_factory=list) @dataclass @@ -85,13 +92,12 @@ class PulseDef: uid: str = None function: str | None = None length: float = None - amplitude: float = None - phase: float = None + amplitude: float = 1.0 + phase: float = 0.0 can_compress: bool = False increment_oscillator_phase: float = None set_oscillator_phase: float = None - samples: ArrayLike = field(default_factory=list) - pulse_parameters: dict | None = None + samples: ArrayLike | None = None @dataclass @@ -99,29 +105,38 @@ class SectionInfo: uid: str = None length: float = None alignment: SectionAlignment | None = None + on_system_grid: bool = None + + children: list[SectionInfo] = field(default_factory=list) + pulses: list[SectionSignalPulse] = field(default_factory=list) + + signals: list[SignalInfo] = field(default_factory=list) + handle: str | None = None state: int | None = None local: bool | None = None + user_register: int | None = None + count: int = None chunk_count: int = 1 execution_type: ExecutionType | None = None averaging_mode: AveragingMode | None = None - acquisition_type: AcquisitionType | None = None repetition_mode: RepetitionMode | None = None repetition_time: float | None = None + + acquisition_type: AcquisitionType | None = None reset_oscillator_phase: bool = False - children: list[SectionInfo] = field(default_factory=list) - pulses: list[SectionSignalPulse] = field(default_factory=list) - on_system_grid: bool = None - trigger: list = field(default_factory=list) + triggers: list = field(default_factory=list) parameters: list[ParameterInfo] = field(default_factory=list) play_after: list[str] = field(default_factory=list) @dataclass class MixerCalibrationInfo: - voltage_offsets: tuple[float, float] = (0.0, 0.0) - correction_matrix: tuple[tuple[float, float], tuple[float, float]] = ( + voltage_offsets: tuple[float, float] | list[float] = (0.0, 0.0) + correction_matrix: tuple[tuple[float, float], tuple[float, float]] | list[ + list[float] + ] = ( (1.0, 0.0), (0.0, 1.0), ) @@ -134,6 +149,10 @@ class PrecompensationInfo: bounce: BounceCompensation | None = None FIR: FIRCompensation | None = None + computed_delay_samples: int | None = None + computed_port_delay: float | None = None + computed_delay_signal: float | None = None + @dataclass class SignalRange: @@ -143,6 +162,7 @@ class SignalRange: @dataclass class AmplifierPumpInfo: + device: DeviceInfo | None = None pump_freq: float | ParameterInfo | None = None pump_power: float | ParameterInfo | None = None cancellation: bool = True @@ -150,6 +170,7 @@ class AmplifierPumpInfo: use_probe: bool = False probe_frequency: float | ParameterInfo | None = None probe_power: float | ParameterInfo | None = None + channel: int | None = None @dataclass @@ -174,18 +195,20 @@ class SignalInfo: @dataclass class SectionSignalPulse: - section: SectionInfo = None signal: SignalInfo = None - pulse_def: PulseDef | None = None + pulse: PulseDef | None = None length: float | ParameterInfo | None = None + offset: float | ParameterInfo | None = None amplitude: float | ParameterInfo | None = None phase: float | ParameterInfo | None = None increment_oscillator_phase: float | ParameterInfo | None = None set_oscillator_phase: float | ParameterInfo | None = None precompensation_clear: bool | None = None - pulse_parameters: dict = field(default_factory=dict) + play_pulse_parameters: dict = field(default_factory=dict) + pulse_pulse_parameters: dict = field(default_factory=dict) acquire_params: AcquireInfo = None - marker: list[Marker] | None = None + markers: list[Marker] = field(default_factory=list) + pulse_group: str | None = None @dataclass diff --git a/laboneq/data/execution_payload/__init__.py b/laboneq/data/execution_payload.py similarity index 89% rename from laboneq/data/execution_payload/__init__.py rename to laboneq/data/execution_payload.py index f1cc396..f5c2871 100644 --- a/laboneq/data/execution_payload/__init__.py +++ b/laboneq/data/execution_payload.py @@ -10,18 +10,12 @@ from laboneq.data import EnumReprMixin from laboneq.data.scheduled_experiment import ScheduledExperiment +from laboneq.data.setup_description import ReferenceClockSource # # Enums # -class ServerType(EnumReprMixin, Enum): - DATA_SERVER = auto() - WEB_SERVER = auto() - SCOPE_SERVER = auto() - POWER_SWITCH_SERVER = auto() - - class TargetDeviceType(EnumReprMixin, Enum): UHFQA = auto() HDAWG = auto() @@ -38,9 +32,8 @@ class TargetDeviceType(EnumReprMixin, Enum): @dataclass class TargetServer: uid: str = None - address: str = None + host: str = None port: int = None - server_type: ServerType = None api_level: int = None @@ -70,7 +63,7 @@ class TargetDevice: calibrations: list[TargetChannelCalibration] | None = None is_qc: bool = False qc_with_qa: bool = False - reference_clock_source: str | None = None # TODO(2K): enum? bool? + reference_clock_source: ReferenceClockSource = None @dataclass diff --git a/laboneq/data/experiment_description/__init__.py b/laboneq/data/experiment_description/__init__.py index 40fa3ab..14926e3 100644 --- a/laboneq/data/experiment_description/__init__.py +++ b/laboneq/data/experiment_description/__init__.py @@ -5,43 +5,24 @@ from __future__ import annotations from dataclasses import dataclass, field -from enum import Enum, auto from typing import Any, Dict, List, Optional from numpy.typing import ArrayLike +from laboneq.core.types.enums import ( + AveragingMode, + ExecutionType, + RepetitionMode, + SectionAlignment, +) from laboneq.core.types.enums.acquisition_type import AcquisitionType -from laboneq.data import EnumReprMixin -from laboneq.data.calibration import SignalCalibration +from laboneq.data.calibration import Calibration from laboneq.data.parameter import Parameter # # Enums # - -class AveragingMode(EnumReprMixin, Enum): - CYCLIC = auto() - SEQUENTIAL = auto() - SINGLE_SHOT = auto() - - -class ExecutionType(EnumReprMixin, Enum): - NEAR_TIME = auto() - REAL_TIME = auto() - - -class RepetitionMode(EnumReprMixin, Enum): - AUTO = auto() - CONSTANT = auto() - FASTEST = auto() - - -class SectionAlignment(EnumReprMixin, Enum): - LEFT = auto() - RIGHT = auto() - - # # Data Classes # @@ -65,7 +46,6 @@ class SignalOperation(Operation): @dataclass class ExperimentSignal: uid: str = None - calibration: Optional[SignalCalibration] = None @dataclass @@ -89,9 +69,9 @@ class Section: @dataclass class Acquire(SignalOperation): handle: str = None - kernel: Pulse = None + kernel: Pulse | list[Pulse] | None = None length: float = None - pulse_parameters: Optional[Any] = None + pulse_parameters: Optional[Any] | list[Optional[Any]] = None @dataclass @@ -136,9 +116,10 @@ class Delay(SignalOperation): class Experiment: uid: str = None signals: List[ExperimentSignal] = field(default_factory=list) - epsilon: float = None sections: List[Section] = field(default_factory=list) pulses: List[Pulse] = field(default_factory=list) + #: Calibration for individual `ExperimentSignal`s. + calibration: Calibration = field(default_factory=Calibration) @dataclass @@ -159,7 +140,7 @@ class PlayPulse(SignalOperation): length: float | Parameter = None pulse_parameters: dict | None = None precompensation_clear: bool | None = None - marker: dict | Optional = None + marker: dict | None = None @dataclass diff --git a/laboneq/data/experiment_schedule/__init__.py b/laboneq/data/experiment_schedule/__init__.py deleted file mode 100644 index 03b1509..0000000 --- a/laboneq/data/experiment_schedule/__init__.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2023 Zurich Instruments AG -# SPDX-License-Identifier: Apache-2.0 - - -# __init__.py of 'experiment_schedule' package - autogenerated, do not edit -from __future__ import annotations - -from dataclasses import dataclass, field -from typing import Dict, List - -# -# Enums -# - -# -# Data Classes -# - - -@dataclass -class Event: - id: int = None - time: float = None - event_type: str = None - event_data: Dict = field(default_factory=dict) - - -@dataclass -class SectionStructure: - structure: Dict = field(default_factory=dict) - - -@dataclass -class ExperimentSchedule: - uid: str = None - events: List[Event] = field(default_factory=list) - section_structure: SectionStructure = None - experiment_hash: str = None - compiled_experiment_hash: str = None diff --git a/laboneq/data/path.py b/laboneq/data/path.py new file mode 100644 index 0000000..45f705b --- /dev/null +++ b/laboneq/data/path.py @@ -0,0 +1,5 @@ +# Copyright 2023 Zurich Instruments AG +# SPDX-License-Identifier: Apache-2.0 + +# Path separator between groups and values +Separator = "/" diff --git a/laboneq/data/recipe.py b/laboneq/data/recipe.py index 713960a..8a7cd04 100644 --- a/laboneq/data/recipe.py +++ b/laboneq/data/recipe.py @@ -16,8 +16,8 @@ class SignalType(Enum): class RefClkType(Enum): - _10MHZ = 10 - _100MHZ = 100 + _10MHZ = 10 # TODO(2K): should be 10e6 + _100MHZ = 100 # TODO(2K): should be 100e6 class TriggeringMode(Enum): @@ -32,6 +32,11 @@ class TriggeringMode(Enum): class NtStepKey: indices: tuple[int] + def __post_init__(self): + # Required for JSON deserialization, as tuples are serialized as lists. + if isinstance(self.indices, list): + object.__setattr__(self, "indices", tuple(self.indices)) + @dataclass class Gains: @@ -44,8 +49,6 @@ class IO: channel: int enable: bool | None = None modulation: bool | None = None - oscillator: int | None = None - oscillator_frequency: int | None = None offset: float | None = None gains: Gains | None = None range: float | None = None @@ -78,7 +81,7 @@ class Measurement: @dataclass class Config: repetitions: int = 1 - reference_clock: RefClkType = None + reference_clock: RefClkType = RefClkType._100MHZ holdoff: float = 0 triggering_mode: TriggeringMode = TriggeringMode.DIO_FOLLOWER sampling_rate: float | None = None @@ -87,12 +90,13 @@ class Config: @dataclass class Initialization: device_uid: str + device_type: str = None config: Config = field(default_factory=Config) - awgs: list[AWG] = None - outputs: list[IO] = None - inputs: list[IO] = None + awgs: list[AWG] = field(default_factory=list) + outputs: list[IO] = field(default_factory=list) + inputs: list[IO] = field(default_factory=list) measurements: list[Measurement] = field(default_factory=list) - ppchannels: list[dict[str, Any]] | None = None + ppchannels: list[dict[str, Any]] = field(default_factory=list) @dataclass @@ -140,3 +144,4 @@ class Recipe: simultaneous_acquires: list[dict[str, str]] = field(default_factory=list) total_execution_time: float = None max_step_execution_time: float = None + is_spectroscopy: bool = False diff --git a/laboneq/data/scheduled_experiment.py b/laboneq/data/scheduled_experiment.py index 999473f..0540207 100644 --- a/laboneq/data/scheduled_experiment.py +++ b/laboneq/data/scheduled_experiment.py @@ -4,7 +4,7 @@ from __future__ import annotations from dataclasses import dataclass, field -from enum import Enum, auto +from enum import Enum from typing import Any from laboneq.core.validators import dicts_equal @@ -12,20 +12,13 @@ from laboneq.data.recipe import Recipe -# -# Enums -# class MixerType(EnumReprMixin, Enum): #: Mixer performs full complex modulation - IQ = auto() - + IQ = "IQ" #: Mixer only performs envelope modulation (UHFQA-style) - UHFQA_ENVELOPE = auto() + UHFQA_ENVELOPE = "UHFQA_ENVELOPE" -# -# Data Classes -# @dataclass class PulseInstance: offset_samples: int diff --git a/laboneq/data/setup_description/__init__.py b/laboneq/data/setup_description/__init__.py index 2793fbc..964d83d 100644 --- a/laboneq/data/setup_description/__init__.py +++ b/laboneq/data/setup_description/__init__.py @@ -4,39 +4,44 @@ from __future__ import annotations from dataclasses import dataclass, field -from enum import Enum, auto -from typing import Any, Dict, List +from enum import Enum +from typing import Dict, List from laboneq.data import EnumReprMixin from laboneq.data.calibration import Calibration -# -# Enums -# class IODirection(EnumReprMixin, Enum): - IN = auto() - OUT = auto() + IN = "in" + OUT = "out" class ReferenceClockSource(EnumReprMixin, Enum): - EXTERNAL = auto() - INTERNAL = auto() + EXTERNAL = "external" + INTERNAL = "internal" class DeviceType(EnumReprMixin, Enum): - HDAWG = auto() - NonQC = auto() - PQSC = auto() - SHFQA = auto() - SHFSG = auto() - UHFQA = auto() - SHFQC = auto() + """Zurich Instruments device type. + + `UNMANAGED` is a Zurich Instruments device which is not directly controlled + by LabOne Q, but which can still be controlled through LabOne Q's interface + with `zhinst.toolkit`. + """ + + HDAWG = "hdawg" + PQSC = "pqsc" + SHFQA = "shfqa" + SHFSG = "shfsg" + UHFQA = "uhfqa" + SHFQC = "shfqc" + SHFPPC = "shfppc" + UNMANAGED = "unmanaged" class PhysicalChannelType(EnumReprMixin, Enum): - IQ_CHANNEL = auto() - RF_CHANNEL = auto() + IQ_CHANNEL = "iq_channel" + RF_CHANNEL = "rf_channel" class PortType(EnumReprMixin, Enum): @@ -45,23 +50,19 @@ class PortType(EnumReprMixin, Enum): ZSYNC = "ZSYNC" -# -# Data Classes -# - - -@dataclass +@dataclass(unsafe_hash=True) class LogicalSignal: name: str - group: str # Needed for referencing. TODO(MH): Remove + group: str # Needed for referencing -@dataclass +@dataclass(unsafe_hash=True) class PhysicalChannel: name: str + group: str type: PhysicalChannelType = None direction: IODirection = None - ports: List[Port] = None + ports: List[Port] = field(default_factory=list) @dataclass @@ -80,29 +81,34 @@ class ReferenceClock: @dataclass class Port: + """Instrument port.""" + path: str type: PortType @dataclass class Server: + """LabOne Dataserver.""" + + host: str + port: int + api_level: int = 6 uid: str = None - api_level: int = None - host: str = None leader_uid: str = None - port: int = None @dataclass class Instrument: - uid: str = None - interface: str = None - reference_clock: ReferenceClock = ReferenceClock() + uid: str + address: str + device_type: DeviceType + interface: str = "1GbE" + reference_clock: ReferenceClock = field(default_factory=ReferenceClock) ports: List[Port] = field(default_factory=list) physical_channels: List[PhysicalChannel] = field(default_factory=list) connections: List[ChannelMapEntry] = field(default_factory=list) - address: str = None - device_type: DeviceType = None + # For ZI devices, the address is the device serial number. server: Server = None @@ -114,6 +120,12 @@ class LogicalSignalGroup: @dataclass class SetupInternalConnection: + """Connections between ports on two devices. + + That is, ports that are connected to each other, rather + than something to be controlled or measured. + """ + from_instrument: Instrument from_port: Port to_instrument: Instrument diff --git a/laboneq/data/setup_description/setup_helper.py b/laboneq/data/setup_description/setup_helper.py index d1b53d6..c427c59 100644 --- a/laboneq/data/setup_description/setup_helper.py +++ b/laboneq/data/setup_description/setup_helper.py @@ -1,12 +1,8 @@ # Copyright 2023 Zurich Instruments AG # SPDX-License-Identifier: Apache-2.0 -import logging - from laboneq.data.setup_description import Setup -_logger = logging.getLogger(__name__) - class SetupHelper: """ diff --git a/laboneq/data/utils/__init__.py b/laboneq/data/utils/__init__.py new file mode 100644 index 0000000..17c557a --- /dev/null +++ b/laboneq/data/utils/__init__.py @@ -0,0 +1,2 @@ +# Copyright 2023 Zurich Instruments AG +# SPDX-License-Identifier: Apache-2.0 diff --git a/laboneq/data/utils/calibration_helper.py b/laboneq/data/utils/calibration_helper.py new file mode 100644 index 0000000..2ac7409 --- /dev/null +++ b/laboneq/data/utils/calibration_helper.py @@ -0,0 +1,31 @@ +# Copyright 2023 Zurich Instruments AG +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from laboneq.data.calibration import Calibration, SignalCalibration +from laboneq.data.path import Separator +from laboneq.data.setup_description import LogicalSignal + + +class CalibrationHelper: + """A helper class for Calibration.""" + + def __init__(self, calibration: Calibration): + if calibration is not None and calibration.items: + self._items = calibration.items + else: + self._items = {} + + def empty(self) -> bool: + """Check whether the calibration is empty.""" + if not self._items: + return True + return False + + def by_logical_signal( + self, logical_signal: LogicalSignal + ) -> SignalCalibration | None: + """Return `SignalCalibration` object for the given `logical_signal`.""" + signal = Separator.join((logical_signal.group, logical_signal.name)) + return self._items.get(signal, None) diff --git a/laboneq/dsl/_inspect.py b/laboneq/dsl/_inspect.py index 5b48fdd..2e3fd5d 100644 --- a/laboneq/dsl/_inspect.py +++ b/laboneq/dsl/_inspect.py @@ -47,7 +47,7 @@ def estimate_runtime(self) -> float: raise NotImplementedError( "Execution time can only be inspected for CompiledExperiment objects." ) - exec_time = self._compiled_exp.recipe["experiment"]["total_execution_time"] + exec_time = self._compiled_exp.recipe.total_execution_time return exec_time diff --git a/laboneq/dsl/calibration/calibration.py b/laboneq/dsl/calibration/calibration.py index 4cd1a78..60c9e52 100644 --- a/laboneq/dsl/calibration/calibration.py +++ b/laboneq/dsl/calibration/calibration.py @@ -1,6 +1,8 @@ # Copyright 2022 Zurich Instruments AG # SPDX-License-Identifier: Apache-2.0 +"""A container for calibration items.""" + from dataclasses import dataclass, field from typing import Any, Dict @@ -20,10 +22,20 @@ def _sanitize_key(key: Any) -> str: @classformatter @dataclass(init=True, repr=True, order=True) class Calibration: - """Calibration object containing a dict of :class:`~.CalibrationItem`. - - The dictionary has the path i.e. UID to the :py:class:`~.Calibratable` object as - key and the actual :py:class:`~.CalibrationItem` object as value. + """Calibration object containing a dictionary of + [CalibrationItem][laboneq.dsl.calibration.CalibrationItem]s. + + Parameters: + calibration_items: + A mapping to initialize the dictionary of + calibration items. + + Attributes: + calibration_items: + mapping from a UID of a + [Calibratable][laboneq.dsl.calibration.Calibratable] to + the actual + [CalibrationItem][laboneq.dsl.calibration.CalibrationItem]. """ calibration_items: Dict[str, CalibrationItem] = field(default_factory=dict) @@ -58,10 +70,11 @@ def values(self): return self.calibration_items.values() @staticmethod - def load(filename): + def load(filename: str): """Load calibration data from file. - The file is in JSON format, as generated via :meth:`save()`. + The file is in JSON format, as generated via + [.save()][laboneq.dsl.calibration.Calibration.save]. Args: filename: The filename to load data from. @@ -71,8 +84,7 @@ def load(filename): return Serializer.from_json_file(filename, Calibration) - def save(self, filename): - + def save(self, filename: str): """Save calibration data to file. The file is written in JSON format. diff --git a/laboneq/dsl/calibration/calibration_item.py b/laboneq/dsl/calibration/calibration_item.py index da8f47b..950c26b 100644 --- a/laboneq/dsl/calibration/calibration_item.py +++ b/laboneq/dsl/calibration/calibration_item.py @@ -3,4 +3,4 @@ class CalibrationItem: - pass + """A base class for calibration items.""" diff --git a/laboneq/dsl/calibration/precompensation.py b/laboneq/dsl/calibration/precompensation.py index 940aa1e..646667e 100644 --- a/laboneq/dsl/calibration/precompensation.py +++ b/laboneq/dsl/calibration/precompensation.py @@ -19,7 +19,7 @@ def precompensation_id_generator(): global precompensation_id - retval = f"mc{precompensation_id}" + retval = f"pc{precompensation_id}" precompensation_id += 1 return retval @@ -40,8 +40,7 @@ class ExponentialCompensation(Observable): class HighPassCompensation(Observable): """Data object containing highpass filter parameters for the signal precompensation. - .. versionchanged:: 2.8 - + !!! version-changed "Changed in version 2.8" Deprecated `clearing` argument: It has no functionality. """ diff --git a/laboneq/dsl/calibration/signal_calibration.py b/laboneq/dsl/calibration/signal_calibration.py index caeff52..98e1bed 100644 --- a/laboneq/dsl/calibration/signal_calibration.py +++ b/laboneq/dsl/calibration/signal_calibration.py @@ -19,13 +19,14 @@ @dataclass(init=False, order=True) class SignalCalibration(Observable): """Dataclass containing all calibration parameters - and settings related to a :class:`~.LogicalSignal`. + and settings related to a + [LogicalSignal][laboneq.dsl.device.io_units.logical_signal.LogicalSignal]. """ - #: The oscillator assigned to the :class:`~.LogicalSignal` + #: The oscillator assigned to the [LogicalSignal][laboneq.dsl.device.io_units.logical_signal.LogicalSignal] #: - determines the frequency and type of modulation for any pulses played back on this line. oscillator: Oscillator | None - #: The local oscillator assigned to the :class:`~.LogicalSignal` + #: The local oscillator assigned to the [LogicalSignal][laboneq.dsl.device.io_units.logical_signal.LogicalSignal] #: - sets the center frequency of the playback - only relevant on SHFSG, SHFQA and SHFQC local_oscillator: Oscillator | None #: Settings to enable the optional mixer calibration correction diff --git a/laboneq/dsl/device/_device_setup_generator.py b/laboneq/dsl/device/_device_setup_generator.py index 18d76fc..c5a599c 100644 --- a/laboneq/dsl/device/_device_setup_generator.py +++ b/laboneq/dsl/device/_device_setup_generator.py @@ -356,23 +356,6 @@ def make_device( elif signal_type_keyword == T_RF_SIGNAL: raise LabOneQException(f"RF signal not supported on {uid}.") - device_connections.extend( - [ - Connection( - local_port="I_measured", - remote_path="$RESULTS", - remote_port="0", - signal_type=IOSignalType.I, - ), - Connection( - local_port="Q_measured", - remote_path="$RESULTS", - remote_port="1", - signal_type=IOSignalType.Q, - ), - ] - ) - return UHFQA( **_skip_nones( server_uid=server_finder(uid), @@ -504,18 +487,6 @@ def make_device( elif signal_type_keyword == T_RF_SIGNAL: raise LabOneQException(f"RF signal not supported on {uid}.") - if len(device_connections) > 0: - device_connections.extend( - [ - Connection( - local_port="IQ_measured", - remote_path="$RESULTS", - remote_port="0", - signal_type=IOSignalType.IQ, - ) - ] - ) - if len(device_connections) == 0: return None @@ -994,7 +965,6 @@ def _path_to_signal(path): def _create_physical_channel( ports: List[str], signal_type_token: str, device_id, physical_signals ) -> Optional[PhysicalChannel]: - if signal_type_token in (T_IQ_SIGNAL, T_ACQUIRE_SIGNAL): channel_type = PhysicalChannelType.IQ_CHANNEL elif signal_type_token == T_RF_SIGNAL: @@ -1254,7 +1224,6 @@ def server_finder(device_uid: str) -> str: ) for device, channels in physical_signals.items() } - device_setup_constructor_args = { "uid": setup_name, "servers": {server.uid: server for server, _ in servers}, diff --git a/laboneq/dsl/device/device_setup.py b/laboneq/dsl/device/device_setup.py index 9189ec6..3710db4 100644 --- a/laboneq/dsl/device/device_setup.py +++ b/laboneq/dsl/device/device_setup.py @@ -197,10 +197,12 @@ def get_calibration(self, path=None) -> Calibration: """Retrieve the calibration of a specific path. Args: - path: Path of the calibration information. default = None. + path (str): + Path of the calibration information. Returns: - Calibration object of the device setup. + calibration: + Calibration object of the device setup. """ if path is not None: return self._get_calibration(path) @@ -354,14 +356,19 @@ def from_dicts( """Construct the device setup from Python dicts, same structure as yaml Args: - instrument_list (dict): List of instruments in the setup (deprecated; for + instrument_list (dict): + List of instruments in the setup (deprecated; for backwards compatibility) - instruments (dict): List of instruments in the setup - connections (dict): Connections between devices - filepath (str): Path to the YAML file containing the device description. - server_host (str): Server host of the setup that should be created. - server_port (str): Port of the server that should be created. - setup_name (str): Name of the setup that should be created. + instruments (dict): + List of instruments in the setup + connections (dict): + Connections between devices + server_host: + Server host of the setup that should be created. + server_port: + Port of the server that should be created. + setup_name: + Name of the setup that should be created. """ return DeviceSetup( **_DeviceSetupGenerator.from_dicts( diff --git a/laboneq/dsl/device/device_setup_helper.py b/laboneq/dsl/device/device_setup_helper.py index 9f6551b..dc4f1b0 100644 --- a/laboneq/dsl/device/device_setup_helper.py +++ b/laboneq/dsl/device/device_setup_helper.py @@ -20,7 +20,7 @@ def upload_wiring(api_url, wiring_text): wiring_text (str): Json-like string contains wiring information. Returns: - status code: 200 if succeeded. + status_code (int): 200 if succeeded. """ with requests.Session() as session: @@ -38,7 +38,7 @@ def upload_wiring_from_descriptor(api_url, descriptor): descriptor (str): yaml-like text contains wiring information. Returns: - status code: 200 if succeeded. + status_code (int): 200 if succeeded. """ res = yaml.safe_load(descriptor) return DeviceSetupHelper.upload_wiring(api_url, json.dumps(res, indent=4)) @@ -51,7 +51,7 @@ def delete_wiring(api_url): api_url (str): URL of the monitoring server. http://localhost:9005/slugname/wiring Returns: - status code: 200 if succeeded. + status_code (int): 200 if succeeded. """ with requests.Session() as session: response = session.delete(api_url, timeout=5) @@ -67,7 +67,8 @@ def download_wiring(api_url): api_url (str): URL of the monitoring server. http://localhost:9005/slugname/wiring Returns: - the GET content if succeeded. + wiring (str): + the GET content if succeeded. """ with requests.Session() as session: diff --git a/laboneq/dsl/device/instruments/shfqa.py b/laboneq/dsl/device/instruments/shfqa.py index f901e8b..28f305f 100644 --- a/laboneq/dsl/device/instruments/shfqa.py +++ b/laboneq/dsl/device/instruments/shfqa.py @@ -62,14 +62,4 @@ def ports(self): connector_labels=[f"Signal Output {ch+1}"], ) ) - for i in range(16): - outputs.append( - Port( - IODirection.OUT, - uid=f"QACHANNELS/{ch}/RESULT/{i}", - signal_type=IOSignalType.IQ, - physical_port_ids=[], - connector_labels=[], - ) - ) return inputs + outputs diff --git a/laboneq/dsl/device/instruments/uhfqa.py b/laboneq/dsl/device/instruments/uhfqa.py index 13e3706..b2412a2 100644 --- a/laboneq/dsl/device/instruments/uhfqa.py +++ b/laboneq/dsl/device/instruments/uhfqa.py @@ -55,25 +55,4 @@ def ports(self): connector_labels=["Signal Output 2"], ), ] - - for i in range(10): - outputs.append( - Port( - IODirection.OUT, - uid=f"QAS/{i}/RESULT/0", - signal_type=IOSignalType.I, - physical_port_ids=[], - connector_labels=[], - ) - ) - outputs.append( - Port( - IODirection.OUT, - uid=f"QAS/{i}/RESULT/1", - signal_type=IOSignalType.Q, - physical_port_ids=[], - connector_labels=[], - ) - ) - return inputs + outputs diff --git a/laboneq/dsl/experiment/__init__.py b/laboneq/dsl/experiment/__init__.py index fbadb3c..55754c1 100644 --- a/laboneq/dsl/experiment/__init__.py +++ b/laboneq/dsl/experiment/__init__.py @@ -11,4 +11,4 @@ from .pulse import PulseFunctional, PulseSampledComplex, PulseSampledReal from .reserve import Reserve from .section import AcquireLoopNt, AcquireLoopRt, Case, Match, Section, Sweep -from .set import Set +from .set_node import SetNode diff --git a/laboneq/dsl/experiment/acquire.py b/laboneq/dsl/experiment/acquire.py index 22cf408..a8226e1 100644 --- a/laboneq/dsl/experiment/acquire.py +++ b/laboneq/dsl/experiment/acquire.py @@ -1,8 +1,10 @@ # Copyright 2022 Zurich Instruments AG # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + from dataclasses import dataclass, field -from typing import Any, Dict, Optional +from typing import Any from ..dsl_dataclass_decorator import classformatter from .operation import Operation @@ -14,17 +16,19 @@ class Acquire(Operation): """Class representing an acquire operation that is used to acquire results.""" - #: Unique identifier of the signal where the result should be acquired. - signal: str = field(default=None) + #: Unique identifier of the signal(s) where the result should be acquired. + signal: str | list[str] = field(default=None) #: Unique identifier of the handle that will be used to access the acquired result. handle: str = field(default=None) - #: Pulse used for the acquisition integration weight (only valid in integration mode). - kernel: Pulse = field(default=None) + #: Pulse(s) used for the acquisition integration weight (only valid in integration mode). + kernel: Pulse | list[Pulse] | None = field(default=None) #: Integration length (only valid in spectroscopy mode). - length: float = field(default=None) + length: float | None = field(default=None) #: Optional (re)binding of user pulse parameters - pulse_parameters: Optional[Dict[str, Any]] = field(default=None) + pulse_parameters: dict[str, Any] | list[dict[str, Any] | None] | None = field( + default=None + ) diff --git a/laboneq/dsl/experiment/builtins.py b/laboneq/dsl/experiment/builtins.py index dd655f3..71f090c 100644 --- a/laboneq/dsl/experiment/builtins.py +++ b/laboneq/dsl/experiment/builtins.py @@ -156,11 +156,11 @@ def reserve(signal): def acquire( - signal: str, + signal: str | list[str], handle: str, - kernel: Pulse = None, - length: float = None, - pulse_parameters: dict[str, Any] | None = None, + kernel: Pulse | list[Pulse] | None = None, + length: float | None = None, + pulse_parameters: dict[str, Any] | list[dict[str, Any]] | None = None, ): return active_section().acquire( signal=signal, @@ -172,10 +172,10 @@ def acquire( def measure( - acquire_signal: str, + acquire_signal: str | list[str], handle: str, - integration_kernel: Pulse | None = None, - integration_kernel_parameters: dict[str, Any] | None = None, + integration_kernel: Pulse | list[Pulse] | None = None, + integration_kernel_parameters: dict[str, Any] | list[dict[str, Any]] | None = None, integration_length: float | None = None, measure_signal: str | None = None, measure_pulse: Pulse | None = None, @@ -210,7 +210,7 @@ def add(section: Section): def set_node(path: str, value: Any): - return active_section().set(path=path, value=value) + return active_section().set_node(path=path, value=value) def sweep_range(start, stop, count, uid=None, axis_name=None, **kwargs): diff --git a/laboneq/dsl/experiment/experiment.py b/laboneq/dsl/experiment/experiment.py index 7ac2e0e..811974c 100644 --- a/laboneq/dsl/experiment/experiment.py +++ b/laboneq/dsl/experiment/experiment.py @@ -76,17 +76,18 @@ def add_signal( ) -> ExperimentSignal: """Add an experiment signal to the experiment. - :param uid: The unique id of the new experiment signal (optional). - :type uid: UID = str - :param connect_to: - The `LogicalSignal` this experiment signal shall be connected to. - Defaults to None, meaning that there is no connection defined yet. - :type connect_to: :class:`~.LogicalSignal`, optional + Args: + uid: + The unique id of the new experiment signal (optional). + connect_to: + The `LogicalSignal` this experiment signal shall be connected to. + Defaults to None, meaning that there is no connection defined yet. - :return: The created and added signal. - :rtype: :class:`~.ExperimentSignal` + Returns: + signal: + The created and added signal. - .. seealso:: :func:`map_signal` + See also [map_signal][laboneq.dsl.experiment.experiment.Experiment.map_signal]. """ if uid is not None and uid in self.signals.keys(): raise LabOneQException(f"Signal with id {uid} already exists.") @@ -96,7 +97,9 @@ def add_signal( def add(self, section: Section): """Add a sweep, a section or an acquire loop to the experiment. - :param section: The object to add. + + Args: + section: The object to add. """ self._add_section_to_current_section(section) @@ -106,16 +109,24 @@ def experiment_signals_uids(self): return self.signals.keys def list_experiment_signals(self) -> List[ExperimentSignal]: - """A list of experiment signals defined in this experiment.""" + """A list of experiment signals defined in this experiment. + + Returns: + signals: List of defined experiment signals. + """ return list(self.signals.values()) def is_experiment_signal(self, uid: str) -> bool: """Check if an experiment signal is defined for this experiment. - :param uid: The unique id of the experiment signal to check for. - :type uid: UID = str - :return (bool): `True` if the experiment signal is defined in this - experiment, `False` otherwise. + Args: + uid: + The unique id of the experiment signal to check for. + + Returns: + is_experiment_signal: + `True` if the experiment signal is defined in this + experiment, `False` otherwise. """ return uid in self.signals.keys() @@ -123,16 +134,16 @@ def map_signal(self, experiment_signal_uid: str, logical_signal: LogicalSignalRe """Connect an experiment signal to a logical signal. In order to relate an experiment signal to a logical signal defined in a - device setup (:class:`~.DeviceSetup`), you need to make - a connection between these two types of signals. + device setup ([DeviceSetup][laboneq.dsl.device.device_setup.DeviceSetup]), + you need to make a connection between these two types of signals. - :param experiment_signal_uid: The unique id of the experiment signal to - be connected. - :type experiment_signal_uid: `UID = str` - :param logical_signal: The logical signal to connect to. - :type logical_signal: :class:`~.LogicalSignal` + Args: + experiment_signal_uid: + The unique id of the experiment signal to be connected. + logical_signal: + The logical signal to connect to. - .. seealso:: :func:`add_signal` + See also [add_signal][laboneq.dsl.experiment.experiment.Experiment.add_signal]. """ if not self.is_experiment_signal(experiment_signal_uid): raise LabOneQException( @@ -144,7 +155,12 @@ def map_signal(self, experiment_signal_uid: str, logical_signal: LogicalSignalRe self.signals[experiment_signal_uid].map(logical_signal) def reset_signal_map(self, signal_map: Dict[str, LogicalSignalRef] = None): - """Reset i.e. disconnect all defined signal connections.""" + """Reset, i.e. disconnect, all defined signal connections and + apply a new signal map if provided. + + Args: + signal_map: The new signal map to apply. + """ for signal in self.signals.values(): signal.disconnect() @@ -155,14 +171,19 @@ def reset_signal_map(self, signal_map: Dict[str, LogicalSignalRef] = None): def signal_mapping_status(self) -> Dict[str, Any]: """Get an overview of the signal mapping. - :return: A dictionary with entries for: - - - `is_all_mapped`: - `True` if all experiment signals are mapped to a logical signal, `False` otherwise. - - `mapped_signals`: - A list of experiment signal uids that have a mapping to a logical signal. - - `not_mapped_signals`: - A list of experiment signal uids that have no mapping to a logical signal. + Returns: + signal_mapping: + A dictionary with entries for: + + - `is_all_mapped`: + `True` if all experiment signals are mapped to a logical + signal, `False` otherwise. + - `mapped_signals`: + A list of experiment signal uids that have a mapping to a + logical signal. + - `not_mapped_signals`: + A list of experiment signal uids that have no mapping to a + logical signal. """ is_all_mapped = True mapped_signals = list() @@ -252,12 +273,12 @@ def set_node(self, path: str, value: Any): path: Path to the node whose value should be set. value: Value that should be set. - .. versionchanged:: 2.0 + !!! version-changed "Changed in version 2.0" Method name renamed from `set` to `set_node`. Removed `key` argument. """ current_section = self._peek_section() - current_section.set(path=path, value=value) + current_section.set_node(path=path, value=value) def play( self, @@ -274,38 +295,38 @@ def play( ): """Play a pulse on a signal line. - :param signal: The unique id of the signal to play the pulse on. - :type signal: `str` - :param pulse: The pulse description of the pulse to be played. - :type pulse: :class:`~.experiment.pulse.Pulse` - :param amplitude: Amplitude the pulse shall be played with. Defaults to - `None`, meaning that the pulse is played as is. - :type amplitude: `float` or :class:`~.dsl.parameter.Parameter`, optional - :param length: Length for which the pulse shall be played. Defaults to - `None`, meaning that the pulse is played for its whole length. - :type length: `float` or :class:`~.dsl.parameter.Parameter`, optional - :param phase: The desired baseband phase (baseband rotation) with which the - pulse shall be played. Given in radians, defaults to `None`, - meaning that the pulse is played with its phase as defined. - :type phase: `float`, optional - :param set_oscillator_phase: The desired oscillator phase at the start of the played pulse, in radians. - The phase setting affects the pulse played in this command, and all following pulses. - Defaults to `None`, meaning no change is made and the phase remains continuous. - :type set_oscillator_phase: `float`, optional - :param increment_oscillator_phase: The desired phase increment of the oscillator phase - at the start of the played pulse, in radians. - The new, incremented phase affects the pulse played in this command, and all following pulses. - Defaults to `None`, meaning no change is made and the phase remains continuous. - :type increment_oscillator_phase: `float`, optional - :param pulse_parameters: Dictionary with user pulse function parameters (re)binding. - :type pulse_parameters: `dict`, optional - :param marker: Dictionary with markers to play. Example: `marker={"marker1": {"enable": True}}` - :type marker: `dict`, optional - - .. note:: - - If markers are specified but `pulse=None`, a zero amplitude pulse as long as the end of the longest - marker will be automatically generated. + Args: + signal (str): + The unique id of the signal to play the pulse on. + pulse (Pulse): + The pulse description of the pulse to be played. + amplitude (float | Parameter): + Amplitude the pulse shall be played with. Defaults to + `None`, meaning that the pulse is played as is. + length (float | Parameter): + Length for which the pulse shall be played. Defaults to + `None`, meaning that the pulse is played for its whole length. + phase (float): + The desired baseband phase (baseband rotation) with which the + pulse shall be played. Given in radians, defaults to `None`, + meaning that the pulse is played with its phase as defined. + set_oscillator_phase (float): + The desired oscillator phase at the start of the played pulse, in radians. + The phase setting affects the pulse played in this command, and all following pulses. + Defaults to `None`, meaning no change is made and the phase remains continuous. + increment_oscillator_phase (float): + The desired phase increment of the oscillator phase + at the start of the played pulse, in radians. + The new, incremented phase affects the pulse played in this command, and all following pulses. + Defaults to `None`, meaning no change is made and the phase remains continuous. + pulse_parameters (dict): + Dictionary with user pulse function parameters (re)binding. + marker (dict): + Dictionary with markers to play. Example: `marker={"marker1": {"enable": True}}` + + !!! note + If markers are specified but `pulse=None`, a zero amplitude pulse as long as the end of the longest + marker will be automatically generated. """ current_section = self._peek_section() current_section.play( @@ -329,12 +350,14 @@ def delay( ): """Delay execution of next operation on the given experiment signal. - :param signal: The unique id of the signal to delay execution of next - operation. - :param time: The delay time in seconds. The parameter can either be - given as a float or as a sweep parameter - (:class:`~.dsl.parameter.Parameter`). - :param precompensation_clear: Clear the precompensation filter during this delay. + Args: + signal: + The unique id of the signal to delay execution of next operation. + time: + The delay time in seconds. The parameter can either be + given as a float or as a sweep parameter. + precompensation_clear: + Clear the precompensation filter during this delay. """ current_section = self._peek_section() current_section.delay( @@ -348,28 +371,31 @@ def reserve(self, signal): operation defined on that signal, it is not available for other sections as long as the active section is scoped. - :param signal: The unique id of the signal to be reserved in the active - section. - :type signal: `str` + Args: + signal: + The unique id of the signal to be reserved in the active + section. """ current_section = self._peek_section() current_section.reserve(signal=signal) def acquire( self, - signal: str, + signal: str | list[str], handle: str, - kernel: Pulse = None, - length: float = None, - pulse_parameters: Optional[Dict[str, Any]] = None, + kernel: Pulse | list[Pulse] | None = None, + length: float | None = None, + pulse_parameters: dict[str, Any] | list[dict[str, Any] | None] | None = None, ): - """Acquire a signal and make it available in :class:`Result`. + """Acquire a signal and make it available in [Result][laboneq.dsl.result.results.Results]. Args: - signal: The input signal to acquire data on. - handle: A unique identifier string that allows to retrieve the - acquired data in the :class:`Result` object. - kernel: Pulse for filtering the acquired signal. + signal: The input signal(s) to acquire data on. + handle: + A unique identifier string that allows to retrieve the + acquired data in the [Result][laboneq.dsl.result.results.Results] + object. + kernel: Pulse(s) for filtering the acquired signal. length: Integration length for spectroscopy mode. pulse_parameters: Dictionary with user pulse function parameters (re)binding. """ @@ -384,10 +410,12 @@ def acquire( def measure( self, - acquire_signal: str, + acquire_signal: str | list[str], handle: str, - integration_kernel: Optional[Pulse] = None, - integration_kernel_parameters: Optional[Dict[str, Any]] = None, + integration_kernel: Optional[Pulse | list[Pulse]] = None, + integration_kernel_parameters: Optional[ + dict[str, Any] | list[dict[str, Any] | None] + ] = None, integration_length: Optional[float] = None, measure_signal: Optional[str] = None, measure_pulse: Optional[Pulse] = None, @@ -404,14 +432,15 @@ def measure( For pulsed spectroscopy, set `integration_length` and either `measure_pulse` or `measure_pulse_length`. For CW spectroscopy, set only `integration_length` and do not specify the measure signal. + For multistate discrimination, use lists of equal length for acquire_signal, integration_kernel and integration_kernel_parameters. For all other measurements, set either length or pulse for both the measure pulse and integration kernel. Args: - acquire_signal: A string that specifies the signal for the data acquisition. + acquire_signal: A string or list of strings that specifies the signal(s) for the data acquisition. handle: A string that specifies the handle of the acquired results. - integration_kernel: An optional Pulse object that specifies the kernel for integration. - integration_kernel_parameters: An optional dictionary that contains pulse parameters for the integration kernel. + integration_kernel: An optional Pulse object or list of Pulse objects that specifies the kernel(s) for integration. + integration_kernel_parameters: An optional dictionary (or list thereof) that contains pulse parameters for the integration kernel. integration_length: An optional float that specifies the integration length. measure_signal: An optional string that specifies the signal to measure. measure_pulse: An optional Pulse object that specifies the readout pulse for measurement. @@ -467,12 +496,15 @@ def sweep( ): """Define a sweep section. - Sections need to open a scope in the following way:: + Sections need to open a scope in the following way: + ``` py with exp.sweep(...): # here come the operations that shall be executed in the sweep section + ``` - :note: A near time section cannot be defined in the scope of a real + !!! note + A near time section cannot be defined in the scope of a real time section. Args: @@ -482,9 +514,10 @@ def sweep( of sweep parameters of equal length. If multiple sweep parameters are given, the parameters are executed in parallel in this sweep loop. execution_type: Defines if the sweep is executed in near time or - real time. Defaults to :class:`.~ExecutionType.NEAR_TIME`. + real time. Defaults to + [ExecutionType.NEAR_TIME][laboneq.core.types.enums.execution_type.ExecutionType.NEAR_TIME]. alignment: Alignment of the operations in the section. Defaults to - :class:`.~SectionAlignment.LEFT`. + [SectionAlignment.LEFT][laboneq.core.types.enums.section_alignment.SectionAlignment.LEFT]. reset_oscillator_phase: When True, reset all oscillators at the start of each step. chunk_count: The number of chunks to split the sweep into. Defaults to 1. @@ -551,19 +584,22 @@ def __exit__(self, exc_type, exc_val, exc_tb): def acquire_loop_nt(self, count, averaging_mode=AveragingMode.CYCLIC, uid=None): """Define an acquire section with averaging in near time. - Sections need to open a scope in the following way:: + Sections need to open a scope in the following way: + ``` py with exp.acquire_loop_nt(...): # here come the operations that shall be executed in the acquire_loop_nt section + ``` - :note: A near time section cannot be defined in the scope of a real + !!! note + A near time section cannot be defined in the scope of a real time section. Args: uid: The unique ID for this section. count: The number of acquire iterations. averaging_mode: The mode of how to average the acquired data. - Defaults to :attr:`~.AveragingMode.CYCLIC`. + Defaults to [AveragingMode.CYCLIC][laboneq.core.types.enums.averaging_mode.AveragingMode.CYCLIC]. """ return Experiment._AcquireLoopNtSectionContext( self, uid=uid, count=count, averaging_mode=averaging_mode @@ -595,31 +631,41 @@ def acquire_loop_rt( ): """Define an acquire section with averaging in real time. - Sections need to open a scope in the following way:: + Sections need to open a scope in the following way: + ``` py with exp.acquire_loop_rt(...): # here come the operations that shall be executed in the acquire_loop_rt section + ``` - :note: A near time section cannot be defined in the scope of a real + !!! note + A near time section cannot be defined in the scope of a real time section. Args: uid: The unique ID for this section. count: The number of acquire iterations. averaging_mode: The mode of how to average the acquired data. - Defaults to :attr:`.AveragingMode.CYCLIC`. - Further options: :attr:`.AveragingMode.SEQUENTIAL` and :attr:`.AveragingMode.SINGLE_SHOT`. + Defaults to [AveragingMode.CYCLIC][laboneq.core.types.enums.averaging_mode.AveragingMode.CYCLIC]. + Further options: [AveragingMode.SEQUENTIAL][laboneq.core.types.enums.averaging_mode.AveragingMode.SEQUENTIAL] + and [AveragingMode.SINGLE_SHOT][laboneq.core.types.enums.averaging_mode.AveragingMode.SINGLE_SHOT]. Single shot measurements are always averaged in cyclic mode. repetition_mode: Defines the shot repetition mode. Defaults to - :attr:`.RepetitionMode.FASTEST`. Further options are - :attr:`.RepetitionMode.CONSTANT` and :attr:`.RepetitionMode.AUTO`. + [RepetitionMode.FASTEST][laboneq.core.types.enums.repetition_mode.RepetitionMode.FASTEST]. + Further options are + [RepetitionMode.CONSTANT][laboneq.core.types.enums.repetition_mode.RepetitionMode.CONSTANT] + and [RepetitionMode.AUTO][laboneq.core.types.enums.repetition_mode.RepetitionMode.AUTO]. repetition_time: This is the shot repetition time in sec. This - argument is only required and valid if :param:repetition_mode is - :class:`RepetitionMode.CONSTANT`. The parameter can either be - given as a float or as a sweep parameter (:class:`~.dsl.parameter.Parameter`). + argument is only required and valid if `repetition_mode` is + [RepetitionMode.CONSTANT][laboneq.core.types.enums.repetition_mode.RepetitionMode.CONSTANT]. + The parameter can either be given as a float or as a sweep parameter + ([Parameter][laboneq.dsl.parameter.Parameter]). acquisition_type: This is the acquisition type. - Defaults to :attr:`.AcquisitionType.INTEGRATION`. Further options are - :attr:`.AcquisitionType.SPECTROSCOPY`, :attr:`.AcquisitionType.DISCRIMINATION` and :attr:`.AcquisitionType.RAW`. + Defaults to [AcquisitionType.INTEGRATION][laboneq.core.types.enums.acquisition_type.AcquisitionType.INTEGRATION]. + Further options are + [AcquisitionType.SPECTROSCOPY][laboneq.core.types.enums.acquisition_type.AcquisitionType.SPECTROSCOPY], + [AcquisitionType.DISCRIMINATION][laboneq.core.types.enums.acquisition_type.AcquisitionType.DISCRIMINATION] + and [AcquisitionType.RAW][laboneq.core.types.enums.acquisition_type.AcquisitionType.RAW]. reset_oscillator_phase: When True, the phase of every oscillator is reset at the start of the each step of the acquire loop. """ @@ -677,12 +723,15 @@ def section( ): """Define an section for scoping operations. - Sections need to open a scope in the following way:: + Sections need to open a scope in the following way: + ``` py with exp.section(...): # here come the operations that shall be executed in the section + ``` - :note: A near time section cannot be defined in the scope of a real + !!! note + A near time section cannot be defined in the scope of a real time section. Args: @@ -693,9 +742,9 @@ def section( Defaults to `None` which means that the section length is derived automatically from the contained operations. The parameter can either be given as a float or as a sweep - parameter (:class:`~.Parameter`). + parameter ([Parameter][laboneq.dsl.parameter.Parameter]). alignment: Alignment of the operations in the section. Defaults to - :class:`~.SectionAlignment.LEFT`. + [SectionAlignment.LEFT][laboneq.core.types.enums.section_alignment.SectionAlignment.LEFT]. play_after: Play this section after the end of the section(s) with the given ID(s). Defaults to None. trigger: Play a pulse a trigger pulse for the duration of this section. @@ -706,31 +755,35 @@ def section( The individual trigger (a.k.a marker) ports on the device are addressed via the experiment signal that is mapped to the corresponding analog port. For playing trigger pulses, pass a dictionary via the `trigger` argument. The - keys of the dictionary must be an ID of an :class:`~.ExperimentSignal`. Each - value is another ``dict`` of the form: :: + keys of the dictionary must be an ID of an + [ExperimentSignal][laboneq.dsl.experiment.experiment_signal.ExperimentSignal]. + Each value is another `dict` of the form: + ``` py {"state": value} + ``` - ``value`` is a bit field that enables the individual trigger signals (on the + `value` is a bit field that enables the individual trigger signals (on the devices that feature more than a single one). - .. code-block:: python - + ``` py {"state": 1} # raise trigger signal 1 {"state": 0b10} # raise trigger signal 2 (on supported devices) {"state": 0b11} # raise both trigger signals + ``` As a more complete example, to fire a trigger pulse on the first port associated - with signal ``"drive_line"``, call :: + with signal `"drive_line"`, call: + ``` py with exp.section(..., trigger={"drive_line": {"state": 0b01}}): ... + ``` When trigger signals on the same signal are issued in nested sections, the values are ORed. - .. versionchanged:: 2.0.0 - + !!! version-changed "Changed in version 2.0.0" Removed deprecated `offset` argument. """ return Experiment._SectionSectionContext( @@ -838,12 +891,15 @@ def match_local( """Define a section which switches between different child sections based on a QA measurement on an SHFQC. - Match needs to open a scope in the following way:: + Match needs to open a scope in the following way: + ``` py with exp.match_local(...): # here come the different branches to be selected + ``` - :note: Only subsections of type ``Case`` are allowed. + !!! note + Only subsections of type `Case` are allowed. Args: uid: The unique ID for this section. @@ -871,12 +927,15 @@ def match_global( """Define a section which switches between different child sections based on a QA measurement via the PQSC. - Match needs to open a scope in the following way:: + Match needs to open a scope in the following way: + ``` py with exp.match_global(...): # here come the different branches to be selected + ``` - :note: Only subsections of type ``Case`` are allowed. + !!! note + Only subsections of type `Case` are allowed. Args: uid: The unique ID for this section. @@ -931,19 +990,21 @@ def match( play_after: Optional[Union[str, Section, List[Union[str, Section]]]] = None, ): """Define a section which switches between different child sections based - on a QA measurement (using ``handle``) or a user register (using ``user_register``). + on a QA measurement (using `handle`) or a user register (using `user_register`). In case of the QA measurement option, the feedback path (local, or global, via PQSC) is chosen automatically. - Match needs to open a scope in the following way:: + Match needs to open a scope in the following way: + ``` py with exp.match(...): # here come the different branches to be selected + ``` - :note: - Only subsections of type ``Case`` are allowed. Exactly one of ``handle`` or - ``user_register`` must be specified, the other one must be None. The user register + !!! note + Only subsections of type `Case` are allowed. Exactly one of `handle` or + `user_register` must be specified, the other one must be None. The user register is evaluated only at the beginning of the experiment, not during the experiment, and only a few user registers per AWG can be used due to the limited number of processor registers. @@ -970,12 +1031,16 @@ def case(self, state: int, uid: str = None): """Define a section which plays after matching with the given value to the result of a QA measurement. - Case needs to open a scope in the following way:: + Case needs to open a scope in the following way: + ``` py with exp.case(...): # here come the operations that shall be executed in the section + ``` - :note: No subsections are allowed, only :meth:`play` and :meth:`delay`. + !!! note + No subsections are allowed, only [play][laboneq.dsl.experiment.experiment.Experiment.play] + and [delay][laboneq.dsl.experiment.experiment.Experiment.delay]. Args: uid: The unique ID for this section. diff --git a/laboneq/dsl/experiment/pulse_library.py b/laboneq/dsl/experiment/pulse_library.py index 902e524..e951157 100644 --- a/laboneq/dsl/experiment/pulse_library.py +++ b/laboneq/dsl/experiment/pulse_library.py @@ -18,11 +18,11 @@ def register_pulse_functional(sampler: Callable, name: str = None): The sampler function must have the following signature: - .. code-block:: python + ``` py def sampler(x: ndarray, **pulse_params: Dict[str, Any]) -> ndarray: pass - + ``` The vector ``x`` marks the points where the pulse function is to be evaluated. The values of ``x`` range from -1 to +1. The argument ``pulse_params`` contains all @@ -39,26 +39,26 @@ def sampler(x: ndarray, **pulse_params: Dict[str, Any]) -> ndarray: amplitude and length. - Args - - sampler (Callable): the function used for sampling the pulse - - - name (str): the name used internally for referring to this pulse type - - Returns - A factory function for new :py:class:`~.Pulse` objects. The return value has the - following signature: - - .. code-block:: python - - def ( - uid: str = None, - length: float = 100e-9, - amplitude: float = 1.0, - **pulse_parameters: Dict[str, Any], - ): - pass - + Args: + sampler: + the function used for sampling the pulse + name: + the name used internally for referring to this pulse type + Returns: + pulse_factory (function): + A factory function for new ``Pulse`` objects. + The return value has the following signature: + ``` py + + def ( + uid: str = None, + length: float = 100e-9, + amplitude: float = 1.0, + **pulse_parameters: Dict[str, Any], + ): + pass + ``` """ if name is None: function_name = sampler.__name__ @@ -112,7 +112,7 @@ def gaussian(x, sigma=1 / 3, zero_boundaries=False, **_): zero_boundaries (bool): Whether to zero the pulse at the boundaries Returns: - Gaussian pulse. + pulse (Pulse): Gaussian pulse. """ gauss = np.exp(-(x**2) / (2 * sigma**2)) if zero_boundaries: @@ -139,7 +139,7 @@ def gaussian_square( zero_boundaries (bool): Whether to zero the pulse at the boundaries Returns: - Gaussian square pulse. + pulse (Pulse): Gaussian square pulse. """ risefall_in_samples = round(len(x) * (1 - width / length) / 2) @@ -171,7 +171,7 @@ def const(x, **_): amplitude (float): Amplitude of the pulse Returns: - Constant pulse. + pulse (Pulse): Constant pulse. """ return np.ones_like(x) @@ -186,7 +186,7 @@ def triangle(x, **_): amplitude (float): Amplitude of the pulse Returns: - Triangle pulse. + pulse (Pulse): Triangle pulse. """ return 1 - np.abs(x) @@ -201,7 +201,7 @@ def sawtooth(x, **_): amplitude (float): Amplitude of the pulse Returns: - Sawtooth pulse. + pulse (Pulse): Sawtooth pulse. """ return 0.5 * (1 - x) @@ -220,7 +220,7 @@ def drag(x, sigma=1 / 3, beta=0.2, zero_boundaries=False, **_): zero_boundaries (bool): Whether to zero the pulse at the boundaries Returns: - DRAG pulse. + pulse (Pulse): DRAG pulse. """ gauss = np.exp(-(x**2) / (2 * sigma**2)) delta = 0 @@ -242,7 +242,7 @@ def cos2(x, **_): amplitude (float): Amplitude of the pulse Returns: - Raised cosine pulse. + pulse (Pulse): Raised cosine pulse. """ return np.cos(x * np.pi / 2) ** 2 @@ -251,11 +251,11 @@ def sampled_pulse_real(samples, uid=None, can_compress=False): """Create a pulse based on a array of real values. Args: - samples: Real valued data. - uid: Unique identifier of the created pulse. + samples (numpy.ndarray): Real valued data. + uid (str): Unique identifier of the created pulse. Returns: - Pulse based on the provided sample values. + pulse (Pulse): Pulse based on the provided sample values. """ if uid is None: return PulseSampledReal(samples=samples, can_compress=can_compress) @@ -267,11 +267,11 @@ def sampled_pulse_complex(samples, uid=None, can_compress=False): """Create a pulse based on a array of complex values. Args: - samples: Complex valued data. - uid: Unique identifier of the created pulse. + samples (numpy.ndarray): Complex valued data. + uid (str): Unique identifier of the created pulse. Returns: - Pulse based on the provided sample values. + pulse (Pulse): Pulse based on the provided sample values. """ if uid is None: return PulseSampledComplex(samples=samples, can_compress=can_compress) diff --git a/laboneq/dsl/experiment/section.py b/laboneq/dsl/experiment/section.py index ef3d4f3..e585e40 100644 --- a/laboneq/dsl/experiment/section.py +++ b/laboneq/dsl/experiment/section.py @@ -7,6 +7,7 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union from laboneq import dsl +from laboneq._utils import id_generator from laboneq.core.exceptions import LabOneQException from laboneq.core.types.enums import SectionAlignment from laboneq.core.validators import validating_allowed_values @@ -25,8 +26,7 @@ from .operation import Operation from .play_pulse import PlayPulse from .reserve import Reserve -from .set import Set -from .utils import id_generator +from .set_node import SetNode if TYPE_CHECKING: from .. import Parameter @@ -40,7 +40,7 @@ class Section: at the same time). Operations within a section can be aligned in various ways (left, right). Sections can have a offset and/or a predefined length, and they can be specified to play after another section. - .. versionchanged:: 2.0.0 + !!! version-changed "Changed in version 2.0.0" Removed `offset` member variable. """ @@ -65,18 +65,19 @@ class Section: default_factory=list ) - #: Optional trigger pulses to play during this section. See :meth:`~.Experiment.section`. + #: Optional trigger pulses to play during this section. + #: See [Experiment.section][laboneq.dsl.experiment.experiment.Experiment.section]. trigger: Dict[str, Dict] = field(default_factory=dict) #: Whether to escalate to the system grid even if tighter alignment is possible. - #: See :meth:`~.Experiment.section`. + #: See [Experiment.section][laboneq.dsl.experiment.experiment.Experiment.section]. on_system_grid: Optional[bool] = field(default=False) def __post_init__(self): if self.uid is None: self.uid = id_generator("s") - def add(self, section: Union[Section, Operation, Set]): + def add(self, section: Union[Section, Operation, SetNode]): """Add a subsection or operation to the section. Args: @@ -85,25 +86,26 @@ def add(self, section: Union[Section, Operation, Set]): self.children.append(section) @property - def sections(self) -> Tuple[Section]: + def sections(self) -> Tuple[Section, ...]: """A list of subsections of this section""" return tuple([s for s in self.children if isinstance(s, Section)]) @property - def operations(self) -> Tuple[Operation]: + def operations(self) -> Tuple[Operation, ...]: """A list of operations in the section. - Note that there may be other children of a section which are not operations but subsections.""" + Note that there may be other children of a section which are not operations but subsections. + """ return tuple([s for s in self.children if isinstance(s, Operation)]) - def set(self, path: str, value: Any): + def set_node(self, path: str, value: Any): """Set the value of an instrument node. Args: path: Path to the node whose value should be set. value: Value that should be set. """ - self.add(Set(path=path, value=value)) + self.add(SetNode(path=path, value=value)) def play( self, @@ -157,11 +159,11 @@ def reserve(self, signal): def acquire( self, - signal: str, + signal: str | list[str], handle: str, - kernel: Pulse = None, - length: float = None, - pulse_parameters: Optional[Dict[str, Any]] = None, + kernel: Pulse | list[Pulse] | None = None, + length: float | None = None, + pulse_parameters: dict[str, Any] | list[dict[str, Any] | None] | None = None, ): """Acquisition of results of a signal. @@ -184,10 +186,12 @@ def acquire( def measure( self, - acquire_signal: str, + acquire_signal: str | List[str], handle: str, - integration_kernel: Optional[Pulse] = None, - integration_kernel_parameters: Optional[Dict[str, Any]] = None, + integration_kernel: Optional[Pulse | list[Pulse]] = None, + integration_kernel_parameters: Optional[ + Dict[str, Any] | List[Dict[str, Any]] + ] = None, integration_length: Optional[float] = None, measure_signal: Optional[str] = None, measure_pulse: Optional[Pulse] = None, @@ -225,8 +229,14 @@ def measure( acquire_delay: An optional float that specifies the delay between the acquisition and the measurement. reset_delay: An optional float that specifies the delay after the acquisition to allow for state relaxation or signal processing. """ - if not isinstance(acquire_signal, str): - raise TypeError("`acquire_signal` must be a string.") + if not ( + isinstance(acquire_signal, str) + or ( + isinstance(acquire_signal, list) + and all(isinstance(s, str) for s in acquire_signal) + ) + ): + raise TypeError("`acquire_signal` must be a string or a list of strings.") if measure_signal is None: self.acquire( @@ -286,7 +296,7 @@ def call(self, func_name, **kwargs): Args: func_name (Union[str, Callable]): Function that should be called. - kwargs: Arguments of the function call. + kwargs (dict): Arguments of the function call. """ self.add(Call(func_name=func_name, **kwargs)) @@ -317,7 +327,8 @@ class AcquireLoopRt(Section): execution_type: ExecutionType = field(default=ExecutionType.REAL_TIME) #: Repetition method. One of fastest, constant and auto. repetition_mode: RepetitionMode = field(default=RepetitionMode.FASTEST) - #: The repetition time, when :py:attr:`repetition_mode` is :py:attr:`~.RepetitionMode.CONSTANT` + #: The repetition time, when `repetition_mode` is + #: [RepetitionMode.CONSTANT][laboneq.core.types.enums.repetition_mode.RepetitionMode.CONSTANT]. repetition_time: float = field(default=None) #: When True, reset all oscillators at the start of every step. reset_oscillator_phase: bool = field(default=False) diff --git a/laboneq/dsl/experiment/section_context.py b/laboneq/dsl/experiment/section_context.py index 5e2fe16..ed95d5e 100644 --- a/laboneq/dsl/experiment/section_context.py +++ b/laboneq/dsl/experiment/section_context.py @@ -231,7 +231,7 @@ def __enter__(self): return super().__enter__() -def active_section(): +def active_section() -> Section: s = peek_context() if s is None or not isinstance(s, SectionContextBase): raise LabOneQException("Must be in a section context") diff --git a/laboneq/dsl/experiment/set.py b/laboneq/dsl/experiment/set_node.py similarity index 67% rename from laboneq/dsl/experiment/set.py rename to laboneq/dsl/experiment/set_node.py index ff4ee8b..f854654 100644 --- a/laboneq/dsl/experiment/set.py +++ b/laboneq/dsl/experiment/set_node.py @@ -1,26 +1,28 @@ # Copyright 2022 Zurich Instruments AG # SPDX-License-Identifier: Apache-2.0 -from dataclasses import dataclass, field +from dataclasses import dataclass from typing import Any +from laboneq.core.utilities.validate_path import validate_path + from ..dsl_dataclass_decorator import classformatter from .operation import Operation @classformatter @dataclass(init=True, repr=True, order=True) -class Set(Operation): +class SetNode(Operation): """Operation that sets a value at a node.""" #: Path to the node whose value should be set. - path: str = field(default=None) - #: Key of the node that should be set. - key: str = field(default=None) + path: str = None #: Value that should be set. - value: Any = field(default=None) + value: Any = None def __post_init__(self): + if self.path is not None: + validate_path(self.path) if hasattr(self.value, "uid"): self.value = self.value.uid diff --git a/laboneq/dsl/experiment/utils.py b/laboneq/dsl/experiment/utils.py deleted file mode 100644 index 99c7135..0000000 --- a/laboneq/dsl/experiment/utils.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright 2023 Zurich Instruments AG -# SPDX-License-Identifier: Apache-2.0 - -from collections import defaultdict -from itertools import count - -_iid_map = defaultdict(count) - - -def id_generator(cat: str = "") -> str: - """Incremental IDs for each category.""" - global _iid_map - return f"_{cat}_{next(_iid_map[cat])}" diff --git a/laboneq/dsl/quantum/quantum_element.py b/laboneq/dsl/quantum/quantum_element.py index c2ad017..0dcf49f 100644 --- a/laboneq/dsl/quantum/quantum_element.py +++ b/laboneq/dsl/quantum/quantum_element.py @@ -214,15 +214,17 @@ def calibration(self) -> Calibration: def experiment_signals( self, - with_types=False, - with_calibration=False, + with_types: bool = False, + with_calibration: bool = False, ) -> Union[List[ExperimentSignal], List[Tuple[SignalType, ExperimentSignal]]]: """Experiment signals of the quantum element. Args: - with_types: When true, return a list of tuples which consist of a mapped logical signal - type and an experiment signal. Otherwise, just return the experiment signals. - with_calibration: Apply the qubit's calibration to the ExperimentSignal. + with_types: + When true, return a list of tuples which consist of a mapped logical signal + type and an experiment signal. Otherwise, just return the experiment signals. + with_calibration: + Apply the qubit's calibration to the ExperimentSignal. """ if not with_calibration: diff --git a/laboneq/dsl/quantum/quantum_operation.py b/laboneq/dsl/quantum/quantum_operation.py index 9b28fcb..5c3fd45 100644 --- a/laboneq/dsl/quantum/quantum_operation.py +++ b/laboneq/dsl/quantum/quantum_operation.py @@ -28,7 +28,7 @@ def __init__( Args: uid: A unique identifier for the quantum operation. - lookup: A dictionary of sections associated with quantum elements. + operation_map: A dictionary of sections associated with quantum elements. """ if uid is None: self.uid = uuid.uuid4().hex diff --git a/laboneq/dsl/quantum/transmon.py b/laboneq/dsl/quantum/transmon.py index 72f6059..610e5d5 100644 --- a/laboneq/dsl/quantum/transmon.py +++ b/laboneq/dsl/quantum/transmon.py @@ -127,10 +127,12 @@ def calibration(self, set_local_oscillators=True) -> Calibration: `Qubit` requires `parameters` for it to be able to produce calibration objects. Args: - set_local_oscillators: if True, adds local oscillator settings to the calibration. + set_local_oscillators (bool): + If True, adds local oscillator settings to the calibration. Returns: - Prefilled calibration object from Qubit parameters. + calibration: + Prefilled calibration object from Qubit parameters. """ if set_local_oscillators: diff --git a/laboneq/dsl/result/results.py b/laboneq/dsl/result/results.py index 59b770d..e681e8f 100644 --- a/laboneq/dsl/result/results.py +++ b/laboneq/dsl/result/results.py @@ -72,7 +72,7 @@ def get_result(self, handle: str) -> AcquiredResult: handle (str): The handle assigned to an 'acquire' event in the experiment definition. Returns: - An object of type :class:`~.dsl.result.acquired_result.AcquiredResult`. + result: The acquire event result. Raises: LabOneQException: No result is available for the provided handle. @@ -172,8 +172,8 @@ def get_last_nt_step(self, handle: str) -> list[int]: def device_calibration(self) -> Calibration | None: """Get the device setup's calibration. - See Also: - :py:meth:`DeviceSetup.get_calibration() ` + See also + [DeviceSetup.get_calibration][laboneq.dsl.device.device_setup.DeviceSetup.get_calibration]. """ if self.device_setup is None: return None diff --git a/laboneq/dsl/session.py b/laboneq/dsl/session.py index 8668ae3..9b3e0fb 100644 --- a/laboneq/dsl/session.py +++ b/laboneq/dsl/session.py @@ -41,21 +41,24 @@ class ConnectionState: class Session: """This Session class represents the main endpoint for the user interaction with the QCCS system. - The session holds - * the wiring definition of the devices - * the experiment definition that should be run on the devices - * the calibration of the devices for experiment - * the compiled experiment - * the result of the executed experiment - - The Session is a stateful object that hold all of the above. The expected steps to interact with the session are: - * initial state (construction) - * setting the device setup (optionally during construction) - * (optional) setting the calibration of the devices - * connecting to the devices (or the emulator) - * compiling the experiment - * running the experiment - * accessing the results of the last run experiment + The session holds: + + * the wiring definition of the devices + * the experiment definition that should be run on the devices + * the calibration of the devices for experiment + * the compiled experiment + * the result of the executed experiment + + The Session is a stateful object that hold all of the above. + The expected steps to interact with the session are: + + * initial state (construction) + * setting the device setup (optionally during construction) + * (optional) setting the calibration of the devices + * connecting to the devices (or the emulator) + * compiling the experiment + * running the experiment + * accessing the results of the last run experiment The session is serializable in every state. """ @@ -64,8 +67,8 @@ def __init__( self, device_setup: DeviceSetup = None, log_level: int = logging.INFO, - performance_log=False, - configure_logging=True, + performance_log: bool = False, + configure_logging: bool = True, _last_results=None, compiled_experiment=None, experiment=None, @@ -83,9 +86,9 @@ def __init__( When True, the system creates a separate logfile containing logs aimed to analyze system performance. configure_logging: Whether to configure logger. Can be disabled for custom logging use cases. - .. versionchanged:: 2.0 - Removed `pass_v3_to_compiler` argument. - Removed `max_simulation_time` instance variable. + !!! version-changed "Changed in version 2.0" + - Removed `pass_v3_to_compiler` argument. + - Removed `max_simulation_time` instance variable. """ self._device_setup = device_setup if device_setup else DeviceSetup() self._controller: Controller = None @@ -115,10 +118,12 @@ def devices(self) -> ToolkitDevices: Usage: + ``` pycon >>> session.connect() >>> session.devices["device_hdawg"].awgs[0].outputs[0].amplitude(1) >>> session.devices["DEV1234"].awgs[0].outputs[0].amplitude() 1 + ``` """ return self._toolkit_devices @@ -194,7 +199,7 @@ def connect( It is suggested to keep the versions aligned and up-to-date to avoid any unexpected behaviour. - .. versionchanged:: 2.4 + !!! version-changed "Changed in version 2.4" Renamed `ignore_lab_one_version_error` to `ignore_version_mismatch` and include LabOne and device firmware version compatibility check. @@ -274,13 +279,12 @@ def compile( experiment: Experiment instance that should be compiled. compiler_settings: Extra options passed to the compiler. - .. versionchanged:: 2.4 - + !!! version-changed "Changed in version 2.4" Raises error if `Session` is not connected. - .. versionchanged:: 2.0 - - Removed `do_simulation` argument. Use :class:`~.OutputSimulator` instead. + !!! version-changed "Changed in version 2.0" + Removed `do_simulation` argument. + Use [OutputSimulator][laboneq.simulator.output_simulator.OutputSimulator] instead. """ self._assert_connected(fail=True) self._experiment_definition = experiment @@ -311,9 +315,9 @@ def run( If an experiment is specified, the provided experiment is assigned to the internal experiment of the session. - .. versionchanged:: 2.0 - - Removed `do_simulation` argument. Use :class:`~.OutputSimulator` instead. + !!! version-changed "Changed in version 2.0" + Removed `do_simulation` argument. + Use [OutputSimulator][laboneq.simulator.output_simulator.OutputSimulator] instead. Args: experiment: Optional. Experiment instance that should be @@ -325,8 +329,7 @@ def run( A `Results` object in case of success. `None` if the session is not connected. - .. versionchanged:: 2.4 - + !!! version-changed "Changed in version 2.4" Raises error if `Session` is not connected. """ self._assert_connected(fail=True) @@ -408,10 +411,13 @@ def get_results(self) -> Results: @property def results(self) -> Results: """ - Object holding the result of the last experiment execution. Attention! This accessor is provided for better - performance, unlike 'get_result' it doesn't make a copy, but instead returns the reference to the live - result object being updated during the session run. Care must be taken for not modifying this object from - the user code, otherwise behavior is undefined. + Object holding the result of the last experiment execution. + + !!! Attention + This accessor is provided for better + performance, unlike `get_result` it doesn't make a copy, but instead returns the reference to the live + result object being updated during the session run. Care must be taken for not modifying this object from + the user code, otherwise behavior is undefined. """ return self._last_results @@ -601,8 +607,9 @@ def load_compiled_experiment(self, filename): """Loads a compiled experiment from a given file into the session. Args: - filename (str): Filename (full path) of the experiment should be loaded - into the session. + filename (str): + Filename (full path) of the experiment should be loaded + into the session. """ self._compiled_experiment = CompiledExperiment.load(filename) @@ -611,8 +618,9 @@ def save_compiled_experiment(self, filename): """Saves the compiled experiment from the session into a given file. Args: - filename (str): Filename (full path) of the file where the experiment - should be stored in. + filename (str): + Filename (full path) of the file where the experiment + should be stored in. """ if self._compiled_experiment is None: self.logger.info( diff --git a/laboneq/executor/execution_from_experiment.py b/laboneq/executor/execution_from_experiment.py index 3ab27bb..8fe2c1c 100644 --- a/laboneq/executor/execution_from_experiment.py +++ b/laboneq/executor/execution_from_experiment.py @@ -6,7 +6,6 @@ from typing import TYPE_CHECKING, List, Union from laboneq.core.exceptions import LabOneQException -from laboneq.core.types.enums import ExecutionType from laboneq.executor import executor if TYPE_CHECKING: @@ -44,7 +43,11 @@ def _handle_children( self._handle_children, child.children, child.uid ) self._append_statement( - executor.ForLoop(child.count, loop_body, executor.LoopFlags.AVERAGE) + executor.ForLoop( + count=child.count, + body=loop_body, + loop_flags=executor.LoopFlags.AVERAGE, + ) ) elif isinstance(child, AcquireLoopRt): loop_body = self._sub_scope( @@ -64,15 +67,7 @@ def _handle_children( elif isinstance(child, Sweep): count = len(child.parameters[0].values) loop_body = self._sub_scope(self._handle_sweep, child) - loop_type = ( - executor.LoopFlags.HARDWARE - if child.execution_type == ExecutionType.REAL_TIME - else executor.LoopFlags.SWEEP - ) - chunk_count = child.chunk_count - self._append_statement( - executor.ForLoop(count, loop_body, loop_type, chunk_count) - ) + self._append_statement(executor.ForLoop(count=count, body=loop_body)) else: sub_sequence = self._sub_scope( self._handle_children, child.children, child.uid @@ -90,11 +85,18 @@ def _statement_from_param(self, parameter: SweepParameter): ) def _statement_from_operation(self, operation, parent_uid: str): - from laboneq.dsl.experiment import Acquire, Call, Delay, PlayPulse, Reserve, Set + from laboneq.dsl.experiment import ( + Acquire, + Call, + Delay, + PlayPulse, + Reserve, + SetNode, + ) if isinstance(operation, Call): return executor.ExecUserCall(operation.func_name, operation.args) - if isinstance(operation, Set): + if isinstance(operation, SetNode): return executor.ExecSet(operation.path, operation.value) if isinstance(operation, PlayPulse): return executor.Nop() @@ -108,17 +110,9 @@ def _statement_from_operation(self, operation, parent_uid: str): def _make_pipelined(self, averaging_loop: executor.Statement): return executor.ForLoop( - self._chunked_sweep.chunk_count, - executor.Sequence( - [ - executor.SetSoftwareParamLinear( - "__pipeline_index", 0, 1, "pipeline_index" - ), - averaging_loop, - ] - ), - executor.LoopFlags.PIPELINE, - 1, + count=self._chunked_sweep.chunk_count, + body=averaging_loop, + loop_flags=executor.LoopFlags.PIPELINE, ) @staticmethod diff --git a/laboneq/executor/executor.py b/laboneq/executor/executor.py index 1ee0e56..0164603 100644 --- a/laboneq/executor/executor.py +++ b/laboneq/executor/executor.py @@ -6,7 +6,7 @@ from abc import ABC, abstractmethod from contextlib import contextmanager from enum import Enum, Flag, auto -from typing import Any, Dict, Iterator, List +from typing import Any, Iterator import numpy as np import numpy.typing as npt @@ -19,16 +19,19 @@ class LoopFlags(Flag): NONE = 0 AVERAGE = auto() - HARDWARE = auto() PIPELINE = auto() - # convenience aliases - SWEEP = NONE # !AVERAGE - RT_AVERAGE = AVERAGE | HARDWARE + @property + def is_average(self) -> bool: + return bool(self & LoopFlags.AVERAGE) + + @property + def is_pipeline(self) -> bool: + return bool(self & LoopFlags.PIPELINE) class LoopingMode(Enum): - EXECUTE = auto() + NEAR_TIME_ONLY = auto() ONCE = auto() @@ -36,10 +39,14 @@ class LoopingMode(Enum): class ExecutionScope: - def __init__(self, parent: ExecutionScope, root: ExecutorBase): + def __init__(self, parent: ExecutionScope | None, root: ExecutorBase): self._parent = parent + self._is_real_time: bool = False if parent is None else parent._is_real_time self._root = root - self._variables: Dict[str, Any] = {} + self._variables: dict[str, Any] = {} + + def enter_real_time(self): + self._is_real_time = True def set_variable(self, name: str, value: Any): self._variables[name] = value @@ -68,7 +75,7 @@ def run(self, scope: ExecutionScope): class Sequence(Statement): def __init__(self, sequence=None): - self.sequence: List[Statement] = sequence or [] + self.sequence: list[Statement] = sequence or [] def append_statement(self, statement: Statement): self.sequence.append(statement) @@ -126,7 +133,7 @@ def __repr__(self): class ExecUserCall(Statement): - def __init__(self, func_name: str, args: Dict[str, Any]): + def __init__(self, func_name: str, args: dict[str, Any]): self.func_name = func_name self.args = args @@ -152,7 +159,7 @@ def __repr__(self): class ExecAcquire(Statement): def __init__(self, handle: str, signal: str, parent_uid: str): self.handle = handle - self.signal = signal + self.signal = min(signal) if isinstance(signal, list) else signal self.parent_uid = parent_uid def run(self, scope: ExecutionScope): @@ -236,17 +243,15 @@ def __init__( self, count: int, body: Sequence, - loop_flags: LoopFlags = LoopFlags.SWEEP, - chunk_count=1, + loop_flags: LoopFlags = LoopFlags.NONE, ): self.count = count self.body = body self.loop_flags = loop_flags - self.chunk_count = chunk_count def _loop_iterator(self, scope: ExecutionScope) -> Iterator[int]: - if scope.root.looping_mode == LoopingMode.EXECUTE: - if self.loop_flags & LoopFlags.HARDWARE: + if scope.root.looping_mode == LoopingMode.NEAR_TIME_ONLY: + if scope._is_real_time: yield 0 else: for i in range(self.count): @@ -267,11 +272,10 @@ def __eq__(self, other): if other is self: return True if type(other) is ForLoop: - return (self.count, self.body, self.loop_flags, self.chunk_count) == ( + return (self.count, self.body, self.loop_flags) == ( other.count, other.body, other.loop_flags, - other.chunk_count, ) return NotImplemented @@ -288,7 +292,7 @@ def __init__( averaging_mode: AveragingMode, acquisition_type: AcquisitionType, ): - super().__init__(count, body, LoopFlags.RT_AVERAGE) + super().__init__(count=count, body=body, loop_flags=LoopFlags.AVERAGE) self.uid = uid self.averaging_mode = averaging_mode self.acquisition_type = acquisition_type @@ -297,10 +301,12 @@ def run(self, scope: ExecutionScope): with scope.root.rt_handler( self.count, self.uid, self.averaging_mode, self.acquisition_type ): - if scope.root.looping_mode == LoopingMode.EXECUTE: + if scope.root.looping_mode == LoopingMode.NEAR_TIME_ONLY: pass elif scope.root.looping_mode == LoopingMode.ONCE: - super().run(scope) + sub_scope = scope.make_sub_scope() + sub_scope.enter_real_time() + super().run(sub_scope) else: raise LabOneQException( f"Unknown looping mode '{scope.root.looping_mode}'" @@ -348,13 +354,13 @@ class ExecutorBase: class as needed. """ - def __init__(self, looping_mode: LoopingMode = LoopingMode.EXECUTE): + def __init__(self, looping_mode: LoopingMode = LoopingMode.NEAR_TIME_ONLY): self.looping_mode: LoopingMode = looping_mode def set_handler(self, path: str, value): pass - def user_func_handler(self, func_name: str, args: Dict[str, Any]): + def user_func_handler(self, func_name: str, args: dict[str, Any]): pass def acquire_handler(self, handle: str, signal: str, parent_uid: str): @@ -367,7 +373,7 @@ def set_sw_param_handler( @contextmanager def for_loop_handler(self, count: int, index: int, loop_flags: LoopFlags): - pass + yield @contextmanager def rt_handler( @@ -377,7 +383,7 @@ def rt_handler( averaging_mode: AveragingMode, acquisition_type: AcquisitionType, ): - pass + yield def run(self, root_sequence: Statement): """Start execution of the provided sequence.""" diff --git a/laboneq/implementation/compilation_service/compilation_service_legacy.py b/laboneq/implementation/compilation_service/compilation_service_legacy.py index 632f4cb..b126af6 100644 --- a/laboneq/implementation/compilation_service/compilation_service_legacy.py +++ b/laboneq/implementation/compilation_service/compilation_service_legacy.py @@ -2,7 +2,6 @@ # SPDX-License-Identifier: Apache-2.0 import copy -import json import logging import time import uuid @@ -11,7 +10,7 @@ from laboneq.core.types.compiled_experiment import ( CompiledExperiment as CompiledExperimentDSL, ) -from laboneq.data.compilation_job import CompilationJob, SignalInfoType +from laboneq.data.compilation_job import CompilationJob from laboneq.data.scheduled_experiment import ScheduledExperiment from laboneq.interfaces.compilation_service import CompilationServiceAPI @@ -29,10 +28,8 @@ def submit_compilation_job(self, job: CompilationJob): """ job_id = len(self._job_queue) queue_entry = {"job_id": job_id, "job": job} - - experiment_json = convert_to_experiment_json(job) compiler = Compiler() - compiler_output = compiler.run(experiment_json) + compiler_output = compiler.run(job.experiment_info) self._job_results[job_id] = convert_compiler_output_to_scheduled_experiment( compiler_output @@ -62,175 +59,6 @@ def compilation_job_result(self, job_id: str) -> ScheduledExperiment: time.sleep(100e-3) -def convert_to_experiment_json(job: CompilationJob): - retval = { - "$schema": "../../schemas/qccs-schema_2_5_0.json", - "metadata": { - "version": "2.5.0", - "unit": {"time": "s", "frequency": "Hz", "phase": "rad"}, - "epsilon": {"time": 1e-12}, - "line_endings": "unix", - }, - "servers": [ - { - "id": "zi_server", - "host": "0.0.0.0", - "port": 8004, - "api_level": 6, - } - ], - } - - devices_in_job = {} - oscillators_in_job = {} - device_oscillators = {} - signal_connections = [] - - for signal in job.experiment_info.signals: - devices_in_job[signal.device.uid] = signal.device - _logger.info(f"Added device {signal.device} to job") - connection_dir = "in" if signal.type == SignalInfoType.INTEGRATION else "out" - signal_connections.append( - { - "signal": {"$ref": signal.uid}, - "device": {"$ref": signal.device.uid}, - "connection": { - "type": connection_dir, - "channels": [int(c) for c in signal.channels], - }, - } - ) - if osc := signal.oscillator is not None: - oscillators_in_job[osc.uid] = osc - if osc.is_hardware: - device_oscillators.setdefault(signal.device.uid, []).append(osc) - - retval["devices"] = [ - { - "id": d.uid, - "server": {"$ref": "zi_server"}, - "serial": "DEV" + str(i), - "interface": "1GbE", - "driver": d.device_type.name.lower(), - } - for i, d in enumerate(devices_in_job.values()) - ] - for d in retval["devices"]: - if d["id"] in device_oscillators: - d["oscillators_list"] = [ - {"$ref": o.uid} for o in device_oscillators[d["id"]] - ] - retval["oscillators"] = [ - {"id": o.uid, "frequency": o.frequency, "hardware": o.is_hardware} - for o in oscillators_in_job.values() - ] - - signal_type_mapping = { - SignalInfoType.INTEGRATION: "integration", - SignalInfoType.RF: "single", - SignalInfoType.IQ: "iq", - } - - retval["signals"] = [ - { - "id": s.uid, - "signal_type": signal_type_mapping[s.type], - "oscillators_list": [{"$ref": s.oscillator.uid}] - if s.oscillator is not None - else [], - } - for s in job.experiment_info.signals - ] - for s in retval["signals"]: - if not len(s["oscillators_list"]): - del s["oscillators_list"] - else: - s["modulation"] = True - - retval["signal_connections"] = signal_connections - - retval["pulses"] = [ - not_none_fields_dict( - p, ["uid", "length", "amplitude", "phase", "function"], {"uid": "id"} - ) - for p in job.experiment_info.pulse_defs - ] - - def walk_sections(section, visitor): - visitor(section) - for s in section.children: - walk_sections(s, visitor) - - sections_flat = [] - - def collector(section): - sections_flat.append(section) - - for s in job.experiment_info.sections: - walk_sections(s, collector) - - retval["sections"] = [] - for s in sections_flat: - out_section = { - "id": s.uid, - "align": s.alignment.name.lower() if s.alignment else "left", - } - if s.count is not None: - out_section["repeat"] = { - "count": s.count, - "sections_list": [{"$ref": c.uid} for c in s.children], - "execution_type": s.execution_type, - "averaging_type": s.averaging_type, - } - else: - out_section["sections_list"] = [{"$ref": c.uid} for c in s.children] - if out_section["sections_list"] == []: - del out_section["sections_list"] - retval["sections"].append(out_section) - - for ssp in job.experiment_info.section_signal_pulses: - for s in retval["sections"]: - if s["id"] == ssp.section.uid: - if "signals_list" not in s: - s["signals_list"] = [] - signals_list_entry = next( - ( - sle - for sle in s["signals_list"] - if sle["signal"]["$ref"] == ssp.signal.uid - ), - None, - ) - if signals_list_entry is None: - signals_list_entry = { - "signal": {"$ref": ssp.signal.uid}, - "pulses_list": [], - } - s["signals_list"].append(signals_list_entry) - - signals_list_entry["pulses_list"].append( - {"pulse": {"$ref": ssp.pulse_def.uid}} - ) - break - - retval["experiment"] = { - "sections_list": [{"$ref": s.uid} for s in job.experiment_info.sections], - "signals_list": [{"$ref": s.uid} for s in job.experiment_info.signals], - } - - _logger.info(f"Generated job: {json.dumps(retval, indent=2)}") - - return retval - - -def not_none_fields_dict(obj, fields, translator): - return { - translator.get(f, f): getattr(obj, f) - for f in fields - if getattr(obj, f) is not None - } - - def convert_compiler_output_to_scheduled_experiment( compiler_output: CompiledExperimentDSL, ) -> ScheduledExperiment: diff --git a/laboneq/implementation/experiment_workflow/device_setup_generator.py b/laboneq/implementation/experiment_workflow/device_setup_generator.py index 1be8c99..3a2a082 100644 --- a/laboneq/implementation/experiment_workflow/device_setup_generator.py +++ b/laboneq/implementation/experiment_workflow/device_setup_generator.py @@ -3,226 +3,24 @@ from __future__ import annotations -import itertools -import logging -import warnings -from collections import UserDict -from typing import Dict, List, Optional, Tuple, Union +from typing import TYPE_CHECKING, Dict, List, Union -from laboneq.core.exceptions.laboneq_exception import LabOneQException -from laboneq.data.setup_description import ( - ChannelMapEntry, - DeviceType, - Instrument, - LogicalSignal, - LogicalSignalGroup, - PhysicalChannel, - PhysicalChannelType, - Server, - Setup, - SetupInternalConnection, -) -from laboneq.implementation.legacy_adapters import device_setup_converter as converter +from yaml import load -_logger = logging.getLogger(__name__) +try: + from yaml import CLoader as Loader +except ImportError: + from yaml import Loader -PATH_SEPARATOR = "/" +if TYPE_CHECKING: + from laboneq.data.setup_description import Setup -# Terminal Symbols -T_HDAWG_DEVICE = "HDAWG" -T_UHFQA_DEVICE = "UHFQA" -T_SHFQA_DEVICE = "SHFQA" -T_SHFSG_DEVICE = "SHFSG" -T_SHFQC_DEVICE = "SHFQC" -T_PQSC_DEVICE = "PQSC" -T_ALL_DEVICE_TYPES = [ - T_HDAWG_DEVICE, - T_UHFQA_DEVICE, - T_SHFQA_DEVICE, - T_SHFSG_DEVICE, - T_SHFQC_DEVICE, - T_PQSC_DEVICE, -] -T_UID = "uid" -T_ADDRESS = "address" -T_INTERFACE = "interface" -T_IQ_SIGNAL = "iq_signal" -T_ACQUIRE_SIGNAL = "acquire_signal" -T_RF_SIGNAL = "rf_signal" -T_TO = "to" -T_EXTCLK = "external_clock_signal" -T_INTCLK = "internal_clock_signal" -T_PORT = "port" -T_PORTS = "ports" - -# Models 'instruments' (former 'instrument_list') part of the descriptor: -# instruments: -# HDAWG: -# - address: DEV8001 -# uid: device_hdawg -# SHFQA: -# - address: DEV12001 -# uid: device_shfqa -# PQSC: -# - address: DEV10001 -# uid: device_pqsc InstrumentsType = Dict[str, List[Dict[str, str]]] - -# Models 'connections' part of the descriptor: -# connections: -# device_hdawg: -# - iq_signal: q0/drive_line -# ports: [SIGOUTS/0, SIGOUTS/1] -# - to: device_uhfqa -# port: DIOS/0 -# device_uhfqa: -# - iq_signal: q0/measure_line -# ports: [SIGOUTS/0, SIGOUTS/1] -# - acquire_signal: q0/acquire_line -# device_pqsc: -# - to: device_hdawg -# port: ZSYNCS/0 ConnectionsType = Dict[str, List[Dict[str, Union[str, List[str]]]]] - -# Models 'dataservers' part of the descriptor: -# dataservers: -# zi_server: -# host: 127.0.0.1 -# port: 8004 -# instruments: [device_hdawg, device_uhfqa, device_pqsc] DataServersType = Dict[str, Dict[str, Union[str, List[str]]]] -def _port_decoder(port_desc, additional_switch_keys=None) -> Tuple[str, str, List[str]]: - if additional_switch_keys is None: - additional_switch_keys = [] - if isinstance(port_desc, dict): - port_desc = dict(port_desc) # make a copy - else: - port_desc = {port_desc: None} - - port = port_desc.pop(T_PORT, None) - ports = port_desc.pop(T_PORTS, None) - if ports is not None and port is not None: - raise LabOneQException( - f"Both ports and port specified, but only one is allowed: {port_desc}" - ) - if ports is None: - if port is None: - local_ports = [] - else: - local_ports = [port] - elif isinstance(ports, str): - local_ports = [ports] - else: - local_ports = ports - - signal_keys = [T_IQ_SIGNAL, T_ACQUIRE_SIGNAL, T_RF_SIGNAL] - trigger_keys = [T_TO] - path_keys = signal_keys + trigger_keys - all_keys = path_keys + additional_switch_keys - - signal_type_keyword = None - remote_path = None - for key in all_keys: - if key in port_desc: - signal_type_keyword = key - remote_path = port_desc.pop(key) - break - - if signal_type_keyword is None: - raise LabOneQException( - "Missing signal type: Expected one of the following keywords: " - + ", ".join(all_keys) - ) - if signal_type_keyword in path_keys and not remote_path: - raise LabOneQException( - f"Missing path: specify '{signal_type_keyword}: /'" - ) - - if signal_type_keyword in signal_keys: - remote_path = PATH_SEPARATOR.join(["", remote_path]) - - if port_desc: - raise LabOneQException(f"Unknown keyword found: {list(port_desc.keys())[0]}") - - return signal_type_keyword, remote_path, local_ports - - -def _create_physical_channel( - ports: List[str], signal_type_token: str, device_id, physical_signals -) -> Optional[PhysicalChannel]: - if signal_type_token in (T_IQ_SIGNAL, T_ACQUIRE_SIGNAL): - channel_type = PhysicalChannelType.IQ_CHANNEL - elif signal_type_token == T_RF_SIGNAL: - channel_type = PhysicalChannelType.RF_CHANNEL - else: - return None - - split_ports = [port.split(PATH_SEPARATOR) for port in ports] - signal_name = "_".join( - ( - group[0] - for group in itertools.groupby([x for y in zip(*split_ports) for x in y]) - ) - ).lower() - if not signal_name: - return - if device_id not in physical_signals: - physical_signals[device_id] = [] - else: - other_signal: PhysicalChannel = next( - (ps for ps in physical_signals[device_id] if ps.name == signal_name), None - ) - if other_signal is not None: - return other_signal - - physical_channel = PhysicalChannel(name=f"{signal_name}", type=channel_type) - physical_signals[device_id].append(physical_channel) - return physical_channel - - -class DescriptorLogicalSignals(UserDict): - def __init__(self, data: Dict) -> Dict[str, LogicalSignalGroup]: - super().__init__(data) - self.data = self._generate_logical_signal_groups(data) - - def _generate_logical_signal_groups( - self, data: Dict - ) -> Dict[str, LogicalSignalGroup]: - logical_signals_candidates = [] - - for conns in data.values(): - for conn in conns: - _, remote_path, _ = _port_decoder(conn) - if PATH_SEPARATOR in remote_path: - logical_signals_candidates.append( - { - "lsg_uid": remote_path.split(PATH_SEPARATOR)[1], - "signal_id": remote_path.split(PATH_SEPARATOR)[2], - } - ) - - logical_signal_groups = {} - for lsg_uid in set([ls["lsg_uid"] for ls in logical_signals_candidates]): - signals = [ - LogicalSignal( - name=ls["signal_id"], - group=ls["lsg_uid"], - ) - for ls in logical_signals_candidates - if ls["lsg_uid"] == lsg_uid - ] - - lsg = LogicalSignalGroup(lsg_uid, {ls.name: ls for ls in signals}) - logical_signal_groups[lsg.uid] = lsg - return logical_signal_groups - - def get_logical_signal(self, group: str, name: str) -> LogicalSignal: - return self.data[group].logical_signals[name] - - class DeviceSetupGenerator: @staticmethod def from_descriptor( @@ -230,19 +28,12 @@ def from_descriptor( server_host: str = None, server_port: int | str = None, setup_name: str = None, - ): - from yaml import load - - try: - from yaml import CLoader as Loader - except ImportError: - from yaml import Loader - + ) -> Setup: setup_desc = load(yaml_text, Loader=Loader) return DeviceSetupGenerator.from_dicts( - instrument_list=setup_desc.get("instrument_list", {}), - instruments=setup_desc.get("instruments", {}), + instrument_list=setup_desc.get("instrument_list"), + instruments=setup_desc.get("instruments"), connections=setup_desc.get("connections"), dataservers=setup_desc.get("dataservers"), server_host=server_host, @@ -258,14 +49,7 @@ def from_yaml( server_host: str = None, server_port: str = None, setup_name: str = None, - ): - from yaml import load - - try: - from yaml import CLoader as Loader - except ImportError: - from yaml import Loader - + ) -> Setup: with open(filepath) as fp: setup_desc = load(fp, Loader=Loader) @@ -290,187 +74,20 @@ def from_dicts( server_host: str = None, server_port: int | str = None, setup_name: str = None, - ): - if instrument_list is not None: - if instruments is None: - warnings.warn( - "'instrument_list' section is deprecated in setup descriptor, use 'instruments' instead.", - FutureWarning, - ) - instruments = instrument_list - else: - warnings.warn( - "Both 'instrument_list' and 'instruments' are present in the setup descriptor, deprecated 'instrument_list' ignored.", - FutureWarning, - ) - - if instruments is None: - raise LabOneQException( - "'instruments' section is mandatory in the setup descriptor." - ) - - if connections is None: - connections = {} - - if setup_name is None: - setup_name = "unknown" - - if server_host is not None: - if dataservers is not None: - _logger.warning( - "Servers definition in the descriptor will be overridden by the server passed to the constructor." - ) - dataservers = { - "zi_server": { - "host": server_host, - "port": "8004" if server_port is None else server_port, - "instruments": [], - } - } - - if dataservers is None: - raise LabOneQException( - "At least one server must be defined either in the descriptor or in the constructor." - ) - - servers = [ - ( - Server( - uid=server_uid, - host=server_def["host"], - port=int(server_def.get("port", 8004)), - api_level=6, - ), - server_def.get("instruments", []), - ) - for server_uid, server_def in dataservers.items() - ] - - # TODO(2K): Remove, leader device must be determined from connections - # Keeping this only to satisfy the tests - if len(servers) == 1: - pqsc_dicts = instruments.get(T_PQSC_DEVICE, []) - if len(pqsc_dicts) > 0: - servers[0][0].leader_uid = pqsc_dicts[0][T_UID] - - def server_finder(device_uid: str) -> str: - default_data_server: Server = None - explicit_data_server: Server = None - for data_server, devices in servers: - if default_data_server is None and len(devices) == 0: - default_data_server = data_server - if device_uid in devices: - if explicit_data_server is not None: - raise LabOneQException( - f"Device '{device_uid}' assigned to multiple data servers: " - f"[{explicit_data_server.uid}, {data_server.uid}]." - ) - explicit_data_server = data_server - if explicit_data_server is not None: - return explicit_data_server - if default_data_server is None: - raise LabOneQException( - f"Couldn't determine the data server for the device '{device_uid}'." - ) - return default_data_server - - # Define instruments - out_instruments: List[Instrument] = [] - for it, il in {**instrument_list, **instruments}.items(): - for instrument_def in il: - legacy_ports = converter.legacy_instrument_ports(DeviceType[it]) - instrument = Instrument( - uid=instrument_def[T_UID], - device_type=DeviceType[it], - server=server_finder(instrument_def[T_UID]), - address=instrument_def.get(T_ADDRESS, ""), - ports=[converter.convert_instrument_port(x) for x in legacy_ports], - ) - out_instruments.append(instrument) - - instruments_by_uid = {i.uid: i for i in out_instruments} - - physical_signals = {} - setup_internal_connections = [] - logical_signal_groups = DescriptorLogicalSignals(connections) - - # Define connections - for device_uid, conns in connections.items(): - physical_channels_by_uid = {} - instrument = instruments_by_uid[device_uid] - for conn in conns: - signal_type_keyword, remote_path, local_ports = _port_decoder(conn) - # TODO (MH): Device processors - if signal_type_keyword == T_ACQUIRE_SIGNAL: - if instrument.device_type == DeviceType.UHFQA: - local_ports = ["QAS/0", "QAS/1"] - - logical_signal = None - if PATH_SEPARATOR in remote_path: - lsg = remote_path.split(PATH_SEPARATOR)[1] - signal_id = remote_path.split(PATH_SEPARATOR)[2] - logical_signal = logical_signal_groups.get_logical_signal( - lsg, signal_id - ) - physical_channel = _create_physical_channel( - local_ports, signal_type_keyword, device_uid, physical_signals - ) - if physical_channel is not None: - if physical_channel.name in physical_channels_by_uid: - physical_channel = physical_channels_by_uid[ - physical_channel.name - ] - else: - physical_channels_by_uid[ - physical_channel.name - ] = physical_channel - if "ports" in conn: - for port in instrument.ports: - if port.path in conn["ports"]: - if not physical_channel.ports: - physical_channel.ports = [port] - if port not in physical_channel.ports: - physical_channel.ports.append(port) - else: - for l_port in local_ports: - for dev_port in instrument.ports: - if l_port == dev_port.path: - if not physical_channel.ports: - physical_channel.ports = [dev_port] - if port not in physical_channel.ports: - physical_channel.ports.append(dev_port) - instrument.physical_channels.append(physical_channel) - - if logical_signal is not None: - instrument.connections.append( - ChannelMapEntry( - logical_signal=logical_signal, - physical_channel=physical_channel, - ) - ) - if "port" in conn: - if signal_type_keyword == T_TO: - if remote_path in instruments_by_uid: - for instr_port in instrument.ports: - if conn["port"] in instr_port.path: - setup_internal_connections.append( - SetupInternalConnection( - from_instrument=instrument, - from_port=instr_port, - to_instrument=instruments_by_uid[ - remote_path - ], - to_port=None, - ) - ) - break + ) -> Setup: + # Go though legacy DeviceSetup for legacy descriptor to avoid duplicate `DeviceSetupGenerator` + from laboneq.dsl.device import DeviceSetup + from laboneq.implementation.legacy_adapters.device_setup_converter import ( + convert_device_setup_to_setup, + ) - servers = {s.uid: s for s, _ in servers} - device_setup_constructor_args = { - "uid": setup_name, - "servers": servers, - "instruments": out_instruments, - "logical_signal_groups": logical_signal_groups.data, - "setup_internal_connections": setup_internal_connections, - } - return Setup(**device_setup_constructor_args) + legacy = DeviceSetup.from_dicts( + instrument_list=instrument_list, + instruments=instruments, + connections=connections, + dataservers=dataservers, + server_host=server_host, + server_port=server_port, + setup_name=setup_name, + ) + return convert_device_setup_to_setup(legacy) diff --git a/laboneq/implementation/experiment_workflow/experiment_workflow.py b/laboneq/implementation/experiment_workflow/experiment_workflow.py index cdea5f2..316c6b7 100644 --- a/laboneq/implementation/experiment_workflow/experiment_workflow.py +++ b/laboneq/implementation/experiment_workflow/experiment_workflow.py @@ -132,12 +132,8 @@ def device_setup_from_descriptor( """ Create a device setup from a descriptor. """ - return DeviceSetupGenerator.from_descriptor( - yaml_text=yaml_text, - server_host=server_host, - server_port=server_port, - setup_name=setup_name, + yaml_text, server_host, server_port, setup_name ) def set_current_setup(self, setup: Setup): diff --git a/laboneq/implementation/legacy_adapters/calibration_converter.py b/laboneq/implementation/legacy_adapters/calibration_converter.py new file mode 100644 index 0000000..515f5e3 --- /dev/null +++ b/laboneq/implementation/legacy_adapters/calibration_converter.py @@ -0,0 +1,196 @@ +# Copyright 2023 Zurich Instruments AG +# SPDX-License-Identifier: Apache-2.0 + +from dataclasses import asdict, is_dataclass +from typing import Any, Callable, Optional + +from laboneq.core.types import enums as legacy_enums +from laboneq.data import calibration, parameter +from laboneq.dsl import calibration as legacy_calibration +from laboneq.dsl import parameter as legacy_parameter +from laboneq.implementation.legacy_adapters.utils import ( + LogicalSignalPhysicalChannelUID, + raise_not_implemented, +) + + +def _change_dataclass_type(source: Any, target: Any) -> Any: + if source is None: + return None + if is_dataclass(source): + return target(**asdict(source)) + raise_not_implemented(source) + + +def convert_maybe_parameter(obj: Any) -> Any: + if obj is None: + return None + if isinstance(obj, legacy_parameter.SweepParameter): + return parameter.SweepParameter( + uid=obj.uid, values=obj.values, axis_name=obj.axis_name + ) + if isinstance(obj, legacy_parameter.LinearSweepParameter): + return parameter.LinearSweepParameter( + uid=obj.uid, + start=obj.start, + stop=obj.stop, + count=obj.count, + axis_name=obj.axis_name, + ) + if isinstance(obj, (float, int)): + return obj + raise_not_implemented(obj) + + +def convert_carrier_type( + obj: Optional[legacy_enums.CarrierType], +) -> Optional[calibration.CarrierType]: + if obj is None: + return None + if obj == legacy_enums.CarrierType.RF: + return calibration.CarrierType.RF + if obj == legacy_enums.CarrierType.IF: + return calibration.CarrierType.IF + raise_not_implemented(obj) + + +def convert_modulation_type( + obj: Optional[legacy_enums.ModulationType], +) -> Optional[calibration.ModulationType]: + if obj is None: + return None + if obj == legacy_enums.ModulationType.AUTO: + return calibration.ModulationType.AUTO + if obj == legacy_enums.ModulationType.HARDWARE: + return calibration.ModulationType.HARDWARE + if obj == legacy_enums.ModulationType.SOFTWARE: + return calibration.ModulationType.SOFTWARE + raise_not_implemented(obj) + + +def convert_oscillator( + obj: Optional[legacy_calibration.Oscillator], +) -> Optional[calibration.Oscillator]: + if obj is None: + return None + return calibration.Oscillator( + uid=obj.uid, + frequency=convert_maybe_parameter(obj.frequency), + modulation_type=convert_modulation_type(obj.modulation_type), + carrier_type=convert_carrier_type(obj.carrier_type), + ) + + +def convert_mixer_calibration( + obj: Optional[legacy_calibration.MixerCalibration], +) -> Optional[calibration.MixerCalibration]: + if obj is None: + return None + if isinstance(obj, legacy_calibration.MixerCalibration): + return calibration.MixerCalibration(**asdict(obj)) + raise_not_implemented(obj) + + +def convert_exponential( + obj: Optional[legacy_calibration.ExponentialCompensation], +) -> Optional[calibration.ExponentialCompensation]: + if obj is None: + return None + if isinstance(obj, list): + return [convert_exponential(x) for x in obj] + return calibration.ExponentialCompensation( + timeconstant=obj.timeconstant, amplitude=obj.amplitude + ) + + +def convert_highpass_compensation( + obj: Optional[legacy_calibration.HighPassCompensation], +) -> Optional[calibration.HighPassCompensation]: + if obj is None: + return None + return calibration.HighPassCompensation(timeconstant=obj.timeconstant) + + +def convert_precompensation( + obj: Optional[legacy_calibration.Precompensation], +) -> Optional[calibration.Precompensation]: + if obj is None: + return obj + new = calibration.Precompensation() + new.uid = obj.uid + new.exponential = convert_exponential(obj.exponential) + new.high_pass = convert_highpass_compensation(obj.high_pass) + new.bounce = _change_dataclass_type(obj.bounce, calibration.BounceCompensation) + new.FIR = _change_dataclass_type(obj.FIR, calibration.FIRCompensation) + return new + + +def convert_amplifier_pump( + obj: Optional[legacy_calibration.AmplifierPump], +) -> Optional[calibration.AmplifierPump]: + if obj is None: + return obj + return calibration.AmplifierPump( + uid=obj.uid, + pump_freq=convert_maybe_parameter(obj.pump_freq), + pump_power=convert_maybe_parameter(obj.pump_power), + alc_engaged=obj.alc_engaged, + use_probe=obj.use_probe, + probe_frequency=convert_maybe_parameter(obj.probe_frequency), + probe_power=convert_maybe_parameter(obj.probe_power), + ) + + +def convert_port_mode( + obj: Optional[legacy_enums.PortMode], +) -> Optional[calibration.PortMode]: + if obj is None: + return None + if obj == legacy_enums.PortMode.LF: + return calibration.PortMode.LF + if obj == legacy_enums.PortMode.RF: + return calibration.PortMode.RF + raise_not_implemented(obj) + + +def convert_maybe_quantity(obj: Any) -> Any: + if obj is None: + return None + if isinstance(obj, (int, float)): + return obj + raise_not_implemented(obj) + + +def format_ls_pc_uid(seq: str) -> str: + return LogicalSignalPhysicalChannelUID(seq).uid + + +def convert_calibration( + target: legacy_calibration.Calibration, + uid_formatter: Callable[[str], str] = format_ls_pc_uid, +) -> calibration.Calibration: + cals = {} + for uid, legacy_ls in target.calibration_items.items(): + if legacy_ls is None: + continue + else: + new = calibration.SignalCalibration() + new.oscillator = convert_oscillator(legacy_ls.oscillator) + new.local_oscillator_frequency = convert_maybe_parameter( + getattr(legacy_ls.local_oscillator, "frequency", None) + ) + new.mixer_calibration = convert_mixer_calibration( + legacy_ls.mixer_calibration + ) + new.precompensation = convert_precompensation(legacy_ls.precompensation) + new.port_delay = convert_maybe_parameter(legacy_ls.port_delay) + new.delay_signal = legacy_ls.delay_signal + new.port_mode = convert_port_mode(legacy_ls.port_mode) + new.voltage_offset = legacy_ls.voltage_offset + new.range = convert_maybe_quantity(legacy_ls.range) + new.threshold = legacy_ls.threshold + new.amplitude = convert_maybe_parameter(legacy_ls.amplitude) + new.amplifier_pump = convert_amplifier_pump(legacy_ls.amplifier_pump) + cals[uid_formatter(uid)] = new + + return calibration.Calibration(cals) diff --git a/laboneq/implementation/legacy_adapters/converters_calibration.py b/laboneq/implementation/legacy_adapters/converters_calibration.py deleted file mode 100644 index 7281a32..0000000 --- a/laboneq/implementation/legacy_adapters/converters_calibration.py +++ /dev/null @@ -1,215 +0,0 @@ -# Copyright 2023 Zurich Instruments AG -# SPDX-License-Identifier: Apache-2.0 - - -from laboneq.core.types.enums.carrier_type import CarrierType as CarrierTypeDSL -from laboneq.core.types.enums.modulation_type import ModulationType as ModulationTypeDSL -from laboneq.data.calibration import BounceCompensation as BounceCompensationDATA -from laboneq.data.calibration import Calibration as CalibrationDATA -from laboneq.data.calibration import CarrierType as CarrierTypeDATA -from laboneq.data.calibration import ( - ExponentialCompensation as ExponentialCompensationDATA, -) -from laboneq.data.calibration import FIRCompensation as FIRCompensationDATA -from laboneq.data.calibration import HighPassCompensation as HighPassCompensationDATA -from laboneq.data.calibration import MixerCalibration as MixerCalibrationDATA -from laboneq.data.calibration import ModulationType as ModulationTypeDATA -from laboneq.data.calibration import Oscillator as OscillatorDATA -from laboneq.data.calibration import Precompensation as PrecompensationDATA -from laboneq.data.calibration import Signal as SignalDATA -from laboneq.dsl.calibration.calibration import Calibration as CalibrationDSL -from laboneq.dsl.calibration.mixer_calibration import ( - MixerCalibration as MixerCalibrationDSL, -) -from laboneq.dsl.calibration.observable import Signal as SignalDSL -from laboneq.dsl.calibration.oscillator import Oscillator as OscillatorDSL -from laboneq.dsl.calibration.precompensation import ( - BounceCompensation as BounceCompensationDSL, -) -from laboneq.dsl.calibration.precompensation import ( - ExponentialCompensation as ExponentialCompensationDSL, -) -from laboneq.dsl.calibration.precompensation import ( - FIRCompensation as FIRCompensationDSL, -) -from laboneq.dsl.calibration.precompensation import ( - HighPassCompensation as HighPassCompensationDSL, -) -from laboneq.dsl.calibration.precompensation import ( - Precompensation as PrecompensationDSL, -) -from laboneq.implementation.legacy_adapters.dynamic_converter import convert_dynamic - -# converter functions for data type package 'calibration' -# AUTOGENERATED, DO NOT EDIT - - -def post_process(source, target): - return target - - -def get_converter_function_calibration(orig): - converter_function_directory = { - BounceCompensationDSL: convert_BounceCompensation, - CalibrationDSL: convert_Calibration, - ExponentialCompensationDSL: convert_ExponentialCompensation, - FIRCompensationDSL: convert_FIRCompensation, - HighPassCompensationDSL: convert_HighPassCompensation, - MixerCalibrationDSL: convert_MixerCalibration, - OscillatorDSL: convert_Oscillator, - PrecompensationDSL: convert_Precompensation, - SignalDSL: convert_Signal, - } - return converter_function_directory.get(orig) - - -def convert_CarrierType(orig: CarrierTypeDSL): - return ( - next(e for e in CarrierTypeDATA if e.name == orig.name) - if orig is not None - else None - ) - - -def convert_ModulationType(orig: ModulationTypeDSL): - return ( - next(e for e in ModulationTypeDATA if e.name == orig.name) - if orig is not None - else None - ) - - -def convert_BounceCompensation(orig: BounceCompensationDSL): - if orig is None: - return None - retval = BounceCompensationDATA() - retval.amplitude = orig.amplitude - retval.delay = orig.delay - return post_process( - orig, retval, conversion_function_lookup=get_converter_function_calibration - ) - - -def convert_Calibration(orig: CalibrationDSL): - if orig is None: - return None - retval = CalibrationDATA() - retval.calibration_items = convert_dynamic( - orig.calibration_items, - source_type_string="Dict", - target_type_string="Dict", - orig_is_collection=True, - conversion_function_lookup=get_converter_function_calibration, - ) - return post_process( - orig, retval, conversion_function_lookup=get_converter_function_calibration - ) - - -def convert_ExponentialCompensation(orig: ExponentialCompensationDSL): - if orig is None: - return None - retval = ExponentialCompensationDATA() - retval.amplitude = orig.amplitude - retval.timeconstant = orig.timeconstant - return post_process( - orig, retval, conversion_function_lookup=get_converter_function_calibration - ) - - -def convert_FIRCompensation(orig: FIRCompensationDSL): - if orig is None: - return None - retval = FIRCompensationDATA() - retval.coefficients = convert_dynamic( - orig.coefficients, - source_type_string="ArrayLike", - target_type_string="ArrayLike", - orig_is_collection=True, - conversion_function_lookup=get_converter_function_calibration, - ) - return post_process( - orig, retval, conversion_function_lookup=get_converter_function_calibration - ) - - -def convert_HighPassCompensation(orig: HighPassCompensationDSL): - if orig is None: - return None - retval = HighPassCompensationDATA() - retval.timeconstant = orig.timeconstant - return post_process( - orig, retval, conversion_function_lookup=get_converter_function_calibration - ) - - -def convert_MixerCalibration(orig: MixerCalibrationDSL): - if orig is None: - return None - retval = MixerCalibrationDATA() - retval.correction_matrix = convert_dynamic( - orig.correction_matrix, - source_type_string="List[List[float]]", - target_type_string="List[List[float]]", - orig_is_collection=True, - conversion_function_lookup=get_converter_function_calibration, - ) - retval.uid = orig.uid - retval.voltage_offsets = convert_dynamic( - orig.voltage_offsets, - source_type_string="List[float]", - target_type_string="List[float]", - orig_is_collection=True, - conversion_function_lookup=get_converter_function_calibration, - ) - return post_process( - orig, retval, conversion_function_lookup=get_converter_function_calibration - ) - - -def convert_Oscillator(orig: OscillatorDSL): - if orig is None: - return None - retval = OscillatorDATA() - retval.carrier_type = convert_CarrierType(orig.carrier_type) - retval.frequency = convert_dynamic( - orig.frequency, - source_type_string="float | Parameter | None", - target_type_string="Any", - orig_is_collection=True, - conversion_function_lookup=get_converter_function_calibration, - ) - retval.modulation_type = convert_ModulationType(orig.modulation_type) - retval.uid = orig.uid - return post_process( - orig, retval, conversion_function_lookup=get_converter_function_calibration - ) - - -def convert_Precompensation(orig: PrecompensationDSL): - if orig is None: - return None - retval = PrecompensationDATA() - retval.FIR = convert_FIRCompensation(orig.FIR) - retval.bounce = convert_BounceCompensation(orig.bounce) - retval.exponential = convert_dynamic( - orig.exponential, - source_type_string="List[ExponentialCompensation]", - target_type_string="List[ExponentialCompensation]", - orig_is_collection=True, - conversion_function_lookup=get_converter_function_calibration, - ) - retval.high_pass = convert_HighPassCompensation(orig.high_pass) - retval.uid = orig.uid - return post_process( - orig, retval, conversion_function_lookup=get_converter_function_calibration - ) - - -def convert_Signal(orig: SignalDSL): - if orig is None: - return None - retval = SignalDATA() - return post_process( - orig, retval, conversion_function_lookup=get_converter_function_calibration - ) diff --git a/laboneq/implementation/legacy_adapters/converters_experiment_description.py b/laboneq/implementation/legacy_adapters/converters_experiment_description.py index 62d214f..2891743 100644 --- a/laboneq/implementation/legacy_adapters/converters_experiment_description.py +++ b/laboneq/implementation/legacy_adapters/converters_experiment_description.py @@ -10,6 +10,7 @@ from laboneq.core.types.enums.section_alignment import ( SectionAlignment as SectionAlignmentDSL, ) +from laboneq.data.calibration import SignalCalibration as SignalCalibrationDATA from laboneq.data.experiment_description import Acquire as AcquireDATA from laboneq.data.experiment_description import AcquireLoopNt as AcquireLoopNtDATA from laboneq.data.experiment_description import AcquireLoopRt as AcquireLoopRtDATA @@ -36,9 +37,6 @@ from laboneq.data.experiment_description import Section as SectionDATA from laboneq.data.experiment_description import SectionAlignment as SectionAlignmentDATA from laboneq.data.experiment_description import Set as SetDATA -from laboneq.data.experiment_description import ( - SignalCalibration as SignalCalibrationDATA, -) from laboneq.data.experiment_description import Sweep as SweepDATA from laboneq.data.parameter import LinearSweepParameter as LinearSweepParameterDATA from laboneq.data.parameter import Parameter as ParameterDATA @@ -66,7 +64,7 @@ from laboneq.dsl.experiment.section import Match as MatchDSL from laboneq.dsl.experiment.section import Section as SectionDSL from laboneq.dsl.experiment.section import Sweep as SweepDSL -from laboneq.dsl.experiment.set import Set as SetDSL +from laboneq.dsl.experiment.set_node import SetNode as SetDSL from laboneq.dsl.parameter import LinearSweepParameter as LinearSweepParameterDSL from laboneq.dsl.parameter import Parameter as ParameterDSL from laboneq.dsl.parameter import SweepParameter as SweepParameterDSL diff --git a/laboneq/implementation/legacy_adapters/converters_setup_description.py b/laboneq/implementation/legacy_adapters/converters_setup_description.py deleted file mode 100644 index 1c120e9..0000000 --- a/laboneq/implementation/legacy_adapters/converters_setup_description.py +++ /dev/null @@ -1,401 +0,0 @@ -# Copyright 2023 Zurich Instruments AG -# SPDX-License-Identifier: Apache-2.0 - - -from typing import Any as AnyDSL - -from laboneq.core.types.enums.io_direction import IODirection as IODirectionDSL -from laboneq.core.types.enums.port_mode import PortMode as PortModeDSL -from laboneq.core.types.enums.reference_clock_source import ( - ReferenceClockSource as ReferenceClockSourceDSL, -) -from laboneq.data.calibration import PortMode as PortModeDATA -from laboneq.data.setup_description import Any as AnyDATA -from laboneq.data.setup_description import ChannelMapEntry as ConnectionDATA -from laboneq.data.setup_description import Instrument as HDAWGDATA -from laboneq.data.setup_description import Instrument as InstrumentDATA -from laboneq.data.setup_description import Instrument as PQSCDATA -from laboneq.data.setup_description import Instrument as SHFQADATA -from laboneq.data.setup_description import Instrument as SHFSGDATA -from laboneq.data.setup_description import Instrument as UHFQADATA -from laboneq.data.setup_description import IODirection as IODirectionDATA -from laboneq.data.setup_description import LogicalSignal as LogicalSignalDATA -from laboneq.data.setup_description import LogicalSignalGroup as LogicalSignalGroupDATA -from laboneq.data.setup_description import PhysicalChannel as PhysicalChannelDATA -from laboneq.data.setup_description import ( - PhysicalChannelType as PhysicalChannelTypeDATA, -) -from laboneq.data.setup_description import Port as PortDATA -from laboneq.data.setup_description import QuantumElement as QuantumElementDATA -from laboneq.data.setup_description import Qubit as QubitDATA -from laboneq.data.setup_description import ( - ReferenceClockSource as ReferenceClockSourceDATA, -) -from laboneq.data.setup_description import Server as DataServerDATA -from laboneq.data.setup_description import Server as ServerDATA -from laboneq.data.setup_description import Setup as DeviceSetupDATA -from laboneq.dsl.device.connection import Connection as ConnectionDSL -from laboneq.dsl.device.device_setup import DeviceSetup as DeviceSetupDSL -from laboneq.dsl.device.instrument import Instrument as InstrumentDSL -from laboneq.dsl.device.instruments.hdawg import HDAWG as HDAWGDSL -from laboneq.dsl.device.instruments.pqsc import PQSC as PQSCDSL -from laboneq.dsl.device.instruments.shfqa import SHFQA as SHFQADSL -from laboneq.dsl.device.instruments.shfsg import SHFSG as SHFSGDSL -from laboneq.dsl.device.instruments.uhfqa import UHFQA as UHFQADSL -from laboneq.dsl.device.io_units.logical_signal import LogicalSignal as LogicalSignalDSL -from laboneq.dsl.device.io_units.physical_channel import ( - PhysicalChannel as PhysicalChannelDSL, -) -from laboneq.dsl.device.io_units.physical_channel import ( - PhysicalChannelType as PhysicalChannelTypeDSL, -) -from laboneq.dsl.device.logical_signal_group import ( - LogicalSignalGroup as LogicalSignalGroupDSL, -) -from laboneq.dsl.device.ports import Port as PortDSL -from laboneq.dsl.device.server import Server as ServerDSL -from laboneq.dsl.device.servers.data_server import DataServer as DataServerDSL -from laboneq.dsl.quantum.quantum_element import QuantumElement as QuantumElementDSL -from laboneq.dsl.quantum.qubit import Qubit as QubitDSL -from laboneq.implementation.legacy_adapters.dynamic_converter import convert_dynamic - -# converter functions for data type package 'setup_description' -# AUTOGENERATED, DO NOT EDIT -from .post_process_setup_description import post_process - - -def get_converter_function_setup_description(orig): - converter_function_directory = { - ConnectionDSL: convert_Connection, - DataServerDSL: convert_DataServer, - DeviceSetupDSL: convert_DeviceSetup, - HDAWGDSL: convert_HDAWG, - InstrumentDSL: convert_Instrument, - LogicalSignalDSL: convert_LogicalSignal, - LogicalSignalGroupDSL: convert_LogicalSignalGroup, - PQSCDSL: convert_PQSC, - PhysicalChannelDSL: convert_PhysicalChannel, - PortDSL: convert_Port, - QuantumElementDSL: convert_QuantumElement, - QubitDSL: convert_Qubit, - SHFQADSL: convert_SHFQA, - SHFSGDSL: convert_SHFSG, - ServerDSL: convert_Server, - UHFQADSL: convert_UHFQA, - } - return converter_function_directory.get(orig) - - -def convert_IODirection(orig: IODirectionDSL): - return ( - next(e for e in IODirectionDATA if e.name == orig.name) - if orig is not None - else None - ) - - -def convert_PhysicalChannelType(orig: PhysicalChannelTypeDSL): - return ( - next(e for e in PhysicalChannelTypeDATA if e.name == orig.name) - if orig is not None - else None - ) - - -def convert_PortMode(orig: PortModeDSL): - return ( - next(e for e in PortModeDATA if e.name == orig.name) - if orig is not None - else None - ) - - -def convert_ReferenceClockSource(orig: ReferenceClockSourceDSL): - return ( - next(e for e in ReferenceClockSourceDATA if e.name == orig.name) - if orig is not None - else None - ) - - -def convert_Connection(orig: ConnectionDSL): - if orig is None: - return None - retval = ConnectionDATA(None, None) - return post_process( - orig, - retval, - conversion_function_lookup=get_converter_function_setup_description, - ) - - -def convert_DataServer(orig: DataServerDSL): - if orig is None: - return None - retval = DataServerDATA() - retval.api_level = orig.api_level - retval.host = orig.host - retval.leader_uid = orig.leader_uid - retval.port = convert_dynamic( - orig.port, - source_type_hint=AnyDSL, - target_type_hint=AnyDATA, - orig_is_collection=False, - conversion_function_lookup=get_converter_function_setup_description, - ) - retval.uid = orig.uid - return post_process( - orig, - retval, - conversion_function_lookup=get_converter_function_setup_description, - ) - - -def convert_DeviceSetup(orig: DeviceSetupDSL): - if orig is None: - return None - retval = DeviceSetupDATA() - retval.instruments = convert_dynamic( - orig.instruments, - source_type_hint=InstrumentDSL, - target_type_hint=InstrumentDATA, - orig_is_collection=True, - conversion_function_lookup=get_converter_function_setup_description, - ) - retval.logical_signal_groups = convert_dynamic( - orig.logical_signal_groups, - source_type_hint=LogicalSignalGroupDSL, - target_type_hint=LogicalSignalGroupDATA, - orig_is_collection=True, - conversion_function_lookup=get_converter_function_setup_description, - ) - retval.servers = convert_dynamic( - orig.servers, - source_type_hint=DataServerDSL, - target_type_hint=ServerDATA, - orig_is_collection=True, - conversion_function_lookup=get_converter_function_setup_description, - ) - retval.uid = orig.uid - retval.calibration = orig.get_calibration() - return post_process( - orig, - retval, - conversion_function_lookup=get_converter_function_setup_description, - ) - - -def convert_HDAWG(orig: HDAWGDSL): - if orig is None: - return None - retval = HDAWGDATA() - retval.address = orig.address - retval.connections = convert_dynamic( - orig.connections, - source_type_hint=ConnectionDSL, - target_type_hint=ConnectionDATA, - orig_is_collection=True, - conversion_function_lookup=get_converter_function_setup_description, - ) - retval.interface = orig.interface - retval.uid = orig.uid - return post_process( - orig, - retval, - conversion_function_lookup=get_converter_function_setup_description, - ) - - -def convert_Instrument(orig: InstrumentDSL): - if orig is None: - return None - retval = InstrumentDATA() - retval.connections = convert_dynamic( - orig.connections, - source_type_hint=ConnectionDSL, - target_type_hint=ConnectionDATA, - orig_is_collection=True, - conversion_function_lookup=get_converter_function_setup_description, - ) - retval.interface = orig.interface - retval.uid = orig.uid - return post_process( - orig, - retval, - conversion_function_lookup=get_converter_function_setup_description, - ) - - -def convert_LogicalSignal(orig: LogicalSignalDSL): - if orig is None: - return None - retval = LogicalSignalDATA(name=orig.name, group=orig.uid.split("/")[0]) - return post_process( - orig, - retval, - conversion_function_lookup=get_converter_function_setup_description, - ) - - -def convert_LogicalSignalGroup(orig: LogicalSignalGroupDSL): - if orig is None: - return None - retval = LogicalSignalGroupDATA() - retval.logical_signals = convert_dynamic( - orig.logical_signals, - source_type_string="Dict", - target_type_string="Dict[str,LogicalSignal]", - orig_is_collection=True, - conversion_function_lookup=get_converter_function_setup_description, - ) - retval.uid = orig.uid - return post_process( - orig, - retval, - conversion_function_lookup=get_converter_function_setup_description, - ) - - -def convert_PQSC(orig: PQSCDSL): - if orig is None: - return None - retval = PQSCDATA() - retval.address = orig.address - retval.connections = convert_dynamic( - orig.connections, - source_type_hint=ConnectionDSL, - target_type_hint=ConnectionDATA, - orig_is_collection=True, - conversion_function_lookup=get_converter_function_setup_description, - ) - retval.interface = orig.interface - retval.uid = orig.uid - return post_process( - orig, - retval, - conversion_function_lookup=get_converter_function_setup_description, - ) - - -def convert_PhysicalChannel(orig: PhysicalChannelDSL): - if orig is None: - return None - retval = PhysicalChannelDATA() - retval.type = convert_PhysicalChannelType(orig.type) - retval.uid = orig.uid - return post_process( - orig, - retval, - conversion_function_lookup=get_converter_function_setup_description, - ) - - -def convert_Port(orig: PortDSL): - if orig is None: - return None - retval = PortDATA() - return post_process( - orig, - retval, - conversion_function_lookup=get_converter_function_setup_description, - ) - - -def convert_QuantumElement(orig: QuantumElementDSL): - if orig is None: - return None - retval = QuantumElementDATA() - retval.parameters = orig.parameters - retval.signals = orig.signals - retval.uid = orig.uid - return post_process( - orig, - retval, - conversion_function_lookup=get_converter_function_setup_description, - ) - - -def convert_Qubit(orig: QubitDSL): - if orig is None: - return None - retval = QubitDATA() - return post_process( - orig, - retval, - conversion_function_lookup=get_converter_function_setup_description, - ) - - -def convert_SHFQA(orig: SHFQADSL): - if orig is None: - return None - retval = SHFQADATA() - retval.address = orig.address - retval.connections = convert_dynamic( - orig.connections, - source_type_hint=ConnectionDSL, - target_type_hint=ConnectionDATA, - orig_is_collection=True, - conversion_function_lookup=get_converter_function_setup_description, - ) - retval.interface = orig.interface - retval.uid = orig.uid - return post_process( - orig, - retval, - conversion_function_lookup=get_converter_function_setup_description, - ) - - -def convert_SHFSG(orig: SHFSGDSL): - if orig is None: - return None - retval = SHFSGDATA() - retval.address = orig.address - retval.connections = convert_dynamic( - orig.connections, - source_type_hint=ConnectionDSL, - target_type_hint=ConnectionDATA, - orig_is_collection=True, - conversion_function_lookup=get_converter_function_setup_description, - ) - retval.interface = orig.interface - retval.uid = orig.uid - return post_process( - orig, - retval, - conversion_function_lookup=get_converter_function_setup_description, - ) - - -def convert_Server(orig: ServerDSL): - if orig is None: - return None - retval = ServerDATA() - retval.uid = orig.uid - return post_process( - orig, - retval, - conversion_function_lookup=get_converter_function_setup_description, - ) - - -def convert_UHFQA(orig: UHFQADSL): - if orig is None: - return None - retval = UHFQADATA() - retval.address = orig.address - retval.connections = convert_dynamic( - orig.connections, - source_type_hint=ConnectionDSL, - target_type_hint=ConnectionDATA, - orig_is_collection=True, - conversion_function_lookup=get_converter_function_setup_description, - ) - retval.interface = orig.interface - retval.uid = orig.uid - return post_process( - orig, - retval, - conversion_function_lookup=get_converter_function_setup_description, - ) diff --git a/laboneq/implementation/legacy_adapters/converters_target_setup.py b/laboneq/implementation/legacy_adapters/converters_target_setup.py index fee0f05..64074de 100644 --- a/laboneq/implementation/legacy_adapters/converters_target_setup.py +++ b/laboneq/implementation/legacy_adapters/converters_target_setup.py @@ -8,7 +8,6 @@ from laboneq.core.types.enums.io_direction import IODirection from laboneq.core.types.enums.io_signal_type import IOSignalType from laboneq.data.execution_payload import ( - ServerType, TargetChannelCalibration, TargetChannelType, TargetDevice, @@ -28,9 +27,8 @@ def convert_dsl_to_target_setup(device_setup: DeviceSetup) -> TargetSetup: servers: dict[str, TargetServer] = { server_uid: TargetServer( uid=server_uid, - address=server.host, + host=server.host, port=int(server.port), - server_type=ServerType.DATA_SERVER, api_level=server.api_level, ) for server_uid, server in device_setup.servers.items() diff --git a/laboneq/implementation/legacy_adapters/device_setup_converter.py b/laboneq/implementation/legacy_adapters/device_setup_converter.py index a0c3e00..5c4d6cb 100644 --- a/laboneq/implementation/legacy_adapters/device_setup_converter.py +++ b/laboneq/implementation/legacy_adapters/device_setup_converter.py @@ -1,43 +1,406 @@ # Copyright 2023 Zurich Instruments AG # SPDX-License-Identifier: Apache-2.0 -"""Module to convert LabOne Q DSL structures into other data types.""" -from typing import List +from __future__ import annotations +from typing import TYPE_CHECKING, Dict, Iterable, List, Mapping, Optional, Tuple + +from laboneq.core.exceptions import LabOneQException from laboneq.core.types import enums as legacy_enums -from laboneq.data import setup_description as setup +from laboneq.data.setup_description import ( + ChannelMapEntry, + DeviceType, + Instrument, + IODirection, + LogicalSignal, + LogicalSignalGroup, + PhysicalChannel, + PhysicalChannelType, + Port, + PortType, + ReferenceClock, + ReferenceClockSource, + Server, + Setup, + SetupInternalConnection, +) from laboneq.dsl import device as legacy_device from laboneq.dsl.device import instruments as legacy_instruments +from laboneq.dsl.device import io_units as legacy_io_units +from laboneq.implementation.legacy_adapters import calibration_converter +from laboneq.implementation.legacy_adapters.utils import ( + LogicalSignalPhysicalChannelUID, + raise_not_implemented, +) + +if TYPE_CHECKING: + from laboneq.dsl.device import logical_signal_group as legacy_lsg + from laboneq.dsl.device import servers as legacy_servers + from laboneq.dsl.device.instruments.zi_standard_instrument import ( + ZIStandardInstrument, + ) -def legacy_instrument_ports(device_type: setup.DeviceType) -> List[legacy_device.Port]: - if device_type == setup.DeviceType.HDAWG: +def legacy_instrument_ports(device_type: DeviceType) -> List[legacy_device.Port]: + if device_type == DeviceType.HDAWG: return legacy_instruments.HDAWG().ports - if device_type == setup.DeviceType.SHFQA: + if device_type == DeviceType.SHFQA: return legacy_instruments.SHFQA().ports - if device_type == setup.DeviceType.PQSC: + if device_type == DeviceType.PQSC: return legacy_instruments.PQSC().ports - if device_type == setup.DeviceType.UHFQA: + if device_type == DeviceType.UHFQA: return legacy_instruments.UHFQA().ports - if device_type == setup.DeviceType.SHFSG: + if device_type == DeviceType.SHFSG: return legacy_instruments.SHFSG().ports - if device_type == setup.DeviceType.SHFQC: + if device_type == DeviceType.SHFQC: return legacy_instruments.SHFSG().ports + legacy_instruments.SHFQA().ports - if device_type == setup.DeviceType.NonQC: + if device_type == DeviceType.SHFPPC: + return legacy_instruments.SHFPPC().ports + if device_type == DeviceType.UNMANAGED: return legacy_instruments.NonQC().ports - raise NotImplementedError("No port converter for ", device_type) + raise_not_implemented(device_type) -def legacy_signal_to_port_type(signal: legacy_enums.IOSignalType) -> setup.PortType: +def legacy_signal_to_port_type( + signal: Optional[legacy_enums.IOSignalType], +) -> Optional[PortType]: + if signal is None: + return None + if not isinstance(signal, legacy_enums.IOSignalType): + raise_not_implemented(signal) if signal == legacy_enums.IOSignalType.DIO: - return setup.PortType.DIO - elif signal == legacy_enums.IOSignalType.ZSYNC: - return setup.PortType.ZSYNC - else: - return setup.PortType.RF + return PortType.DIO + if signal == legacy_enums.IOSignalType.ZSYNC: + return PortType.ZSYNC + return PortType.RF + + +def convert_instrument_port(legacy: legacy_device.Port) -> Port: + return Port(path=legacy.uid, type=legacy_signal_to_port_type(legacy.signal_type)) + + +def convert_io_direction(obj: legacy_enums.io_direction.IODirection) -> IODirection: + return IODirection[obj.name] + + +def convert_logical_signal(target: legacy_lsg.LogicalSignal) -> LogicalSignal: + path = LogicalSignalPhysicalChannelUID(target.uid) + return LogicalSignal(name=path.name, group=path.group) + + +def convert_logical_signal_groups_with_ls_mapping( + logical_signal_groups: Dict[str, legacy_lsg.LogicalSignalGroup] +) -> Tuple[ + Dict[str, LogicalSignalGroup], Dict[legacy_lsg.LogicalSignal, LogicalSignal] +]: + mapping = {} + legacy_to_new = {} + for uid, lsg in logical_signal_groups.items(): + new_lsg = LogicalSignalGroup(uid) + for ls in lsg.logical_signals.values(): + new_ls = convert_logical_signal(ls) + legacy_to_new[ls] = new_ls + new_lsg.logical_signals[new_ls.name] = new_ls + mapping[new_lsg.uid] = new_lsg + return mapping, legacy_to_new + + +def convert_dataserver(target: legacy_servers.DataServer) -> Server: + return Server( + uid=target.uid, + api_level=target.api_level, + host=target.host, + leader_uid=target.leader_uid, + port=int(target.port), + ) + + +def convert_reference_clock_source( + target: Optional[legacy_enums.ReferenceClockSource], +) -> Optional[ReferenceClockSource]: + if target is None: + return None + if target == legacy_enums.ReferenceClockSource.EXTERNAL: + return ReferenceClockSource.EXTERNAL + if target == legacy_enums.ReferenceClockSource.INTERNAL: + return ReferenceClockSource.INTERNAL + raise_not_implemented(target) + + +def convert_device_type(target: ZIStandardInstrument) -> DeviceType: + if isinstance(target, legacy_instruments.HDAWG): + return DeviceType.HDAWG + if isinstance(target, legacy_instruments.SHFQA): + return DeviceType.SHFQA + if isinstance(target, legacy_instruments.PQSC): + return DeviceType.PQSC + if isinstance(target, legacy_instruments.UHFQA): + return DeviceType.UHFQA + if isinstance(target, legacy_instruments.SHFPPC): + return DeviceType.SHFPPC + if isinstance(target, legacy_instruments.SHFSG): + return DeviceType.SHFSG + if isinstance(target, legacy_instruments.NonQC): + return DeviceType.UNMANAGED + raise_not_implemented(target) + + +def convert_physical_channel_type( + target: Optional[legacy_io_units.PhysicalChannelType], +) -> Optional[PhysicalChannelType]: + if target is None: + return None + if target == legacy_io_units.PhysicalChannelType.IQ_CHANNEL: + return PhysicalChannelType.IQ_CHANNEL + if target == legacy_io_units.PhysicalChannelType.RF_CHANNEL: + return PhysicalChannelType.RF_CHANNEL + raise_not_implemented(target) + + +from laboneq.implementation.legacy_adapters import utils + + +def convert_physical_channel( + target: legacy_io_units.PhysicalChannel, + ports: Optional[Iterable[legacy_device.Port]] = None, +) -> PhysicalChannel: + if ports is None: + ports = [] + + pc_helper = utils.LogicalSignalPhysicalChannelUID(target.uid) + + return PhysicalChannel( + name=pc_helper.name, + group=pc_helper.group, + type=convert_physical_channel_type(target.type), + ports=[convert_instrument_port(port) for port in ports], + ) + +def _make_converted_lookup(ports: Iterable[legacy_device.Port]) -> Mapping[str, Port]: + return { + port.path: port for port in [convert_instrument_port(port) for port in ports] + } + + +def make_logical_signal_to_physical_signal_connections( + connections: List[legacy_device.Connection], + ls_legacy_new_map: Mapping[legacy_lsg.LogicalSignal, LogicalSignal], + ports: Iterable[legacy_device.Port], +) -> Tuple[List[ChannelMapEntry], List[PhysicalChannel], List[Port]]: + logical_signal_lookup = {ls.path: ls for ls in ls_legacy_new_map.keys()} + port_lookup = {port.uid: port for port in ports} + ports_converted = _make_converted_lookup(ports) + physical_chs = {} + channel_map_entries = [] + # Find connected logical signals and to which ports they point to. + for conn in connections: + if logical_signal := logical_signal_lookup.get(conn.remote_path): + if port := port_lookup.get(conn.local_port): + # Port and LogicalSignal point to the same direction, should be the same device. + if port.direction == logical_signal.direction: + phys_ch_uid = logical_signal.physical_channel.uid + if phys_ch_uid not in physical_chs: + physical_ch = convert_physical_channel( + logical_signal.physical_channel + ) + physical_ch.direction = convert_io_direction( + logical_signal.direction + ) + physical_chs[phys_ch_uid] = physical_ch + + entry = ChannelMapEntry( + physical_channel=physical_chs[phys_ch_uid], + logical_signal=ls_legacy_new_map[logical_signal], + ) + if entry not in channel_map_entries: + channel_map_entries.append(entry) + + if ports_converted[port.uid] not in physical_chs[phys_ch_uid].ports: + physical_chs[phys_ch_uid].ports.append( + ports_converted[port.uid] + ) + return ( + channel_map_entries, + list(physical_chs.values()), + list(ports_converted.values()), + ) + + +def convert_instrument( + target: ZIStandardInstrument, + ls_legacy_new_map: Mapping[legacy_lsg.LogicalSignal, LogicalSignal], + server: Server, +) -> Instrument: + # TODO: How to handle SHFQA / SHFSG with `is_qc=True` + ref_clk = ReferenceClock() + if target.reference_clock_source is not None: + ref_clk.source = convert_reference_clock_source(target.reference_clock_source) + # Only PQSC has reference clock frequency + if isinstance(target, legacy_instruments.PQSC): + ref_clk.frequency = target.reference_clock + ( + connections, + physical_channels, + ports, + ) = make_logical_signal_to_physical_signal_connections( + target.connections, ls_legacy_new_map, target.ports + ) + + obj = Instrument( + uid=target.uid, + address=target.address, + interface=target.interface, + reference_clock=ref_clk, + ports=ports, + device_type=convert_device_type(target), + physical_channels=physical_channels, + connections=connections, + server=server, + ) + return obj + + +class _InstrumentsLookup: + """A class for getting new `Instrument` types via old UIDs.""" + + def __init__(self, mapping: Mapping[str, Instrument]): + self._mapping = mapping + self._pc_by_instrument_lookup = {} + self._port_by_instrument_lookup = {} + for instrument in self._mapping.values(): + self._pc_by_instrument_lookup[instrument.uid] = { + pc.name: pc for pc in instrument.physical_channels + } + self._port_by_instrument_lookup[instrument.uid] = { + port.path: port for port in instrument.ports + } + + def get_instrument(self, uid: str) -> Instrument: + try: + return self._mapping[uid] + except KeyError: + return self._mapping[LogicalSignalPhysicalChannelUID(uid).group] + + def get_physical_channel(self, uid: str) -> PhysicalChannel: + pc_uid = LogicalSignalPhysicalChannelUID(uid) + return self._pc_by_instrument_lookup[self._mapping[pc_uid.group].uid][ + pc_uid.name + ] + + def get_instrument_port(self, uid: str, port: str) -> Port: + return self._port_by_instrument_lookup[self._mapping[uid].uid][port] + + +def _make_internal_connection_to_same_type_input_port( + from_instrument, from_port, to_instrument, to_port_type: PortType +): + to_port = None + for to_instrument_port in to_instrument.ports: + if to_instrument_port.type == to_port_type: + to_port = to_instrument_port + break + if not to_port: + raise LabOneQException( + f"Instrument '{from_instrument.uid}' '{from_port}' has a connection to an instrument '{to_instrument.uid}' without '{to_port_type}' ports." + ) + return SetupInternalConnection( + from_instrument=from_instrument, + from_port=from_port, + to_instrument=to_instrument, + to_port=to_port, + ) + + +def _make_device_to_device_connections( + legacy_instruments: List[legacy_device.Instrument], + legacy_logical_signals: List[legacy_lsg.LogicalSignal], + instrument_lookup: _InstrumentsLookup, +) -> List[SetupInternalConnection]: + legacy_ls_lookup = {ls.path: ls for ls in legacy_logical_signals} + conns = [] + for instr in legacy_instruments: + from_instrument = instrument_lookup.get_instrument(instr.uid) + for conn in instr.connections: + remote_logical_signal = legacy_ls_lookup.get(conn.remote_path) + from_port = instrument_lookup.get_instrument_port( + instr.uid, conn.local_port + ) + # Port is defined to go to logical signal. + if remote_logical_signal: + remote_ls_pc_uid = remote_logical_signal.physical_channel.uid + to_instrument = instrument_lookup.get_instrument(remote_ls_pc_uid) + # Logical signal and connection point to the different direction: Device to device connection. + if remote_logical_signal.direction != conn.direction: + for to_port in instrument_lookup.get_physical_channel( + remote_ls_pc_uid + ).ports: + conns.append( + SetupInternalConnection( + from_instrument=from_instrument, + from_port=from_port, + to_instrument=to_instrument, + to_port=to_port, + ) + ) + else: + if from_instrument.address != to_instrument.address: + from_instr_display = f"{instr.uid}/{conn.local_port}" + to_instr_display = f"{remote_ls_pc_uid}" + raise LabOneQException( + "Instrument to instrument connection ports point to the same direction: " + f"{from_instr_display} -> {remote_logical_signal.uid} -> {to_instr_display}" + ) + else: + # Port is defined to go straight to device. + # Most commonly ZSYNC / DIO input connections. + if from_port.type in {PortType.ZSYNC, PortType.DIO}: + to_instrument = instrument_lookup.get_instrument(conn.remote_path) + c = _make_internal_connection_to_same_type_input_port( + from_instrument=from_instrument, + from_port=from_port, + to_instrument=to_instrument, + to_port_type=from_port.type, + ) + conns.append(c) + else: + raise NotImplementedError( + f"No known input port type in device {to_instrument.uid} for port type {from_port.type}." + ) + return conns + + +def convert_device_setup_to_setup( + device_setup: legacy_device.device_setup.DeviceSetup, +) -> Setup: + """Convert legacy `DeviceSetup` into `Setup`.""" + setup = Setup( + uid=device_setup.uid, + ) + for name, server in device_setup.servers.items(): + setup.servers[name] = convert_dataserver(server) + + setup.calibration = calibration_converter.convert_calibration( + device_setup.get_calibration(), + uid_formatter=calibration_converter.format_ls_pc_uid, + ) + lsgs, ls_legacy_to_new_map = convert_logical_signal_groups_with_ls_mapping( + device_setup.logical_signal_groups + ) + setup.logical_signal_groups = lsgs -def convert_instrument_port(legacy: legacy_device.Port) -> setup.Port: - return setup.Port( - path=legacy.uid, type=legacy_signal_to_port_type(legacy.signal_type) + legacy_to_new_instrument_map = {} + for legacy_instr in device_setup.instruments: + instr = convert_instrument( + legacy_instr, + ls_legacy_to_new_map, + setup.servers[legacy_instr.server_uid], + ) + setup.instruments.append(instr) + legacy_to_new_instrument_map[legacy_instr.uid] = instr + setup.setup_internal_connections = _make_device_to_device_connections( + device_setup.instruments, + ls_legacy_to_new_map.keys(), + _InstrumentsLookup(legacy_to_new_instrument_map), ) + return setup diff --git a/laboneq/implementation/legacy_adapters/post_process_setup_description.py b/laboneq/implementation/legacy_adapters/post_process_setup_description.py deleted file mode 100644 index 314d62d..0000000 --- a/laboneq/implementation/legacy_adapters/post_process_setup_description.py +++ /dev/null @@ -1,158 +0,0 @@ -# Copyright 2023 Zurich Instruments AG -# SPDX-License-Identifier: Apache-2.0 - -from laboneq.data.setup_description import ( - ChannelMapEntry, - DeviceType, - LogicalSignalGroup, - PhysicalChannel, - PhysicalChannelType, - Port, - Setup, - SetupInternalConnection, -) -from laboneq.dsl.device.instrument import Instrument as InstrumentDSL -from laboneq.dsl.device.io_units.physical_channel import ( - PhysicalChannelType as PhysicalChannelTypeDSL, -) - -in_postprocess = False - - -def post_process(source, target, conversion_function_lookup): - global in_postprocess - if in_postprocess: - return target - - device_types = {e.name: e for e in DeviceType} - if type(source).__name__ in device_types: - in_postprocess = True - retval = conversion_function_lookup(InstrumentDSL)(source) - in_postprocess = False - retval.address = source.address - retval.device_type = device_types[type(source).__name__] - retval.server = source.server_uid - return retval - - if type(target) == Setup: - post_process_setup(source, target) - - if type(target) == LogicalSignalGroup: - target.logical_signals = {ls.group: ls for ls in target.logical_signals} - return target - - -def is_node_path_in_physical_channel(node_path, physical_channel): - path_split = node_path.split("/") - first_part_of_node_path = path_split[0] - first_path_of_pc_name = physical_channel.name.split("_")[0] - if not first_part_of_node_path.lower() == first_path_of_pc_name: - return False - if len(path_split) >= 3: - last_parth_of_node_path = path_split[-1] - last_part_of_pc_name = physical_channel.name.split("_")[-1] - if not last_parth_of_node_path.lower() == last_part_of_pc_name: - return False - - channel_numbers_in_pc_name = [ - int(s) for s in physical_channel.name.split("_") if s.isdecimal() - ] - if len(channel_numbers_in_pc_name) == 0: - return False - channel_numbers_in_node_path = [ - int(s) for s in node_path.split("/") if s.isdecimal() - ] - if len(channel_numbers_in_node_path) > 1: - return False - if len(channel_numbers_in_node_path) == 0: - return False - if channel_numbers_in_node_path[0] not in channel_numbers_in_pc_name: - return False - return True - - -def post_process_setup(dsl_setup, data_setup): - data_instrument_map = {i.uid: i for i in data_setup.instruments} - dsl_instrument_map = {i.uid: i for i in dsl_setup.instruments} - all_pcs = {} - for device_id, pcg in dsl_setup.physical_channel_groups.items(): - for pc_uid, pc in pcg.channels.items(): - all_pcs[(device_id, pc.name)] = pc - - all_ls = {} - for lsg in data_setup.logical_signal_groups: - for ls in lsg.logical_signals.values(): - all_ls[ls.group + "/" + ls.name] = ls - - for i in data_setup.instruments: - server_uid = i.server - if server_uid is not None: - i.server = next(s for s in data_setup.servers if s.uid == server_uid) - - i.physical_channels = [ - PhysicalChannel( - name=pc.name, - type=PhysicalChannelType.IQ_CHANNEL - if pc.type == PhysicalChannelTypeDSL.IQ_CHANNEL - else PhysicalChannelType.RF_CHANNEL, - ) - for k, pc in all_pcs.items() - if k[0] == i.uid - ] - - i.ports = [] - i.connections = [] - pcs_of_instrument = [pc for k, pc in all_pcs.items() if k[0] == i.uid] - - for c in dsl_instrument_map[i.uid].connections: - node_path = c.local_port - - pc_of_connection = next( - ( - pc - for pc in pcs_of_instrument - if is_node_path_in_physical_channel(node_path, pc) - ), - None, - ) - - if pc_of_connection is not None: - pc_of_connection = next( - ( - pc - for pc in i.physical_channels - if pc.name == pc_of_connection.name - ), - None, - ) - from laboneq.implementation.legacy_adapters import ( - device_setup_converter as converter, - ) - - current_port = Port( - path=c.local_port, - type=converter.legacy_signal_to_port_type(c.signal_type), - ) - i.ports.append(current_port) - - if c.remote_path in all_ls and pc_of_connection is not None: - i.connections.append( - ChannelMapEntry( - physical_channel=pc_of_connection, - logical_signal=all_ls[c.remote_path], - ) - ) - elif c.remote_path in data_instrument_map: - data_setup.setup_internal_connections.append( - SetupInternalConnection( - from_instrument=i, - to_instrument=data_instrument_map[c.remote_path], - from_port=current_port, - to_port=None, - ) - ) - - data_setup.servers = {s.uid: s for s in data_setup.servers} - data_setup.logical_signal_groups = { - lsg.uid: lsg for lsg in data_setup.logical_signal_groups - } diff --git a/laboneq/implementation/legacy_adapters/utils.py b/laboneq/implementation/legacy_adapters/utils.py new file mode 100644 index 0000000..7a65d60 --- /dev/null +++ b/laboneq/implementation/legacy_adapters/utils.py @@ -0,0 +1,38 @@ +# Copyright 2023 Zurich Instruments AG +# SPDX-License-Identifier: Apache-2.0 + +from typing import Any + +from laboneq.core import path as legacy_path +from laboneq.data.path import Separator + + +def raise_not_implemented(obj: Any): + raise NotImplementedError(f"Legacy converter could not convert: {obj} to new type.") + + +class LogicalSignalPhysicalChannelUID: + """A helper for legacy logical signal and physical channel UIDs.""" + + def __init__(self, seq: str): + self._seq = Separator.join(seq.split(legacy_path.Separator)[-2:]) + try: + self._group, self._name = self._seq.split(Separator) + if not self._group or not self._name: + raise ValueError + except ValueError as e: + raise ValueError( + f"Invalid path format. Required {Separator}." + ) from e + + @property + def uid(self): + return self._seq + + @property + def group(self): + return self._group + + @property + def name(self): + return self._name diff --git a/laboneq/implementation/payload_builder/convert_from_legacy_json_recipe.py b/laboneq/implementation/payload_builder/convert_from_legacy_json_recipe.py deleted file mode 100644 index e30c3d8..0000000 --- a/laboneq/implementation/payload_builder/convert_from_legacy_json_recipe.py +++ /dev/null @@ -1,322 +0,0 @@ -# Copyright 2019 Zurich Instruments AG -# SPDX-License-Identifier: Apache-2.0 - -from __future__ import annotations - -from dataclasses import dataclass -from typing import Any, cast - -from marshmallow import EXCLUDE, Schema, fields, post_load - -from laboneq.data import recipe - - -@dataclass -class JsonRecipe: - line_endings: str - experiment: recipe.Recipe - servers: list[Any] | None = None - devices: list[Any] | None = None - - -explicit_mapping = { - "JsonRecipeLoader": JsonRecipe, - "Experiment": recipe.Recipe, - "Device": dict, -} - - -def convert_from_legacy_json_recipe(legacy_recipe: dict) -> recipe.Recipe: - return cast(JsonRecipe, JsonRecipeLoader().load(legacy_recipe)).experiment - - -class QCCSSchema(Schema): - @post_load - def from_json(self, data, **kwargs): - cls = explicit_mapping.get(self.__class__.__name__) - if cls is None: - cls = getattr(recipe, self.__class__.__name__) - return cls(**data) - - -class Server(QCCSSchema): - class Meta: - fields = ("server_uid", "host", "port", "api_level") - ordered = True - - server_uid = fields.Str() - host = fields.Str() - port = fields.Integer() - api_level = fields.Integer() - - -class DriverOption(fields.Field): - class Meta: - fields = ("parameter_name", "value") - ordered = False - - key = fields.Str(required=True) - value = fields.Str(required=True) - - -class Device(QCCSSchema): - class Meta: - fields = ("device_uid", "driver", "options") - ordered = True - unknown = EXCLUDE - - device_uid = fields.Str() - driver = fields.Str() - options = fields.List(DriverOption(), required=False) - - -class Gains(QCCSSchema): - class Meta: - fields = ("diagonal", "off_diagonal") - ordered = True - - diagonal = fields.Float() - off_diagonal = fields.Float() - - -class IO(QCCSSchema): - class Meta: - fields = ( - "channel", - "enable", - "modulation", - "oscillator", - "oscillator_frequency", - "offset", - "gains", - "range", - "range_unit", - "precompensation", - "lo_frequency", - "port_mode", - "port_delay", - "scheduler_port_delay", - "delay_signal", - "marker_mode", - "amplitude", - ) - ordered = True - - channel = fields.Integer() - enable = fields.Boolean(required=False) - modulation = fields.Boolean(required=False) - oscillator = fields.Integer(required=False) - oscillator_frequency = fields.Float(required=False) - offset = fields.Float(required=False) - gains = fields.Nested(Gains, required=False) - range = fields.Float(required=False) - range_unit = fields.Str(required=False) - precompensation = fields.Dict(required=False) - lo_frequency = fields.Raw(required=False) - port_mode = fields.Str(required=False) - port_delay = fields.Raw(required=False) - scheduler_port_delay = fields.Float(required=False) - delay_signal = fields.Float(required=False) - marker_mode = fields.Str(required=False) - amplitude = fields.Raw(required=False) - - -class SignalTypeField(fields.Field): - def _serialize(self, value, attr, obj, **kwargs): - return value.name - - def _deserialize(self, value, attr, data, **kwargs): - return recipe.SignalType[value.upper()] - - -class AWG(QCCSSchema): - class Meta: - fields = ( - "awg", - "signal_type", - "qa_signal_id", - "command_table_match_offset", - "feedback_register", - ) - ordered = False - - awg = fields.Integer() - signal_type = SignalTypeField() - qa_signal_id = fields.Str(required=False, allow_none=True) - command_table_match_offset = fields.Integer(required=False, allow_none=True) - feedback_register = fields.Integer(required=False, allow_none=True) - - -class Port(QCCSSchema): - class Meta: - fields = ("port", "device_uid") - ordered = True - - port = fields.Integer() - device_uid = fields.Str() - - -class Measurement(QCCSSchema): - class Meta: - fields = ("length", "channel") - - length = fields.Integer() - channel = fields.Integer() - - -class RefClkTypeField(fields.Field): - def _serialize(self, value, attr, obj, **kwargs): - return value.name - - def _deserialize(self, value, attr, data, **kwargs): - if value == 10e6: - return recipe.RefClkType._10MHZ.value - else: - return recipe.RefClkType._100MHZ.value - - -class TriggeringModeField(fields.Field): - def _serialize(self, value, attr, obj, **kwargs): - return value.name - - def _deserialize(self, value, attr, data, **kwargs): - return recipe.TriggeringMode[value.upper()] - - -class Config(QCCSSchema): - class Meta: - fields = ( - "repetitions", - "reference_clock", - "holdoff", - "triggering_mode", - "sampling_rate", - ) - ordered = True - - repetitions = fields.Int() - reference_clock = RefClkTypeField() - holdoff = fields.Float() - triggering_mode = TriggeringModeField() - sampling_rate = fields.Float() - - -class Initialization(QCCSSchema): - class Meta: - fields = ( - "device_uid", - "config", - "awgs", - "outputs", - "inputs", - "measurements", - "ppchannels", - ) - ordered = True - - device_uid = fields.Str() - config = fields.Nested(Config) - awgs = fields.List(fields.Nested(AWG), required=False) - outputs = fields.List(fields.Nested(IO), required=False) - inputs = fields.List(fields.Nested(IO), required=False) - measurements = fields.List(fields.Nested(Measurement), required=False) - ppchannels = fields.List(fields.Raw, required=False, allow_none=True) - - -class OscillatorParam(QCCSSchema): - class Meta: - fields = ("id", "device_id", "channel", "frequency", "param") - ordered = True - - id = fields.Str() - device_id = fields.Str() - channel = fields.Int() - frequency = fields.Float(required=False, allow_none=True) - param = fields.Str(required=False, allow_none=True) - - -class IntegratorAllocation(QCCSSchema): - class Meta: - fields = ("signal_id", "device_id", "awg", "channels", "weights", "threshold") - ordered = True - - signal_id = fields.Str() - device_id = fields.Str() - awg = fields.Int() - channels = fields.List(fields.Int()) - weights = fields.Str(required=False, allow_none=True) - threshold = fields.Float(required=False) - - -class AcquireLength(QCCSSchema): - class Meta: - fields = ("section_id", "signal_id", "acquire_length") - ordered = True - - section_id = fields.Str() - signal_id = fields.Str() - acquire_length = fields.Int() - - -class NtStepKeyField(fields.Field): - def _deserialize(self, value, attr, data, **kwargs): - return recipe.NtStepKey(indices=tuple(value["indices"])) - - -class RealtimeExecutionInit(QCCSSchema): - class Meta: - fields = ( - "device_id", - "awg_id", - "seqc_ref", - "wave_indices_ref", - "nt_step", - ) - ordered = True - - device_id = fields.Str() - awg_id = fields.Int() - seqc_ref = fields.Str() - wave_indices_ref = fields.Str() - nt_step = NtStepKeyField() - - -class Experiment(QCCSSchema): - class Meta: - fields = ( - "initializations", - "realtime_execution_init", - "oscillator_params", - "integrator_allocations", - "acquire_lengths", - "simultaneous_acquires", - "total_execution_time", - "max_step_execution_time", - ) - ordered = True - unknown = EXCLUDE - - initializations = fields.List(fields.Nested(Initialization)) - realtime_execution_init = fields.List(fields.Nested(RealtimeExecutionInit)) - oscillator_params = fields.List(fields.Nested(OscillatorParam), required=False) - integrator_allocations = fields.List( - fields.Nested(IntegratorAllocation), required=False - ) - acquire_lengths = fields.List(fields.Nested(AcquireLength), required=False) - simultaneous_acquires = fields.List( - fields.Dict(fields.Str(), fields.Str()), required=False, allow_none=True - ) - total_execution_time = fields.Float(required=False, allow_none=True) - max_step_execution_time = fields.Float(required=False, allow_none=True) - - -class JsonRecipeLoader(QCCSSchema): - class Meta: - unknown = EXCLUDE - fields = ("line_endings", "experiment", "servers", "devices") - ordered = False - - line_endings = fields.Str() - experiment = fields.Nested(Experiment) - servers = fields.List(fields.Nested(Server), required=False) - devices = fields.List(fields.Nested(Device), required=False) diff --git a/laboneq/implementation/payload_builder/payload_builder.py b/laboneq/implementation/payload_builder/payload_builder.py index c2078ff..5b26e84 100644 --- a/laboneq/implementation/payload_builder/payload_builder.py +++ b/laboneq/implementation/payload_builder/payload_builder.py @@ -4,8 +4,6 @@ import copy import logging import uuid -from dataclasses import dataclass, field -from typing import TYPE_CHECKING, Any, Iterator from laboneq.data.compilation_job import ( CompilationJob, @@ -18,16 +16,7 @@ SignalInfo, SignalInfoType, ) -from laboneq.data.execution_payload import ( - ExecutionPayload, - ServerType, - TargetChannelCalibration, - TargetChannelType, - TargetDevice, - TargetDeviceType, - TargetServer, - TargetSetup, -) +from laboneq.data.execution_payload import ExecutionPayload, TargetSetup from laboneq.data.experiment_description import ( Delay, Experiment, @@ -37,130 +26,23 @@ Reserve, Section, ) -from laboneq.data.setup_description import ( - DeviceType, - Instrument, - PhysicalChannelType, - Setup, -) +from laboneq.data.setup_description import Instrument, Setup from laboneq.data.setup_description.setup_helper import SetupHelper -from laboneq.dsl.calibration.signal_calibration import SignalCalibration -from laboneq.implementation.payload_builder.convert_from_legacy_json_recipe import ( - convert_from_legacy_json_recipe, +from laboneq.implementation.payload_builder.target_setup_generator import ( + TargetSetupGenerator, ) from laboneq.interfaces.compilation_service.compilation_service_api import ( CompilationServiceAPI, ) from laboneq.interfaces.payload_builder.payload_builder_api import PayloadBuilderAPI -if TYPE_CHECKING: - from laboneq.dsl.calibration import Calibration - _logger = logging.getLogger(__name__) -@dataclass -class GlobalSetupProperties: - global_leader: str = None - is_desktop_setup: bool = False - internal_followers: list[str] = field(default_factory=list) - clock_settings: dict[str, Any] = field(default_factory=dict) - - class PayloadBuilder(PayloadBuilderAPI): def __init__(self, compilation_service: CompilationServiceAPI = None): self._compilation_service: CompilationServiceAPI = compilation_service - @staticmethod - def _convert_to_target_deviceType(dt: DeviceType) -> TargetDeviceType: - return next(t for t in TargetDeviceType if t.name == dt.name) - - def convert_to_target_setup(self, device_setup: Setup) -> TargetSetup: - """ - Convert the given device setup to a target setup. - """ - target_setup = TargetSetup() - - servers = [ - TargetServer( - uid=s.uid, - address=s.host, - server_type=ServerType.DATA_SERVER, - port=s.port, - api_level=s.api_level, - ) - for s in device_setup.servers.values() - ] - server_dict = {s.uid: s for s in servers} - - calibration: Calibration = device_setup.calibration - - def instrument_calibrations( - i: Instrument, - ) -> Iterator[TargetChannelCalibration]: - if calibration is None: - return - for c in i.connections: - sig_calib: SignalCalibration = calibration.calibration_items.get( - c.logical_signal.path - ) - if sig_calib is not None: - ports = [ - port.path - for port in i.ports - if ( - port.physical_channel - and port.physical_channel.uid == c.physical_channel.uid - ) - ] - channel_type = { - PhysicalChannelType.IQ_CHANNEL: TargetChannelType.IQ, - PhysicalChannelType.RF_CHANNEL: TargetChannelType.RF, - }.get(c.physical_channel.type, TargetChannelType.UNKNOWN) - yield TargetChannelCalibration( - channel_type=channel_type, - ports=ports, - voltage_offset=sig_calib.voltage_offset, - ) - - def connected_outputs(i: Instrument) -> dict[str, list[int]]: - ls_ports: dict[str, list[int]] = {} - for c in i.connections: - ports: list[int] = [] - for port in c.physical_channel.ports: - if ( - port.path.startswith("SIGOUTS") - or port.path.startswith("SGCHANNELS") - or port.path.startswith("QACHANNELS") - ): - ports.append(int(port.path.split("/")[1])) - if ports: - ls_ports.setdefault( - c.logical_signal.group + "/" + c.logical_signal.name, [] - ).extend(ports) - return ls_ports - - target_setup.devices = [ - TargetDevice( - uid=i.uid, - device_serial=i.address, - device_type=self._convert_to_target_deviceType(i.device_type), - server=server_dict[i.server.uid], - interface=i.interface if i.interface else "1GbE", - has_signals=len(i.connections) > 0, - connected_outputs=connected_outputs(i), - internal_connections=[ - (c.from_port.path, c.to_instrument.uid) - for c in device_setup.setup_internal_connections - if c.from_instrument.uid == i.uid - ], - calibrations=list(instrument_calibrations(i)), - ) - for i in device_setup.instruments - ] - target_setup.servers = servers - return target_setup - def build_payload( self, device_setup: Setup, @@ -175,19 +57,15 @@ def build_payload( if experiment.signals is None: experiment.signals = [] - experiment_info = self.extract_experiment_info( + experiment_info = self._extract_experiment_info( experiment, device_setup, signal_mappings ) job = CompilationJob(experiment_info=experiment_info) job_id = self._compilation_service.submit_compilation_job(job) scheduled_experiment = self._compilation_service.compilation_job_result(job_id) - if isinstance(scheduled_experiment.recipe, dict): - scheduled_experiment.recipe = convert_from_legacy_json_recipe( - scheduled_experiment.recipe - ) - target_setup = self.convert_to_target_setup(device_setup) + target_setup = TargetSetupGenerator.from_setup(device_setup) run_job = ExecutionPayload( uid=uuid.uuid4().hex, @@ -198,8 +76,11 @@ def build_payload( ) return run_job + def convert_to_target_setup(self, device_setup: Setup) -> TargetSetup: + return TargetSetupGenerator.from_setup(device_setup) + @classmethod - def extract_experiment_info( + def _extract_experiment_info( cls, exp: Experiment, setup: Setup, @@ -213,7 +94,7 @@ def extract_experiment_info( device_infos = {} for i in setup.instruments: - device_infos[i.uid] = cls.extract_device_info(i) + device_infos[i.uid] = cls._extract_device_info(i) device_mappings = {} for experiment_signal in exp.signals: @@ -240,15 +121,15 @@ def calc_remote_port(port_path): f"Signal {signal.uid} is not connected to any physical channel" ) experiment_info.signals.append( - cls.extract_signal_info(signal, device_mappings[signal.uid], channels) + cls._extract_signal_info(signal, device_mappings[signal.uid], channels) ) for p in exp.pulses: - experiment_info.pulse_defs.append(cls.extract_pulse_info(p)) + experiment_info.pulse_defs.append(cls._extract_pulse_info(p)) section_signal_pulses = [] experiment_info.sections = [ - cls.extract_section_info( + cls._extract_section_info( s, experiment_info.signals, experiment_info.pulse_defs, @@ -262,7 +143,7 @@ def calc_remote_port(port_path): return experiment_info @classmethod - def extract_section_info( + def _extract_section_info( cls, s: Section, signals, pulse_defs, section_signal_pulses ) -> SectionInfo: section_info = SectionInfo() @@ -279,7 +160,7 @@ def extract_section_info( for child in s.children: if isinstance(child, Section): section_info.children.append( - cls.extract_section_info( + cls._extract_section_info( child, signals, pulse_defs, section_signal_pulses ) ) @@ -292,30 +173,26 @@ def extract_section_info( section_signal_pulses.append( SectionSignalPulse( - section=section_info, signal=signal, - pulse_def=pulse_def, + pulse=pulse_def, ) ) elif isinstance(child, Delay): signal = next(s for s in signals if s.uid == child.signal) section_signal_pulses.append( SectionSignalPulse( - section=section_info, signal=signal, - delay=child.delay, + offset=child.time, ) ) elif isinstance(child, Reserve): signal = next(s for s in signals if s.uid == child.signal) - section_signal_pulses.append( - SectionSignalPulse(section=section_info, signal=signal) - ) + section_signal_pulses.append(SectionSignalPulse(signal=signal)) return section_info @classmethod - def extract_device_info(cls, instrument: Instrument) -> DeviceInfo: + def _extract_device_info(cls, instrument: Instrument) -> DeviceInfo: device_info = DeviceInfo() device_info.uid = instrument.uid if instrument.device_type is None: @@ -329,7 +206,7 @@ def extract_device_info(cls, instrument: Instrument) -> DeviceInfo: return device_info @classmethod - def extract_signal_info(cls, signal, device_info, channels) -> SignalInfo: + def _extract_signal_info(cls, signal, device_info, channels) -> SignalInfo: signal_info = SignalInfo(uid=signal.uid, device=device_info) signal_info.channels = channels @@ -342,7 +219,7 @@ def extract_signal_info(cls, signal, device_info, channels) -> SignalInfo: return signal_info @classmethod - def extract_pulse_info(cls, pulse: Pulse) -> PulseDef: + def _extract_pulse_info(cls, pulse: Pulse) -> PulseDef: pulse_def = PulseDef() pulse_def.uid = pulse.uid if isinstance(pulse, PulseFunctional): diff --git a/laboneq/implementation/payload_builder/target_setup_generator.py b/laboneq/implementation/payload_builder/target_setup_generator.py new file mode 100644 index 0000000..6c6d672 --- /dev/null +++ b/laboneq/implementation/payload_builder/target_setup_generator.py @@ -0,0 +1,166 @@ +# Copyright 2022 Zurich Instruments AG +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from laboneq.data.execution_payload import ( + TargetChannelCalibration, + TargetChannelType, + TargetDevice, + TargetDeviceType, + TargetServer, + TargetSetup, +) +from laboneq.data.setup_description import ( + DeviceType, + Instrument, + PhysicalChannelType, + Server, + Setup, +) +from laboneq.data.utils.calibration_helper import CalibrationHelper + + +class TargetSetupGenerator: + @classmethod + def from_setup(cls, setup: Setup, with_calibration: bool = True) -> TargetSetup: + """Return a TargetSetup from the given device setup. + + Currently calibration is included in the TargetSetup but the intention is + that it will be split out into its own data object and excluded from + the execution payload in future. The `with_calibration` parameter is + provided to make this change simpler. + + Args: + setup: + The device setup to generate a target setup for. + with_calibration: + Whether to include the calibration. If false, the `.calibration` + attributes of the returned `TargetDevice` instances are set to `None`. + + Returns: + target_setup: The generated target setup. + """ + assert isinstance(setup, Setup) + servers = [ + cls._target_server_from_server(server) for server in setup.servers.values() + ] + server_lookup = {server.uid: server for server in servers} + devices = [ + cls._target_device_from_instrument( + instrument, + server_lookup[instrument.server.uid], + setup, + with_calibration=with_calibration, + ) + for instrument in setup.instruments + ] + return TargetSetup( + uid=setup.uid, + servers=servers, + devices=devices, + ) + + @classmethod + def _target_server_from_server(cls, server: Server) -> TargetServer: + assert isinstance(server, Server) + return TargetServer( + uid=server.uid, + host=server.host, + port=server.port, + api_level=server.api_level, + ) + + @classmethod + def _convert_to_target_device_type(cls, dt: DeviceType) -> TargetDeviceType: + return TargetDeviceType[dt.name] + + @classmethod + def _target_device_from_instrument( + cls, + instrument: Instrument, + server: Server, + setup: Setup, + with_calibration: bool, + ) -> TargetDevice: + device_type = cls._convert_to_target_device_type(instrument.device_type) + connected_outputs = cls._connected_outputs_from_instrument(instrument) + internal_connections = cls._internal_connections(instrument, setup) + if with_calibration: + calibrations = cls._target_calibrations_from_instrument(instrument, setup) + else: + calibrations = None + return TargetDevice( + uid=instrument.uid, + server=server, + # Here we translate from a theoretical instrument address + # to a device serial number. For all ZI devices these + # are the same: + device_serial=instrument.address, + device_type=device_type, + interface=instrument.interface, + has_signals=len(instrument.connections) > 0, + # ... + connected_outputs=connected_outputs, + internal_connections=internal_connections, + calibrations=calibrations, + is_qc=False, # XXX: handle this here or in the device setup? + qc_with_qa=False, # XXX: handle this here or in the device setup? + reference_clock_source=instrument.reference_clock.source, + ) + + @classmethod + def _connected_outputs_from_instrument( + cls, instrument: Instrument + ) -> dict[str, list[int]]: + ls_ports = {} + for c in instrument.connections: + ports = [] + for port in c.physical_channel.ports: + if ( + port.path.startswith("SIGOUTS") + or port.path.startswith("SGCHANNELS") + or port.path.startswith("QACHANNELS") + ): + ports.append(int(port.path.split("/")[1])) + if ports: + ls_ports.setdefault( + f"{c.logical_signal.group}/{c.logical_signal.name}", [] + ).extend(ports) + return ls_ports + + @classmethod + def _internal_connections( + cls, instrument: Instrument, setup: Setup + ) -> list[tuple[str, str]]: + return [ + (c.from_port.path, c.to_instrument.uid) + for c in setup.setup_internal_connections + if c.from_instrument.uid == instrument.uid + ] + + @classmethod + def _target_calibrations_from_instrument( + cls, + instrument: Instrument, + setup: Setup, + ) -> list[TargetChannelCalibration]: + calibration = CalibrationHelper(setup.calibration) + calibrations = [] + if calibration.empty(): + return calibrations + for c in instrument.connections: + sig_calib = calibration.by_logical_signal(c.logical_signal) + if sig_calib is not None: + channel_type = { + PhysicalChannelType.IQ_CHANNEL: TargetChannelType.IQ, + PhysicalChannelType.RF_CHANNEL: TargetChannelType.RF, + }.get(c.physical_channel.type, TargetChannelType.UNKNOWN) + calibrations.append( + TargetChannelCalibration( + channel_type=channel_type, + ports=[p.path for p in c.physical_channel.ports], + voltage_offset=sig_calib.voltage_offset, + ) + ) + return calibrations diff --git a/laboneq/interfaces/payload_builder/payload_builder_api.py b/laboneq/interfaces/payload_builder/payload_builder_api.py index f493c1a..852029e 100644 --- a/laboneq/interfaces/payload_builder/payload_builder_api.py +++ b/laboneq/interfaces/payload_builder/payload_builder_api.py @@ -2,7 +2,6 @@ # SPDX-License-Identifier: Apache-2.0 from abc import ABC -from dataclasses import dataclass from typing import Dict from laboneq.data.execution_payload import ExecutionPayload, TargetSetup @@ -10,7 +9,6 @@ from laboneq.data.setup_description import Setup -@dataclass class PayloadBuilderAPI(ABC): def build_payload( self, diff --git a/laboneq/openqasm3/gate_store.py b/laboneq/openqasm3/gate_store.py index 2a46748..4ce7ec3 100644 --- a/laboneq/openqasm3/gate_store.py +++ b/laboneq/openqasm3/gate_store.py @@ -3,9 +3,9 @@ from typing import Callable, Dict, Optional, Tuple +from laboneq._utils import id_generator from laboneq.dsl.experiment import Section from laboneq.dsl.experiment.pulse import Pulse -from laboneq.dsl.experiment.utils import id_generator class GateStore: diff --git a/laboneq/openqasm3/openqasm3_importer.py b/laboneq/openqasm3/openqasm3_importer.py index 486b4f6..c29526a 100644 --- a/laboneq/openqasm3/openqasm3_importer.py +++ b/laboneq/openqasm3/openqasm3_importer.py @@ -11,9 +11,9 @@ from openpulse import ast import openqasm3.visitor +from laboneq._utils import id_generator from laboneq.core.exceptions import LabOneQException from laboneq.dsl.experiment import Experiment, Section -from laboneq.dsl.experiment.utils import id_generator from laboneq.dsl.quantum.quantum_element import SignalType from laboneq.dsl.quantum.qubit import Qubit from laboneq.dsl.quantum.transmon import Transmon @@ -413,10 +413,12 @@ def exp_from_qasm(program: str, qubits: dict[str, Qubit], gate_store: GateStore) """Create an experiment from an OpenQASM program. Args: - ----- - program (str): OpenQASM program - qubits (dict[str, Qubit]): map from OpenQASM qubit names to LabOne Q DSL Qubit objects - gate_store (GateStore): map from OpenQASM gate names to LabOne Q DSL Gate objects + program: + OpenQASM program + qubits: + Map from OpenQASM qubit names to LabOne Q DSL Qubit objects + gate_store: + Map from OpenQASM gate names to LabOne Q DSL Gate objects """ importer = OpenQasm3Importer(qubits=qubits, gate_store=gate_store) qasm_section = importer(text=program) diff --git a/laboneq/openqasm3/reset_gate_factory.py b/laboneq/openqasm3/reset_gate_factory.py index 5db773d..95657db 100644 --- a/laboneq/openqasm3/reset_gate_factory.py +++ b/laboneq/openqasm3/reset_gate_factory.py @@ -5,11 +5,11 @@ from typing import TYPE_CHECKING, Callable, Optional +from laboneq._utils import id_generator from laboneq.dsl.experiment.acquire import Acquire from laboneq.dsl.experiment.play_pulse import PlayPulse from laboneq.dsl.experiment.pulse import Pulse from laboneq.dsl.experiment.section import Case, Match, Section -from laboneq.dsl.experiment.utils import id_generator from laboneq.openqasm3.signal_store import SignalLineType if TYPE_CHECKING: diff --git a/laboneq/pulse_sheet_viewer/pulse_sheet_viewer.py b/laboneq/pulse_sheet_viewer/pulse_sheet_viewer.py index cb65307..b3f4b6a 100644 --- a/laboneq/pulse_sheet_viewer/pulse_sheet_viewer.py +++ b/laboneq/pulse_sheet_viewer/pulse_sheet_viewer.py @@ -3,6 +3,7 @@ from __future__ import annotations +import copy import datetime import json import logging @@ -26,7 +27,37 @@ def _get_html_template(): class PulseSheetViewer: @staticmethod def generate_viewer_html_text(events, title, interactive: bool = False): - events_json = json.dumps(events["event_list"], indent=2) + fixed_events = [] + for e in events["event_list"]: + if isinstance(e.get("signal"), list): + n = len(e["signal"]) + for s, p, par, ph, f, a, plp, pup, oph, bph in zip( + e["signal"], + e["play_wave_id"], + e["parametrized_with"], + e["phase"], + e["oscillator_frequency"], + e["amplitude"], + e["play_pulse_parameters"], + e["pulse_pulse_parameters"], + e.get("oscillator_phase", [None] * n), + e.get("baseband_phase", [None] * n), + ): + e_new = copy.deepcopy(e) + e_new["signal"] = s + e_new["play_wave_id"] = p + e_new["parametrized_with"] = par + e_new["phase"] = ph + e_new["oscillator_frequency"] = f + e_new["amplitude"] = a + e_new["play_pulse_parameters"] = plp + e_new["pulse_pulse_parameters"] = pup + e_new["oscillator_phase"] = oph + e_new["baseband_phase"] = bph + fixed_events.append(e_new) + else: + fixed_events.append(e) + events_json = json.dumps(fixed_events, indent=2) section_graph_json = json.dumps(events["section_graph"], indent=2) section_info_json = json.dumps(events["section_info"], indent=2) section_signals_with_children_json = json.dumps( @@ -82,8 +113,9 @@ def show_pulse_sheet(name: str, compiled_experiment: CompiledExperiment): compiled_experiment: The compiled experiment to show. Returns: - A link to the HTML output if `IPython` is installed, otherwise - returns the output filename as a string. + link (IPython link or filename): + A link to the HTML output if `IPython` is installed, otherwise + returns the output filename as a string. """ timestamp = datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S") filename = f"{name}_{timestamp}.html" diff --git a/laboneq/simple.py b/laboneq/simple.py index aeafb94..6bd6183 100644 --- a/laboneq/simple.py +++ b/laboneq/simple.py @@ -48,6 +48,7 @@ pulse_library, ) from laboneq.dsl.quantum import ( + QuantumElement, QuantumOperation, Qubit, QubitParameters, @@ -58,6 +59,7 @@ from laboneq.dsl.session import Session from laboneq.dsl.utils import has_onboard_lo from laboneq.implementation.data_storage.laboneq_database import DataStore +from laboneq.openqasm3.gate_store import GateStore from laboneq.openqasm3.openqasm3_importer import exp_from_qasm from laboneq.pulse_sheet_viewer.pulse_sheet_viewer import show_pulse_sheet from laboneq.simulator.output_simulator import OutputSimulator diff --git a/laboneq/simulator/output_simulator.py b/laboneq/simulator/output_simulator.py index 054430f..a178f9f 100644 --- a/laboneq/simulator/output_simulator.py +++ b/laboneq/simulator/output_simulator.py @@ -4,11 +4,11 @@ from __future__ import annotations from dataclasses import dataclass -from typing import List, Union from numpy.typing import ArrayLike from laboneq.core.types.compiled_experiment import CompiledExperiment +from laboneq.data.recipe import RealtimeExecutionInit from laboneq.dsl.device.device_setup import DeviceSetup from laboneq.dsl.device.io_units.physical_channel import PhysicalChannel from laboneq.simulator.seqc_parser import simulate @@ -28,10 +28,13 @@ class OutputData: class _AWG_ID: is_out: bool prog: str - channels: List[int] + channels: list[int] def __init__( - self, device_setup: DeviceSetup, ch: PhysicalChannel, realtime_inits: list + self, + device_setup: DeviceSetup, + ch: PhysicalChannel, + realtime_inits: list[RealtimeExecutionInit], ): self._device_setup = device_setup [self._dev_uid, pch] = ch.uid.split("/") @@ -43,28 +46,31 @@ def __init__( "sgchannels": self._decode_sgchannels, }[ch_attrs[0]](ch_attrs[1:], realtime_inits) - def find_seqc(self, device_name: str, awg_no: int, realtime_inits): + def find_seqc( + self, device_name: str, awg_no: int, realtime_inits: list[RealtimeExecutionInit] + ): self.prog = next( iter( sorted( ( rt_init for rt_init in realtime_inits - if rt_init["awg_id"] == awg_no - and rt_init["device_id"] == device_name + if rt_init.awg_id == awg_no and rt_init.device_id == device_name ), - key=lambda rt_init: rt_init["nt_step"]["indices"], + key=lambda rt_init: rt_init.nt_step.indices, ) ) - )["seqc_ref"] + ).seqc_ref - def _decode_sigouts(self, chs: List[str], realtime_inits): + def _decode_sigouts( + self, chs: list[str], realtime_inits: list[RealtimeExecutionInit] + ): self.is_out = True self.channels = [int(ch) for ch in chs] awg_no = self.channels[0] // 2 self.find_seqc(self._dev_uid, awg_no, realtime_inits) - def _decode_qas(self, chs: List[str], realtime_inits): + def _decode_qas(self, chs: list[str], realtime_inits: list[RealtimeExecutionInit]): self.is_out = False self.channels = [int(ch) for ch in chs] self.find_seqc(self._dev_uid, 0, realtime_inits) @@ -73,12 +79,16 @@ def _is_qc(self): dev = self._device_setup.instrument_by_uid(self._dev_uid) return dev.calc_driver() == "SHFQA" and dev.is_qc - def _decode_qachannels(self, chs: List[str], realtime_inits): + def _decode_qachannels( + self, chs: list[str], realtime_inits: list[RealtimeExecutionInit] + ): self.is_out = chs[1] == "output" self.channels = [0] self.find_seqc(self._dev_uid, int(chs[0]), realtime_inits) - def _decode_sgchannels(self, chs: List[str], realtime_inits): + def _decode_sgchannels( + self, chs: list[str], realtime_inits: list[RealtimeExecutionInit] + ): internal_device_name = ( self._dev_uid if not self._is_qc() else f"{self._dev_uid}_sg" ) @@ -91,11 +101,26 @@ def _decode_sgchannels(self, chs: List[str], realtime_inits): class OutputSimulator: """Interface to the output simulator. - .. highlight:: python - .. code-block:: python + Arguments: + compiled_experiment: + The compiled experiment to simulate. + max_simulation_length: + The maximum amount of time to simulate (in seconds). + max_output_length: + Deprecated and has no effect. Use the `output_length` argument to + the `get_snippet` method instead. + + Attributes: + max_output_length: + Deprecated nad has no effect. Use the `output_length` argument to + the `get_snippet` method instead. - # Usage: + Examples: + Example showing how to compile an experiment and make use of the + output simulator: + + ``` py # Given compiled_experiment compiled_experiment = session.compile(exp) @@ -111,7 +136,6 @@ class OutputSimulator: # Maximum output length can also be set later output_simulator.max_output_length = 5e-6 - # As next, retrieve the actual simulated waveform data = output_simulator.get_snippet( physical_channel, @@ -127,6 +151,7 @@ class OutputSimulator: data.wave # waveform data data.trigger # trigger values data.frequency # frequency data + ``` """ def __init__( @@ -159,7 +184,7 @@ def _uid_to_channel(self, uid: str) -> PhysicalChannel: def get_snippet( self, - physical_channel: Union[str, PhysicalChannel], + physical_channel: str | PhysicalChannel, start: float, output_length: float, get_wave: bool = True, @@ -167,6 +192,28 @@ def get_snippet( get_marker: bool = False, get_frequency: bool = False, ) -> OutputData: + """Retrieve the simulated waveforms for a given channel and window of time. + + Arguments: + physical_channel: The physical channel to retrieve waveforms for. + start: The start time of the window of events to retrieve (in seconds). + output_length: The maximum length of the window to retrieve (in seconds). + get_wave: Whether to return the waveform data. + get_trigger: Whether to return the trigger data. + get_marker: Whether to return the marker data. + get_frequency: Whether to return the oscillator frequency data. + + Returns: + The output data has the following attributes: + + - `time`: an array of the times corresponding to the returned waveform samples. + - `wave`: an array of waveform values at the given times. + - `trigger`: an array of trigger values at the given times. + - `frequency`: an array of oscillator frequencies at the given times. + + The corresponding attribute is `None` if the associated data was not + requested. + """ channel = ( physical_channel if isinstance(physical_channel, PhysicalChannel) @@ -175,9 +222,7 @@ def get_snippet( awg_id = _AWG_ID( self._compiled_experiment.device_setup, channel, - self._compiled_experiment.scheduled_experiment.recipe["experiment"][ - "realtime_execution_init" - ], + self._compiled_experiment.scheduled_experiment.recipe.realtime_execution_init, ) sim = self._simulations[awg_id.prog] diff --git a/laboneq/simulator/seqc_parser.py b/laboneq/simulator/seqc_parser.py index a7d38a5..59d1f2c 100644 --- a/laboneq/simulator/seqc_parser.py +++ b/laboneq/simulator/seqc_parser.py @@ -11,7 +11,7 @@ # Note: The simulator may be used as a testing tool, so it must be independent of the production code # Do not add dependencies to the code being tested here (such as compiler, DSL asf.) -from typing import TYPE_CHECKING, Any, Dict, List +from typing import TYPE_CHECKING, Any import numpy as np from numpy import typing as npt @@ -32,6 +32,7 @@ from pycparser.c_parser import CParser from laboneq.compiler.common.compiler_settings import EXECUTETABLEENTRY_LATENCY +from laboneq.data.recipe import Recipe, TriggeringMode if TYPE_CHECKING: from laboneq.core.types.compiled_experiment import CompiledExperiment @@ -294,10 +295,10 @@ class SeqCDescriptor: sampling_rate: float output_port_delay: float source: str = None - channels: List[int] = None - wave_index: Dict[Any, Any] = None - command_table: List[Any] = None - acquisition_type: str = None + channels: list[int] = None + wave_index: dict[Any, Any] = None + command_table: list[Any] = None + is_spectroscopy: bool = False class Operation(Enum): @@ -394,7 +395,7 @@ class SeqCSimulation: sampling_rate: float = field(default=2.0e9) startup_delay: float = field(default=0.0) output_port_delay: float = field(default=0.0) - acquisition_type: str = field(default="") + is_spectroscopy: bool = False class SimpleRuntime: @@ -467,7 +468,7 @@ def __init__( } self.variables = {} self.seqc_simulation = SeqCSimulation() - self.seqc_simulation.acquisition_type = descriptor.acquisition_type + self.seqc_simulation.is_spectroscopy = descriptor.is_spectroscopy self.times = {} self.times_at_port = {} self.descriptor = descriptor @@ -770,7 +771,7 @@ def add_wave(gen_index, wave_data_idx, event_length): wave_data_idx.append(known_wave.wave_data_idx) return wave_data_idx, event_length - if "spectroscopy" in self.descriptor.acquisition_type: + if self.descriptor.is_spectroscopy: assert generators_mask == self.predefined_consts["QA_GEN_NONE"] wave_data_idx, event_length = add_wave(0, wave_data_idx, event_length) else: @@ -936,33 +937,22 @@ def waitZSyncTrigger(self): self._start_trigger() -def find_device(recipe, device_uid): - for device in recipe["devices"]: - if device["device_uid"] == device_uid: - return device - return None - - def analyze_recipe( - recipe, sources, wave_indices, command_tables + recipe: Recipe, sources, wave_indices, command_tables ) -> list[SeqCDescriptor]: outputs: dict[str, list[int]] = {} seqc_descriptors_from_recipe: dict[str, SeqCDescriptor] = {} - for init in recipe["experiment"]["initializations"]: - device_uid = init["device_uid"] - device = find_device(recipe, device_uid) - device_type = device["driver"] + for init in recipe.initializations: + device_uid = init.device_uid + device_type = init.device_type sample_multiple = get_sample_multiple(device_type) - try: - sampling_rate = init["config"]["sampling_rate"] - except KeyError: - sampling_rate = None + sampling_rate = init.config.sampling_rate if sampling_rate is None or sampling_rate == 0: sampling_rate = get_frequency(device_type) startup_delay = -80e-9 - if device_type == "HDAWG" and "config" in init: - triggering_mode = init["config"].get("triggering_mode") - if triggering_mode == "desktop_leader": + if device_type == "HDAWG": + triggering_mode = init.config.triggering_mode + if triggering_mode == TriggeringMode.DESKTOP_LEADER: if sampling_rate == 2e9: startup_delay = -24e-9 else: @@ -971,74 +961,71 @@ def analyze_recipe( # TODO(2K): input port_delay previously was not taken into account by the simulator # - keeping it as is for not breaking the tests. To be cleaned up. input_channel_delays: dict[int, float] = { - i["channel"]: i["scheduler_port_delay"] # + i.get("port_delay", 0.0) - for i in init.get("inputs", []) + i.channel: i.scheduler_port_delay # + (0.0 if i.port_delay is None else i.port_delay) + for i in init.inputs } output_channel_delays: dict[int, float] = { - o["channel"]: o["scheduler_port_delay"] + o.get("port_delay", 0.0) - for o in init.get("outputs", []) + o.channel: o.scheduler_port_delay + + (0.0 if o.port_delay is None else o.port_delay) + for o in init.outputs } output_channel_precompensation = { - o["channel"]: o.get("precompensation", {}) for o in init.get("outputs", []) + o.channel: o.precompensation for o in init.outputs } awg_index = 0 - if "awgs" in init: - for awg in init["awgs"]: - awg_nr = awg["awg"] - rt_exec_step = next( - r - for r in recipe["experiment"]["realtime_execution_init"] - if r["device_id"] == device_uid and r["awg_id"] == awg_nr - ) - seqc = rt_exec_step["seqc_ref"] - if device_type == "SHFSG" or device_type == "SHFQA": - input_channel = awg_nr - output_channels = [awg_nr] - else: - input_channel = 2 * awg_nr - output_channels = [2 * awg_nr, 2 * awg_nr + 1] - - seqc_descriptors_from_recipe[seqc] = SeqCDescriptor( - name=seqc, - device_uid=device_uid, - device_type=device_type, - awg_index=awg_index, - measurement_delay_samples=round( - input_channel_delays.get(input_channel, 0.0) * sampling_rate - ), - startup_delay=startup_delay, - sample_multiple=sample_multiple, - sampling_rate=sampling_rate, - output_port_delay=output_channel_delays.get( - output_channels[0], 0.0 - ), - ) + for awg in init.awgs: + awg_nr = awg.awg + rt_exec_step = next( + r + for r in recipe.realtime_execution_init + if r.device_id == device_uid and r.awg_id == awg_nr + ) + seqc = rt_exec_step.seqc_ref + if device_type == "SHFSG" or device_type == "SHFQA": + input_channel = awg_nr + output_channels = [awg_nr] + else: + input_channel = 2 * awg_nr + output_channels = [2 * awg_nr, 2 * awg_nr + 1] + + seqc_descriptors_from_recipe[seqc] = SeqCDescriptor( + name=seqc, + device_uid=device_uid, + device_type=device_type, + awg_index=awg_index, + measurement_delay_samples=round( + input_channel_delays.get(input_channel, 0.0) * sampling_rate + ), + startup_delay=startup_delay, + sample_multiple=sample_multiple, + sampling_rate=sampling_rate, + output_port_delay=output_channel_delays.get(output_channels[0], 0.0), + ) - precompensation_info = output_channel_precompensation.get( - output_channels[0] + precompensation_info = output_channel_precompensation.get( + output_channels[0] + ) + if precompensation_info is not None: + precompensation_delay = ( + precompensation_delay_samples(precompensation_info) / sampling_rate ) - if precompensation_info is not None: - precompensation_delay = ( - precompensation_delay_samples(precompensation_info) - / sampling_rate - ) - seqc_descriptors_from_recipe[ - seqc - ].output_port_delay += precompensation_delay - - channels: list[int] = [ - output["channel"] - for output in init["outputs"] - if output["channel"] in output_channels - ] - if len(channels) == 0: - channels.append(0) - outputs[seqc] = channels + seqc_descriptors_from_recipe[ + seqc + ].output_port_delay += precompensation_delay + + channels: list[int] = [ + output.channel + for output in init.outputs + if output.channel in output_channels + ] + if len(channels) == 0: + channels.append(0) + outputs[seqc] = channels - awg_index += 1 + awg_index += 1 seq_c_wave_indices = {} for wave_index in wave_indices: @@ -1062,7 +1049,7 @@ def analyze_recipe( seqc_descriptor.channels = outputs[name] seqc_descriptor.wave_index = seq_c_wave_indices.get(name, {}) seqc_descriptor.command_table = command_table - seqc_descriptor.acquisition_type = recipe["experiment"]["acquisition_type"] + seqc_descriptor.is_spectroscopy = recipe.is_spectroscopy seqc_descriptors.append(seqc_descriptor) return seqc_descriptors @@ -1102,7 +1089,7 @@ def _replacer(match): # Note that the result does not correspond to the usual C for-loop semantics. # This is fine though, as we do not emit 'regular' for loops from L1Q. pattern = r"repeat\s*\((\d+)\)\s*{" - regex = re.compile(pattern, re.MULTILINE | re.DOTALL) + regex = re.compile(pattern) def replace_repeat(match_obj): if match_obj.group(1) is not None: @@ -1111,6 +1098,11 @@ def replace_repeat(match_obj): main = regex.sub(replace_repeat, main) + # Constant definitions in SeqC omit the type (constants can only be int or float; + # compile-time strings are instead defined via the `string` keyword). This is not + # valid C, so we 'patch' statements like `const a = 5;` to `const int a = 5;`. + main = re.sub(r"const(\s+[A-Za-z_]\w*\s+=)", r"const int\1", main) + # Define SeqC built-ins and wrap program into function # to make the program syntactically correct for C parser. if len(main) > 0: diff --git a/laboneq/simulator/wave_scroller.py b/laboneq/simulator/wave_scroller.py index 1bfbbb3..e7d2d46 100644 --- a/laboneq/simulator/wave_scroller.py +++ b/laboneq/simulator/wave_scroller.py @@ -149,7 +149,7 @@ def __init__( self.sim = sim self.is_shfqa = sim.device_type == "SHFQA" - self.acquisition_type = sim.acquisition_type + self.is_spectroscopy = sim.is_spectroscopy self.sim_targets = sim_targets @@ -327,7 +327,7 @@ def retrieve_wave(real_idx, imag_idx): wave_indices = event.args[4] - if "spectroscopy" in self.acquisition_type: + if self.is_spectroscopy: spectroscopy_mask = 0 assert generator_mask == spectroscopy_mask wave = retrieve_wave( diff --git a/pyproject.toml b/pyproject.toml index dd20ad1..005cb61 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,9 +53,9 @@ dependencies = [ "lagom", "attrs", "sortedcontainers", - "zhinst-core==23.2.42414", - "zhinst-toolkit~=0.5.0", - "zhinst-utils~=0.3.0", + "zhinst-core==23.6.45428", + "zhinst-toolkit~=0.6.0", + "zhinst-utils==0.3.2", ] [project.urls]