umaudemc 0.15.1__py3-none-any.whl → 0.17.0__py3-none-any.whl

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.
umaudemc/statistical.py CHANGED
@@ -2,6 +2,7 @@
2
2
  # Statistical model-checking engine
3
3
  #
4
4
 
5
+ import contextlib
5
6
  import math
6
7
  import os
7
8
  import random
@@ -40,7 +41,7 @@ def run(program, qdata, simulator):
40
41
  # List where to store the results of each query
41
42
  results = [None] * len(qdata)
42
43
  # Remaining queries for being processed
43
- remaining = list(range(len(qdata)))
44
+ remaining = [k for k, q in enumerate(qdata) if not q.converged]
44
45
 
45
46
  # Variables when evaluating expressions
46
47
  cvars = [{'rval': simulator.rval, **q.params} for q in qdata]
@@ -62,8 +63,16 @@ def run(program, qdata, simulator):
62
63
  # Execute the compiled slot
63
64
  value = eval(program.slots[pc[k]], cvars[k])
64
65
 
66
+ # The expression finishes with a call
67
+ if isinstance(value, tuple):
68
+ has_next, jump, *args = value
69
+
70
+ pc[k] = jump
71
+ cvars[k] = dict(zip(program.varnames[pc[k]], args),
72
+ rval=simulator.rval)
73
+
65
74
  # The evaluation of the k-th query has finished
66
- if isinstance(value, float):
75
+ else:
67
76
  remaining.pop(index)
68
77
  results[k] = value
69
78
 
@@ -73,13 +82,6 @@ def run(program, qdata, simulator):
73
82
 
74
83
  break
75
84
 
76
- # The expression finishes with a call
77
- else:
78
- has_next, jump, *args = value
79
-
80
- pc[k] = jump
81
- cvars[k] = dict(zip(program.varnames[pc[k]], args),
82
- rval=simulator.rval)
83
85
 
84
86
  if has_next:
85
87
  index += 1
@@ -91,11 +93,13 @@ def run(program, qdata, simulator):
91
93
  class QueryData:
92
94
  """Data associated to a query under evaluation"""
93
95
 
94
- def __init__(self, query, params):
96
+ def __init__(self, query, delta, params):
95
97
  # Query expression index
96
98
  self.query = query
97
99
  # Initial dictionary of variable values
98
100
  self.params = params
101
+ # Radius of the confidence interval
102
+ self.delta = delta
99
103
 
100
104
  # Sum of the query outcomes
101
105
  self.sum = 0.0
@@ -107,50 +111,74 @@ class QueryData:
107
111
  self.s = 0.0
108
112
  # Radius of the confidence interval
109
113
  self.h = 0.0
114
+ # Number of runs
115
+ self.n = 0
116
+ # Whether the query has converged
117
+ self.converged = False
118
+
119
+ # Number of discarded runs
120
+ self.discarded = 0
110
121
 
111
122
 
112
- def make_parameter_dicts(qinfo):
123
+ def make_parameter_dicts(qinfo, delta):
113
124
  """Make the initial variable mapping for the parameters of a query"""
114
125
 
115
- if qinfo is None:
116
- yield {}
126
+ if qinfo.parameters is None:
127
+ yield {}, (qinfo.delta() if qinfo.delta else delta)
117
128
 
118
129
  else:
119
- var, x, step, end = qinfo
130
+ var, x, step, end = qinfo.parameters
120
131
 
121
132
  while x <= end:
122
- yield {var: x}
133
+ yield {var: x}, (qinfo.delta(x) if qinfo.delta else delta)
123
134
  x += step
124
135
 
125
136
 
126
- def check_interval(qdata, num_sims, alpha, delta, quantile, verbose):
137
+ def check_interval(qdata, num_sims, min_sim, alpha, quantile, verbose):
127
138
  """Check the confidence interval"""
128
139
 
129
- # The radius of encloses the confidence level in the reference
130
- # distribution for calculating confidence intervals
131
- tinv = quantile(num_sims - 1, 1 - alpha / 2) / math.sqrt(num_sims)
132
-
133
140
  # Whether the size of the confidence interval for all queries have converged
134
141
  converged = True
135
142
 
136
143
  for query in qdata:
137
- query.mu = query.sum / num_sims
138
- query.s = math.sqrt(max(query.sum_sq - query.sum * query.mu, 0.0) / (num_sims - 1))
139
- query.h = query.s * tinv
144
+ # This query has already converged
145
+ if query.converged:
146
+ continue
147
+ # All executions of this query have been discarded
148
+ elif query.n == 0:
149
+ converged = False
150
+ continue
151
+ # A single execution
152
+ elif query.n == 1:
153
+ query.mu, query.s, query.h = query.sum, 0.0, 0.0
154
+ # General case
155
+ else:
156
+ # The radius encloses the confidence level in the reference
157
+ # distribution for calculating confidence intervals
158
+ tinv = quantile(query.n - 1, 1 - alpha / 2) / math.sqrt(query.n)
140
159
 
141
- if query.h > delta:
160
+ query.mu = query.sum / query.n
161
+ query.s = math.sqrt(max(query.sum_sq - query.sum * query.mu, 0.0) / (query.n - 1))
162
+ query.h = query.s * tinv
163
+
164
+ if query.h <= query.delta and query.n >= min_sim:
165
+ query.converged = True
166
+ query.discarded = num_sims - query.n
167
+ else:
142
168
  converged = False
143
169
 
144
170
  # Print intermediate results if in verbose mode
145
171
  if verbose:
146
- usermsgs.print_info(f' step={num_sims} μ={" ".join(str(q.mu) for q in qdata)}'
172
+ usermsgs.print_info(f' step={num_sims} n={" ".join(str(q.n) for q in qdata)}'
173
+ f' μ={" ".join(str(q.mu) for q in qdata)}'
147
174
  f' σ={" ".join(str(q.s) for q in qdata)}'
148
175
  f' r={" ".join(str(q.h) for q in qdata)}')
149
176
 
150
177
  return converged
151
178
 
152
179
 
153
- def run_single(program, qdata, num_sims, max_sim, simulator, alpha, delta, block_size, verbose=False):
180
+ def run_single(program, qdata, num_sims, min_sim, max_sim, simulator, alpha, block_size,
181
+ verbose=False, dump=None):
154
182
  """Run simulation in a single thread"""
155
183
 
156
184
  # Size of the first block of execution (it coincides with num_sims
@@ -166,11 +194,17 @@ def run_single(program, qdata, num_sims, max_sim, simulator, alpha, delta, block
166
194
  # Run the simulation and compute all queries at once
167
195
  values = run(program, qdata, simulator)
168
196
 
169
- for k, query in enumerate(qdata):
170
- query.sum += values[k]
171
- query.sum_sq += values[k] * values[k]
197
+ # Dump evaluations if required
198
+ if dump:
199
+ print(*values, file=dump)
172
200
 
173
- converged = check_interval(qdata, num_sims, alpha, delta, quantile, verbose)
201
+ for value, query in zip(values, qdata):
202
+ if value is not None:
203
+ query.sum += value
204
+ query.sum_sq += value * value
205
+ query.n += 1
206
+
207
+ converged = check_interval(qdata, num_sims, min_sim, alpha, quantile, verbose)
174
208
 
175
209
  if converged or max_sim and num_sims >= max_sim:
176
210
  break
@@ -181,7 +215,7 @@ def run_single(program, qdata, num_sims, max_sim, simulator, alpha, delta, block
181
215
  return num_sims, qdata
182
216
 
183
217
 
184
- def thread_main(program, qdata, simulator, num_sims, block_size, seed, queue, barrier, more):
218
+ def thread_main(program, qdata, simulator, num_sims, block_size, seed, queue, barrier, more, dump=None):
185
219
  """Entry point of a calculating thread"""
186
220
 
187
221
  maude.setRandomSeed(seed)
@@ -189,8 +223,12 @@ def thread_main(program, qdata, simulator, num_sims, block_size, seed, queue, ba
189
223
 
190
224
  block = num_sims
191
225
 
226
+ # Open dump file for the raw data
227
+ dump_file = open(dump, 'w') if dump else None
228
+
192
229
  sums = [0.0] * len(qdata)
193
230
  sum_sq = [0.0] * len(qdata)
231
+ counts = [0] * len(qdata)
194
232
 
195
233
  # Repeat until the main process says we are done
196
234
  while True:
@@ -199,17 +237,23 @@ def thread_main(program, qdata, simulator, num_sims, block_size, seed, queue, ba
199
237
  # Run the simulation and compute all queries at once
200
238
  values = run(program, qdata, simulator)
201
239
 
240
+ if dump is not None:
241
+ print(*values, file=dump_file)
242
+
202
243
  for k in range(len(qdata)):
203
- sums[k] += values[k]
204
- sum_sq[k] += values[k] * values[k]
244
+ if values[k] is not None:
245
+ sums[k] += values[k]
246
+ sum_sq[k] += values[k] * values[k]
247
+ counts[k] += 1
205
248
 
206
249
  # Send the results to the main process and wait for it
207
- queue.put((sums, sum_sq))
250
+ queue.put((sums, sum_sq, counts))
208
251
  barrier.wait()
209
252
 
210
253
  for k in range(len(qdata)):
211
254
  sums[k] = 0.0
212
255
  sum_sq[k] = 0.0
256
+ counts[k] = 0
213
257
 
214
258
  if not more.value:
215
259
  break
@@ -218,7 +262,7 @@ def thread_main(program, qdata, simulator, num_sims, block_size, seed, queue, ba
218
262
  block = block_size
219
263
 
220
264
 
221
- def run_parallel(program, qdata, num_sims, max_sim, simulator, alpha, delta, block_size, jobs, verbose=False):
265
+ def run_parallel(program, qdata, num_sims, min_sim, max_sim, simulator, alpha, block_size, jobs, verbose=False, dump=None):
222
266
  """Run the simulation in multiple threads"""
223
267
  import multiprocessing as mp
224
268
  mp.set_start_method('fork', force=True)
@@ -234,6 +278,8 @@ def run_parallel(program, qdata, num_sims, max_sim, simulator, alpha, delta, blo
234
278
 
235
279
  # Random number seeds
236
280
  seeds = [random.getrandbits(20) for _ in range(jobs)]
281
+ # Dump file names
282
+ dumps = [f'{dump}.{os.getpid()}-{k}' for k in range(jobs)] if dump else ([None] * jobs)
237
283
  # Queue for transferring the query evaluations
238
284
  queue = mp.Queue()
239
285
  barrier = mp.Barrier(jobs + 1)
@@ -243,7 +289,7 @@ def run_parallel(program, qdata, num_sims, max_sim, simulator, alpha, delta, blo
243
289
  processes = [mp.Process(target=thread_main,
244
290
  args=(program, qdata, simulator, num_sims // jobs + (k < rest),
245
291
  block_size // jobs + (k < rest_block),
246
- seeds[k], queue, barrier, more)) for k in range(jobs)]
292
+ seeds[k], queue, barrier, more, dumps[k])) for k in range(jobs)]
247
293
 
248
294
  # Start all processes
249
295
  for p in processes:
@@ -252,13 +298,14 @@ def run_parallel(program, qdata, num_sims, max_sim, simulator, alpha, delta, blo
252
298
  # Exactly as in run_single but with several threads
253
299
  while True:
254
300
  for _ in range(jobs):
255
- sums, sum_sq = queue.get()
301
+ sums, sum_sq, counts = queue.get()
256
302
 
257
303
  for k, query in enumerate(qdata):
258
304
  query.sum += sums[k]
259
305
  query.sum_sq += sum_sq[k]
306
+ query.n += counts[k]
260
307
 
261
- converged = check_interval(qdata, num_sims, alpha, delta, quantile, verbose)
308
+ converged = check_interval(qdata, num_sims, min_sim, alpha, quantile, verbose)
262
309
 
263
310
  if converged or max_sim and num_sims >= max_sim:
264
311
  break
@@ -285,30 +332,35 @@ def qdata_to_dict(num_sims, qdata, program):
285
332
  qdata_it = iter(qdata)
286
333
  q = next(qdata_it, None)
287
334
 
288
- for k, (fname, line, column, params) in enumerate(program.query_locations):
335
+ for k, query in enumerate(program.queries):
289
336
  # For parametric queries, we return an array of values
290
- if params:
291
- mean, std, radius = [], [], []
337
+ if query.parameters:
338
+ mean, std, radius, count, discarded = [], [], [], [], []
292
339
 
293
340
  while q and q.query == k:
294
341
  mean.append(q.mu)
295
342
  std.append(q.s)
296
343
  radius.append(q.h)
344
+ count.append(q.n)
345
+ discarded.append(q.discarded)
297
346
  q = next(qdata_it, None)
298
347
 
299
348
  # We also write information about the parameter
300
- param_info = {'params': [dict(name=params[0], start=params[1], step=params[2], stop=params[3])]}
349
+ param_info = {'params': [dict(zip(('name', 'start', 'step', 'stop'), query.parameters))]}
301
350
 
302
351
  else:
303
- mean, std, radius = q.mu, q.s, q.h
352
+ mean, std, radius, count, discarded = q.mu, q.s, q.h, q.n, q.discarded
353
+ q = next(qdata_it, None)
304
354
  param_info = {}
305
355
 
306
- queries.append(dict(mean=mean, std=std, radius=radius, file=fname, line=line, column=column, **param_info))
356
+ queries.append(dict(mean=mean, std=std, radius=radius,
357
+ file=query.filename, line=query.line, column=query.column,
358
+ nsims=count, discarded=discarded, **param_info))
307
359
 
308
360
  return dict(nsims=num_sims, queries=queries)
309
361
 
310
362
 
311
- def check(program, simulator, seed, alpha, delta, block, min_sim, max_sim, jobs, verbose=False):
363
+ def check(program, simulator, seed, alpha, delta, block, min_sim, max_sim, jobs, verbose=False, dump=None):
312
364
  """Run the statistical model checker"""
313
365
 
314
366
  # The number of simulations for the first block
@@ -324,14 +376,15 @@ def check(program, simulator, seed, alpha, delta, block, min_sim, max_sim, jobs,
324
376
 
325
377
  # Each query maintains some data like the sum of the outcomes
326
378
  # and the sum of their squares
327
- qdata = [QueryData(k, idict)
328
- for k, qinfo in enumerate(program.query_locations)
329
- for idict in make_parameter_dicts(qinfo[3])]
379
+ qdata = [QueryData(k, dt, idict)
380
+ for k, qinfo in enumerate(program.queries)
381
+ for idict, dt in make_parameter_dicts(qinfo, delta)]
330
382
 
331
383
  # Run the simulations
332
384
  if jobs == 1 and num_sims != 1:
333
- return run_single(program, qdata, num_sims, max_sim, simulator, alpha,
334
- delta, block, verbose=verbose)
385
+ with (open(dump, 'w') if dump else contextlib.nullcontext()) as dump_file:
386
+ return run_single(program, qdata, num_sims, min_sim, max_sim, simulator, alpha,
387
+ block, verbose=verbose, dump=dump_file)
335
388
  else:
336
- return run_parallel(program, qdata, num_sims, max_sim, simulator, alpha,
337
- delta, block, jobs, verbose=verbose)
389
+ return run_parallel(program, qdata, num_sims, min_sim, max_sim, simulator, alpha,
390
+ block, jobs, verbose=verbose, dump=dump)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: umaudemc
3
- Version: 0.15.1
3
+ Version: 0.17.0
4
4
  Summary: Unified Maude model-checking utility
5
5
  Author-email: ningit <ningit@users.noreply.github.com>
6
6
  License-Expression: GPL-3.0-or-later
@@ -1,10 +1,10 @@
1
- umaudemc/__init__.py,sha256=8WhJAZouJDJEIY8zYgYIOv2VtMy_b_0q-sscUgsm7U0,23
2
- umaudemc/__main__.py,sha256=x7HLEryX--lIKW1JYjF66XLZQ9lUnAQmd2ADGRipEfo,14823
3
- umaudemc/api.py,sha256=I-o5foy8NUlO4JT4pX9L7kkuHQG_8_GMkWlOKt708E8,19733
1
+ umaudemc/__init__.py,sha256=ctD9pjqBvASXR0DHHzalDZFaQsnMJWDpTalYrvY3e_Y,23
2
+ umaudemc/__main__.py,sha256=LgKeZWi1JRrEclPS3asyZziErEJyldQvlZTtTt_tcyc,15063
3
+ umaudemc/api.py,sha256=naZ5edEbvx-S-NU29yAAJtqglfYnSAYVS2RJNyxJMQQ,19893
4
4
  umaudemc/backends.py,sha256=mzJkALYwcKPInT0lBiRsCxJSewKvx5j_akQsqWN1Ezo,4590
5
5
  umaudemc/common.py,sha256=UcIf7hTpP2qjcT9u_9-UcYR0nNeosx1xRZW7wsuT2bE,7305
6
6
  umaudemc/counterprint.py,sha256=vVqM_UjGRk_xeftFxBGI5m6cQXV7mf8KvbQ_fvAvSQk,9226
7
- umaudemc/distributed.py,sha256=2InONr9a4-n8lFVMWr57Hai3Rbuq6m4K-X4aDD1dYgE,8842
7
+ umaudemc/distributed.py,sha256=aCvU1TaM5SZhAtWahclyANc1I0YXj8SuPXO_VjaqDdo,9115
8
8
  umaudemc/formatter.py,sha256=nbQlIsR5Xv18OEcpJdnTDGqO9xGL_amvBGFMU2OmheU,6026
9
9
  umaudemc/formulae.py,sha256=jZPPDhjgsb7cs5rWvitiQoO0fd8JIlK98at2SN-LzVE,12156
10
10
  umaudemc/grapher.py,sha256=K1chKNNlEzQvfOsiFmRPJmd9OpxRIrg6OyiMW6gqOCU,4348
@@ -14,11 +14,11 @@ umaudemc/kleene.py,sha256=sW5SGdXpbLrjGtihPn8qgnhSH5WgltFaLVRx6GLwQU4,4697
14
14
  umaudemc/mproc.py,sha256=9X5pTb3Z3XHcdOo8ynH7I5RZQpjzm9xr4IBbEtaglUE,11766
15
15
  umaudemc/opsem.py,sha256=Xfdi9QGy-vcpmQ9ni8lBDAlKNw-fCRzYr6wnPbv6m1s,9448
16
16
  umaudemc/probabilistic.py,sha256=MNvFeEd84-OYedSnyksZB87UckPfwizVNJepCItgRy8,29306
17
- umaudemc/pyslang.py,sha256=zOfVGtfnOWDGghtaYLfQHq61KvbzVFmAM_0-upNhrTk,87753
18
- umaudemc/quatex.py,sha256=SQAbVz1csGXGqcfzFcjP89BdIpN8K2aiwP_PMLGPr1o,23239
17
+ umaudemc/pyslang.py,sha256=ABSXYUQO2TmDq8EZ3EpVZV8NecZ0p0gERlSvLUIVAm8,87970
18
+ umaudemc/quatex.py,sha256=7n7ugusjFUyO5jqKeb6-O8hdH4vj2fgnc-V_Z41w-40,27271
19
19
  umaudemc/resources.py,sha256=qKqvgLYTJVtsQHQMXFObyCLTo6-fssQeu_mG7tvVyD0,932
20
- umaudemc/simulators.py,sha256=Lk50Ql7hWUasWkQSWxboeR5LYfJtpwrANjUDuxYjuZ4,13232
21
- umaudemc/statistical.py,sha256=buthWv4ovvxsvDs0eWgJw7lX2_9BsnLsW_PxW17RHCI,9087
20
+ umaudemc/simulators.py,sha256=K8NFgwvvTgj1lueksnWOSc-7bo6VxS1_CA9g-6l5umc,14367
21
+ umaudemc/statistical.py,sha256=aogqx2JCur9Lw9ZfOI6jaggwLcVTflDSs_Mn7yCmOGE,10815
22
22
  umaudemc/terminal.py,sha256=B4GWLyW4Sdymgoavj418y4TI4MnWqNu3JS4BBoSYeTc,1037
23
23
  umaudemc/usermsgs.py,sha256=h4VPxljyKidEI8vpPcToKJA6mcLu9PtMkIh6vH3rDuA,719
24
24
  umaudemc/webui.py,sha256=XlDV87tOOdcclHp2_oerrvHwRmCZdqAR4PppqeZm47A,11072
@@ -40,8 +40,8 @@ umaudemc/command/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,
40
40
  umaudemc/command/check.py,sha256=PyaPDMw5OnPxSIZ10U4we0b5tTrjnYKAtAeQkJh2uLE,12031
41
41
  umaudemc/command/graph.py,sha256=JqGzESC2sn-LBh2sqctrij03ItzwDO808s2qkNKUle0,6112
42
42
  umaudemc/command/pcheck.py,sha256=eV4e4GcOHanP4hcIhMKd5Js22_ONac6kYj70FXun3mY,7274
43
- umaudemc/command/scheck.py,sha256=jiVNsLfbNDUleWl9HuNW7GTQdszd5cefZJn0_Epm9UU,4967
44
- umaudemc/command/sworker.py,sha256=0WzLoJBnjc5EYTuZK0fOQ5yoVhEBCH2ffm4WS8oM_yw,4383
43
+ umaudemc/command/scheck.py,sha256=mmAzCdGdIHgKbsyEi0uW-jcj9X0MSc-EEkEPR-EiB8U,6208
44
+ umaudemc/command/sworker.py,sha256=1icakFDO_qSr4ga-5Cu6eDfLirjLPJLNtO7OkbwTUQQ,4651
45
45
  umaudemc/command/test.py,sha256=Ru21JXNF61F5N5jayjwxp8okIjOAvuZuAlV_5ltQ-GU,37088
46
46
  umaudemc/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
47
47
  umaudemc/data/opsem.maude,sha256=geDP3_RMgtS1rRmYOybJDCXn_-dyHHxg0JxfYg1ftv0,27929
@@ -52,9 +52,9 @@ umaudemc/data/smcgraph.js,sha256=iCNQNmsuGdL_GLnqVhGDisediFtedxw3C24rxSiQwx8,667
52
52
  umaudemc/data/smcview.css,sha256=ExFqrMkSeaf8VxFrJXflyCsRW3FTwbv78q0Hoo2UVrM,3833
53
53
  umaudemc/data/smcview.js,sha256=_fHum1DRU1mhco-9-c6KqTLgiC5u_cCUf61jIK7wcIQ,14509
54
54
  umaudemc/data/templog.maude,sha256=TZ-66hVWoG6gp7gJpS6FsQn7dpBTLrr76bKo-UfHGcA,9161
55
- umaudemc-0.15.1.dist-info/licenses/LICENSE,sha256=MrEGL32oSWfnAZ0Bq4BZNcqnq3Mhp87Q4w6-deXfFnA,17992
56
- umaudemc-0.15.1.dist-info/METADATA,sha256=zePwkmspjbYQuOOw0NfnjpS3XxFa-LvX5LH_azwapys,1654
57
- umaudemc-0.15.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
58
- umaudemc-0.15.1.dist-info/entry_points.txt,sha256=8rYRlLkn4orZtAoujDSeol1t_UFBrK0bfjmLTNv9B44,52
59
- umaudemc-0.15.1.dist-info/top_level.txt,sha256=Yo_CF78HLGBSblk3890qLcx6XZ17zHCbGcT9iG8sfMw,9
60
- umaudemc-0.15.1.dist-info/RECORD,,
55
+ umaudemc-0.17.0.dist-info/licenses/LICENSE,sha256=MrEGL32oSWfnAZ0Bq4BZNcqnq3Mhp87Q4w6-deXfFnA,17992
56
+ umaudemc-0.17.0.dist-info/METADATA,sha256=HMhQv0StUz5ODf2H8vanIt_FnkK6p7BDZd-nkc8o0Fg,1654
57
+ umaudemc-0.17.0.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
58
+ umaudemc-0.17.0.dist-info/entry_points.txt,sha256=8rYRlLkn4orZtAoujDSeol1t_UFBrK0bfjmLTNv9B44,52
59
+ umaudemc-0.17.0.dist-info/top_level.txt,sha256=Yo_CF78HLGBSblk3890qLcx6XZ17zHCbGcT9iG8sfMw,9
60
+ umaudemc-0.17.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5