# 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
"""Tools for dealing with simulated 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, cast, overload
from cocotb import simulator
from cocotb._py_compat import Literal, TypeAlias
from cocotb._typing import RoundMode, TimeUnit
__all__ = (
"convert",
"get_sim_time",
"time_precision",
)
Steps: TypeAlias = Literal["step"]
TimeUnitWithoutSteps: TypeAlias = Literal["fs", "ps", "ns", "us", "ms", "sec"]
@overload
def convert(
value: Union[float, Fraction, Decimal],
unit: TimeUnit,
*,
to: Steps,
round_mode: RoundMode = "error",
) -> int: ...
@overload
def convert(
value: Union[float, Fraction, Decimal],
unit: TimeUnit,
*,
to: TimeUnitWithoutSteps,
round_mode: RoundMode = "error",
) -> float: ...
[docs]
def convert(
value: Union[float, Decimal, Fraction],
unit: TimeUnit,
*,
to: TimeUnit,
round_mode: RoundMode = "error",
) -> float:
"""Convert time values from one unit to another unit.
Args:
value: The time value.
unit: The unit of *value* (one of ``'step'``, ``'fs'``, ``'ps'``, ``'ns'``, ``'us'``, ``'ms'``, ``'sec'``).
to: The unit to convert *value* to (one of ``'step'``, ``'fs'``, ``'ps'``, ``'ns'``, ``'us'``, ``'ms'``, ``'sec'``).
round_mode:
How to handle non-integral step values (one of ``'error'``, ``'round'``, ``'ceil'``, ``'floor'``).
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.
Returns:
The value scaled by the difference in units.
.. versionadded:: 2.0
"""
if unit == "step":
steps = cast("int", value)
else:
steps = _get_sim_steps(value, unit, round_mode=round_mode)
if to == "step":
return steps
else:
return _get_time_from_sim_steps(steps, to)
@overload
def get_sim_time(unit: Steps = "step", *, units: None = None) -> int: ...
@overload
def get_sim_time(unit: TimeUnitWithoutSteps, *, units: None = None) -> float: ...
[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".
.. versionchanged:: 2.0
Moved from :mod:`cocotb.utils` to :mod:`cocotb.simtime`.
"""
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)
def _get_time_from_sim_steps(
steps: int,
unit: TimeUnit,
) -> float:
if unit == "step":
return steps
return _ldexp10(steps, time_precision - _get_log_time_scale(unit))
def _get_sim_steps(
time: Union[float, Fraction, Decimal],
unit: TimeUnit = "step",
*,
round_mode: RoundMode = "error",
) -> int:
result: Union[float, Fraction, Decimal]
if unit != "step":
result = _ldexp10(time, _get_log_time_scale(unit) - time_precision)
else:
result = time
if round_mode == "error":
result_rounded = floor(result)
if result_rounded != result:
raise ValueError(
f"Unable to accurately represent {time}({unit}) with the simulator precision of 1e{time_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
@lru_cache(maxsize=None)
def _get_log_time_scale(unit: TimeUnitWithoutSteps) -> int:
"""Retrieve 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]
time_precision: int = _get_log_time_scale("fs")
"""The precision of time in the current simulation.
The value is seconds in powers of tens,
i.e. ``-15`` is ``10**-15`` seconds or 1 femtosecond.
.. versionadded:: 2.0
"""
def _init() -> None:
global time_precision
time_precision = simulator.get_precision()