pasqal-io / qadence2-expressions

Qadence 2 expressions consisting of symbolic expressions and blocks, i.e. quantum digital, analog, digital-analog and composite gates, together with their logic and engine.
https://pasqal-io.github.io/qadence2-expressions/latest/
1 stars 0 forks source link

[Feature] Rust ports #18

Closed RolandMacDoland closed 1 week ago

RolandMacDoland commented 4 months ago

Closes #16

RolandMacDoland commented 4 months ago

@dominikandreasseitz @kaosmicadei Here is a very naive attempt at porting lower level types for expressions. If you want to have a look (can't add multiple reviewers until it goes public).

kaosmicadei commented 4 months ago

Suggestion.

It would be possible to consider the whole expression + symbol as a single enum to preserve the monoid properties of addition and other operations. In the sense that:

That would make Int(...) + Int(…) : Expr the same way Expr(…) + Symbol(…) : Expr

use num::Complex;

#[derive(Debug, PartialEq)]
pub enum Operator {
   Add,
   Mul,
   Pow,
   NonComm,
   Call
}

#[derive(Debug, PartialEq)]
pub enum Numerical {
    Int(i64),
    Float(f64),
    Complex(Complex<f64>),
}

#[derive(Debug, PartialEq)]
pub enum Expression {
    Symbol(&'static str),
    Value(Numerical),
    Expr { head: Operator, args: Vec<Box<Expression>> },
}
RolandMacDoland commented 4 months ago

@kaosmicadei Makes sense. Lemme have a go at it.

RolandMacDoland commented 4 months ago

Suggestion.

It would be possible to consider the whole expression + symbol as a single enum to preserve the monoid properties of addition and other operations. In the sense that:

Naive question: would anything else than an enum break monoidal properties ?

kaosmicadei commented 4 months ago

Suggestion. It would be possible to consider the whole expression + symbol as a single enum to preserve the monoid properties of addition and other operations. In the sense that:

Naive question: would anything else than an enum break monoidal properties ?

Short answer: yes.
Let me correct my statement first. Technically, It preserves the _semigroup_ property (a set with a closed associative binary operation). However, since the aimed operations have identity elements (0 for Add and 1 for Mul/Div), the expression is a monoid for those operations (subtraction _is not_ associative, but it's still a closed binary operation with identity, 0). A semigroup keeps the operation in the same type, `a,b: T ⟹ a∙b: T`. The `enum` provides a set of elements, in our case `E = {Symbol, Value, Expr}`. When we equip them with the addition, we guarantee that `a,b: E ⟹ a+b: E`. This means that any two elements of the set `E` can be added together, and the result will always be another element from the same set. Rust allows to implement operations doing typecasting, e.g., `a: T, b: U ⟹ a∙b: V`. That means we could have a `struct Symbol(&'static str)` and a `struct Value(f64)` and implement the addition as the following. ```rust impl Add for Value { type Output = Self; fn add(self, other: Self) -> Self { Self(self.0 + other.0) } } impl Add for Symbol { type Output = Expression; fn add(self, other: Self) -> Expression { Expression { head: Operator::ADD, args: vec![self, other] } } } impl Add for Symbol { type Output = Expression; fn add(self, other: Value) -> Expression { Expression { head: Operator::ADD, args: vec![self, other] } } } impl Add for Value { type Output = Expression; fn add(self, other: Symbol) -> Expression { Expression { head: Operator::ADD, args: vec![self, other] } } } ``` In this second approach, only `Value` forms a semigroup, and mixing `Value` and `Symbol` sends the result to a third type. Something similar to it is what is currently implemented in the Python prototype.
RolandMacDoland commented 4 months ago

Quick comment @kaosmicadei. Would it make sense to move all boxing constructors to the convenience functions ?

kaosmicadei commented 4 months ago

Quick comment @kaosmicadei. Would it make sense to move all boxing constructors to the convenience functions ?

Based on https://doc.rust-lang.org/std/macro.vec.html we could do something like

macro_rules! vbox {
    () => { vec![] };
    ($elem:expr; $n:expr) => { vec![Box::new($elem); $n] };
    ($($x:expr),+ $(,)?) => { vec![$(Box::new($x)),*] };
}

Actually, in our case, the second pattern could be omitted.

kaosmicadei commented 4 months ago

The macro implementation.

macro_rules! vbox {
   () => { vec![] };
   ($($x:expr),+ $(,)?) => { vec![$(Box::new($x)),*] };
}

macro_rules! impl_binary_operator_for_expression {
   ($trait:ident, $method:ident, $operator:path) => {
       impl $trait for Expression {
          type Output = Self;

          fn $method(self, other: Self) -> Self {
             use Expression::*;

             match (self, other) {
                (Value(x), Value(y)) => Value(x.$method(y)),

                (Expr {head: $operator, args: args_lhs}, Expr {head: $operator, args: args_rhs}) => {
                   let args = args_lhs.into_iter().chain(args_rhs.into_iter()).collect();
                   Expr{head: $operator, args}
                },

                (Expr {head: $operator, args: mut args_lhs}, rhs) => {
                   args_lhs.push(Box::new(rhs));
                   Expr {head: $operator, args: args_lhs}
                },

                (lhs, Expr {head: $operator, args: mut args_rhs}) => {
                   args_rhs.push(Box::new(lhs));
                   Expr {head: $operator, args: args_rhs}
                },

                (lhs, rhs) => Expr{head: $operator, args: vbox![lhs, rhs]},
             }
          }
       }
   };

   ($trait:ident, $method:ident, $operator:path, $inv:expr) => {
      impl $trait for Expression {
         type Output = Self;

         fn $method(self, other: Self) -> Self {
            use Expression::*;

            match (self, other) {
               (Value(x), Value(y)) => Value(x.$method(y)),
               (lhs, rhs) => Expr {
                  head: $operator,
                  args: vbox![lhs, $inv(rhs)]
               },
            }
         }
      }
   }
}

impl_binop!(Add, add, Operator::ADD);
impl_binop!(Mul, mul, Operator::MUL);
impl_binop!(Sub, sub, Operator:: ADD, |x: Expression| { x.neg() });
impl_binop!(Div, div, Operator:: ADD, |x: Expression| { x.pow(Expression::float(-1.0)) });

It requires the implementation of the Neg trait.

RolandMacDoland commented 4 months ago

The macro implementation. ...

It would also require the pow trait, wouldn't it ?

kaosmicadei commented 4 months ago

The macro implementation. ...

It would also require the pow trait, wouldn't it ?

This one? I think it could be optional, but maybe by completeness

RolandMacDoland commented 4 months ago

The macro implementation. ...

It would also require the pow trait, wouldn't it ?

This one? I think it could be optional, but maybe by completeness

Yes, I was on that one too.