pasqal-io / horqrux

Jax-based quantum state vector simulator.
https://pasqal-io.github.io/horqrux/latest/
Apache License 2.0
24 stars 2 forks source link

[Discussion] How to handle multiple observations #26

Closed atiyo closed 1 month ago

atiyo commented 3 months ago

TLDR: Currently when we ask for observables = [Z(0), Z(1)], in an expectation, we get something like <state|Z(0) Z(1)|state>, but I think it'd be more useful if we returned [<state|Z(0)|state>, <state|Z(1)|state> ] and so this issue is to discuss this potentially breaking change.


As it currently stands, when we provide a list of observables to an expectation, the observables are applied sequentially before a measurement is taken. i.e.

https://github.com/pasqal-io/horqrux/blob/2824f4b3fc44285694158c8d6bd1ea77672f7e98/horqrux/api.py#L54-L63

A GateSequence can be thought of as a list[Primitive] above. So passing something like observable = [Z(0), Z(1)] yields something like <gates| Z(0) Z(1) | gates>, with an abuse of notation.

This feels a little off (to me) for two reasons:

Presumably we would like to make it easier to sample multiple observations per shot in an easy way. The main question is what this should look like?

My preferred solution is to repurpose the interpretation of observable so that we return an expectation for each element in observable, while respecting the correlations between different observables.

However, this is a breaking change and fundamentally changes the behaviour of functions in API. Hence, I thought it good to open a discussion on this.

Any thoughts? @jpmoutinho @Roland-djee @gvelikova

jpmoutinho commented 3 months ago

Hi @atiyo. Overall, this makes sense to me.

Let me tell you a bit about how it currently works in PyQ to see also how we can harmonize the design.

Currently there are a few different ways to instantiate compositions of operations, each imposing different rules:

Sequence([Z(0), Z(1)]) -> applies Z(0)Z(1)
Add([Z(0), Z(1)]) -> applies Z(0) + Z(1)
Scale(Z(0), scale) -> applies scale * Z(0)

Furthermore there is an Observable class that inherits from Add and simply adds an expectation method. So if you do

obs = Observable([Z(0), Z(1)])
obs.expectation(state)

It will essentially return <state|Z(0)+Z(1)|state>.

There is also an expectation function that just runs a given circuit and performs observable.expectation(state) at the end.

If I understand, your proposal would be to return the expectation of each term separately, and then leave the user to sum them together if they wish. Currently, there is no option in PyQ to do that, but it would be very easy to add it.

I see two options: adding it to the Observable class, which would probably be more similar to what you are suggesting. In PyQ, I think I would possibly add this as an option, something like

obs = Observable([Z(0), Z(1)])
obs.expectation(state, compose = "add") -> return `<state|Z(0) + Z(1)|state>`.
obs.expectation(state, compose = "batch") -> return `[<state|Z(0)|state>, <state|Z(1)|state>]`.

Another option would be keep the Observable class as a "single" observable, and instead force the user to pass a list of observables to a given circuit. This would be more similar to the way it currently works in Qadence:

obs_0 = Observable(Z(0))
obs_1 = Observable(Z(1))

circuit = QuantumCircuit(some_operation_sequence)

expectation(circuit, [obs_0, obs_1]) -> return `[<state|Z(0)|state>, <state|Z(1)|state>]`.

The main advantage I see with this approach is that it seems more flexible in case the user wants to have each of the "batched" expectations to be more complicated compositions of terms.

What do you think?

atiyo commented 1 month ago

Thanks for the feedback @jpmoutinho!

The overall design of horqrux, and the design philosophy of jax in general is to encourage a functional approach. So the latter approach---making users supply a list of observables---seems most compatible with this and has my vote as the way forward.