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

Fix #45: Parameterized measurement patterns #158

Open
wants to merge 43 commits into
base: master
Choose a base branch
from

Conversation

thierry-martinez
Copy link
Contributor

This commit is yet another tentative to implement parameterized measurement patterns, to fulfill issue #45 (previous tentative: #68).

This commit adds two methods to the class Pattern:

  • is_parameterized() returns True if there is at least one measurement angle that is not just an instance of numbers.Number: indeed, a parameterized pattern is a pattern where at least one measurement angle is an expression that is not a number, typically an instance of sympy.Expr (but we don't force to choose sympy here).

  • subs(variable, substitute) returns a copy of the pattern where occurrences of the given variable in measurement angles are substituted by the given value. Substitution is performed by calling the method subs on measurement angles, if the method exists, which is the case in particular for sympy.Expr. If the substitution returns a number, this number is coerced to float, to get numbers that implement the full number protocol (in particular, sympy numbers don't implement cos).

@thierry-martinez thierry-martinez force-pushed the parameterized branch 2 times, most recently from 3093bdd to a5e16de Compare May 30, 2024 09:38
Copy link

codecov bot commented May 30, 2024

Codecov Report

Attention: Patch coverage is 81.87050% with 126 lines in your changes missing coverage. Please review.

Project coverage is 72.95%. Comparing base (ec4c582) to head (5feac05).
Report is 5 commits behind head on master.

Files Patch % Lines
graphix/parameter.py 80.54% 122 Missing ⚠️
graphix/transpiler.py 80.95% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #158      +/-   ##
==========================================
+ Coverage   71.82%   72.95%   +1.13%     
==========================================
  Files          30       31       +1     
  Lines        5359     6038     +679     
==========================================
+ Hits         3849     4405     +556     
- Misses       1510     1633     +123     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@shinich1
Copy link
Contributor

@thierry-martinez great, is the intention to merge @pafloxy's #68-equivalent code first (when it's ready), before merging this?

@thierry-martinez
Copy link
Contributor Author

@thierry-martinez great, is the intention to merge @pafloxy's #68-equivalent code first (when it's ready), before merging this?

I adapted #68 code for Parameter class in the commit 81bbeb7 I just pushed. #68 was incorrect because arithmetic operators changed parameters in place (computing alpha + 1 did change the value of alpha to alpha + 1!). In this PR, we distinguish between Parameter (alpha) and ParameterExpression (alpha + 1), and a Parameter is a particular case of ParameterExpression.

The test test_parameter_simulation is illustrative: we can either substitute a parameter by a value in a pattern then simulate, or simulate symbolically (given that the pattern is deterministic!) then substitute in the (symbolic) state vector or the density matrix, and we get the same result.

Copy link

@pafloxy pafloxy left a comment

Choose a reason for hiding this comment

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

I think as for now this is all that I was looking for, If @shinich1 thinks its alright maybe we should go for the merge. :)

@shinich1
Copy link
Contributor

shinich1 commented Jun 1, 2024

@thierry-martinez @pafloxy great! give us a few days to look through. A few quick comments:

  1. is TN backend working with parameters, or should we throw error in TN backend if pattern is parametrised?
    2. could you add sympy to requirements.txt?
  2. will circuit.simulate_statevector, pattern.perform_pauli_measurements, as well as visualization tool, work with parameters?

@shinich1 shinich1 requested a review from EarlMilktea June 1, 2024 09:45
@EarlMilktea
Copy link
Contributor

Let me review it later.

Anyway, is it absolutely necessary to use sympy?
I'm concerned with...

  • performance: Personally I feel sympy is far from performant.
  • type stability: sympy has no type annotations, and it seems that not even planned.
  • developer experience: Sometimes sympy makes linter (ex. flake8, Ruff) extremely slow.

Additionally, let me point out that @masa10-f is planning to completely eliminate sympy from the source.

@EarlMilktea
Copy link
Contributor

If sympy is absolutely necessary, it's OK, but we may need to verify that we're using it wisely.

expected by the simulator back-ends.
"""

def __init__(self, expression: sp.Expr):
Copy link
Contributor

Choose a reason for hiding this comment

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

Please do not omit -> None.

def __repr__(self) -> str:
return str(self._expression)

def subs(self, variable, value) -> ParameterExpression | numbers.Number:
Copy link
Contributor

Choose a reason for hiding this comment

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

Please use complex instead of abstract numbers.Number .

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Numbers from numpy (or sympy...) are not necessarily instances of complex, for instance:

isinstance(np.array([1])[0], complex) == False
isinstance(np.array([1])[0], numbers.Number) == True
isinstance(sp.parse_expr(1), complex) == False
isinstance(sp.parse_expr(1), numbers.Number) == True

Copy link
Contributor

Choose a reason for hiding this comment

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

So do you mean this block can sometimes return non-complex value?

if isinstance(result, numbers.Number):
    return complex(result)

Copy link
Contributor

@EarlMilktea EarlMilktea Jun 3, 2024

Choose a reason for hiding this comment

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

If you're sure that complex(result) is always complex type, please use ParameterExpression | complex.

I personally believe that return type annotation should be as concrete as possible, or we need extra type narrowings.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oops, sorry, I answered without thinking enough: you are right, and after the coercion, the result is complex indeed.

assert np.allclose(sv.psi, sv2.subs(alpha, 0.5).psi)
"""

def __init__(self, name):
Copy link
Contributor

Choose a reason for hiding this comment

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

Add -> None .

name : str
name of the parameter, used for binding values.
"""
assert isinstance(name, str)
Copy link
Contributor

Choose a reason for hiding this comment

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

Please do not use assert if you're planning to expose this class.

super().__init__(sp.Symbol(name=name))

@property
def name(self):
Copy link
Contributor

Choose a reason for hiding this comment

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

Please explicitly specify the return type.

op_mat = op_mat_from_result(vec, result)
state.evolve_single(op_mat, qubit)
return result


class Backend:
def __init__(self, pr_calc: bool = True):
def __init__(self, pr_calc: bool = True, rng: np.random.Generator = None):
Copy link
Contributor

Choose a reason for hiding this comment

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

Please use np.random.Generator | None.


class DensityMatrixBackend(graphix.sim.base_backend.Backend):
"""MBQC simulator with density matrix method."""

def __init__(self, pattern, max_qubit_num=12, pr_calc=True):
def __init__(self, pattern, max_qubit_num=12, pr_calc=True, rng: np.random.Generator = None):
Copy link
Contributor

Choose a reason for hiding this comment

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

Use | None .

@pafloxy
Copy link

pafloxy commented Jun 6, 2024

@thierry-martinez @pafloxy great! give us a few days to look through. A few quick comments:

  1. is TN backend working with parameters, or should we throw error in TN backend if pattern is parametrised?
    2. could you add sympy to requirements.txt?
  2. will circuit.simulate_statevector, pattern.perform_pauli_measurements, as well as visualization tool, work with parameters?

@shinich1 @thierry-martinez
Maybe you guys figured it already but I think we need to make some small modification to the visualization.py module as well. There are some checks to determine if a measurement angle is Pauli in some of the class methods for GraphVisualizer of the form

elif (
                show_pauli_measurement
                and self.meas_angles is not None
                and (
                    2 * self.meas_angles[node] == int(2 * self.meas_angles[node])
                )  # measurement angle is integer or half-integer
            ):

where we might need to add another line or so to exclude the case where the measurement angles is an instance of Parameter/ParameterExpression.

@shinich1
Copy link
Contributor

Hi @pafloxy @thierry-martinez

I think the best way is to move parameter implementation with sympy out of main graphix repository, for example a separate repo (wrapper/module) dedicated to parameterized patterns and their executions (it seems possible, given the relatively small changes required to implement?). I am happy to initiate a creation of such repository in teamgraphix organization, if that makes sense to you? for example, that can be part of extra pip installation.

The reason mostly follows @EarlMilktea 's. Sustaining maintainability is going to help a lot in the long run for everyone contributing to this repo and will help a lot those maintaining.

  1. sympy has quite a performance issue. Looking at their github repo, it seems like a recurring issue in qiskit (which has sympy incorporated) and quite a bit of effort can be saved by deciding to move it out at this point (nb as @EarlMilktea mentioned sympy will be removed from requirements.txt when gflow is refactored for performance soon.)
  2. typing, linter compatibility, as in @EarlMilktea's comment

@thierry-martinez
Copy link
Contributor Author

I propose in my last commits a version which does not require mypy.

  • The module parameter.py implements symbolic expressions so as to support parameters, but without all the symbolic. machinery of mypy (computations with known values are done numerically, not symbolically).

  • The rest of the code does not depend on parameter.py: in particular, the code for simulators is generic. If we still need the sympy version of parameters, we can implement it separately, and simulators should work without any specific modification in the code of graphix.

  • Tests for parameters are done in a specific test_parameter.py module, and the tests include circuit simulation.

@shinich1
Copy link
Contributor

shinich1 commented Jun 20, 2024

I propose in my last commits a version which does not require mypy.

  • The module parameter.py implements symbolic expressions so as to support parameters, but without all the symbolic. machinery of mypy (computations with known values are done numerically, not symbolically).
  • The rest of the code does not depend on parameter.py: in particular, the code for simulators is generic. If we still need the sympy version of parameters, we can implement it separately, and simulators should work without any specific modification in the code of graphix.
  • Tests for parameters are done in a specific test_parameter.py module, and the tests include circuit simulation.

Thank you! I believe you mean sympy instead of mypy above? Let me take a detailed look at the code soon.

@shinich1
Copy link
Contributor

shinich1 commented Jun 23, 2024

@thierry-martinez the implementation looks good to me! we need to:

  • fix visualization which seems to throw error in the presence of parameter in pattern
  • test more situations, so we can catch more errors (currently rz only with no arithmetic of param covered?)
  • do we intend to cover simulations without subs? if so we should throw error if parameterized and being simulated?
  • update QAOA, VQE and QNN examples.
  • add appropriate page to docs (just autodoc, not much to write - see existing pages)
  • device interface - may need care. easy solution is to ask subs before PatternRunner initialization.

else:
return NotImplemented

def __rsub__(self, other) -> Expression | complex | type(NotImplemented):
Copy link
Contributor

Choose a reason for hiding this comment

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

Duplicated.

class Expression:
"""Expression with parameters."""

def __mul__(self, other) -> Expression | complex | type(NotImplemented):
Copy link
Contributor

Choose a reason for hiding this comment

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

Please do not specify NotImplemented type, as this value itself is never returned from __mul__.

It's just a marker for internals.

class Expression:
"""Expression with parameters."""

def __mul__(self, other) -> Expression | complex | type(NotImplemented):
Copy link
Contributor

Choose a reason for hiding this comment

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

Please annotate other.

def subs(self, variable: Parameter, value: Expression | numbers.Number) -> Expression | complex:
if self == variable:
if isinstance(value, numbers.Number):
return complex(value)
Copy link
Contributor

Choose a reason for hiding this comment

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

Type checker complains that Number cannot be converted to complex. Could you check it?

def type(self) -> type:
return self.__type

def subs(self, variable: Parameter, value: Expression | numbers.Number) -> Expression | complex:
Copy link
Contributor

Choose a reason for hiding this comment

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

I once here that numbers.XXX does not work efficiently with modern typing systems (see Fluent Python 2nd ed.).

Do you come up with alternatives to that?



class Compound(Expression):
def __init__(self, children: typing.Iterable[Expression | complex]) -> None:
Copy link
Contributor

Choose a reason for hiding this comment

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

  • typing is not imported at all.
  • Please import from collections.abc.

return simplify_product(simpl)

def is_float(self) -> bool:
return lhs.is_float() and rhs.is_float()
Copy link
Contributor

Choose a reason for hiding this comment

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

lhs undefined.

terms[expr] = terms.get(expr, 0) + coef


def syntactic_order_key(key: Expression | complex | None) -> Any:
Copy link
Contributor

Choose a reason for hiding this comment

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

Please import typing. Additionally, please do not use Any if possible as it ruins type annotations.

@EarlMilktea
Copy link
Contributor

I'm a little bit concerned about...

  • complexity: Is it possible to maintain parameter.py ? It's already really huge.
  • performance: I feel that current impl. cannot be faster than sympy as it's doing almost the same things.

@shinich1
Copy link
Contributor

shinich1 commented Jul 3, 2024

We suggest the following:

@thierry-martinez
Copy link
Contributor Author

As @shinich1 suggested in the above message, this PR now only provides abstract base classes for parameters, and a minimal concrete class for placeholders (symbolic angles that can solely be substituted, and that cannot appear in computation). The sympy part is implemented as a plugin in the graphix-symbolic repo: TeamGraphix/graphix-symbolic#1 .

thierry-martinez added a commit to thierry-martinez/graphix that referenced this pull request Jul 10, 2024
graphix/parameter.py Outdated Show resolved Hide resolved
tests/test_parameter.py Outdated Show resolved Hide resolved
This commit is yet another tentative to implement parameterized
measurement patterns, to fulfill issue TeamGraphix#45 (previous tentative: TeamGraphix#68).

This commit adds two methods to the class `Pattern`:

- `is_parameterized()` returns True if there is at least one
measurement angle that is not just an instance of numbers.Number:
indeed, a parameterized pattern is a pattern where at least one
measurement angle is an expression that is not a number, typically an
instance of `sympy.Expr` (but we don't force to choose sympy here).

- `subs(variable, substitute)` returns a copy of the pattern where
occurrences of the given variable in measurement angles are
substituted by the given value.  Substitution is performed by calling
the method `subs` on measurement angles, if the method exists, which
is the case in particular for `sympy.Expr`. If the substitution
returns a number, this number is coerced to `float`, to get numbers
that implement the full number protocol (in particular, sympy numbers
don't implement `cos`).
`parameter.py` is inspired by TeamGraphix#68 but is split in Parameter and
ParameterExpression.
@shinich1
Copy link
Contributor

shinich1 commented Aug 20, 2024

@thierry-martinez the implementation looks good to me! we need to:

  • fix visualization which seems to throw error in the presence of parameter in pattern
  • test more situations, so we can catch more errors (currently rz only with no arithmetic of param covered?)
  • do we intend to cover simulations without subs? if so we should throw error if parameterized and being simulated?
  • update QAOA, VQE and QNN examples.
  • add appropriate page to docs (just autodoc, not much to write - see existing pages)
  • device interface - may need care. easy solution is to ask subs before PatternRunner initialization.

bump above list posted some time ago

@thierry-martinez
Copy link
Contributor Author

Thank you, @shinich1, for the list!

  • fix visualization which seems to throw error in the presence of parameter in pattern

Fixed in 34c16ed, with a dedicated test.

  • test more situations, so we can catch more errors (currently rz only with no arithmetic of param covered?)

Should have been fixed with test on random circuits (05c05a0).

  • do we intend to cover simulations without subs? if so we should throw error if parameterized and being simulated?

Simulations without subs are handled with graphix-symbolic. In 3a6e38d, I added a test to check that there is an exception PlaceholderOperationError if a simulation is run on a circuit with a non-substituted Placeholder.

  • update QAOA, VQE and QNN examples.

I am not sure what to do with QAOA since there is only one pattern in qaoa.py.
I added a placeholder version in VQE (with performance comparison) in 4cb326b.
I added placeholders for QNN in b337abe.

  • add appropriate page to docs (just autodoc, not much to write - see existing pages)

Placeholders and affine expressions are documented in 149b089.

  • device interface - may need care. easy solution is to ask subs before PatternRunner initialization.

I added a test to check that there is an exception CircuitError if a PatternRunner is initialized on non-substituted parameterized patterns in 1e2f3c1.

@EarlMilktea
Copy link
Contributor

@thierry-martinez

Is it impossible to move symbolic-related ABCs/subs functions to graphix-symbolic ?
Additionally, please do not define unnecessary mathematical/arithmetic abstract methods in AffineExpression, as they're not implemented or even used in graphix: consider extending it in the separate package.
I understand graphix should be updated so that non-float can pass through functions, but the impact should be minimal.

@thierry-martinez
Copy link
Contributor Author

@EarlMilktea I believe that all the functions (subs, xreplace, and the trigonometric functions) are used in graphix. Simple and parallel substitutions apply to both Placeholder and AffineExpression. All trigonometric functions are potentially called during simulation; in these cases, AffineExpression raises an Exception, as expected, since simulation is not supported. The purpose of the parameter module is to define a common interface, which is why an abstract base class is defined.

@EarlMilktea
Copy link
Contributor

@thierry-martinez
OK, I trust you.
Let me do final checks...

examples/MBQCvqe.py Outdated Show resolved Hide resolved
@thierry-martinez
Copy link
Contributor Author

I added missing xreplace in Circuit, Statevec and DensityMatrix, and I replace some uses of numbers.Number with typing.SupportsComplex.

@EarlMilktea
Copy link
Contributor

EarlMilktea commented Aug 23, 2024

I re-reviewed you code and felt that the whole logic of graphix (inclding simulator) is affected.
Could you request reviews from @masa10-f , as I'm not sure your idea is compatible with ZX calculus?
Additionally, please wait until his changes are merged and update your code accordingly.
Meantime, let me carefully examine your code in terms of type stability, performance, and maintainability.

Copy link
Contributor

@EarlMilktea EarlMilktea left a comment

Choose a reason for hiding this comment

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

WIP: May be updated later.

@@ -59,7 +60,9 @@ class RotationInstruction(OneQubitInstruction):
Rotation instruction base class model.
"""

angle: float
model_config = ConfigDict(arbitrary_types_allowed=True)
Copy link
Contributor

Choose a reason for hiding this comment

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

This is introducing an "escape-hatch" to BaseModel.
If this is really necessary, let's remove validation itself completely.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Setting arbitrary_types_allowed=True does not eliminate validation. For types that do not implement their own validation (such as Expression in this case), the validation defaults to using isinstance, which is appropriate. Previously, I implemented validation in Expression specifically using isinstance (as discussed here: #158 (comment)). Both approaches are equivalent: we can either specify the use of isinstance for validation within the Expression implementation, or we can specify it when using Expression within the model.

Copy link
Contributor

Choose a reason for hiding this comment

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

I see. Sorry for my misunderstanding...
By the way, why BaseModel is introduced here in the first place?

graphix/parameter.py Show resolved Hide resolved
graphix/parameter.py Outdated Show resolved Hide resolved
graphix/parameter.py Outdated Show resolved Hide resolved
"""Expression with parameters."""

@abstractmethod
def __mul__(self, other) -> ExpressionOrFloat: ...
Copy link
Contributor

Choose a reason for hiding this comment

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

Please do not omit annotations as I'm planning to enable Ruff rule ANN (enforce type annotation) for easier code reviews.
(Any would be acceptable here though)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in b720e48.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah sorry, I made you confused.
My true implication here is that, Any with @abstractmethod is acceptable (as we cannot call them at all and the concrete argument types can be overridden), but as usual, not allowed for implementation.

Copy link
Contributor

@EarlMilktea EarlMilktea Aug 24, 2024

Choose a reason for hiding this comment

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

Memo: mypy check results for this file:

graphix/parameter.py:185: error: Argument 1 to "float" has incompatible type "Expression | SupportsFloat"; expected "str | Buffer | SupportsFloat | SupportsIndex"  [arg-type]
graphix/parameter.py:282: error: Argument 2 to "subs" of "Expression" has incompatible type "Expression | SupportsFloat"; expected "Expression | float"  [arg-type]
graphix/parameter.py:285: error: Incompatible return value type (got "Expression", expected "T | complex")  [return-value]
graphix/parameter.py:302: error: Argument 1 to "xreplace" of "Expression" has incompatible type "Mapping[Parameter, Expression | SupportsFloat]"; expected "Mapping[Parameter, Expression | float]"  [arg-type]
graphix/parameter.py:305: error: Incompatible return value type (got "Expression", expected "T | complex")  [return-value]

graphix/parameter.py Outdated Show resolved Hide resolved
op_mat = np.eye(2, dtype=np.complex128) / 2
else:
# At least one value is symbolic (i.e., parameterized).
op_mat = np.eye(2, dtype="O") / 2
Copy link
Contributor

Choose a reason for hiding this comment

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

dtype="O" needs extra care as it can impact the performance of simulators.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We know that symbolic simulations are extra-costly!

Copy link
Contributor

Choose a reason for hiding this comment

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

Then I suggest completely isolating the logic to different functions: numpy is notorious for unpredictable automatic type conversion, so I don't want it close to numerical simulator that should be as lightweight as possible.
Current approach (e.g., add conditional branches to existing functions) is very vulnerable to accidental contamination.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants