-
Notifications
You must be signed in to change notification settings - Fork 2.6k
[Alternative] Move Parameter to Rust #14757
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
base: main
Are you sure you want to change the base?
Conversation
-- 606 failing tests
- name conflicts - hash(2.3) == hash(ParamExpr(2.3)) - some error messages
This allows keeping track of which parameters are still in the expression, even though the expression is optimized. E.g. x*0 = 0, but x is still valid to bind.
- don't use UUID in to_string, only in string_id (using strings for comparisons should probably be avoided in future) - clippy - relax Python is-checks to eq-checks
this is rather a temporary solution, maybe we ought to disable string parsing in favor or a proper SymPy->parameter expression conversion
One or more of the following people are relevant to this code:
|
Pull Request Test Coverage Report for Build 16525568695Warning: This coverage report may be inaccurate.This pull request's base commit is no longer the HEAD commit of its target branch. This means it includes changes from outside the original pull request, including, potentially, unrelated coverage changes.
Details
💛 - Coveralls |
@@ -74,9 +76,9 @@ fn parse_symbol(s: &str) -> IResult<&str, SymbolExpr, VerboseError<&str>> { | |||
// if array indexing is required in the future | |||
// add indexing in Symbol struct | |||
let s = format!("{v}[{i}]"); | |||
Ok(SymbolExpr::Symbol(Arc::new(s))) | |||
Ok(SymbolExpr::Symbol(Symbol::new(&s, None, None))) |
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.
Here we parse index of vector element, so Symbol can be initialized as
Ok(SymbolExpr::Symbol(Symbol::new(&s, None, None))) | |
Ok(SymbolExpr::Symbol(Symbol::new(v, None, Some(i)))) |
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.
Adding just a couple of comments before you push the PyParameterExpression
split stuff.
UnknownParameter(Symbol), | ||
#[error("Cannot bind following parameters not present in expression: {0:?}")] | ||
UnknownParameters(HashSet<Symbol>), |
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.
These should be combined I'd think, since otherwise callers have to handle both errors.
// let as_str = expr.call_method0("__str__")?; | ||
// let params = expr.getattr(parameters_attr)?; | ||
// println!("expr {as_str:?} {params:?}"); |
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.
// let as_str = expr.call_method0("__str__")?; | |
// let params = expr.getattr(parameters_attr)?; | |
// println!("expr {as_str:?} {params:?}"); |
// let as_str = new_expr.call_method0("__str__")?; | ||
// let params = new_expr.getattr(parameters_attr)?; | ||
// println!("new_expr {as_str:?} {params:?}"); |
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.
// let as_str = new_expr.call_method0("__str__")?; | |
// let params = new_expr.getattr(parameters_attr)?; | |
// println!("new_expr {as_str:?} {params:?}"); |
} | ||
} | ||
|
||
/// Attempt to extract a [PyParameterExpression] from a bound [PyAny]. |
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.
/// Attempt to extract a [PyParameterExpression] from a bound [PyAny]. | |
/// Attempt to extract a [ParameterExpression] from a bound [PyAny]. |
// A map keeping track of all symbols, with their name. This map *must* be kept | ||
// up to date upon any operation performed on the expression. | ||
// TODO it would be nicer to just store [Symbol]s as values, which can maybe be done | ||
// with a secondary PyParameterExpression storing the [PyParameter] in a map. |
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.
This comment seems to imply that ParameterExpression
s are mutable. I guess they are technically because these fields are public, but would it be better if they weren't? It looks like in most cases any operation on a ParameterExpression
results in a brand new instance.
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.
They are immutable -- this was a comment targeting devs 🙂 With only the double backslash this shouldn't show up in any docs, right?
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.
It's a good idea to make these attributes private. I think some attributes I just marked public because I needed them at some points where they weren't accessible. I'll go through to check that they are correctly private 👍🏻
} | ||
} | ||
|
||
/// Substitute symbols with [PyParameterExpression]s. |
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.
/// Substitute symbols with [PyParameterExpression]s. | |
/// Substitute symbols with [ParameterExpression]s. |
Probably should do a replace-all for this.
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.
Not a complete review yet, but submitting what I have so far. I'll plan to finish up on Monday 🙂.
// A map keeping track of all symbols, with their name. This map *must* be kept | ||
// up to date upon any operation performed on the expression. |
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.
The reason this comment bugs me is that it makes it seem like as a developer, I should expect to mutate a ParameterExpression
while performing operations that involve it. But it sounds like from your earlier reply that this would only ever be set once during construction. Maybe tweak it to say "This map must have entries for all symbols used by the expression."
|
||
impl Hash for ParameterExpression { | ||
fn hash<H: Hasher>(&self, state: &mut H) { | ||
self.expr.to_string().hash(state); |
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.
Just to be sure, it's not possible for two different expressions from a tree-perspective to have the same string representation, correct?
match self.expr.eval(true) { | ||
Some(e) => e.to_string(), | ||
None => self.expr.optimize().to_string(), |
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.
Why should we evaluate or optimize the expression? Isn't it better to just display it exactly as it is? Imagine if we compare two expressions for equality and find that they aren't equal, but then when we print, they evaluate to the same thing.
/// | ||
/// Caution: The caller **guarantees** that ``name_map`` is consistent with ``expr``. | ||
/// If uncertain, call [Self::from_symbol_expr], which automatically builds the correct name map. | ||
pub fn new(expr: &SymbolExpr, name_map: &HashMap<String, Symbol>) -> Self { |
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 the use-case for creating a custom mapping, rather than reading it from the SymbolExpr
?
In any case, this function should at least take both of its inputs by value and force the caller to do the clone. This is better practice in general since it is honest about the ownership needs of the call.
} | ||
|
||
/// Construct from a [Symbol]. | ||
pub fn from_symbol(symbol: &Symbol) -> Self { |
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.
pub fn from_symbol(symbol: &Symbol) -> Self { | |
pub fn from_symbol(symbol: Symbol) -> Self { |
Same comment
/// | ||
/// This is backed by Qiskit's symbolic expression engine and a cache | ||
/// for the parameters inside the expression. | ||
#[pyclass( |
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 think you can just add hash
to this list if you want to skip having __hash__
.
pub struct PyParameterExpression { | ||
pub inner: Arc<RwLock<ParameterExpression>>, | ||
// in contrast to ParameterExpression::name_map, this stores [PyParameter]s as value | ||
pub name_map: HashMap<String, PyParameter>, |
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.
We already talked about this offline, but it feels like it's probably better not to have this mapping if we already have one inside ParameterExpression
.
let name_map = value | ||
.name_map | ||
.iter() | ||
.map(|(name, symbol)| (name.clone(), PyParameter::new(symbol))) | ||
.collect(); |
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.
let name_map = value | |
.name_map | |
.iter() | |
.map(|(name, symbol)| (name.clone(), PyParameter::new(symbol))) | |
.collect(); | |
let name_map = value | |
.name_map | |
.into_iter() | |
.map(|(name, symbol)| (name, PyParameter::new(symbol))) | |
.collect(); |
maybe?
fn extract_coerce(ob: &Bound<'_, PyAny>) -> PyResult<Self> { | ||
if let Ok(i) = ob.extract::<i64>() { | ||
Ok( | ||
ParameterExpression::new(&SymbolExpr::Value(Value::from(i)), &HashMap::new()) |
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.
Yes, this callsite definitely looks like it wants to take its arguments by value!
|
||
/// Check if the expression corresponds to a plain symbol. | ||
/// | ||
/// TODO can we delete this? Not part of public interface before. |
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.
If we don't, then it will be. Just making a note here to delete or prefix with _
.
Summary
Based on the excellent work in #14207, this is PR implements a slightly alternative approach to porting the
ParameterExpression
to Rust, based on discussions with @doichanj, @kevinhartman and @mtreinish. The main differences are outlined below. We should compare both approaches and check which one is closer to what we'd like. Of course the efforts could also be merged.Details and comments
This PR does not supersede #14207, it is merely a slightly different approach and it's majority is based on #14207. The main differences are:
ParameterExpression
,Parameter
andParameterVectorElement
fully in Rust, without a Python-side class. This is done to reduce the code base and maintainer effort. This could also improve performance as we need less back and forth between the Rust and Python boundaries.Symbol
in the symbolic expression engine itself, rather than keep a separate map of symbols to their UUID. This is done to couple the UUID and symbol closer instead of relying on a third object to make this connection.Things that could make this PR better (maybe in this PR or a follow up)
ParameterExpression
andPyParameterExpression
for clearer Python split. Doing this would allow us to make the expression'sname_map
useSymbol
s directly. Let's think about how we want to use this from C!Symbol
not apyclass
(probably easy to do, just didn't get to it yet)Symbol
and have anIndexedSymbol
, like in Move Parameter classes from Python to Rust #14207?Co-authored-by: "U-AzureAD\JUNDOI" [email protected]