Skip to content

Conversation

@JHopeCollins
Copy link
Member

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 value to set defaults for both -fieldsplit_0_option and -fieldsplit_1_option

@JHopeCollins JHopeCollins self-assigned this Nov 6, 2025
@JHopeCollins JHopeCollins added the enhancement New feature or request label Nov 6, 2025
assert default_options["opt3"] == "3"

options0 = petsctools.OptionsManager(
parameters=default_options, options_prefix="base_0")
Copy link
Collaborator

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.

Copy link
Collaborator

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)

Copy link
Member Author

@JHopeCollins JHopeCollins Nov 7, 2025

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:

  1. The outer solver has a mix of default and custom options.
  2. The outer solver inserts all options into the global dictionary using inserted_options.
  3. get_default_options extracts only the default options.
  4. Create an OptionsManager with a custom prefix and passing the default options in parameters.
  5. This inner OptionsManager will 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"))

Copy link
Collaborator

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')
        # etc

Copy link
Member Author

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.

Copy link
Member Author

@JHopeCollins JHopeCollins Nov 17, 2025

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:

  1. outer_options.inserted_options puts the default options in the global database.
  2. with default_options splats the defaults out to all the custom prefixes
  3. inner_options picks 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).
  4. with.default_options.__exit__ removes the custom-prefixed options from the global database.
  5. outer_options.inserted_options.__exit__ removes the default options from the global database.

When it comes time to solve, the following will happen:

  1. with inner_options.inserted_options places all the custom-prefixed options back into the global database.
  2. Some solve happens.
  3. 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.
  4. Once all the inner_options_n.inserted_options have 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)

    # etc

Copy link
Collaborator

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.

Copy link
Member Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants