Source code for mimiqcircuits.backends._rng_utils

#
# Copyright © 2023-2026 QPerfect. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

"""Seed / RNG helpers shared by every backend wrapper.

Two helpers live here:

- :func:`normalize_seed` reconciles a caller's ``seed=`` and
  ``rngs=`` arguments into a single ``int`` (or ``None``) suitable
  for forwarding to a downstream simulator that accepts only one
  seed value.
- :func:`derive_grid_seeds` deterministically produces ``n``
  distinct seeds from one base seed, used by parameter-grid loops
  so each grid point gets an independent RNG stream.

The XOR-fold of an :class:`RNGs` bundle into one int is a
compromise: the underlying simulator PRNGs (Rust ``Rng``, Julia
``MersenneTwister``) take a single seed, so the four logical
streams collapse to one PRNG until per-stream seeding is plumbed
end-to-end.
"""

from __future__ import annotations

from typing import Optional, Union

from mimiqcircuits.backends.backend import RNGs


_MASK64 = (1 << 64) - 1
_MASK63 = (1 << 63) - 1


[docs] def normalize_seed( seed: Optional[int], rngs: Union[RNGs, int, None] ) -> Optional[int]: """Reconcile the legacy ``seed=`` and the new ``rngs=`` kwargs. Either may be provided, never both. ``rngs`` may be an ``int`` (passed through), an :class:`RNGs` bundle (xor-folded to one seed), or ``None`` (returns the ``seed`` argument). """ if seed is not None and rngs is not None: raise TypeError("pass either seed= or rngs=, not both") if rngs is None: return seed if isinstance(rngs, int): return rngs if isinstance(rngs, RNGs): return ( rngs.shot.getrandbits(63) ^ rngs.noise.getrandbits(63) ^ rngs.trajectory.getrandbits(63) ^ rngs.pass_.getrandbits(63) ) raise TypeError(f"rngs must be RNGs, int, or None; got {type(rngs).__name__}")
def _splitmix64(x: int) -> int: """SplitMix64 finaliser. Consecutive ``x`` values produce decorrelated 63-bit outputs, which a plain LCG mix cannot guarantee — `(base*c1 + i*c2)` collapses to a constant at ``base_seed == 0`` and gives every grid point the same stream. """ x = (x + 0x9E3779B97F4A7C15) & _MASK64 x = ((x ^ (x >> 30)) * 0xBF58476D1CE4E5B9) & _MASK64 x = ((x ^ (x >> 27)) * 0x94D049BB133111EB) & _MASK64 return (x ^ (x >> 31)) & _MASK63
[docs] def derive_grid_seeds( base_seed: Optional[int], n: int ) -> list[Optional[int]]: """Deterministically derive ``n`` distinct seeds from ``base_seed``. The ``param_grid`` loop calls this so each parameter point gets a different RNG stream; reusing ``base_seed`` directly would tie measurement outcomes across grid points. A ``None`` ``base_seed`` propagates as ``[None] * n`` (caller wants nondeterminism). """ if base_seed is None: return [None] * n return [_splitmix64(base_seed + i) for i in range(n)]