Renmusxd / RustQIP

Quantum computing using rust. Efficient and a borrow-checked no cloning theorem!
https://docs.rs/qip/
MIT License
223 stars 18 forks source link

[WIP] Function chain #51

Open OmmyZhang opened 4 months ago

OmmyZhang commented 4 months ago

This is the beginning of the plan to rewrite program macro to the function chain.

The program macro seems not necessary, as rust's type system should work fine with the process of applying a gate / sub-circuit to some qubits.

I see that the plan of rewriting program macro was also mentioned in #39

OmmyZhang commented 4 months ago

The usage is showed in test

        let mut b = LocalBuilder::default();
        let ra = b.try_register(3).unwrap();
        let rb = b.try_register(3).unwrap();

        let [ra, rb] = Circuit::default()
            // Applies gamma to |ra>|rb>
            .apply(gamma, [0, 1])
            // Applies gamma to |ra[0] ra[1]>|ra[2]>
            .under_new_indices(
                [&[(0, 0), (0, 1)], &[(0, 2)]],
                Circuit::default().apply(gamma, [0, 1]),
            )
            .input([ra, rb])
            .run(&mut b)?;

I have just finished fn apply, and will continue to work on fn apply_when fn apply_control fn apply_control_with. Docs and some function names may also need to be rewritten. (If the general design is accepted)

Renmusxd commented 4 months ago

I like the idea -- I'd prefer to keep the program! macro after this addition, since it's made specifically to line up with common notation in quantum computing textbooks.

I'm not sure about the under_new_indices api however, the first argument is difficult to read, although the idea of chaining circuits like that sounds great (particularly if we can add some other methods to the circuit object like .invert()) -- the benefit of the program! macro in this context is the ability to write ra[0..3] to neatly communicate which qubits we access.

Maybe we could support a function to remap the indices, so the user may define

.apply_subcircuit(|[ra, rb]| {
    (ra[0..2], rb)
}, Circuit::default().apply(gamma, [0, 1]))?

Here ra and rb are handles of some sort, the try question mark is likely necessary for the try_into() call for the argument [Handle; 2].

Just a thought -- any ideas?

OmmyZhang commented 4 months ago

To be honest, I also think my under_new_indices looks quite confusing.. It's a great idea to use a closure! Let me try it.

I'm considering also supporting something like .apply_to(gamma, |[ra, rb]| [ra[0..2], rb]), but this may make things more complex. I'm not sure.

OmmyZhang commented 4 months ago

Now, with trait AsSubcircuit and CircuitBuilder, things get a little better. apply can accept either an array or a closure as the indices information . But now I have to write type annotations for closures.

       let [ra, rb] = Circuit::default()
            // Applies gamma to |ra>|rb>
            .apply(gamma, [0, 1])
            // Applies gamma to |ra[0] ra[1]>|ra[2]>
            .apply(gamma, |[ra, _]: Idx<2>| {
                [ra[0..=1].to_vec(), vec![ra[2]]]
            })
            // Applies gamma to |ra[0] rb[0]>|ra[2]>
            .apply(gamma, |[ra, rb]: Idx<2>| {
                [vec![ra[0], rb[0]], vec![ra[2]]]
            })
            // Applies gamma to |ra[0]>|rb[0] ra[2]>
            .apply(gamma, |[ra, rb]: Idx<2>| {
                [vec![ra[0]], vec![rb[0], ra[2]]]
            })
            // Applies a more complex subcircuit to |ra[1]>|ra[2]>|rb>
            .apply(
                Circuit::default()
                    .apply(gamma, [0, 1])
                    .apply(gamma, [1, 2])
                    .apply(
                        |b: &mut CurrentBuilderType, rs| {
                            let [ra, rb] = rs;
                            let (ra, rb) = b.cnot(ra, rb)?;
                            Ok([ra, rb])
                        },
                        |[_, r2, r3]: Idx<3>| [r2, vec![r3[0]]],
                    ),
                |[ra, rb]: Idx<2>| [vec![ra[1]], vec![ra[2]], rb],
            )
            .input([ra, rb])
            .run(&mut b)?;
Renmusxd commented 4 months ago

Yeah this version is much more readable to me - only other note: is it possible to use some Into traits on the closure signature so it can return arrays, vectors, or handles?

We can also just push a simple version and add that afterwards.

OmmyZhang commented 4 months ago

The bad news is that rust doesn't support generics for closures. Maybe we can put generics into the apply function, by not only implementing IndicesInfo for Fn([sth;N]) -> [sth; N] but also for Fn([sth;N]) -> It where It: .... Thanks for you suggestion!

I'm facing another problem. I worry that my Circuit<CB> will not work fine for control under the current design. While CircuitBuilder seems to represent the backend / implementation of the quantum circuit system upon my first inspection, I have realized that it also contains the control information. As Circuit<CB> and Circuit<<Conditioned<'a, CB>> are different types, I can't reuse a Circuit instance for a controlled subcircuit. Also, Circuit<<Conditioned<'a, CB>> will limited by the lifetime 'a.

Would it be possible to split CircuitBuilder into a trait Backend, which provides the register type and a few basic gates, and an executor Executor<Backend>, which stores control registers and can be created at any time? I may spend more time this weekend to think about this problem.

Additionally, is the builder trait doing too much? How about only providing fn cnot API in the trait, and moving all implementations into the specific type? Another example is fn swap. We need 3 cnot gates for the swap gate for some architectures, but not for some other architectures (Trapped-ion), and we also don't need 3 cnot for the local simulation.

Also, I'm considering rewriting Circuit to something similar to pipeline in LocalBuilder, storing some CB::CircuitObjects along with some indices. This may make things easier.

Renmusxd commented 4 months ago

I see, the issue you're running into is that you'd like the internal structure of the Circuit to be a single function which contains the whole function chain? You may have to structure it as a vector of enums which representing a single function application or conditioned application?

As for the generics, I'm not sure I understand. I was thinking of using the Into trait heavily. Something like the following:

#[derive(Default)]
struct Circuit<const N: usize> {
    pipeline: Vec<IndexMask<N>>,
}

impl<const N: usize> Circuit<N> {
    fn index<F, RH>(&mut self, indices: F) -> &mut Self where F: Fn([RegisterHandle; N]) -> RH + 'static, RH: Into<RegisterHandle> {
        self.index_mask(indices)
    }

    fn index_mask<MI>(&mut self, indices: MI) -> &mut Self where MI: Into<IndexMask<N>> {
        let indices = indices.into();
        self.pipeline.push(indices);
        self
    }
}

struct RegisterHandle {
    registers: Vec<(usize, usize)>,
}

enum IndexMask<const N: usize> {
    Function(Box<dyn Fn([RegisterHandle; N]) -> RegisterHandle>)
}

impl<const N: usize, F, RH> From<F> for IndexMask<N> where F: Fn([RegisterHandle; N]) -> RH + 'static, RH: Into<RegisterHandle> {
    fn from(value: F) -> Self {
        IndexMask::Function(Box::new(move |x| -> RegisterHandle {
            value(x).into()
        }))
    }
}

impl<RHI, RH> From<RHI> for RegisterHandle where RHI: IntoIterator<Item=RH>, RH: Into<RegisterHandle> {
    fn from(value: RHI) -> Self {
        RegisterHandle {
            registers: value.into_iter().flat_map(|x| x.into().registers).collect(),
        }
    }
}

This allows us to write code such as:

fn test_remap() {
    Circuit::default().index(|[a, b, c]| {
        [a, b]
    }).index(|[a, b, c]| {
        vec![a, b, c]
    }).index(|[a, b, c]| {
        [vec![a, b], vec![c]]
    }).index(|[a, b, c]| a);
}

Notably the return type can be whatever is convenient/readable at the time