|
| 1 | +- Start Date: 2024-07-29 |
| 2 | +- RFC PR: [amaranth-lang/rfcs#66](https://github.com/amaranth-lang/rfcs/pull/66) |
| 3 | +- Amaranth Issue: [amaranth-lang/amaranth#0000](https://github.com/amaranth-lang/amaranth/issues/1469) |
| 4 | + |
| 5 | +# Simulation time |
| 6 | + |
| 7 | +## Summary |
| 8 | +[summary]: #summary |
| 9 | + |
| 10 | +Add a type for representing time durations (including clock periods) and simulator methods to query the elapsed time. |
| 11 | + |
| 12 | +## Motivation |
| 13 | +[motivation]: #motivation |
| 14 | + |
| 15 | +In [RFC #36](0036-async-testbench-functions.md), there were a plan to introduce a `.time()` method to query the current simulation time, but it got deferred due to the lack of a suitable return type. |
| 16 | + |
| 17 | +Internally simulation time is an integer number of femtoseconds, but exposing that directly is not ergonomic and making it a `float` of seconds sacrifices precision the longer a simulation runs. |
| 18 | + |
| 19 | +Also, to quote @whitequark: Time is not a number. |
| 20 | + |
| 21 | +## Guide- and reference-level explanation |
| 22 | +[guide-level-explanation]: #guide-level-explanation |
| 23 | + |
| 24 | +A new type `Period` is introduced. |
| 25 | +It is exported from `amaranth.hdl` and also re-exported from `amaranth.sim`. |
| 26 | +It is immutable and has a constructor accepting at most one named argument, giving the following valid forms of construction: |
| 27 | +- `Period()` |
| 28 | + - Constructs a zero period. |
| 29 | +- `Period(s: numbers.Real)` |
| 30 | +- `Period(ms: numbers.Real)` |
| 31 | +- `Period(us: numbers.Real)` |
| 32 | +- `Period(ns: numbers.Real)` |
| 33 | +- `Period(ps: numbers.Real)` |
| 34 | +- `Period(fs: numbers.Real)` |
| 35 | + - The argument will be scaled according to its SI prefix and used to calculate the closest integer femtosecond representation. |
| 36 | +- `Period(Hz: numbers.Real)` |
| 37 | +- `Period(kHz: numbers.Real)` |
| 38 | +- `Period(MHz: numbers.Real)` |
| 39 | +- `Period(GHz: numbers.Real)` |
| 40 | + - The argument will be scaled according to its SI prefix and its reciprocal used to calculate the closest integer femtosecond representation. |
| 41 | + - A value of zero will raise `ZeroDivisionError`. |
| 42 | + - A negative value will raise `ValueError`. |
| 43 | + |
| 44 | +To convert it back to a number, the following properties are available: |
| 45 | +- `.seconds -> float` |
| 46 | +- `.milliseconds -> float` |
| 47 | +- `.microseconds -> float` |
| 48 | +- `.nanoseconds -> float` |
| 49 | +- `.picoseconds -> float` |
| 50 | +- `.femtoseconds -> int` |
| 51 | + |
| 52 | +To calculate the reciprocal frequency, the following properties are available: |
| 53 | +- `.hertz -> float` |
| 54 | +- `.kilohertz -> float` |
| 55 | +- `.megahertz -> float` |
| 56 | +- `.gigahertz -> float` |
| 57 | + - Accessing these properties when the period is zero will raise `ZeroDivisionError`. |
| 58 | + - Accessing these properties when the period is negative will raise `ValueError`. |
| 59 | + |
| 60 | +The following operators are defined: |
| 61 | +- `.__lt__(other: Period) -> bool` |
| 62 | +- `.__le__(other: Period) -> bool` |
| 63 | +- `.__eq__(other: Period) -> bool` |
| 64 | +- `.__ne__(other: Period) -> bool` |
| 65 | +- `.__gt__(other: Period) -> bool` |
| 66 | +- `.__ge__(other: Period) -> bool` |
| 67 | +- `.__hash__() -> int` |
| 68 | +- `.__bool__() -> bool` |
| 69 | +- `.__neg__() -> Period` |
| 70 | +- `.__pos__() -> Period` |
| 71 | +- `.__abs__() -> Period` |
| 72 | +- `.__add__(other: Period) -> Period` |
| 73 | +- `.__sub__(other: Period) -> Period` |
| 74 | +- `.__mul__(other: numbers.Real) -> Period` |
| 75 | +- `.__rmul__(other: numbers.Real) -> Period` |
| 76 | +- `.__truediv__(other: numbers.Real) -> Period` |
| 77 | +- `.__truediv__(other: Period) -> float` |
| 78 | +- `.__floordiv__(other: Period) -> int` |
| 79 | +- `.__mod__(other: Period) -> Period` |
| 80 | + - Operators on `Period`s are performed on the underlying femtosecond values. |
| 81 | + - Operators involving `numbers.Real` operands have the result rounded back to the closest integer femtosecond representation. |
| 82 | + - Operators given unsupported operand combinations will return `NotImplemented`. |
| 83 | +- `.__str__() -> str` |
| 84 | + - Equivalent to `.__format__("")` |
| 85 | +- `.__format__(format_spec: str) -> str` |
| 86 | + - The format specifier format is `[width][.precision][ ][unit]`. |
| 87 | + - An invalid format specifier raises `ValueError`. |
| 88 | + - If `width` is specified, the string is left-padded with space to at least the requested width. |
| 89 | + - If `precision` is specified, the requested number of decimal digits will be emitted. |
| 90 | + Otherwise, duration units will emit as many digits required for an exact result, while frequency units will defer to default `float` formatting. |
| 91 | + - If a space is present in the format specifier, the formatted string will have a space between the number and the unit. |
| 92 | + - `unit` can be specified as any of the argument names accepted by the constructor. |
| 93 | + If a unit is not specified, the largest duration unit that has a nonzero integer part is used. |
| 94 | + Formatting frequency units have the same restrictions and exception behavior as accessing frequency properties. |
| 95 | + |
| 96 | +`SimulatorContext` have an `.elapsed_time() -> Period` method added that returns the elapsed time since start of simulation. |
| 97 | + |
| 98 | +These methods that has a `period` argument currently taking seconds as a `float` are updated to take a `Period`: |
| 99 | +- `Simulator.add_clock()` |
| 100 | +- `SimulatorContext.delay()` |
| 101 | + |
| 102 | +These methods that has a `frequency` argument currently taking Hz as a `float` are updated to take a `Period`: |
| 103 | +- `ResourceManager.add_clock_constraint()` |
| 104 | +- `Clock.__init__()` |
| 105 | + |
| 106 | +The ability to pass seconds or Hz directly as a `float` is deprecated and removed in a future Amaranth version. |
| 107 | + |
| 108 | +The `Clock.period` property is updated to return a `Period` and the `Clock.frequency` property is deprecated. |
| 109 | + |
| 110 | +Consequently, `Platform.default_clk_frequency()` is also deprecated and replaced with a new method `Platform.default_clk_period() -> Period`. |
| 111 | + |
| 112 | + |
| 113 | +## Drawbacks |
| 114 | +[drawbacks]: #drawbacks |
| 115 | + |
| 116 | +- Deprecating being able to pass seconds or Hz directly as a `float` creates churn. |
| 117 | + |
| 118 | +## Rationale and alternatives |
| 119 | +[rationale-and-alternatives]: #rationale-and-alternatives |
| 120 | + |
| 121 | +- `.add_clock()` is usually passed the reciprocal of a frequency, so being able to construct a `Period` from a frequency instead of a duration is useful. |
| 122 | + - We could add a separate `Frequency` type, but in practice it would likely almost exclusively be used to calculate the reciprocal `Period`. |
| 123 | + |
| 124 | +- Accepting a `numbers.Real` when we're converting from a number lets us pass in any standard suitable number type. |
| 125 | + |
| 126 | +- The supported set of operators are the ones that have meaningful and useful semantics: |
| 127 | + - Comparing, adding and subtracting time periods makes sense. |
| 128 | + - Multiplying a time period with or dividing it by a real number scales the time period. |
| 129 | + - Dividing time periods gives the ratio between them. |
| 130 | + - Modulo of time periods is the remainder and thus still a time period. |
| 131 | + Potentially useful to calculate the phase offset between a timestamp and a clock period. |
| 132 | + - Reflected operators that only accept `Period` operands are redundant and omitted. |
| 133 | + |
| 134 | +- `.elapsed_time()` is only available from within a testbench/process, where it is well defined. |
| 135 | + |
| 136 | +- Instead of named constructor arguments, we could use a classmethod for each SI prefix. |
| 137 | + This was proposed in an earlier revision of this RFC. |
| 138 | + |
| 139 | +- Instead of returning `float`, we could return `fractions.Fraction` to ensure no precision loss when the number of femtoseconds is larger than the `float` significand. |
| 140 | + This was proposed in an earlier revision of this RFC. |
| 141 | + - Rounding to integer femtoseconds is already lossy and a further precision loss from converting to a `float` is negligible in most real world use cases. |
| 142 | + For any applications requiring an exact conversion of a `Period`, the `.femtoseconds` property is always exact. |
| 143 | + |
| 144 | +## Prior art |
| 145 | +[prior-art]: #prior-art |
| 146 | + |
| 147 | +None. |
| 148 | + |
| 149 | +## Unresolved questions |
| 150 | +[unresolved-questions]: #unresolved-questions |
| 151 | + |
| 152 | +None. |
| 153 | + |
| 154 | +## Future possibilities |
| 155 | +[future-possibilities]: #future-possibilities |
| 156 | + |
| 157 | +None. |
0 commit comments