qadence 1.6.2__py3-none-any.whl → 1.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,774 @@
1
+ from __future__ import annotations
2
+
3
+ import numpy as np
4
+
5
+ from qadence.blocks import chain, kron
6
+ from qadence.blocks.abstract import AbstractBlock
7
+ from qadence.blocks.composite import ChainBlock, KronBlock
8
+ from qadence.blocks.utils import add
9
+ from qadence.circuit import QuantumCircuit
10
+ from qadence.constructors import (
11
+ analog_feature_map,
12
+ feature_map,
13
+ hamiltonian_factory,
14
+ identity_initialized_ansatz,
15
+ rydberg_feature_map,
16
+ rydberg_hea,
17
+ rydberg_tower_feature_map,
18
+ )
19
+ from qadence.constructors.ansatze import hea_digital, hea_sDAQC
20
+ from qadence.constructors.hamiltonians import ObservableConfig, TDetuning
21
+ from qadence.operations import CNOT, RX, RY, H, I, N, Z
22
+ from qadence.parameters import Parameter
23
+ from qadence.register import Register
24
+ from qadence.types import (
25
+ AnsatzType,
26
+ Interaction,
27
+ MultivariateStrategy,
28
+ ObservableTransform,
29
+ ReuploadScaling,
30
+ Strategy,
31
+ TParameter,
32
+ )
33
+
34
+ from .config import AnsatzConfig, FeatureMapConfig
35
+ from .models import QNN
36
+
37
+
38
+ def _create_support_arrays(
39
+ num_qubits: int,
40
+ num_features: int,
41
+ multivariate_strategy: str,
42
+ ) -> list[tuple[int, ...]]:
43
+ """
44
+ Create the support arrays for the digital feature map.
45
+
46
+ Args:
47
+ num_qubits (int): The number of qubits.
48
+ num_features (int): The number of features.
49
+ multivariate_strategy (str): The multivariate encoding strategy.
50
+ Either 'series' or 'parallel'.
51
+
52
+ Returns:
53
+ list[tuple[int, ...]]: The list of support arrays. ith element of the list is the support
54
+ array for the ith feature.
55
+
56
+ Raises:
57
+ ValueError: If the number of features is greater than the number of qubits
58
+ with parallel encoding. Not possible to encode these features in parallel.
59
+ ValueError: If the multivariate strategy is not 'series' or 'parallel'.
60
+ """
61
+ if multivariate_strategy == "series":
62
+ return [tuple(range(num_qubits)) for i in range(num_features)]
63
+ elif multivariate_strategy == "parallel":
64
+ if num_features <= num_qubits:
65
+ return [tuple(x.tolist()) for x in np.array_split(np.arange(num_qubits), num_features)]
66
+ else:
67
+ raise ValueError(
68
+ f"Number of features {num_features} must be less than or equal to the number of \
69
+ qubits {num_qubits}. if the features are to be encoded is parallely."
70
+ )
71
+ else:
72
+ raise ValueError(
73
+ f"Invalid encoding strategy {multivariate_strategy} provided. Only 'series' or \
74
+ 'parallel' are allowed."
75
+ )
76
+
77
+
78
+ def _encode_features_series_digital(
79
+ register: int | Register,
80
+ config: FeatureMapConfig,
81
+ ) -> list[AbstractBlock]:
82
+ """
83
+ Encode the features in series using digital feature map.
84
+
85
+ Args:
86
+ register (int | Register): The number of qubits or the register object.
87
+ config (FeatureMapConfig): The feature map configuration.
88
+
89
+ Returns:
90
+ list[AbstractBlock]: The list of digital feature map blocks.
91
+ """
92
+ num_qubits = register if isinstance(register, int) else register.n_qubits
93
+
94
+ support_arrays_list = _create_support_arrays(
95
+ num_qubits=num_qubits,
96
+ num_features=config.num_features,
97
+ multivariate_strategy=config.multivariate_strategy,
98
+ )
99
+
100
+ support_arrays = {
101
+ key: support for key, support in zip(config.inputs, support_arrays_list) # type: ignore[union-attr, arg-type]
102
+ }
103
+
104
+ num_uploads = {key: value + 1 for key, value in config.num_repeats.items()} # type: ignore[union-attr]
105
+
106
+ if config.param_prefix is None:
107
+ param_prefixes = {feature: None for feature in config.inputs} # type: ignore[union-attr]
108
+ else:
109
+ param_prefixes = {feature: f"{config.param_prefix}_{feature}" for feature in config.inputs} # type: ignore
110
+
111
+ fm_blocks: list[AbstractBlock] = []
112
+
113
+ for i in range(max(num_uploads.values())):
114
+ for feature in config.inputs: # type: ignore[union-attr]
115
+ if num_uploads[feature] > 0:
116
+ fm_blocks.append(
117
+ feature_map(
118
+ num_qubits,
119
+ support=support_arrays[feature],
120
+ param=feature,
121
+ op=config.operation, # type: ignore[arg-type]
122
+ fm_type=config.basis_set[feature], # type: ignore[arg-type, index]
123
+ reupload_scaling=config.reupload_scaling[feature], # type: ignore[arg-type, index]
124
+ feature_range=config.feature_range[feature], # type: ignore[arg-type, index]
125
+ target_range=config.target_range[feature], # type: ignore[arg-type, index]
126
+ param_prefix=param_prefixes[feature],
127
+ )
128
+ )
129
+ num_uploads[feature] -= 1
130
+
131
+ return fm_blocks
132
+
133
+
134
+ def _encode_features_parallel_digital(
135
+ register: int | Register,
136
+ config: FeatureMapConfig,
137
+ ) -> list[AbstractBlock]:
138
+ """
139
+ Encode the features in parallel using digital feature map.
140
+
141
+ Args:
142
+ register (int | Register): The number of qubits or the register object.
143
+ config (FeatureMapConfig): The feature map configuration.
144
+
145
+ Returns:
146
+ list[AbstractBlock]: The list of digital feature map blocks.
147
+ """
148
+ num_qubits = register if isinstance(register, int) else register.n_qubits
149
+
150
+ support_arrays_list = _create_support_arrays(
151
+ num_qubits=num_qubits,
152
+ num_features=config.num_features,
153
+ multivariate_strategy=config.multivariate_strategy,
154
+ )
155
+
156
+ support_arrays = {
157
+ key: support for key, support in zip(config.inputs, support_arrays_list) # type: ignore[union-attr, arg-type]
158
+ }
159
+
160
+ num_uploads = {key: value + 1 for key, value in config.num_repeats.items()} # type: ignore[union-attr]
161
+
162
+ if config.param_prefix is None:
163
+ param_prefixes = {feature: None for feature in config.inputs} # type: ignore[union-attr]
164
+ else:
165
+ param_prefixes = {feature: f"{config.param_prefix}_{feature}" for feature in config.inputs} # type: ignore
166
+
167
+ fm_blocks: list[AbstractBlock] = []
168
+
169
+ for i in range(max(num_uploads.values())):
170
+ fm_layer = []
171
+ for feature in config.inputs: # type: ignore[union-attr]
172
+ if num_uploads[feature] > 0:
173
+ fm_layer.append(
174
+ feature_map(
175
+ len(support_arrays[feature]),
176
+ support=support_arrays[feature],
177
+ param=feature,
178
+ op=config.operation, # type: ignore[arg-type]
179
+ fm_type=config.basis_set[feature], # type: ignore[index]
180
+ reupload_scaling=config.reupload_scaling[feature], # type: ignore[index]
181
+ feature_range=config.feature_range[feature], # type: ignore[arg-type, index]
182
+ target_range=config.target_range[feature], # type: ignore[arg-type, index]
183
+ param_prefix=param_prefixes[feature],
184
+ )
185
+ )
186
+ num_uploads[feature] -= 1
187
+
188
+ fm_blocks.append(kron(*fm_layer))
189
+
190
+ return fm_blocks
191
+
192
+
193
+ def _create_digital_fm(
194
+ register: int | Register,
195
+ config: FeatureMapConfig,
196
+ ) -> list[AbstractBlock]:
197
+ """
198
+ Create the digital feature map.
199
+
200
+ Args:
201
+ register (int | Register): The number of qubits or the register object.
202
+ config (FeatureMapConfig): The feature map configuration.
203
+
204
+ Returns:
205
+ list[AbstractBlock]: The list of digital feature map blocks.
206
+
207
+ Raises:
208
+ ValueError: If the encoding strategy is invalid. Only 'series' or 'parallel' are allowed.
209
+ """
210
+ if config.multivariate_strategy == MultivariateStrategy.SERIES:
211
+ fm_blocks = _encode_features_series_digital(register, config)
212
+ elif config.multivariate_strategy == MultivariateStrategy.PARALLEL:
213
+ fm_blocks = _encode_features_parallel_digital(register, config)
214
+ else:
215
+ raise ValueError(
216
+ f"Invalid encoding strategy {config.multivariate_strategy} provided. Only 'series' or \
217
+ 'parallel' are allowed."
218
+ )
219
+
220
+ return fm_blocks
221
+
222
+
223
+ def _create_analog_fm(
224
+ register: int | Register,
225
+ config: FeatureMapConfig,
226
+ ) -> list[AbstractBlock]:
227
+ """
228
+ Create the analog feature map.
229
+
230
+ Args:
231
+ register (int | Register): The number of qubits or the register object.
232
+ config (FeatureMapConfig): The feature map configuration.
233
+
234
+ Returns:
235
+ list[AbstractBlock]: The list of analog feature map blocks.
236
+ """
237
+
238
+ num_uploads = {key: value + 1 for key, value in config.num_repeats.items()} # type: ignore[union-attr]
239
+
240
+ fm_blocks: list[AbstractBlock] = []
241
+
242
+ for i in range(max(num_uploads.values())):
243
+ for feature in config.inputs: # type: ignore[union-attr]
244
+ if num_uploads[feature] > 0:
245
+ fm_blocks.append(
246
+ analog_feature_map(
247
+ param=feature,
248
+ op=config.operation, # type: ignore[arg-type]
249
+ fm_type=config.basis_set[feature], # type: ignore[arg-type, index]
250
+ reupload_scaling=config.reupload_scaling[feature], # type: ignore[arg-type, index]
251
+ feature_range=config.feature_range[feature], # type: ignore[arg-type, index]
252
+ target_range=config.target_range[feature], # type: ignore[arg-type, index]
253
+ )
254
+ )
255
+ num_uploads[feature] -= 1
256
+
257
+ return fm_blocks
258
+
259
+
260
+ def _encode_feature_rydberg(
261
+ num_qubits: int,
262
+ param: str,
263
+ reupload_scaling: ReuploadScaling,
264
+ ) -> AbstractBlock:
265
+ """
266
+ Encode features using a Rydberg feature map.
267
+
268
+ Args:
269
+ num_qubits (int): The number of qubits to encode the features on.
270
+ param (str): The parameter prefix to use for the feature map parameter names.
271
+ reupload_scaling (ReuploadScaling): The scaling strategy for reuploads.
272
+
273
+ Returns:
274
+ The Rydberg feature map.
275
+
276
+ Raises:
277
+ NotImplementedError: If the reupload scaling strategy is not implemented.
278
+ Only `ReuploadScaling.CONSTANT` and `ReuploadScaling.TOWER` are supported.
279
+ """
280
+ if reupload_scaling == ReuploadScaling.CONSTANT:
281
+ return rydberg_feature_map(n_qubits=num_qubits, param=param)
282
+
283
+ elif reupload_scaling == ReuploadScaling.TOWER:
284
+ return rydberg_tower_feature_map(n_qubits=num_qubits, param=param)
285
+
286
+ else:
287
+ raise NotImplementedError(f"Rydberg feature map not implemented for {reupload_scaling}")
288
+
289
+
290
+ def _create_rydberg_fm(
291
+ register: int | Register,
292
+ config: FeatureMapConfig,
293
+ ) -> list[AbstractBlock]:
294
+ """
295
+ Create a Rydberg feature map for the given configuration.
296
+
297
+ Args:
298
+ register (int | Register): The number of qubits or the register to apply the feature map to.
299
+ config (FeatureMapConfig): The configuration for the feature map.
300
+
301
+ Returns:
302
+ list: A list of Rydberg feature map blocks.
303
+ """
304
+ num_qubits = register if isinstance(register, int) else register.n_qubits
305
+
306
+ num_uploads = {key: value + 1 for key, value in config.num_repeats.items()} # type: ignore[union-attr]
307
+
308
+ fm_blocks = []
309
+
310
+ for i in range(max(num_uploads.values())):
311
+ for feature in config.inputs: # type: ignore[union-attr]
312
+ if num_uploads[feature] > 0:
313
+ fm_blocks.append(
314
+ _encode_feature_rydberg(
315
+ num_qubits=num_qubits,
316
+ param=feature,
317
+ reupload_scaling=config.reupload_scaling[feature], # type: ignore[arg-type, index]
318
+ )
319
+ )
320
+ num_uploads[feature] -= 1
321
+
322
+ return fm_blocks
323
+
324
+
325
+ def create_fm_blocks(
326
+ register: int | Register,
327
+ config: FeatureMapConfig,
328
+ ) -> list[AbstractBlock]:
329
+ """
330
+ Create a list of feature map blocks based on the given configuration.
331
+
332
+ In case of series encoding or even parallel encoding with data reuploads,
333
+ the outputs is a list of blocks that still need to be interleaved with non
334
+ commuting blocks.
335
+
336
+ Args:
337
+ register (int | Register): The number of qubits or the register.
338
+ config (FeatureMapConfig): The configuration for the feature map.
339
+
340
+ Returns:
341
+ list[AbstractBlock]: A list of feature map blocks.
342
+
343
+ Raises:
344
+ ValueError: If the feature map strategy is not 'digital', 'analog' or 'rydberg'.
345
+ """
346
+ if config.feature_map_strategy == Strategy.DIGITAL:
347
+ return _create_digital_fm(register=register, config=config)
348
+ elif config.feature_map_strategy == Strategy.ANALOG:
349
+ return _create_analog_fm(register=register, config=config)
350
+ elif config.feature_map_strategy == Strategy.RYDBERG:
351
+ return _create_rydberg_fm(register=register, config=config)
352
+ else:
353
+ raise NotImplementedError(
354
+ f"Feature map not implemented for strategy {config.feature_map_strategy}. \
355
+ Only 'digital', 'analog' or 'rydberg' allowed."
356
+ )
357
+
358
+
359
+ def _ansatz_layer(
360
+ register: int | Register,
361
+ ansatz_config: AnsatzConfig,
362
+ index: int,
363
+ ) -> AbstractBlock:
364
+ """
365
+ Create a layer of the ansatz based on the configuration.
366
+
367
+ Args:
368
+ register (int | Register): The number of qubits or the register.
369
+ ansatz_config (AnsatzConfig): The configuration for the ansatz.
370
+ index (int): The index of the layer.
371
+
372
+ Returns:
373
+ AbstractBlock: The layer of the ansatz.
374
+ """
375
+ new_config = AnsatzConfig(
376
+ depth=1,
377
+ ansatz_type=ansatz_config.ansatz_type,
378
+ ansatz_strategy=ansatz_config.ansatz_strategy,
379
+ strategy_args=ansatz_config.strategy_args,
380
+ param_prefix=f"fm_{index}",
381
+ )
382
+
383
+ return create_ansatz(register=register, config=new_config)
384
+
385
+
386
+ def _create_iia_digital(
387
+ num_qubits: int,
388
+ config: AnsatzConfig,
389
+ ) -> AbstractBlock:
390
+ """
391
+ Create the Digital Identity Initialized Ansatz based on the configuration.
392
+
393
+ Args:
394
+ num_qubits (int): The number of qubits.
395
+ config (AnsatzConfig): The configuration for the ansatz.
396
+
397
+ Returns:
398
+ AbstractBlock: The Identity Initialized Ansatz.
399
+ """
400
+ operations = config.strategy_args.get("operations", [RX, RY])
401
+ entangler = config.strategy_args.get("entangler", CNOT)
402
+ periodic = config.strategy_args.get("periodic", False)
403
+
404
+ return identity_initialized_ansatz(
405
+ n_qubits=num_qubits,
406
+ depth=config.depth,
407
+ param_prefix=config.param_prefix,
408
+ strategy=Strategy.DIGITAL,
409
+ rotations=operations,
410
+ entangler=entangler,
411
+ periodic=periodic,
412
+ )
413
+
414
+
415
+ def _create_iia_sdaqc(
416
+ num_qubits: int,
417
+ config: AnsatzConfig,
418
+ ) -> AbstractBlock:
419
+ """
420
+ Create the SDAQC Identity Initialized Ansatz based on the configuration.
421
+
422
+ Args:
423
+ num_qubits (int): The number of qubits.
424
+ config (AnsatzConfig): The configuration for the ansatz.
425
+
426
+ Returns:
427
+ AbstractBlock: The SDAQC Identity Initialized Ansatz.
428
+ """
429
+ operations = config.strategy_args.get("operations", [RX, RY])
430
+ entangler = config.strategy_args.get("entangler", CNOT)
431
+ periodic = config.strategy_args.get("periodic", False)
432
+
433
+ return identity_initialized_ansatz(
434
+ n_qubits=num_qubits,
435
+ depth=config.depth,
436
+ param_prefix=config.param_prefix,
437
+ strategy=Strategy.SDAQC,
438
+ rotations=operations,
439
+ entangler=entangler,
440
+ periodic=periodic,
441
+ )
442
+
443
+
444
+ def _create_iia(
445
+ num_qubits: int,
446
+ config: AnsatzConfig,
447
+ ) -> AbstractBlock:
448
+ """
449
+ Create the Identity Initialized Ansatz based on the configuration.
450
+
451
+ Args:
452
+ num_qubits (int): The number of qubits.
453
+ config (AnsatzConfig): The configuration for the ansatz.
454
+
455
+ Returns:
456
+ AbstractBlock: The Identity Initialized Ansatz.
457
+
458
+ Raises:
459
+ ValueError: If the ansatz strategy is not supported. Only 'digital' and 'sdaqc' are allowed.
460
+ """
461
+ if config.ansatz_strategy == Strategy.DIGITAL:
462
+ return _create_iia_digital(num_qubits=num_qubits, config=config)
463
+ elif config.ansatz_strategy == Strategy.SDAQC:
464
+ return _create_iia_sdaqc(num_qubits=num_qubits, config=config)
465
+ else:
466
+ raise ValueError(
467
+ f"Invalid ansatz strategy {config.ansatz_strategy} provided. Only 'digital', 'sdaqc', \
468
+ allowed for IIA."
469
+ )
470
+
471
+
472
+ def _create_hea_digital(num_qubits: int, config: AnsatzConfig) -> AbstractBlock:
473
+ """
474
+ Create the Digital Hardware Efficient Ansatz based on the configuration.
475
+
476
+ Args:
477
+ num_qubits (int): The number of qubits.
478
+ config (AnsatzConfig): The configuration for the ansatz.
479
+
480
+ Returns:
481
+ AbstractBlock: The Digital Hardware Efficient Ansatz.
482
+ """
483
+ operations = config.strategy_args.get("rotations", [RX, RY, RX])
484
+ entangler = config.strategy_args.get("entangler", CNOT)
485
+ periodic = config.strategy_args.get("periodic", False)
486
+
487
+ return hea_digital(
488
+ n_qubits=num_qubits,
489
+ depth=config.depth,
490
+ param_prefix=config.param_prefix,
491
+ operations=operations,
492
+ entangler=entangler,
493
+ periodic=periodic,
494
+ )
495
+
496
+
497
+ def _create_hea_sdaqc(num_qubits: int, config: AnsatzConfig) -> AbstractBlock:
498
+ """
499
+ Create the SDAQC Hardware Efficient Ansatz based on the configuration.
500
+
501
+ Args:
502
+ num_qubits (int): The number of qubits.
503
+ config (AnsatzConfig): The configuration for the ansatz.
504
+
505
+ Returns:
506
+ AbstractBlock: The SDAQC Hardware Efficient Ansatz.
507
+ """
508
+ operations = config.strategy_args.get("rotations", [RX, RY, RX])
509
+ entangler = config.strategy_args.get(
510
+ "entangler", hamiltonian_factory(num_qubits, interaction=Interaction.NN)
511
+ )
512
+
513
+ return hea_sDAQC(
514
+ n_qubits=num_qubits,
515
+ depth=config.depth,
516
+ param_prefix=config.param_prefix,
517
+ operations=operations,
518
+ entangler=entangler,
519
+ )
520
+
521
+
522
+ def _create_hea_rydberg(
523
+ register: int | Register,
524
+ config: AnsatzConfig,
525
+ ) -> AbstractBlock:
526
+ """
527
+ Create the Rydberg Hardware Efficient Ansatz based on the configuration.
528
+
529
+ Args:
530
+ register (int | Register): The number of qubits or the register object.
531
+ config (AnsatzConfig): The configuration for the ansatz.
532
+
533
+ Returns:
534
+ AbstractBlock: The Rydberg Hardware Efficient Ansatz.
535
+ """
536
+ register = register if isinstance(register, Register) else Register.circle(n_qubits=register)
537
+
538
+ addressable_detuning = config.strategy_args.get("addressable_detuning", True)
539
+ addressable_drive = config.strategy_args.get("addressable_drive", False)
540
+ tunable_phase = config.strategy_args.get("tunable_phase", False)
541
+
542
+ return rydberg_hea(
543
+ register=register,
544
+ n_layers=config.depth,
545
+ addressable_detuning=addressable_detuning,
546
+ addressable_drive=addressable_drive,
547
+ tunable_phase=tunable_phase,
548
+ additional_prefix=config.param_prefix,
549
+ )
550
+
551
+
552
+ def _create_hea_ansatz(
553
+ register: int | Register,
554
+ config: AnsatzConfig,
555
+ ) -> AbstractBlock:
556
+ """
557
+ Create the Hardware Efficient Ansatz based on the configuration.
558
+
559
+ Args:
560
+ register (int | Register): The number of qubits or the register to create the ansatz for.
561
+ config (AnsatzConfig): The configuration for the ansatz.
562
+
563
+ Returns:
564
+ AbstractBlock: The hardware efficient ansatz block.
565
+
566
+ Raises:
567
+ ValueError: If the ansatz strategy is not 'digital', 'sdaqc', or 'rydberg'.
568
+ """
569
+ num_qubits = register if isinstance(register, int) else register.n_qubits
570
+
571
+ if config.ansatz_strategy == Strategy.DIGITAL:
572
+ return _create_hea_digital(num_qubits=num_qubits, config=config)
573
+ elif config.ansatz_strategy == Strategy.SDAQC:
574
+ return _create_hea_sdaqc(num_qubits=num_qubits, config=config)
575
+ elif config.ansatz_strategy == Strategy.RYDBERG:
576
+ return _create_hea_rydberg(register=register, config=config)
577
+ else:
578
+ raise ValueError(
579
+ f"Invalid ansatz strategy {config.ansatz_strategy} provided. Only 'digital', 'sdaqc', \
580
+ and 'rydberg' allowed"
581
+ )
582
+
583
+
584
+ def create_ansatz(
585
+ register: int | Register,
586
+ config: AnsatzConfig,
587
+ ) -> AbstractBlock:
588
+ """
589
+ Create the ansatz based on the configuration.
590
+
591
+ Args:
592
+ register (int | Register): Number of qubits or a register object.
593
+ config (AnsatzConfig): Configuration for the ansatz.
594
+
595
+ Returns:
596
+ AbstractBlock: The ansatz block.
597
+
598
+ Raises:
599
+ NotImplementedError: If the ansatz type is not implemented.
600
+ """
601
+ num_qubits = register if isinstance(register, int) else register.n_qubits
602
+
603
+ if config.ansatz_type == AnsatzType.IIA:
604
+ return _create_iia(num_qubits=num_qubits, config=config)
605
+ elif config.ansatz_type == AnsatzType.HEA:
606
+ return _create_hea_ansatz(register=register, config=config)
607
+ else:
608
+ raise NotImplementedError(
609
+ f"Ansatz of type {config.ansatz_type} not implemented yet. Only 'hea' and\
610
+ 'iia' available."
611
+ )
612
+
613
+
614
+ def _interleave_ansatz_in_fm(
615
+ register: int | Register,
616
+ fm_blocks: list[AbstractBlock],
617
+ ansatz_config: AnsatzConfig,
618
+ ) -> ChainBlock:
619
+ """
620
+ Interleave the ansatz layers in between the feature map layers.
621
+
622
+ Args:
623
+ register (int | Register): Number of qubits or a register object.
624
+ fm_blocks (list[AbstractBlock]): List of feature map blocks.
625
+ ansatz_config (AnsatzConfig): Ansatz configuration.
626
+
627
+ Returns:
628
+ ChainBlock: A block containing feature map layers with interleaved ansatz layers.
629
+ """
630
+ full_fm = []
631
+ for idx, block in enumerate(fm_blocks):
632
+ full_fm.append(block)
633
+ if idx + 1 < len(fm_blocks):
634
+ full_fm.append(_ansatz_layer(register, ansatz_config, idx))
635
+
636
+ return chain(*full_fm)
637
+
638
+
639
+ def load_observable_transformations(config: ObservableConfig) -> tuple[Parameter, Parameter]:
640
+ """
641
+ Get the observable shifting and scaling factors.
642
+
643
+ Args:
644
+ config (ObservableConfig): Observable configuration.
645
+
646
+ Returns:
647
+ tuple[float, float]: The observable shifting and scaling factors.
648
+ """
649
+ shift = config.shift
650
+ scale = config.scale
651
+ if config.trainable_transform is not None:
652
+ shift = Parameter(name=shift, trainable=config.trainable_transform)
653
+ scale = Parameter(name=scale, trainable=config.trainable_transform)
654
+ else:
655
+ shift = Parameter(shift)
656
+ scale = Parameter(scale)
657
+ return scale, shift
658
+
659
+
660
+ ObservableTransformMap = {
661
+ ObservableTransform.RANGE: lambda detuning, scale, shift: (shift, shift - scale)
662
+ if detuning is N
663
+ else (0.5 * (shift - scale), 0.5 * (scale + shift)),
664
+ ObservableTransform.SCALE: lambda _, scale, shift: (scale, shift),
665
+ }
666
+
667
+
668
+ def _global_identity(register: int | Register) -> KronBlock:
669
+ """Create a global identity block."""
670
+ return kron(
671
+ *[I(i) for i in range(register if isinstance(register, int) else register.n_qubits)]
672
+ )
673
+
674
+
675
+ def observable_from_config(
676
+ register: int | Register,
677
+ config: ObservableConfig,
678
+ ) -> AbstractBlock:
679
+ """
680
+ Create an observable block.
681
+
682
+ Args:
683
+ register (int | Register): Number of qubits or a register object.
684
+ config (ObservableConfig): Observable configuration.
685
+
686
+ Returns:
687
+ AbstractBlock: The observable block.
688
+ """
689
+ scale, shift = load_observable_transformations(config)
690
+ return create_observable(register, config.detuning, scale, shift, config.transformation_type)
691
+
692
+
693
+ def create_observable(
694
+ register: int | Register,
695
+ detuning: TDetuning = Z,
696
+ scale: TParameter | None = None,
697
+ shift: TParameter | None = None,
698
+ transformation_type: ObservableTransform = ObservableTransform.NONE, # type: ignore[assignment]
699
+ ) -> AbstractBlock:
700
+ """
701
+ Create an observable block.
702
+
703
+ Args:
704
+ register (int | Register): Number of qubits or a register object.
705
+ detuning: The type of detuning.
706
+ scale: A parameter for the scale.
707
+ shift: A parameter for the shift.
708
+
709
+ Returns:
710
+ AbstractBlock: The observable block.
711
+ """
712
+ if transformation_type == ObservableTransform.RANGE:
713
+ scale, shift = ObservableTransformMap[transformation_type](detuning, scale, shift) # type: ignore[index]
714
+ shifting_term = shift * _global_identity(register) # type: ignore[operator]
715
+ detuning_hamiltonian = scale * hamiltonian_factory( # type: ignore[operator]
716
+ register=register,
717
+ detuning=detuning,
718
+ )
719
+ return add(shifting_term, detuning_hamiltonian)
720
+
721
+
722
+ def build_qnn_from_configs(
723
+ register: int | Register,
724
+ fm_config: FeatureMapConfig,
725
+ ansatz_config: AnsatzConfig,
726
+ observable_config: ObservableConfig | list[ObservableConfig],
727
+ ) -> QNN:
728
+ """
729
+ Build a QNN model.
730
+
731
+ Args:
732
+ register (int | Register): Number of qubits or a register object.
733
+ fm_config (FeatureMapConfig): Feature map configuration.
734
+ ansatz_config (AnsatzConfig): Ansatz configuration.
735
+ observable_config (ObservableConfig): Observable configuration.
736
+
737
+ Returns:
738
+ QNN: A QNN model.
739
+ """
740
+ fm_blocks = create_fm_blocks(register=register, config=fm_config)
741
+ full_fm = _interleave_ansatz_in_fm(
742
+ register=register,
743
+ fm_blocks=fm_blocks,
744
+ ansatz_config=ansatz_config,
745
+ )
746
+
747
+ ansatz = create_ansatz(register=register, config=ansatz_config)
748
+
749
+ # Add a block before the Featuer Map to move from 0 state to an
750
+ # equal superposition of all states. This needs to be here only for rydberg
751
+ # feature map and only as long as the feature map is not updated to include
752
+ # a driving term in the Hamiltonian.
753
+
754
+ if ansatz_config.ansatz_strategy == "rydberg":
755
+ num_qubits = register if isinstance(register, int) else register.n_qubits
756
+ mixing_block = kron(*[H(i) for i in range(num_qubits)])
757
+ full_fm = chain(mixing_block, full_fm)
758
+
759
+ circ = QuantumCircuit(
760
+ register,
761
+ full_fm,
762
+ ansatz,
763
+ )
764
+
765
+ if isinstance(observable_config, list):
766
+ observable = [
767
+ observable_from_config(register=register, config=cfg) for cfg in observable_config
768
+ ]
769
+ else:
770
+ observable = observable_from_config(register=register, config=observable_config) # type: ignore[assignment]
771
+
772
+ ufa = QNN(circ, observable, inputs=fm_config.inputs)
773
+
774
+ return ufa