Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/deploy-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
- name: Install documentation dependencies
run: uv sync --locked --group docs --no-dev
- name: Build site
run: uv run mkdocs build
run: uv run mkdocs build --strict
- uses: actions/upload-pages-artifact@v3
with:
path: site
Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ hide:

---

Narrative guides and API reference will land here as the library stabilizes.
Start with the [API reference](reference/utils.md) for standalone utilities.

- :material-github:{ .lg .middle } __Source__

Expand Down
16 changes: 16 additions & 0 deletions docs/javascript/mathjax.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
window.MathJax = {
tex: {
inlineMath: [["\\(", "\\)"]],
displayMath: [["\\[", "\\]"]],
processEscapes: true,
processEnvironments: true,
},
options: {
ignoreHtmlClass: ".*|",
processHtmlClass: "arithmatex",
},
};

document$.subscribe(() => {
MathJax.typesetPromise();
});
5 changes: 5 additions & 0 deletions docs/reference/combined_hamiltonian_matrix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Combined Hamiltonian matrix

Build a full-system Hamiltonian matrix from local and global terms.

::: shadowsim.core.combined_hamiltonian_matrix.combined_hamiltonian_matrix
23 changes: 23 additions & 0 deletions docs/reference/utils.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Utils

Standalone matrix and helper utilities.

::: shadowsim.utils.hermitian.hermitian

::: shadowsim.utils.unitary.unitary

::: shadowsim.utils.positive_definite.positive_definite

::: shadowsim.utils.positive_semidefinite.positive_semidefinite

::: shadowsim.utils.negative_definite.negative_definite

::: shadowsim.utils.negative_semidefinite.negative_semidefinite

::: shadowsim.utils.indefinite.indefinite

::: shadowsim.utils.tensor.tensor

::: shadowsim.utils.flip_dict.flip_dict

::: shadowsim.utils.next_power_of_two.next_power_of_two
30 changes: 30 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,16 @@ theme:

markdown_extensions:
- pymdownx.superfences
- pymdownx.highlight:
anchor_linenums: true
- pymdownx.inlinehilite
- pymdownx.snippets
- admonition
- pymdownx.details
- attr_list
- md_in_html
- pymdownx.arithmatex:
generic: true
- pymdownx.emoji:
emoji_index: !!python/name:material.extensions.emoji.twemoji
emoji_generator: !!python/name:material.extensions.emoji.to_svg
Expand All @@ -54,10 +58,36 @@ markdown_extensions:

nav:
- Home: index.md
- API Reference:
- Utils: reference/utils.md
- Combined Hamiltonian matrix: reference/combined_hamiltonian_matrix.md

plugins:
- meta
- search
- markdown-exec
- mkdocstrings:
handlers:
python:
paths:
- .
options:
show_root_heading: true
show_root_full_path: false
show_source: true
show_symbol_type_heading: true
show_symbol_type_toc: true
heading_level: 2
signature_crossrefs: true
separate_signature: true
docstring_style: google
docstring_section_style: list
show_signature_annotations: true
members_order: source

extra_javascript:
- javascript/mathjax.js
- https://unpkg.com/mathjax@3/es5/tex-mml-chtml.js

extra:
social:
Expand Down
7 changes: 7 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ dev = [
docs = [
"mkdocs>=1.6",
"mkdocs-material>=9.5",
"mkdocstrings[python]>=0.24",
"markdown-exec[ansi]>=1.7",
# Needed so mkdocstrings can format signatures during the docs build.
"ruff==0.16.0",
]
lint = [
"ruff==0.16.0",
Expand Down Expand Up @@ -101,6 +105,9 @@ ignore = ["D407", "D203", "D213", "D416", "PLR0912", "PLR0911", "PLR0915", "PLR2
# E203 -- Whitespace before ':'. This rule conflicts with Black's formatting style.
# It was removing definitions with the same name creating conflicts in test_is_stochastic.py

[tool.ruff.lint.pydocstyle]
convention = "google"

[tool.ruff.lint.pylint]
# Simulator constructors take many positional model parameters (default limit is 5).
max-positional-args = 15
Expand Down
32 changes: 27 additions & 5 deletions shadowsim/core/combined_hamiltonian_matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,40 @@ def combined_hamiltonian_matrix(
hamiltonians: list[Hamiltonian],
num_qubits: int,
) -> np.ndarray:
"""Sum Hamiltonian terms on the full ``num_qubits``-site tensor space.
r"""Sum Hamiltonian terms on the full ``num_qubits``-site tensor space.

``LocalHamiltonian`` terms are embedded with identities on the remaining
sites; full-domain ``Hamiltonian`` matrices are added as-is. All terms must
match a common Hilbert-space dimension (``local_dim ** num_qubits`` for
locals, or the matrix size of bare terms).

Parameter hamiltonians: The list of Hamiltonians to sum.
Precondition: hamiltonians is a list of Hamiltonian objects.
Args:
hamiltonians: Non-empty list of ``Hamiltonian`` objects to sum.
num_qubits: Positive number of sites in the full system.

Returns:
Complex matrix of shape ``(d, d)`` equal to the sum of the (embedded)
terms, where ``d`` is the shared Hilbert-space dimension.

Raises:
ValueError: If ``LocalHamiltonian`` terms use mixed ``local_dim`` values,
or if term dimensions disagree for the requested ``num_qubits``.
AssertionError: If ``hamiltonians`` / ``num_qubits`` fail basic type and
positivity checks.

Examples:
Embed a single-site \(Z\) into a three-qubit chain and inspect the shape:

```python exec="1" source="above" result="text"
import numpy as np
from shadowsim.core import LocalHamiltonian
from shadowsim.core.combined_hamiltonian_matrix import combined_hamiltonian_matrix

local = LocalHamiltonian(np.diag([1.0, -1.0]), sites=[1], local_dim=2)
out = combined_hamiltonian_matrix([local], num_qubits=3)
print(out.shape)
```

Parameter num_qubits: The number of qubits in the full system.
Precondition: num_qubits is a positive integer.
"""
assert isinstance(hamiltonians, list), "hamiltonians must be a list"
assert all(isinstance(h, Hamiltonian) for h in hamiltonians), "all hamiltonians must be Hamiltonian objects"
Expand Down
24 changes: 22 additions & 2 deletions shadowsim/utils/flip_dict.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,29 @@
"""Helpers for reversing dictionary keys (bitstring endianness)."""

from typing import Any

def flip_dict(d):

def flip_dict(d: dict[str, Any]) -> dict[str, Any]:
"""Return a new dictionary with each string key reversed.

This is used to switch the endianness of measurement results.
Measurement outcomes are often labeled by bitstrings whose endianness
differs between frameworks; reversing the keys converts between those
conventions.

Args:
d: Mapping whose keys are strings (typically measurement bitstrings).

Returns:
A new dictionary with the same values and each key reversed.

Examples:
Flip Qiskit-style bitstring keys:

```python exec="1" source="above" result="text"
from shadowsim.utils.flip_dict import flip_dict

print(flip_dict({"01": 3, "10": 5}))
```

"""
return {k[::-1]: v for k, v in d.items()}
35 changes: 33 additions & 2 deletions shadowsim/utils/hermitian.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,37 @@
import numpy as np


def hermitian(H):
"""Return whether the NumPy matrix ``H`` is Hermitian."""
def hermitian(H: np.ndarray) -> bool:
r"""Return whether the NumPy matrix ``H`` is Hermitian.

A square matrix \(H\) is Hermitian when it equals its conjugate transpose,
\(H = H^\dagger\).

Args:
H: Square complex matrix.

Returns:
``True`` if ``H`` is Hermitian up to numerical tolerance, else ``False``.

Examples:
The Pauli \(Y\) matrix is Hermitian:

```python exec="1" source="above" result="text"
import numpy as np
from shadowsim.utils.hermitian import hermitian

Y = np.array([[0.0, -1j], [1j, 0.0]])
print(hermitian(Y))
```

A non-symmetric complex matrix is not:

```python exec="1" source="above" result="text"
import numpy as np
from shadowsim.utils.hermitian import hermitian

print(hermitian(np.array([[1.0 + 2j, 0.0], [0.0, 0.0]])))
```

"""
return np.allclose(H, H.conjugate().T)
27 changes: 25 additions & 2 deletions shadowsim/utils/indefinite.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,30 @@
from shadowsim.utils._real_parts_of_eigenvalues import _real_parts_of_eigenvalues


def indefinite(matrix):
"""Return whether a matrix has both positive and negative eigenvalues."""
def indefinite(matrix: np.ndarray) -> bool:
r"""Return whether a matrix is indefinite.

A matrix is indefinite when it has at least one eigenvalue with positive real
part and at least one with negative real part.

Args:
matrix: Square matrix to check.

Returns:
``True`` if the matrix has both positive and negative eigenvalue real
parts, else ``False``.

Examples:
Pauli \(Z\) is indefinite:

```python exec="1" source="above" result="text"
import numpy as np
from shadowsim.utils.indefinite import indefinite

Z = np.array([[1.0, 0.0], [0.0, -1.0]])
print(indefinite(Z))
```

"""
eigvals = _real_parts_of_eigenvalues(matrix)
return np.any(eigvals > 0) and np.any(eigvals < 0)
25 changes: 23 additions & 2 deletions shadowsim/utils/negative_definite.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,27 @@
from shadowsim.utils._real_parts_of_eigenvalues import _real_parts_of_eigenvalues


def negative_definite(matrix):
"""Return whether a matrix is negative definite."""
def negative_definite(matrix: np.ndarray) -> bool:
r"""Return whether a matrix is negative definite.

A matrix is negative definite when every eigenvalue has strictly negative
real part.

Args:
matrix: Square matrix to check.

Returns:
``True`` if all real parts of the eigenvalues are negative, else ``False``.

Examples:
A negative multiple of the identity is negative definite:

```python exec="1" source="above" result="text"
import numpy as np
from shadowsim.utils.negative_definite import negative_definite

print(negative_definite(-np.eye(2)))
```

"""
return np.all(_real_parts_of_eigenvalues(matrix) < 0)
26 changes: 24 additions & 2 deletions shadowsim/utils/negative_semidefinite.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,28 @@
from shadowsim.utils._real_parts_of_eigenvalues import _real_parts_of_eigenvalues


def negative_semidefinite(matrix):
"""Return whether a matrix is negative semidefinite."""
def negative_semidefinite(matrix: np.ndarray) -> bool:
r"""Return whether a matrix is negative semidefinite.

A matrix is negative semidefinite when every eigenvalue has non-positive real
part.

Args:
matrix: Square matrix to check.

Returns:
``True`` if all real parts of the eigenvalues are non-positive, else ``False``.

Examples:
A diagonal matrix with a zero and a negative entry is negative
semidefinite:

```python exec="1" source="above" result="text"
import numpy as np
from shadowsim.utils.negative_semidefinite import negative_semidefinite

print(negative_semidefinite(np.diag([0.0, -1.0])))
```

"""
return np.all(_real_parts_of_eigenvalues(matrix) <= 0)
24 changes: 22 additions & 2 deletions shadowsim/utils/next_power_of_two.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,30 @@
"""Helpers for rounding up to a power of two."""


def next_power_of_two(n):
def next_power_of_two(n: int) -> int:
"""Return the first power of two greater than or equal to ``n``.

``next_power_of_two(5)`` returns 8 and ``next_power_of_two(8)`` returns 8.
Useful when allocating padded Hilbert-space dimensions or buffer sizes that
must be a power of two.

Args:
n: Non-negative integer.

Returns:
The smallest power of two that is at least ``n``. For ``n <= 1`` the
result is ``1``.

Raises:
AssertionError: If ``n`` is not a non-negative integer.

Examples:
```python exec="1" source="above" result="text"
from shadowsim.utils.next_power_of_two import next_power_of_two

print(next_power_of_two(5))
print(next_power_of_two(8))
```

"""
assert isinstance(n, int), "n must be an integer"
assert n >= 0, "n must be non-negative"
Expand Down
Loading
Loading