Skip to content

Commit fcbb0d3

Browse files
zypwhitequark
authored andcommitted
hdl.time: Implement Period type for representing time periods.
1 parent f2fdc41 commit fcbb0d3

File tree

5 files changed

+400
-4
lines changed

5 files changed

+400
-4
lines changed

amaranth/hdl/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from ._ir import Instance, IOBufferInstance
1111
from ._mem import MemoryData, MemoryInstance
1212
from ._nir import CombinationalCycle
13+
from ._time import Period
1314
from ._xfrm import DomainRenamer, ResetInserter, EnableInserter
1415

1516

@@ -32,6 +33,8 @@
3233
"CombinationalCycle",
3334
# _mem
3435
"MemoryData", "MemoryInstance",
36+
# _time
37+
"Period",
3538
# _xfrm
3639
"DomainRenamer", "ResetInserter", "EnableInserter",
3740
]

amaranth/hdl/_time.py

+252
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
import numbers
2+
import re
3+
4+
5+
__all__ = ["Period"]
6+
7+
8+
_TIME_UNITS = {
9+
"s": 1_000_000_000_000_000,
10+
"ms": 1_000_000_000_000,
11+
"us": 1_000_000_000,
12+
"ns": 1_000_000,
13+
"ps": 1_000,
14+
"fs": 1,
15+
}
16+
17+
18+
_FREQUENCY_UNITS = {
19+
"Hz": 1_000_000_000_000_000,
20+
"kHz": 1_000_000_000_000,
21+
"MHz": 1_000_000_000,
22+
"GHz": 1_000_000,
23+
}
24+
25+
26+
class Period:
27+
def __init__(self, **kwargs):
28+
if not kwargs:
29+
self._femtoseconds = 0
30+
return
31+
32+
if len(kwargs) > 1:
33+
raise TypeError("Period accepts at most one argument")
34+
35+
(unit, value), = kwargs.items()
36+
37+
if not isinstance(value, numbers.Real):
38+
raise TypeError(f"{unit} value must be a real number")
39+
40+
if unit in _TIME_UNITS:
41+
self._femtoseconds = round(value * _TIME_UNITS[unit])
42+
43+
elif unit in _FREQUENCY_UNITS:
44+
if value == 0:
45+
raise ZeroDivisionError("Frequency can't be zero")
46+
elif value < 0:
47+
raise ValueError("Frequency can't be negative")
48+
49+
self._femtoseconds = round(_FREQUENCY_UNITS[unit] / value)
50+
51+
else:
52+
raise TypeError(f"{unit} is not a valid unit")
53+
54+
@property
55+
def seconds(self):
56+
return self._femtoseconds / 1_000_000_000_000_000
57+
58+
@property
59+
def milliseconds(self):
60+
return self._femtoseconds / 1_000_000_000_000
61+
62+
@property
63+
def microseconds(self):
64+
return self._femtoseconds / 1_000_000_000
65+
66+
@property
67+
def nanoseconds(self):
68+
return self._femtoseconds / 1_000_000
69+
70+
@property
71+
def picoseconds(self):
72+
return self._femtoseconds / 1_000
73+
74+
@property
75+
def femtoseconds(self):
76+
return self._femtoseconds
77+
78+
def _check_reciprocal(self):
79+
if self._femtoseconds == 0:
80+
raise ZeroDivisionError("Can't calculate the frequency of a zero period")
81+
elif self._femtoseconds < 0:
82+
raise ValueError("Can't calculate the frequency of a negative period")
83+
84+
@property
85+
def hertz(self):
86+
self._check_reciprocal()
87+
return 1_000_000_000_000_000 / self._femtoseconds
88+
89+
@property
90+
def kilohertz(self):
91+
self._check_reciprocal()
92+
return 1_000_000_000_000 / self._femtoseconds
93+
94+
@property
95+
def megahertz(self):
96+
self._check_reciprocal()
97+
return 1_000_000_000 / self._femtoseconds
98+
99+
@property
100+
def gigahertz(self):
101+
self._check_reciprocal()
102+
return 1_000_000 / self._femtoseconds
103+
104+
def __lt__(self, other):
105+
if not isinstance(other, Period):
106+
return NotImplemented
107+
return self._femtoseconds < other._femtoseconds
108+
109+
def __le__(self, other):
110+
if not isinstance(other, Period):
111+
return NotImplemented
112+
return self._femtoseconds <= other._femtoseconds
113+
114+
def __eq__(self, other):
115+
if not isinstance(other, Period):
116+
return NotImplemented
117+
return self._femtoseconds == other._femtoseconds
118+
119+
def __ne__(self, other):
120+
if not isinstance(other, Period):
121+
return NotImplemented
122+
return self._femtoseconds != other._femtoseconds
123+
124+
def __gt__(self, other):
125+
if not isinstance(other, Period):
126+
return NotImplemented
127+
return self._femtoseconds > other._femtoseconds
128+
129+
def __ge__(self, other):
130+
if not isinstance(other, Period):
131+
return NotImplemented
132+
return self._femtoseconds >= other._femtoseconds
133+
134+
def __hash__(self):
135+
return hash(self._femtoseconds)
136+
137+
def __bool__(self):
138+
return bool(self._femtoseconds)
139+
140+
def __neg__(self):
141+
return Period(fs=-self._femtoseconds)
142+
143+
def __pos__(self):
144+
return self
145+
146+
def __abs__(self):
147+
return Period(fs=abs(self._femtoseconds))
148+
149+
def __add__(self, other):
150+
if not isinstance(other, Period):
151+
return NotImplemented
152+
return Period(fs=self._femtoseconds + other._femtoseconds)
153+
154+
def __sub__(self, other):
155+
if not isinstance(other, Period):
156+
return NotImplemented
157+
return Period(fs=self._femtoseconds - other._femtoseconds)
158+
159+
def __mul__(self, other):
160+
if not isinstance(other, numbers.Real):
161+
return NotImplemented
162+
return Period(fs=self._femtoseconds * other)
163+
164+
__rmul__ = __mul__
165+
166+
def __truediv__(self, other):
167+
if isinstance(other, Period):
168+
return self._femtoseconds / other._femtoseconds
169+
elif isinstance(other, numbers.Real):
170+
return Period(fs=self._femtoseconds / other)
171+
else:
172+
return NotImplemented
173+
174+
def __floordiv__(self, other):
175+
if not isinstance(other, Period):
176+
return NotImplemented
177+
return self._femtoseconds // other._femtoseconds
178+
179+
def __mod__(self, other):
180+
if not isinstance(other, Period):
181+
return NotImplemented
182+
return Period(fs=self._femtoseconds % other._femtoseconds)
183+
184+
def __str__(self):
185+
return self.__format__("")
186+
187+
def __format__(self, format_spec):
188+
m = re.match(r"^([1-9]\d*)?(\.\d+)?( ?)(([munpf]?)s|([kMG]?)Hz)?$", format_spec)
189+
190+
if m is None:
191+
raise ValueError(f"Invalid format specifier '{format_spec}' for object of type 'Period'")
192+
193+
width, precision, space, unit, s_unit, hz_unit = m.groups()
194+
195+
if unit is None:
196+
if abs(self._femtoseconds) >= 1_000_000_000_000_000:
197+
s_unit = ""
198+
elif abs(self._femtoseconds) >= 1_000_000_000_000:
199+
s_unit = "m"
200+
elif abs(self._femtoseconds) >= 1_000_000_000:
201+
s_unit = "u"
202+
elif abs(self._femtoseconds) >= 1_000_000:
203+
s_unit = "n"
204+
elif abs(self._femtoseconds) >= 1_000:
205+
s_unit = "p"
206+
else:
207+
s_unit = "f"
208+
209+
unit = f"{s_unit}s"
210+
211+
if s_unit is not None:
212+
div, digits = {
213+
"": (1_000_000_000_000_000, 15),
214+
"m": (1_000_000_000_000, 12),
215+
"u": (1_000_000_000, 9),
216+
"n": (1_000_000, 6),
217+
"p": (1_000, 3),
218+
"f": (1, 0),
219+
}[s_unit]
220+
integer, decimal = divmod(self._femtoseconds, div)
221+
222+
if precision:
223+
precision = int(precision[1:])
224+
decimal = round(decimal * 10**(precision - digits))
225+
digits = precision
226+
227+
value = f"{integer}.{decimal:0{digits}}"
228+
229+
if not precision:
230+
value = value.rstrip('0')
231+
value = value.rstrip('.')
232+
233+
else:
234+
if hz_unit == "":
235+
value = f"{self.hertz:{precision or ''}f}"
236+
elif hz_unit == "k":
237+
value = f"{self.kilohertz:{precision or ''}f}"
238+
elif hz_unit == "M":
239+
value = f"{self.megahertz:{precision or ''}f}"
240+
elif hz_unit == "G":
241+
value = f"{self.gigahertz:{precision or ''}f}"
242+
243+
str = f"{value}{space}{unit}"
244+
if width:
245+
str = f"{str:>{width}}"
246+
247+
return str
248+
249+
def __repr__(self):
250+
for unit, div in _TIME_UNITS.items():
251+
if self._femtoseconds % div == 0:
252+
return f"Period({unit}={self._femtoseconds // div})"

amaranth/sim/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
from .core import Simulator
22
from ._async import DomainReset, BrokenTrigger, SimulatorContext, TickTrigger, TriggerCombination
33
from ._pycoro import Settle, Delay, Tick, Passive, Active
4+
from ..hdl import Period
45

56

67
__all__ = [
78
"DomainReset", "BrokenTrigger",
89
"SimulatorContext", "Simulator", "TickTrigger", "TriggerCombination",
10+
"Period",
911
# deprecated
1012
"Settle", "Delay", "Tick", "Passive", "Active",
1113
]

amaranth/sim/core.py

-4
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,6 @@
1717
]
1818

1919

20-
def _seconds_to_femtos(delay: float):
21-
return int(delay * 1e15) # seconds to femtoseconds
22-
23-
2420
class Simulator:
2521
# Simulator engines aren't yet a part of the public API.
2622
"""Simulator(toplevel)

0 commit comments

Comments
 (0)