Skip to content
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

Towards linear optimization (dual problem, sensitivity problem, output gradient) #1110

Merged
merged 38 commits into from Dec 4, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
1209196
add interface methods for dual model, sensitivities and gradient of o…
Oct 1, 2020
b58d6f2
add implementation for dual model, sensitivities and gradient of outp…
Oct 1, 2020
f05189e
add a simple demo script for linear optimization.
Oct 1, 2020
75c901a
edit AUTHORS
Oct 1, 2020
b5d1e03
[models] adjust interface to new _compute methods
Oct 20, 2020
7618fa2
[models] adjust StationaryModel to new _compute methods
Oct 20, 2020
b37ffb0
[models] a more pythonic way for the dual model
Oct 20, 2020
4b657f1
fix docs
Oct 20, 2020
bcbde26
[docs] add a reference
Oct 26, 2020
dde4b3a
add dual_rhs and dual_operator to StationaryModel
Oct 26, 2020
581d174
refactor d_mu computations in Model and StationaryModel
Oct 26, 2020
6b68d09
[pymordemos] also test the standard derivative without adjoint approach
Oct 26, 2020
02ac555
[models] properly cite the reference
Oct 27, 2020
aecfe12
[models] remove the "dual" of a model but keep dual_operator
Nov 2, 2020
ab65902
[model] also remove dual_operator
Nov 13, 2020
00fbe40
[models] fix docs warning
Nov 28, 2020
16a9143
[pymordemos] adapt demo to typer
Nov 28, 2020
925ed07
small fix in demo
Nov 28, 2020
8f13951
[pymordemos] last update of demo
Nov 29, 2020
0799165
[pymordemos] update demo
Nov 30, 2020
a6606c4
[models] some updates for d_mu
Nov 30, 2020
8873740
[models] some change for solve_d_mu
Nov 30, 2020
aaae7f9
[models] minor updates from requested changes
Dec 1, 2020
f262e95
[models] generalize output_d_mu for vector valued functionals and int…
Dec 2, 2020
ea0d9d6
[operators.blocks] implement d_mu for BlockOperators
Dec 2, 2020
768027c
[pymordemos] update demo
Dec 2, 2020
62571dc
[pymortests] test output_d_mu for complex and vector valued cases
Dec 2, 2020
05f0439
[models] include return_array to the compute method to avoid second f…
Dec 2, 2020
c0b41fb
[models] drop BlockOperatorBase assumption and fix some things
Dec 2, 2020
3265154
[models] avoid loop over d in output_d_mu
Dec 2, 2020
e661030
[pymordemos] remove sensitivity case since it is also tested in mu_de…
Dec 2, 2020
5267b66
[models] correct shape for the gradient
Dec 3, 2020
59b890b
[pymortest] add an example with another parameter
Dec 3, 2020
30503b0
[pymordemos] small typo
Dec 3, 2020
289330b
[models] fix doc strings
Dec 4, 2020
5e6ad6d
[pymortest] reduce computational time
Dec 4, 2020
02fc4ff
[models] change default for use_adjoint
Dec 4, 2020
6abf6dd
[pymordemos] partly align the printout
Dec 4, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion AUTHORS.md
Expand Up @@ -12,9 +12,10 @@
## pyMOR 2020.2

* Tim Keil, tim.keil@uni-muenster.de
* Energy product in elliptic discretizer
* energy product in elliptic discretizer
* rename estimate --> estimate_error and estimator -> error_estimator
* avoid nested Product and Lincomb Functionals and Functions
* linear optimization (dual solution, sensitivities, output gradient)

* Hendrik Kleikamp, hendrik.kleikamp@uni-muenster.de
* artificial neural networks for instationary problems
Expand Down
4 changes: 4 additions & 0 deletions docs/source/bibliography.rst
Expand Up @@ -91,6 +91,10 @@ Bibliography
Hierarchical Approximate Proper Orthogonal Decomposition,
SIAM J. Sci. Comput. 40, A3267-A3292, 2018.

.. [HPUU09] M. Hinze, R. Pinnau, M. Ulbrich, S. Ulbrich,
Optimization with PDE constraints,
Springer Netherlands, 2009.

.. [HU18] J. Hesthaven, S. Ubbiali,
Non-intrusive reduced order modeling of nonlinear problems using neural networks,
Journal of Computational Physics, 363, pp. 55-78, 2018.
Expand Down
63 changes: 63 additions & 0 deletions src/pymor/models/basic.py
Expand Up @@ -2,8 +2,11 @@
# Copyright 2013-2020 pyMOR developers and contributors. All rights reserved.
# License: BSD 2-Clause License (http://opensource.org/licenses/BSD-2-Clause)

import numpy as np

from pymor.algorithms.timestepping import TimeStepper
from pymor.models.interface import Model
from pymor.operators.block import BlockOperatorBase
from pymor.operators.constructions import IdentityOperator, VectorOperator, ZeroOperator
from pymor.vectorarrays.interface import VectorArray
from pymor.vectorarrays.numpy import NumpyVectorSpace
Expand Down Expand Up @@ -84,6 +87,66 @@ def __str__(self):
def _compute_solution(self, mu=None, **kwargs):
return self.operator.apply_inverse(self.rhs.as_range_array(mu), mu=mu)

def _compute_solution_d_mu_single_direction(self, parameter, index, solution, mu):
lhs_d_mu = self.operator.d_mu(parameter, index).apply(solution, mu=mu)
rhs_d_mu = self.rhs.d_mu(parameter, index).as_range_array(mu)
rhs = rhs_d_mu - lhs_d_mu
return self.operator.jacobian(solution, mu=mu).apply_inverse(rhs)

_compute_allowed_kwargs = frozenset({'use_adjoint'})

def _compute_output_d_mu(self, solution, mu, return_array=False, use_adjoint=None):
"""compute the gradient of the output functional w.r.t. the parameters

Parameters
----------
solution
Internal model state for the given |Parameter value|
mu
|Parameter value| for which to compute the gradient
TiKeil marked this conversation as resolved.
Show resolved Hide resolved
return_array
if `True`, return the output gradient as a |NumPy array|.
Otherwise, return a dict of gradients for each |Parameter|.
use_adjoint
if `None` use standard approach, if `True`, use
the adjoint solution for a more efficient way of computing the gradient.
See Section 1.6.2 in [HPUU09]_ for more details.
So far, the adjoint approach is only valid for linear models.

Returns
-------
The gradient as a |NumPy array| or a dict of |NumPy arrays|.
"""
sdrave marked this conversation as resolved.
Show resolved Hide resolved
if use_adjoint is None:
use_adjoint = True if (self.output_functional.linear and self.operator.linear) else False
if not use_adjoint:
return super()._compute_output_d_mu(solution, mu, return_array)
else:
assert self.output_functional is not None
assert self.operator.linear
assert self.output_functional.linear
TiKeil marked this conversation as resolved.
Show resolved Hide resolved
dual_solutions = self.operator.range.empty()
for d in range(self.output_functional.range.dim):
dual_problem = self.with_(operator=self.operator.H, rhs=self.output_functional.H.as_range_array(mu)[d])
TiKeil marked this conversation as resolved.
Show resolved Hide resolved
dual_solutions.append(dual_problem.solve(mu))
gradients = [] if return_array else {}
for (parameter, size) in self.parameters.items():
array = np.empty(shape=(size,self.output_functional.range.dim))
for index in range(size):
output_partial_dmu = self.output_functional.d_mu(parameter, index).apply(solution,
mu=mu).to_numpy()[0]
lhs_d_mu = self.operator.d_mu(parameter, index).apply2(dual_solutions, solution, mu=mu)[:,0]
rhs_d_mu = self.rhs.d_mu(parameter, index).apply_adjoint(dual_solutions, mu=mu).to_numpy()[:,0]
array[index] = output_partial_dmu + rhs_d_mu - lhs_d_mu
if return_array:
gradients.extend(array)
else:
gradients[parameter] = array
if return_array:
return np.array(gradients)
else:
return gradients


class InstationaryModel(Model):
"""Generic class for models of instationary problems.
Expand Down
174 changes: 167 additions & 7 deletions src/pymor/models/interface.py
Expand Up @@ -48,9 +48,9 @@ def __init__(self, products=None, error_estimator=None, visualizer=None,

self.__auto_init(locals())

def _compute(self, solution=False, output=False,
def _compute(self, solution=False, output=False, solution_d_mu=False, output_d_mu=False,
solution_error_estimate=False, output_error_estimate=False,
mu=None, **kwargs):
output_d_mu_return_array=False, mu=None, **kwargs):
return {}

def _compute_solution(self, mu=None, **kwargs):
Expand Down Expand Up @@ -109,6 +109,86 @@ def _compute_output(self, solution, mu=None, **kwargs):
else:
return self.output_functional.apply(solution, mu=mu).to_numpy()

def _compute_solution_d_mu_single_direction(self, parameter, index, solution, mu=None, **kwargs):
"""Compute the partial derivative of the solution w.r.t. a parameter index

Parameters
----------
parameter
parameter for which to compute the sensitivity
index
parameter index for which to compute the sensitivity
solution
Internal model state for the given |Parameter value|.
mu
|Parameter value| for which to solve

Returns
-------
The sensitivity of the solution as a |VectorArray|.
"""
raise NotImplementedError

def _compute_solution_d_mu(self, solution, mu=None, **kwargs):
"""Compute all partial derivative of the solution w.r.t. a parameter index

Parameters
----------
solution
Internal model state for the given |Parameter value|.
mu
|Parameter value| for which to solve

Returns
-------
A dict of all partial sensitivities of the solution.
TiKeil marked this conversation as resolved.
Show resolved Hide resolved
"""
sensitivities = {}
for (parameter, size) in self.parameters.items():
sens_for_param = self.solution_space.empty()
for l in range(size):
sens_for_param.append(self._compute_solution_d_mu_single_direction(
parameter, l, solution, mu))
sensitivities[parameter] = sens_for_param
return sensitivities

def _compute_output_d_mu(self, solution, mu=None, return_array=False, **kwargs):
"""compute the gradient w.r.t. the parameter of the output functional

Parameters
----------
solution
Internal model state for the given |Parameter value|.
mu
|Parameter value| for which to compute the gradient
TiKeil marked this conversation as resolved.
Show resolved Hide resolved
return_array
if `True`, return the output gradient as a |NumPy array|.
Otherwise, return a dict of gradients for each |Parameter|.

Returns
-------
The gradient as a |NumPy array| or a dict of |NumPy arrays|.
"""
assert self.output_functional is not None
U_d_mus = self._compute_solution_d_mu(solution, mu)
gradients = [] if return_array else {}
for (parameter, size) in self.parameters.items():
array = np.empty(shape=(size,self.output_functional.range.dim))
for index in range(size):
output_partial_dmu = self.output_functional.d_mu(parameter, index).apply(
solution, mu=mu).to_numpy()[0]
U_d_mu = U_d_mus[parameter][index]
array[index] = output_partial_dmu + self.output_functional.jacobian(
solution, mu).apply(U_d_mu, mu).to_numpy()[0]
if return_array:
gradients.extend(array)
else:
gradients[parameter] = array
if return_array:
return np.array(gradients)
else:
return gradients

def _compute_solution_error_estimate(self, solution, mu=None, **kwargs):
"""Compute an error estimate for the computed internal state.

Expand Down Expand Up @@ -179,9 +259,9 @@ def _compute_output_error_estimate(self, solution, mu=None, **kwargs):

_compute_allowed_kwargs = frozenset()

def compute(self, solution=False, output=False,
solution_error_estimate=False, output_error_estimate=False, *,
mu=None, **kwargs):
def compute(self, solution=False, output=False, solution_d_mu=False, output_d_mu=False,
solution_error_estimate=False, output_error_estimate=False,
output_d_mu_return_array=False, *, mu=None, **kwargs):
"""Compute the solution of the model and associated quantities.

This methods computes the output of the model it's internal state
Expand All @@ -205,10 +285,19 @@ def compute(self, solution=False, output=False,
If `True`, return the model's internal state.
output
If `True`, return the model output.
solution_d_mu
If not `False`, either `True` to return the derivative of the model's
internal state w.r.t. all parameter components or a tuple `(parameter, index)`
to return the derivative of a single parameter component.
output_d_mu
If `True`, return the gradient of the model output w.r.t. the |Parameter|.
sdrave marked this conversation as resolved.
Show resolved Hide resolved
solution_error_estimate
If `True`, return an error estimate for the computed internal state.
output_error_estimate
If `True`, return an error estimate for the computed output.
output_d_mu_return_array
if `True`, return the output gradient as a |NumPy array|.
Otherwise, return a dict of gradients for each |Parameter|.
mu
|Parameter values| for which to compute the values.
kwargs
Expand All @@ -235,12 +324,14 @@ def compute(self, solution=False, output=False,

# first call _compute to give subclasses more control
data = self._compute(solution=solution, output=output,
solution_d_mu=solution_d_mu, output_d_mu=output_d_mu,
solution_error_estimate=solution_error_estimate,
output_error_estimate=output_error_estimate,
mu=mu, **kwargs)

if (solution or output or solution_error_estimate or output_error_estimate) and \
'solution' not in data:
if (solution or output or solution_error_estimate or
output_error_estimate or solution_d_mu or output_d_mu) \
and 'solution' not in data:
retval = self.cached_method_call(self._compute_solution, mu=mu, **kwargs)
if isinstance(retval, dict):
assert 'solution' in retval
Expand All @@ -257,6 +348,29 @@ def compute(self, solution=False, output=False,
else:
data['output'] = retval

if solution_d_mu and 'solution_d_mu' not in data:
if isinstance(solution_d_mu, tuple):
retval = self._compute_solution_d_mu_single_direction(
solution_d_mu[0], solution_d_mu[1], data['solution'], mu=mu, **kwargs)
else:
retval = self._compute_solution_d_mu(data['solution'], mu=mu, **kwargs)
# retval is always a dict
if isinstance(retval, dict) and 'solution_d_mu' in retval:
data.update(retval)
else:
data['solution_d_mu'] = retval

if output_d_mu and 'output_d_mu' not in data:
# TODO use caching here (requires skipping args in key generation)
retval = self._compute_output_d_mu(data['solution'], mu=mu,
return_array=output_d_mu_return_array,
**kwargs)
# retval is always a dict
if isinstance(retval, dict) and 'output_d_mu' in retval:
data.update(retval)
else:
data['output_d_mu'] = retval

if solution_error_estimate and 'solution_error_estimate' not in data:
# TODO use caching here (requires skipping args in key generation)
retval = self._compute_solution_error_estimate(data['solution'], mu=mu, **kwargs)
Expand Down Expand Up @@ -349,6 +463,52 @@ def output(self, mu=None, return_error_estimate=False, **kwargs):
else:
return data['output']

def solve_d_mu(self, parameter, index, mu=None, **kwargs):
"""Solve for the partial derivative of the solution w.r.t. a parameter index

Parameters
----------
parameter
parameter for which to compute the sensitivity
index
parameter index for which to compute the sensitivity
mu
|Parameter value| for which to solve

Returns
-------
The sensitivity of the solution as a |VectorArray|.
"""
data = self.compute(
solution_d_mu=(parameter, index),
mu=mu,
**kwargs
)
return data['solution_d_mu']

def output_d_mu(self, mu=None, return_array=False, **kwargs):
"""compute the gradient w.r.t. the parameter of the output functional

Parameters
----------
mu
|Parameter value| for which to compute the gradient
return_array
if `True`, return the output gradient as a |NumPy array|.
Otherwise, return a dict of gradients for each |Parameter|.

Returns
-------
The gradient as a |NumPy array| or a dict of |NumPy arrays|.
"""
data = self.compute(
output_d_mu=True,
mu=mu,
output_d_mu_return_array=return_array,
**kwargs
)
return data['output_d_mu']

def estimate_error(self, mu=None, **kwargs):
"""Estimate the error for the computed internal state.

Expand Down
6 changes: 6 additions & 0 deletions src/pymor/operators/block.py
Expand Up @@ -119,6 +119,12 @@ def process_col(col, space):
blocks = [process_col(col, space) for col, space in zip(self.blocks.T, subspaces)]
return self.source.make_array(blocks) if self.blocked_source else blocks[0]

def d_mu(self, parameter, index=0):
blocks = np.empty(self.blocks.shape, dtype=object)
for (i, j) in np.ndindex(self.blocks.shape):
blocks[i, j] = self.blocks[i, j].d_mu(parameter, index)
return self.with_(blocks=blocks)


class BlockOperator(BlockOperatorBase):
"""A matrix of arbitrary |Operators|.
Expand Down