umaudemc 0.13.1__py3-none-any.whl → 0.15.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/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = '0.13.1'
1
+ __version__ = '0.15.0'
umaudemc/__main__.py CHANGED
@@ -312,6 +312,10 @@ def build_parser():
312
312
  type=int,
313
313
  default=1
314
314
  )
315
+ parser_scheck.add_argument(
316
+ '--distribute',
317
+ help='Distribute the computation over some machines'
318
+ )
315
319
  parser_scheck.add_argument(
316
320
  '--format', '-f',
317
321
  help='Output format for the simulation results',
@@ -326,6 +330,18 @@ def build_parser():
326
330
 
327
331
  parser_scheck.set_defaults(mode='scheck')
328
332
 
333
+ #
334
+ # Statistical model checking distributed worker
335
+ #
336
+
337
+ parser_sworker = subparsers.add_parser('sworker', help='Worker for distributed statistical model checking')
338
+
339
+ parser_sworker.add_argument('--address', '-a', help='listening address', default='127.0.0.1')
340
+ parser_sworker.add_argument('--port', '-p', help='listening port', type=int, default=1234)
341
+ parser_sworker.add_argument('--keep-file', help='keep received Maude file for debugging', action='store_true')
342
+
343
+ parser_sworker.set_defaults(mode='sworker')
344
+
329
345
  #
330
346
  # Test and benchmark test suites from the command line
331
347
  #
@@ -501,6 +517,11 @@ def main():
501
517
  from .command.scheck import scheck
502
518
  return scheck(args)
503
519
 
520
+ # Werker for distributed statistical model-checking
521
+ elif args.mode == 'sworker':
522
+ from .command.sworker import sworker
523
+ return sworker(args)
524
+
504
525
  # Batch test subcommand
505
526
  elif args.mode == 'test':
506
527
  from .command.test import test
@@ -80,7 +80,7 @@ class StormBackend:
80
80
  builder.add_next_value(row=st, column=st, value=1.0)
81
81
 
82
82
  def _build_mdp(self, builder, graph):
83
- """Build an MDP from the probabilsitic rewrite graph"""
83
+ """Build an MDP from the probabilistic rewrite graph"""
84
84
 
85
85
  last_visited, row_index, num_states = -1, 0, len(graph)
86
86
 
@@ -127,7 +127,6 @@ class StormBackend:
127
127
  self._build_mdp(builder, graph)
128
128
 
129
129
  transition_matrix = builder.build()
130
- #input(transition_matrix)
131
130
 
132
131
  # State labeling with atomic propositions
133
132
  state_labeling = stormpy.storage.StateLabeling(num_states)
@@ -233,7 +232,7 @@ class StormBackend:
233
232
  def state_analysis(self, *args, **kwargs):
234
233
  """Steady and transient state analysis"""
235
234
 
236
- # StormPy does not allow calculting state probabilities, as far as we know, so
235
+ # StormPy does not allow calculating state probabilities, as far as we know, so
237
236
  # we delegate on the command-line interface to Storm
238
237
  from ._storm import StormBackend as StormCmdLineBackend
239
238
 
@@ -479,22 +479,22 @@ class BuiltinBackend:
479
479
  return [modop, self.ctl2mucalc(rest[0], index=index)]
480
480
  elif head == '_U_':
481
481
  return ['mu_._', ['Var', f'Z{index}'], ['_\\/_',
482
- self.ctl2mucalc(rest[1], index=index + 1),
483
- ['_/\\_', self.ctl2mucalc(rest[0], index=index + 1),
484
- [modop, ['Var', f'Z{index}']]]]]
482
+ self.ctl2mucalc(rest[1], index=index + 1),
483
+ ['_/\\_', self.ctl2mucalc(rest[0], index=index + 1),
484
+ [modop, ['Var', f'Z{index}']]]]]
485
485
  elif head == '_R_':
486
486
  return ['nu_._', ['Var', f'Z{index}'], ['_/\\_',
487
- self.ctl2mucalc(rest[1], index=index + 1),
488
- ['_\\/_', self.ctl2mucalc(rest[0], index=index + 1),
489
- [modop, ['Var', f'Z{index}']]]]]
487
+ self.ctl2mucalc(rest[1], index=index + 1),
488
+ ['_\\/_', self.ctl2mucalc(rest[0], index=index + 1),
489
+ [modop, ['Var', f'Z{index}']]]]]
490
490
  elif head == '`[`]_':
491
491
  return ['nu_._', ['Var', f'Z{index}'], ['_/\\_',
492
- self.ctl2mucalc(rest[0], index=index + 1),
493
- [modop, ['Var', f'Z{index}']]]]
492
+ self.ctl2mucalc(rest[0], index=index + 1),
493
+ [modop, ['Var', f'Z{index}']]]]
494
494
  elif head == '<>_':
495
495
  return ['mu_._', ['Var', f'Z{index}'], ['_\\/_',
496
- self.ctl2mucalc(rest[0], index=index + 1),
497
- [modop, ['Var', f'Z{index}']]]]
496
+ self.ctl2mucalc(rest[0], index=index + 1),
497
+ [modop, ['Var', f'Z{index}']]]]
498
498
  else:
499
499
  return [head] + [self.ctl2mucalc(arg, index=index) for arg in rest]
500
500
 
@@ -552,7 +552,7 @@ class LTSminRunner:
552
552
  maude_lib_ltsmin = os.getenv('MAUDE_LIB_LTSMIN')
553
553
  new_maude_lib = os.getenv('MAUDE_LIB', '') if maude_lib_ltsmin is None else maude_lib_ltsmin
554
554
  new_maude_lib = ('' if new_maude_lib == '' else new_maude_lib + os.pathsep) \
555
- + os.path.dirname(filename)
555
+ + os.path.dirname(filename)
556
556
 
557
557
  if raw:
558
558
  os.execve(self.ltsmin.pins2lts[variant], [self.ltsmin.pins2lts[variant]] + args,
umaudemc/command/check.py CHANGED
@@ -8,7 +8,7 @@ import sys
8
8
  import tempfile
9
9
 
10
10
  from ..common import parse_initial_data, usermsgs
11
- from ..backends import kleene_backends, get_backends, backend_for, format_statistics,\
11
+ from ..backends import kleene_backends, get_backends, backend_for, format_statistics, \
12
12
  advance_counterexample, advance_kleene
13
13
  from ..counterprint import SimplePrinter, JSONPrinter, HTMLPrinter, DOTPrinter, print_counterexample
14
14
  from ..formulae import Parser, collect_aprops, add_path_premise, formula_list2str
umaudemc/command/graph.py CHANGED
@@ -71,7 +71,7 @@ def deduce_format(args):
71
71
  usermsgs.print_warning('PDF output is only supported with DOT. Changing to DOT.')
72
72
  return 'dot'
73
73
 
74
- # Probabilitic information is not considered in some cases
74
+ # Probabilistic information is not considered in some cases
75
75
  if args.passign and oformat not in ('dot', 'prism', 'jani', 'default'):
76
76
  usermsgs.print_warning('Probabilistic graphs are only supported with PRISM, JANI and DOT output. '
77
77
  'Ignoring probabilities.')
@@ -107,7 +107,6 @@ def graph(args):
107
107
  else:
108
108
  kleene_graph = True
109
109
 
110
-
111
110
  # Select the appropriate rewriting graph
112
111
  # (probabilistic, strategy-controlled, or standard)
113
112
  if args.passign or oformat in ('prism', 'jani'):
@@ -50,6 +50,7 @@ def _print_fraction(value):
50
50
 
51
51
  return f'{num}/{den}' if num > 0 and den != 1 else num
52
52
 
53
+
53
54
  def _select_backend(known_backends, backend_list):
54
55
  """Get the first available backend according to the user preferences"""
55
56
 
@@ -78,7 +79,6 @@ def _select_backend(known_backends, backend_list):
78
79
  elif first_known is None:
79
80
  first_known = name
80
81
 
81
-
82
82
  if first_available is None:
83
83
  if first_known is None:
84
84
  usermsgs.print_error('The backend list does not contain any valid option.')
@@ -19,10 +19,10 @@ def show_results(program, nsims, qdata):
19
19
  qdata_it = iter(qdata)
20
20
  q = next(qdata_it, None)
21
21
 
22
- for k, (line, column, params) in enumerate(program.query_locations):
22
+ for k, (fname, line, column, params) in enumerate(program.query_locations):
23
23
  # Print the query name and location only if there are many
24
24
  if program.nqueries > 1:
25
- print(f'Query {k + 1} (line {line}:{column})')
25
+ print(f'Query {k + 1} ({fname}:{line}:{column})')
26
26
 
27
27
  # For parametric queries, we show the result for every value
28
28
  var = params[0] if params else None
@@ -136,7 +136,7 @@ def scheck(args):
136
136
  return 1
137
137
 
138
138
  with open(args.query) as quatex_file:
139
- program = parse_quatex(quatex_file, filename=args.query)
139
+ program, seen_files = parse_quatex(quatex_file, filename=args.query, legacy=args.assign == 'pmaude')
140
140
 
141
141
  if not program:
142
142
  return 1
@@ -145,14 +145,7 @@ def scheck(args):
145
145
  usermsgs.print_warning('No queries in the input file.')
146
146
  return 0
147
147
 
148
- # Get the simulator for the given assignment method
149
- simulator = get_simulator(args.assign, data)
150
-
151
- if not simulator:
152
- return 1
153
-
154
148
  # Check the simulation parameters
155
-
156
149
  if not (0 <= args.alpha <= 1):
157
150
  usermsgs.print_error(f'Wrong value {args.alpha} for the alpha parameter (must be between 0 and 1).')
158
151
  return 1
@@ -170,10 +163,26 @@ def scheck(args):
170
163
  if min_sim is None and max_sim is None:
171
164
  return 1
172
165
 
173
- # Call the statistical model checker
174
- num_sims, qdata = check(program, simulator,
175
- args.seed, args.alpha, args.delta, args.block,
176
- min_sim, max_sim, args.jobs, args.verbose)
166
+ # Distributed computations follow a different path
167
+ if args.distribute:
168
+ from ..distributed import distributed_check
169
+
170
+ num_sims, qdata = distributed_check(args, data, min_sim, max_sim, program, seen_files)
171
+
172
+ if num_sims is None:
173
+ return 1
174
+
175
+ else:
176
+ # Get the simulator for the given assignment method
177
+ simulator = get_simulator(args.assign, data)
178
+
179
+ if not simulator:
180
+ return 1
181
+
182
+ # Call the statistical model checker
183
+ num_sims, qdata = check(program, simulator,
184
+ args.seed, args.alpha, args.delta, args.block,
185
+ min_sim, max_sim, args.jobs, args.verbose)
177
186
 
178
187
  # Print the results on the terminal
179
188
  (show_json if args.format == 'json' else show_results)(program, num_sims, qdata)
@@ -0,0 +1,185 @@
1
+ #
2
+ # Command for a worker of the statistical model checker
3
+ #
4
+
5
+ import json
6
+ import multiprocessing as mp
7
+ import os
8
+ import socket
9
+ import sys
10
+ import tarfile
11
+ import tempfile
12
+ from array import array
13
+
14
+ from ..common import maude, parse_initial_data, usermsgs
15
+ from ..quatex import parse_quatex
16
+ from ..simulators import get_simulator
17
+ from ..statistical import run, QueryData, make_parameter_dicts
18
+
19
+ # Python-version-dependent option for safer TAR extraction
20
+ EXTRACT_OPTIONS = {'filter': 'data'} if sys.version_info >= (3, 12) else {}
21
+
22
+
23
+ def read_json(sock):
24
+ """Read a JSON value from a socket"""
25
+
26
+ # Read a 32 bit integer for the value length in bytes
27
+ if not (data := sock.recv(4)):
28
+ return None
29
+
30
+ length = int.from_bytes(data, 'big')
31
+
32
+ # Read the JSON to a byte string
33
+ data = b''
34
+
35
+ while len(data) < length:
36
+ data += sock.recv(length)
37
+
38
+ return json.loads(data.decode())
39
+
40
+
41
+ class Dummy:
42
+ """Dummy class to be used as a namespace"""
43
+
44
+ def __init__(self, values):
45
+ self.__dict__ = values
46
+
47
+
48
+ class Worker:
49
+ """Worker for simulations"""
50
+
51
+ def __init__(self):
52
+ self.program = None
53
+ self.simulator = None
54
+ self.block = 100
55
+
56
+ def setup(self, tmp_dir, args):
57
+ """Setup the execution environment"""
58
+
59
+ # Copy parameters
60
+ self.block = args.block
61
+
62
+ maude.setRandomSeed(args.seed)
63
+
64
+ # Do the same as the scheck command without checks
65
+ args.file = os.path.join(tmp_dir, args.file)
66
+
67
+ if not (data := parse_initial_data(args)):
68
+ return False
69
+
70
+ with open(os.path.join(tmp_dir, args.query)) as quatex_file:
71
+ self.program, _ = parse_quatex(quatex_file, filename=args.query,
72
+ legacy=args.assign == 'pmaude')
73
+
74
+ if not self.program:
75
+ return False
76
+
77
+ # Get the simulator for the given assignment method
78
+ self.simulator = get_simulator(args.assign, data)
79
+
80
+ if not self.simulator:
81
+ return False
82
+
83
+ return True
84
+
85
+ def run(self, conn):
86
+ """Run the simulation until it is finished"""
87
+
88
+ program = self.program
89
+ simulator = self.simulator
90
+ block = self.block
91
+
92
+ # Query data
93
+ qdata = [QueryData(k, idict)
94
+ for k, qinfo in enumerate(program.query_locations)
95
+ for idict in make_parameter_dicts(qinfo[3])]
96
+
97
+ sums = array('d', [0.0] * len(qdata))
98
+ sum_sq = array('d', [0.0] * len(qdata))
99
+
100
+ while True:
101
+
102
+ for _ in range(block):
103
+ # Run the simulation and compute all queries at once
104
+ values = run(program, qdata, simulator)
105
+
106
+ for k in range(len(qdata)):
107
+ sums[k] += values[k]
108
+ sum_sq[k] += values[k] * values[k]
109
+
110
+ conn.send(b'b' + sums.tobytes() + sum_sq.tobytes())
111
+
112
+ # Check whether to continue
113
+ answer = conn.recv(1)
114
+
115
+ if answer == b's':
116
+ print('Done')
117
+ return
118
+
119
+ elif answer != b'c':
120
+ usermsgs.print_error(f'Unknown command {answer.decode()}. Stopping.')
121
+ return
122
+
123
+ for k in range(len(qdata)):
124
+ sums[k] = 0
125
+ sum_sq[k] = 0
126
+
127
+
128
+ def handle_request(message, conn, addr, keep_file):
129
+ """Handle a request in a separate process"""
130
+
131
+ command = Dummy(message)
132
+
133
+ with tempfile.TemporaryDirectory(delete=not keep_file) as tmp_dir:
134
+ # Print the temporary working directory for debugging purposes
135
+ if keep_file:
136
+ print('Temporary directory:', tmp_dir)
137
+
138
+ # Recover the required files
139
+ with conn.makefile('rb', buffering=0) as fobj:
140
+ with tarfile.open(mode='r|*', fileobj=fobj) as tarf:
141
+ tarf.extractall(tmp_dir, **EXTRACT_OPTIONS)
142
+
143
+ # Setup a worker object
144
+ worker = Worker()
145
+
146
+ if not worker.setup(tmp_dir, command):
147
+ usermsgs.print_error(f'{addr}: bad request from scheck')
148
+ conn.send(b'e')
149
+ return
150
+
151
+ # Send confirmation
152
+ conn.send(b'o')
153
+
154
+ # Wait for start signal
155
+ conn.recv(1)
156
+
157
+ worker.run(conn)
158
+
159
+
160
+ def sworker(args):
161
+ """Worker for distributed statistical model checking"""
162
+
163
+ # Parse the listing address
164
+ try:
165
+ with socket.create_server((args.address, args.port), backlog=1) as sock:
166
+ while True:
167
+ print(f'👂 Listening on {args.address}:{args.port}...')
168
+ conn, addr = sock.accept()
169
+
170
+ usermsgs.print_info(f'Accepted connection from {":".join(map(str, addr))}.')
171
+
172
+ while True:
173
+ # Read the initiation message
174
+ if (message := read_json(conn)) is None:
175
+ break
176
+
177
+ # A separate process to cleanup Maude state
178
+ process = mp.Process(target=handle_request, args=(message, conn, addr, args.keep_file))
179
+ process.start()
180
+ process.join()
181
+
182
+ except KeyboardInterrupt:
183
+ print('Server closed by the user.')
184
+
185
+ return 0
umaudemc/command/test.py CHANGED
@@ -14,7 +14,7 @@ import sys # To find the Python executable path
14
14
  import threading # To support timeouts
15
15
  import time # To measure time
16
16
 
17
- from ..common import maude, usermsgs
17
+ from ..common import maude, usermsgs, load_specification
18
18
  from ..formulae import Parser, collect_aprops
19
19
  from ..backends import supported_logics, get_backends, backend_for
20
20
  from ..terminal import terminal as tmn
@@ -62,67 +62,7 @@ class TestCase:
62
62
  def load_cases(filename):
63
63
  """Load test cases from YAML or JSON specifications"""
64
64
 
65
- extension = os.path.splitext(filename)[1]
66
-
67
- # The YAML package is only loaded when needed
68
- # (pyaml is an optional dependency)
69
- if extension in ('.yaml', '.yml'):
70
- try:
71
- import yaml
72
- from yaml.loader import SafeLoader
73
-
74
- except ImportError:
75
- usermsgs.print_error(
76
- 'Cannot load cases from YAML file, since the yaml package is not installed.\n'
77
- 'Please convert the YAML to JSON or install it with pip install pyaml.')
78
- return None
79
-
80
- # The YAML loader is replaced so that entities have its line number
81
- # associated to print more useful messages. This is not possible with
82
- # the standard JSON library.
83
-
84
- class SafeLineLoader(SafeLoader):
85
- def construct_mapping(self, node, deep=False):
86
- mapping = super(SafeLineLoader, self).construct_mapping(node, deep=deep)
87
- # Add 1 so line numbering starts at 1
88
- mapping['__line__'] = node.start_mark.line + 1
89
- return mapping
90
-
91
- try:
92
- with open(filename) as caspec:
93
- return yaml.load(caspec, Loader=SafeLineLoader)
94
-
95
- except yaml.error.YAMLError as ype:
96
- usermsgs.print_error(f'Error while parsing test file: {ype}.')
97
-
98
- # TOML format
99
- if extension == '.toml':
100
- try:
101
- import tomllib
102
-
103
- except ImportError:
104
- usermsgs.print_error(
105
- 'Cannot load cases from TOML file, '
106
- 'which is only available since Python 3.10.')
107
- return None
108
-
109
- try:
110
- with open(filename) as caspec:
111
- return tomllib.load(caspec)
112
-
113
- except tomllib.TOMLDecodeError as tde:
114
- usermsgs.print_error(f'Error while parsing test file: {tde}.')
115
-
116
- # JSON format
117
- else:
118
- try:
119
- with open(filename) as caspec:
120
- return json.load(caspec)
121
-
122
- except json.JSONDecodeError as jde:
123
- usermsgs.print_error(f'Error while parsing test file: {jde}.')
124
-
125
- return None
65
+ return load_specification(filename, 'cases')
126
66
 
127
67
 
128
68
  def read_suite(filename, from_file=None, skip_case=0):
@@ -280,7 +220,7 @@ def _depends_on_vars(node, variables):
280
220
 
281
221
 
282
222
  class ParameterSet:
283
- """Assignement to parameters to be replaced on test attributes"""
223
+ """Assignment to parameters to be replaced on test attributes"""
284
224
 
285
225
  def __init__(self, dic, is_subs=None):
286
226
  # Dictionary with the parameter assignments
umaudemc/common.py CHANGED
@@ -18,7 +18,8 @@ if not hasattr(maude.Term, 'getVarName'):
18
18
  usermsgs.print_warning('Version 1.0 of the maude package adds some useful features for this program.\n'
19
19
  'Please update.')
20
20
 
21
- maude.Term.getVarName = lambda self: str(self).split(':')[0] if self.symbol() == self.symbol().getModule().parseTerm(f'$$$:{self.getSort()}').symbol() else None
21
+ maude.Term.getVarName = lambda self: str(self).split(':')[0] if self.symbol() == \
22
+ self.symbol().getModule().parseTerm(f'$$$:{self.getSort()}').symbol() else None
22
23
  maude.Term.isVariable = lambda self: self.getVarName() is None
23
24
 
24
25
 
@@ -43,27 +44,43 @@ def find_maude_file_abs(filename):
43
44
  return None
44
45
 
45
46
 
46
- def find_maude_file(filename):
47
- """Find a Maude file taking MAUDE_LIB into account"""
48
- if os.path.isabs(filename):
49
- return find_maude_file_abs(filename)
47
+ class MaudeFileFinder:
48
+ """Locate Maude files as Maude does"""
50
49
 
51
- # Maude also considers the current working directory
52
- # and the directory of the Maude binary
53
- paths = [os.getcwd(), os.path.dirname(maude.__file__)]
50
+ MAUDE_STD = {'file', 'linear', 'machine-int', 'metaInterpreter', 'model-checker',
51
+ 'prelude', 'prng', 'process', 'smt', 'socket', 'term-order', 'time'}
54
52
 
55
- maudelib = os.getenv('MAUDE_LIB')
56
53
 
57
- if maudelib is not None:
58
- paths += maudelib.split(os.pathsep)
54
+ def __init__(self):
55
+ # Maude also considers the current working directory
56
+ # and the directory of the Maude binary
57
+ self.paths = [os.path.dirname(maude.__file__),
58
+ *os.getenv('MAUDE_LIB', '').split(os.pathsep)]
59
59
 
60
- for path in paths:
61
- abspath = os.path.join(path, filename)
62
- fullname = find_maude_file_abs(abspath)
63
- if fullname is not None:
64
- return fullname
60
+ def _is_std(self, name):
61
+ """Is this a file from the Maude distribution"""
65
62
 
66
- return None
63
+ return name in self.MAUDE_STD or f'{name}.maude' in self.MAUDE_STD
64
+
65
+ def find(self, name, cwd):
66
+ # Absolute path, no ambiguity
67
+ if os.path.isabs(name):
68
+ return find_maude_file_abs(name), False
69
+
70
+ for path in (cwd, *self.paths):
71
+ abspath = os.path.join(path, name)
72
+ fullname = find_maude_file_abs(abspath)
73
+
74
+ if fullname is not None:
75
+ return fullname, path != cwd and self._is_std(name)
76
+
77
+ return None, None
78
+
79
+
80
+ def find_maude_file(filename):
81
+ """Find a Maude file taking MAUDE_LIB into account"""
82
+
83
+ return MaudeFileFinder().find(filename, os.getcwd())[0]
67
84
 
68
85
 
69
86
  def parse_initial_data(args):
@@ -190,3 +207,71 @@ def default_model_settings(logic, purge_fails, merge_states, strategy, tableau=F
190
207
  merge_states = 'state' if logic.startswith('CTL') else ('edge' if logic == 'Mucalc' else 'no')
191
208
 
192
209
  return purge_fails, merge_states
210
+
211
+
212
+ def load_specification(filename, topic):
213
+ """Load specifications from YAML or JSON files"""
214
+
215
+ extension = os.path.splitext(filename)[1]
216
+
217
+ # The YAML package is only loaded when needed
218
+ # (pyaml is an optional dependency)
219
+ if extension in ('.yaml', '.yml'):
220
+ try:
221
+ import yaml
222
+ from yaml.loader import SafeLoader
223
+
224
+ except ImportError:
225
+ usermsgs.print_error(
226
+ f'Cannot load {topic} from YAML file, since the yaml package is not installed.\n'
227
+ 'Please convert the YAML to JSON or install it with pip install pyaml.')
228
+ return None
229
+
230
+ # The YAML loader is replaced so that entities have its line number
231
+ # associated to print more useful messages. This is not possible with
232
+ # the standard JSON library.
233
+
234
+ class SafeLineLoader(SafeLoader):
235
+ def construct_mapping(self, node, deep=False):
236
+ mapping = super(SafeLineLoader, self).construct_mapping(node, deep=deep)
237
+ # Add 1 so line numbering starts at 1
238
+ mapping['__line__'] = node.start_mark.line + 1
239
+ return mapping
240
+
241
+ try:
242
+ with open(filename) as caspec:
243
+ return yaml.load(caspec, Loader=SafeLineLoader)
244
+
245
+ except yaml.error.YAMLError as ype:
246
+ usermsgs.print_error(f'Error while parsing {topic} file: {ype}.')
247
+
248
+ # TOML format
249
+ if extension == '.toml':
250
+ try:
251
+ import tomllib
252
+
253
+ except ImportError:
254
+ usermsgs.print_error(
255
+ f'Cannot load {topic} from TOML file, '
256
+ 'which is only available since Python 3.10.')
257
+ return None
258
+
259
+ try:
260
+ with open(filename, 'rb') as caspec:
261
+ return tomllib.load(caspec)
262
+
263
+ except tomllib.TOMLDecodeError as tde:
264
+ usermsgs.print_error(f'Error while parsing {topic} file: {tde}.')
265
+
266
+ # JSON format
267
+ else:
268
+ import json
269
+
270
+ try:
271
+ with open(filename) as caspec:
272
+ return json.load(caspec)
273
+
274
+ except json.JSONDecodeError as jde:
275
+ usermsgs.print_error(f'Error while parsing {topic} file: {jde}.')
276
+
277
+ return None
umaudemc/data/select.htm CHANGED
@@ -85,7 +85,7 @@
85
85
  <option value="metadata">Weighted by metadata</option>
86
86
  <option value="strategy">Probabilistic strategy</option>
87
87
  </select>
88
- <input type="checkbox" id="mdp" style="margin-left: 1.5ex; display: none;"></input>
88
+ <input type="checkbox" id="mdp" style="margin-left: 1.5ex; display: none;"/>
89
89
  <label for="mdp" style="display: none;">MDP</label>
90
90
  <label for="parg" style="margin-left: 1.5ex; display: none;">Weight term:</label>
91
91
  <input type="text" id="parg" style="flex-grow: 1; display: none;" onchange="buttonToggle()"