v0.16.0
Loading...
Searching...
No Matches
param_file_to_json_config.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2
3import argparse
4import json
5import shlex
6import sys
7from pathlib import Path
8
9
10REJECT_OPTION_NAMES = {
11 "json_config",
12 "param_file",
13 "meshsets_config",
14}
15
16DEFAULT_PRIMARY_MESH_FILE = "mesh.h5m"
17
18# FIXME: one might need to edit this if custom prefix is defined for petsc options
19PETSC_PREFIXES = {
20 "dm",
21 "eps",
22 "fieldsplit",
23 "ksp",
24 "mat",
25 "mg",
26 "pc",
27 "pep",
28 "snes",
29 "st",
30 "sub",
31 "svd",
32 "tao",
33 "ts",
34}
35
36# Broadly reusable MoFEM options observed across tutorials and atom tests.
37# Keep this list focused on generic framework/example controls rather than
38# module-specific physics/material parameters.
39MOFEM_OPTION_NAMES = {
40 "atom_test",
41 "base",
42 "base_order",
43 "calculate_reactions",
44 "field_eval_coords",
45 "geom_order",
46 "is_atom_test",
47 "is_partitioned",
48 "log_no_color",
49 "log_quiet",
50 "log_sl",
51 "log_view",
52 "mofem_mg_coarse_order",
53 "mofem_mg_levels",
54 "mofem_mg_verbose",
55 "my_geom_order",
56 "my_is_partitioned",
57 "my_order",
58 "my_order_p",
59 "my_order_u",
60 "my_ref",
61 "my_ref_order",
62 "no_output",
63 "order",
64 "output_mesh_file",
65 "post_proc_skin",
66 "post_proc_vol",
67 "project_geometry",
68 "regression_test",
69 "save_every",
70 "save_step",
71 "space",
72 "space_order",
73 "test",
74 "test_num",
75 "test_tol",
76}
77
78
79def parse_scalar(value):
80 lower = value.lower()
81 if lower == "true":
82 return True
83 if lower == "false":
84 return False
85
86 try:
87 return int(value, 10)
88 except ValueError:
89 pass
90
91 try:
92 return float(value)
93 except ValueError:
94 return value
95
96
97def is_number_token(token):
98 try:
99 int(token, 10)
100 return True
101 except ValueError:
102 pass
103
104 try:
105 float(token)
106 return True
107 except ValueError:
108 return False
109
110
112 return token.startswith("-") and not is_number_token(token)
113
114
116 prefix = name.split("_", 1)[0]
117 if prefix in PETSC_PREFIXES:
118 return "petsc"
119 if name in MOFEM_OPTION_NAMES:
120 return "mofem"
121 return None
122
123
124def warn(message):
125 print(f"warning: {message}", file=sys.stderr)
126
127
129 expanded = []
130 for token in tokens:
131 stripped = token.strip()
132 if not stripped:
133 continue
134
135 if " " in token and stripped.startswith("-"):
136 try:
137 nested_tokens = shlex.split(token, posix=True)
138 except ValueError:
139 nested_tokens = []
140 if nested_tokens and nested_tokens[0].startswith("-"):
141 expanded.extend(nested_tokens)
142 continue
143
144 expanded.append(token)
145
146 return expanded
147
148
149def set_nested_option(tree, name, value):
150 parts = [part for part in name.split("_") if part]
151 if not parts:
152 raise ValueError("empty option name")
153
154 node = tree
155 for part in parts[:-1]:
156 existing = node.get(part)
157 if existing is None:
158 node[part] = {}
159 node = node[part]
160 continue
161 if not isinstance(existing, dict):
162 raise ValueError(
163 f"cannot create nested option '{name}' because '{part}' is already scalar"
164 )
165 node = existing
166
167 last = parts[-1]
168 existing = node.get(last)
169 if isinstance(existing, dict):
170 raise ValueError(
171 f"cannot assign scalar option '{name}' because it is already an object"
172 )
173 node[last] = value
174
175
176def parse_tokens(tokens):
177 parsed = []
178 index = 0
179 while index < len(tokens):
180 token = tokens[index]
181 if not token.startswith("-"):
182 raise ValueError(f"unexpected token '{token}'")
183
184 name = token.lstrip("-")
185 if not name:
186 raise ValueError("encountered empty option name")
187
188 value = True
189 if index + 1 < len(tokens) and not is_option_token(tokens[index + 1]):
190 value = parse_scalar(tokens[index + 1])
191 index += 1
192
193 parsed.append((name, value))
194 index += 1
195
196 return parsed
197
198
200 options = []
201 with path.open("r", encoding="utf-8") as handle:
202 for line_number, raw_line in enumerate(handle, start=1):
203 stripped = raw_line.strip()
204 if not stripped or stripped.startswith("#"):
205 continue
206
207 lexer = shlex.shlex(raw_line, posix=True)
208 lexer.whitespace_split = True
209 lexer.commenters = "#"
210 try:
211 tokens = list(lexer)
212 except ValueError as exc:
213 warn(f"{path}:{line_number}: failed to tokenize line: {exc}")
214 continue
215
216 if not tokens:
217 continue
218 tokens = expand_inline_option_strings(tokens)
219
220 try:
221 options.extend(parse_tokens(tokens))
222 except ValueError as exc:
223 warn(f"{path}:{line_number}: {exc}")
224 continue
225
226 return options
227
228
229def parse_inline_options(text, source="<inline options>"):
230 lexer = shlex.shlex(text, posix=True)
231 lexer.whitespace_split = True
232 lexer.commenters = "#"
233 try:
234 tokens = list(lexer)
235 except ValueError as exc:
236 warn(f"{source}: failed to tokenize input: {exc}")
237 return []
238
239 if not tokens:
240 return []
241
242 tokens = expand_inline_option_strings(tokens)
243
244 try:
245 return parse_tokens(tokens)
246 except ValueError as exc:
247 warn(f"{source}: {exc}")
248 return []
249
250
251def build_config(options, version):
252 primary_mesh_file = DEFAULT_PRIMARY_MESH_FILE
253 used_placeholder_mesh = True
254 python_script_files = []
255 config = {
256 "version": version,
257 "meshes": [],
258 "petsc": {},
259 "mofem": {},
260 }
261
262 for name, value in options:
263 if name == "file_name":
264 primary_mesh_file = str(value)
265 used_placeholder_mesh = False
266 continue
267
268 if name == "sdf_file":
269 python_script_files.append(str(value))
270 continue
271
272 if name in REJECT_OPTION_NAMES:
273 raise ValueError(
274 f"option '-{name}' is not supported by the converter; "
275 "remove it and configure that part manually"
276 )
277
278 section = classify_option(name)
279 if section is None:
280 warn(f"skipping unsupported option '-{name}' with value '{value}'")
281 continue
282 try:
283 set_nested_option(config[section], name, value)
284 except ValueError as exc:
285 warn(f"skipping '-{name}' with value '{value}': {exc}")
286 continue
287
288 config["meshes"].append(
289 {
290 "name": "primary",
291 "type": "MOAB_MESH",
292 "file_name": primary_mesh_file,
293 }
294 )
295
296 for index, python_script_file in enumerate(python_script_files, start=1):
297 config["meshes"].append(
298 {
299 "name": f"python_script_{index}",
300 "type": "PYTHON_SCRIPT",
301 "file_name": python_script_file,
302 }
303 )
304
305 return config, used_placeholder_mesh
306
307
309 parser = argparse.ArgumentParser(
310 description=(
311 "Convert a PETSc/MoFEM param file or raw option string into a JSON "
312 "config skeleton. The primary MOAB mesh comes from -file_name or "
313 "defaults to mesh.h5m."
314 )
315 )
316 parser.add_argument(
317 "input",
318 nargs="?",
319 help="input PETSc param file path or raw option string",
320 )
321 parser.add_argument(
322 "-o",
323 "--output",
324 type=Path,
325 help="write JSON config to this file instead of stdout",
326 )
327 parser.add_argument(
328 "--version",
329 default="0.15",
330 help="JSON config version to write (default: %(default)s)",
331 )
332 parser.add_argument(
333 "--indent",
334 type=int,
335 default=2,
336 help="JSON indentation level (default: %(default)s)",
337 )
338 return parser
339
340
341def main():
342 script_option_names = {"-h", "--help", "-o", "--output", "--version", "--indent"}
343 raw_argv = sys.argv[1:]
344
345 if raw_argv and raw_argv[0].startswith("-") and raw_argv[0] not in script_option_names:
346 args = argparse.Namespace(output=None, version="0.15", indent=2)
347 input_arg = " ".join(raw_argv)
348 else:
349 parser = create_argument_parser()
350 args, unknown_args = parser.parse_known_args()
351
352 input_arg = args.input
353 if input_arg is None and unknown_args:
354 input_arg = " ".join(unknown_args)
355
356 if input_arg is None:
357 parser.error("the following arguments are required: input")
358
359 try:
360 input_path = Path(input_arg)
361 if input_path.exists():
362 options = parse_param_file(input_path)
363 else:
364 options = parse_inline_options(input_arg)
365 config, used_placeholder_mesh = build_config(options, args.version)
366 except ValueError as exc:
367 print(exc, file=sys.stderr)
368 return 1
369
370 output = json.dumps(config, indent=args.indent)
371 if args.output:
372 args.output.write_text(output + "\n", encoding="utf-8")
373 else:
374 print(output)
375
376 if used_placeholder_mesh:
377 print(
378 f"no '-file_name' found; using placeholder primary mesh '{DEFAULT_PRIMARY_MESH_FILE}'",
379 file=sys.stderr,
380 )
381
382 return 0
383
384
385if __name__ == "__main__":
386 raise SystemExit(main())
parse_inline_options(text, source="<inline options>")