wavekit 0.7.1__tar.gz → 0.7.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {wavekit-0.7.1 → wavekit-0.7.2}/PKG-INFO +5 -5
- {wavekit-0.7.1 → wavekit-0.7.2}/README.md +3 -4
- {wavekit-0.7.1 → wavekit-0.7.2}/pyproject.toml +2 -1
- wavekit-0.7.2/src/wavekit/expression.py +165 -0
- {wavekit-0.7.1 → wavekit-0.7.2}/src/wavekit/readers/base.py +64 -64
- {wavekit-0.7.1 → wavekit-0.7.2}/src/wavekit/waveform.py +63 -79
- {wavekit-0.7.1 → wavekit-0.7.2}/LICENSE +0 -0
- {wavekit-0.7.1 → wavekit-0.7.2}/build.py +0 -0
- {wavekit-0.7.1 → wavekit-0.7.2}/src/wavekit/__init__.py +0 -0
- {wavekit-0.7.1 → wavekit-0.7.2}/src/wavekit/pattern/__init__.py +0 -0
- {wavekit-0.7.1 → wavekit-0.7.2}/src/wavekit/pattern/compiler.py +0 -0
- {wavekit-0.7.1 → wavekit-0.7.2}/src/wavekit/pattern/dsl.py +0 -0
- {wavekit-0.7.1 → wavekit-0.7.2}/src/wavekit/pattern/errors.py +0 -0
- {wavekit-0.7.1 → wavekit-0.7.2}/src/wavekit/pattern/result.py +0 -0
- {wavekit-0.7.1 → wavekit-0.7.2}/src/wavekit/pattern/runtime.py +0 -0
- {wavekit-0.7.1 → wavekit-0.7.2}/src/wavekit/pattern/steps.py +0 -0
- {wavekit-0.7.1 → wavekit-0.7.2}/src/wavekit/readers/expr_parser.py +0 -0
- {wavekit-0.7.1 → wavekit-0.7.2}/src/wavekit/readers/fsdb/npi_fsdb.pxd +0 -0
- {wavekit-0.7.1 → wavekit-0.7.2}/src/wavekit/readers/fsdb/npi_fsdb_reader.pyx +0 -0
- {wavekit-0.7.1 → wavekit-0.7.2}/src/wavekit/readers/fsdb/reader.py +0 -0
- {wavekit-0.7.1 → wavekit-0.7.2}/src/wavekit/readers/fst/reader.py +0 -0
- {wavekit-0.7.1 → wavekit-0.7.2}/src/wavekit/readers/hierarchy.py +0 -0
- {wavekit-0.7.1 → wavekit-0.7.2}/src/wavekit/readers/matcher.py +0 -0
- {wavekit-0.7.1 → wavekit-0.7.2}/src/wavekit/readers/range.py +0 -0
- {wavekit-0.7.1 → wavekit-0.7.2}/src/wavekit/readers/value_change.pyx +0 -0
- {wavekit-0.7.1 → wavekit-0.7.2}/src/wavekit/readers/vcd/reader.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wavekit
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.2
|
|
4
4
|
Summary: a fundamental package for digital circuit waveform analysis
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Author: cxzzzz
|
|
@@ -13,6 +13,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.13
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Requires-Dist: lark (>=1.2,<2)
|
|
16
17
|
Requires-Dist: numpy (>=2.0.0,<3.0.0)
|
|
17
18
|
Requires-Dist: pylibfst (>=0.2.1,<0.3.0)
|
|
18
19
|
Requires-Dist: pytest (>=8.2.2,<9.0.0)
|
|
@@ -294,7 +295,7 @@ Some tips for programmable patterns:
|
|
|
294
295
|
|
|
295
296
|
| Method | Description |
|
|
296
297
|
|--------|-------------|
|
|
297
|
-
| `VcdReader(file)` / `FstReader(file)` / `FsdbReader(file)` | Open a waveform file. Use as a context manager. `FsdbReader` requires Verdi runtime (`WAVEKIT_NPI_LIB`, `VERDI_HOME`, or `LD_LIBRARY_PATH`). |
|
|
298
|
+
| `VcdReader(file)` / `FstReader(file)` / `FsdbReader(file, *, quiet=True)` | Open a waveform file. Use as a context manager. `FsdbReader` requires Verdi runtime (`WAVEKIT_NPI_LIB`, `VERDI_HOME`, or `LD_LIBRARY_PATH`) and suppresses the NPI startup banner by default; pass `quiet=False` to keep it. |
|
|
298
299
|
| `reader.load_waveform(signal, clock, ...)` | Load one signal sampled on every clock edge. Returns `Waveform`. |
|
|
299
300
|
| `reader.load_unknown_mask(signal, clock, ...)` | **Experimental.** Load X/Z bit presence as an unsigned mask `Waveform`. |
|
|
300
301
|
| `reader.load_matched_waveforms(signal_path, clock_path, ...)` | Batch-load matching signals. Returns `dict[CaptureKey, Waveform]`. |
|
|
@@ -319,9 +320,8 @@ Some tips for programmable patterns:
|
|
|
319
320
|
| `$ModName` | `tb.$fifo_unit.ptr` | Match a direct-child scope by module/definition name (FSDB only) | `ExactCapture(path=..., definition=...)` |
|
|
320
321
|
| `$$ModName` | `tb.$$fifo_unit.ptr` | Match any-depth descendant scope by module/definition name (FSDB only) | `ExactCapture(path=..., definition=...)` |
|
|
321
322
|
|
|
322
|
-
`$` and `$$` are path-step modifiers
|
|
323
|
-
|
|
324
|
-
For example: `tb.$/fifo_(in|out)/.data_{0..3}[7:0]`.
|
|
323
|
+
`$` and `$$` are path-step modifiers that can combine with exact-name, brace,
|
|
324
|
+
and regex matchers. For example: `tb.$/fifo_(in|out)/.data_{0..3}[7:0]`.
|
|
325
325
|
|
|
326
326
|
Matched-reader and hierarchy query APIs return dictionaries keyed by
|
|
327
327
|
`CaptureKey = tuple[Capture, ...]`. Exact-name components are omitted, so a
|
|
@@ -272,7 +272,7 @@ Some tips for programmable patterns:
|
|
|
272
272
|
|
|
273
273
|
| Method | Description |
|
|
274
274
|
|--------|-------------|
|
|
275
|
-
| `VcdReader(file)` / `FstReader(file)` / `FsdbReader(file)` | Open a waveform file. Use as a context manager. `FsdbReader` requires Verdi runtime (`WAVEKIT_NPI_LIB`, `VERDI_HOME`, or `LD_LIBRARY_PATH`). |
|
|
275
|
+
| `VcdReader(file)` / `FstReader(file)` / `FsdbReader(file, *, quiet=True)` | Open a waveform file. Use as a context manager. `FsdbReader` requires Verdi runtime (`WAVEKIT_NPI_LIB`, `VERDI_HOME`, or `LD_LIBRARY_PATH`) and suppresses the NPI startup banner by default; pass `quiet=False` to keep it. |
|
|
276
276
|
| `reader.load_waveform(signal, clock, ...)` | Load one signal sampled on every clock edge. Returns `Waveform`. |
|
|
277
277
|
| `reader.load_unknown_mask(signal, clock, ...)` | **Experimental.** Load X/Z bit presence as an unsigned mask `Waveform`. |
|
|
278
278
|
| `reader.load_matched_waveforms(signal_path, clock_path, ...)` | Batch-load matching signals. Returns `dict[CaptureKey, Waveform]`. |
|
|
@@ -297,9 +297,8 @@ Some tips for programmable patterns:
|
|
|
297
297
|
| `$ModName` | `tb.$fifo_unit.ptr` | Match a direct-child scope by module/definition name (FSDB only) | `ExactCapture(path=..., definition=...)` |
|
|
298
298
|
| `$$ModName` | `tb.$$fifo_unit.ptr` | Match any-depth descendant scope by module/definition name (FSDB only) | `ExactCapture(path=..., definition=...)` |
|
|
299
299
|
|
|
300
|
-
`$` and `$$` are path-step modifiers
|
|
301
|
-
|
|
302
|
-
For example: `tb.$/fifo_(in|out)/.data_{0..3}[7:0]`.
|
|
300
|
+
`$` and `$$` are path-step modifiers that can combine with exact-name, brace,
|
|
301
|
+
and regex matchers. For example: `tb.$/fifo_(in|out)/.data_{0..3}[7:0]`.
|
|
303
302
|
|
|
304
303
|
Matched-reader and hierarchy query APIs return dictionaries keyed by
|
|
305
304
|
`CaptureKey = tuple[Capture, ...]`. Exact-name components are omitted, so a
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "wavekit"
|
|
3
|
-
version = "0.7.
|
|
3
|
+
version = "0.7.2"
|
|
4
4
|
description = "a fundamental package for digital circuit waveform analysis"
|
|
5
5
|
authors = ["cxzzzz <cxz19961010@outlook.com>"]
|
|
6
6
|
readme = "README.md"
|
|
@@ -16,6 +16,7 @@ numpy = "^2.0.0"
|
|
|
16
16
|
vcdvcd = "^2.3.5"
|
|
17
17
|
pylibfst = "^0.2.1"
|
|
18
18
|
pytest = "^8.2.2"
|
|
19
|
+
lark = ">=1.2,<2"
|
|
19
20
|
|
|
20
21
|
[tool.poetry.group.dev.dependencies]
|
|
21
22
|
ruff = "^0.6.2"
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Parsing and evaluating WaveKit waveform expressions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from lark import Lark, Transformer
|
|
9
|
+
|
|
10
|
+
_EXPRESSION_GRAMMAR = r"""
|
|
11
|
+
?start: expr
|
|
12
|
+
?expr: bit_or
|
|
13
|
+
?bit_or: bit_xor | bit_or VBAR bit_xor
|
|
14
|
+
?bit_xor: bit_and | bit_xor CIRCUMFLEX bit_and
|
|
15
|
+
?bit_and: comparison | bit_and AMPERSAND comparison
|
|
16
|
+
?comparison: shift
|
|
17
|
+
| comparison EQEQ shift
|
|
18
|
+
| comparison NEQ shift
|
|
19
|
+
| comparison LE shift
|
|
20
|
+
| comparison LT shift
|
|
21
|
+
| comparison GE shift
|
|
22
|
+
| comparison GT shift
|
|
23
|
+
?shift: sum | shift LSHIFT sum | shift RSHIFT sum
|
|
24
|
+
?sum: product | sum PLUS product | sum MINUS product
|
|
25
|
+
?product: unary
|
|
26
|
+
| product STAR unary
|
|
27
|
+
| product SLASH unary
|
|
28
|
+
| product FLOOR unary
|
|
29
|
+
| product PERCENT unary
|
|
30
|
+
| product POWER unary
|
|
31
|
+
?unary: TILDE unary | PLUS unary | MINUS unary | atom
|
|
32
|
+
?atom: call | path | NUMBER | group
|
|
33
|
+
group: LPAR expr RPAR
|
|
34
|
+
call: NAME LPAR [expr (COMMA expr)*] RPAR
|
|
35
|
+
path: segment (DOT segment)* SUBSCRIPT*
|
|
36
|
+
segment: segment_text | WILDCARD
|
|
37
|
+
segment_text: segment_piece+
|
|
38
|
+
?segment_piece: NAME | MODULE | BRACE | REGEX
|
|
39
|
+
|
|
40
|
+
MODULE: /\${1,2}/
|
|
41
|
+
BRACE: /\{[^{}]*\}/
|
|
42
|
+
WILDCARD.3: "**" | "*"
|
|
43
|
+
REGEX: /\/(?:\\.|[^\/\\])*\//
|
|
44
|
+
SUBSCRIPT: /\[\d+(?::\d+)?\]/
|
|
45
|
+
NAME: /[A-Za-z_][A-Za-z0-9_]*/
|
|
46
|
+
HEX_NUMBER.2: /0[xX][0-9a-fA-F](?:_?[0-9a-fA-F])*/
|
|
47
|
+
BIN_NUMBER.2: /0[bB][01](?:_?[01])*/
|
|
48
|
+
OCT_NUMBER.2: /0[oO][0-7](?:_?[0-7])*/
|
|
49
|
+
DECIMAL_FLOAT_NUMBER.2: /(?:\d(?:_?\d)*)?\.(?:\d(?:_?\d)*)?(?:[eE][+-]?\d(?:_?\d)*)?/
|
|
50
|
+
EXP_NUMBER.2: /\d(?:_?\d)*[eE][+-]?\d(?:_?\d)*/
|
|
51
|
+
DEC_NUMBER.1: /\d(?:_?\d)*/
|
|
52
|
+
NUMBER: HEX_NUMBER | BIN_NUMBER | OCT_NUMBER | DECIMAL_FLOAT_NUMBER | EXP_NUMBER | DEC_NUMBER
|
|
53
|
+
|
|
54
|
+
VBAR: "|"
|
|
55
|
+
CIRCUMFLEX: "^"
|
|
56
|
+
AMPERSAND: "&"
|
|
57
|
+
EQEQ: "=="
|
|
58
|
+
NEQ: "!="
|
|
59
|
+
LE: "<="
|
|
60
|
+
LT: "<"
|
|
61
|
+
GE: ">="
|
|
62
|
+
GT: ">"
|
|
63
|
+
LSHIFT: "<<"
|
|
64
|
+
RSHIFT: ">>"
|
|
65
|
+
PLUS: "+"
|
|
66
|
+
MINUS: "-"
|
|
67
|
+
STAR: "*"
|
|
68
|
+
SLASH: "/"
|
|
69
|
+
FLOOR: "//"
|
|
70
|
+
PERCENT: "%"
|
|
71
|
+
POWER: "**"
|
|
72
|
+
TILDE: "~"
|
|
73
|
+
LPAR: "("
|
|
74
|
+
RPAR: ")"
|
|
75
|
+
COMMA: ","
|
|
76
|
+
DOT: "."
|
|
77
|
+
|
|
78
|
+
%import common.WS_INLINE
|
|
79
|
+
%ignore WS_INLINE
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
_EXPRESSION_PARSER = Lark(
|
|
83
|
+
_EXPRESSION_GRAMMAR,
|
|
84
|
+
parser='lalr',
|
|
85
|
+
lexer='contextual',
|
|
86
|
+
)
|
|
87
|
+
_EXPRESSION_FUNCTIONS: dict[str, Callable[..., Any]] = {}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def expression_function(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
91
|
+
"""Register a Waveform method as callable from an expression string."""
|
|
92
|
+
_EXPRESSION_FUNCTIONS[func.__name__] = func
|
|
93
|
+
return func
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class ExpressionTransformer(Transformer):
|
|
97
|
+
def __init__(self, *, function_names: set[str]):
|
|
98
|
+
super().__init__()
|
|
99
|
+
self.function_names = function_names
|
|
100
|
+
self.path_entries: list[tuple[str, str]] = []
|
|
101
|
+
|
|
102
|
+
@staticmethod
|
|
103
|
+
def join(items: list[Any]) -> str:
|
|
104
|
+
return ''.join(str(item) for item in items)
|
|
105
|
+
|
|
106
|
+
def call(self, items: list[Any]) -> str:
|
|
107
|
+
name = str(items[0])
|
|
108
|
+
if name not in self.function_names:
|
|
109
|
+
raise ValueError(f'unknown expression function {name!r}')
|
|
110
|
+
return self.join(items)
|
|
111
|
+
|
|
112
|
+
def path(self, items: list[Any]) -> str:
|
|
113
|
+
path = self.join(items)
|
|
114
|
+
placeholder = f'__wave_{len(self.path_entries)}__'
|
|
115
|
+
self.path_entries.append((placeholder, path))
|
|
116
|
+
return placeholder
|
|
117
|
+
|
|
118
|
+
def __default__(self, data: str, children: list[Any], meta: Any) -> Any:
|
|
119
|
+
return self.join(children)
|
|
120
|
+
|
|
121
|
+
def __default_token__(self, token: Any) -> str:
|
|
122
|
+
return str(token)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def parse_expression(expr: str) -> tuple[str, list[tuple[str, str]]]:
|
|
126
|
+
"""Parse an expression and replace each signal path with a placeholder.
|
|
127
|
+
|
|
128
|
+
Parameters
|
|
129
|
+
----------
|
|
130
|
+
expr:
|
|
131
|
+
WaveKit expression containing physical signal paths.
|
|
132
|
+
|
|
133
|
+
Returns
|
|
134
|
+
-------
|
|
135
|
+
tuple[str, list[tuple[str, str]]]
|
|
136
|
+
The substituted expression and ordered ``(placeholder, path)`` entries.
|
|
137
|
+
"""
|
|
138
|
+
tree = _EXPRESSION_PARSER.parse(expr)
|
|
139
|
+
transformer = ExpressionTransformer(
|
|
140
|
+
function_names=set(_EXPRESSION_FUNCTIONS),
|
|
141
|
+
)
|
|
142
|
+
result = transformer.transform(tree)
|
|
143
|
+
return str(result), transformer.path_entries
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def evaluate_expression(expr: str, namespace: dict[str, Any]) -> Any:
|
|
147
|
+
"""Evaluate an already-parsed expression against preloaded Waveforms.
|
|
148
|
+
|
|
149
|
+
Parameters
|
|
150
|
+
----------
|
|
151
|
+
expr:
|
|
152
|
+
The substituted expression returned by :func:`parse_expression`.
|
|
153
|
+
Physical paths must be resolved and replaced before calling this
|
|
154
|
+
function.
|
|
155
|
+
namespace:
|
|
156
|
+
Names available to the expression, normally Waveform objects.
|
|
157
|
+
|
|
158
|
+
Returns
|
|
159
|
+
-------
|
|
160
|
+
Any
|
|
161
|
+
The result produced by the Waveform expression.
|
|
162
|
+
"""
|
|
163
|
+
evaluation_namespace = {**_EXPRESSION_FUNCTIONS, **namespace}
|
|
164
|
+
code = compile(expr, '<wavekit-expression>', 'eval')
|
|
165
|
+
return eval(code, {'__builtins__': {}}, evaluation_namespace) # noqa: S307
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import ast
|
|
4
3
|
from abc import abstractmethod
|
|
5
4
|
from dataclasses import dataclass, field
|
|
6
5
|
from typing import Any, Generic, Literal, TypeVar, cast
|
|
7
6
|
|
|
8
7
|
import numpy as np
|
|
9
8
|
|
|
9
|
+
from ..expression import evaluate_expression, parse_expression
|
|
10
10
|
from ..waveform import Waveform
|
|
11
|
-
from .expr_parser import extract_wave_paths
|
|
12
11
|
from .hierarchy import Node, Scope, Signal
|
|
13
12
|
from .matcher import CaptureKey
|
|
14
13
|
from .value_change import value_change_to_value_array
|
|
@@ -684,35 +683,33 @@ class Reader(Generic[SignalT]):
|
|
|
684
683
|
mode: Literal['single', 'zip'] = 'single',
|
|
685
684
|
root_scope: Scope | None = None,
|
|
686
685
|
) -> Waveform | dict[CaptureKey, Waveform]:
|
|
687
|
-
"""Evaluate a waveform expression
|
|
688
|
-
|
|
689
|
-
Wave signal paths embedded in *expr* are automatically extracted,
|
|
690
|
-
loaded, and substituted before evaluating the resulting Python
|
|
691
|
-
arithmetic expression.
|
|
686
|
+
"""Evaluate a waveform expression containing physical signal paths.
|
|
692
687
|
|
|
693
688
|
Parameters
|
|
694
689
|
----------
|
|
695
690
|
expr:
|
|
696
|
-
Expression string.
|
|
697
|
-
|
|
698
|
-
Zip mode example: ``"tb.dut.fifo_{0..3}.w_ptr[3:0] - tb.dut.fifo_{0..3}.r_ptr[3:0]"``.
|
|
691
|
+
Expression string. Signal paths may be used as operands or as
|
|
692
|
+
arguments to registered expression functions.
|
|
699
693
|
clock:
|
|
700
694
|
Clock signal used for all waveform loads.
|
|
695
|
+
xz_value, signed, sample_on_posedge, begin_time, end_time, begin_cycle,
|
|
696
|
+
end_cycle:
|
|
697
|
+
Forwarded to :meth:`load_matched_waveforms` for every path.
|
|
701
698
|
mode:
|
|
702
|
-
``'single'``
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
same set of pattern keys; single-match paths are broadcast;
|
|
706
|
-
returns ``dict[CaptureKey, Waveform]``.
|
|
699
|
+
``'single'`` requires every path to match one signal. ``'zip'``
|
|
700
|
+
evaluates once per shared multi-match key and broadcasts singleton
|
|
701
|
+
paths.
|
|
707
702
|
root_scope:
|
|
708
|
-
If provided,
|
|
709
|
-
|
|
703
|
+
If provided, resolve paths within this scope.
|
|
704
|
+
|
|
705
|
+
Returns
|
|
706
|
+
-------
|
|
707
|
+
Waveform or dict[CaptureKey, Waveform]
|
|
708
|
+
The evaluated waveform, or one waveform per zip key.
|
|
710
709
|
"""
|
|
711
710
|
self._validate_xz_value(xz_value)
|
|
712
|
-
substituted, path_entries =
|
|
713
|
-
|
|
711
|
+
substituted, path_entries = parse_expression(expr)
|
|
714
712
|
load_kwargs: dict[str, Any] = dict(
|
|
715
|
-
clock=clock,
|
|
716
713
|
xz_value=xz_value,
|
|
717
714
|
signed=signed,
|
|
718
715
|
sample_on_posedge=sample_on_posedge,
|
|
@@ -720,81 +717,84 @@ class Reader(Generic[SignalT]):
|
|
|
720
717
|
end_time=end_time,
|
|
721
718
|
begin_cycle=begin_cycle,
|
|
722
719
|
end_cycle=end_cycle,
|
|
720
|
+
root_scope=root_scope,
|
|
723
721
|
)
|
|
724
722
|
|
|
725
|
-
|
|
726
|
-
matched_per_path: list[tuple[str, str, dict[CaptureKey, SignalT]]] = []
|
|
723
|
+
loaded_per_path: list[tuple[str, str, dict[CaptureKey, Waveform]]] = []
|
|
727
724
|
for placeholder, path in path_entries:
|
|
728
|
-
matched = self.
|
|
725
|
+
matched = self.load_matched_waveforms(
|
|
726
|
+
signal_path=path,
|
|
727
|
+
clock_path=clock,
|
|
728
|
+
**load_kwargs,
|
|
729
|
+
)
|
|
729
730
|
if not matched:
|
|
730
731
|
raise ValueError(f"path '{path}' matched no signals")
|
|
731
|
-
|
|
732
|
+
loaded_per_path.append((placeholder, path, matched))
|
|
732
733
|
|
|
733
734
|
if mode == 'single':
|
|
734
|
-
for _placeholder, path, matched in
|
|
735
|
+
for _placeholder, path, matched in loaded_per_path:
|
|
735
736
|
if len(matched) > 1:
|
|
737
|
+
matched_names = [
|
|
738
|
+
wave.signal.full_name if wave.signal is not None else None
|
|
739
|
+
for wave in matched.values()
|
|
740
|
+
]
|
|
736
741
|
raise ValueError(
|
|
737
742
|
f"path '{path}' matched {len(matched)} signals in mode='single',"
|
|
738
|
-
f" use mode='zip'. Matched:
|
|
743
|
+
f" use mode='zip'. Matched: "
|
|
744
|
+
f'{matched_names}'
|
|
739
745
|
)
|
|
740
|
-
|
|
741
|
-
placeholder:
|
|
742
|
-
for placeholder, _, matched in
|
|
746
|
+
namespace = {
|
|
747
|
+
placeholder: next(iter(matched.values()))
|
|
748
|
+
for placeholder, _, matched in loaded_per_path
|
|
743
749
|
}
|
|
744
750
|
try:
|
|
745
|
-
|
|
746
|
-
return eval(code, {'__builtins__': {}}, ns) # noqa: S307
|
|
751
|
+
return evaluate_expression(substituted, namespace)
|
|
747
752
|
except Exception as exc:
|
|
748
753
|
raise ValueError(
|
|
749
|
-
f"failed to evaluate expression '{expr}' (substituted: '{substituted}')"
|
|
754
|
+
f"failed to evaluate expression '{expr}' " f"(substituted: '{substituted}')"
|
|
750
755
|
) from exc
|
|
751
756
|
|
|
752
|
-
|
|
753
|
-
multi_paths = [
|
|
754
|
-
|
|
757
|
+
if mode == 'zip':
|
|
758
|
+
multi_paths = [
|
|
759
|
+
(placeholder, path, matched)
|
|
760
|
+
for placeholder, path, matched in loaded_per_path
|
|
761
|
+
if len(matched) > 1
|
|
762
|
+
]
|
|
763
|
+
single_paths = [
|
|
764
|
+
(placeholder, path, matched)
|
|
765
|
+
for placeholder, path, matched in loaded_per_path
|
|
766
|
+
if len(matched) == 1
|
|
767
|
+
]
|
|
755
768
|
|
|
756
769
|
if multi_paths:
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
if set(matched.keys()) != ref_keys:
|
|
770
|
+
_ref_placeholder, ref_path, ref_matched = multi_paths[0]
|
|
771
|
+
ref_keys = set(ref_matched)
|
|
772
|
+
for _placeholder, path, matched in multi_paths[1:]:
|
|
773
|
+
if set(matched) != ref_keys:
|
|
762
774
|
raise ValueError(
|
|
763
|
-
|
|
764
|
-
f"'{
|
|
765
|
-
f"'{
|
|
775
|
+
'inconsistent match keys between paths: '
|
|
776
|
+
f"'{ref_path}' has keys {ref_keys!r}, "
|
|
777
|
+
f"'{path}' has keys {set(matched)!r}"
|
|
766
778
|
)
|
|
767
|
-
zip_keys
|
|
779
|
+
zip_keys = list(ref_keys)
|
|
768
780
|
else:
|
|
769
|
-
# All paths are single-match; degenerate to single behaviour
|
|
770
781
|
zip_keys = [()]
|
|
771
782
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
placeholder: self.load_waveform(next(iter(matched.values())), **load_kwargs)
|
|
783
|
+
broadcast_namespace = {
|
|
784
|
+
placeholder: next(iter(matched.values()))
|
|
775
785
|
for placeholder, _, matched in single_paths
|
|
776
786
|
}
|
|
777
|
-
|
|
778
787
|
result: dict[CaptureKey, Waveform] = {}
|
|
779
|
-
try:
|
|
780
|
-
code = compile(ast.parse(substituted, mode='eval'), '<eval_expr>', 'eval')
|
|
781
|
-
except SyntaxError as exc:
|
|
782
|
-
raise ValueError(
|
|
783
|
-
f"invalid expression '{expr}' (substituted: '{substituted}')"
|
|
784
|
-
) from exc
|
|
785
|
-
|
|
786
788
|
for key in zip_keys:
|
|
787
|
-
|
|
788
|
-
for placeholder,
|
|
789
|
-
|
|
789
|
+
namespace = dict(broadcast_namespace)
|
|
790
|
+
for placeholder, _path, matched in multi_paths:
|
|
791
|
+
namespace[placeholder] = matched[key]
|
|
790
792
|
try:
|
|
791
|
-
result[key] =
|
|
793
|
+
result[key] = evaluate_expression(substituted, namespace)
|
|
792
794
|
except Exception as exc:
|
|
793
795
|
raise ValueError(
|
|
794
|
-
f
|
|
796
|
+
f'failed to evaluate expression {expr!r} for key {key!r}'
|
|
795
797
|
) from exc
|
|
796
|
-
|
|
797
798
|
return result
|
|
798
799
|
|
|
799
|
-
|
|
800
|
-
raise ValueError(f"unknown mode '{mode}', expected 'single' or 'zip'")
|
|
800
|
+
raise ValueError(f"unknown mode '{mode}', expected 'single' or 'zip'")
|
|
@@ -5,6 +5,7 @@ from typing import Any, Callable, Literal, Union, cast
|
|
|
5
5
|
import numpy as np
|
|
6
6
|
import numpy.typing as npt
|
|
7
7
|
|
|
8
|
+
from .expression import expression_function
|
|
8
9
|
from .readers.hierarchy import Signal
|
|
9
10
|
|
|
10
11
|
|
|
@@ -276,6 +277,7 @@ class Waveform:
|
|
|
276
277
|
value.astype(np.uint64) - offset,
|
|
277
278
|
)
|
|
278
279
|
|
|
280
|
+
@expression_function
|
|
279
281
|
def as_signed(self) -> Waveform:
|
|
280
282
|
"""Reinterpret the unsigned bit pattern as a two's-complement signed integer.
|
|
281
283
|
|
|
@@ -293,6 +295,7 @@ class Waveform:
|
|
|
293
295
|
def _unsigned(value: np.ndarray, width: int):
|
|
294
296
|
return value & ((1 << width) - 1)
|
|
295
297
|
|
|
298
|
+
@expression_function
|
|
296
299
|
def as_unsigned(self) -> Waveform:
|
|
297
300
|
"""Reinterpret the signed value as an unsigned bit pattern.
|
|
298
301
|
|
|
@@ -308,10 +311,6 @@ class Waveform:
|
|
|
308
311
|
lambda x: self._unsigned(x, width), width=self.width, signed=False
|
|
309
312
|
)
|
|
310
313
|
|
|
311
|
-
@staticmethod
|
|
312
|
-
def _add(a, b):
|
|
313
|
-
return a + b
|
|
314
|
-
|
|
315
314
|
def _check_sign(self, other: WaveformOrScalar):
|
|
316
315
|
if isinstance(other, Waveform) and self.signed != other.signed:
|
|
317
316
|
raise ValueError('signedness mismatch')
|
|
@@ -367,9 +366,10 @@ class Waveform:
|
|
|
367
366
|
return max_width + 1
|
|
368
367
|
|
|
369
368
|
new_width = self._infer_arithmetic_op_width(inferred_width)
|
|
369
|
+
other_value = self._get_value(other)
|
|
370
370
|
|
|
371
371
|
return self.vectorized_map(
|
|
372
|
-
lambda x:
|
|
372
|
+
lambda x: x + other_value,
|
|
373
373
|
width=new_width,
|
|
374
374
|
signed=self.signed,
|
|
375
375
|
)
|
|
@@ -377,10 +377,6 @@ class Waveform:
|
|
|
377
377
|
def __radd__(self, other: WaveformOrScalar) -> Waveform:
|
|
378
378
|
return self.__add__(other)
|
|
379
379
|
|
|
380
|
-
@staticmethod
|
|
381
|
-
def _sub(a, b):
|
|
382
|
-
return a - b
|
|
383
|
-
|
|
384
380
|
def __sub__(self, other: WaveformOrScalar) -> Waveform:
|
|
385
381
|
self._check_sign(other)
|
|
386
382
|
|
|
@@ -388,9 +384,10 @@ class Waveform:
|
|
|
388
384
|
return self._optional_max_width(other)
|
|
389
385
|
|
|
390
386
|
new_width = self._infer_arithmetic_op_width(inferred_width)
|
|
387
|
+
other_value = self._get_value(other)
|
|
391
388
|
|
|
392
389
|
return self.vectorized_map(
|
|
393
|
-
lambda x:
|
|
390
|
+
lambda x: x - other_value,
|
|
394
391
|
width=new_width,
|
|
395
392
|
signed=self.signed,
|
|
396
393
|
)
|
|
@@ -402,17 +399,14 @@ class Waveform:
|
|
|
402
399
|
return self._optional_max_width(other)
|
|
403
400
|
|
|
404
401
|
new_width = self._infer_arithmetic_op_width(inferred_width)
|
|
402
|
+
other_value = self._get_value(other)
|
|
405
403
|
|
|
406
404
|
return self.vectorized_map(
|
|
407
|
-
lambda x:
|
|
405
|
+
lambda x: other_value - x,
|
|
408
406
|
width=new_width,
|
|
409
407
|
signed=self.signed,
|
|
410
408
|
)
|
|
411
409
|
|
|
412
|
-
@staticmethod
|
|
413
|
-
def _mul(a, b):
|
|
414
|
-
return a * b
|
|
415
|
-
|
|
416
410
|
def __mul__(self, other: WaveformOrScalar) -> Waveform:
|
|
417
411
|
self._check_sign(other)
|
|
418
412
|
|
|
@@ -423,9 +417,10 @@ class Waveform:
|
|
|
423
417
|
return self.width + other_width
|
|
424
418
|
|
|
425
419
|
new_width = self._infer_arithmetic_op_width(inferred_width)
|
|
420
|
+
other_value = self._get_value(other)
|
|
426
421
|
|
|
427
422
|
return self.vectorized_map(
|
|
428
|
-
lambda x:
|
|
423
|
+
lambda x: x * other_value,
|
|
429
424
|
width=new_width,
|
|
430
425
|
signed=self.signed,
|
|
431
426
|
)
|
|
@@ -433,13 +428,9 @@ class Waveform:
|
|
|
433
428
|
def __rmul__(self, other: WaveformOrScalar) -> Waveform:
|
|
434
429
|
return self.__mul__(other)
|
|
435
430
|
|
|
436
|
-
@staticmethod
|
|
437
|
-
def _truediv(a, b):
|
|
438
|
-
return a / b
|
|
439
|
-
|
|
440
431
|
def __truediv__(self, other: WaveformOrScalar) -> Waveform:
|
|
441
432
|
self._check_sign(other)
|
|
442
|
-
new_value = self.
|
|
433
|
+
new_value = self.value / self._get_value(other)
|
|
443
434
|
|
|
444
435
|
return Waveform(
|
|
445
436
|
value=new_value,
|
|
@@ -451,7 +442,7 @@ class Waveform:
|
|
|
451
442
|
|
|
452
443
|
def __rtruediv__(self, other: WaveformOrScalar) -> Waveform:
|
|
453
444
|
self._check_sign(other)
|
|
454
|
-
new_value = self.
|
|
445
|
+
new_value = self._get_value(other) / self.value
|
|
455
446
|
|
|
456
447
|
return Waveform(
|
|
457
448
|
value=new_value,
|
|
@@ -461,14 +452,10 @@ class Waveform:
|
|
|
461
452
|
signed=self.signed,
|
|
462
453
|
)
|
|
463
454
|
|
|
464
|
-
@staticmethod
|
|
465
|
-
def _floordiv(a, b):
|
|
466
|
-
return a // b
|
|
467
|
-
|
|
468
455
|
def __floordiv__(self, other: WaveformOrScalar) -> Waveform:
|
|
469
456
|
self._check_sign(other)
|
|
470
457
|
new_width = self._infer_arithmetic_op_width(lambda: self.width)
|
|
471
|
-
new_value = self.
|
|
458
|
+
new_value = self.value // self._get_value(other)
|
|
472
459
|
|
|
473
460
|
return Waveform(
|
|
474
461
|
value=new_value,
|
|
@@ -481,7 +468,7 @@ class Waveform:
|
|
|
481
468
|
def __rfloordiv__(self, other: WaveformOrScalar) -> Waveform:
|
|
482
469
|
self._check_sign(other)
|
|
483
470
|
new_width = self._infer_arithmetic_op_width(lambda: self._get_width(other))
|
|
484
|
-
new_value = self.
|
|
471
|
+
new_value = self._get_value(other) // self.value
|
|
485
472
|
|
|
486
473
|
return Waveform(
|
|
487
474
|
value=new_value,
|
|
@@ -491,15 +478,11 @@ class Waveform:
|
|
|
491
478
|
signed=self.signed,
|
|
492
479
|
)
|
|
493
480
|
|
|
494
|
-
@staticmethod
|
|
495
|
-
def _mod(a, b):
|
|
496
|
-
return a % b
|
|
497
|
-
|
|
498
481
|
def __mod__(self, other: WaveformOrScalar) -> Waveform:
|
|
499
482
|
self._check_sign(other)
|
|
500
483
|
new_width = self._infer_arithmetic_op_width(lambda: self.width)
|
|
501
484
|
|
|
502
|
-
new_value = self.
|
|
485
|
+
new_value = self.value % self._get_value(other)
|
|
503
486
|
|
|
504
487
|
return Waveform(
|
|
505
488
|
value=new_value,
|
|
@@ -513,7 +496,7 @@ class Waveform:
|
|
|
513
496
|
self._check_sign(other)
|
|
514
497
|
new_width = self._infer_arithmetic_op_width(lambda: self._get_width(other))
|
|
515
498
|
|
|
516
|
-
new_value = self.
|
|
499
|
+
new_value = self._get_value(other) % self.value
|
|
517
500
|
|
|
518
501
|
return Waveform(
|
|
519
502
|
value=new_value,
|
|
@@ -523,15 +506,11 @@ class Waveform:
|
|
|
523
506
|
signed=self.signed,
|
|
524
507
|
)
|
|
525
508
|
|
|
526
|
-
@staticmethod
|
|
527
|
-
def _pow(a, b):
|
|
528
|
-
return a**b
|
|
529
|
-
|
|
530
509
|
def __pow__(self, other: WaveformOrScalar) -> Waveform:
|
|
531
510
|
self._check_sign(other)
|
|
532
511
|
new_width = self._infer_arithmetic_op_width(lambda: 64)
|
|
533
512
|
|
|
534
|
-
new_value = self.
|
|
513
|
+
new_value = self.value ** self._get_value(other)
|
|
535
514
|
|
|
536
515
|
return Waveform(
|
|
537
516
|
value=new_value,
|
|
@@ -576,10 +555,6 @@ class Waveform:
|
|
|
576
555
|
raise ValueError(f'width mismatch: {self.width} and {other}')
|
|
577
556
|
return self.width
|
|
578
557
|
|
|
579
|
-
@staticmethod
|
|
580
|
-
def _lshift(a, b):
|
|
581
|
-
return a << b
|
|
582
|
-
|
|
583
558
|
def __lshift__(self, other: WaveformOrScalar) -> Waveform:
|
|
584
559
|
self._check_sign(other)
|
|
585
560
|
self._check_logical_op_type(other)
|
|
@@ -597,10 +572,9 @@ class Waveform:
|
|
|
597
572
|
inferred_width=base_width + shift_width,
|
|
598
573
|
)
|
|
599
574
|
|
|
600
|
-
new_value =
|
|
601
|
-
self.value.astype(np.object_) if new_width > 64 else self.value
|
|
602
|
-
|
|
603
|
-
)
|
|
575
|
+
new_value = (
|
|
576
|
+
self.value.astype(np.object_) if new_width > 64 else self.value
|
|
577
|
+
) << self._get_value(other)
|
|
604
578
|
|
|
605
579
|
return Waveform(
|
|
606
580
|
value=new_value,
|
|
@@ -613,10 +587,6 @@ class Waveform:
|
|
|
613
587
|
def __rlshift__(self, other: WaveformOrScalar) -> Waveform:
|
|
614
588
|
return self.__lshift__(other)
|
|
615
589
|
|
|
616
|
-
@staticmethod
|
|
617
|
-
def _rshift(a, b):
|
|
618
|
-
return a >> b
|
|
619
|
-
|
|
620
590
|
def __rshift__(self, other: WaveformOrScalar) -> Waveform:
|
|
621
591
|
self._check_sign(other)
|
|
622
592
|
self._check_logical_op_type(other)
|
|
@@ -631,7 +601,7 @@ class Waveform:
|
|
|
631
601
|
inferred_width=max((self.width or 0) - other, 0),
|
|
632
602
|
)
|
|
633
603
|
|
|
634
|
-
new_value = self.
|
|
604
|
+
new_value = self.value >> self._get_value(other)
|
|
635
605
|
|
|
636
606
|
return Waveform(
|
|
637
607
|
value=new_value,
|
|
@@ -655,7 +625,7 @@ class Waveform:
|
|
|
655
625
|
inferred_width=max((self.width or 0) - other, 0),
|
|
656
626
|
)
|
|
657
627
|
|
|
658
|
-
new_value = self.
|
|
628
|
+
new_value = self.value >> self._get_value(other)
|
|
659
629
|
|
|
660
630
|
return Waveform(
|
|
661
631
|
value=new_value,
|
|
@@ -665,15 +635,12 @@ class Waveform:
|
|
|
665
635
|
signed=self.signed,
|
|
666
636
|
)
|
|
667
637
|
|
|
668
|
-
@staticmethod
|
|
669
|
-
def _and(a, b):
|
|
670
|
-
return a & b
|
|
671
|
-
|
|
672
638
|
def __and__(self, other: WaveformOrScalar) -> Waveform:
|
|
673
639
|
self._check_sign(other)
|
|
674
640
|
new_width = self._infer_logical_op_width(other)
|
|
641
|
+
other_value = self._get_value(other)
|
|
675
642
|
return self.vectorized_map(
|
|
676
|
-
lambda x:
|
|
643
|
+
lambda x: x & other_value,
|
|
677
644
|
width=new_width,
|
|
678
645
|
signed=False,
|
|
679
646
|
)
|
|
@@ -681,29 +648,21 @@ class Waveform:
|
|
|
681
648
|
def __rand__(self, other: WaveformOrScalar) -> Waveform:
|
|
682
649
|
return self.__and__(other)
|
|
683
650
|
|
|
684
|
-
@staticmethod
|
|
685
|
-
def _or(a, b):
|
|
686
|
-
return a | b
|
|
687
|
-
|
|
688
651
|
def __or__(self, other: WaveformOrScalar, width: int = None) -> Waveform:
|
|
689
652
|
self._check_sign(other)
|
|
690
653
|
new_width = self._infer_logical_op_width(other)
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
)
|
|
654
|
+
other_value = self._get_value(other)
|
|
655
|
+
return self.vectorized_map(lambda x: x | other_value, width=new_width, signed=False)
|
|
694
656
|
|
|
695
657
|
def __ror__(self, other: WaveformOrScalar, width: int = None) -> Waveform:
|
|
696
658
|
return self.__or__(other, width)
|
|
697
659
|
|
|
698
|
-
@staticmethod
|
|
699
|
-
def _xor(a, b):
|
|
700
|
-
return a ^ b
|
|
701
|
-
|
|
702
660
|
def __xor__(self, other: WaveformOrScalar, width: int = None) -> Waveform:
|
|
703
661
|
self._check_sign(other)
|
|
704
662
|
new_width = self._infer_logical_op_width(other)
|
|
663
|
+
other_value = self._get_value(other)
|
|
705
664
|
return self.vectorized_map(
|
|
706
|
-
lambda x:
|
|
665
|
+
lambda x: x ^ other_value,
|
|
707
666
|
width=new_width,
|
|
708
667
|
signed=False,
|
|
709
668
|
)
|
|
@@ -725,30 +684,50 @@ class Waveform:
|
|
|
725
684
|
signed=False,
|
|
726
685
|
)
|
|
727
686
|
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
687
|
+
def _compare(self, other: object, comparator: Callable[[Any, Any], Any]) -> Any:
|
|
688
|
+
if not isinstance(other, (Waveform, int, float)):
|
|
689
|
+
return NotImplemented
|
|
690
|
+
self._check_sign(other)
|
|
691
|
+
return self.vectorized_map(
|
|
692
|
+
lambda x: comparator(x, self._get_value(other)),
|
|
693
|
+
width=1,
|
|
694
|
+
signed=False,
|
|
695
|
+
)
|
|
696
|
+
|
|
697
|
+
def __lt__(self, other: object) -> Any:
|
|
698
|
+
"""Return a 1-bit waveform for ``self < other``."""
|
|
699
|
+
return self._compare(other, lambda a, b: a < b)
|
|
700
|
+
|
|
701
|
+
def __le__(self, other: object) -> Any:
|
|
702
|
+
"""Return a 1-bit waveform for ``self <= other``."""
|
|
703
|
+
return self._compare(other, lambda a, b: a <= b)
|
|
704
|
+
|
|
705
|
+
def __gt__(self, other: object) -> Any:
|
|
706
|
+
"""Return a 1-bit waveform for ``self > other``."""
|
|
707
|
+
return self._compare(other, lambda a, b: a > b)
|
|
708
|
+
|
|
709
|
+
def __ge__(self, other: object) -> Any:
|
|
710
|
+
"""Return a 1-bit waveform for ``self >= other``."""
|
|
711
|
+
return self._compare(other, lambda a, b: a >= b)
|
|
731
712
|
|
|
732
713
|
def __eq__(self, other: object) -> Any:
|
|
733
714
|
if not isinstance(other, (Waveform, int, float)):
|
|
734
715
|
return NotImplemented
|
|
735
716
|
self._check_sign(other)
|
|
717
|
+
other_value = self._get_value(other)
|
|
736
718
|
return self.vectorized_map(
|
|
737
|
-
lambda x:
|
|
719
|
+
lambda x: x == other_value,
|
|
738
720
|
width=1,
|
|
739
721
|
signed=False,
|
|
740
722
|
)
|
|
741
723
|
|
|
742
|
-
@staticmethod
|
|
743
|
-
def _ne(a, b):
|
|
744
|
-
return a != b
|
|
745
|
-
|
|
746
724
|
def __ne__(self, other: object) -> Any:
|
|
747
725
|
if not isinstance(other, (Waveform, int, float)):
|
|
748
726
|
return NotImplemented
|
|
749
727
|
self._check_sign(other)
|
|
728
|
+
other_value = self._get_value(other)
|
|
750
729
|
return self.vectorized_map(
|
|
751
|
-
lambda x:
|
|
730
|
+
lambda x: x != other_value,
|
|
752
731
|
width=1,
|
|
753
732
|
signed=False,
|
|
754
733
|
)
|
|
@@ -962,6 +941,7 @@ class Waveform:
|
|
|
962
941
|
vectorized_func = np.vectorize(func)
|
|
963
942
|
return self.vectorized_map(vectorized_func, width, signed)
|
|
964
943
|
|
|
944
|
+
@expression_function
|
|
965
945
|
def falling_edge(self) -> Waveform:
|
|
966
946
|
"""Detect 1→0 transitions in a 1-bit waveform.
|
|
967
947
|
|
|
@@ -991,6 +971,7 @@ class Waveform:
|
|
|
991
971
|
signed=False,
|
|
992
972
|
)
|
|
993
973
|
|
|
974
|
+
@expression_function
|
|
994
975
|
def rising_edge(self) -> Waveform:
|
|
995
976
|
"""Detect 0→1 transitions in a 1-bit waveform.
|
|
996
977
|
|
|
@@ -1020,6 +1001,7 @@ class Waveform:
|
|
|
1020
1001
|
signed=False,
|
|
1021
1002
|
)
|
|
1022
1003
|
|
|
1004
|
+
@expression_function
|
|
1023
1005
|
def bit_count(self) -> Waveform:
|
|
1024
1006
|
"""Count the number of set bits (population count) in each sample value.
|
|
1025
1007
|
|
|
@@ -1419,6 +1401,7 @@ class Waveform:
|
|
|
1419
1401
|
signed=self.signed,
|
|
1420
1402
|
)
|
|
1421
1403
|
|
|
1404
|
+
@expression_function
|
|
1422
1405
|
def ahead(
|
|
1423
1406
|
self,
|
|
1424
1407
|
n: int = 1,
|
|
@@ -1452,6 +1435,7 @@ class Waveform:
|
|
|
1452
1435
|
"""
|
|
1453
1436
|
return self.relative(n, pad, pad_value)
|
|
1454
1437
|
|
|
1438
|
+
@expression_function
|
|
1455
1439
|
def back(
|
|
1456
1440
|
self,
|
|
1457
1441
|
n: int = 1,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|