Skip to content

Commit

Permalink
Incorporating UUIDs in observables
Browse files Browse the repository at this point in the history
  • Loading branch information
HGSilveri committed Dec 13, 2024
1 parent ab1dfab commit 28c9c57
Show file tree
Hide file tree
Showing 5 changed files with 180 additions and 85 deletions.
11 changes: 11 additions & 0 deletions pulser-core/pulser/backend/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import copy
import warnings
from collections import Counter
from dataclasses import dataclass, field
from typing import (
Any,
Expand Down Expand Up @@ -141,12 +142,22 @@ def __init__(
**backend_options: Any,
) -> None:
"""Initializes the EmulationConfig."""
obs_tags = []
for obs in observables:
if not isinstance(obs, Observable):
raise TypeError(
"All entries in 'observables' must be instances of "
f"Observable. Instead, got instance of type {type(obs)}."
)
obs_tags.append(obs.tag)
repeated_tags = [k for k, v in Counter(obs_tags).items() if v > 1]
if repeated_tags:
raise ValueError(
"Some of the provided 'observables' share identical tags. Use "
"'tag_suffix' when instantiating multiple instances of the "
"same observable so they can be distinguished. "
f"Repeated tags found: {repeated_tags}"
)

if default_evaluation_times != "Full":
eval_times_arr = np.array(default_evaluation_times, dtype=float)
Expand Down
114 changes: 70 additions & 44 deletions pulser-core/pulser/backend/default_observables.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

import copy
import functools
import itertools
from collections import Counter
from collections.abc import Sequence
from typing import Any, Type
Expand All @@ -35,10 +34,13 @@ class StateResult(Observable):
evaluation_times: The relative times at which to store the state.
If left as `None`, uses the `default_evaluation_times` of the
backend's `EmulationConfig`.
tag_suffix: An optional suffix to append to the tag. Needed if
multiple instances of the same observable are given to the
same EmulationConfig.
"""

def name(self) -> str:
"""Returns the name of the observable."""
@property
def _base_tag(self) -> str:
return "state"

def apply(self, *, state: StateType, **kwargs: Any) -> StateType:
Expand All @@ -61,6 +63,9 @@ class BitStrings(Observable):
one_state: The eigenstate that measures to 1. Can be left undefined
if the state's eigenstates form a known eigenbasis with a
defined "one state".
tag_suffix: An optional suffix to append to the tag. Needed if
multiple instances of the same observable are given to the
same EmulationConfig.
"""

def __init__(
Expand All @@ -69,14 +74,17 @@ def __init__(
evaluation_times: Sequence[float] | None = None,
num_shots: int = 1000,
one_state: Eigenstate | None = None,
tag_suffix: str | None = None,
):
"""Initializes the observable."""
super().__init__(evaluation_times=evaluation_times)
super().__init__(
evaluation_times=evaluation_times, tag_suffix=tag_suffix
)
self.num_shots = num_shots
self.one_state = one_state

def name(self) -> str:
"""Returns the name of the observable."""
@property
def _base_tag(self) -> str:
return "bitstrings"

def apply(
Expand All @@ -95,22 +103,7 @@ def apply(
)


class _Counted:
"""A class with counted instances."""

_counter = itertools.count(0)

@classmethod
def reset_count(cls) -> None:
cls._counter = itertools.count(0)

@functools.cached_property
def index(self) -> int:
"""The instance count."""
return next(self._counter)


class Fidelity(Observable, _Counted):
class Fidelity(Observable):
"""Stores the fidelity with a pure state at the evaluation times.
The fidelity uses the overlap between the given state and the state of
Expand All @@ -124,29 +117,35 @@ class Fidelity(Observable, _Counted):
evaluation_times: The relative times at which to compute the fidelity.
If left as `None`, uses the `default_evaluation_times` of the
backend's `EmulationConfig`.
tag_suffix: An optional suffix to append to the tag. Needed if
multiple instances of the same observable are given to the
same EmulationConfig.
"""

def __init__(
self,
state: State,
*,
evaluation_times: Sequence[float] | None = None,
tag_suffix: str | None = None,
):
"""Initializes the observable."""
super().__init__(evaluation_times=evaluation_times)
super().__init__(
evaluation_times=evaluation_times, tag_suffix=tag_suffix
)
# TODO: Checks
self.state = state

def name(self) -> str:
"""Returns the name of the observable."""
return f"fidelity_{self.index}"
@property
def _base_tag(self) -> str:
return "fidelity"

def apply(self, *, state: State, **kwargs: Any) -> Any:
"""Calculates the observable to store in the Results."""
return self.state.overlap(state)


class Expectation(Observable, _Counted):
class Expectation(Observable):
"""Stores the expectation of the given operator on the current state.
Args:
Expand All @@ -155,22 +154,28 @@ class Expectation(Observable, _Counted):
`default_evaluation_times` of the backend's `EmulationConfig`.
operator: The operator to measure. Must be of the appropriate type
for the backend.
tag_suffix: An optional suffix to append to the tag. Needed if
multiple instances of the same observable are given to the
same EmulationConfig.
"""

def __init__(
self,
operator: Operator,
*,
evaluation_times: Sequence[float] | None = None,
tag_suffix: str | None = None,
):
"""Initializes the observable."""
super().__init__(evaluation_times=evaluation_times)
super().__init__(
evaluation_times=evaluation_times, tag_suffix=tag_suffix
)
# TODO: Checks
self.operator = operator

def name(self) -> str:
"""Returns the name of the observable."""
return f"expectation_{self.index}"
@property
def _base_tag(self) -> str:
return "expectation"

def apply(self, *, state: State, **kwargs: Any) -> Any:
"""Calculates the observable to store in the Results."""
Expand All @@ -187,21 +192,27 @@ class CorrelationMatrix(Observable):
one_state: The eigenstate to measure the population of in the
correlation matrix. Can be left undefined if the state's
eigenstates form a known eigenbasis with a defined "one state".
tag_suffix: An optional suffix to append to the tag. Needed if
multiple instances of the same observable are given to the
same EmulationConfig.
"""

def __init__(
self,
*,
evaluation_times: Sequence[float] | None = None,
one_state: Eigenstate | None = None,
tag_suffix: str | None = None,
):
"""Initializes the observable."""
super().__init__(evaluation_times=evaluation_times)
super().__init__(
evaluation_times=evaluation_times, tag_suffix=tag_suffix
)
# TODO: Checks
self.one_state = one_state

def name(self) -> str:
"""Returns the name of the observable."""
@property
def _base_tag(self) -> str:
return "correlation_matrix"

@staticmethod
Expand Down Expand Up @@ -257,22 +268,28 @@ class Occupation(Observable):
one_state: The eigenstate to measure the population of. Can be left
undefined if the state's eigenstates form a known eigenbasis with
a defined "one state".
tag_suffix: An optional suffix to append to the tag. Needed if
multiple instances of the same observable are given to the
same EmulationConfig.
"""

def __init__(
self,
*,
evaluation_times: Sequence[float] | None = None,
one_state: Eigenstate | None = None,
tag_suffix: str | None = None,
):
"""Initializes the observable."""
super().__init__(evaluation_times=evaluation_times)
super().__init__(
evaluation_times=evaluation_times, tag_suffix=tag_suffix
)
# TODO: Checks
self.one_state = one_state

def name(self) -> str:
"""Returns the name of the observable."""
return "occupation" + f"_{self.one_state or ''}"
@property
def _base_tag(self) -> str:
return "occupation"

def apply(
self, *, state: State, hamiltonian: Operator, **kwargs: Any
Expand All @@ -297,10 +314,13 @@ class Energy(Observable):
evaluation_times: The relative times at which to compute the energy.
If left as `None`, uses the `default_evaluation_times` of the
backend's `EmulationConfig`.
tag_suffix: An optional suffix to append to the tag. Needed if
multiple instances of the same observable are given to the
same EmulationConfig.
"""

def name(self) -> str:
"""Returns the name of the observable."""
@property
def _base_tag(self) -> str:
return "energy"

def apply(
Expand All @@ -317,10 +337,13 @@ class EnergyVariance(Observable):
evaluation_times: The relative times at which to compute the variance.
If left as `None`, uses the `default_evaluation_times` of the
backend's `EmulationConfig`.
tag_suffix: An optional suffix to append to the tag. Needed if
multiple instances of the same observable are given to the
same EmulationConfig.
"""

def name(self) -> str:
"""Returns the name of the observable."""
@property
def _base_tag(self) -> str:
return "energy_variance"

def apply(
Expand All @@ -343,10 +366,13 @@ class SecondMomentOfEnergy(Observable):
evaluation_times: The relative times at which to compute the variance.
If left as `None`, uses the `default_evaluation_times` of the
backend's `EmulationConfig`.
tag_suffix: An optional suffix to append to the tag. Needed if
multiple instances of the same observable are given to the
same EmulationConfig.
"""

def name(self) -> str:
"""Returns the name of the observable."""
@property
def _base_tag(self) -> str:
return "second_moment_of_energy"

def apply(
Expand Down
61 changes: 44 additions & 17 deletions pulser-core/pulser/backend/observable.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"""Defines the abstract base class for a callback and an observable."""
from __future__ import annotations

import uuid
from abc import ABC, abstractmethod
from collections.abc import Sequence
from typing import TYPE_CHECKING, Any
Expand All @@ -29,6 +30,15 @@
class Callback(ABC):
"""A general Callback that is called during the emulation."""

def __init__(self) -> None:
"""Initializes a Callback."""
self._uuid: uuid.UUID = uuid.uuid4()

@property
def uuid(self) -> uuid.UUID:
"""A universal unique identifier for this instance."""
return self._uuid

@abstractmethod
def __call__(
self,
Expand Down Expand Up @@ -63,11 +73,40 @@ class Observable(Callback):
evaluation_times: The times at which to add a result to Results.
If left as `None`, uses the `default_evaluation_times` of the
backend's `EmulationConfig`.
tag_suffix: An optional suffix to append to the tag. Needed if
multiple instances of the same observable are given to the
same EmulationConfig.
"""

def __init__(self, *, evaluation_times: Sequence[float] | None = None):
def __init__(
self,
*,
evaluation_times: Sequence[float] | None = None,
tag_suffix: str | None = None,
):
"""Initializes the observable."""
super().__init__()
self.evaluation_times = evaluation_times
self._tag_suffix = tag_suffix

@property
@abstractmethod
def _base_tag(self) -> str:
pass

@property
def tag(self) -> str:
"""Label for the observable, used to index the Results object.
Within a Results instance, all computed observables must have different
tags.
Returns:
The tag of the observable.
"""
if self._tag_suffix is None:
return self._base_tag
return f"{self._base_tag}_{self._tag_suffix}"

def __call__(
self,
Expand Down Expand Up @@ -106,22 +145,7 @@ def __call__(
value_to_store = self.apply(
config=config, state=state, hamiltonian=hamiltonian
)
result._store(
observable_name=self.name(), time=t, value=value_to_store
)

@abstractmethod
def name(self) -> str:
"""Name of the observable, normally used to index the Results object.
Some Observables might have multiple instances, such as an observable
to compute a fidelity on some given state. In that case, this method
could make sure each instance has a unique name.
Returns:
The name of the observable.
"""
pass
result._store(observable=self, time=t, value=value_to_store)

@abstractmethod
def apply(
Expand All @@ -142,3 +166,6 @@ def apply(
The result to put in Results.
"""
pass

def __repr__(self) -> str:
return f"{self.tag}:{self.uuid}"
Loading

0 comments on commit 28c9c57

Please sign in to comment.