Skip to content

Commit a241feb

Browse files
authored
Merge pull request #115 from tomc271/bout_v6_coordinates_upgrader
Add bout_v6_coordinates_upgrader script
2 parents 4a1a8c4 + c6e7629 commit a241feb

File tree

2 files changed

+230
-1
lines changed

2 files changed

+230
-1
lines changed

src/boutupgrader/__init__.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from .bout_v5_macro_upgrader import add_parser as add_macro_parser
1010
from .bout_v5_physics_model_upgrader import add_parser as add_model_parser
1111
from .bout_v5_xzinterpolation_upgrader import add_parser as add_xzinterp_parser
12+
from .bout_v6_coordinates_upgrader import add_parser as add_v6_coordinates_parser
1213

1314
try:
1415
# This gives the version if the boututils package was installed
@@ -52,19 +53,24 @@ def main():
5253
v4_subcommand = subcommand.add_parser(
5354
"v4", help="BOUT++ v4 upgrades"
5455
).add_subparsers(title="v4 subcommands", required=True)
55-
add_3to4_parser(v4_subcommand, common_args, files_args)
5656

5757
v5_subcommand = subcommand.add_parser(
5858
"v5", help="BOUT++ v5 upgrades"
5959
).add_subparsers(title="v5 subcommands", required=True)
6060

61+
v6_subcommand = subcommand.add_parser(
62+
"v6", help="BOUT++ v6 upgrades"
63+
).add_subparsers(title="v6 subcommands", required=True)
64+
65+
add_3to4_parser(v4_subcommand, common_args, files_args)
6166
add_factory_parser(v5_subcommand, common_args, files_args)
6267
add_format_parser(v5_subcommand, common_args, files_args)
6368
add_header_parser(v5_subcommand, common_args)
6469
add_input_parser(v5_subcommand, common_args, files_args)
6570
add_macro_parser(v5_subcommand, common_args, files_args)
6671
add_model_parser(v5_subcommand, common_args, files_args)
6772
add_xzinterp_parser(v5_subcommand, common_args, files_args)
73+
add_v6_coordinates_parser(v6_subcommand, common_args, files_args)
6874

6975
args = parser.parse_args()
7076
args.func(args)
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
#!/usr/bin/env python3
2+
import argparse
3+
import copy
4+
import pathlib
5+
import re
6+
import textwrap
7+
8+
from .common import apply_or_display_patch
9+
10+
# find lines like: c->g_11 = x; and c.g_11 = x;
11+
SETTING_METRIC_COMPONENT_REGEX = re.compile(
12+
r"(\b.+\-\>|\.)" # arrow or dot (-> or .)
13+
r"(g_?)(\d\d)" # g12 or g_12, etc
14+
r"\s?\=\s?" # equals (maybe with spaces)
15+
r"(.+)" # anything
16+
r"(?=;)" # followed by ;
17+
)
18+
19+
# c->g11, etc
20+
GETTING_METRIC_COMPONENT_REGEX = re.compile(
21+
r"(\b\w+->|\.)" # e.g. coord. or coord->
22+
r"(?P<component>g_?\d\d)" # g12 or g_12, etc
23+
)
24+
25+
# find the string `geometry()`
26+
GEOMETRY_METHOD_CALL_REGEX = re.compile(r"geometry\(\)")
27+
28+
29+
def add_parser(subcommand, default_args, files_args):
30+
31+
help_text = textwrap.dedent(
32+
"""\
33+
Upgrade files to use the refactored Coordinates class.
34+
35+
For example, changes coords->dx to coords->dx()
36+
"""
37+
)
38+
parser = subcommand.add_parser(
39+
"v6_upgrader",
40+
help=help_text,
41+
formatter_class=argparse.RawDescriptionHelpFormatter,
42+
description=help_text,
43+
parents=[default_args, files_args],
44+
)
45+
parser.set_defaults(func=run)
46+
47+
48+
def run(args):
49+
for filename in args.files:
50+
try:
51+
contents = pathlib.Path(filename).read_text()
52+
except Exception as e:
53+
error_message = textwrap.indent(f"{e}", " ")
54+
print(f"Error reading {filename}:\n\n{error_message}")
55+
continue
56+
57+
original = copy.deepcopy(contents)
58+
modified_contents = modify(contents)
59+
60+
apply_or_display_patch(
61+
filename,
62+
original,
63+
modified_contents,
64+
args.patch_only,
65+
args.quiet,
66+
args.force,
67+
)
68+
69+
return modified_contents
70+
71+
72+
def modify(original_string):
73+
using_new_metric_accessor_methods = use_metric_accessors(original_string)
74+
without_geometry_calls = remove_geometry_calls(using_new_metric_accessor_methods)
75+
without_geometry_calls.append("") # insert a blank line at the end of the file
76+
lines_as_single_string = "\n".join(without_geometry_calls)
77+
modified_contents = replace_one_line_cases(lines_as_single_string)
78+
return modified_contents
79+
80+
81+
def indices_of_matching_lines(pattern, lines):
82+
search_result_for_all_lines = [pattern.search(line) for line in lines]
83+
matches = [x for x in search_result_for_all_lines if x is not None]
84+
return [lines.index(match.string) for match in matches]
85+
86+
87+
def use_metric_accessors(original_string):
88+
89+
lines = original_string.splitlines()
90+
91+
line_matches = SETTING_METRIC_COMPONENT_REGEX.findall(original_string)
92+
93+
if len(line_matches) == 0:
94+
return lines
95+
96+
metric_components = {match[1] + match[2]: match[3] for match in line_matches}
97+
lines_to_remove = indices_of_matching_lines(SETTING_METRIC_COMPONENT_REGEX, lines)
98+
lines_removed_count = 0
99+
for line_index in lines_to_remove:
100+
del lines[line_index - lines_removed_count]
101+
lines_removed_count += 1
102+
metric_components_with_value = {
103+
key: value for key, value in metric_components.items() if value is not None
104+
}
105+
newline_inserted = False
106+
for key, value in metric_components_with_value.items().__reversed__():
107+
# Replace `c->g11` with `g11`, etc
108+
new_value = GETTING_METRIC_COMPONENT_REGEX.sub(r"\g<component>", value)
109+
if not key.startswith("g_") and not newline_inserted:
110+
lines.insert(lines_to_remove[0], "")
111+
newline_inserted = True
112+
local_variable_line = rf" const auto {key} = {new_value};"
113+
lines.insert(lines_to_remove[0], local_variable_line)
114+
# insert a blank line
115+
lines.insert(lines_to_remove[0] + len(metric_components_with_value) + 1, "")
116+
coordinates_name_and_arrow = line_matches[0][0]
117+
new_metric_tensor_setter = (
118+
f" {coordinates_name_and_arrow}setMetricTensor(ContravariantMetricTensor(g11, g22, g33, g12, g13, g23),\n"
119+
f" CovariantMetricTensor(g_11, g_22, g_33, g_12, g_13, g_23));"
120+
)
121+
lines.insert(
122+
lines_to_remove[0] + len(metric_components_with_value) + 2,
123+
new_metric_tensor_setter,
124+
)
125+
del lines[lines_to_remove[-1] + 3]
126+
return lines
127+
128+
129+
def remove_geometry_calls(lines):
130+
# Remove lines calling geometry()
131+
lines_to_remove = indices_of_matching_lines(GEOMETRY_METHOD_CALL_REGEX, lines)
132+
for line_index in lines_to_remove:
133+
# If both the lines above and below are blank then remove one of them
134+
if lines[line_index - 1].strip() == "" and lines[line_index + 1].strip() == "":
135+
del lines[line_index + 1]
136+
del lines[line_index]
137+
return lines
138+
139+
140+
def assignment_regex_pairs(var):
141+
142+
arrow_or_dot = r"\b.+\-\>|\."
143+
not_followed_by_equals = r"(?!\s?=)"
144+
equals_something = r"\=\s?(.+)(?=;)"
145+
146+
def replacement_for_assignment(match):
147+
coord_and_arrow_or_dot = match.groups()[0]
148+
variable_name = match.groups()[1]
149+
capitalised_name = variable_name[0].upper() + variable_name[1:]
150+
value = match.groups()[2]
151+
return rf"{coord_and_arrow_or_dot}set{capitalised_name}({value})"
152+
153+
def replacement_for_division_assignment(match):
154+
coord_and_arrow_or_dot = match.groups()[0]
155+
variable_name = match.groups()[1]
156+
capitalised_name = variable_name[0].upper() + variable_name[1:]
157+
value = match.groups()[2]
158+
denominator = (
159+
f"{value}" if value[0] == "(" and value[-1] == ")" else f"({value})"
160+
)
161+
return rf"{coord_and_arrow_or_dot}set{capitalised_name}({coord_and_arrow_or_dot}{variable_name} / {denominator})"
162+
163+
return [
164+
# Replace `->var =` with `->setVar()`, etc
165+
(rf"({arrow_or_dot})({var})\s?{equals_something}", replacement_for_assignment),
166+
# Replace `foo->var /= bar` with `foo->setVar(foo->var() / (bar))`
167+
(
168+
rf"({arrow_or_dot})({var})\s?\/{equals_something}",
169+
replacement_for_division_assignment,
170+
),
171+
# Replace `c->var` with `c->var()` etc, but not if is assignment
172+
(rf"({arrow_or_dot})({var})(?!\(){not_followed_by_equals}", r"\1\2()"),
173+
]
174+
175+
176+
def mesh_get_pattern_and_replacement():
177+
178+
# Convert `mesh->get(coord->dx(), "dx")` to `coord->setDx(mesh->get("dx"));`, etc
179+
180+
def replacement_for_assignment_with_mesh_get(match):
181+
arrow_or_dot = match.groups()[0]
182+
coords = match.groups()[1]
183+
variable_name = match.groups()[3]
184+
new_value = match.groups()[4]
185+
capitalised_name = variable_name[0].upper() + variable_name[1:]
186+
return rf"{coords}{arrow_or_dot}set{capitalised_name}(mesh->get({new_value}))"
187+
188+
arrow_or_dot = r"\-\>|\."
189+
190+
mesh_get_pattern_replacement = (
191+
rf"mesh({arrow_or_dot})get\((\w+)({arrow_or_dot})(\w+)\(?\)?, (\"\w+\")\)",
192+
replacement_for_assignment_with_mesh_get,
193+
)
194+
return mesh_get_pattern_replacement
195+
196+
197+
# Deal with the basic find-and-replace cases that do not involve multiple lines
198+
def replace_one_line_cases(modified):
199+
200+
metric_component = r"g_?\d\d"
201+
mesh_spacing = r"d[xyz]"
202+
203+
patterns_with_replacements = (
204+
assignment_regex_pairs(metric_component)
205+
+ assignment_regex_pairs(mesh_spacing)
206+
+ assignment_regex_pairs("Bxy")
207+
+ assignment_regex_pairs("J")
208+
+ assignment_regex_pairs("IntShiftTorsion")
209+
+ assignment_regex_pairs("G1")
210+
+ assignment_regex_pairs("G2")
211+
+ assignment_regex_pairs("G3")
212+
)
213+
214+
patterns_with_replacements.append(mesh_get_pattern_and_replacement())
215+
216+
for pattern, replacement in patterns_with_replacements:
217+
compiled_pattern = re.compile(pattern)
218+
MAX_OCCURRENCES = 12
219+
count = 0
220+
while compiled_pattern.search(modified) and count < MAX_OCCURRENCES:
221+
count += 1
222+
modified = compiled_pattern.sub(replacement, modified)
223+
return modified

0 commit comments

Comments
 (0)