CUQIpy 1.1.1.post0.dev36__py3-none-any.whl → 1.4.1.post0.dev124__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of CUQIpy might be problematic. Click here for more details.

Files changed (92) hide show
  1. cuqi/__init__.py +2 -0
  2. cuqi/_version.py +3 -3
  3. cuqi/algebra/__init__.py +2 -0
  4. cuqi/algebra/_abstract_syntax_tree.py +358 -0
  5. cuqi/algebra/_ordered_set.py +82 -0
  6. cuqi/algebra/_random_variable.py +457 -0
  7. cuqi/array/_array.py +4 -13
  8. cuqi/config.py +7 -0
  9. cuqi/density/_density.py +9 -1
  10. cuqi/distribution/__init__.py +3 -2
  11. cuqi/distribution/_beta.py +7 -11
  12. cuqi/distribution/_cauchy.py +2 -2
  13. cuqi/distribution/_custom.py +0 -6
  14. cuqi/distribution/_distribution.py +31 -45
  15. cuqi/distribution/_gamma.py +7 -3
  16. cuqi/distribution/_gaussian.py +2 -12
  17. cuqi/distribution/_inverse_gamma.py +4 -10
  18. cuqi/distribution/_joint_distribution.py +112 -15
  19. cuqi/distribution/_lognormal.py +0 -7
  20. cuqi/distribution/{_modifiedhalfnormal.py → _modified_half_normal.py} +23 -23
  21. cuqi/distribution/_normal.py +34 -7
  22. cuqi/distribution/_posterior.py +9 -0
  23. cuqi/distribution/_truncated_normal.py +129 -0
  24. cuqi/distribution/_uniform.py +47 -1
  25. cuqi/experimental/__init__.py +2 -2
  26. cuqi/experimental/_recommender.py +216 -0
  27. cuqi/geometry/__init__.py +2 -0
  28. cuqi/geometry/_geometry.py +15 -1
  29. cuqi/geometry/_product_geometry.py +181 -0
  30. cuqi/implicitprior/__init__.py +5 -3
  31. cuqi/implicitprior/_regularized_gaussian.py +483 -0
  32. cuqi/implicitprior/{_regularizedGMRF.py → _regularized_gmrf.py} +4 -2
  33. cuqi/implicitprior/{_regularizedUnboundedUniform.py → _regularized_unbounded_uniform.py} +3 -2
  34. cuqi/implicitprior/_restorator.py +269 -0
  35. cuqi/legacy/__init__.py +2 -0
  36. cuqi/{experimental/mcmc → legacy/sampler}/__init__.py +7 -11
  37. cuqi/legacy/sampler/_conjugate.py +55 -0
  38. cuqi/legacy/sampler/_conjugate_approx.py +52 -0
  39. cuqi/legacy/sampler/_cwmh.py +196 -0
  40. cuqi/legacy/sampler/_gibbs.py +231 -0
  41. cuqi/legacy/sampler/_hmc.py +335 -0
  42. cuqi/{experimental/mcmc → legacy/sampler}/_langevin_algorithm.py +82 -111
  43. cuqi/legacy/sampler/_laplace_approximation.py +184 -0
  44. cuqi/legacy/sampler/_mh.py +190 -0
  45. cuqi/legacy/sampler/_pcn.py +244 -0
  46. cuqi/{experimental/mcmc → legacy/sampler}/_rto.py +132 -90
  47. cuqi/legacy/sampler/_sampler.py +182 -0
  48. cuqi/likelihood/_likelihood.py +9 -1
  49. cuqi/model/__init__.py +1 -1
  50. cuqi/model/_model.py +1361 -359
  51. cuqi/pde/__init__.py +4 -0
  52. cuqi/pde/_observation_map.py +36 -0
  53. cuqi/pde/_pde.py +134 -33
  54. cuqi/problem/_problem.py +93 -87
  55. cuqi/sampler/__init__.py +120 -8
  56. cuqi/sampler/_conjugate.py +376 -35
  57. cuqi/sampler/_conjugate_approx.py +40 -16
  58. cuqi/sampler/_cwmh.py +132 -138
  59. cuqi/{experimental/mcmc → sampler}/_direct.py +1 -1
  60. cuqi/sampler/_gibbs.py +288 -130
  61. cuqi/sampler/_hmc.py +328 -201
  62. cuqi/sampler/_langevin_algorithm.py +284 -100
  63. cuqi/sampler/_laplace_approximation.py +87 -117
  64. cuqi/sampler/_mh.py +47 -157
  65. cuqi/sampler/_pcn.py +65 -213
  66. cuqi/sampler/_rto.py +211 -142
  67. cuqi/sampler/_sampler.py +553 -136
  68. cuqi/samples/__init__.py +1 -1
  69. cuqi/samples/_samples.py +24 -18
  70. cuqi/solver/__init__.py +6 -4
  71. cuqi/solver/_solver.py +230 -26
  72. cuqi/testproblem/_testproblem.py +2 -3
  73. cuqi/utilities/__init__.py +6 -1
  74. cuqi/utilities/_get_python_variable_name.py +2 -2
  75. cuqi/utilities/_utilities.py +182 -2
  76. {CUQIpy-1.1.1.post0.dev36.dist-info → cuqipy-1.4.1.post0.dev124.dist-info}/METADATA +10 -6
  77. cuqipy-1.4.1.post0.dev124.dist-info/RECORD +101 -0
  78. {CUQIpy-1.1.1.post0.dev36.dist-info → cuqipy-1.4.1.post0.dev124.dist-info}/WHEEL +1 -1
  79. CUQIpy-1.1.1.post0.dev36.dist-info/RECORD +0 -92
  80. cuqi/experimental/mcmc/_conjugate.py +0 -197
  81. cuqi/experimental/mcmc/_conjugate_approx.py +0 -81
  82. cuqi/experimental/mcmc/_cwmh.py +0 -191
  83. cuqi/experimental/mcmc/_gibbs.py +0 -268
  84. cuqi/experimental/mcmc/_hmc.py +0 -470
  85. cuqi/experimental/mcmc/_laplace_approximation.py +0 -156
  86. cuqi/experimental/mcmc/_mh.py +0 -78
  87. cuqi/experimental/mcmc/_pcn.py +0 -89
  88. cuqi/experimental/mcmc/_sampler.py +0 -561
  89. cuqi/experimental/mcmc/_utilities.py +0 -17
  90. cuqi/implicitprior/_regularizedGaussian.py +0 -323
  91. {CUQIpy-1.1.1.post0.dev36.dist-info → cuqipy-1.4.1.post0.dev124.dist-info/licenses}/LICENSE +0 -0
  92. {CUQIpy-1.1.1.post0.dev36.dist-info → cuqipy-1.4.1.post0.dev124.dist-info}/top_level.txt +0 -0
cuqi/pde/__init__.py CHANGED
@@ -4,3 +4,7 @@ from ._pde import (
4
4
  SteadyStateLinearPDE,
5
5
  TimeDependentLinearPDE
6
6
  )
7
+
8
+ from ._observation_map import (
9
+ FD_spatial_gradient
10
+ )
@@ -0,0 +1,36 @@
1
+ import scipy
2
+ import numpy as np
3
+ """
4
+ This module contains observation map examples for PDE problems. The map can
5
+ be passed to the `PDE` object initializer via the `observation_map` argument.
6
+
7
+ For example on how to use set observation maps in time dependent PDEs, see
8
+ `demos/howtos/time_dependent_linear_pde.py`.
9
+ """
10
+
11
+ # 1. Steady State Observation Maps
12
+ # --------------------------------
13
+
14
+ # 2. Time-Dependent Observation Maps
15
+ # -----------------------------------
16
+ def FD_spatial_gradient(sol, grid, times):
17
+ """Time dependent observation map that computes the finite difference (FD) spatial gradient of a solution given at grid points (grid) and times (times). This map is supported for 1D spatial domains only.
18
+
19
+ Parameters
20
+ ----------
21
+ sol : np.ndarray
22
+ The solution array of shape (number of grid points, number of time steps).
23
+
24
+ grid : np.ndarray
25
+ The spatial grid points of shape (number of grid points,).
26
+
27
+ times : np.ndarray
28
+ The discretized time steps of shape (number of time steps,)."""
29
+
30
+ if len(grid.shape) != 1:
31
+ raise ValueError("FD_spatial_gradient only supports 1D spatial domains.")
32
+ observed_quantity = np.zeros((len(grid)-1, len(times)))
33
+ for i in range(observed_quantity.shape[0]):
34
+ observed_quantity[i, :] = ((sol[i, :] - sol[i+1, :])/
35
+ (grid[i] - grid[i+1]))
36
+ return observed_quantity
cuqi/pde/_pde.py CHANGED
@@ -3,6 +3,8 @@ import scipy
3
3
  from inspect import getsource
4
4
  from scipy.interpolate import interp1d
5
5
  import numpy as np
6
+ from cuqi.utilities import get_non_default_args
7
+
6
8
 
7
9
  class PDE(ABC):
8
10
  """
@@ -13,14 +15,15 @@ class PDE(ABC):
13
15
  PDE_form : callable function
14
16
  Callable function which returns a tuple of the needed PDE components (expected components are explained in the subclasses)
15
17
 
16
- observation_map: a function handle
17
- A function that takes the PDE solution as input and the returns the observed solution. e.g. `observation_map=lambda u: u**2` or `observation_map=lambda u: u[0]`
18
-
19
18
  grid_sol: np.ndarray
20
19
  The grid on which solution is defined
21
20
 
22
21
  grid_obs: np.ndarray
23
- The grid on which the observed solution should be interpolated (currently only supported for 1D problems).
22
+ The grid on which the observed solution should be interpolated (currently only supported for 1D problems).
23
+
24
+ observation_map: a function handle
25
+ A function that takes the PDE solution, interpolated on `grid_obs`, as input and returns the observed solution. e.g., `observation_map=lambda u, grid_obs: u**2`.
26
+
24
27
  """
25
28
 
26
29
  def __init__(self, PDE_form, grid_sol=None, grid_obs=None, observation_map=None):
@@ -28,9 +31,10 @@ class PDE(ABC):
28
31
  self.grid_sol = grid_sol
29
32
  self.grid_obs = grid_obs
30
33
  self.observation_map = observation_map
34
+ self._stored_non_default_args = None
31
35
 
32
36
  @abstractmethod
33
- def assemble(self,parameter):
37
+ def assemble(self, *args, **kwargs):
34
38
  pass
35
39
 
36
40
  @abstractmethod
@@ -63,6 +67,13 @@ class PDE(ABC):
63
67
 
64
68
  return equal_arrays
65
69
 
70
+ @property
71
+ def _non_default_args(self):
72
+ """Returns the non-default arguments of the PDE_form function"""
73
+ if self._stored_non_default_args is None:
74
+ self._stored_non_default_args = get_non_default_args(self.PDE_form)
75
+ return self._stored_non_default_args
76
+
66
77
  @property
67
78
  def grid_sol(self):
68
79
  if hasattr(self,"_grid_sol"):
@@ -93,6 +104,48 @@ class PDE(ABC):
93
104
  def grids_equal(self):
94
105
  return self._grids_equal
95
106
 
107
+ def _parse_args_add_to_kwargs(
108
+ self, *args, map_name, **kwargs):
109
+ """ Private function that parses the input arguments and adds them as
110
+ keyword arguments matching (the order of) the non default arguments of
111
+ the pde class.
112
+ """
113
+
114
+ # If any args are given, add them to kwargs
115
+ if len(args) > 0:
116
+ if len(kwargs) > 0:
117
+ raise ValueError(
118
+ + map_name.lower()
119
+ + " input is specified both as positional and keyword arguments. This is not supported."
120
+ )
121
+
122
+ # Check if the number of args does not match the number of
123
+ # non_default_args of the model
124
+ if len(args) != len(self._non_default_args):
125
+ raise ValueError(
126
+ "The number of positional arguments does not match the number of non-default arguments of "
127
+ + map_name.lower()
128
+ + "."
129
+ )
130
+
131
+ # Add args to kwargs following the order of non_default_args
132
+ for idx, arg in enumerate(args):
133
+ kwargs[self._non_default_args[idx]] = arg
134
+
135
+ # Check kwargs matches non_default_args
136
+ if set(list(kwargs.keys())) != set(self._non_default_args):
137
+ error_msg = (
138
+ map_name.lower()
139
+ + f" input is specified by keywords arguments {list(kwargs.keys())} that does not match the non_default_args of "
140
+ + map_name
141
+ + f" {self._non_default_args}."
142
+ )
143
+ raise ValueError(error_msg)
144
+
145
+ # Make sure order of kwargs is the same as non_default_args
146
+ kwargs = {k: kwargs[k] for k in self._non_default_args}
147
+
148
+ return kwargs
96
149
 
97
150
  class LinearPDE(PDE):
98
151
  """
@@ -135,6 +188,10 @@ class LinearPDE(PDE):
135
188
  info = None
136
189
 
137
190
  return solution, info
191
+
192
+ def interpolate_on_observed_domain(self, solution):
193
+ """Interpolate solution on observed space domain."""
194
+ raise NotImplementedError("interpolate_on_observed_domain method is not implemented for LinearPDE base class.")
138
195
 
139
196
  class SteadyStateLinearPDE(LinearPDE):
140
197
  """Linear steady state PDE.
@@ -142,22 +199,28 @@ class SteadyStateLinearPDE(LinearPDE):
142
199
  Parameters
143
200
  -----------
144
201
  PDE_form : callable function
145
- Callable function with signature `PDE_form(parameter)` where `parameter` is the Bayesian parameter. The function returns a tuple with the discretized differential operator A and right-hand-side b. The types of A and b are determined by what the method :meth:`linalg_solve` accepts as first and second parameters, respectively.
202
+ Callable function with signature `PDE_form(parameter1, parameter2, ...)` where `parameter1`, `parameter2`, etc. are the Bayesian unknown parameters (the user can choose any names for these parameters, e.g. `a`, `b`, etc.). The function returns a tuple with the discretized differential operator A and right-hand-side b. The types of A and b are determined by what the method :meth:`linalg_solve` accepts as first and second parameters, respectively.
203
+
204
+ observation_map: a function handle
205
+ A function that takes the PDE solution, interpolated on `grid_obs`, as input and returns the observed solution. e.g. `observation_map=lambda u, grid_obs: u**2`.
146
206
 
147
207
  kwargs:
148
208
  See :class:`~cuqi.pde.LinearPDE` for the remaining keyword arguments.
149
209
 
150
210
  Example
151
211
  --------
152
- See demo demos/demo24_fwd_poisson.py for an illustration on how to use SteadyStateLinearPDE with varying solver choices. And demos demos/demo25_fwd_poisson_2D.py and demos/demo26_fwd_poisson_mixedBC.py for examples with mixed (Dirichlet and Neumann) boundary conditions problems. demos/demo25_fwd_poisson_2D.py also illustrates how to observe on a specific boundary, for example.
212
+ See demo demos/demo24_fwd_poisson.py for an illustration on how to use SteadyStateLinearPDE with varying solver choices. And demos demos/demo25_fwd_poisson_2d.py and demos/demo26_fwd_poisson_mixed_bc.py for examples with mixed (Dirichlet and Neumann) boundary conditions problems. demos/demo25_fwd_poisson_2d.py also illustrates how to observe on a specific boundary, for example.
153
213
  """
154
214
 
155
- def __init__(self, PDE_form, **kwargs):
156
- super().__init__(PDE_form, **kwargs)
215
+ def __init__(self, PDE_form, observation_map=None, **kwargs):
216
+ super().__init__(PDE_form, observation_map=observation_map, **kwargs)
157
217
 
158
- def assemble(self, parameter):
218
+ def assemble(self, *args, **kwargs):
159
219
  """Assembles differential operator and rhs according to PDE_form"""
160
- self.diff_op, self.rhs = self.PDE_form(parameter)
220
+ kwargs = self._parse_args_add_to_kwargs(
221
+ *args, map_name="assemble", **kwargs
222
+ )
223
+ self.diff_op, self.rhs = self.PDE_form(**kwargs)
161
224
 
162
225
  def solve(self):
163
226
  """Solve the PDE and returns the solution and an information variable `info` which is a tuple of all variables returned by the function `linalg_solve` after the solution."""
@@ -166,26 +229,34 @@ class SteadyStateLinearPDE(LinearPDE):
166
229
 
167
230
  return self._solve_linear_system(self.diff_op, self.rhs, self._linalg_solve, self._linalg_solve_kwargs)
168
231
 
169
-
170
- def observe(self, solution):
171
-
232
+ def interpolate_on_observed_domain(self, solution):
233
+ """Interpolate solution on observed space grid."""
172
234
  if self.grids_equal:
173
235
  solution_obs = solution
174
236
  else:
175
237
  solution_obs = interp1d(self.grid_sol, solution, kind='quadratic')(self.grid_obs)
238
+ return solution_obs
239
+
240
+ def observe(self, solution):
241
+ """Apply observation operator to the solution. This includes
242
+ interpolation to observation points (if different from the
243
+ solution grid) then applying the observation map (if provided)."""
244
+
245
+ # Interpolate solution on observed domain
246
+ solution_obs = self.interpolate_on_observed_domain(solution)
176
247
 
177
248
  if self.observation_map is not None:
178
- solution_obs = self.observation_map(solution_obs)
179
-
249
+ solution_obs = self.observation_map(solution_obs, self.grid_obs)
250
+
180
251
  return solution_obs
181
-
252
+
182
253
  class TimeDependentLinearPDE(LinearPDE):
183
254
  """Time Dependent Linear PDE with fixed time stepping using Euler method (backward or forward).
184
255
 
185
256
  Parameters
186
257
  -----------
187
258
  PDE_form : callable function
188
- Callable function with signature `PDE_form(parameter, t)` where `parameter` is the Bayesian parameter and `t` is the time at which the PDE form is evaluated. The function returns a tuple of (`differential_operator`, `source_term`, `initial_condition`) where `differential_operator` is the linear operator at time `t`, `source_term` is the source term at time `t`, and `initial_condition` is the initial condition. The types of `differential_operator` and `source_term` are determined by what the method :meth:`linalg_solve` accepts as linear operator and right-hand side, respectively. The type of `initial_condition` should be the same type as the solution returned by :meth:`linalg_solve`.
259
+ Callable function with signature `PDE_form(parameter1, parameter2, ..., t)` where `parameter1`, `parameter2`, etc. are the Bayesian unknown parameters (the user can choose any names for these parameters, e.g. `a`, `b`, etc.) and `t` is the time at which the PDE form is evaluated. The function returns a tuple of (`differential_operator`, `source_term`, `initial_condition`) where `differential_operator` is the linear operator at time `t`, `source_term` is the source term at time `t`, and `initial_condition` is the initial condition. The types of `differential_operator` and `source_term` are determined by what the method :meth:`linalg_solve` accepts as linear operator and right-hand side, respectively. The type of `initial_condition` should be the same type as the solution returned by :meth:`linalg_solve`.
189
260
 
190
261
  time_steps : ndarray
191
262
  An array of the discretized times corresponding to the time steps that starts with the initial time and ends with the final time
@@ -196,16 +267,20 @@ class TimeDependentLinearPDE(LinearPDE):
196
267
  method: str
197
268
  Time stepping method. Currently two options are available `forward_euler` and `backward_euler`.
198
269
 
270
+ observation_map: a function handle
271
+ A function that takes the PDE solution, interpolated on `grid_obs` and `time_obs`, as input and returns the observed solution. e.g. `observation_map=lambda u, grid_obs, time_obs: u**2`.
272
+
199
273
  kwargs:
200
274
  See :class:`~cuqi.pde.LinearPDE` for the remaining keyword arguments
201
275
 
202
276
  Example
203
277
  -----------
204
- See demos/demo34_TimeDependentLinearPDE.py for 1D heat and 1D wave equations.
278
+ See demos/howtos/time_dependent_linear_pde.py for 1D heat and 1D wave equations examples. It demonstrates setting up `TimeDependentLinearPDE` objects, including the choice of time stepping methods, observation domain, and observation map.
205
279
  """
206
280
 
207
- def __init__(self, PDE_form, time_steps, time_obs='final', method='forward_euler', **kwargs):
208
- super().__init__(PDE_form, **kwargs)
281
+ def __init__(self, PDE_form, time_steps, time_obs='final',
282
+ method='forward_euler', observation_map=None, **kwargs):
283
+ super().__init__(PDE_form, observation_map=observation_map, **kwargs)
209
284
 
210
285
  self.time_steps = time_steps
211
286
  self.method = method
@@ -227,6 +302,18 @@ class TimeDependentLinearPDE(LinearPDE):
227
302
  def method(self):
228
303
  return self._method
229
304
 
305
+ @property
306
+ def _non_default_args(self):
307
+ """Returns the non-default arguments of the PDE_form function"""
308
+ if self._stored_non_default_args is None:
309
+ self._stored_non_default_args = get_non_default_args(self.PDE_form)
310
+ # Remove the time argument from the non-default arguments
311
+ # since it is provided automatically by `solve` method and is not
312
+ # an argument to be inferred in Bayesian inference setting.
313
+ if 't' in self._stored_non_default_args:
314
+ self._stored_non_default_args.remove('t')
315
+ return self._stored_non_default_args
316
+
230
317
  @method.setter
231
318
  def method(self, value):
232
319
  if value.lower() != 'forward_euler' and value.lower() != 'backward_euler':
@@ -234,13 +321,16 @@ class TimeDependentLinearPDE(LinearPDE):
234
321
  "method can be set to either `forward_euler` or `backward_euler`")
235
322
  self._method = value
236
323
 
237
- def assemble(self, parameter):
324
+ def assemble(self, *args, **kwargs):
238
325
  """Assemble PDE"""
239
- self._parameter = parameter
326
+ kwargs = self._parse_args_add_to_kwargs(*args, map_name="assemble", **kwargs)
327
+ self._parameter_kwargs = kwargs
240
328
 
241
329
  def assemble_step(self, t):
242
330
  """Assemble time step at time t"""
243
- self.diff_op, self.rhs, self.initial_condition = self.PDE_form(self._parameter, t)
331
+ self.diff_op, self.rhs, self.initial_condition = self.PDE_form(
332
+ **self._parameter_kwargs, t=t
333
+ )
244
334
 
245
335
  def solve(self):
246
336
  """Solve PDE by time-stepping"""
@@ -269,8 +359,8 @@ class TimeDependentLinearPDE(LinearPDE):
269
359
 
270
360
  return u, info
271
361
 
272
- def observe(self, solution):
273
-
362
+ def interpolate_on_observed_domain(self, solution):
363
+ """Interpolate solution on observed time and space points."""
274
364
  # If observation grid is the same as solution grid and observation time
275
365
  # is the final time step then no need to interpolate
276
366
  if self.grids_equal and np.all(self.time_steps[-1:] == self._time_obs):
@@ -279,7 +369,7 @@ class TimeDependentLinearPDE(LinearPDE):
279
369
  # Interpolate solution in time and space to the observation
280
370
  # time and space
281
371
  else:
282
- # Raise error if solution is 2D or 3D in space
372
+ # Raise error if solution is 2D or 3D in space
283
373
  if len(solution.shape) > 2:
284
374
  raise ValueError("Interpolation of solutions of 2D and 3D "+
285
375
  "space dimensions based on the provided "+
@@ -287,19 +377,30 @@ class TimeDependentLinearPDE(LinearPDE):
287
377
  "You can, instead, pass a custom "+
288
378
  "observation_map and pass grid_obs and "+
289
379
  "time_obs as None.")
290
-
380
+
291
381
  # Interpolate solution in space and time to the observation
292
382
  # time and space
293
383
  solution_obs = scipy.interpolate.RectBivariateSpline(
294
- self.grid_sol, self.time_steps, solution)(self.grid_obs,
295
- self._time_obs)
384
+ self.grid_sol, self.time_steps, solution
385
+ )(self.grid_obs, self._time_obs)
386
+
387
+ return solution_obs
388
+
389
+ def observe(self, solution):
390
+ """Apply observation operator to the solution. This includes
391
+ interpolation to observation points (if different from the
392
+ solution grid) then applying the observation map (if provided)."""
296
393
 
394
+ # Interpolate solution on observed domain
395
+ solution_obs = self.interpolate_on_observed_domain(solution)
396
+
297
397
  # Apply observation map
298
398
  if self.observation_map is not None:
299
- solution_obs = self.observation_map(solution_obs)
300
-
399
+ solution_obs = self.observation_map(solution_obs, self.grid_obs,
400
+ self._time_obs)
401
+
301
402
  # squeeze if only one time observation
302
403
  if len(self._time_obs) == 1:
303
404
  solution_obs = solution_obs.squeeze()
304
405
 
305
- return solution_obs
406
+ return solution_obs