Skip to content

Implementation of a Parametrised Reduced Functional - #241

Open
divijghose wants to merge 57 commits into
dolfin-adjoint:masterfrom
divijghose:parametrised_reduced_functional
Open

Implementation of a Parametrised Reduced Functional#241
divijghose wants to merge 57 commits into
dolfin-adjoint:masterfrom
divijghose:parametrised_reduced_functional

Conversation

@divijghose

@divijghose divijghose commented Feb 4, 2026

Copy link
Copy Markdown
Contributor

Current method to use parameters

The derivative_components optional argument of ReducedFunctional is used after adding parameters to the list of controls, to specify which components are to be zeroed out (by omitting them from derivative_components). This allows the user to update parameters by calling the Reduced Functional while zeroing out the gradient with respect to the parameters.

Parametrised Reduced Functional

ParametrisedReducedFunctional is a subclass of ReducedFunctional with wrapping call and derivative methods, with the parameters as attributes. The parameter_update method is called to update parameters. The parameters are not included in the derivative calculation and the optional argument derivative_components is not required.

…educedFunctional` where parameters can be updated but are not included in the derivative calculations:

1. Adds a `parameter_update` method
2. Parameters are appended at the end of the list of optimization controls, so `derivative_components` is not a required argument.
3. The `derivative` method returns only derivative corresponding to optimization controls.
@JHopeCollins

Copy link
Copy Markdown
Contributor

In the current implementation ParameterisedReducedFunctional inherits the controls property from ReducedFunctional. I think this means that this will pass when really it shouldn't:

prf = ParameterisedReducedFunctional(functional, user_controls, parameters)
assert len(prf.controls) == len(user_controls) + len(parameters)

This will also be a problem later because the optimisers will expect:

len(prf.derivative()) == len(prf.controls)

when you will actually have (correctly):

len(prf.derivative()) == len(prf.user_controls)

If this is the case then you may need to override the controls property for ParameterisedReducedFunctional so that it returns only what the user thinks are the controls.
However, if I remember correctly how Python inheritance works, that will then mean that when the parent ReducedFunctional accesses self.controls (for example to calculate the derivative here), it will won't see the full list of user_controls + parameter, but will only see user_controls.

To get around this, you may have to instead inherit from the AbstractReducedFunctional base class and just internally create your own ReducedFunctional(functional=functional, controls=user_controls+parameters).

@divijghose

Copy link
Copy Markdown
Contributor Author

In this intermediate implementation, the ParametrisedReducedFunctional inherits from AbstractReducedFunctional. This means that self.controls returns user_controls, which is the required behaviour as discussed above.

However, a lot of the code is duplicated, and, as @colinjcotter suggested, a more efficient way to do this would be to call ReducedFunctional inside ParametrisedReducedFunctional, which will get the required behaviour from self.controls while reusing most of the methods from ReducedFunctional.

…dFunctional` internally:

- Instead of inheriting the `AbstractReducedFunctional` or `ReducedFunctional` classes, `ParametrisedReducedFunctional` simply calls a `ReducedFunctional` object internally and passes the controls and parameters together as `all_controls`
Comment thread pyadjoint/reduced_functional.py Outdated
`ParametrisedReducedFunctional` must be a subclass of `AbstractReducedFunctional`

Co-authored-by: Josh Hope-Collins <jhc.jss@gmail.com>
Comment thread pyadjoint/reduced_functional.py Outdated
…h component of the parameter list must first be wrapped in `Control`.
1. Basic test to check `call`, `derivative` and `parameter_update` methods
2. Combination tests with single/multiple controls and single/multiple parameters
3. Tests to check behaviour of `controls` and `parameters` property
4. Evaluation on a more complex example
5. Tests to check behaviour in case of multiple parameter updates before call.
…cedFunctional` with `derivative_components`.
Comment thread pyadjoint/reduced_functional.py Outdated
Comment thread pyadjoint/reduced_functional.py Outdated
Comment thread pyadjoint/reduced_functional.py Outdated
Comment thread pyadjoint/reduced_functional.py Outdated
Comment thread pyadjoint/reduced_functional.py Outdated
Comment thread pyadjoint/reduced_functional.py Outdated
Comment thread pyadjoint/reduced_functional.py Outdated
Comment thread pyadjoint/reduced_functional.py Outdated
Comment thread pyadjoint/reduced_functional.py Outdated
Comment thread pyadjoint/reduced_functional.py Outdated
Comment thread pyadjoint/reduced_functional.py Outdated
Comment thread pyadjoint/reduced_functional.py Outdated
Comment thread pyadjoint/reduced_functional.py Outdated
Comment thread tests/pyadjoint/test_parametrised_rf.py Outdated
Comment thread tests/pyadjoint/test_parametrised_rf.py
Comment thread pyadjoint/reduced_functional.py Outdated
Comment thread tests/pyadjoint/test_parametrised_rf.py Outdated
@divijghose

divijghose commented Apr 30, 2026

Copy link
Copy Markdown
Contributor Author

Considering the discussion at the Firedrake meeting, the following commits rewrite ReducedFunctional to accept a parameters argument, with the eventual aim of deprecating the use of derivative_components. For now, a user can pass either parameters or derivative_components (but not both together, this would throw up an error).

1. `ParametrisedReducedFunctional` has been removed in favour of a `ReducedFunctional` that accepts `parameters` as an argument, along with a check to make sure either `derivative_components` or `parameters` is passed, but not both simultaneously.
2. If `parameters` is passed, a new `ReducedFunctional` object is created recursively. Methods will check if the `parameters` attribute is present to branch out their implementation.
3. Derivative callback include a `parameters` argument in their signature.
…est to validate initialization of `ReducedFunctional` with either `derivative_components` or `parameters`
@divijghose

Copy link
Copy Markdown
Contributor Author

@JHopeCollins, with respect to the recent CI failure:

    @no_annotations
    def derivative(self, adj_input=1.0, apply_riesz=False):
        values = [c.tape_value() for c in self.controls]
>       controls = self.derivative_cb_pre(self.parameters if hasattr(self, "_parameters") else None, self.controls)
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E       TypeError: EnsembleReducedFunctional.<lambda>() takes 1 positional argument but 2 were given

This happens because the derivative callback now has the signature derivative_cb_pre(parameters, controls), which does not reflect downstream in EnsembleReducedFunctional.

@JHopeCollins

Copy link
Copy Markdown
Contributor

This happens because the derivative callback now has the signature derivative_cb_pre(parameters, controls), which does not reflect downstream in EnsembleReducedFunctional.

If the user has not passed parameters then we shouldn't change the callback interface otherwise we will break current code before the deprecation cycle is done. For now the callback signature will depend on whether parameters are passed or not.
I also think it makes more sense to put the controls first in the signature: derivative_cb_pre(controls, parameters)

@divijghose
divijghose force-pushed the parametrised_reduced_functional branch from 47ef338 to 6f74a9f Compare June 16, 2026 15:20
1. This commit introduces the functions `_call_derivative_cb_pre` and `_call_derivative_cb_post` to handle callbacks if `parameters` are passed, or maintain backwards compatibility and a deprecation warning if only `controls` are passed.
@divijghose

Copy link
Copy Markdown
Contributor Author

This happens because the derivative callback now has the signature derivative_cb_pre(parameters, controls), which does not reflect downstream in EnsembleReducedFunctional.

If the user has not passed parameters then we shouldn't change the callback interface otherwise we will break current code before the deprecation cycle is done. For now the callback signature will depend on whether parameters are passed or not. I also think it makes more sense to put the controls first in the signature: derivative_cb_pre(controls, parameters)

@JHopeCollins, I've added the functions _call_derivative_cb_pre and _call_derivative_db_post to handle the callbacks wit parameters while maintaining backwards compatibility and emitting a deprecation warning. derivative has been updated accordingly.

Comment thread pyadjoint/reduced_functional.py Outdated
Comment thread pyadjoint/reduced_functional.py Outdated
Comment on lines +516 to +518
else:
full_values = values + self._parameters
return self._reduced_functional(full_values)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This case will cause the tape to be evaluated twice. Put this check at the beginning of the method so we recurse early before any recomputations.

Comment thread pyadjoint/reduced_functional.py Outdated
Comment on lines +454 to +460
if not hasattr(self, "_parameters"):
return tlm
else:
# self._reduced_functional.tlm will expect len(m_dot) = len(self._all_controls), so we pad it with zeros.
m_dot_all = Enlist(m_dot) + [p._ad_init_zero() for p in self._parameters]
tlm_all = self._reduced_functional.tlm(m_dot_all)
return tlm_all

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as __call__, move this check to the beginning of the method.

Comment on lines +176 to +181
except TypeError:
warnings.warn(
message="derivative_cb_pre should accept (controls, parameters)."
"Falling back to deprecated signature (controls). ",
category=DeprecationWarning
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
except TypeError:
warnings.warn(
message="derivative_cb_pre should accept (controls, parameters)."
"Falling back to deprecated signature (controls). ",
category=DeprecationWarning
)
except TypeError as err:
raise TypeError(
"derivative_cb_pre should accept (controls, parameters)."
) from err

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

change the default callbacks to take (controls, parameters=None)

Comment thread pyadjoint/reduced_functional.py Outdated
derivatives,
values)
values,
getattr(self, "_parameters", None),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change callback signature depending on parameters like the others.
(and raise informative type error).

Comment thread pyadjoint/reduced_functional.py Outdated
Comment on lines +314 to +321
eval_cb_pre=eval_cb_pre,
eval_cb_post=eval_cb_post,
derivative_cb_pre=derivative_cb_pre,
derivative_cb_post=derivative_cb_post,
hessian_cb_pre=hessian_cb_pre,
hessian_cb_post=hessian_cb_post,
tlm_cb_pre=tlm_cb_pre,
tlm_cb_post=tlm_cb_post,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Callbacks for the inner RF should all be None because the inner one doesn't know about controls vs parameters, but the callback signature does.

@JHopeCollins JHopeCollins left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally good, just the points discussed:

  • Don't do unnecessary work to throw away the result.
  • Minimal scope for dropping into the inner RF (only for the computation). Keep the controls vs parameters information to do the callbacks etc.
  • 0 parameters shouldn't be so special. Just have an empty list rather than a non-existent attribute.
  • Remove any changes that are purely formatting so it's clearer what the actual changes are.

Comment thread pyadjoint/reduced_functional.py Outdated


def _call_derivative_cb_pre(cb, controls, parameters=None):
"""Call `derivative_cb_pre` with (controls, parameters) if parameters are passd, otherwise preserve backwards

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"""Call `derivative_cb_pre` with (controls, parameters) if parameters are passd, otherwise preserve backwards
"""Call `derivative_cb_pre` with (controls, parameters) if parameters are passed, otherwise preserve backwards

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants