wavekit 0.5.1__tar.gz → 0.5.3__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.
Files changed (23) hide show
  1. {wavekit-0.5.1 → wavekit-0.5.3}/PKG-INFO +1 -1
  2. {wavekit-0.5.1 → wavekit-0.5.3}/pyproject.toml +1 -1
  3. {wavekit-0.5.1 → wavekit-0.5.3}/src/wavekit/readers/base.py +3 -3
  4. {wavekit-0.5.1 → wavekit-0.5.3}/src/wavekit/readers/fsdb/npi_fsdb_reader.pyx +126 -252
  5. {wavekit-0.5.1 → wavekit-0.5.3}/src/wavekit/readers/fsdb/reader.py +85 -78
  6. {wavekit-0.5.1 → wavekit-0.5.3}/src/wavekit/readers/vcd/reader.py +0 -3
  7. {wavekit-0.5.1 → wavekit-0.5.3}/src/wavekit/scope.py +84 -82
  8. {wavekit-0.5.1 → wavekit-0.5.3}/src/wavekit/signal.py +9 -4
  9. {wavekit-0.5.1 → wavekit-0.5.3}/LICENSE +0 -0
  10. {wavekit-0.5.1 → wavekit-0.5.3}/README.md +0 -0
  11. {wavekit-0.5.1 → wavekit-0.5.3}/build.py +0 -0
  12. {wavekit-0.5.1 → wavekit-0.5.3}/src/wavekit/__init__.py +0 -0
  13. {wavekit-0.5.1 → wavekit-0.5.3}/src/wavekit/pattern/__init__.py +0 -0
  14. {wavekit-0.5.1 → wavekit-0.5.3}/src/wavekit/pattern/dsl.py +0 -0
  15. {wavekit-0.5.1 → wavekit-0.5.3}/src/wavekit/pattern/engine.py +0 -0
  16. {wavekit-0.5.1 → wavekit-0.5.3}/src/wavekit/pattern/instance.py +0 -0
  17. {wavekit-0.5.1 → wavekit-0.5.3}/src/wavekit/pattern/result.py +0 -0
  18. {wavekit-0.5.1 → wavekit-0.5.3}/src/wavekit/pattern/steps.py +0 -0
  19. {wavekit-0.5.1 → wavekit-0.5.3}/src/wavekit/readers/expr_parser.py +0 -0
  20. {wavekit-0.5.1 → wavekit-0.5.3}/src/wavekit/readers/fsdb/npi_fsdb.pxd +0 -0
  21. {wavekit-0.5.1 → wavekit-0.5.3}/src/wavekit/readers/pattern_parser.py +0 -0
  22. {wavekit-0.5.1 → wavekit-0.5.3}/src/wavekit/readers/value_change.pyx +0 -0
  23. {wavekit-0.5.1 → wavekit-0.5.3}/src/wavekit/waveform.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wavekit
3
- Version: 0.5.1
3
+ Version: 0.5.3
4
4
  Summary: a fundamental package for digital circuit waveform analysis
5
5
  License-File: LICENSE
6
6
  Author: cxzzzz
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "wavekit"
3
- version = "0.5.1"
3
+ version = "0.5.3"
4
4
  description = "a fundamental package for digital circuit waveform analysis"
5
5
  authors = ["cxzzzz <cxz19961010@outlook.com>"]
6
6
  readme = "README.md"
@@ -38,7 +38,7 @@ class Reader:
38
38
  If the bit-range suffix is omitted and the file stores the signal with a
39
39
  range, the range is appended automatically.
40
40
 
41
- Pattern syntax (used by :meth:`get_matched_signals`,
41
+ Pattern syntax (used by :meth:`get_matched_signals`, :meth:`get_matched_scopes`,
42
42
  :meth:`load_matched_waveforms`, :meth:`eval`)
43
43
  -------------------------------------------------
44
44
  * ``{a,b,c}`` — matches ``a``, ``b``, or ``c``; captures each as a key.
@@ -233,7 +233,7 @@ class Reader:
233
233
  )
234
234
  return matched_signals
235
235
 
236
- def get_matched_scope(
236
+ def get_matched_scopes(
237
237
  self,
238
238
  pattern: str,
239
239
  root_scope: Scope | None = None,
@@ -270,7 +270,7 @@ class Reader:
270
270
  ::
271
271
 
272
272
  # Find all fifo_N sub-scopes under tb.dut
273
- scopes = reader.get_matched_scope("tb.dut.fifo_{0..3}")
273
+ scopes = reader.get_matched_scopes("tb.dut.fifo_{0..3}")
274
274
  for (idx,), scope in scopes.items():
275
275
  waves = reader.load_matched_waveforms(
276
276
  "w_ptr[2:0]", clock_pattern="clk", root_scope=scope
@@ -3,7 +3,6 @@ import math
3
3
  import sys
4
4
  import numpy as np
5
5
  cimport numpy as np
6
- #from cpython import PyBytes_FromString
7
6
  import cython
8
7
  from libcpp.vector cimport vector
9
8
  from libc.stdlib cimport malloc, free
@@ -17,165 +16,159 @@ NPI_FSDB_CT_TAGGED_UNION = <int>npiFsdbSigCtTaggedUnion
17
16
  NPI_FSDB_CT_RECORD = <int>npiFsdbSigCtRecord
18
17
 
19
18
 
20
- cdef class NpiFsdbScope:
21
- pass
19
+ cdef class NpiFsdbSignal:
20
+ """Wraps a npiFsdbSigHandle — a single signal (leaf or composite) in the FSDB hierarchy."""
21
+ cdef npiFsdbSigHandle sig_handle
22
+
23
+ @staticmethod
24
+ cdef init(npiFsdbSigHandle sig_handle):
25
+ sig = NpiFsdbSignal()
26
+ sig.sig_handle = sig_handle
27
+ return sig
28
+
29
+ def __init__(self):
30
+ self.sig_handle = NULL
31
+
32
+ def name(self) -> str:
33
+ assert self.sig_handle != NULL
34
+ name = npi_fsdb_sig_property_str(npiFsdbSigName, self.sig_handle).decode('ascii')
35
+ # NPI returns fully-qualified names like "tb.dut.field"; take only the last component.
36
+ # Array elements are named "a[0]", "a[0][1]" etc. — splitting by "." is safe since
37
+ # brackets cannot contain ".".
38
+ name = name.split(".")
39
+ name = name[len(name)-1]
40
+ return name
41
+
42
+ def has_member(self) -> bool:
43
+ assert self.sig_handle != NULL
44
+ cdef int has_member
45
+ if npi_fsdb_sig_property(npiFsdbSigHasMember, self.sig_handle, &has_member):
46
+ return has_member != 0
47
+ return False
48
+
49
+ def composite_type(self):
50
+ """Return the npiFsdbSigCompositeType_e int value, or None if not composite."""
51
+ assert self.sig_handle != NULL
52
+ cdef int has_member
53
+ cdef int ct
54
+ if not (npi_fsdb_sig_property(npiFsdbSigHasMember, self.sig_handle, &has_member) and has_member):
55
+ return None
56
+ if npi_fsdb_sig_property(npiFsdbSigCompositeType, self.sig_handle, &ct):
57
+ return ct
58
+ return None
59
+
60
+ def width(self) -> int:
61
+ """Return the total bit-width of this signal (sum of all leaf members for composites)."""
62
+ assert self.sig_handle != NULL
63
+ cdef int has_member
64
+ cdef int w
65
+ cdef npiFsdbSigIter sub_signal_iter
66
+ cdef npiFsdbSigHandle sub_signal
67
+ assert npi_fsdb_sig_property(npiFsdbSigHasMember, self.sig_handle, &has_member)
68
+ if has_member == 0:
69
+ assert npi_fsdb_sig_property(npiFsdbSigRangeSize, self.sig_handle, &w)
70
+ return w
71
+ else:
72
+ sub_signal_iter = npi_fsdb_iter_member(self.sig_handle)
73
+ assert sub_signal_iter != NULL
74
+ w = 0
75
+ while (sub_signal := npi_fsdb_iter_sig_next(sub_signal_iter)):
76
+ w += NpiFsdbSignal.init(sub_signal).width()
77
+ return w
78
+
79
+ def range(self):
80
+ """Return the (high, low) bit-range tuple, or None for non-array composites."""
81
+ assert self.sig_handle != NULL
82
+ cdef int has_member
83
+ cdef int ct
84
+ cdef int left_range, right_range
85
+ assert npi_fsdb_sig_property(npiFsdbSigHasMember, self.sig_handle, &has_member)
86
+ assert npi_fsdb_sig_property(npiFsdbSigLeftRange, self.sig_handle, &left_range)
87
+ assert npi_fsdb_sig_property(npiFsdbSigRightRange, self.sig_handle, &right_range)
88
+ if has_member != 0:
89
+ assert npi_fsdb_sig_property(npiFsdbSigCompositeType, self.sig_handle, &ct)
90
+ if ct == <int>npiFsdbSigCtArray:
91
+ return (left_range, right_range)
92
+ return None
93
+ return None
94
+
95
+ def member_list(self) -> list:
96
+ """Return direct member NpiFsdbSignal objects for composite signals."""
97
+ assert self.sig_handle != NULL
98
+ cdef int has_member
99
+ cdef npiFsdbSigIter member_iter
100
+ cdef npiFsdbSigHandle member_handle
101
+
102
+ res = []
103
+ if npi_fsdb_sig_property(npiFsdbSigHasMember, self.sig_handle, &has_member) and has_member:
104
+ member_iter = npi_fsdb_iter_member(self.sig_handle)
105
+ while True:
106
+ member_handle = npi_fsdb_iter_sig_next(member_iter)
107
+ if member_handle == NULL:
108
+ break
109
+ res.append(NpiFsdbSignal.init(member_handle))
110
+ npi_fsdb_iter_sig_stop(member_iter)
111
+ return res
22
112
 
23
113
 
24
- cdef class NpiFsdbNormalScope(NpiFsdbScope):
114
+ cdef class NpiFsdbScope:
115
+ """Wraps a npiFsdbScopeHandle — a scope node (module/block) in the FSDB hierarchy."""
25
116
  cdef npiFsdbScopeHandle scope_handle
26
117
 
27
118
  @staticmethod
28
119
  cdef init(npiFsdbScopeHandle scope_handle):
29
- scope = NpiFsdbNormalScope()
120
+ scope = NpiFsdbScope()
30
121
  scope.scope_handle = scope_handle
31
122
  return scope
32
123
 
33
124
  def __init__(self):
34
125
  self.scope_handle = NULL
35
126
 
36
- def child_scope_list(self, include_signal_scope:bool = True) -> list[NpiFsdbScope]:
127
+ def child_scope_list(self) -> list:
128
+ """Return direct child NpiFsdbScope nodes (not signals)."""
37
129
  assert self.scope_handle != NULL
38
130
  res = []
39
131
  child_scope_handle_iter = npi_fsdb_iter_child_scope(self.scope_handle)
40
-
132
+
41
133
  cdef npiFsdbScopeHandle child_scope_handle
42
134
  while True:
43
135
  child_scope_handle = npi_fsdb_iter_scope_next(child_scope_handle_iter)
44
136
  if child_scope_handle == NULL:
45
137
  break
46
- res.append(NpiFsdbNormalScope.init(child_scope_handle))
138
+ res.append(NpiFsdbScope.init(child_scope_handle))
47
139
  npi_fsdb_iter_scope_stop(child_scope_handle_iter)
48
-
49
-
50
- cdef npiFsdbSigHandle child_sig_handle
51
- if include_signal_scope:
52
- child_sig_handle_iter = npi_fsdb_iter_sig(self.scope_handle)
53
- while True:
54
- child_sig_handle = npi_fsdb_iter_sig_next(child_sig_handle_iter)
55
- if child_sig_handle == NULL:
56
- break
57
- res.append(NpiFsdbSignalScope.init(child_sig_handle))
58
- npi_fsdb_iter_sig_stop(child_sig_handle_iter)
59
140
  return res
60
141
 
61
- def signal_list(self) -> list[str]:
142
+ def signal_list(self) -> list:
143
+ """Return direct child NpiFsdbSignal objects declared in this scope."""
62
144
  assert self.scope_handle != NULL
63
-
64
145
  res = []
65
- sig_handle_iter = npi_fsdb_iter_sig(self.scope_handle)
66
- cdef npiFsdbSigHandle sig_handle
67
- cdef const char* c_str
146
+ child_sig_handle_iter = npi_fsdb_iter_sig(self.scope_handle)
147
+
148
+ cdef npiFsdbSigHandle child_sig_handle
68
149
  while True:
69
- sig_handle = npi_fsdb_iter_sig_next(sig_handle_iter)
70
- if sig_handle == NULL:
150
+ child_sig_handle = npi_fsdb_iter_sig_next(child_sig_handle_iter)
151
+ if child_sig_handle == NULL:
71
152
  break
72
- c_str = npi_fsdb_sig_property_str(npiFsdbSigName, sig_handle)
73
- name = c_str.decode('ascii')
74
- res.append(name)
75
- npi_fsdb_iter_sig_stop(sig_handle_iter)
76
-
153
+ res.append(NpiFsdbSignal.init(child_sig_handle))
154
+ npi_fsdb_iter_sig_stop(child_sig_handle_iter)
77
155
  return res
78
156
 
79
157
  def name(self) -> str:
80
158
  assert self.scope_handle != NULL
81
- name = npi_fsdb_scope_property_str(npiFsdbScopeName, self.scope_handle).decode('ascii')
82
- return name
159
+ return npi_fsdb_scope_property_str(npiFsdbScopeName, self.scope_handle).decode('ascii')
83
160
 
84
161
  def type(self) -> str:
85
162
  assert self.scope_handle != NULL
86
- type_str = npi_fsdb_scope_property_str(npiFsdbScopeType, self.scope_handle).decode('ascii')
87
- return type_str
88
-
89
- def def_name(self) -> str:
90
- assert self.scope_handle != NULL
91
- def_name = npi_fsdb_scope_property_str(npiFsdbScopeDefName, self.scope_handle).decode('ascii')
92
- return def_name
93
-
94
- cdef class NpiFsdbSignalScope(NpiFsdbScope):
95
- cdef npiFsdbSigHandle sig_handle
96
-
97
- @staticmethod
98
- cdef init(npiFsdbSigHandle sig_handle):
99
- scope = NpiFsdbSignalScope()
100
- scope.sig_handle = sig_handle
101
- return scope
102
-
103
- def __init__(self):
104
- self.sig_handle = NULL
163
+ return npi_fsdb_scope_property_str(npiFsdbScopeType, self.scope_handle).decode('ascii')
105
164
 
106
- def child_scope_list(self, include_signal_scope:bool = True) -> list[NpiFsdbScope]:
107
- assert self.sig_handle != NULL
108
-
109
- cdef int has_member
110
- cdef npiFsdbSigIter member_iter
111
- cdef npiFsdbSigHandle member_handle
112
-
113
- res = []
114
- if include_signal_scope:
115
- if npi_fsdb_sig_property(npiFsdbSigHasMember, self.sig_handle, &has_member) and has_member:
116
- member_iter = npi_fsdb_iter_member(self.sig_handle)
117
- while True:
118
- member_handle = npi_fsdb_iter_sig_next(member_iter)
119
- if member_handle == NULL:
120
- break
121
- res.append(NpiFsdbSignalScope.init(member_handle))
122
- npi_fsdb_iter_sig_stop(member_iter)
123
- return res
124
-
125
- def signal_list(self) -> list[str]:
126
- assert self.sig_handle != NULL
127
-
128
- cdef const char* c_str
129
- cdef npiFsdbSigIter member_iter
130
- cdef npiFsdbSigHandle member_handle
131
- cdef int has_member
132
- res = []
133
-
134
- if npi_fsdb_sig_property(npiFsdbSigHasMember, self.sig_handle, &has_member) and has_member:
135
- member_iter = npi_fsdb_iter_member(self.sig_handle)
136
- while True:
137
- member_handle = npi_fsdb_iter_sig_next(member_iter)
138
- if member_handle == NULL:
139
- break
140
- c_str = npi_fsdb_sig_property_str(npiFsdbSigName, member_handle)
141
- name = c_str.decode('ascii')
142
- name = name.split(".")
143
- name = name[len(name)-1]
144
- res.append(name)
145
- npi_fsdb_iter_sig_stop(member_iter)
146
- return res
147
-
148
- def name(self) -> str:
149
- assert self.sig_handle != NULL
150
- name = npi_fsdb_sig_property_str(npiFsdbSigName, self.sig_handle).decode('ascii')
151
- #TODO: this only works for non-array type (array's name is like data[0],can't be split by ".")
152
- name = name.split(".")
153
- name = name[len(name)-1]
154
- return name
155
-
156
- def has_member(self) -> bool:
157
- assert self.sig_handle != NULL
158
- cdef int has_member
159
- if npi_fsdb_sig_property(npiFsdbSigHasMember, self.sig_handle, &has_member):
160
- return has_member != 0
161
- return False
162
-
163
- def composite_type(self):
164
- """Return the npiFsdbSigCompositeType_e int value, or None if not composite."""
165
- assert self.sig_handle != NULL
166
- cdef int has_member
167
- cdef int ct
168
- if not (npi_fsdb_sig_property(npiFsdbSigHasMember, self.sig_handle, &has_member) and has_member):
165
+ def def_name(self) -> str | None:
166
+ """Return the module definition name, or None if this scope is not a module."""
167
+ if self.type() != 'npiFsdbScopeSvModule':
169
168
  return None
170
- if npi_fsdb_sig_property(npiFsdbSigCompositeType, self.sig_handle, &ct):
171
- return ct
172
- return None
169
+ return npi_fsdb_scope_property_str(npiFsdbScopeDefName, self.scope_handle).decode('ascii')
173
170
 
174
- def type(self) -> str:
175
- raise NotImplementedError("type property of signal scope is not supported")
176
171
 
177
- def def_name(self) -> str:
178
- raise NotImplementedError("def_name property of signal scope is not supported")
179
172
  @cython.boundscheck(False) # 关闭边界检查以提升性能
180
173
  @cython.wraparound(False) # 关闭负索引检查以提升性能
181
174
  cdef inline cstr_to_ull(char* str, unsigned int xz_value, int max_bit_num = 2147483647):
@@ -210,88 +203,6 @@ cdef inline cstr_to_bit(char* str, unsigned int xz_value):
210
203
  value = xz_value
211
204
  return value
212
205
 
213
- cdef inline fsdb_read_value_change(vct_handle: npiFsdbVctHandle, begin_time, end_time, width:int, xz_value:int):
214
-
215
- cdef npiFsdbTime cur_time
216
- cdef npiFsdbValue cur_value
217
- cdef int stat
218
-
219
- cdef vector[unsigned long long] time_array
220
-
221
- cdef int value_array_num = math.ceil(width/64)
222
- assert width <= 64*64
223
- cdef vector[unsigned long long] value_array[64]
224
-
225
- cdef int first = 1
226
- cdef int i,j
227
- cdef unsigned long long cur_int_value
228
- while True:
229
- if first:
230
- if begin_time is None:
231
- stat = npi_fsdb_goto_first(vct_handle)
232
- else:
233
- stat = npi_fsdb_goto_time(vct_handle, <npiFsdbTime> begin_time)
234
- else:
235
- stat = npi_fsdb_goto_next(vct_handle)
236
- if stat == 0:
237
- break
238
-
239
- cur_value.format = npiFsdbBinStrVal
240
- npi_fsdb_vct_time(vct_handle, &cur_time)
241
- npi_fsdb_vct_value(vct_handle, &cur_value)
242
- if cur_time > end_time:
243
- break
244
-
245
- time_array.push_back(cur_time)
246
- if width == 1: # opt for clk
247
- value_array[0].push_back(cstr_to_bit(<char*>cur_value.value.str, xz_value))
248
- else:
249
- cur_lllint_value = 0
250
- for i in range(value_array_num):
251
- value_array[i].push_back(cstr_to_ull(<char*>cur_value.value.str + 64*i, xz_value, 64))
252
-
253
- result = np.zeros((value_array[0].size(), 2), dtype=np.uint64 if width <= 64 else np.object_) # 创建一个 (n, 2) 的二维数组,dtype 为 int32
254
- for j in range(value_array[0].size()):
255
- result[j][0] = time_array[j]
256
- if width <= 64:
257
- result[j][1] = value_array[0][j]
258
- else:
259
- for i in range(value_array_num):
260
- result[j][1] = (result[j][1] << 64) + value_array[i][j]
261
-
262
- return result
263
-
264
- cdef get_signal_handle_width(npiFsdbSigHandle signal_handle):
265
- cdef int has_member
266
- cdef int width
267
- cdef npiFsdbSigIter sub_signal_iter
268
- cdef npiFsdbSigHandle sub_signal
269
- assert(npi_fsdb_sig_property(npiFsdbSigHasMember, signal_handle, &has_member))
270
- if(has_member == 0):
271
- assert(npi_fsdb_sig_property(npiFsdbSigRangeSize, signal_handle, &width))
272
- return width
273
- else:
274
- sub_signal_iter = npi_fsdb_iter_member(signal_handle)
275
- assert(sub_signal_iter != NULL)
276
- width = 0
277
- while(sub_signal := npi_fsdb_iter_sig_next(sub_signal_iter)):
278
- width += get_signal_handle_width(sub_signal)
279
- return width
280
-
281
- cdef get_signal_handle_range(npiFsdbSigHandle signal_handle):
282
- cdef int has_member
283
- cdef int type
284
- cdef int left_range, right_range
285
- assert(npi_fsdb_sig_property(npiFsdbSigHasMember, signal_handle, &has_member))
286
- assert(npi_fsdb_sig_property(npiFsdbSigLeftRange, signal_handle, &left_range))
287
- assert(npi_fsdb_sig_property(npiFsdbSigRightRange, signal_handle, &right_range))
288
-
289
- if has_member != 0:
290
- assert(npi_fsdb_sig_property(npiFsdbSigCompositeType, signal_handle, &type))
291
- if type == <int>npiFsdbSigCtArray:
292
- return (left_range, right_range)
293
- return None
294
- return None
295
206
 
296
207
  cdef class NpiFsdbReader:
297
208
  cdef npiFsdbFileHandle fsdb_handle
@@ -299,69 +210,36 @@ cdef class NpiFsdbReader:
299
210
 
300
211
  def __init__(self, str file):
301
212
  import os
302
-
213
+
303
214
  # 检查VERDI_HOME环境变量
304
215
  verdi_home = os.environ.get('VERDI_HOME')
305
216
  if verdi_home:
306
217
  npi_lib_path = os.path.join(verdi_home, 'share/NPI/lib/LINUX64/libNPI.so')
307
-
308
- #cdef int argc = len(sys.argv)
309
- #cdef char** argv = <char**>malloc((argc + 1) * sizeof(char*))
310
-
311
- #for i in range(argc):
312
- # argv[i] = <char*>malloc(strlen(sys.argv[i].encode('ascii')))
313
- # strcpy(argv[i], sys.argv[i].encode('ascii'))
314
- #argv[argc] = NULL # Null-terminate the array
315
-
316
- #npi_init(argc, argv)
317
- #self.file = file
318
- #free(argv)
319
218
 
320
- #cdef npiFsdbFileHandle fsdb_handle
321
219
  file_str = file.encode('utf-8')
322
220
  cdef char* file_s = file_str
323
221
  self.fsdb_handle = npi_fsdb_open(file_s)
324
222
  if(self.fsdb_handle == NULL):
325
223
  raise OSError(f"Failed to open fsdb file :{file_str}")
326
224
 
327
-
328
-
329
- def get_signal_width(
330
- self,
331
- str signal
332
- ) -> int:
333
-
334
- cdef npiFsdbSigHandle signal_handle = npi_fsdb_sig_by_name(self.fsdb_handle, signal.encode('ascii'), NULL)
335
- assert signal_handle != NULL, f"can't find signal: {signal}"
336
-
337
- cdef int width = get_signal_handle_width(signal_handle)
338
- return width
339
-
340
- def get_signal_range(
341
- self,
342
- str signal
343
- ) -> tuple[int,int]:
344
-
345
- cdef npiFsdbSigHandle signal_handle = npi_fsdb_sig_by_name(self.fsdb_handle, signal.encode('ascii'), NULL)
225
+ def get_signal(self, str signal) -> NpiFsdbSignal:
226
+ """Look up a signal by its full hierarchical path and return an NpiFsdbSignal handle."""
227
+ cdef npiFsdbSigHandle signal_handle = npi_fsdb_sig_by_name(self.fsdb_handle, signal.encode('ascii'), NULL)
346
228
  assert signal_handle != NULL, f"can't find signal: {signal}"
347
-
348
- return get_signal_handle_range(signal_handle)
229
+ return NpiFsdbSignal.init(signal_handle)
349
230
 
350
231
  @cython.boundscheck(False) # 关闭边界检查以提升性能
351
232
  @cython.wraparound(False) # 关闭负索引检查以提升性能
352
233
  def load_value_change(
353
234
  self,
354
- str signal,
235
+ NpiFsdbSignal signal,
355
236
  unsigned long long begin_time,
356
237
  unsigned long long end_time,
357
238
  int xz_value
358
239
  ) -> np.ndarray:
359
-
360
- cdef npiFsdbSigHandle signal_handle = npi_fsdb_sig_by_name(self.fsdb_handle, signal.encode('ascii'), NULL)
361
- assert signal_handle != NULL, f"can't find signal: {signal}"
362
- cdef int width = get_signal_handle_width(signal_handle)
363
240
 
364
- cdef npiFsdbVctHandle signal_vct_handle = npi_fsdb_create_vct(signal_handle)
241
+ cdef int width = signal.width()
242
+ cdef npiFsdbVctHandle signal_vct_handle = npi_fsdb_create_vct(signal.sig_handle)
365
243
  cdef npiFsdbTime cur_time
366
244
  cdef npiFsdbValue cur_value
367
245
  cdef int stat
@@ -382,7 +260,7 @@ cdef class NpiFsdbReader:
382
260
  stat = npi_fsdb_goto_time(signal_vct_handle, <npiFsdbTime> begin_time)
383
261
  else:
384
262
  stat = npi_fsdb_goto_next(signal_vct_handle)
385
-
263
+
386
264
  if stat == 0:
387
265
  break
388
266
 
@@ -422,11 +300,7 @@ cdef class NpiFsdbReader:
422
300
  result_object[:,1] = (result_object[:,1] << shift) + value_np_array
423
301
  return result_object
424
302
 
425
- #npi_fsdb_release_vct(signal_vct_handle)
426
- #npi_fsdb_unload_vc(self.fsdb_handle)
427
- #return result
428
-
429
- def top_scope_list(self) -> list[NpiFsdbScope]:
303
+ def top_scope_list(self) -> list:
430
304
  cdef npiFsdbScopeIter top_scope_iter
431
305
  cdef npiFsdbScopeHandle top_scope_handle
432
306
 
@@ -436,7 +310,7 @@ cdef class NpiFsdbReader:
436
310
  top_scope_handle = npi_fsdb_iter_scope_next(top_scope_iter)
437
311
  if top_scope_handle == NULL:
438
312
  break
439
- res.append(NpiFsdbNormalScope.init(top_scope_handle))
313
+ res.append(NpiFsdbScope.init(top_scope_handle))
440
314
  npi_fsdb_iter_scope_stop(top_scope_iter)
441
315
  return res
442
316
 
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import importlib
4
4
  from collections import defaultdict
5
5
  from collections.abc import Sequence
6
+ from dataclasses import dataclass, field
6
7
  from functools import cached_property
7
8
  from typing import Any
8
9
 
@@ -20,92 +21,95 @@ from .npi_fsdb_reader import (
20
21
  NPI_FSDB_CT_UNION,
21
22
  NpiFsdbReader,
22
23
  NpiFsdbScope,
23
- NpiFsdbSignalScope,
24
+ NpiFsdbSignal,
24
25
  )
25
26
 
26
- _NPI_CT_TO_ENUM = {
27
- NPI_FSDB_CT_ARRAY: SignalCompositeType.ARRAY,
28
- NPI_FSDB_CT_STRUCT: SignalCompositeType.STRUCT,
29
- NPI_FSDB_CT_UNION: SignalCompositeType.UNION,
30
- NPI_FSDB_CT_TAGGED_UNION: SignalCompositeType.TAGGED_UNION,
31
- NPI_FSDB_CT_RECORD: SignalCompositeType.RECORD,
32
- }
33
-
34
-
35
- def _build_signal_from_handle(
36
- sig_scope: NpiFsdbSignalScope,
37
- full_name: str,
38
- reader: FsdbReader,
39
- ) -> Signal:
40
- """Recursively build a Signal (with children) from a NpiFsdbSignalScope handle."""
41
- width = reader._get_signal_width(full_name)
42
- rng = reader._get_signal_range(full_name)
43
- ct_raw = sig_scope.composite_type()
44
- if ct_raw is None:
45
- composite_type = None
46
- elif ct_raw in _NPI_CT_TO_ENUM:
47
- composite_type = _NPI_CT_TO_ENUM[ct_raw]
48
- else:
49
- raise ValueError(f"Unknown NPI composite type value: {ct_raw} for signal '{full_name}'")
50
-
51
- children = None
52
- if composite_type is not None:
53
- children = []
54
- for child_scope in sig_scope.child_scope_list():
55
- child_full_name = f'{full_name}.{child_scope.name()}'
56
- children.append(_build_signal_from_handle(child_scope, child_full_name, reader))
57
-
58
- return Signal(
59
- name=sig_scope.name(),
60
- full_name=full_name,
61
- width=width,
62
- range=rng,
63
- signed=False,
64
- composite_type=composite_type,
65
- children=children,
66
- )
27
+
28
+ @dataclass
29
+ class FsdbSignal(Signal):
30
+ """FSDB-backed :class:`~wavekit.signal.Signal` with lazy member loading.
31
+
32
+ Extends :class:`~wavekit.signal.Signal` by holding an NPI signal handle
33
+ directly, enabling on-demand loading of composite members without any
34
+ reader reference. Construct via :meth:`from_handle`.
35
+ """
36
+
37
+ _npi_signal: NpiFsdbSignal | None = field(default=None, repr=False, compare=False)
38
+
39
+ @classmethod
40
+ def from_handle(cls, npi_sig: NpiFsdbSignal, full_name: str) -> FsdbSignal:
41
+ """Build an :class:`FsdbSignal` from an NPI signal handle and its full path."""
42
+ _npi_ct_to_enum = {
43
+ NPI_FSDB_CT_ARRAY: SignalCompositeType.ARRAY,
44
+ NPI_FSDB_CT_STRUCT: SignalCompositeType.STRUCT,
45
+ NPI_FSDB_CT_UNION: SignalCompositeType.UNION,
46
+ NPI_FSDB_CT_TAGGED_UNION: SignalCompositeType.TAGGED_UNION,
47
+ NPI_FSDB_CT_RECORD: SignalCompositeType.RECORD,
48
+ }
49
+ ct_raw = npi_sig.composite_type()
50
+ if ct_raw is None:
51
+ composite_type = None
52
+ elif ct_raw in _npi_ct_to_enum:
53
+ composite_type = _npi_ct_to_enum[ct_raw]
54
+ else:
55
+ raise ValueError(f"Unknown NPI composite type value: {ct_raw} for signal '{full_name}'")
56
+ return cls(
57
+ name=npi_sig.name(),
58
+ full_name=full_name,
59
+ width=npi_sig.width(),
60
+ range=npi_sig.range(),
61
+ composite_type=composite_type,
62
+ _npi_signal=npi_sig,
63
+ )
64
+
65
+ @cached_property
66
+ def member_list(self) -> list[Signal] | None:
67
+ if self.composite_type is None:
68
+ return None
69
+ assert self._npi_signal is not None
70
+ npi_members = self._npi_signal.member_list()
71
+ if self.composite_type == SignalCompositeType.ARRAY:
72
+ # Array members' NPI names already include the array base name at every
73
+ # level, e.g. parent "a" has members "a[0]", "a[1]", and "a[0]" further
74
+ # has members "a[0][0]", "a[0][1]", etc. The member name is therefore an
75
+ # extension of the parent name with no extra "." separator, so we must NOT
76
+ # prepend full_name (which would produce "tb.dut.a.a[0]"). Instead we use
77
+ # the parent scope path (full_name minus the signal's own local name) as
78
+ # the base, giving "tb.dut.a[0]", "tb.dut.a[0][1]", etc.
79
+ scope_path = self.full_name[: -(len(self.name) + 1)]
80
+ return [FsdbSignal.from_handle(m, f'{scope_path}.{m.name()}') for m in npi_members]
81
+ else:
82
+ return [FsdbSignal.from_handle(m, f'{self.full_name}.{m.name()}') for m in npi_members]
67
83
 
68
84
 
69
85
  class FsdbScope(Scope):
70
86
  def __init__(self, handle: NpiFsdbScope, parent_scope: FsdbScope | None, reader: FsdbReader):
71
87
  super().__init__(name=handle.name())
72
- self.handle = handle
88
+ self._npi_scope = handle
73
89
  self.parent_scope = parent_scope
74
90
  self.reader = reader
75
91
 
76
92
  @cached_property
77
93
  def signal_list(self) -> Sequence[Signal]:
78
94
  full_scope_name = self.full_name()
79
- signals = []
80
- for sig_scope in self.handle.child_scope_list(include_signal_scope=True):
81
- if not isinstance(sig_scope, NpiFsdbSignalScope):
82
- continue
83
- signal_path = f'{full_scope_name}.{sig_scope.name()}'
84
- signals.append(_build_signal_from_handle(sig_scope, signal_path, self.reader))
85
- return signals
95
+ return [
96
+ FsdbSignal.from_handle(npi_sig, f'{full_scope_name}.{npi_sig.name()}')
97
+ for npi_sig in self._npi_scope.signal_list()
98
+ ]
86
99
 
87
100
  @cached_property
88
101
  def child_scope_list(self) -> Sequence[Scope]:
89
- return [FsdbScope(c, self, self.reader) for c in self.handle.child_scope_list()]
102
+ return [FsdbScope(c, self, self.reader) for c in self._npi_scope.child_scope_list()]
90
103
 
91
104
  @cached_property
92
- def child_normal_scope_list(self) -> Sequence[Scope]:
93
- return [
94
- FsdbScope(c, self, self.reader)
95
- for c in self.handle.child_scope_list(include_signal_scope=False)
96
- ]
97
-
98
- @property
99
105
  def type(self) -> str:
100
- if not hasattr(self, '_type'):
101
- self._type = self.handle.type()
102
- return self._type
106
+ """Return the NPI scope type string, e.g. 'npiFsdbScopeSvModule'."""
107
+ return self._npi_scope.type()
103
108
 
104
- @property
109
+ @cached_property
105
110
  def def_name(self) -> str | None:
106
- if not hasattr(self, '_def_name'):
107
- self._def_name = self.handle.def_name()
108
- return self._def_name
111
+ """Return the module definition name, or None if this scope is not a module."""
112
+ return self._npi_scope.def_name()
109
113
 
110
114
  def find_scope_by_module(self, module_name: str, depth: int = 0) -> list[Scope]:
111
115
  if not hasattr(self, '_preloaded_module_scope'):
@@ -113,11 +117,8 @@ class FsdbScope(Scope):
113
117
  return self._preloaded_module_scope[module_name]
114
118
 
115
119
  def preload_module_scope(self):
116
- if isinstance(self.handle, NpiFsdbSignalScope):
117
- return {}
118
-
119
120
  preloaded_module_scope = defaultdict(list)
120
- for c in self.child_normal_scope_list:
121
+ for c in self.child_scope_list:
121
122
  for module_name, module_scope_list in c.preload_module_scope().items():
122
123
  preloaded_module_scope[module_name].extend(module_scope_list)
123
124
 
@@ -166,9 +167,21 @@ class FsdbReader(Reader):
166
167
  signal_path = signal.full_name if isinstance(signal, Signal) else signal
167
168
  clock_path = clock.full_name if isinstance(clock, Signal) else clock
168
169
 
170
+ # Resolve NPI signal handles — reuse the handle if already available
171
+ npi_signal = (
172
+ signal._npi_signal
173
+ if isinstance(signal, FsdbSignal) and signal._npi_signal is not None
174
+ else self.file_handle.get_signal(signal_path)
175
+ )
176
+ npi_clock = (
177
+ clock._npi_signal
178
+ if isinstance(clock, FsdbSignal) and clock._npi_signal is not None
179
+ else self.file_handle.get_signal(clock_path)
180
+ )
181
+
169
182
  # Always load the full clock to compute absolute cycle numbers
170
183
  all_clock_changes = self.file_handle.load_value_change(
171
- clock_path,
184
+ npi_clock,
172
185
  begin_time=0,
173
186
  end_time=2**64 - 1,
174
187
  xz_value=0,
@@ -199,7 +212,7 @@ class FsdbReader(Reader):
199
212
  # Load signal within the requested window only (FSDB NPI provides the
200
213
  # correct initial value at begin_time even if the last change was earlier)
201
214
  signal_value_change = self.file_handle.load_value_change(
202
- signal_path,
215
+ npi_signal,
203
216
  begin_time=begin_time_actual,
204
217
  end_time=end_time_actual,
205
218
  xz_value=xz_value,
@@ -208,7 +221,7 @@ class FsdbReader(Reader):
208
221
  full_wave = self.value_change_to_waveform(
209
222
  signal_value_change,
210
223
  windowed_clock_changes,
211
- width=self.file_handle.get_signal_width(signal_path),
224
+ width=npi_signal.width(),
212
225
  signed=signed,
213
226
  sample_on_posedge=sample_on_posedge,
214
227
  signal=signal_path,
@@ -229,12 +242,6 @@ class FsdbReader(Reader):
229
242
  ]
230
243
  return self._top_scope_list
231
244
 
232
- def _get_signal_width(self, signal: str) -> int:
233
- return self.file_handle.get_signal_width(signal)
234
-
235
- def _get_signal_range(self, signal: str) -> tuple[int, int]:
236
- return self.file_handle.get_signal_range(signal)
237
-
238
245
  @property
239
246
  def begin_time(self) -> str:
240
247
  return self.file_handle.min_time()
@@ -75,9 +75,6 @@ class VcdReader(Reader):
75
75
  def end_time(self) -> int:
76
76
  return self.file_handle.endtime
77
77
 
78
- def _get_signal_width(self, signal: str) -> int:
79
- return int(self.file_handle[signal].size)
80
-
81
78
  def load_waveform(
82
79
  self,
83
80
  signal: Signal | str,
@@ -11,7 +11,7 @@ from .readers.pattern_parser import (
11
11
  PatternMap,
12
12
  split_by_range_expr,
13
13
  )
14
- from .signal import Signal
14
+ from .signal import Signal, SignalCompositeType
15
15
 
16
16
  T = TypeVar('T')
17
17
 
@@ -84,25 +84,6 @@ class Scope:
84
84
  raise NotImplementedError()
85
85
 
86
86
 
87
- def _range_str_to_tuple(range_str: str) -> tuple[int, int] | None:
88
- """Extract the innermost (last) ``[high:low]`` or ``[n]`` from *range_str*.
89
-
90
- For multi-dimensional suffixes such as ``"[3][7:0]"`` only the final
91
- bracket pair is returned (``(7, 0)``), because in practice only the last
92
- dimension carries a bit-range; all preceding dimensions are index
93
- selectors of the form ``[n]`` and are encoded in the signal name only.
94
- Returns ``None`` if *range_str* is empty or contains no bracket expression.
95
- """
96
- if not range_str:
97
- return None
98
- last = re.search(r'\[(\d+)(?::(\d+))?\]$', range_str)
99
- if last is None:
100
- return None
101
- high = int(last.group(1))
102
- low = int(last.group(2)) if last.group(2) is not None else high
103
- return (high, low)
104
-
105
-
106
87
  def _prepend_scope_name(value: Any, scope_name: str) -> Any:
107
88
  if isinstance(value, Signal):
108
89
  return dataclasses.replace(value, full_name=f'{scope_name}.{value.full_name}')
@@ -212,68 +193,6 @@ def _traverse_scope_tree(
212
193
  return res
213
194
 
214
195
 
215
- def _match_signals_in_list(
216
- signals: Sequence[Signal], pattern_list: list[PatternMap]
217
- ) -> dict[tuple[Any, ...], Signal]:
218
- """Match *pattern_list* against *signals*, recursing into composite children.
219
-
220
- The first element of *pattern_list* is matched against signal names in
221
- *signals*. When more patterns remain and the matched signal is composite
222
- (``signal.children is not None``), the function recurses into those
223
- children with the remaining patterns, allowing patterns to address
224
- struct/union members across multiple levels.
225
-
226
- .. note::
227
- Composite member path separators are assumed to be ``"."`` (the same
228
- as scope separators). For array elements such as ``a[10].b`` this
229
- means the pattern segment for the array element is ``"a[10]"`` and the
230
- next segment is ``"b"``.
231
-
232
- TODO: Verify actual NPI naming for composite members — NPI may return the
233
- full name from the root signal rather than just the local node name, and
234
- array-element separators may differ from ``"."``. Adjust accordingly once
235
- confirmed.
236
- """
237
- if not pattern_list:
238
- return {}
239
-
240
- def resolve_leaf(sig: Signal, sig_bare: str, range_suffix: str) -> Signal:
241
- new_range = _range_str_to_tuple(range_suffix) if range_suffix else sig.range
242
- new_local = f'{sig_bare}{range_suffix}' if range_suffix else sig.name
243
- return dataclasses.replace(sig, full_name=new_local, range=new_range)
244
-
245
- res: dict[tuple[Any, ...], Signal] = {}
246
- for sig in signals:
247
- sig_bare, _ = split_by_range_expr(sig.name)
248
- for k, p in pattern_list[0].items():
249
- if p[0] == '@':
250
- name_regex, range_suffix = split_by_range_expr(p[1:])
251
- if match := re.fullmatch(name_regex, sig.name):
252
- assert len(k) == 0
253
- key = (match.groups(),)
254
- if len(pattern_list) == 1:
255
- assert key not in res, f'pattern {name_regex} matches more than one signal'
256
- res[key] = resolve_leaf(sig, sig_bare, range_suffix)
257
- elif sig.children is not None:
258
- for ck, cv in _match_signals_in_list(
259
- sig.children, pattern_list[1:]
260
- ).items():
261
- res[key + ck] = _prepend_scope_name(cv, sig.name)
262
- else:
263
- p_bare, range_suffix = split_by_range_expr(p)
264
- if p_bare != sig_bare:
265
- continue
266
- key = k
267
- if len(pattern_list) == 1:
268
- assert key not in res
269
- res[key] = resolve_leaf(sig, sig_bare, range_suffix)
270
- elif sig.children is not None:
271
- for ck, cv in _match_signals_in_list(sig.children, pattern_list[1:]).items():
272
- res[key + ck] = _prepend_scope_name(cv, sig.name)
273
- break
274
- return res
275
-
276
-
277
196
  def match_signals(
278
197
  scope: Scope, descendant_scope_pattern_list: list[PatternMap]
279
198
  ) -> dict[tuple[Any, ...], Signal]:
@@ -286,6 +205,89 @@ def match_signals(
286
205
  objects whose ``full_name`` is the complete hierarchical path.
287
206
  """
288
207
 
208
+ def _match_signals_in_list(
209
+ signals: Sequence[Signal], pattern_list: list[PatternMap]
210
+ ) -> dict[tuple[Any, ...], Signal]:
211
+ """Match *pattern_list* against *signals*, recursing into composite members.
212
+
213
+ The first element of *pattern_list* is matched against signal names in
214
+ *signals*. When more patterns remain and the matched signal is composite
215
+ (``signal.member_list is not None``), the function recurses into those
216
+ members with the remaining patterns, allowing patterns to address
217
+ struct/union members across multiple levels.
218
+
219
+ **Array signals** require special treatment because NPI reports each
220
+ element's name as an extension of the parent name with no ``"."``
221
+ separator. For example, signal ``a`` (ARRAY) has members ``a[0]``,
222
+ ``a[1]``, which in turn have members ``a[0][0]``, ``a[0][1]``, etc.
223
+ A user pattern like ``a[10][0]`` therefore cannot be resolved by a
224
+ single exact-name match at the top level. Instead, for ARRAY signals
225
+ we check whether the signal name is a string prefix of the pattern:
226
+ if yes, we recurse into members with the **same** pattern (not
227
+ advancing to the next element), letting each member self-select by the
228
+ same prefix rule until an exact match is found.
229
+ """
230
+ if not pattern_list:
231
+ return {}
232
+
233
+ def resolve_leaf(sig: Signal, sig_bare: str, range_suffix: str) -> Signal:
234
+ if range_suffix:
235
+ # Extract (high, low) from the innermost bracket of range_suffix,
236
+ # e.g. "[7:0]" → (7, 0), "[3]" → (3, 3).
237
+ m = re.search(r'\[(\d+)(?::(\d+))?\]$', range_suffix)
238
+ assert m is not None
239
+ h = int(m.group(1))
240
+ low = int(m.group(2)) if m.group(2) is not None else h
241
+ new_range: tuple[int, int] | None = (h, low)
242
+ else:
243
+ new_range = sig.range
244
+ return dataclasses.replace(
245
+ sig,
246
+ full_name=f'{sig_bare}{range_suffix}' if range_suffix else sig.name,
247
+ range=new_range,
248
+ )
249
+
250
+ res: dict[tuple[Any, ...], Signal] = {}
251
+ for sig in signals:
252
+ sig_bare, _ = split_by_range_expr(sig.name)
253
+ for k, p in pattern_list[0].items():
254
+ if p[0] == '@':
255
+ name_regex, range_suffix = split_by_range_expr(p[1:])
256
+ if match := re.fullmatch(name_regex, sig.name):
257
+ assert len(k) == 0
258
+ key = (match.groups(),)
259
+ if len(pattern_list) == 1:
260
+ assert (
261
+ key not in res
262
+ ), f'pattern {name_regex} matches more than one signal'
263
+ res[key] = resolve_leaf(sig, sig_bare, range_suffix)
264
+ elif sig.member_list is not None:
265
+ for ck, cv in _match_signals_in_list(
266
+ sig.member_list, pattern_list[1:]
267
+ ).items():
268
+ res[key + ck] = _prepend_scope_name(cv, sig.name)
269
+ else:
270
+ p_bare, range_suffix = split_by_range_expr(p)
271
+ if p_bare != sig_bare:
272
+ continue
273
+ key = k
274
+ if sig.composite_type == SignalCompositeType.ARRAY and range_suffix:
275
+ if sig.member_list is not None:
276
+ for ck, cv in _match_signals_in_list(
277
+ sig.member_list, pattern_list
278
+ ).items():
279
+ res[key + ck] = cv
280
+ elif len(pattern_list) == 1:
281
+ assert key not in res
282
+ res[key] = resolve_leaf(sig, sig_bare, range_suffix)
283
+ elif sig.member_list is not None:
284
+ for ck, cv in _match_signals_in_list(
285
+ sig.member_list, pattern_list[1:]
286
+ ).items():
287
+ res[key + ck] = _prepend_scope_name(cv, sig.name)
288
+ break
289
+ return res
290
+
289
291
  def match_signals_in_scope(
290
292
  scope: Scope, signal_pattern_list: list[PatternMap]
291
293
  ) -> dict[tuple[Any, ...], Signal]:
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass
4
4
  from enum import Enum
5
+ from functools import cached_property
5
6
 
6
7
 
7
8
  class SignalCompositeType(Enum):
@@ -24,7 +25,7 @@ class Signal:
24
25
 
25
26
  Stores the signal's local name, full hierarchical path, bit-width,
26
27
  declared bit-range, and signedness. For composite signals (structs,
27
- unions, arrays) the ``composite_type`` and ``children`` fields carry
28
+ unions, arrays) the ``composite_type`` and ``member_list`` fields carry
28
29
  the internal structure.
29
30
 
30
31
  Attributes
@@ -59,12 +60,13 @@ class Signal:
59
60
  value describing the kind of composite. Not all backends populate
60
61
  this field; backends that do not support composite introspection leave
61
62
  it as ``None`` (e.g. VCD).
62
- children:
63
+ member_list:
63
64
  ``None`` for leaf signals. For composite signals this is the list of
64
65
  direct member :class:`Signal` objects, populated in the same order the
65
66
  backend reports them. Always ``None`` when ``composite_type`` is
66
67
  ``None``, and always a list (possibly empty) when ``composite_type``
67
- is set.
68
+ is set. Lazily evaluated on first access; backend-specific subclasses
69
+ override the ``member_list`` cached property to provide the actual loading logic.
68
70
  """
69
71
 
70
72
  name: str
@@ -73,7 +75,10 @@ class Signal:
73
75
  range: tuple[int, int] | None
74
76
  signed: bool = False
75
77
  composite_type: SignalCompositeType | None = None
76
- children: list[Signal] | None = None
78
+
79
+ @cached_property
80
+ def member_list(self) -> list[Signal] | None:
81
+ return None
77
82
 
78
83
  def __str__(self) -> str:
79
84
  return (
File without changes
File without changes
File without changes
File without changes
File without changes