qiboteam / qibo-core

Core qibo elements
https://qibo.science
1 stars 0 forks source link

Measurement gate #30

Open stavros11 opened 3 weeks ago

stavros11 commented 3 weeks ago

Currently measurement is a gate, but has some additional attributes and arguments compared to other gates, in particular:

stavros11 commented 3 weeks ago

Given these points, an alternative solution that does not require adding measurement gates to the Gate enumerations would be to leave them in the Circuit API only and provide a circuit.measure, separate from circuit.add, with the following signature:

measure(register_name: str, collapse: bool)

I haven't thought this thoroughly but the main motivation is that currently adding measurements is slightly different than adding other gates anyway, since it is returning a reference to the corresponding result (to be replaced by some id?) that can be used to control other gates. Therefore it may be cleaner to have a separate interface altogether.

renatomello commented 3 weeks ago

To add to this, I’d suggest removing the bit flip method entirely and forcing the user to use the ReadoutError channel

On Thu, 13 Jun 2024 at 17:32, Stavros Efthymiou @.***> wrote:

Currently measurement is a gate, but has some additional attributes and arguments compared to other gates, in particular:

  • register_name: @alecandido https://github.com/alecandido I believe we have not discussed what to do with that, which is relevant for the creation of results. One potential solution would be to lift to the circuit and keep a map {measurement_gid: register_name}.
  • collapse: can be handled as a separate measurement gate (MC) in qibo-core.
  • basis: will be left at high-level (not qibo-core) since it is only used to prepend other gates and does not affect execution.
  • bitflip probabilities (p0, p1): only affect the state post-execution by introducing bitflips, so it is not needed in qibo-core.
  • result: used to condition other gates on measurement outcomes. Will be decoupled from the gate and handled by the circuit.
  • pulses: most likely legacy and will be dropped.

— Reply to this email directly, view it on GitHub https://github.com/qiboteam/qibo-core/issues/30, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABH5QVSC7PACP7V3RAEVDVTZHGNOHAVCNFSM6AAAAABJINN7AGVHI2DSMVQWIX3LMV43ASLTON2WKOZSGM2TCMJXGAZTCNY . You are receiving this because you are subscribed to this thread.Message ID: @.***>

stavros11 commented 3 weeks ago

To add to this, I’d suggest removing the bit flip method entirely and forcing the user to use the ReadoutError channel

Good point, thanks. I forgot to mention here, but indeed, for mid-circuit measurements noise should be added using channels. From the qibo-core point of view there isn't much to do on this other than properly supporting channels (#27).

As for removing bitflip, I am fine with that, but should be done on the qibo side.

alecandido commented 3 weeks ago

register_name: @alecandido I believe we have not discussed what to do with that, which is relevant for the creation of results. One potential solution would be to lift to the circuit and keep a map {measurement_gid: register_name}.

I wonder whether we even need the map inside the Circuit. In the end, measurement_gid is already unique, and it is returned when you add the map, so that the user could maintain the map by himself, just for convenience.

Since the map would be an interface feature, I'd consider keeping it in the Qibo Circuit object, and give up on measurement names within qibo-core (just identifying them by gid as any other gate).

alecandido commented 3 weeks ago

Given these points, an alternative solution that does not require adding measurement gates to the Gate enumerations would be to leave them in the Circuit API only and provide a circuit.measure, separate from circuit.add, with the following signature:

measure(register_name: str, collapse: bool)

I haven't thought this thoroughly but the main motivation is that currently adding measurements is slightly different than adding other gates anyway, since it is returning a reference to the corresponding result (to be replaced by some id?) that can be used to control other gates. Therefore it may be cleaner to have a separate interface altogether.

What you're proposing is sensible, but if we keep using the gid (as proposed in the previous comment) maybe we could even keep adding measurement gates, since it's how they will get represented internally anyhow.

But if the Circuit.measure() interface is convenient enough, we could consider adding it to the high-level Qibo.

stavros11 commented 3 weeks ago

Since the map would be an interface feature, I'd consider keeping it in the Qibo Circuit object, and give up on measurement names within qibo-core (just identifying them by gid as any other gate).

I agree with this.

What you're proposing is sensible, but if we keep using the gid (as proposed in the previous comment) maybe we could even keep adding measurement gates, since it's how they will get represented internally anyhow.

Indeed, if we drop register_name (from qibo-core) and collapse is just another gate in the enumaration, then .measure wouldn't be very useful as it should be identical to .add. So we can just go with two measurement gates (collapse / not collapse) to be used with .add.

stavros11 commented 1 week ago

I have been thinking how to implement this but ended up discovering a few further issues/open questions, mainly related to the results and not the measurement gate itself. In summary these are the following:

  1. How we actually handle registers (register_name). We already discussed not having this here, but this may cause an issue.
  2. Dependencies between MeasurementResult and MeasurementOutcomes.

More details:

For 1: Let's assume that we completely ignore registers (register_names) from qibo-core. Since all backends depend only on qibo-core and not on qibo, the result of backend.execute_circuit(circuit: qibo_core.Circuit, nshots) will not have registers. On the other hand, I believe (based on earlier discussions with others) that in qibo we would like to maintain the same interface and therefore the result of qcircuit(nshots) (with qcircuit: qibo.models.Circuit) will have registers. This is possible, but it creates an asymmetry between circuit execution using backends (low level user) vs using the GlobalBackend (high level user), and I am not sure if everyone would agree with that. It would also mean that if at some point we decide to drop GlobalBackend, we lose the registers.

Related, but not entirely relevant to measurements, is whether we want to support something like backend.execute_circuit(qcircuit: qibo.models.Circuit). In principle this creates a cyclic dependency as the backends should not depend on qibo, but as discussed previously there are ways around that. In this case, registers would most likely be dropped.

If we do not support registers in qibo-core, it is sufficient to introduce the measurement as a single-qubit gate under the One enumeration. Otherwise, measurements will need to have variable number of targets which is not supported by the current infrastructure (number of targets of each gate is hardcoded). This will also be a problem for the Unitary/Matrix gate but that's for another issue.

For 2: This is not a qibo-core specific thing, but it mostly came up because MeasurementResult is currently hanging from the measurement gate, which will no longer be the case, therefore some related refactoring is needed. We could use that opportunity to clean up the relationship between these objects.

What are these objects currently:

What are their relationships:

  1. MeasurementOutcomes is a collection (concatenation) of MeasurementResults
  2. MeasurementResult is a view (slice) of MeasurementOutcomes

Why we need both relationships:

  1. When we execute on hardware, we typically get samples per qubit (or measurement) and we collect them to create the MeasurementOutcomes that we return to the user (case 1).
  2. When we simulate, we typically sample all the measured qubits to create the MeasurementOutcomes and then slice this to update the individual MeasurementResults. In principle, this is not required and we could sample each MeasurementResult individually, but I guess it happens like this because historically in qibo simulation came before hardware and because sampling one array of (nqubits, nshots) is probably more efficient than sampling nqubits arrays of (nshots,) (but difference may be negligible).

In practice, now MeasurementOutcomes holds references to all the associated MeasurementResults through the corresponding gates. Relationship 1 is handled in https://github.com/qiboteam/qibo-core/blob/a5e0ac9c9a043fe81f4741d3c628cf7f480ee3dc/crates/py/qibo_core/result.py#L333-L335 and relationship 2 in https://github.com/qiboteam/qibo-core/blob/a5e0ac9c9a043fe81f4741d3c628cf7f480ee3dc/crates/py/qibo_core/result.py#L364-L366 so MeasurementOutcomes is essentially mutating its MeasurementResults.

The main issue with this approach is that samples are duplicated. I believe it is preferrable to have them in a single place and treat the other as a view, however I am not sure which should be the single place (basically multiple 1D-arrays vs single 2D-array). In hardware execution we will receive the data as 1D arrays from the instrument at some point (I think there is no way around that), but we can still concatenate to 2D and forget the original arrays.

Another point is that currently MeasurementResult has two roles: the one described above (view of a single gate results) and also to allow using the results of collapse measurements for parametrization of other gates (#32). For the first role, we probably don't even need an object (array is sufficient), for the second we probably need but this will depend on how we handle #32.

alecandido commented 1 week ago

Ok, I will now reply to 1., and then read thoroughly 2. after.

Let's assume that we completely ignore registers (register_names) from qibo-core. Since all backends depend only on qibo-core and not on qibo, the result of backend.execute_circuit(circuit: qibo_core.Circuit, nshots) will not have registers.

Registers are not disappearing at all, it's just the aliases that are not supported any longer. Internally, the backend may want to (or have to) keep track of the connection between the result and the gate. This could be done explicitly, with a {measurements_gid: result} mapping, or implicitly, with a [result] array, and sorting uniquely the [measurement_gids], such that the results[gid] access is realized either by the hashmap, or computed as in results[measurements.index(gid)].

On the other hand, I believe (based on earlier discussions with others) that in qibo we would like to maintain the same interface and therefore the result of qcircuit(nshots) (with qcircuit: qibo.models.Circuit) will have registers. This is possible, but it creates an asymmetry between circuit execution using backends (low level user) vs using the GlobalBackend (high level user), and I am not sure if everyone would agree with that. It would also mean that if at some point we decide to drop GlobalBackend, we lose the registers.

That's fully a Qibo problem, and we have all the tools to solve that in Qibo. The library could maintain an alias map, and this could be done in the GlobalBackend, in the qibo.models.Circuit object, or in a separate Measurements object that is generated by the Circuit, and returned to the user.

Though, I have the feeling I might be missing your point. Because I really don't see registers as substantially disappearing. Only the result stored inside won't be there any longer, but the Circuit, while executing, can always populate it from the qibo_core result object, as needed. Or whoever else will implement the wrapping execute_circuit(circuit: qibo.models.Circuit, backend: str/qibo_core.Backend) function in Qibo (that will certainly be there, since we need at least the circuit translation).

Related, but not entirely relevant to measurements, is whether we want to support something like backend.execute_circuit(qcircuit: qibo.models.Circuit). In principle this creates a cyclic dependency as the backends should not depend on qibo, but as discussed previously there are ways around that. In this case, registers would most likely be dropped.

Answered above. backend will be a string, or qibo_core object. We need to implement this function somewhere else, and it could be:

If we do not support registers in qibo-core, it is sufficient to introduce the measurement as a single-qubit gate under the One enumeration. Otherwise, measurements will need to have variable number of targets which is not supported by the current infrastructure (number of targets of each gate is hardcoded). This will also be a problem for the Unitary/Matrix gate but that's for another issue.

It would not be a problem in any case. I'm planning to drop One as soon as we'll face #22, making a single plain enum. We can store the information about the number of qubits the gate is referred to in a further attribute in Circuit. Similar to all the other proposals to replace the gates' content. We're just moving from the list of objects to the database representation, having all the information as types as plain as possible, with a simple container objects, handled by a single manager, the Circuit.

alecandido commented 1 week ago

Ok, about 2. I believe there is only one complex point, everything else is "easily" solved: let's use a single 2D-array, and let's make it an array. At least in qibo-core.

About the Qibo interface, I would worry at a second stage, and it's always possible to fix it: if qibo-core stores enough information, as all the backends do, we'll always be able to handle and reshape it in Qibo, as we're now doing one backend at a time. The price is just wrapping.

Another point is that currently MeasurementResult has two roles: the one described above (view of a single gate results) and also to allow using the results of collapse measurements for parametrization of other gates (#32). For the first role, we probably don't even need an object (array is sufficient), for the second we probably need but this will depend on how we handle #32.

To be fair, this is more is not even that much related to qibo-core, but to the backends implementing the collapse in simulation. Qibotn won't support mid-circuit collapse (to the best of my understanding, it doesn't make much sense to use tensor networks for that). Qibolab will have to handle it differently anyhow, despite supporting it, since the results to be used will live on the boards, and they are not going back and forth to qibo-core. Possibly, same story for GPU, where it would be convenient to hold them on the device, without downloading (if possible). External backends (Qulacs and cloud) are maintaining their own, of course. Thus, I'd treat the problem as backend-specific, and each backend will store it in the place which is most convenient for it. The current mechanism is definitely tuned for the NumpyBackend, and extended to all the other ones. I would code this as part of the CPU backends library, whenever we'll implement it.