• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Qiskit / qiskit / 13540158629

26 Feb 2025 09:01AM UTC coverage: 87.854% (-0.7%) from 88.599%
13540158629

Pull #12814

github

web-flow
Merge f02a7b9b8 into 7169f6db0
Pull Request #12814: Light Cone Transpiler Pass

79 of 81 new or added lines in 2 files covered. (97.53%)

2576 existing lines in 133 files now uncovered.

77200 of 87873 relevant lines covered (87.85%)

339322.89 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.02
/crates/circuit/src/parameter_table.rs
1
// This code is part of Qiskit.
2
//
3
// (C) Copyright IBM 2024
4
//
5
// This code is licensed under the Apache License, Version 2.0. You may
6
// obtain a copy of this license in the LICENSE.txt file in the root directory
7
// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
8
//
9
// Any modifications or derivative works of this code must retain this
10
// copyright notice, and modified files need to carry a notice indicating
11
// that they have been altered from the originals.
12

13
use std::sync::OnceLock;
14

15
use hashbrown::hash_map::Entry;
16
use hashbrown::{HashMap, HashSet};
17
use thiserror::Error;
18

19
use pyo3::exceptions::PyTypeError;
20
use pyo3::prelude::*;
21
use pyo3::pybacked::PyBackedStr;
22
use pyo3::types::{PyList, PySet};
23
use pyo3::{import_exception, intern, PyTraverseError, PyVisit};
24

25
use crate::imports::UUID;
26

27
import_exception!(qiskit.circuit, CircuitError);
28

29
#[derive(Error, Debug)]
30
pub enum ParameterTableError {
31
    #[error("parameter '{0:?}' is not tracked in the table")]
32
    ParameterNotTracked(ParameterUuid),
33
    #[error("usage {0:?} is not tracked by the table")]
34
    UsageNotTracked(ParameterUse),
35
}
36
impl From<ParameterTableError> for PyErr {
37
    fn from(value: ParameterTableError) -> PyErr {
×
38
        CircuitError::new_err(value.to_string())
×
39
    }
×
40
}
41

42
/// A single use of a symbolic parameter.
43
#[derive(Clone, Copy, Hash, PartialEq, Eq, Debug)]
44
pub enum ParameterUse {
45
    Index { instruction: usize, parameter: u32 },
46
    GlobalPhase,
47
}
48

49
/// Rust-space extra information that a `ParameterVectorElement` has.  This is used most heavily
50
/// during sorting; vector elements are sorted by their parent name, and index within that.
51
#[derive(Clone, Debug)]
52
struct VectorElement {
53
    vector_uuid: VectorUuid,
54
    index: usize,
55
}
56

57
/// Tracked data tied to each parameter's UUID in the table.
58
#[derive(Clone, Debug)]
59
pub struct ParameterInfo {
60
    uses: HashSet<ParameterUse>,
61
    name: PyBackedStr,
62
    element: Option<VectorElement>,
63
    object: Py<PyAny>,
64
}
65

66
/// Rust-space information on a Python `ParameterVector` and its uses in the table.
67
#[derive(Clone, Debug)]
68
struct VectorInfo {
69
    name: PyBackedStr,
70
    /// Number of elements of the vector tracked within the parameter table.
71
    refcount: usize,
72
}
73

74
/// Type-safe UUID for a symbolic parameter.  This does not track the name of the `Parameter`; it
75
/// can't be used alone to reconstruct a Python instance.  That tracking remains only withing the
76
/// `ParameterTable`.
77
#[derive(Clone, Copy, Hash, PartialEq, Eq, Debug)]
78
pub struct ParameterUuid(u128);
79
impl ParameterUuid {
80
    /// Extract a UUID from a Python-space `Parameter` object. This assumes that the object is known
81
    /// to be a parameter.
82
    pub fn from_parameter(ob: &Bound<PyAny>) -> PyResult<Self> {
185,088✔
83
        ob.getattr(intern!(ob.py(), "_uuid"))?.extract()
185,088✔
84
    }
185,088✔
85
}
86

87
/// This implementation of `FromPyObject` is for the UUID itself, which is what the `ParameterUuid`
88
/// struct actually encapsulates.
89
impl<'py> FromPyObject<'py> for ParameterUuid {
90
    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
185,088✔
91
        if ob.is_exact_instance(UUID.get_bound(ob.py())) {
185,088✔
92
            ob.getattr(intern!(ob.py(), "int"))?.extract().map(Self)
185,088✔
93
        } else {
94
            Err(PyTypeError::new_err("not a UUID"))
×
95
        }
96
    }
185,088✔
97
}
98

99
/// Type-safe UUID for a parameter vector.  This is just used internally for tracking.
100
#[derive(Clone, Copy, Hash, PartialEq, Eq, Debug)]
101
struct VectorUuid(u128);
102
impl VectorUuid {
103
    /// Extract a UUID from a Python-space `ParameterVector` object. This assumes that the object is
104
    /// the correct type.
105
    fn from_vector(ob: &Bound<PyAny>) -> PyResult<Self> {
74,582✔
106
        ob.getattr(intern!(ob.py(), "_root_uuid"))?.extract()
74,582✔
107
    }
74,582✔
108
}
109
impl<'py> FromPyObject<'py> for VectorUuid {
110
    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
74,582✔
111
        if ob.is_exact_instance(UUID.get_bound(ob.py())) {
74,582✔
112
            ob.getattr(intern!(ob.py(), "int"))?.extract().map(Self)
74,582✔
113
        } else {
114
            Err(PyTypeError::new_err("not a UUID"))
×
115
        }
116
    }
74,582✔
117
}
118

119
#[derive(Clone, Default, Debug)]
120
pub struct ParameterTable {
121
    /// Mapping of the parameter key (its UUID) to the information on it tracked by this table.
122
    by_uuid: HashMap<ParameterUuid, ParameterInfo>,
123
    /// Mapping of the parameter names to the UUID that represents them.  Since we always get
124
    /// parameters in as Python-space objects, we use the string object extracted from Python space.
125
    by_name: HashMap<PyBackedStr, ParameterUuid>,
126
    /// Additional information on any `ParameterVector` instances that have elements in the circuit.
127
    vectors: HashMap<VectorUuid, VectorInfo>,
128
    /// Cache of the sort order of the parameters.  This is lexicographical for most parameters,
129
    /// except elements of a `ParameterVector` are sorted within the vector by numerical index.  We
130
    /// calculate this on demand and cache it.
131
    ///
132
    /// Any method that adds or removes a parameter needs to invalidate this.
133
    order_cache: OnceLock<Vec<ParameterUuid>>,
134
    /// Cache of a Python-space list of the parameter objects, in order.  We only generate this
135
    /// specifically when asked.
136
    ///
137
    /// Any method that adds or removes a parameter needs to invalidate this.
138
    py_parameters_cache: OnceLock<Py<PyList>>,
139
}
140

141
impl ParameterTable {
142
    pub fn new() -> Self {
3,091,690✔
143
        Default::default()
3,091,690✔
144
    }
3,091,690✔
145

146
    /// Get the number of parameters tracked by the table.
147
    pub fn num_parameters(&self) -> usize {
20,576✔
148
        self.by_uuid.len()
20,576✔
149
    }
20,576✔
150

151
    /// Add a new usage of a parameter coming in from Python space, optionally adding a first usage
152
    /// to it.
153
    ///
154
    /// The no-use form is useful when doing parameter assignments from Rust space, where the
155
    /// replacement is itself parametric; the replacement can be extracted once, then subsequent
156
    /// lookups and updates done without interaction with Python.
157
    pub fn track(
90,092✔
158
        &mut self,
90,092✔
159
        param_ob: &Bound<PyAny>,
90,092✔
160
        usage: Option<ParameterUse>,
90,092✔
161
    ) -> PyResult<ParameterUuid> {
90,092✔
162
        let py = param_ob.py();
90,092✔
163
        let uuid = ParameterUuid::from_parameter(param_ob)?;
90,092✔
164
        match self.by_uuid.entry(uuid) {
90,092✔
165
            Entry::Occupied(mut entry) => {
7,312✔
166
                if let Some(usage) = usage {
7,312✔
167
                    entry.get_mut().uses.insert(usage);
7,242✔
168
                }
7,242✔
169
            }
170
            Entry::Vacant(entry) => {
82,780✔
171
                let py_name_attr = intern!(py, "name");
82,780✔
172
                let name = param_ob.getattr(py_name_attr)?.extract::<PyBackedStr>()?;
82,780✔
173
                if self.by_name.contains_key(&name) {
82,780✔
174
                    return Err(CircuitError::new_err(format!(
4✔
175
                        "name conflict adding parameter '{}'",
4✔
176
                        &name
4✔
177
                    )));
4✔
178
                }
82,776✔
179
                let element = if let Ok(vector) = param_ob.getattr(intern!(py, "vector")) {
82,776✔
180
                    let vector_uuid = VectorUuid::from_vector(&vector)?;
74,582✔
181
                    match self.vectors.entry(vector_uuid) {
74,582✔
182
                        Entry::Occupied(mut entry) => entry.get_mut().refcount += 1,
54,724✔
183
                        Entry::Vacant(entry) => {
19,858✔
184
                            entry.insert(VectorInfo {
19,858✔
185
                                name: vector.getattr(py_name_attr)?.extract()?,
19,858✔
186
                                refcount: 1,
187
                            });
188
                        }
189
                    }
190
                    Some(VectorElement {
191
                        vector_uuid,
74,582✔
192
                        index: param_ob.getattr(intern!(py, "index"))?.extract()?,
74,582✔
193
                    })
194
                } else {
195
                    None
8,194✔
196
                };
197
                self.by_name.insert(name.clone(), uuid);
82,776✔
198
                let mut uses = HashSet::new();
82,776✔
199
                if let Some(usage) = usage {
82,776✔
200
                    unsafe {
54,684✔
201
                        uses.insert_unique_unchecked(usage);
54,684✔
202
                    }
54,684✔
203
                };
28,092✔
204
                entry.insert(ParameterInfo {
82,776✔
205
                    name,
82,776✔
206
                    uses,
82,776✔
207
                    element,
82,776✔
208
                    object: param_ob.clone().unbind(),
82,776✔
209
                });
82,776✔
210
                self.invalidate_cache();
82,776✔
211
            }
212
        }
213
        Ok(uuid)
90,088✔
214
    }
90,092✔
215

216
    /// Untrack one use of a single Python-space `Parameter` object from the table, discarding all
217
    /// other tracking of that `Parameter` if this was the last usage of it.
218
    pub fn untrack(&mut self, param_ob: &Bound<PyAny>, usage: ParameterUse) -> PyResult<()> {
4✔
219
        self.remove_use(ParameterUuid::from_parameter(param_ob)?, usage)
4✔
220
            .map_err(PyErr::from)
4✔
221
    }
4✔
222

223
    /// Lookup the Python parameter object by name.
224
    pub fn py_parameter_by_name(&self, name: &PyBackedStr) -> Option<&Py<PyAny>> {
35,596✔
225
        self.by_name
35,596✔
226
            .get(name)
35,596✔
227
            .map(|uuid| &self.by_uuid[uuid].object)
35,596✔
228
    }
35,596✔
229

230
    /// Lookup the Python parameter object by uuid.
231
    pub fn py_parameter_by_uuid(&self, uuid: ParameterUuid) -> Option<&Py<PyAny>> {
43,518✔
232
        self.by_uuid.get(&uuid).map(|param| &param.object)
43,518✔
233
    }
43,518✔
234

235
    /// Get the (maybe cached) Python list of the sorted `Parameter` objects.
236
    pub fn py_parameters<'py>(&self, py: Python<'py>) -> Bound<'py, PyList> {
375,298✔
237
        self.py_parameters_cache
375,298✔
238
            .get_or_init(|| {
375,298✔
239
                PyList::new(
178,794✔
240
                    py,
178,794✔
241
                    self.order_cache
178,794✔
242
                        .get_or_init(|| self.sorted_order())
178,794✔
243
                        .iter()
178,794✔
244
                        .map(|uuid| self.by_uuid[uuid].object.bind(py).clone()),
178,794✔
245
                )
178,794✔
246
                .unwrap()
178,794✔
247
                .unbind()
178,794✔
248
            })
375,298✔
249
            .bind(py)
375,298✔
250
            .clone()
375,298✔
251
    }
375,298✔
252

253
    /// Get a Python set of all tracked `Parameter` objects.
254
    pub fn py_parameters_unsorted<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PySet>> {
1,330✔
255
        PySet::new(py, self.by_uuid.values().map(|info| &info.object))
1,330✔
256
    }
1,330✔
257

258
    /// Get the sorted order of the `ParameterTable`.  This does not access the cache.
259
    fn sorted_order(&self) -> Vec<ParameterUuid> {
178,794✔
260
        let mut out = self.by_uuid.keys().copied().collect::<Vec<_>>();
178,794✔
261
        out.sort_unstable_by_key(|uuid| {
178,794✔
262
            let info = &self.by_uuid[uuid];
122,244✔
263
            if let Some(vec) = info.element.as_ref() {
122,244✔
264
                (&self.vectors[&vec.vector_uuid].name, vec.index)
121,616✔
265
            } else {
266
                (&info.name, 0)
628✔
267
            }
268
        });
178,794✔
269
        out
178,794✔
270
    }
178,794✔
271

272
    /// Add a use of a parameter to the table.
273
    pub fn add_use(
31,240✔
274
        &mut self,
31,240✔
275
        uuid: ParameterUuid,
31,240✔
276
        usage: ParameterUse,
31,240✔
277
    ) -> Result<(), ParameterTableError> {
31,240✔
278
        self.by_uuid
31,240✔
279
            .get_mut(&uuid)
31,240✔
280
            .ok_or(ParameterTableError::ParameterNotTracked(uuid))?
31,240✔
281
            .uses
282
            .insert(usage);
31,240✔
283
        Ok(())
31,240✔
284
    }
31,240✔
285

286
    /// Return a use of a parameter.
287
    ///
288
    /// If the last use a parameter is discarded, the parameter is untracked.
289
    pub fn remove_use(
21,674✔
290
        &mut self,
21,674✔
291
        uuid: ParameterUuid,
21,674✔
292
        usage: ParameterUse,
21,674✔
293
    ) -> Result<(), ParameterTableError> {
21,674✔
294
        let Entry::Occupied(mut entry) = self.by_uuid.entry(uuid) else {
21,674✔
295
            return Err(ParameterTableError::ParameterNotTracked(uuid));
20,040✔
296
        };
297
        let info = entry.get_mut();
1,634✔
298
        if !info.uses.remove(&usage) {
1,634✔
299
            return Err(ParameterTableError::UsageNotTracked(usage));
2✔
300
        }
1,632✔
301
        if info.uses.is_empty() {
1,632✔
302
            self.by_name.remove(&info.name);
1,412✔
303
            if let Some(vec) = info.element.as_ref() {
1,412✔
UNCOV
304
                let Entry::Occupied(mut vec_entry) = self.vectors.entry(vec.vector_uuid) else {
×
305
                    unreachable!()
×
306
                };
307
                vec_entry.get_mut().refcount -= 1;
×
308
                if vec_entry.get().refcount == 0 {
×
UNCOV
309
                    vec_entry.remove_entry();
×
UNCOV
310
                }
×
311
            }
1,412✔
312
            entry.remove_entry();
1,412✔
313
            self.invalidate_cache();
1,412✔
314
        }
220✔
315
        Ok(())
1,632✔
316
    }
21,674✔
317

318
    /// Remove a parameter from the table, returning the tracked uses of it.
319
    pub fn pop(
73,164✔
320
        &mut self,
73,164✔
321
        uuid: ParameterUuid,
73,164✔
322
    ) -> Result<HashSet<ParameterUse>, ParameterTableError> {
73,164✔
323
        let info = self
73,164✔
324
            .by_uuid
73,164✔
325
            .remove(&uuid)
73,164✔
326
            .ok_or(ParameterTableError::ParameterNotTracked(uuid))?;
73,164✔
327
        self.by_name
73,164✔
328
            .remove(&info.name)
73,164✔
329
            .expect("each parameter should be tracked by both UUID and name");
73,164✔
330
        if let Some(element) = info.element {
73,164✔
331
            self.vectors
27,750✔
332
                .entry(element.vector_uuid)
27,750✔
333
                .and_replace_entry_with(|_k, mut vector_info| {
27,750✔
334
                    vector_info.refcount -= 1;
27,750✔
335
                    (vector_info.refcount > 0).then_some(vector_info)
27,750✔
336
                });
27,750✔
337
        }
45,414✔
338
        self.invalidate_cache();
73,164✔
339
        Ok(info.uses)
73,164✔
340
    }
73,164✔
341

342
    /// Clear this table, yielding the Python parameter objects and their uses in sorted order.
343
    ///
344
    /// The clearing effect is eager and not dependent on the iteration.
345
    pub fn drain_ordered(
374✔
346
        &mut self,
374✔
347
    ) -> impl ExactSizeIterator<Item = (Py<PyAny>, HashSet<ParameterUse>)> {
374✔
348
        let order = self
374✔
349
            .order_cache
374✔
350
            .take()
374✔
351
            .unwrap_or_else(|| self.sorted_order());
374✔
352
        let by_uuid = ::std::mem::take(&mut self.by_uuid);
374✔
353
        self.by_name.clear();
374✔
354
        self.vectors.clear();
374✔
355
        self.py_parameters_cache.take();
374✔
356
        ParameterTableDrain {
374✔
357
            order: order.into_iter(),
374✔
358
            by_uuid,
374✔
359
        }
374✔
360
    }
374✔
361

362
    /// Empty this `ParameterTable` of all its contents.  This does not affect the capacities of the
363
    /// internal data storage.
364
    pub fn clear(&mut self) {
220,962✔
365
        self.by_uuid.clear();
220,962✔
366
        self.by_name.clear();
220,962✔
367
        self.vectors.clear();
220,962✔
368
        self.invalidate_cache();
220,962✔
369
    }
220,962✔
370

371
    fn invalidate_cache(&mut self) {
378,314✔
372
        self.order_cache.take();
378,314✔
373
        self.py_parameters_cache.take();
378,314✔
374
    }
378,314✔
375

376
    /// Expose the tracked data for a given parameter as directly as possible to Python space.
377
    ///
378
    /// This is only really intended for use in testing.
379
    pub(crate) fn _py_raw_entry(&self, param: Bound<PyAny>) -> PyResult<Py<PySet>> {
158✔
380
        let py = param.py();
158✔
381
        let uuid = ParameterUuid::from_parameter(&param)?;
158✔
382
        let info = self
158✔
383
            .by_uuid
158✔
384
            .get(&uuid)
158✔
385
            .ok_or(ParameterTableError::ParameterNotTracked(uuid))?;
158✔
386
        // PyO3's `PySet::new` only accepts iterables of references.
387
        let out = PySet::empty(py)?;
158✔
388
        for usage in info.uses.iter() {
174✔
389
            match usage {
174✔
390
                ParameterUse::GlobalPhase => out.add((py.None(), py.None()))?,
8✔
391
                ParameterUse::Index {
392
                    instruction,
166✔
393
                    parameter,
166✔
394
                } => out.add((*instruction, *parameter))?,
166✔
395
            }
396
        }
397
        Ok(out.unbind())
158✔
398
    }
158✔
399

400
    /// Accept traversal of this object by the Python garbage collector.
401
    ///
402
    /// This is not a pyclass, so it's up to our owner to delegate their own traversal to us.
403
    pub fn py_gc_traverse(&self, visit: &PyVisit) -> Result<(), PyTraverseError> {
22,930,716✔
404
        for info in self.by_uuid.values() {
22,930,716✔
405
            visit.call(&info.object)?
462,092✔
406
        }
407
        // We don't need to / can't visit the `PyBackedStr` stores.
408
        if let Some(list) = self.py_parameters_cache.get() {
22,930,716✔
409
            visit.call(list)?
2,840,152✔
410
        }
20,090,564✔
411
        Ok(())
22,930,716✔
412
    }
22,930,716✔
413
}
414

415
struct ParameterTableDrain {
416
    order: ::std::vec::IntoIter<ParameterUuid>,
417
    by_uuid: HashMap<ParameterUuid, ParameterInfo>,
418
}
419
impl Iterator for ParameterTableDrain {
420
    type Item = (Py<PyAny>, HashSet<ParameterUse>);
421

422
    fn next(&mut self) -> Option<Self::Item> {
6,264✔
423
        self.order.next().map(|uuid| {
6,264✔
424
            let info = self
6,264✔
425
                .by_uuid
6,264✔
426
                .remove(&uuid)
6,264✔
427
                .expect("tracked UUIDs should be consistent");
6,264✔
428
            (info.object, info.uses)
6,264✔
429
        })
6,264✔
430
    }
6,264✔
431

432
    fn size_hint(&self) -> (usize, Option<usize>) {
×
UNCOV
433
        self.order.size_hint()
×
UNCOV
434
    }
×
435
}
436
impl ExactSizeIterator for ParameterTableDrain {}
437
impl ::std::iter::FusedIterator for ParameterTableDrain {}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc