Skip to content

Commit 962d5a5

Browse files
authored
Merge pull request #137 from boutproject/cmacmackin/hermes_collisions
Add tool to upgrade collisions/closure in Hermes-3
2 parents f5d308e + 4a1293c commit 962d5a5

4 files changed

Lines changed: 163 additions & 8 deletions

File tree

src/boutupgrader/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from .bout_v5_xzinterpolation_upgrader import add_parser as add_xzinterp_parser
1212
from .bout_v6_coordinates_upgrader import add_parser as add_v6_coordinates_parser
1313
from .bout_v6_input_file_upgrader import add_parser as add_v6_input_parser
14+
from .hermes_collisions_input_file_upgrader import add_parser as add_hermes_input_parser
1415

1516
try:
1617
# This gives the version if the boututils package was installed
@@ -63,6 +64,10 @@ def main():
6364
"v6", help="BOUT++ v6 upgrades"
6465
).add_subparsers(title="v6 subcommands", required=True)
6566

67+
hermes_subcommand = subcommand.add_parser(
68+
"hermes_refactor_closure", help="Hermes-3 upgrade following closure refactor"
69+
).add_subparsers(title="hermes subcommands", required=True)
70+
6671
add_3to4_parser(v4_subcommand, common_args, files_args)
6772
add_factory_parser(v5_subcommand, common_args, files_args)
6873
add_format_parser(v5_subcommand, common_args, files_args)
@@ -73,6 +78,7 @@ def main():
7378
add_xzinterp_parser(v5_subcommand, common_args, files_args)
7479
add_v6_coordinates_parser(v6_subcommand, common_args, files_args)
7580
add_v6_input_parser(v6_subcommand, common_args, files_args)
81+
add_hermes_input_parser(hermes_subcommand, common_args, files_args)
7682

7783
args = parser.parse_args()
7884
args.func(args)

src/boutupgrader/bout_v5_input_file_upgrader.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ def remove_deleted(deleted, options_file):
203203
del options_file[key]
204204

205205

206-
def apply_fixes(replacements, deleted, options_file):
206+
def apply_fixes(replacements, deleted, options_file, additional_modifications):
207207
"""Apply all fixes in this module"""
208208

209209
modified = copy.deepcopy(options_file)
@@ -212,6 +212,8 @@ def apply_fixes(replacements, deleted, options_file):
212212

213213
remove_deleted(deleted, modified)
214214

215+
additional_modifications(modified)
216+
215217
return modified
216218

217219

@@ -236,14 +238,14 @@ def possibly_apply_patch(patch, options_file, quiet=False, force=False):
236238
return make_change
237239

238240

239-
def add_parser_general(subcommand, default_args, files_args, run):
241+
def add_parser_general(subcommand, default_args, files_args, run, name):
240242
parser = subcommand.add_parser(
241243
"input",
242244
formatter_class=argparse.RawDescriptionHelpFormatter,
243245
help="Fix input files",
244246
description=textwrap.dedent(
245-
"""\
246-
Fix input files for BOUT++ v5+
247+
f"""\
248+
Fix input files for {name}
247249
248250
Please note that this will only fix input options in sections with
249251
standard or default names. You may also need to fix options in custom
@@ -290,7 +292,7 @@ def add_parser_general(subcommand, default_args, files_args, run):
290292
parser.set_defaults(func=run)
291293

292294

293-
def run_general(REPLACEMENTS, DELETED, args):
295+
def run_general(REPLACEMENTS, DELETED, args, *, additional_modifications=None):
294296
from boutdata.data import BoutOptions, BoutOptionsFile
295297

296298
# Monkey-patch BoutOptions to make sure it's case sensitive
@@ -324,7 +326,9 @@ def run_general(REPLACEMENTS, DELETED, args):
324326
continue
325327

326328
try:
327-
modified = apply_fixes(REPLACEMENTS, DELETED, original)
329+
modified = apply_fixes(
330+
REPLACEMENTS, DELETED, original, additional_modifications
331+
)
328332
except RuntimeError as e:
329333
print(e)
330334
continue
@@ -347,4 +351,4 @@ def run(args):
347351

348352

349353
def add_parser(subcommand, default_args, files_args):
350-
return add_parser_general(subcommand, default_args, files_args, run)
354+
return add_parser_general(subcommand, default_args, files_args, run, "BOUT++ v5+")

src/boutupgrader/bout_v6_input_file_upgrader.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,4 @@ def run(args):
1919

2020

2121
def add_parser(subcommand, default_args, files_args):
22-
return add_parser_general(subcommand, default_args, files_args, run)
22+
return add_parser_general(subcommand, default_args, files_args, run, "BOUT++ v6+")
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
from itertools import chain
2+
from typing import TypedDict
3+
from warnings import warn
4+
5+
from boutdata.data import BoutOptionsFile
6+
7+
from .bout_v5_input_file_upgrader import add_parser_general, run_general
8+
9+
10+
class Replacement(TypedDict):
11+
old: str
12+
new: str
13+
14+
15+
REPLACEMENTS = []
16+
DELETED = []
17+
18+
NEW_NAMES: dict[str, str | dict[str, list[str]]] = {
19+
"collisions": {
20+
"braginskii_collisions": [
21+
"electron_electron",
22+
"electron_ion",
23+
"electron_neutral",
24+
"ion_ion",
25+
"ion_neutral",
26+
"neutral_neutral",
27+
"ei_multiplier",
28+
"diagnose",
29+
],
30+
"braginskii_friction": ["frictional_heating", "diagnose"],
31+
"braginskii_heat_exchange": ["diagnose"],
32+
},
33+
"electron_viscosity": "braginskii_electron_viscosity",
34+
"ion_viscosity": "braginskii_ion_viscosity",
35+
"thermal_force": "braginskii_thermal_force",
36+
}
37+
38+
# If name in `components` and type not given then change the name in components and heading; if a multi-replacement, duplicate the section
39+
# If type is same as name, duplicate section
40+
# If name in type of any component, change it there (don't duplicate)
41+
42+
43+
def split_list_string(list_string: str) -> tuple[list[str], bool]:
44+
result = [tname.strip() for tname in list_string.split(",")]
45+
open_paren = result[0][0] == "("
46+
close_paren = result[-1][-1] == ")"
47+
if open_paren != close_paren:
48+
warn(f'Unmatched parentheses around "{list_string}"')
49+
if open_paren:
50+
result[0] = result[0][1:]
51+
if close_paren:
52+
result[-1] = result[-1][:-1]
53+
return result, open_paren and close_paren
54+
55+
56+
def rename_simple_component(
57+
options_file: BoutOptionsFile, section_name: str
58+
) -> list[str]:
59+
"""Rename a component when its type is the same as its name."""
60+
if section_name in NEW_NAMES:
61+
new_names = NEW_NAMES[section_name]
62+
if isinstance(new_names, dict):
63+
old_section = options_file.pop(section_name)
64+
new_components = []
65+
for new_name, configs in new_names.items():
66+
new_components.append(new_name)
67+
new_section = options_file.getSection(new_name)
68+
for conf in configs:
69+
if conf in old_section:
70+
new_section[conf] = old_section[conf]
71+
return new_components
72+
else:
73+
options_file.rename(section_name, new_names)
74+
return [new_names]
75+
return [section_name]
76+
77+
78+
def update_component_names(options_file: BoutOptionsFile) -> None:
79+
"""Change the names of closure-related components to reflect the refactor"""
80+
has_collisions = False
81+
recycling_component = ""
82+
old_components, has_parens = split_list_string(options_file["hermes:components"])
83+
new_components = []
84+
for section_name in old_components:
85+
section = options_file.getSection(section_name)
86+
# Component type is set explicitly
87+
if "type" in section:
88+
old_types, types_have_parens = split_list_string(section["type"])
89+
# If component name and type match, treat similarly to if no type were given (see below)
90+
if len(old_types) == 1 and old_types[0] == section_name:
91+
has_collisions = has_collisions or section_name == "collisions"
92+
new_types = rename_simple_component(options_file, section_name)
93+
for t in new_types:
94+
options_file[f"{t}:type"] = (
95+
("(" if types_have_parens else "")
96+
+ t
97+
+ (")" if types_have_parens else "")
98+
)
99+
new_components.extend(new_types)
100+
# Otherwise simply replace any type-names that need changing
101+
else:
102+
has_collisions = has_collisions or section_name in old_types
103+
new_types = list(
104+
chain.from_iterable(
105+
[nt] if isinstance((nt := NEW_NAMES.get(t, t)), str) else nt
106+
for t in old_types
107+
)
108+
)
109+
if new_types != old_types:
110+
section["type"] = (
111+
("(" if types_have_parens else "")
112+
+ ", ".join(new_types)
113+
+ (")" if types_have_parens else "")
114+
)
115+
new_components.append(section_name)
116+
# Component type is same as component name
117+
else:
118+
has_collisions = has_collisions or section_name == "collisions"
119+
new_types = rename_simple_component(options_file, section_name)
120+
new_components.extend(new_types)
121+
if "recycling" in new_types:
122+
recycling_component = section_name
123+
# Add braginskii_conduction to the end of the list of components
124+
if has_collisions:
125+
new_components.append("braginskii_conduction")
126+
# Make sure recycling is evaluated after conduction
127+
if recycling_component != "":
128+
new_components.remove(recycling_component)
129+
new_components.append(recycling_component)
130+
if new_components != old_components:
131+
options_file["hermes:components"] = (
132+
("(" if has_parens else "")
133+
+ ", ".join(new_components)
134+
+ (")" if has_parens else "")
135+
)
136+
137+
138+
def run(args) -> None:
139+
run_general(
140+
REPLACEMENTS, DELETED, args, additional_modifications=update_component_names
141+
)
142+
143+
144+
def add_parser(subcommand, default_args, files_args):
145+
return add_parser_general(subcommand, default_args, files_args, run, "Hermes-3")

0 commit comments

Comments
 (0)