CUQIpy 1.3.0.post0.dev277__tar.gz → 1.3.0.post0.dev292__tar.gz

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 (126) hide show
  1. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/CUQIpy.egg-info/PKG-INFO +1 -1
  2. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/PKG-INFO +1 -1
  3. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/_version.py +3 -3
  4. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/model/_model.py +73 -21
  5. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/pde/_pde.py +71 -5
  6. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/utilities/_utilities.py +1 -1
  7. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/tests/test_model.py +237 -32
  8. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/CUQIpy.egg-info/SOURCES.txt +0 -0
  9. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/CUQIpy.egg-info/dependency_links.txt +0 -0
  10. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/CUQIpy.egg-info/requires.txt +0 -0
  11. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/CUQIpy.egg-info/top_level.txt +0 -0
  12. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/LICENSE +0 -0
  13. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/README.md +0 -0
  14. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/__init__.py +0 -0
  15. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/_messages.py +0 -0
  16. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/array/__init__.py +0 -0
  17. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/array/_array.py +0 -0
  18. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/config.py +0 -0
  19. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/data/__init__.py +0 -0
  20. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/data/_data.py +0 -0
  21. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/data/astronaut.npz +0 -0
  22. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/data/camera.npz +0 -0
  23. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/data/cat.npz +0 -0
  24. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/data/cookie.png +0 -0
  25. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/data/satellite.mat +0 -0
  26. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/density/__init__.py +0 -0
  27. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/density/_density.py +0 -0
  28. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/diagnostics.py +0 -0
  29. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/distribution/__init__.py +0 -0
  30. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/distribution/_beta.py +0 -0
  31. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/distribution/_cauchy.py +0 -0
  32. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/distribution/_cmrf.py +0 -0
  33. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/distribution/_custom.py +0 -0
  34. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/distribution/_distribution.py +0 -0
  35. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/distribution/_gamma.py +0 -0
  36. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/distribution/_gaussian.py +0 -0
  37. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/distribution/_gmrf.py +0 -0
  38. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/distribution/_inverse_gamma.py +0 -0
  39. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/distribution/_joint_distribution.py +0 -0
  40. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/distribution/_laplace.py +0 -0
  41. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/distribution/_lmrf.py +0 -0
  42. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/distribution/_lognormal.py +0 -0
  43. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/distribution/_modifiedhalfnormal.py +0 -0
  44. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/distribution/_normal.py +0 -0
  45. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/distribution/_posterior.py +0 -0
  46. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/distribution/_smoothed_laplace.py +0 -0
  47. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/distribution/_truncated_normal.py +0 -0
  48. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/distribution/_uniform.py +0 -0
  49. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/experimental/__init__.py +0 -0
  50. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/experimental/_recommender.py +0 -0
  51. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/experimental/algebra/__init__.py +0 -0
  52. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/experimental/algebra/_ast.py +0 -0
  53. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/experimental/algebra/_orderedset.py +0 -0
  54. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/experimental/algebra/_randomvariable.py +0 -0
  55. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/experimental/geometry/__init__.py +0 -0
  56. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/experimental/geometry/_productgeometry.py +0 -0
  57. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/experimental/mcmc/__init__.py +0 -0
  58. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/experimental/mcmc/_conjugate.py +0 -0
  59. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/experimental/mcmc/_conjugate_approx.py +0 -0
  60. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/experimental/mcmc/_cwmh.py +0 -0
  61. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/experimental/mcmc/_direct.py +0 -0
  62. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/experimental/mcmc/_gibbs.py +0 -0
  63. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/experimental/mcmc/_hmc.py +0 -0
  64. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/experimental/mcmc/_langevin_algorithm.py +0 -0
  65. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/experimental/mcmc/_laplace_approximation.py +0 -0
  66. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/experimental/mcmc/_mh.py +0 -0
  67. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/experimental/mcmc/_pcn.py +0 -0
  68. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/experimental/mcmc/_rto.py +0 -0
  69. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/experimental/mcmc/_sampler.py +0 -0
  70. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/geometry/__init__.py +0 -0
  71. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/geometry/_geometry.py +0 -0
  72. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/implicitprior/__init__.py +0 -0
  73. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/implicitprior/_regularizedGMRF.py +0 -0
  74. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/implicitprior/_regularizedGaussian.py +0 -0
  75. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/implicitprior/_regularizedUnboundedUniform.py +0 -0
  76. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/implicitprior/_restorator.py +0 -0
  77. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/likelihood/__init__.py +0 -0
  78. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/likelihood/_likelihood.py +0 -0
  79. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/model/__init__.py +0 -0
  80. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/operator/__init__.py +0 -0
  81. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/operator/_operator.py +0 -0
  82. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/pde/__init__.py +0 -0
  83. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/problem/__init__.py +0 -0
  84. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/problem/_problem.py +0 -0
  85. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/sampler/__init__.py +0 -0
  86. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/sampler/_conjugate.py +0 -0
  87. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/sampler/_conjugate_approx.py +0 -0
  88. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/sampler/_cwmh.py +0 -0
  89. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/sampler/_gibbs.py +0 -0
  90. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/sampler/_hmc.py +0 -0
  91. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/sampler/_langevin_algorithm.py +0 -0
  92. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/sampler/_laplace_approximation.py +0 -0
  93. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/sampler/_mh.py +0 -0
  94. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/sampler/_pcn.py +0 -0
  95. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/sampler/_rto.py +0 -0
  96. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/sampler/_sampler.py +0 -0
  97. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/samples/__init__.py +0 -0
  98. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/samples/_samples.py +0 -0
  99. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/solver/__init__.py +0 -0
  100. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/solver/_solver.py +0 -0
  101. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/testproblem/__init__.py +0 -0
  102. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/testproblem/_testproblem.py +0 -0
  103. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/utilities/__init__.py +0 -0
  104. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/cuqi/utilities/_get_python_variable_name.py +0 -0
  105. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/pyproject.toml +0 -0
  106. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/requirements.txt +0 -0
  107. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/setup.cfg +0 -0
  108. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/setup.py +0 -0
  109. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/tests/test_MRFs.py +0 -0
  110. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/tests/test_abstract_distribution_density.py +0 -0
  111. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/tests/test_bayesian_inversion.py +0 -0
  112. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/tests/test_density.py +0 -0
  113. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/tests/test_distribution.py +0 -0
  114. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/tests/test_distributions_shape.py +0 -0
  115. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/tests/test_geometry.py +0 -0
  116. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/tests/test_implicit_priors.py +0 -0
  117. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/tests/test_joint_distribution.py +0 -0
  118. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/tests/test_likelihood.py +0 -0
  119. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/tests/test_pde.py +0 -0
  120. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/tests/test_posterior.py +0 -0
  121. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/tests/test_problem.py +0 -0
  122. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/tests/test_sampler.py +0 -0
  123. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/tests/test_samples.py +0 -0
  124. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/tests/test_solver.py +0 -0
  125. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/tests/test_testproblem.py +0 -0
  126. {cuqipy-1.3.0.post0.dev277 → cuqipy-1.3.0.post0.dev292}/tests/test_utilities.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: CUQIpy
3
- Version: 1.3.0.post0.dev277
3
+ Version: 1.3.0.post0.dev292
4
4
  Summary: Computational Uncertainty Quantification for Inverse problems in Python
5
5
  Maintainer-email: "Nicolai A. B. Riis" <nabr@dtu.dk>, "Jakob S. Jørgensen" <jakj@dtu.dk>, "Amal M. Alghamdi" <amaal@dtu.dk>, Chao Zhang <chaz@dtu.dk>
6
6
  License: Apache License
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: CUQIpy
3
- Version: 1.3.0.post0.dev277
3
+ Version: 1.3.0.post0.dev292
4
4
  Summary: Computational Uncertainty Quantification for Inverse problems in Python
5
5
  Maintainer-email: "Nicolai A. B. Riis" <nabr@dtu.dk>, "Jakob S. Jørgensen" <jakj@dtu.dk>, "Amal M. Alghamdi" <amaal@dtu.dk>, Chao Zhang <chaz@dtu.dk>
6
6
  License: Apache License
@@ -8,11 +8,11 @@ import json
8
8
 
9
9
  version_json = '''
10
10
  {
11
- "date": "2025-06-26T10:10:00+0300",
11
+ "date": "2025-07-04T09:41:59+0300",
12
12
  "dirty": false,
13
13
  "error": null,
14
- "full-revisionid": "52c8f8ffc38956c4aee969ccc70cd3db24ee774e",
15
- "version": "1.3.0.post0.dev277"
14
+ "full-revisionid": "7536b0d728d704a7ab46bca7ff4756075ba0e573",
15
+ "version": "1.3.0.post0.dev292"
16
16
  }
17
17
  ''' # END VERSION_JSON
18
18
 
@@ -179,7 +179,7 @@ class Model(object):
179
179
  self._stored_non_default_args =\
180
180
  cuqi.utilities.get_non_default_args(self._forward_func)
181
181
  return self._stored_non_default_args
182
-
182
+
183
183
  @property
184
184
  def number_of_inputs(self):
185
185
  """ The number of inputs of the model. """
@@ -422,7 +422,7 @@ class Model(object):
422
422
  # Use CUQIarray funvals if geometry is consistent
423
423
  if isinstance(v, CUQIarray) and v.geometry == geometries[i]:
424
424
  kwargs[k] = v.funvals
425
- # Else, if we still need to convert to function value (is_par[i] is True)
425
+ # Else, if we still need to convert to function value (is_par[i] is True)
426
426
  # we use the geometry par2fun method
427
427
  elif is_par[i] and v is not None:
428
428
  kwargs[k] = geometries[i].par2fun(v)
@@ -496,7 +496,7 @@ class Model(object):
496
496
  # Use CUQIarray parameters if geometry is consistent
497
497
  if isinstance(v, CUQIarray) and v.geometry == geometries[i]:
498
498
  v = v.parameters
499
- # Else, if we still need to convert to parameter value (is_par[i] is False)
499
+ # Else, if we still need to convert to parameter value (is_par[i] is False)
500
500
  # we use the geometry fun2par method
501
501
  elif not is_par[i] and v is not None:
502
502
  v = geometries[i].fun2par(v)
@@ -665,7 +665,7 @@ class Model(object):
665
665
  error_msg = (
666
666
  "The "
667
667
  + map_name.lower()
668
- + f" input is specified by a keywords arguments {list(kwargs.keys())} that does not match the non_default_args of the "
668
+ + f" input is specified by keywords arguments {list(kwargs.keys())} that does not match the non_default_args of the "
669
669
  + map_name
670
670
  + f" {non_default_args}."
671
671
  )
@@ -808,7 +808,11 @@ class Model(object):
808
808
  new_model = copy(self)
809
809
 
810
810
  # Store the original non_default_args of the model
811
- new_model._original_non_default_args = self._non_default_args
811
+ new_model._original_non_default_args = (
812
+ self._original_non_default_args
813
+ if hasattr(self, "_original_non_default_args")
814
+ else self._non_default_args
815
+ )
812
816
 
813
817
  # Update the non_default_args of the model to match the distribution
814
818
  # names. Defaults to x in the case of only one distribution that has no
@@ -1052,7 +1056,7 @@ class Model(object):
1052
1056
 
1053
1057
  # turn grad_is_par to a tuple of bools if it is not already
1054
1058
  if isinstance(grad_is_par, bool):
1055
- grad_is_par = tuple([grad_is_par]*len(grad))
1059
+ grad_is_par = tuple([grad_is_par]*self.number_of_inputs)
1056
1060
 
1057
1061
  # If the domain geometry is a _ProductGeometry and the gradient is
1058
1062
  # stacked, split it
@@ -1451,7 +1455,7 @@ class PDEModel(Model):
1451
1455
  :ivar range_geometry: The geometry representing the range.
1452
1456
  :ivar domain_geometry: The geometry representing the domain.
1453
1457
  """
1454
- def __init__(self, PDE: cuqi.pde.PDE, range_geometry, domain_geometry):
1458
+ def __init__(self, PDE: cuqi.pde.PDE, range_geometry, domain_geometry, **kwargs):
1455
1459
 
1456
1460
  if not isinstance(PDE, cuqi.pde.PDE):
1457
1461
  raise ValueError("PDE needs to be a cuqi PDE.")
@@ -1460,23 +1464,30 @@ class PDEModel(Model):
1460
1464
  self.pde = PDE
1461
1465
  self._stored_non_default_args = None
1462
1466
 
1463
- super().__init__(self._forward_func, range_geometry, domain_geometry)
1467
+ # If gradient or jacobian is not provided, we create it from the PDE
1468
+ if not np.any([k in kwargs.keys() for k in ["gradient", "jacobian"]]):
1469
+ # Create gradient or jacobian function to pass to the Model based on
1470
+ # the PDE object. The dictionary derivative_kwarg contains the
1471
+ # created function along with the function type (either "gradient"
1472
+ # or "jacobian")
1473
+ derivative_kwarg = self._create_derivative_function()
1474
+ # append derivative_kwarg to kwargs
1475
+ kwargs.update(derivative_kwarg)
1476
+
1477
+ super().__init__(forward=self._forward_func_pde,
1478
+ range_geometry=range_geometry,
1479
+ domain_geometry=domain_geometry,
1480
+ **kwargs)
1464
1481
 
1465
1482
  @property
1466
1483
  def _non_default_args(self):
1467
1484
  if self._stored_non_default_args is None:
1468
1485
  # extract the non-default arguments of the PDE
1469
- self._stored_non_default_args = cuqi.utilities.get_non_default_args(
1470
- self.pde.PDE_form
1471
- )
1472
- # remove t from the non-default arguments
1473
- self._stored_non_default_args = self._non_default_args
1474
- if "t" in self._non_default_args:
1475
- self._stored_non_default_args.remove("t")
1486
+ self._stored_non_default_args = self.pde._non_default_args
1476
1487
 
1477
1488
  return self._stored_non_default_args
1478
1489
 
1479
- def _forward_func(self, **kwargs):
1490
+ def _forward_func_pde(self, **kwargs):
1480
1491
 
1481
1492
  self.pde.assemble(**kwargs)
1482
1493
 
@@ -1486,14 +1497,55 @@ class PDEModel(Model):
1486
1497
 
1487
1498
  return obs
1488
1499
 
1489
- def _gradient_func(self, direction, wrt):
1490
- """ Compute direction-Jacobian product (gradient) of the model. """
1500
+ def _create_derivative_function(self):
1501
+ """Private function that creates the derivative function (gradient or
1502
+ jacobian) based on the PDE object. The derivative function is created as
1503
+ a lambda function that takes the direction and the parameters as input
1504
+ and returns the gradient or jacobian of the PDE. This private function
1505
+ returns a dictionary with the created function and the function type
1506
+ (either "gradient" or "jacobian")."""
1507
+
1491
1508
  if hasattr(self.pde, "gradient_wrt_parameter"):
1492
- return self.pde.gradient_wrt_parameter(direction, wrt)
1509
+ # Build the string that will be used to create the lambda function
1510
+ function_str = (
1511
+ "lambda direction, "
1512
+ + ", ".join(self._non_default_args)
1513
+ + ", pde_func: pde_func(direction, "
1514
+ + ", ".join(self._non_default_args)
1515
+ + ")"
1516
+ )
1517
+
1518
+ # create the lambda function from the string
1519
+ function = eval(function_str)
1520
+
1521
+ # create partial function from the lambda function with gradient_wrt_parameter
1522
+ # as the first argument
1523
+ grad_func = partial(function, pde_func=self.pde.gradient_wrt_parameter)
1524
+
1525
+ # Return the gradient function
1526
+ return {"gradient": grad_func}
1527
+
1493
1528
  elif hasattr(self.pde, "jacobian_wrt_parameter"):
1494
- return direction@self.pde.jacobian_wrt_parameter(wrt)
1529
+ # Build the string that will be used to create the lambda function
1530
+ function_str = (
1531
+ "lambda "
1532
+ + ", ".join(self._non_default_args)
1533
+ + ", pde_func: pde_func( "
1534
+ + ", ".join(self._non_default_args)
1535
+ + ")"
1536
+ )
1537
+
1538
+ # create the lambda function from the string
1539
+ function = eval(function_str)
1540
+
1541
+ # create partial function from the lambda function with jacobian_wrt_parameter
1542
+ # as the first argument
1543
+ jacobian_func = partial(function, pde_func=self.pde.jacobian_wrt_parameter)
1544
+
1545
+ # Return the jacobian function
1546
+ return {"jacobian": jacobian_func}
1495
1547
  else:
1496
- raise NotImplementedError("Gradient is not implemented for this model.")
1548
+ return {} # empty dictionary if no gradient or jacobian is found
1497
1549
 
1498
1550
  # Add the underlying PDE class name to the repr.
1499
1551
  def __repr__(self) -> str:
@@ -3,6 +3,7 @@ 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
6
7
 
7
8
 
8
9
  class PDE(ABC):
@@ -29,6 +30,7 @@ class PDE(ABC):
29
30
  self.grid_sol = grid_sol
30
31
  self.grid_obs = grid_obs
31
32
  self.observation_map = observation_map
33
+ self._stored_non_default_args = None
32
34
 
33
35
  @abstractmethod
34
36
  def assemble(self, *args, **kwargs):
@@ -64,6 +66,13 @@ class PDE(ABC):
64
66
 
65
67
  return equal_arrays
66
68
 
69
+ @property
70
+ def _non_default_args(self):
71
+ """Returns the non-default arguments of the PDE_form function"""
72
+ if self._stored_non_default_args is None:
73
+ self._stored_non_default_args = get_non_default_args(self.PDE_form)
74
+ return self._stored_non_default_args
75
+
67
76
  @property
68
77
  def grid_sol(self):
69
78
  if hasattr(self,"_grid_sol"):
@@ -94,6 +103,48 @@ class PDE(ABC):
94
103
  def grids_equal(self):
95
104
  return self._grids_equal
96
105
 
106
+ def _parse_args_add_to_kwargs(
107
+ self, *args, map_name, **kwargs):
108
+ """ Private function that parses the input arguments and adds them as
109
+ keyword arguments matching (the order of) the non default arguments of
110
+ the pde class.
111
+ """
112
+
113
+ # If any args are given, add them to kwargs
114
+ if len(args) > 0:
115
+ if len(kwargs) > 0:
116
+ raise ValueError(
117
+ + map_name.lower()
118
+ + " input is specified both as positional and keyword arguments. This is not supported."
119
+ )
120
+
121
+ # Check if the number of args does not match the number of
122
+ # non_default_args of the model
123
+ if len(args) != len(self._non_default_args):
124
+ raise ValueError(
125
+ "The number of positional arguments does not match the number of non-default arguments of "
126
+ + map_name.lower()
127
+ + "."
128
+ )
129
+
130
+ # Add args to kwargs following the order of non_default_args
131
+ for idx, arg in enumerate(args):
132
+ kwargs[self._non_default_args[idx]] = arg
133
+
134
+ # Check kwargs matches non_default_args
135
+ if set(list(kwargs.keys())) != set(self._non_default_args):
136
+ error_msg = (
137
+ map_name.lower()
138
+ + f" input is specified by keywords arguments {list(kwargs.keys())} that does not match the non_default_args of "
139
+ + map_name
140
+ + f" {self._non_default_args}."
141
+ )
142
+ raise ValueError(error_msg)
143
+
144
+ # Make sure order of kwargs is the same as non_default_args
145
+ kwargs = {k: kwargs[k] for k in self._non_default_args}
146
+
147
+ return kwargs
97
148
 
98
149
  class LinearPDE(PDE):
99
150
  """
@@ -143,7 +194,7 @@ class SteadyStateLinearPDE(LinearPDE):
143
194
  Parameters
144
195
  -----------
145
196
  PDE_form : callable function
146
- 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.
197
+ 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.
147
198
 
148
199
  kwargs:
149
200
  See :class:`~cuqi.pde.LinearPDE` for the remaining keyword arguments.
@@ -158,7 +209,10 @@ class SteadyStateLinearPDE(LinearPDE):
158
209
 
159
210
  def assemble(self, *args, **kwargs):
160
211
  """Assembles differential operator and rhs according to PDE_form"""
161
- self.diff_op, self.rhs = self.PDE_form(*args, **kwargs)
212
+ kwargs = self._parse_args_add_to_kwargs(
213
+ *args, map_name="assemble", **kwargs
214
+ )
215
+ self.diff_op, self.rhs = self.PDE_form(**kwargs)
162
216
 
163
217
  def solve(self):
164
218
  """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."""
@@ -186,7 +240,7 @@ class TimeDependentLinearPDE(LinearPDE):
186
240
  Parameters
187
241
  -----------
188
242
  PDE_form : callable function
189
- 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`.
243
+ 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`.
190
244
 
191
245
  time_steps : ndarray
192
246
  An array of the discretized times corresponding to the time steps that starts with the initial time and ends with the final time
@@ -228,6 +282,18 @@ class TimeDependentLinearPDE(LinearPDE):
228
282
  def method(self):
229
283
  return self._method
230
284
 
285
+ @property
286
+ def _non_default_args(self):
287
+ """Returns the non-default arguments of the PDE_form function"""
288
+ if self._stored_non_default_args is None:
289
+ self._stored_non_default_args = get_non_default_args(self.PDE_form)
290
+ # Remove the time argument from the non-default arguments
291
+ # since it is provided automatically by `solve` method and is not
292
+ # an argument to be inferred in Bayesian inference setting.
293
+ if 't' in self._stored_non_default_args:
294
+ self._stored_non_default_args.remove('t')
295
+ return self._stored_non_default_args
296
+
231
297
  @method.setter
232
298
  def method(self, value):
233
299
  if value.lower() != 'forward_euler' and value.lower() != 'backward_euler':
@@ -237,13 +303,13 @@ class TimeDependentLinearPDE(LinearPDE):
237
303
 
238
304
  def assemble(self, *args, **kwargs):
239
305
  """Assemble PDE"""
306
+ kwargs = self._parse_args_add_to_kwargs(*args, map_name="assemble", **kwargs)
240
307
  self._parameter_kwargs = kwargs
241
- self._parameter_args = args
242
308
 
243
309
  def assemble_step(self, t):
244
310
  """Assemble time step at time t"""
245
311
  self.diff_op, self.rhs, self.initial_condition = self.PDE_form(
246
- *self._parameter_args, **self._parameter_kwargs, t=t
312
+ **self._parameter_kwargs, t=t
247
313
  )
248
314
 
249
315
  def solve(self):
@@ -188,7 +188,7 @@ def approx_derivative(func, wrt, direction=None, epsilon=np.sqrt(np.finfo(float)
188
188
  # We compute the Jacobian matrix of func using forward differences.
189
189
  # If the function is scalar-valued, we compute the gradient instead.
190
190
  # If the direction is provided, we compute the direction-Jacobian product.
191
- wrt = np.asarray(wrt)
191
+ wrt = force_ndarray(wrt, flatten=True)
192
192
  f0 = func(wrt)
193
193
  Matr = np.zeros([infer_len(wrt), infer_len(f0)])
194
194
  dx = np.zeros(len(wrt))
@@ -7,15 +7,15 @@ import cuqi
7
7
  import pytest
8
8
  from scipy import optimize
9
9
  from copy import copy, deepcopy
10
- from cuqi.geometry import _identity_geometries, _DefaultGeometry1D, _DefaultGeometry2D, Geometry, Discrete, Image2D
10
+ from cuqi.geometry import _identity_geometries, _DefaultGeometry1D, _DefaultGeometry2D, Geometry, Discrete, Image2D, KLExpansion
11
11
  from cuqi.utilities import force_ndarray
12
12
  from cuqi.experimental.geometry import _ProductGeometry
13
13
 
14
- def test_PDE_model_multiple_input():
15
- """ Test that the PDE model can accept multiple inputs specified as positional arguments or keyword arguments """
14
+ def test_steady_state_PDE_model_multiple_input():
15
+ """ Test that the steady state PDE model and gradient can accept multiple inputs specified as positional arguments or keyword arguments """
16
16
  pde_test_model = MultipleInputTestModel.helper_build_steady_state_PDE_test_model()
17
17
  pde_test_model.populate_model_variations()
18
- CUQI_pde = pde_test_model.model_variations[0] # PDE model with multiple inputs
18
+ CUQI_pde = pde_test_model.model_variations[1] # PDE model with multiple inputs
19
19
 
20
20
  # Check that the model has correct parameter name
21
21
  assert CUQI_pde._non_default_args == ['mag', 'kappa_scale']
@@ -29,6 +29,54 @@ def test_PDE_model_multiple_input():
29
29
  # Check that the two outputs are the same
30
30
  assert np.allclose(output1, output2)
31
31
 
32
+ # Assert evaluating gradient works
33
+ direction = np.random.randn(CUQI_pde.range_dim)
34
+
35
+ # Make sure gradient can be computed with positional or keyword arguments
36
+ grad1 = CUQI_pde.gradient(direction, mag=2, kappa_scale=2)
37
+ grad2 = CUQI_pde.gradient(direction, 2, 2)
38
+
39
+ # Passing wrong kwargs should raise an error
40
+ with pytest.raises(
41
+ ValueError,
42
+ match=r"The gradient input is specified by a direction and keywords arguments \['mag', 'kappa'\] that does not match the non_default_args of the model \['mag', 'kappa_scale'\].",
43
+ ):
44
+ CUQI_pde.gradient(direction, mag=2, kappa=2)
45
+
46
+ def test_time_dependent_PDE_model_multiple_input():
47
+ """ Test that the time dependent PDE model and gradient can accept multiple inputs specified as positional arguments or keyword arguments """
48
+ pde_test_model = MultipleInputTestModel.helper_build_time_dependent_PDE_test_model()
49
+ pde_test_model.populate_model_variations()
50
+ CUQI_pde = pde_test_model.model_variations[1] # PDE model with multiple inputs
51
+
52
+ # Check that the model has correct parameter name
53
+ assert CUQI_pde._non_default_args == ['mag', 'IC']
54
+
55
+ # Check that we can provide parameter names when evaluating the model
56
+ mag = 2
57
+ IC_ = np.random.randn(CUQI_pde.domain_geometry.geometries[1].par_dim)
58
+ output1 = CUQI_pde(mag=mag, IC=IC_)
59
+
60
+ # And check that we can provide positional arguments
61
+ output2 = CUQI_pde(mag, IC_)
62
+
63
+ # Check that the two outputs are the same
64
+ assert np.allclose(output1, output2)
65
+
66
+ # Assert evaluating gradient works
67
+ direction = np.random.randn(CUQI_pde.range_dim)
68
+
69
+ # Make sure gradient can be computed with positional or keyword arguments
70
+ grad1 = CUQI_pde.gradient(direction, mag=mag, IC=IC_)
71
+ grad2 = CUQI_pde.gradient(direction, mag, IC_)
72
+
73
+ # Passing wrong kwargs should raise an error
74
+ with pytest.raises(
75
+ ValueError,
76
+ match=r"The gradient input is specified by a direction and keywords arguments \['mag', 'IC_value'\] that does not match the non_default_args of the model \['mag', 'IC'\].",
77
+ ):
78
+ CUQI_pde.gradient(direction, mag=mag, IC_value=IC_)
79
+
32
80
  def test_constructing_gradient_from_jacobian():
33
81
  """ Test that the gradient is correctly constructed from the
34
82
  jacobian when only the jacobian is specified """
@@ -392,18 +440,17 @@ class MultipleInputTestModel:
392
440
 
393
441
  def populate_model_variations(self):
394
442
  """Populate the `model_variations` list with different variations of the model that share the same forward map but differ in some other aspect like gradient, jacobian, etc."""
443
+
395
444
  if self.pde is not None:
396
- self.populate_pde_model_variations()
397
- elif self.forward_map is not None:
398
- self.populate_general_model_variations()
445
+ first_kwarg = {"PDE": self.pde}
446
+ else:
447
+ first_kwarg = {"forward": self.forward_map}
399
448
 
400
- def populate_general_model_variations(self):
401
- """Populate the `model_variations` for general models that are not PDE models."""
402
449
  # Model with forward only
403
450
  model = self.model_class(
404
- forward=self.forward_map,
451
+ **first_kwarg,
405
452
  domain_geometry=self.domain_geometry,
406
- range_geometry=self.range_geometry,
453
+ range_geometry=self.range_geometry
407
454
  )
408
455
  model._do_test_gradient = False # do not test this model for gradient
409
456
  self.model_variations.append(model)
@@ -411,7 +458,7 @@ class MultipleInputTestModel:
411
458
  # Model with gradient of from 1 (callable)
412
459
  if self.gradient_form1 is not None:
413
460
  model = self.model_class(
414
- forward=self.forward_map,
461
+ **first_kwarg,
415
462
  gradient=self.gradient_form1,
416
463
  domain_geometry=self.domain_geometry,
417
464
  range_geometry=self.range_geometry,
@@ -421,7 +468,7 @@ class MultipleInputTestModel:
421
468
  # Model with gradient of from 2 (tuple of callables)
422
469
  if self.gradient_form2 is not None:
423
470
  model = self.model_class(
424
- forward=self.forward_map,
471
+ **first_kwarg,
425
472
  gradient=self.gradient_form2,
426
473
  domain_geometry=self.domain_geometry,
427
474
  range_geometry=self.range_geometry,
@@ -431,7 +478,7 @@ class MultipleInputTestModel:
431
478
  # Model with gradient of from 2 incomplete (tuple of callables with some None elements)
432
479
  if self.gradient_form2_incomplete is not None:
433
480
  model = self.model_class(
434
- forward=self.forward_map,
481
+ **first_kwarg,
435
482
  gradient=self.gradient_form2_incomplete,
436
483
  domain_geometry=self.domain_geometry,
437
484
  range_geometry=self.range_geometry,
@@ -441,7 +488,7 @@ class MultipleInputTestModel:
441
488
  # Model with jacobian of from 1 (callable)
442
489
  if self.jacobian_form1 is not None:
443
490
  model = self.model_class(
444
- forward=self.forward_map,
491
+ **first_kwarg,
445
492
  jacobian=self.jacobian_form1,
446
493
  domain_geometry=self.domain_geometry,
447
494
  range_geometry=self.range_geometry,
@@ -451,7 +498,7 @@ class MultipleInputTestModel:
451
498
  # Model with jacobian of from 2 (tuple of callables)
452
499
  if self.jacobian_form2 is not None:
453
500
  model = self.model_class(
454
- forward=self.forward_map,
501
+ **first_kwarg,
455
502
  jacobian=self.jacobian_form2,
456
503
  domain_geometry=self.domain_geometry,
457
504
  range_geometry=self.range_geometry,
@@ -461,24 +508,13 @@ class MultipleInputTestModel:
461
508
  # Model with jacobian of from 2 incomplete (tuple of callables with some None elements)
462
509
  if self.jacobian_form2_incomplete is not None:
463
510
  model = self.model_class(
464
- forward=self.forward_map,
511
+ **first_kwarg,
465
512
  jacobian=self.jacobian_form2_incomplete,
466
513
  domain_geometry=self.domain_geometry,
467
514
  range_geometry=self.range_geometry,
468
515
  )
469
516
  self.model_variations.append(model)
470
517
 
471
- def populate_pde_model_variations(self):
472
- """Populate the `model_variations` for PDE models."""
473
- # Model with PDE (no gradient)
474
- model = self.model_class(
475
- self.pde,
476
- domain_geometry=self.domain_geometry,
477
- range_geometry=self.range_geometry,
478
- )
479
- model._do_test_gradient = False # do not test this model for gradient
480
- self.model_variations.append(model)
481
-
482
518
  @staticmethod
483
519
  def create_model_test_case_combinations():
484
520
  """Create all combinations of test model variations and test cases
@@ -510,6 +546,16 @@ class MultipleInputTestModel:
510
546
  TestCase.create_test_cases_for_test_model(test_model)
511
547
  test_model_list.append(test_model)
512
548
 
549
+ # Model 4
550
+ test_model = MultipleInputTestModel.helper_build_time_dependent_PDE_test_model()
551
+ test_model.populate_model_variations()
552
+ test_model.input_bounds = [
553
+ 0.1,
554
+ 4,
555
+ ] # choose input from uniform distribution in [0.1, 4]
556
+ TestCase.create_test_cases_for_test_model(test_model)
557
+ test_model_list.append(test_model)
558
+
513
559
  # Append all combinations of test model variations and test cases
514
560
  # to model_test_case_combinations
515
561
  for test_model in test_model_list:
@@ -589,7 +635,7 @@ class MultipleInputTestModel:
589
635
 
590
636
  @staticmethod
591
637
  def helper_build_steady_state_PDE_test_model():
592
- """Build a PDE model with a steady state Poisson equation and two inputs: mag and kappa_scale. This model does not have gradient or jacobian functions."""
638
+ """Build a PDE model with a steady state Poisson equation and two inputs: mag and kappa_scale."""
593
639
 
594
640
  # Poisson equation setup
595
641
  dim = 20 # Number of nodes
@@ -635,6 +681,68 @@ class MultipleInputTestModel:
635
681
  test_model.domain_geometry = (Discrete(["mag"]), Discrete(["kappa_scale"]))
636
682
  test_model.range_geometry = Continuous1D(len(grid_obs))
637
683
 
684
+ # Gradient with respect to mag
685
+ def gradient_mag(direction, mag, kappa_scale):
686
+ def fwd_mag(mag_):
687
+ CUQI_pde.assemble(mag_, kappa_scale)
688
+ u, _ = CUQI_pde.solve()
689
+ obs_u = CUQI_pde.observe(u)
690
+ return obs_u
691
+ mag = mag.to_numpy() if isinstance(mag, CUQIarray) else mag
692
+ return direction @ cuqi.utilities.approx_derivative(fwd_mag, mag)
693
+
694
+ # Gradient with respect to kappa_scale
695
+ def gradient_kappa_scale(direction, mag, kappa_scale):
696
+ def fwd_kappa_scale(kappa_scale_):
697
+ CUQI_pde.assemble(mag, kappa_scale_)
698
+ u, _ = CUQI_pde.solve()
699
+ obs_u = CUQI_pde.observe(u)
700
+ return obs_u
701
+ kappa_scale = kappa_scale.to_numpy() if isinstance(kappa_scale, CUQIarray) else kappa_scale
702
+ return direction @ cuqi.utilities.approx_derivative(fwd_kappa_scale, kappa_scale)
703
+
704
+ # Gradient with respect to all inputs (form 1, callable)
705
+ def gradient_form1(direction, mag, kappa_scale):
706
+ grad_mag = gradient_mag(direction, mag, kappa_scale)
707
+ grad_kappa_scale = gradient_kappa_scale(direction, mag, kappa_scale)
708
+ return (grad_mag, grad_kappa_scale)
709
+
710
+ # Assign the gradient functions to the test model
711
+ test_model.gradient_form1 = gradient_form1
712
+ test_model.gradient_form2 = (gradient_mag, gradient_kappa_scale)
713
+ test_model.gradient_form2_incomplete = (gradient_mag, None)
714
+
715
+ # Jacobian with respect to mag
716
+ def jacobian_mag(mag, kappa_scale):
717
+ def fwd_mag(mag_):
718
+ CUQI_pde.assemble(mag_, kappa_scale)
719
+ u, _ = CUQI_pde.solve()
720
+ obs_u = CUQI_pde.observe(u)
721
+ return obs_u
722
+ mag = mag.to_numpy() if isinstance(mag, CUQIarray) else mag
723
+ return cuqi.utilities.approx_derivative(fwd_mag, mag).reshape(-1, 1)
724
+
725
+ # Jacobian with respect to kappa_scale
726
+ def jacobian_kappa_scale(mag, kappa_scale):
727
+ def fwd_kappa_scale(kappa_scale_):
728
+ CUQI_pde.assemble(mag, kappa_scale_)
729
+ u, _ = CUQI_pde.solve()
730
+ obs_u = CUQI_pde.observe(u)
731
+ return obs_u
732
+ kappa_scale = kappa_scale.to_numpy() if isinstance(kappa_scale, CUQIarray) else kappa_scale
733
+ return cuqi.utilities.approx_derivative(fwd_kappa_scale, kappa_scale).reshape(-1, 1)
734
+
735
+ # Jacobian with respect to all inputs (form 1, callable)
736
+ def jacobian_form1(mag, kappa_scale):
737
+ jac_mag = jacobian_mag(mag, kappa_scale)
738
+ jac_kappa_scale = jacobian_kappa_scale(mag, kappa_scale)
739
+ return (jac_mag, jac_kappa_scale)
740
+
741
+ # Assign the jacobian functions to the test model
742
+ test_model.jacobian_form1 = jacobian_form1
743
+ test_model.jacobian_form2 = (jacobian_mag, jacobian_kappa_scale)
744
+ test_model.jacobian_form2_incomplete = (jacobian_mag, None)
745
+
638
746
  return test_model
639
747
 
640
748
  @staticmethod
@@ -699,6 +807,103 @@ class MultipleInputTestModel:
699
807
  test_model.model_class = cuqi.model.Model
700
808
  return test_model
701
809
 
810
+ @staticmethod
811
+ def helper_build_time_dependent_PDE_test_model():
812
+ """Build a PDE model with a time-dependent PDE and two inputs: mag, and IC."""
813
+
814
+ # Prepare PDE form
815
+ N = 20 # Number of solution nodes
816
+ endpoint = 1.0 # Length of the domain
817
+ max_time = 0.1 # Maximum time
818
+ dx = endpoint/(N+1) # space step size
819
+ cfl = 5/11 # the cfl condition to have a stable solution
820
+ dt_approx = cfl*dx**2 # defining approximate time step size
821
+ max_iter = int(max_time/dt_approx) # number of time steps
822
+ Dxx_matr = (np.diag( -2*np.ones(N) ) + np.diag(np.ones(N-1),-1) + np.diag(np.ones(N-1),1))/dx**2
823
+ Dxx = lambda mag: mag * Dxx_matr # FD diffusion operator
824
+
825
+ # Grids for model
826
+ grid_domain = np.linspace(dx, endpoint, N, endpoint=False)
827
+ grid_range = np.linspace(dx, endpoint, N, endpoint=False)
828
+ time_steps = np.linspace(0,max_time,max_iter+1,endpoint=True)
829
+
830
+ # PDE form (mag, IC, time)
831
+ def PDE_form(mag, IC, t): return (Dxx(mag), np.zeros(N), IC)
832
+ PDE = cuqi.pde.TimeDependentLinearPDE(
833
+ PDE_form, time_steps, grid_sol=grid_domain, grid_obs=grid_range, method='backward_euler')
834
+
835
+ # Build the test model
836
+ test_model = MultipleInputTestModel()
837
+ test_model.model_class = cuqi.model.PDEModel
838
+ test_model.pde = PDE
839
+ test_model.domain_geometry = (Discrete(["mag"]), Continuous1D(grid_domain))
840
+ test_model.range_geometry = Continuous1D(grid_range)
841
+
842
+ test_model.model_class = cuqi.model.PDEModel
843
+
844
+ # Gradient with respect to mag
845
+ def gradient_mag(direction, mag, IC):
846
+ def fwd_mag(mag_):
847
+ PDE.assemble(mag_, IC)
848
+ u, _ = PDE.solve()
849
+ obs_u = PDE.observe(u)
850
+ return obs_u
851
+ mag = mag.to_numpy() if isinstance(mag, CUQIarray) else mag
852
+ return direction @ cuqi.utilities.approx_derivative(fwd_mag, mag)
853
+
854
+ # Gradient with respect to IC
855
+ def gradient_IC(direction, mag, IC):
856
+ def fwd_IC(IC_):
857
+ PDE.assemble(mag, IC_)
858
+ u, _ = PDE.solve()
859
+ obs_u = PDE.observe(u)
860
+ return obs_u
861
+ IC = IC.to_numpy() if isinstance(IC, CUQIarray) else IC
862
+ return direction @ cuqi.utilities.approx_derivative(fwd_IC, IC)
863
+
864
+ # Gradient with respect to all inputs (form 1, callable)
865
+ def gradient_form1(direction, mag, IC):
866
+ grad_mag = gradient_mag(direction, mag, IC)
867
+ grad_IC = gradient_IC(direction, mag, IC)
868
+ return (grad_mag, grad_IC)
869
+
870
+ # Assign the gradient functions to the test model
871
+ test_model.gradient_form1 = gradient_form1
872
+ test_model.gradient_form2 = (gradient_mag, gradient_IC)
873
+ test_model.gradient_form2_incomplete = (gradient_mag, None)
874
+
875
+ # Jacobian with respect to mag
876
+ def jacobian_mag(mag, IC):
877
+ def fwd_mag(mag_):
878
+ PDE.assemble(mag_, IC)
879
+ u, _ = PDE.solve()
880
+ obs_u = PDE.observe(u)
881
+ return obs_u
882
+ mag = mag.to_numpy() if isinstance(mag, CUQIarray) else mag
883
+ return cuqi.utilities.approx_derivative(fwd_mag, mag)
884
+
885
+ # Jacobian with respect to IC
886
+ def jacobian_IC(mag, IC):
887
+ def fwd_IC(IC_):
888
+ PDE.assemble(mag, IC_)
889
+ u, _ = PDE.solve()
890
+ obs_u = PDE.observe(u)
891
+ return obs_u
892
+ IC = IC.to_numpy() if isinstance(IC, CUQIarray) else IC
893
+ return cuqi.utilities.approx_derivative(fwd_IC, IC)
894
+
895
+ # Jacobian with respect to all inputs (form 1, callable)
896
+ def jacobian_form1(mag, IC):
897
+ jac_mag = jacobian_mag(mag, IC)
898
+ jac_IC = jacobian_IC(mag, IC)
899
+ return (jac_mag, jac_IC)
900
+
901
+ # Assign the jacobian functions to the test model
902
+ test_model.jacobian_form1 = jacobian_form1
903
+ test_model.jacobian_form2 = (jacobian_mag, jacobian_IC)
904
+ test_model.jacobian_form2_incomplete = (jacobian_mag, None)
905
+
906
+ return test_model
702
907
 
703
908
  class TestCase:
704
909
  """Class representing a test case for a test model. A test case consists of the input values, the expected output values, and the expected output types and error messages."""
@@ -1754,7 +1959,7 @@ def test_linear_model_allow_other_parameter_names():
1754
1959
  # Check providing the wrong parameter name raises an error
1755
1960
  with pytest.raises(
1756
1961
  ValueError,
1757
- match=r"The model input is specified by a keywords arguments \['y'\] that does not match the non_default_args of the model \['x'\].",
1962
+ match=r"The model input is specified by keywords arguments \['y'\] that does not match the non_default_args of the model \['x'\].",
1758
1963
  ):
1759
1964
  model_x_w(y=1)
1760
1965
 
@@ -1779,7 +1984,7 @@ def test_linear_model_allow_other_parameter_names():
1779
1984
  # Check providing the wrong parameter name raises an error
1780
1985
  with pytest.raises(
1781
1986
  ValueError,
1782
- match=r"The adjoint input is specified by a keywords arguments \['v'\] that does not match the non_default_args of the adjoint \['w'\].",
1987
+ match=r"The adjoint input is specified by keywords arguments \['v'\] that does not match the non_default_args of the adjoint \['w'\].",
1783
1988
  ):
1784
1989
  model_x_w.adjoint(v=1)
1785
1990
 
@@ -1938,4 +2143,4 @@ def test_setting_domain_and_range_geometry(domain_geometry, range_geometry, num_
1938
2143
 
1939
2144
  # Check the model domain and range geometries are of the expected type
1940
2145
  assert model.domain_geometry == expected_domain_geometry
1941
- assert model.range_geometry == expected_range_geometry
2146
+ assert model.range_geometry == expected_range_geometry