Skip to content

Solver options manager

solver_options_manager

DeleteParam

An empty class to indicate a solver option will be deleted.

Since None is a valid value for PETSc solver options, a separate object must be used to denote that a parameter is to be deleted from the solver configuration at init time. Use as follows:

stokes_solver = StokesSolver( ...
    solver_parameters_extra = { 'ksp_monitor' : DeleteParam }
)
The resulting stokes_solver.solver_parameters will not contain the ksp_monitor key.

SolverConfigurationMixin

Manage PETSc solver options in G-ADOPT.

This class is designed to be subclassed by the base class for any solvers included in G-ADOPT. It provides methods for handling and modifying solver parameters passed to Firedrake's [Non]LinearVariationalSolver solver object during initialisation of a G-ADOPT solver object.

reset_solver_config(default_config, extra_config=None)

Resets the solver_parameters dict.

Empties the existing solver_parameters dict and creates a new one by first running add_to_solver_config on the empty dict with default_config, and then again on the resulting dict with extra_config. default_config is mandatory, extra_config is optional. Will invoke callback_ref if it is set.

Source code in g-adopt/gadopt/solver_options_manager.py
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
def reset_solver_config(
    self,
    default_config: ConfigType,
    extra_config: ConfigType | None = None,
) -> None:
    """Resets the `solver_parameters` dict.

    Empties the existing `solver_parameters` dict and creates a new one by first
    running `add_to_solver_config` on the empty dict with `default_config`, and
    then again on the resulting dict with `extra_config`. `default_config` is
    mandatory, `extra_config` is optional. Will invoke `callback_ref` if it is set.
    """
    debug_print(self._class_name, "Input default solver configuration:")
    debug_print(self._class_name, pprint.pformat(default_config, indent=2))
    self.solver_parameters = {}
    debug_print(self._class_name, "Processing default config")
    self.add_to_solver_config(default_config, extra_config is None)
    if extra_config is not None:
        debug_print(self._class_name, "Processing extra config")
        self.add_to_solver_config(extra_config, True)

print_solver_config()

Prints the solver_parameters dict.

Uses pprint to write the final solver_paramseters dict in a human-readable way. Useful for debugging purposes when a user wishes to verify PETSc solver settings.

Source code in g-adopt/gadopt/solver_options_manager.py
90
91
92
93
94
95
96
97
def print_solver_config(self) -> None:
    """Prints the solver_parameters dict.

    Uses pprint to write the final `solver_paramseters` dict in a human-readable
    way. Useful for debugging purposes when a user wishes to verify PETSc solver
    settings.
    """
    pprint.pprint(self.solver_parameters, indent=2)

register_update_callback(callback)

Register a function to call whenever solver_parameters is updated

The function provided to register_update_callback must take no arguments and return nothing. When a subclass provides this function, a user does not need to be aware of the underlying Problem/Solver objects in order to ensure that a configuration update during an in-progress simulation takes effect properly.

A weakref is used here in order to prevent circular references, which would prevent Python's automatic garbage collection from cleaning up G-ADOPT solver objects.

Source code in g-adopt/gadopt/solver_options_manager.py
 99
100
101
102
103
104
105
106
107
108
109
110
111
def register_update_callback(self, callback: Callable[[], None]) -> None:
    """Register a function to call whenever `solver_parameters` is updated

    The function provided to `register_update_callback` must take no arguments and
    return nothing. When a subclass provides this function, a user does not need to
    be aware of the underlying Problem/Solver objects in order to ensure that a
    configuration update during an in-progress simulation takes effect properly.

    A weakref is used here in order to prevent circular references, which would
    prevent Python's automatic garbage collection from cleaning up G-ADOPT solver
    objects.
    """
    self.callback_ref = weakref.WeakMethod(callback)

process_mapping(key_prefix, inmap, delta_map)

Copy inmap into a dictionary and apply the changes in delta_map

If any element of delta_map is another Mapping, recursively calls itself to process the changes in that mapping to the corresponding element in inmap. If an element delta_map is DeleteParam, remove it from the dict if it exists in inmap.

The key_prefix argument is provided for logging purposes to indicate the current mapping being processed

Source code in g-adopt/gadopt/solver_options_manager.py
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
def process_mapping(
    self,
    key_prefix: str,
    inmap: _InternalConfigType,
    delta_map: ConfigType,
) -> _InternalMutableConfigType:
    """Copy `inmap` into a dictionary and apply the changes in `delta_map`

    If any element of `delta_map` is another Mapping, recursively calls itself to
    process the changes in that mapping to the corresponding element in inmap. If
    an element `delta_map` is `DeleteParam`, remove it from the dict if it exists
    in `inmap`.

    The `key_prefix` argument is provided for logging purposes to indicate the
    current mapping being processed
    """
    outmap = dict(inmap)
    for k, v in delta_map.items():
        if v is DeleteParam:
            if k in outmap:
                debug_print(self._class_name, f"Deleting {key_prefix}[{k}]")
                del outmap[k]
            else:
                debug_print(
                    self._class_name,
                    f"Requested deletion of {key_prefix}[{k}] but this key was not found in original mapping",
                )
        elif isinstance(v, Mapping):
            kp = f"{key_prefix}[{k}]"
            if k in inmap:
                outmap[k] = self.process_mapping(kp, inmap[k], v)
            else:
                outmap[k] = self.process_mapping(kp, {}, v)
        else:
            debug_print(self._class_name, f"Adding {key_prefix}[{k}] = {v}")
            if k in outmap and isinstance(outmap[k], Mapping):
                warning_print(
                    self._class_name,
                    (
                        f"WARNING: key '{k}' holds a parameter dict in the original mapping"
                        ", but is being overwritten with a scalar parameter. This may have "
                        "unintended consequences for this solver's parameters"
                    ),
                )
            outmap[k] = v

    return outmap

add_to_solver_config(in_config, reinit=False)

Updates the solver_parameters dict

Takes a single Mapping argument that added to the existing solver_parameters dict, overwriting any existing parameters of the same name. On the first call to this method the SolverConfigurationMixin objects are created; a logging prefix based on the class hierarchy, a reference to a callback initialised to None and an empty solver configuration. This structure allows a subclass to build its solver parameters step-by-step.

Any Mapping type can be passed into this function, and the function will take care of copying the mapping to a mutable dictionary.

If the callback_ref attribute is set, it will be called on completion of the update.

Source code in g-adopt/gadopt/solver_options_manager.py
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
def add_to_solver_config(self, in_config: ConfigType | None, reinit=False) -> None:
    """Updates the `solver_parameters` dict

    Takes a single Mapping argument that added to the existing `solver_parameters`
    dict, overwriting any existing parameters of the same name. On the first call
    to this method the `SolverConfigurationMixin` objects are created; a logging
    prefix based on the class hierarchy, a reference to a callback initialised to
    `None` and an empty solver configuration. This structure allows a subclass to
    build its solver parameters step-by-step.

    Any Mapping type can be passed into this function, and the function will take
    care of copying the mapping to a mutable dictionary.

    If the `callback_ref` attribute is set, it will be called on completion of the
    update.
    """
    # "Initialise" solver config attrs on first call
    if not self._solver_options_initialised:
        self._class_name = self.__class__.__name__
        self.solver_parameters = {}
        self.callback_ref = None
        self._solver_options_initialised = True
        debug_print(self._class_name, "Initialised SolverConfigurationMixin")

    if in_config is not None:
        self.solver_parameters = self.process_mapping("solver_parameters", self.solver_parameters, in_config)
        debug_print(self._class_name, "Solver configuration after additions:")
        debug_print(self._class_name, pprint.pformat(self.solver_parameters, indent=2))
        if reinit and self.callback_ref is not None:
            debug_print(self._class_name, "Running callback")
            self.callback_ref()()
    else:
        debug_print(self._class_name, "in_config is empty, doing nothing")

is_iterative_solver()

Decide if a solver is iterative or not

Return a boolean that indicates whether this solver is an iterative solver or a direct solver.

Source code in g-adopt/gadopt/solver_options_manager.py
195
196
197
198
199
200
201
202
def is_iterative_solver(self) -> bool:
    """
    Decide if a solver is iterative or not

    Return a boolean that indicates whether this solver is an iterative solver or a
    direct solver.
    """
    return self.solver_parameters.get("pc_type") not in ["lu", "cholesky"]

debug_print(class_name, string)

Print a debugging message.

When log_level is set to DEBUG or higher, print a formatted message to stderr. The class_name variable is used to prefix each line, where class_name is generally used to identify the lowest level class in the method resolution order (e.g. StokesSolver, EnergySolver, etc)

Source code in g-adopt/gadopt/solver_options_manager.py
10
11
12
13
14
15
16
17
18
19
def debug_print(class_name: str, string: str):
    """Print a debugging message.

    When `log_level` is set to DEBUG or higher, print a formatted message to stderr.
    The `class_name` variable is used to prefix each line, where `class_name` is
    generally used to identify the lowest level class in the method resolution order
    (e.g. StokesSolver, EnergySolver, etc)
    """
    if DEBUG >= log_level:
        log(textwrap.indent(string, f"{class_name}: "))

warning_print(class_name, string)

Print a warning message.

When log_level is set to WARNING or higher, print a formatted message to stderr. The class_name variable is used to prefix each line, where class_name is generally used to identify the lowest level class in the method resolution order (e.g. StokesSolver, EnergySolver, etc)

Source code in g-adopt/gadopt/solver_options_manager.py
22
23
24
25
26
27
28
29
30
31
def warning_print(class_name: str, string: str):
    """Print a warning message.

    When `log_level` is set to WARNING or higher, print a formatted message to stderr.
    The `class_name` variable is used to prefix each line, where `class_name` is
    generally used to identify the lowest level class in the method resolution order
    (e.g. StokesSolver, EnergySolver, etc)
    """
    if WARNING >= log_level:
        log(textwrap.indent(string, f"{class_name}: "))