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

Lock down CircuitOperation and ParamResolver #5548

Merged

Conversation

95-martin-orion
Copy link
Collaborator

@95-martin-orion 95-martin-orion commented Jun 17, 2022

Fixes #4327. Fixes #5507.

This PR converts all ParamResolver and CircuitOperation attributes to _private attributes with read-only @property accessors. The CircuitOperation.extern_keys field is not given an accessor as it is exclusively for decomposition and should never be referenced outside the class.

Attributes with mutable types (e.g. dicts) are still technically possible to mutate, but will fail mypy checks if this is attempted. Comments have been added to further clarify the prohibition on mutation.

This is technically a breaking change, but the workflows it breaks are limited to mutations of CircuitOperation and ParamResolver which were advised against in docstrings.

@95-martin-orion 95-martin-orion requested review from a team, vtomole and cduck as code owners June 17, 2022 17:16
@CirqBot CirqBot added the size: M 50< lines changed <250 label Jun 17, 2022
@95-martin-orion 95-martin-orion added the BREAKING CHANGE For pull requests that are important to mention in release notes. label Jun 17, 2022
@95-martin-orion
Copy link
Collaborator Author

Changes to resolve type conflicts were made with the goal of minimizing impact to the code. This may result in slightly reduced performance in the affected code (specifically any of the new dict calls to avoid retyping Dict args to Mapping) but should not break any additional workflows.

If we really wanted to, we could retype the affected protocols to use Mapping to regain the lost performance, but that would be a huge breaking change.

@maffoo maffoo self-requested a review June 17, 2022 18:56
@@ -531,7 +531,7 @@ def measure_grouped_settings(
for max_setting, param_resolver in itertools.product(
grouped_settings.keys(), study.to_resolvers(circuit_sweep)
):
circuit_params = param_resolver.param_dict
circuit_params = dict(param_resolver.param_dict)
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's file an issue about fixing the types so we can avoid the copies here and in sample_expectation_values below. I think these should be fixable without worrying too much about compatibility, since AFAICT the dicts are not exposed anywhere in a way that is intended to be mutable, but we can deal with that in a future 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.

Opened #5554.

frozenset(self.measurement_key_map.items()),
self.param_resolver,
self.parent_path,
tuple([] if self.repetition_ids is None else self.repetition_ids),
Copy link
Contributor

Choose a reason for hiding this comment

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

alternately:

Suggested change
tuple([] if self.repetition_ids is None else self.repetition_ids),
() if self.repetition_ids is None else tuple(self.repetition_ids),

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done.

@@ -259,7 +315,7 @@ def _measurement_key_objs_(self) -> AbstractSet['cirq.MeasurementKey']:
self,
'_cached_measurement_key_objs',
{
protocols.with_measurement_key_mapping(key, self.measurement_key_map)
protocols.with_measurement_key_mapping(key, dict(self.measurement_key_map))
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's file an issue to fix the with_measurement_key_mapping protocol to we can avoid the copy here and on line 361 below.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done: #5552. Unclear if this should be before or after 1.0 (may be more effort than it's worth likely breaking but not 100% certain), so I marked as triage-discuss.

Comment on lines 244 to 256
kwargs = {
'circuit': self.circuit,
'repetitions': self.repetitions,
'qubit_map': self.qubit_map,
'measurement_key_map': self.measurement_key_map,
'param_resolver': self.param_resolver,
'repetition_ids': self.repetition_ids,
'parent_path': self.parent_path,
'extern_keys': self._extern_keys,
'use_repetition_ids': self.use_repetition_ids,
'repeat_until': self.repeat_until,
}
kwargs.update(changes)
Copy link
Contributor

Choose a reason for hiding this comment

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

can use literal unpacking since later value overwrite earlier ones:

Suggested change
kwargs = {
'circuit': self.circuit,
'repetitions': self.repetitions,
'qubit_map': self.qubit_map,
'measurement_key_map': self.measurement_key_map,
'param_resolver': self.param_resolver,
'repetition_ids': self.repetition_ids,
'parent_path': self.parent_path,
'extern_keys': self._extern_keys,
'use_repetition_ids': self.use_repetition_ids,
'repeat_until': self.repeat_until,
}
kwargs.update(changes)
kwargs = {
'circuit': self.circuit,
'repetitions': self.repetitions,
'qubit_map': self.qubit_map,
'measurement_key_map': self.measurement_key_map,
'param_resolver': self.param_resolver,
'repetition_ids': self.repetition_ids,
'parent_path': self.parent_path,
'extern_keys': self._extern_keys,
'use_repetition_ids': self.use_repetition_ids,
'repeat_until': self.repeat_until,
**changes,
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done.

Comment on lines 126 to 132
# These fields are purely private: they should not be accessed
# from outside this class.
self._hash = None
self._cached_measurement_key_objs = None
self._cached_control_keys = None
self._cached_mapped_single_loop = None
self._extern_keys = extern_keys
Copy link
Contributor

Choose a reason for hiding this comment

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

Can make these cached properties instead:

from cirq._compat import cached_property

...
@cached_property
def _hash(self) -> int:
    return hash(...)

This should simplify some code below and let us get rid of a few more object.__setattr__ calls.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done. (The __setattr__ calls were no longer needed anyhow, but it's a good change anyways)

Copy link
Contributor

Choose a reason for hiding this comment

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

Didn't know this existed! There are lots of other places we do manual caching for our immutable circuit and op classes and final results. (I looked into using functools.lru_cache for those, but that caused memory leaks)

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, cached_property is very useful (it's part of functools in the standard library starting in python 3.8, but we still have to use a backport for now until we drop support for 3.7).

@95-martin-orion 95-martin-orion requested a review from maffoo June 17, 2022 22:43
Copy link
Contributor

@maffoo maffoo left a comment

Choose a reason for hiding this comment

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

Minor comments, then LGTM.

}

def _measurement_key_objs_(self) -> AbstractSet['cirq.MeasurementKey']:
return self._measurement_key_objs # type: ignore
Copy link
Contributor

Choose a reason for hiding this comment

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

What's the type error here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Looks like it was carry-over from the old code. Removed.

circuit = protocols.resolve_parameters(
circuit, self.param_resolver, recursive=False
)
return cast(circuits.Circuit, circuit)
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we have to return cirq.Circuit here or could this be cirq.AbstractCircuit? If the former then I'd suggest unfreezing instead:

Suggested change
return cast(circuits.Circuit, circuit)
return circuit.unfreeze(copy=False)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Public methods that consume this expect Circuit, so I think we're stuck with that. unfreeze seems to work well, though.

)
),
@cached_property
def _hash(self):
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: add return type

Suggested change
def _hash(self):
def _hash(self) -> int:

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done

)
)

def __hash__(self):
Copy link
Contributor

Choose a reason for hiding this comment

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

here, too, can add return type.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

and done.

extern_keys: FrozenSet['cirq.MeasurementKey'] = frozenset(),
use_repetition_ids: bool = True,
repeat_until: Optional['cirq.Condition'] = None,
):
Copy link
Contributor

Choose a reason for hiding this comment

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

Should the docstring be moved from the class to here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done.

if q_new.dimension != q.dimension:
raise ValueError(f'Qid dimension conflict.\nFrom qid: {q}\nTo qid: {q_new}')

self._measurement_key_map = measurement_key_map or {}
Copy link
Contributor

Choose a reason for hiding this comment

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

Defensive copy for the dicts? Or comment in the docstring to state no defensive copy will be made?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is a good call, as neither should be large enough for copy to be an issue and the dict must not change. Technically a breaking change, but that's par for the course on this PR.

@MichaelBroughton MichaelBroughton added the automerge Tells CirqBot to sync and merge this PR. (If it's running.) label Jun 22, 2022
@CirqBot CirqBot added the front_of_queue_automerge CirqBot uses this label to indicate (and remember) what's being merged next. label Jun 22, 2022
@CirqBot
Copy link
Collaborator

CirqBot commented Jun 22, 2022

Automerge cancelled: A required status check is not present.

Missing statuses: ['Pytest Ubuntu (3.7)', 'Pytest Ubuntu (3.8)', 'Pytest Ubuntu (3.9)', 'Pytest Windows (3.7)', 'Pytest Windows (3.8)', 'Pytest Windows (3.9)', 'Typescript lint check', 'Typescript tests', 'Typescript tests coverage']

@CirqBot CirqBot removed automerge Tells CirqBot to sync and merge this PR. (If it's running.) front_of_queue_automerge CirqBot uses this label to indicate (and remember) what's being merged next. labels Jun 22, 2022
@95-martin-orion 95-martin-orion merged commit 61fefe6 into quantumlib:master Jun 22, 2022
@95-martin-orion 95-martin-orion deleted the cirq-private-subcirq branch June 22, 2022 21:08
CirqBot pushed a commit that referenced this pull request Jun 25, 2022
Fixes #5552.

Since this change only* affects type annotations, it is non-breaking. Any breakages due to `ParamResolver.param_dict` becoming immutable should instead refer to #5548, where this change was applied.

*Okay, it also removes some `dict` calls, but those were added in #5548.
rht pushed a commit to rht/Cirq that referenced this pull request May 1, 2023
* Lock down CircuitOperation attributes.

* Reduce attribute lockdown

* Resolve type conflicts

* review comments

* docs and defensive copies

* document error modes

Co-authored-by: Cirq Bot <craiggidney+github+cirqbot@google.com>
rht pushed a commit to rht/Cirq that referenced this pull request May 1, 2023
Fixes quantumlib#5552.

Since this change only* affects type annotations, it is non-breaking. Any breakages due to `ParamResolver.param_dict` becoming immutable should instead refer to quantumlib#5548, where this change was applied.

*Okay, it also removes some `dict` calls, but those were added in quantumlib#5548.
harry-phasecraft pushed a commit to PhaseCraft/Cirq that referenced this pull request Oct 31, 2024
* Lock down CircuitOperation attributes.

* Reduce attribute lockdown

* Resolve type conflicts

* review comments

* docs and defensive copies

* document error modes

Co-authored-by: Cirq Bot <craiggidney+github+cirqbot@google.com>
harry-phasecraft pushed a commit to PhaseCraft/Cirq that referenced this pull request Oct 31, 2024
Fixes quantumlib#5552.

Since this change only* affects type annotations, it is non-breaking. Any breakages due to `ParamResolver.param_dict` becoming immutable should instead refer to quantumlib#5548, where this change was applied.

*Okay, it also removes some `dict` calls, but those were added in quantumlib#5548.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
BREAKING CHANGE For pull requests that are important to mention in release notes. size: M 50< lines changed <250
Projects
None yet
Development

Successfully merging this pull request may close these issues.

CircuitOperation mutability ParamResolver is hashable but mutable
5 participants