-
Notifications
You must be signed in to change notification settings - Fork 1
get_default_options for multiple subsolvers a la -fieldsplit
#24
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
| assert default_options["opt3"] == "3" | ||
|
|
||
| options0 = petsctools.OptionsManager( | ||
| parameters=default_options, options_prefix="base_0") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I find the way that this is used in practice to be quite confusing. get_default_options is sort of a filter that you apply to the global options before passing to the OptionsManager? I wonder if you could instead have something like:
options0 = petsctools.OptionsManager(
parameters=parameters, options_prefix="base_0", shared_defaults_prefix="base")that would do this filtering internally instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or even
options0 = petsctools.OptionsManager(
parameters=parameters, options_prefix="base_0", detect_defaults=True)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
get_default_options is sort of a filter that you apply to the global options before passing to the OptionsManager?
Yes exactly. The key point is that options passed to the OptionsManager via the parameters kwarg are always given a lower priority than those currently in the global PETSc.Options database.
Assume I am setting up a PC with multiple subsolvers inside a variational solver:
- The outer solver has a mix of default and custom options.
- The outer solver inserts all options into the global dictionary using
inserted_options. get_default_optionsextracts only the default options.- Create an
OptionsManagerwith a custom prefix and passing the default options inparameters. - This inner
OptionsManagerwill see all custom options in the global dictionary so will give them priority over the default options passed to the constructor.
This pattern will automatically DTRT to combine default and custom options (i.e. default options will be overridden by custom options).
outer_options = OptionsManager(
parameters = {
'base_opt0': 0
'base_opt1': 1
'base_0_opt0': 2,
'base_1_opt2': 3},
options_prefix=''
)
with outer_options.inserted_options():
default_options = get_default_options(base_prefix='base', custom_prefix_endings=('0', '1', '2'))
# default_options = {'opt0': 0, 'opt1': 1}
inner_options_0 = OptionsManager(parameters=default_parameters, options_prefix='base_0')
inner_options_1 = OptionsManager(parameters=default_parameters, options_prefix='base_1')
inner_options_2 = OptionsManager(parameters=default_parameters, options_prefix='base_2')
# 'base_0' uses one default option and overrides the other.
# inner_options_0.parameters = {
# 'opt0': 2,
# 'opt1': 1'
# }
# 'base_1' uses both default options and adds a third.
# inner_options_1.parameters = {
# 'opt0': 0,
# 'opt1': 1,
# 'opt2': 3
# }
# 'base_2' uses both default options and no others.
# inner_options_2.parameters = {
# 'opt0': 0,
# 'opt1': 1,
# }I wonder if you could instead have something like:
options0 = petsctools.OptionsManager( parameters=parameters, options_prefix="base_0", shared_defaults_prefix="base")that would do this filtering internally instead.
Getting the OptionsManager to do this rather than having the explicit get_default_options step externally would be nice, but we couldn't do it automatically. Given an option 'base_word0_word1' there's no way of knowing in general whether word0 is a custom prefix ending or an actual option. There's no way of querying if something is a valid PETSc option because "valid" is defined on the fly by each solver component.
You'd have to have something like this (kwarg name subject to bikeshedding):
options0 = OptionsManager(
parameters=parameters,
options_prefix="base_0",
shared_defaults_prefix="base",
other_prefix_endings=("1", "2"))There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if you had a context manager that added suffix-ed defaults to the available options? Something like
with outer_options.inserted_options():
# so PETSc.Options is
# {'base_opt0': 0,
# 'base_opt1': 1,
# 'base_0_opt0': 2,
# 'base_1_opt2': 3}
with default_options(base_prefix="base", custom_prefix_endings=('0', '1', '2'):
# so PETSc.Options is
# { 'base_0_opt0': 2,
# 'base_0_opt1': 1,
# 'base_1_opt0': 0,
# 'base_1_opt1': 1,
# 'base_1_opt2': 3,
# 'base_2_opt0': 0,
# 'base_2_opt1': 1}
inner_options_0 = OptionsManager(options_prefix='base_0')
# etcThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh boy. Maybe? I'll have to have a think about that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I now think that the extra context manager is not the right way to go because it will unfortunately, and somewhat opaquely, end up with all of the options eventually leaking back into the global database.
When the OptionsManagers are created the following will happen:
outer_options.inserted_optionsputs the default options in the global database.with default_optionssplats the defaults out to all the custom prefixesinner_optionspicks up on the options with its own prefix and stores them as if they were passed on the command line (this distinction is important later).with.default_options.__exit__removes the custom-prefixed options from the global database.outer_options.inserted_options.__exit__removes the default options from the global database.
When it comes time to solve, the following will happen:
with inner_options.inserted_optionsplaces all the custom-prefixed options back into the global database.- Some solve happens.
inner_options.inserted_options.__exit__will not remove the custom-prefixed options from the global database because it thinks they come from the command line so can't be touched.- Once all the
inner_options_n.inserted_optionshave been called, every single custom-prefixed option is back in the global database.
You could get around this by calling the default_options context manager every time inserted_options is used, but if two calls always need to be made together then they should be one call.
My proposal would be having an optional NamedTuple kwarg for the OptionsManager that holds the base_prefix and the list of custom_prefix_endings. If this kwarg is present then the OptionsManager will internally call get_default_options and sort out the logic for what options get priority. This means that later on you'd only have to call OptionsManager.inserted_option and not have to know/worry about whether any defaults were used.
I like a (named) tuple here because it doesn't make sense to have either base_prefix or custom_endings without the other.
from typing import NamedTuple
DefaultOptionSet = NamedTuple("DefaultOptionsSet", ["base_prefix", "custom_endings"])
default_opts = DefaultOptionSet("base", [0, 1, 2])
with outer_options.inserted_options():
# so PETSc.Options is
# { 'base_opt0': 0,
# 'base_opt1': 1,
# 'base_0_opt0': 2,
# 'base_1_opt2': 3}
# inner_options_0 ends up with
# { 'base_0_opt0': 2,
# 'base_0_opt1': 1}
inner_options_0 = OptionsManager(options_prefix='base_0', default_options=default_opts)
# inner_options_1 ends up with
# { 'base_1_opt0': 0,
# 'base_1_opt1': 1
# 'base_1_opt2': 3 }
inner_options_1 = OptionsManager(options_prefix='base_1', default_options=default_opts)
# etcThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is good. There is very slight duplication of the base prefix but that's probably hard to get rid of.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was planning on having an assert options_prefix.startswith(default_opts.base_prefix) check to make sure everything is consistent.
To avoid duplication we could remove base_prefix from the DefaultOptionSet and try to automagically detect it from the custom_endings and options_prefix, but that is a bit opaque. I'd lean towards a bit of duplication here for clarity.
This is a convenience function to extract default arguments when you have multiple subsolvers whose prefixes have a common base, e.g. using
-fieldsplit_option valueto set defaults for both-fieldsplit_0_optionand-fieldsplit_1_option