Source code for cocotb.utils

# Copyright cocotb contributors
# Copyright (c) 2013 Potential Ventures Ltd
# Copyright (c) 2013 SolarFlare Communications Inc
# Licensed under the Revised BSD License, see LICENSE for details.
# SPDX-License-Identifier: BSD-3-Clause

"""Utility functions for dealing with simulation time."""

import warnings
from decimal import Decimal
from fractions import Fraction
from functools import lru_cache
from math import ceil, floor
from typing import Union, overload

from cocotb import simulator
from cocotb._py_compat import Literal, TypeAlias
from cocotb._typing import RoundMode, TimeUnit

__all__ = (
    "get_sim_steps",
    "get_sim_time",
    "get_time_from_sim_steps",
)


def _get_simulator_precision() -> int:
    # cache and replace this function
    precision = simulator.get_precision()
    global _get_simulator_precision
    _get_simulator_precision = precision.__int__
    return _get_simulator_precision()


# Simulator helper functions
[docs] def get_sim_time(unit: TimeUnit = "step", *, units: None = None) -> float: """Retrieve the simulation time from the simulator. Args: unit: String specifying the unit of the result (one of ``'step'``, ``'fs'``, ``'ps'``, ``'ns'``, ``'us'``, ``'ms'``, ``'sec'``). ``'step'`` will return the raw simulation time. .. versionchanged:: 2.0 Passing ``None`` as the *unit* argument was removed, use ``'step'`` instead. .. versionchanged:: 2.0 Renamed from ``units``. Raises: ValueError: If *unit* is not a valid unit. Returns: The simulation time in the specified unit. .. versionchanged:: 1.6 Support ``'step'`` as the the *unit* argument to mean "simulator time step". """ if units is not None: warnings.warn( "The 'units' argument has been renamed to 'unit'.", DeprecationWarning, stacklevel=2, ) unit = units timeh, timel = simulator.get_sim_time() steps = timeh << 32 | timel return get_time_from_sim_steps(steps, unit) if unit != "step" else steps
@overload def _ldexp10(frac: float, exp: int) -> float: ... @overload def _ldexp10(frac: Fraction, exp: int) -> Fraction: ... @overload def _ldexp10(frac: Decimal, exp: int) -> Decimal: ... def _ldexp10( frac: Union[float, Fraction, Decimal], exp: int ) -> Union[float, Fraction, Decimal]: """Like :func:`math.ldexp`, but base 10.""" # using * or / separately prevents rounding errors if `frac` is a # high-precision type if exp > 0: return frac * (10**exp) else: return frac / (10**-exp)
[docs] def get_time_from_sim_steps( steps: int, unit: Union[TimeUnit, None] = None, *, units: None = None, ) -> float: """Calculate simulation time in the specified *unit* from the *steps* based on the simulator precision. Args: steps: Number of simulation steps. unit: String specifying the unit of the result (one of ``'fs'``, ``'ps'``, ``'ns'``, ``'us'``, ``'ms'``, ``'sec'``). .. versionchanged:: 2.0 Renamed from ``units``. Raises: ValueError: If *unit* is not a valid unit. Returns: The simulation time in the specified unit. """ if units is not None: warnings.warn( "The 'units' argument has been renamed to 'unit'.", DeprecationWarning, stacklevel=2, ) unit = units if unit is None: raise TypeError("Missing required argument 'unit'") if unit == "step": return steps return _ldexp10(steps, _get_simulator_precision() - _get_log_time_scale(unit))
[docs] def get_sim_steps( time: Union[float, Fraction, Decimal], unit: TimeUnit = "step", *, round_mode: RoundMode = "error", units: None = None, ) -> int: """Calculates the number of simulation time steps for a given amount of *time*. When *round_mode* is ``"error"``, a :exc:`ValueError` is thrown if the value cannot be accurately represented in terms of simulator time steps. When *round_mode* is ``"round"``, ``"ceil"``, or ``"floor"``, the corresponding rounding function from the standard library will be used to round to a simulator time step. Args: time: The value to convert to simulation time steps. unit: String specifying the unit of the result (one of ``'step'``, ``'fs'``, ``'ps'``, ``'ns'``, ``'us'``, ``'ms'``, ``'sec'``). ``'step'`` means *time* is already in simulation time steps. .. versionchanged:: 2.0 Renamed from ``units``. round_mode: String specifying how to handle time values that sit between time steps (one of ``'error'``, ``'round'``, ``'ceil'``, ``'floor'``). Returns: The number of simulation time steps. Raises: ValueError: if the value cannot be represented accurately in terms of simulator time steps when *round_mode* is ``"error"``. .. versionchanged:: 1.5 Support ``'step'`` as the *unit* argument to mean "simulator time step". .. versionchanged:: 1.6 Support rounding modes. """ if units is not None: warnings.warn( "The 'units' argument has been renamed to 'unit'.", DeprecationWarning, stacklevel=2, ) unit = units result: Union[float, Fraction, Decimal] if unit != "step": result = _ldexp10(time, _get_log_time_scale(unit) - _get_simulator_precision()) else: result = time if round_mode == "error": result_rounded = floor(result) if result_rounded != result: precision = _get_simulator_precision() raise ValueError( f"Unable to accurately represent {time}({unit}) with the simulator precision of 1e{precision}" ) elif round_mode == "ceil": result_rounded = ceil(result) elif round_mode == "round": result_rounded = round(result) elif round_mode == "floor": result_rounded = floor(result) else: raise ValueError(f"Invalid round_mode specifier: {round_mode}") return result_rounded
TimeUnitWithoutSteps: TypeAlias = Literal["fs", "ps", "ns", "us", "ms", "sec"] @lru_cache(maxsize=None) def _get_log_time_scale(unit: TimeUnitWithoutSteps) -> int: """Retrieves the ``log10()`` of the scale factor for a given time unit. Args: unit: String specifying the unit (one of ``'fs'``, ``'ps'``, ``'ns'``, ``'us'``, ``'ms'``, ``'sec'``). .. versionchanged:: 2.0 Renamed from ``units``. Raises: ValueError: If *unit* is not a valid unit. Returns: The ``log10()`` of the scale factor for the time unit. """ scale = {"fs": -15, "ps": -12, "ns": -9, "us": -6, "ms": -3, "sec": 0} unit_lwr = unit.lower() if unit_lwr not in scale: raise ValueError(f"Invalid unit ({unit}) provided") else: return scale[unit_lwr]