Closed Strilanc closed 4 years ago
to take an observable instead of searching in the circuit for displays
I think this is a really nice consequence of constraining displays into observables
@babbush @john6060 @jarrodmcc
I think this is a nice idea. But when you say "don't allow observables to be added into circuits", what do you mean exactly? Do you mean that all observables will be understood to be taken at the end of a circuit? While this is appropriate for most of the early NISQ experiments, eventually we'll want to be able to schedule measurements during a circuit. I realize that fast feedforward is still years away - that's not what I'm talking about. I'm talking about some scheme like (https://arxiv.org/abs/1711.07500) where measurements during the circuit are used to create dissipative maps that extract entropy. Its something I expect we'll want in the next couple years.
Also, I do think that while doing classical simulations it will often be a useful debugging tool to be able to request expectation values at intermediate parts of the circuit, even though that is clearly not practical to do on the chip.
eventually we'll want to be able to schedule measurements during a circuit. I realize that fast feedforward is still years away
Displays cannot be used in that way. Later parts of the circuit do not have access to the statistical result that was computed by the display during its associated sampling runs.
it will often be a useful debugging tool to be able to request expectation values at intermediate parts of the circuit
Yes. But that functionality can be emulated relatively easily by a user submitting the first half of a circuit.
Here is some rough prototyping code:
async def sample_observable(sampler: Sampler,
circuit: cirq.Circuit,
observable: Observable[TKey, TVal],
desired_concurrent_jobs: int,
desired_total_samples: int) -> TVal:
pool = CompletionOrderedAsyncWorkPool()
collector = observable.collector_for_circuit(circuit)
remaining_samples = desired_total_samples
active_jobs = 0
async def _get_new_pool_work():
nonlocal remaining_samples
nonlocal active_jobs
job = collector.next_job()
if job is None:
return None
remaining_samples -= job.repetitions
active_jobs += 1
return job, await sampler.sample_async(
job.circuit, repetitions=job.repetitions)
while remaining_samples > 0:
while active_jobs < desired_concurrent_jobs:
work = _get_new_pool_work()
if work is None:
break
pool.include_work(work)
if active_jobs < desired_concurrent_jobs:
break
if active_jobs:
done_job, done_val = await pool.__anext__()
collector.on_job_result(done_job, done_val)
pool.no_more_work()
async for done_job, done_val in pool:
collector.on_job_result(done_job, done_val)
return collector.statistic()
class PauliObservable(Observable[cirq.PauliString]):
def __init__(self, terms: cirq.LinearDict[cirq.PauliString]):
self.terms = terms
def collector_for_circuit(self, circuit: cirq.Circuit):
return PauliObservableCollector(circuit, self)
class PauliObservableCollector(ObservableCollector[cirq.PauliString]):
def __init__(self, circuit: cirq.Circuit, observable: PauliObservable):
self._circuit = circuit
self._observable = observable
self._zeros = cirq.LinearDict()
self._ones = cirq.LinearDict()
def on_job_result(self, job: CircuitJob, result: cirq.TrialResult):
parities = result.histogram(
key='out',
fold_func=lambda rep: np.sum(rep) & 1)
self._zeros[job.key] += parities[0]
self._ones[job.key] += parities[1]
def next_job(self) -> Optional[CircuitJob[cirq.PauliString]]:
pauli = random.choice(self._observable.terms.keys())
yield CircuitJob(
circuit=with_pauli_string_out(self._circuit, pauli),
repetitions=1000,
key=pauli)
def statistic(self) -> float:
result = 0
for term, coef in self._observable.terms.items():
a = self._zeros[term]
b = self._ones[term]
if a + b:
result += coef * (a - b) / (a + b)
return result
it will often be a useful debugging tool to be able to request expectation values at intermediate parts of the circuit
Yes. But that functionality can be emulated relatively easily by a user submitting the first half of a circuit.
I don't know if this is the right place to discuss this, but do you have an interface in mind for simulating part of a circuit, resolving expectation values, then picking up simulation where it left off to continue?
This functionality would improve efficiency for measurement of noncommuting observables. e.g. to measure <X0 + Z0> the simulator should run the circuit to do sampling on
A version of this exists in pauli_sum_collector
for samplers. I'm going to close this, because I think we are moving to that pattern, though there is still work to be done there.
I was talking with @ncrubin and @mpharrigan about displays, and they made some good points against them. Mainly it comes down to: this feature has a ton of engineering complexity, but cover a lot of use cases outside of what researchers actually need to do. We would likely be better served by, instead of a "compute_displays" method where there can be a series of displays strewn throughout the circuit, having an "estimate_observable" method that specifies a single observable we would like to apply to the output of the circuit.
So the proposal is:
@dabacon @kevinsung