Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/boutupgrader/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from .bout_v5_xzinterpolation_upgrader import add_parser as add_xzinterp_parser
from .bout_v6_coordinates_upgrader import add_parser as add_v6_coordinates_parser
from .bout_v6_input_file_upgrader import add_parser as add_v6_input_parser
from .hermes_collisions_input_file_upgrader import add_parser as add_hermes_input_parser

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

hermes_subcommand = subcommand.add_parser(
"hermes", help="Hermes-3 subcommands"
).add_subparsers(title="hermes subcommands", required=True)

add_3to4_parser(v4_subcommand, common_args, files_args)
add_factory_parser(v5_subcommand, common_args, files_args)
add_format_parser(v5_subcommand, common_args, files_args)
Expand All @@ -73,6 +78,7 @@ def main():
add_xzinterp_parser(v5_subcommand, common_args, files_args)
add_v6_coordinates_parser(v6_subcommand, common_args, files_args)
add_v6_input_parser(v6_subcommand, common_args, files_args)
add_hermes_input_parser(hermes_subcommand, common_args, files_args)

args = parser.parse_args()
args.func(args)
18 changes: 11 additions & 7 deletions src/boutupgrader/bout_v5_input_file_upgrader.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ def remove_deleted(deleted, options_file):
del options_file[key]


def apply_fixes(replacements, deleted, options_file):
def apply_fixes(replacements, deleted, options_file, additional_modifications):
"""Apply all fixes in this module"""

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

remove_deleted(deleted, modified)

additional_modifications(modified)

return modified


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


def add_parser_general(subcommand, default_args, files_args, run):
def add_parser_general(subcommand, default_args, files_args, run, name):
parser = subcommand.add_parser(
"input",
formatter_class=argparse.RawDescriptionHelpFormatter,
help="Fix input files",
description=textwrap.dedent(
"""\
Fix input files for BOUT++ v5+
f"""\
Fix input files for {name}

Please note that this will only fix input options in sections with
standard or default names. You may also need to fix options in custom
Expand Down Expand Up @@ -290,7 +292,7 @@ def add_parser_general(subcommand, default_args, files_args, run):
parser.set_defaults(func=run)


def run_general(REPLACEMENTS, DELETED, args):
def run_general(REPLACEMENTS, DELETED, args, *, additional_modifications=None):
from boutdata.data import BoutOptions, BoutOptionsFile

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

try:
modified = apply_fixes(REPLACEMENTS, DELETED, original)
modified = apply_fixes(
REPLACEMENTS, DELETED, original, additional_modifications
)
except RuntimeError as e:
print(e)
continue
Expand All @@ -347,4 +351,4 @@ def run(args):


def add_parser(subcommand, default_args, files_args):
return add_parser_general(subcommand, default_args, files_args, run)
return add_parser_general(subcommand, default_args, files_args, run, "BOUT++ v5+")
2 changes: 1 addition & 1 deletion src/boutupgrader/bout_v6_input_file_upgrader.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ def run(args):


def add_parser(subcommand, default_args, files_args):
return add_parser_general(subcommand, default_args, files_args, run)
return add_parser_general(subcommand, default_args, files_args, run, "BOUT++ v6+")
145 changes: 145 additions & 0 deletions src/boutupgrader/hermes_collisions_input_file_upgrader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
from itertools import chain
from typing import TypedDict
from warnings import warn

from boutdata.data import BoutOptionsFile

from .bout_v5_input_file_upgrader import add_parser_general, run_general


class Replacement(TypedDict):
old: str
new: str


REPLACEMENTS = []
DELETED = []

NEW_NAMES: dict[str, str | dict[str, list[str]]] = {
"collisions": {
"braginskii_collisions": [
"electron_electron",
"electron_ion",
"electron_neutral",
"ion_ion",
"ion_neutral",
"neutral_neutral",
"ei_multiplier",
"diagnose",
],
"braginskii_friction": ["frictional_heating", "diagnose"],
"braginskii_heat_exchange": ["diagnose"],
},
"electron_viscosity": "braginskii_electron_viscosity",
"ion_viscosity": "braginskii_ion_viscosity",
"thermal_force": "braginskii_thermal_force",
}

# If name in `components` and type not given then change the name in components and heading; if a multi-replacement, duplicate the section
# If type is same as name, duplicate section
# If name in type of any component, change it there (don't duplicate)


def split_list_string(list_string: str) -> tuple[list[str], bool]:
result = [tname.strip() for tname in list_string.split(",")]
open_paren = result[0][0] == "("
close_paren = result[-1][-1] == ")"
if open_paren != close_paren:
warn(f'Unmatched parentheses around "{list_string}"')
if open_paren:
result[0] = result[0][1:]
if close_paren:
result[-1] = result[-1][:-1]
return result, open_paren and close_paren


def rename_simple_component(
options_file: BoutOptionsFile, section_name: str
) -> list[str]:
"""Rename a component when its type is the same as its name."""
if section_name in NEW_NAMES:
new_names = NEW_NAMES[section_name]
if isinstance(new_names, dict):
old_section = options_file.getSection(section_name)
new_components = []
for new_name, configs in new_names.items():
new_components.append(new_name)
new_section = options_file.getSection(new_name)
for conf in configs:
if conf in old_section:
new_section[conf] = old_section[conf]
return new_components
else:
options_file.rename(section_name, new_names)
return [new_names]
return [section_name]


def update_component_names(options_file: BoutOptionsFile) -> None:
"""Change the names of closure-related components to reflect the refactor"""
has_collisions = False
recycling_component = ""
old_components, has_parens = split_list_string(options_file["hermes:components"])
new_components = []
for section_name in old_components:
section = options_file.getSection(section_name)
# Component type is set explicitly
if "type" in section:
old_types, types_have_parens = split_list_string(section["type"])
# If component name and type match, treat similarly to if no type were given (see below)
if len(old_types) == 1 and old_types[0] == section_name:
has_collisions = has_collisions or section_name == "collisions"
new_types = rename_simple_component(options_file, section_name)
for t in new_types:
options_file[f"{t}:type"] = (
("(" if types_have_parens else "")
+ t
+ (")" if types_have_parens else "")
)
new_components.extend(new_types)
# Otherwise simply replace any type-names that need changing
else:
has_collisions = has_collisions or section_name in old_types
new_types = list(
chain.from_iterable(
[nt] if isinstance((nt := NEW_NAMES.get(t, t)), str) else nt
for t in old_types
)
)
if new_types != old_types:
section["type"] = (
("(" if types_have_parens else "")
+ ", ".join(new_types)
+ (")" if types_have_parens else "")
)
new_components.append(section_name)
# Component type is same as component name
else:
has_collisions = has_collisions or section_name == "collisions"
new_types = rename_simple_component(options_file, section_name)
new_components.extend(new_types)
if "recycling" in new_types:
recycling_component = section_name
# Add braginskii_conduction to the end of the list of components
if has_collisions:
new_components.append("braginskii_conduction")
# Make sure recycling is evaluated after conduction
if recycling_component != "":
new_components.remove(recycling_component)
new_components.append(recycling_component)
if new_components != old_components:
options_file["hermes:components"] = (
("(" if has_parens else "")
+ ", ".join(new_components)
+ (")" if has_parens else "")
)


def run(args) -> None:
run_general(
REPLACEMENTS, DELETED, args, additional_modifications=update_component_names
)


def add_parser(subcommand, default_args, files_args):
return add_parser_general(subcommand, default_args, files_args, run, "Hermes-3")
Loading