Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add hashable Angle type to Quantum extension #608

Merged
merged 25 commits into from
Nov 1, 2023
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
a09da1d
Add quantum angle type to spec.
cqc-alec Oct 16, 2023
0868128
Implement quantum angle type.
cqc-alec Oct 16, 2023
35be685
Add tests.
cqc-alec Oct 16, 2023
d00bbd2
Merge branch 'main' into angletype
cqc-alec Oct 16, 2023
b06b277
Merge branch 'main' into angletype
cqc-alec Oct 16, 2023
3635a3f
Merge branch 'main' into angletype
cqc-alec Oct 17, 2023
4f8c361
Rename "precision" to "log-denominator".
cqc-alec Oct 17, 2023
15504b1
Allow addition and subtraction of angles with different log-denoms.
cqc-alec Oct 17, 2023
fc4f0e1
Improve formatting.
cqc-alec Oct 17, 2023
14021af
Correct signature of atrunc.
cqc-alec Oct 17, 2023
37395f9
Generalize awiden to aconvert, with a possiblity of error.
cqc-alec Oct 17, 2023
2dde0d0
Add method to construct from radians (with rounding).
cqc-alec Oct 17, 2023
4b31667
Merge branch 'main' into angletype
cqc-alec Oct 19, 2023
22ca691
Merge branch 'main' into angletype
cqc-alec Oct 30, 2023
f502823
Make method name more descriptive.
cqc-alec Oct 30, 2023
9126a91
Merge branch 'main' into angletype
cqc-alec Oct 31, 2023
0326f40
Fix unsafety.
cqc-alec Oct 31, 2023
5c5f3c1
Use LOG_DENOM_MAX (53) instead of LOG_DENOM_BOUND (54).
cqc-alec Oct 31, 2023
3df78cd
Use FunctionType::new_linear() where applicable.
cqc-alec Oct 31, 2023
571e641
Clarify spec.
cqc-alec Oct 31, 2023
3e58d0c
Merge branch 'main' into angletype
cqc-alec Oct 31, 2023
ea66afb
Add test for ConstAngle::from_radians_rounding().
cqc-alec Oct 31, 2023
0be25bc
Merge branch 'main' into angletype
cqc-alec Oct 31, 2023
b1bdd9a
Update specification/hugr.md
cqc-alec Oct 31, 2023
edf1cd3
Merge branch 'main' into angletype
cqc-alec Oct 31, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion specification/hugr.md
Original file line number Diff line number Diff line change
Expand Up @@ -1735,7 +1735,7 @@ Note there are also `measurez: Qubit -> (i1, Qubit)` and on supported
targets `reset: Qubit -> Qubit` operations to measure or reset a qubit
without losing a handle to it.

**Dynamic vs static allocation**
#### Dynamic vs static allocation

With these operations the programmer/front-end can request dynamic qubit
allocation, and the compiler can add/remove/move these operations to use
Expand All @@ -1758,6 +1758,27 @@ allocation for all `Qubit` wires.
If further the program does not contain any `qalloc` or `qfree`
operations we can state the program only uses `N` qubits.

#### Angles

The Quantum extension also defines a specialized `angle<N>` type which is used
to express parameters of rotation gates. The type is parametrized by the
_log-denominator_, which is an integer $N \in [0, 53]$; angles with
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Including N=0 ? So angles that are multiples of 2pi? Do those do anything? I guess it may be easier to store 0-based, but 53 is an unusual choice of maximum anyway...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They don't do much, but an Rz(2*pi) operation is a global phase flip so it's conceivable. I don't think there's any harm in allowing 0?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually because we restrict the numerator to [0, 2^n) we couldn't express that. Hmm, this is a bit annoying, because in tket we can.

Copy link
Collaborator Author

@cqc-alec cqc-alec Oct 31, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Morally an angle type should be modulo 2pi (not modulo 4pi as it sometimes is in tket), so the parameter to Rz is not really an angle (but a half-angle).

log-denominator $N$ are multiples of $2 \pi / 2^N$, where the multiplier is an
unsigned `int<N>`. The maximum log-denominator $53$ effectively gives the
resolution of a `float64` value; but note that unlike `float64` all angle values
are equatable and hashable.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering whether x / (2pi/2^N) has the same hash as (2x)/(2pi/2^(N+1)), but I don't think you give the hash "algorithm" so fine

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and they are different types so I guess there is no need for them to

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, the angles are effectively interpreted modulo 2^N. So does, say, (3 / (2pi/2^2)) have the same hash as (7/(2pi/2^2)) ? Should it?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we restrict the numerator to the range $[0, 2^n]$, which I think we should, then all values with a given denominator $2^n$ have different hashes. Interesting question is whether values with different denominators should have the same hash if they represent the same angle: e.g. $1 \times 2\pi/2^2$ and $2 \times 2\pi/2^3$. I think probably not, since they are different types and you can do different things with them.


The following operations are defined:

| Name | Inputs | Outputs | Meaning |
| -------------- | ---------- | ---------- | ------- |
| `aconst<N, x>` | none | `angle<N>` | const node producing angle $2 \pi x / 2^N$ (where $0 \leq x \lt 2^N$) |
| `atrunc<M,N>` | `angle<M>` | `angle<N>` | round `angle<M>` to `angle<N>`, where $M \geq N$, rounding down in $[0, 2\pi)$ if necessary |
| `aconvert<M,N>` | `angle<M>` | `Sum(angle<N>, ErrorType` | convert `angle<M>` to `angle<N>`, returning an error if $M \gt N$ and exact conversion is impossible |
cqc-alec marked this conversation as resolved.
Show resolved Hide resolved
| `aadd<M,N>` | `angle<M>`, `angle<N>` | `angle<max(M,N)>` | add two angles |
| `asub<M,N>` | `angle<M>`, `angle<N>` | `angle<max(M,N)>` | subtract the second angle from the first |
| `aneg<N>` | `angle<N>` | `angle<N>` | negate an angle |

### Higher-order (Tierkreis) Extension

In **some** contexts, notably the Tierkreis runtime, higher-order
Expand Down
274 changes: 270 additions & 4 deletions src/std_extensions/quantum.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,190 @@
//! Basic HUGR quantum operations

use std::cmp::max;
use std::f64::consts::TAU;
use std::num::NonZeroU64;

use smol_str::SmolStr;

use crate::extension::prelude::{BOOL_T, QB_T};
use crate::extension::prelude::{BOOL_T, ERROR_TYPE, QB_T};
use crate::extension::{ExtensionId, SignatureError};
use crate::std_extensions::arithmetic::float_types::FLOAT64_TYPE;
use crate::type_row;
use crate::types::type_param::TypeArg;
use crate::types::FunctionType;
use crate::types::type_param::{TypeArg, TypeArgError, TypeParam};
use crate::types::{ConstTypeError, CustomCheckFailure, CustomType, FunctionType, Type, TypeBound};
use crate::utils::collect_array;
use crate::values::CustomConst;
use crate::Extension;

use lazy_static::lazy_static;

/// The extension identifier.
pub const EXTENSION_ID: ExtensionId = ExtensionId::new_unchecked("quantum");

/// Identifier for the angle type.
const ANGLE_TYPE_ID: SmolStr = SmolStr::new_inline("angle");

fn angle_custom_type(log_denom_arg: TypeArg) -> CustomType {
CustomType::new(ANGLE_TYPE_ID, [log_denom_arg], EXTENSION_ID, TypeBound::Eq)
}

/// Angle type with a given log-denominator (specified by the TypeArg).
///
/// This type is capable of representing angles that are multiples of 2π / 2^N where N is the
/// log-denominator.
pub(super) fn angle_type(log_denom_arg: TypeArg) -> Type {
Type::new_extension(angle_custom_type(log_denom_arg))
}

/// The smallest forbidden log-denominator.
pub const LOG_DENOM_BOUND: u8 = 54;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

super-nit: I find LOG_DENOM_MAX clearer (without having to read the doc) as to whether it's inclusive/exclusive; then you'd change < to <=

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed.


const fn is_valid_log_denom(n: u8) -> bool {
n < LOG_DENOM_BOUND
}

/// Type parameter for the log-denominator of an angle.
// SAFETY: unsafe block should be ok as the value is definitely not zero.
#[allow(clippy::assertions_on_constants)]
pub const LOG_DENOM_TYPE_PARAM: TypeParam = TypeParam::bounded_nat(unsafe {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoa! new_unchecked needs unsafe, I see. Hmmmm. Ick. (I think? Or should we make our new_uncheckeds also need unsafe?)

The following works without an unsafe, but you can keep the assert:
NonZeroU64::MIN.saturating_add((LOG_WIDTH_BOUND-1) as u64)

or even
NonZeroU64::MIN.saturating_add((LOG_WIDTH_BOUND as u64)-NonZeroU64::MIN.get())

Ymmv :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, we can probably do this in the integer extension code too where there is a similar unsafe block.

assert!(LOG_DENOM_BOUND > 0);
NonZeroU64::new_unchecked(LOG_DENOM_BOUND as u64)
});

/// Get the log-denominator of the specified type argument or error if the argument is invalid.
pub(super) fn get_log_denom(arg: &TypeArg) -> Result<u8, TypeArgError> {
match arg {
TypeArg::BoundedNat { n } if is_valid_log_denom(*n as u8) => Ok(*n as u8),
_ => Err(TypeArgError::TypeMismatch {
arg: arg.clone(),
param: LOG_DENOM_TYPE_PARAM,
}),
}
}

pub(super) const fn type_arg(log_denom: u8) -> TypeArg {
TypeArg::BoundedNat {
n: log_denom as u64,
}
}

/// An angle
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I note we aren't actually deriving (or implementing) Hash here...that may be right though...

pub struct ConstAngle {
log_denom: u8,
value: u64,
}

impl ConstAngle {
/// Create a new [`ConstAngle`] from a log-denominator and a numerator
pub fn new(log_denom: u8, value: u64) -> Result<Self, ConstTypeError> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to make this const ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ach, boo, it isn't - because of the String in our CustomCheckFailure type!! We should change that (SmolStr perhaps), but I guess that doesn't really belong in this PR

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems not, because of the to_owned() inside it.

if !is_valid_log_denom(log_denom) {
return Err(ConstTypeError::CustomCheckFail(
crate::types::CustomCheckFailure::Message(
"Invalid angle log-denominator.".to_owned(),
),
));
}
if value >= (1u64 << log_denom) {
return Err(ConstTypeError::CustomCheckFail(
crate::types::CustomCheckFailure::Message(
"Invalid unsigned integer value.".to_owned(),
),
));
}
Ok(Self { log_denom, value })
}

/// Create a new [`ConstAngle`] from a log-denominator and a floating-point value in radians,
/// rounding to the nearest corresponding value. (Ties round away from zero.)
pub fn from_radians(log_denom: u8, theta: f64) -> Result<Self, ConstTypeError> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: maybe rounding_radians ?

if !is_valid_log_denom(log_denom) {
return Err(ConstTypeError::CustomCheckFail(
crate::types::CustomCheckFailure::Message(
"Invalid angle log-denominator.".to_owned(),
),
));
}
let a = (((1u64 << log_denom) as f64) * theta / TAU).round() as i64;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙏 TAU 🙏

Ok(Self {
log_denom,
value: a.rem_euclid(1i64 << log_denom) as u64,
})
}

/// Returns the value of the constant
pub fn value(&self) -> u64 {
self.value
}

/// Returns the log-denominator of the constant
pub fn log_denom(&self) -> u8 {
self.log_denom
}
}

#[typetag::serde]
impl CustomConst for ConstAngle {
fn name(&self) -> SmolStr {
format!("a(2π*{}/2^{})", self.value, self.log_denom).into()
}
fn check_custom_type(&self, typ: &CustomType) -> Result<(), CustomCheckFailure> {
if typ.clone() == angle_custom_type(type_arg(self.log_denom)) {
Ok(())
} else {
Err(CustomCheckFailure::Message(
"Angle constant type mismatch.".into(),
))
}
}
fn equal_consts(&self, other: &dyn CustomConst) -> bool {
crate::values::downcast_equal_consts(self, other)
}
}

fn atrunc_sig(arg_values: &[TypeArg]) -> Result<FunctionType, SignatureError> {
let [arg0, arg1] = collect_array(arg_values);
let m: u8 = get_log_denom(arg0)?;
let n: u8 = get_log_denom(arg1)?;
if m < n {
return Err(SignatureError::InvalidTypeArgs);
}
Ok(FunctionType::new(
vec![angle_type(arg0.clone())],
acl-cqc marked this conversation as resolved.
Show resolved Hide resolved
vec![angle_type(arg1.clone())],
))
}

fn aconvert_sig(arg_values: &[TypeArg]) -> Result<FunctionType, SignatureError> {
let [arg0, arg1] = collect_array(arg_values);
Ok(FunctionType::new(
vec![angle_type(arg0.clone())],
vec![Type::new_sum(vec![angle_type(arg1.clone()), ERROR_TYPE])],
))
}

fn abinop_sig(arg_values: &[TypeArg]) -> Result<FunctionType, SignatureError> {
let [arg0, arg1] = collect_array(arg_values);
let m: u8 = get_log_denom(arg0)?;
let n: u8 = get_log_denom(arg1)?;
let l: u8 = max(m, n);
Ok(FunctionType::new(
vec![
angle_type(TypeArg::BoundedNat { n: m as u64 }),
angle_type(TypeArg::BoundedNat { n: n as u64 }),
],
vec![angle_type(TypeArg::BoundedNat { n: l as u64 })],
))
}

fn aunop_sig(arg_values: &[TypeArg]) -> Result<FunctionType, SignatureError> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naming perhaps a little terse here...took me a while to figure out thataun is short for "angle unary"

let [arg] = collect_array(arg_values);
Ok(FunctionType::new(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: FunctionType::new_linear

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, changed this and a couple of others.

vec![angle_type(arg.clone())],
vec![angle_type(arg.clone())],
))
}

fn one_qb_func(_: &[TypeArg]) -> Result<FunctionType, SignatureError> {
Ok(FunctionType::new(type_row![QB_T], type_row![QB_T]))
}
Expand All @@ -28,6 +199,64 @@ fn two_qb_func(_: &[TypeArg]) -> Result<FunctionType, SignatureError> {
fn extension() -> Extension {
let mut extension = Extension::new(EXTENSION_ID);

extension
.add_type(
ANGLE_TYPE_ID,
vec![LOG_DENOM_TYPE_PARAM],
"angle value with a given log-denominator".to_owned(),
TypeBound::Eq.into(),
)
.unwrap();

extension
.add_op_custom_sig_simple(
"atrunc".into(),
"truncate an angle to one with a lower log-denominator with the same value, rounding \
down in [0, 2π) if necessary"
.to_owned(),
vec![LOG_DENOM_TYPE_PARAM, LOG_DENOM_TYPE_PARAM],
atrunc_sig,
)
.unwrap();

extension
.add_op_custom_sig_simple(
"aconvert".into(),
"convert an angle to one with another log-denominator having the same value, if \
possible, otherwise return an error"
.to_owned(),
vec![LOG_DENOM_TYPE_PARAM, LOG_DENOM_TYPE_PARAM],
aconvert_sig,
)
.unwrap();

extension
.add_op_custom_sig_simple(
"aadd".into(),
"addition of angles".to_owned(),
vec![LOG_DENOM_TYPE_PARAM],
abinop_sig,
)
.unwrap();

extension
.add_op_custom_sig_simple(
"asub".into(),
"subtraction of the second angle from the first".to_owned(),
vec![LOG_DENOM_TYPE_PARAM],
abinop_sig,
)
.unwrap();

extension
.add_op_custom_sig_simple(
"aneg".into(),
"negation of an angle".to_owned(),
vec![LOG_DENOM_TYPE_PARAM],
aunop_sig,
)
.unwrap();

extension
.add_op_custom_sig_simple(
SmolStr::new_inline("H"),
Expand Down Expand Up @@ -78,7 +307,14 @@ lazy_static! {

#[cfg(test)]
pub(crate) mod test {
use crate::{extension::EMPTY_REG, ops::LeafOp};
use cool_asserts::assert_matches;

use crate::{
extension::EMPTY_REG,
ops::LeafOp,
std_extensions::quantum::{get_log_denom, ConstAngle},
types::{type_param::TypeArgError, ConstTypeError, TypeArg},
};

use super::EXTENSION;

Expand All @@ -100,4 +336,34 @@ pub(crate) mod test {
pub(crate) fn measure() -> LeafOp {
get_gate("Measure")
}

#[test]
fn test_angle_log_denoms() {
let type_arg_53 = TypeArg::BoundedNat { n: 53 };
assert_matches!(get_log_denom(&type_arg_53), Ok(53));

let type_arg_54 = TypeArg::BoundedNat { n: 54 };
assert_matches!(
get_log_denom(&type_arg_54),
Err(TypeArgError::TypeMismatch { .. })
);
}

#[test]
fn test_angle_consts() {
let const_a32_7 = ConstAngle::new(5, 7);
let const_a33_7 = ConstAngle::new(6, 7);
let const_a32_8 = ConstAngle::new(6, 8);
assert_ne!(const_a32_7, const_a33_7);
assert_ne!(const_a32_7, const_a32_8);
assert_eq!(const_a32_7, ConstAngle::new(5, 7));
assert_matches!(
ConstAngle::new(3, 256),
Err(ConstTypeError::CustomCheckFail(_))
);
assert_matches!(
ConstAngle::new(54, 256),
Err(ConstTypeError::CustomCheckFail(_))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add a test for from_radians?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now added.

);
}
}