-
Notifications
You must be signed in to change notification settings - Fork 3k
Add impls of ProgramNode for various math operations #16107
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
Changes from 15 commits
ee18ccf
d01ed27
d91de1b
cc82ad1
5def7ba
c7315e3
fd9b273
bd06804
e296409
337dba8
f3d7dbc
2639c1b
0dc1c02
0f95a1c
aea04da
eb2006d
a801fc0
04f5b27
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,269 @@ | ||
| // This code is part of Qiskit. | ||
| // | ||
| // (C) Copyright IBM 2026 | ||
| // | ||
| // This code is licensed under the Apache License, Version 2.0. You may | ||
| // obtain a copy of this license in the LICENSE.txt file in the root directory | ||
| // of this source tree or at https://www.apache.org/licenses/LICENSE-2.0. | ||
| // | ||
| // Any modifications or derivative works of this code must retain this | ||
| // copyright notice, and modified files need to carry a notice indicating | ||
| // that they have been altered from the originals. | ||
|
|
||
| use crate::data_tree::DataTree; | ||
| use crate::program_node::ProgramNode; | ||
| use crate::tensor::{DTypeLike, Tensor, TensorType, promotion}; | ||
| use crate::unpack_tensor_args; | ||
| use std::sync::LazyLock; | ||
|
|
||
| /// Shared input type spec for all elementwise binary nodes: two broadcastable tensors `x` and `y`. | ||
| static INPUT_TYPES: LazyLock<DataTree<TensorType>> = LazyLock::new(|| { | ||
| let mut types = DataTree::with_capacity(2); | ||
| types.insert_leaf( | ||
| "x", | ||
| TensorType { | ||
| dtype: DTypeLike::Var("x".into()), | ||
| shape: vec![], | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The numpy convention is that empty shape lists represent scalars (0 dimensional tensors). I'm not sure if we follow the numpy convention here, but if we do, and if the reason for an empty list in this case is because for
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, the intention is to follow NumPy conventions here, where 0-length shapes represent scalars. So the declaration here is that these binary operations fundamentally have scalar inputs and outputs, though they are happy to broadcast other dimensions. I'd prefer to keep it the way it is to avoid case work. |
||
| broadcastable: true, | ||
| }, | ||
| ); | ||
| types.insert_leaf( | ||
| "y", | ||
| TensorType { | ||
| dtype: DTypeLike::Var("y".into()), | ||
| shape: vec![], | ||
| broadcastable: true, | ||
| }, | ||
| ); | ||
| types | ||
| }); | ||
|
|
||
| /// Shared output type spec for all elementwise binary nodes: a single tensor of the promoted dtype. | ||
| static OUTPUT_TYPES: LazyLock<DataTree<TensorType>> = LazyLock::new(|| { | ||
| DataTree::new_leaf(TensorType { | ||
| dtype: DTypeLike::Promotion( | ||
| vec![DTypeLike::Var("x".into()), DTypeLike::Var("y".into())].into(), | ||
| ), | ||
| shape: vec![], | ||
| broadcastable: true, | ||
| }) | ||
| }); | ||
|
|
||
| /// Generate a [`ProgramNode`] struct for an elementwise binary operation. | ||
| macro_rules! elementwise_binary_node { | ||
| ($name:ident, $node_name:literal, $call_fn:expr) => { | ||
| #[doc = concat!("Elementwise `", $node_name, "` of two broadcastable tensors.")] | ||
| pub struct $name; | ||
|
|
||
| impl ProgramNode for $name { | ||
| type CallError = super::MathNodeError; | ||
|
|
||
| fn name(&self) -> &str { | ||
| $node_name | ||
| } | ||
| fn namespace(&self) -> &str { | ||
| "qiskit" | ||
| } | ||
| fn input_types(&self) -> &DataTree<TensorType> { | ||
| &INPUT_TYPES | ||
| } | ||
| fn output_types(&self) -> &DataTree<TensorType> { | ||
| &OUTPUT_TYPES | ||
| } | ||
| fn implements_call(&self) -> bool { | ||
| true | ||
| } | ||
| fn call_flat(&self, args: &[Tensor]) -> Result<Vec<Tensor>, Self::CallError> { | ||
| unpack_tensor_args!(args, [x, y]); | ||
| let out_dtype = promotion(x.dtype(), y.dtype()); | ||
| let x = x.clone().cast(out_dtype); | ||
| let y = y.clone().cast(out_dtype); | ||
| Ok(vec![$call_fn(&x, &y)?]) | ||
| } | ||
| } | ||
| }; | ||
| } | ||
|
|
||
| elementwise_binary_node!(Add, "add", Tensor::add_tensor); | ||
| elementwise_binary_node!(Subtract, "subtract", Tensor::sub_tensor); | ||
| elementwise_binary_node!(Multiply, "multiply", Tensor::mul_tensor); | ||
| elementwise_binary_node!(Divide, "divide", Tensor::div_tensor); | ||
| elementwise_binary_node!(Remainder, "remainder", Tensor::rem_tensor); | ||
| elementwise_binary_node!(Power, "power", Tensor::pow); | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
| use crate::math_nodes::MathNodeError; | ||
| use crate::program_node::{CallError, CallInputError, ProgramNodeExt}; | ||
| use crate::tensor::{DType, Tensor}; | ||
|
|
||
| #[test] | ||
| fn test_add_same_dtype() { | ||
| let result = Add | ||
| .call_flat(&[ | ||
| Tensor::from([1.0_f64, 2.0, 3.0]), | ||
| Tensor::from([4.0_f64, 5.0, 6.0]), | ||
| ]) | ||
| .unwrap(); | ||
| assert_eq!(result.len(), 1); | ||
| let Tensor::F64(arr) = &result[0] else { | ||
| panic!("expected f64") | ||
| }; | ||
| assert_eq!(arr.as_slice().unwrap(), &[5.0, 7.0, 9.0]); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_add_promotes_dtype() { | ||
| let result = Add | ||
| .call_flat(&[Tensor::from([1.0_f32, 2.0]), Tensor::from([3.0_f64, 4.0])]) | ||
| .unwrap(); | ||
| assert_eq!(result[0].dtype(), DType::F64); | ||
| let Tensor::F64(arr) = &result[0] else { | ||
| panic!("expected f64") | ||
| }; | ||
| assert_eq!(arr.as_slice().unwrap(), &[4.0, 6.0]); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_add_broadcasts_2d_with_1d() { | ||
| use ndarray::arr2; | ||
| let x = Tensor::F64( | ||
| arr2(&[[1.0_f64, 2.0, 3.0], [4.0, 5.0, 6.0]]) | ||
| .into_dyn() | ||
| .into_shared(), | ||
| ); | ||
| let y = Tensor::from([10.0_f64, 20.0, 30.0]); | ||
| let result = Add.call_flat(&[x, y]).unwrap(); | ||
| let Tensor::F64(arr) = &result[0] else { | ||
| panic!("expected f64") | ||
| }; | ||
| let expected = arr2(&[[11.0_f64, 22.0, 33.0], [14.0, 25.0, 36.0]]) | ||
| .into_dyn() | ||
| .into_shared(); | ||
| assert_eq!(arr, &expected); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_subtract() { | ||
| let result = Subtract | ||
| .call_flat(&[ | ||
| Tensor::from([5.0_f64, 6.0, 7.0]), | ||
| Tensor::from([1.0_f64, 2.0, 3.0]), | ||
| ]) | ||
| .unwrap(); | ||
| let Tensor::F64(arr) = &result[0] else { | ||
| panic!() | ||
| }; | ||
| assert_eq!(arr.as_slice().unwrap(), &[4.0, 4.0, 4.0]); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_multiply() { | ||
| let result = Multiply | ||
| .call_flat(&[ | ||
| Tensor::from([2.0_f64, 3.0, 4.0]), | ||
| Tensor::from([10.0_f64, 10.0, 10.0]), | ||
| ]) | ||
| .unwrap(); | ||
| let Tensor::F64(arr) = &result[0] else { | ||
| panic!() | ||
| }; | ||
| assert_eq!(arr.as_slice().unwrap(), &[20.0, 30.0, 40.0]); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_divide() { | ||
| let result = Divide | ||
| .call_flat(&[ | ||
| Tensor::from([10.0_f64, 9.0, 8.0]), | ||
| Tensor::from([2.0_f64, 3.0, 4.0]), | ||
| ]) | ||
| .unwrap(); | ||
| let Tensor::F64(arr) = &result[0] else { | ||
| panic!() | ||
| }; | ||
| assert_eq!(arr.as_slice().unwrap(), &[5.0, 3.0, 2.0]); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_remainder() { | ||
| let result = Remainder | ||
| .call_flat(&[ | ||
| Tensor::from([7.0_f64, 8.0, 9.0]), | ||
| Tensor::from([3.0_f64, 3.0, 3.0]), | ||
| ]) | ||
| .unwrap(); | ||
| let Tensor::F64(arr) = &result[0] else { | ||
| panic!() | ||
| }; | ||
| assert_eq!(arr.as_slice().unwrap(), &[1.0, 2.0, 0.0]); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_power() { | ||
| let result = Power | ||
| .call_flat(&[ | ||
| Tensor::from([2.0_f64, 3.0, 4.0]), | ||
| Tensor::from([3.0_f64, 2.0, 1.0]), | ||
| ]) | ||
| .unwrap(); | ||
| let Tensor::F64(arr) = &result[0] else { | ||
| panic!() | ||
| }; | ||
| for (a, b) in arr.as_slice().unwrap().iter().zip(&[8.0_f64, 9.0, 4.0]) { | ||
| assert!(approx::abs_diff_eq!(a, b, epsilon = 1e-12)); | ||
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_call_missing_input_errors() { | ||
| let mut tree = DataTree::new(); | ||
| tree.insert_leaf("x", Tensor::from([1.0_f64])); | ||
| let err = Add.call(&tree).unwrap_err(); | ||
| assert!(matches!( | ||
| err, | ||
| CallError::<MathNodeError>::Input(CallInputError::MissingInput { | ||
| ref key, | ||
| }) if key == "y" | ||
| )); | ||
| } | ||
|
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It might be worth having a few tests showing a successful pattern with
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added some tests of this flavour in e296409. |
||
| #[test] | ||
| fn test_call_branch_where_leaf_expected_errors() { | ||
| let mut tree = DataTree::new(); | ||
| tree.insert_leaf("x", Tensor::from([1.0_f64])); | ||
| tree.insert_branch("y", DataTree::new()); | ||
| let err = Add.call(&tree).unwrap_err(); | ||
| assert!(matches!( | ||
| err, | ||
| CallError::<MathNodeError>::Input(CallInputError::ExpectedLeaf { | ||
| ref key, | ||
| }) if key == "y" | ||
| )); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_add_call_end_to_end() { | ||
| let mut tree = DataTree::new(); | ||
| tree.insert_leaf("x", Tensor::from([1.0_f64, 2.0, 3.0])); | ||
| tree.insert_leaf("y", Tensor::from([4.0_f64, 5.0, 6.0])); | ||
| let result = Add.call(&tree).unwrap(); | ||
| let Tensor::F64(arr) = result.unwrap_leaf() else { | ||
| panic!("expected f64") | ||
| }; | ||
| assert_eq!(arr.as_slice().unwrap(), &[5.0, 7.0, 9.0]); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_add_wrong_arity_errors() { | ||
| let err = Add.call_flat(&[Tensor::from([1.0_f64])]).unwrap_err(); | ||
| assert_eq!( | ||
| err, | ||
| MathNodeError::Input(CallInputError::WrongArity { | ||
| expected: 2, | ||
| actual: 1, | ||
| }) | ||
| ); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know how exactly to structure subfolders for the same reason that I don't know how to define namespaces.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is your desired ideal structure? Like this will only expose the math_nodes from rust as
qiskit_providers::math_nodes::binary::Add. If you wanted to doqiskit_providers::math_nodes::Addyou will need to add apub usetoqiskit_providers/src/math_nodes/mod.rsfor all the things you want to re-export up one level.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I flattened the namespace of
math_nodesin bd06804, which is probably better than current, but honestly, I don't have an ideal structure in mind yet.