optPricing 2.0.0__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.
Files changed (92) hide show
  1. optpricing-2.0.0.dist-info/METADATA +105 -0
  2. optpricing-2.0.0.dist-info/RECORD +92 -0
  3. optpricing-2.0.0.dist-info/WHEEL +5 -0
  4. optpricing-2.0.0.dist-info/entry_points.txt +3 -0
  5. optpricing-2.0.0.dist-info/licenses/LICENSE +9 -0
  6. optpricing-2.0.0.dist-info/top_level.txt +1 -0
  7. quantfin/__init__.py +101 -0
  8. quantfin/atoms/__init__.py +21 -0
  9. quantfin/atoms/bond.py +28 -0
  10. quantfin/atoms/option.py +70 -0
  11. quantfin/atoms/rate.py +46 -0
  12. quantfin/atoms/stock.py +39 -0
  13. quantfin/calibration/__init__.py +25 -0
  14. quantfin/calibration/calibrator.py +155 -0
  15. quantfin/calibration/fit_jump_parameters.py +66 -0
  16. quantfin/calibration/fit_market_params.py +120 -0
  17. quantfin/calibration/iv_surface.py +155 -0
  18. quantfin/calibration/technique_selector.py +47 -0
  19. quantfin/calibration/vectorized_bsm_iv.py +93 -0
  20. quantfin/calibration/vectorized_integration_iv.py +143 -0
  21. quantfin/cli.py +372 -0
  22. quantfin/config.py +57 -0
  23. quantfin/dashboard/__init__.py +14 -0
  24. quantfin/dashboard/app.py +26 -0
  25. quantfin/dashboard/pages/1_Calibration.py +57 -0
  26. quantfin/dashboard/pages/2_Pricer_and_Greeks.py +150 -0
  27. quantfin/dashboard/pages/3_Financial_Tools.py +69 -0
  28. quantfin/dashboard/pages/__init__.py +0 -0
  29. quantfin/dashboard/plots.py +157 -0
  30. quantfin/dashboard/service.py +176 -0
  31. quantfin/dashboard/widgets.py +87 -0
  32. quantfin/data/__init__.py +26 -0
  33. quantfin/data/historical_manager.py +86 -0
  34. quantfin/data/market_data_manager.py +236 -0
  35. quantfin/models/__init__.py +54 -0
  36. quantfin/models/base/__init__.py +16 -0
  37. quantfin/models/base/base_model.py +198 -0
  38. quantfin/models/base/validators.py +95 -0
  39. quantfin/models/bates.py +192 -0
  40. quantfin/models/blacks_approx.py +134 -0
  41. quantfin/models/bsm.py +262 -0
  42. quantfin/models/cev.py +109 -0
  43. quantfin/models/cgmy.py +189 -0
  44. quantfin/models/cir.py +90 -0
  45. quantfin/models/dupire_local.py +67 -0
  46. quantfin/models/heston.py +204 -0
  47. quantfin/models/hyperbolic.py +170 -0
  48. quantfin/models/kou.py +148 -0
  49. quantfin/models/merton_jump.py +225 -0
  50. quantfin/models/nig.py +180 -0
  51. quantfin/models/perpetual_put.py +115 -0
  52. quantfin/models/sabr.py +64 -0
  53. quantfin/models/sabr_jump.py +68 -0
  54. quantfin/models/vasicek.py +88 -0
  55. quantfin/models/vg.py +178 -0
  56. quantfin/parity/__init__.py +13 -0
  57. quantfin/parity/implied_rate.py +110 -0
  58. quantfin/parity/parity_model.py +178 -0
  59. quantfin/techniques/__init__.py +31 -0
  60. quantfin/techniques/base/__init__.py +21 -0
  61. quantfin/techniques/base/base_technique.py +54 -0
  62. quantfin/techniques/base/greek_mixin.py +265 -0
  63. quantfin/techniques/base/iv_mixin.py +116 -0
  64. quantfin/techniques/base/lattice_technique.py +129 -0
  65. quantfin/techniques/base/pricing_result.py +27 -0
  66. quantfin/techniques/base/random_utils.py +31 -0
  67. quantfin/techniques/closed_form.py +296 -0
  68. quantfin/techniques/crr.py +59 -0
  69. quantfin/techniques/fft.py +178 -0
  70. quantfin/techniques/integration.py +202 -0
  71. quantfin/techniques/kernels/__init__.py +37 -0
  72. quantfin/techniques/kernels/lattice_kernels.py +286 -0
  73. quantfin/techniques/kernels/mc_kernels.py +337 -0
  74. quantfin/techniques/leisen_reimer.py +59 -0
  75. quantfin/techniques/monte_carlo.py +259 -0
  76. quantfin/techniques/pde.py +155 -0
  77. quantfin/techniques/topm.py +59 -0
  78. quantfin/workflows/__init__.py +14 -0
  79. quantfin/workflows/backtest_workflow.py +133 -0
  80. quantfin/workflows/configs/__init__.py +51 -0
  81. quantfin/workflows/configs/bates_config.py +25 -0
  82. quantfin/workflows/configs/bsm_config.py +9 -0
  83. quantfin/workflows/configs/cev_config.py +16 -0
  84. quantfin/workflows/configs/cgmy_config.py +18 -0
  85. quantfin/workflows/configs/heston_config.py +21 -0
  86. quantfin/workflows/configs/hyperbolic_config.py +18 -0
  87. quantfin/workflows/configs/kou_config.py +16 -0
  88. quantfin/workflows/configs/merton_config.py +18 -0
  89. quantfin/workflows/configs/nig_config.py +13 -0
  90. quantfin/workflows/configs/sabr_config.py +19 -0
  91. quantfin/workflows/configs/vg_config.py +9 -0
  92. quantfin/workflows/daily_workflow.py +150 -0
@@ -0,0 +1,170 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from typing import Any
5
+
6
+ import numpy as np
7
+ from scipy.special import kv
8
+ from scipy.stats import genhyperbolic
9
+
10
+ from quantfin.models.base import CF, BaseModel, ParamValidator
11
+
12
+ __doc__ = """
13
+ Defines the Hyperbolic pure-jump Lévy model.
14
+ """
15
+
16
+
17
+ class HyperbolicModel(BaseModel):
18
+ """Hyperbolic pure-jump Lévy model."""
19
+
20
+ name: str = "Hyperbolic"
21
+ supports_cf: bool = True
22
+ is_pure_levy: bool = True
23
+ default_params = {"alpha": 15.0, "beta": -5.0, "delta": 0.5, "mu": 0.0}
24
+
25
+ def __init__(self, params: dict[str, float] | None = None):
26
+ """
27
+ Initializes the Hyperbolic model.
28
+
29
+ Parameters
30
+ ----------
31
+ params : dict[str, float] | None, optional
32
+ A dictionary of model parameters. If None, `default_params` are used.
33
+ Defaults to None.
34
+ """
35
+ super().__init__(params or self.default_params)
36
+
37
+ def _validate_params(self) -> None:
38
+ p = self.params
39
+ req = ["alpha", "beta", "delta", "mu"]
40
+ ParamValidator.require(p, req, model=self.name)
41
+ ParamValidator.positive(p, ["alpha", "delta"], model=self.name)
42
+ if not (abs(p["beta"]) < p["alpha"]):
43
+ raise ValueError("Hyperbolic params must satisfy |beta| < alpha.")
44
+
45
+ def __eq__(self, other: object) -> bool:
46
+ if not isinstance(other, HyperbolicModel):
47
+ return NotImplemented
48
+ return self.params == other.params
49
+
50
+ def __hash__(self) -> int:
51
+ return hash((self.__class__, tuple(sorted(self.params.items()))))
52
+
53
+ def _cf_impl(
54
+ self,
55
+ *,
56
+ t: float,
57
+ spot: float,
58
+ r: float,
59
+ q: float,
60
+ **_: Any,
61
+ ) -> CF:
62
+ """
63
+ Risk-neutral characteristic function for the log-spot price log(S_t).
64
+
65
+ This is the risk-neutral characteristic function, which includes the
66
+ drift adjustment.
67
+
68
+ Parameters
69
+ ----------
70
+ t : float
71
+ The time to maturity of the option, in years.
72
+ spot : float
73
+ The current price of the underlying asset.
74
+ r : float
75
+ The continuously compounded risk-free rate.
76
+ q : float
77
+ The continuously compounded dividend yield.
78
+
79
+ Returns
80
+ -------
81
+ CF
82
+ The characteristic function.
83
+ """
84
+ p = self.params
85
+ alpha, beta, delta, mu = p["alpha"], p["beta"], p["delta"], p["mu"]
86
+ compensator = np.log(self.raw_cf(t=1.0)(-1j)) # E[exp(X_1)] = phi_raw(-i)
87
+ drift = r - q - compensator
88
+
89
+ def phi(u: np.ndarray | complex):
90
+ return self.raw_cf(t=t)(u) * np.exp(1j * u * (np.log(spot) + drift * t))
91
+
92
+ return phi
93
+
94
+ def raw_cf(self, *, t: float) -> Callable:
95
+ """
96
+ Returns the CF of the raw Hyperbolic process, without drift or spot.
97
+
98
+ This function represents the characteristic function of the process
99
+ before the risk-neutral drift adjustment is applied.
100
+
101
+ Parameters
102
+ ----------
103
+ t : float
104
+ The time horizon.
105
+
106
+ Returns
107
+ -------
108
+ Callable
109
+ The raw characteristic function.
110
+ """
111
+ p = self.params
112
+ alpha, beta, delta, mu = p["alpha"], p["beta"], p["delta"], p["mu"]
113
+ gamma_0 = np.sqrt(alpha**2 - beta**2)
114
+
115
+ def phi_raw(u: np.ndarray | complex) -> np.ndarray | complex:
116
+ gamma_u = np.sqrt(alpha**2 - (beta + 1j * u) ** 2)
117
+ term1 = np.exp(1j * u * mu * t)
118
+ term2 = (gamma_0 / gamma_u) ** t * (
119
+ kv(1, delta * t * gamma_u) / kv(1, delta * t * gamma_0)
120
+ )
121
+ return term1 * term2
122
+
123
+ return phi_raw
124
+
125
+ def sample_terminal_log_return(
126
+ self,
127
+ T: float,
128
+ size: int,
129
+ rng: np.random.Generator,
130
+ ) -> np.ndarray:
131
+ """
132
+ Generates exact samples of the terminal log-return for the Hyperbolic process.
133
+
134
+ This uses the `scipy.stats.genhyperbolic` distribution, which is the
135
+ exact distribution of the process at a given time horizon.
136
+
137
+ Parameters
138
+ ----------
139
+ T : float
140
+ The time to maturity, in years.
141
+ size : int
142
+ The number of samples to generate.
143
+ rng : np.random.Generator
144
+ A random number generator instance for reproducibility.
145
+
146
+ Returns
147
+ -------
148
+ np.ndarray
149
+ An array of simulated terminal log-returns.
150
+ """
151
+ p = self.params
152
+ return genhyperbolic.rvs(
153
+ p=1.0,
154
+ a=p["alpha"],
155
+ b=p["beta"],
156
+ loc=p["mu"] * T,
157
+ scale=p["delta"] * T,
158
+ size=size,
159
+ random_state=rng,
160
+ )
161
+
162
+ # Abstract Method Implementations
163
+ def _sde_impl(self, **kwargs: Any) -> Any:
164
+ raise NotImplementedError
165
+
166
+ def _pde_impl(self, **kwargs: Any) -> Any:
167
+ raise NotImplementedError
168
+
169
+ def _closed_form_impl(self, **kwargs: Any) -> Any:
170
+ raise NotImplementedError
quantfin/models/kou.py ADDED
@@ -0,0 +1,148 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ import numpy as np
6
+
7
+ from quantfin.models.base import CF, BaseModel, ParamValidator
8
+
9
+ __doc__ = """
10
+ Defines the Kou double-exponential jump-diffusion model.
11
+ """
12
+
13
+
14
+ class KouModel(BaseModel):
15
+ """Kou double-exponential jump-diffusion model."""
16
+
17
+ name: str = "Kou Double-Exponential Jump"
18
+ supports_cf: bool = True
19
+ supports_sde: bool = True
20
+ has_jumps: bool = True
21
+
22
+ default_params = {
23
+ "sigma": 0.15,
24
+ "lambda": 1.0,
25
+ "p_up": 0.6,
26
+ "eta1": 10.0,
27
+ "eta2": 5.0,
28
+ }
29
+ param_defs = {
30
+ "sigma": {
31
+ "label": "Volatility",
32
+ "default": 0.15,
33
+ "min": 0.01,
34
+ "max": 1.0,
35
+ "step": 0.01,
36
+ },
37
+ "lambda": {
38
+ "label": "Jump Intensity",
39
+ "default": 1.0,
40
+ "min": 0.0,
41
+ "max": 10.0,
42
+ "step": 0.1,
43
+ },
44
+ "p_up": {
45
+ "label": "Up Prob",
46
+ "default": 0.6,
47
+ "min": 0.0,
48
+ "max": 1.0,
49
+ "step": 0.05,
50
+ },
51
+ "eta1": {
52
+ "label": "Up Size",
53
+ "default": 10.0,
54
+ "min": 0.1,
55
+ "max": 50.0,
56
+ "step": 0.5,
57
+ },
58
+ "eta2": {
59
+ "label": "Down Size",
60
+ "default": 5.0,
61
+ "min": 0.1,
62
+ "max": 50.0,
63
+ "step": 0.5,
64
+ },
65
+ }
66
+
67
+ def __init__(self, params: dict[str, float] | None = None):
68
+ """
69
+ Initializes the Kou Double-Exponential Jump model.
70
+
71
+ Parameters
72
+ ----------
73
+ params : dict[str, float] | None, optional
74
+ A dictionary of model parameters. If None, `default_params` are used.
75
+ """
76
+ super().__init__(params or self.default_params)
77
+
78
+ def _validate_params(self) -> None:
79
+ """Validate the models parameter."""
80
+ p = self.params
81
+ req = ["sigma", "lambda", "p_up", "eta1", "eta2"]
82
+ ParamValidator.require(p, req, model=self.name)
83
+ ParamValidator.positive(p, ["sigma", "lambda", "eta1", "eta2"], model=self.name)
84
+ ParamValidator.bounded(p, "p_up", 0.0, 1.0, model=self.name)
85
+
86
+ def __eq__(self, other: object) -> bool:
87
+ if not isinstance(other, KouModel):
88
+ return NotImplemented
89
+ return self.params == other.params
90
+
91
+ def __hash__(self) -> int:
92
+ return hash((self.__class__, tuple(sorted(self.params.items()))))
93
+
94
+ def _cf_impl(self, *, t: float, spot: float, r: float, q: float, **_: Any) -> CF:
95
+ """
96
+ Kou characteristic function for the log-spot price log(S_t).
97
+
98
+ Parameters
99
+ ----------
100
+ t : float
101
+ The time to maturity of the option, in years.
102
+ spot : float
103
+ The current price of the underlying asset.
104
+ r : float
105
+ The continuously compounded risk-free rate.
106
+ q : float
107
+ The continuously compounded dividend yield.
108
+
109
+ Returns
110
+ -------
111
+ CF
112
+ The characteristic function.
113
+ """
114
+ p = self.params
115
+ sigma, lambda_, p_up, eta1, eta2 = (
116
+ p["sigma"],
117
+ p["lambda"],
118
+ p["p_up"],
119
+ p["eta1"],
120
+ p["eta2"],
121
+ )
122
+
123
+ compensator = lambda_ * (p_up / (eta1 - 1) - (1 - p_up) / (eta2 + 1))
124
+ drift = r - q - 0.5 * sigma**2 - compensator
125
+
126
+ def phi(u: np.ndarray | complex) -> np.ndarray | complex:
127
+ # BSM component
128
+ bsm_part = 1j * u * (np.log(spot) + drift * t) - 0.5 * u**2 * sigma**2 * t
129
+ # Jump component formulation
130
+ jump_part = (
131
+ lambda_
132
+ * t
133
+ * (p_up / (eta1 - 1j * u) + (1 - p_up) / (eta2 + 1j * u) - 1)
134
+ )
135
+
136
+ return np.exp(bsm_part + jump_part)
137
+
138
+ return phi
139
+
140
+ # Abstract Method Implementations
141
+ def _sde_impl(self, **kwargs: Any) -> Any:
142
+ raise NotImplementedError("Kou uses a specialized kernel.")
143
+
144
+ def _pde_impl(self, **kwargs: Any) -> Any:
145
+ raise NotImplementedError
146
+
147
+ def _closed_form_impl(self, **kwargs: Any) -> Any:
148
+ raise NotImplementedError
@@ -0,0 +1,225 @@
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ from typing import Any
5
+
6
+ import numpy as np
7
+ from scipy.special import factorial
8
+
9
+ from quantfin.models.base import CF, BaseModel, ParamValidator
10
+ from quantfin.models.bsm import BSMModel
11
+
12
+ __doc__ = """
13
+ Defines the Merton jump-diffusion model.
14
+ """
15
+
16
+
17
+ class MertonJumpModel(BaseModel):
18
+ """
19
+ Merton Jump-Diffusion model, implemented as a Poisson-weighted sum of BSM prices.
20
+ """
21
+
22
+ name: str = "Merton Jump-Diffusion"
23
+ supports_cf: bool = True
24
+ has_closed_form: bool = True
25
+ supports_sde: bool = True
26
+ has_jumps: bool = True
27
+
28
+ default_params = {
29
+ "sigma": 0.2,
30
+ "lambda": 0.5,
31
+ "mu_j": -0.1,
32
+ "sigma_j": 0.15,
33
+ "max_sum_terms": 100,
34
+ }
35
+ param_defs = {
36
+ "sigma": {
37
+ "label": "Volatility",
38
+ "default": 0.2,
39
+ "min": 0.01,
40
+ "max": 1.0,
41
+ "step": 0.01,
42
+ },
43
+ "lambda": {
44
+ "label": "Jump Intensity",
45
+ "default": 0.5,
46
+ "min": 0.0,
47
+ "max": 5.0,
48
+ "step": 0.1,
49
+ },
50
+ "mu_j": {
51
+ "label": "Mean Jump",
52
+ "default": -0.1,
53
+ "min": -1.0,
54
+ "max": 1.0,
55
+ "step": 0.05,
56
+ },
57
+ "sigma_j": {
58
+ "label": "Jump Vol",
59
+ "default": 0.15,
60
+ "min": 0.01,
61
+ "max": 1.0,
62
+ "step": 0.01,
63
+ },
64
+ }
65
+
66
+ def __init__(self, params: dict[str, float]):
67
+ """
68
+ Initializes the Merton Jump-Diffusion model.
69
+ Uses BSMModel instance for the core diffusion
70
+
71
+ Parameters
72
+ ----------
73
+ params : dict[str, float]
74
+ A dictionary of model parameters.
75
+ """
76
+ super().__init__(params)
77
+ self.bsm_solver = BSMModel(params={"sigma": self.params["sigma"]})
78
+
79
+ def _validate_params(self) -> None:
80
+ """Validates parameters for diffusion and jump components."""
81
+ p = self.params
82
+ req = ["lambda", "mu_j", "sigma_j", "sigma", "max_sum_terms"]
83
+ ParamValidator.require(p, req, model=self.name)
84
+ ParamValidator.positive(
85
+ p, ["lambda", "sigma_j", "sigma", "max_sum_terms"], model=self.name
86
+ )
87
+
88
+ def _closed_form_impl(
89
+ self,
90
+ *,
91
+ spot: float,
92
+ strike: float,
93
+ r: float,
94
+ q: float,
95
+ t: float,
96
+ call: bool = True,
97
+ ) -> float:
98
+ """
99
+ Merton's closed-form solution as a sum of BSM prices.
100
+
101
+ Parameters
102
+ ----------
103
+ spot : float
104
+ The current price of the underlying asset.
105
+ strike : float
106
+ The strike price of the option.
107
+ r : float
108
+ The continuously compounded risk-free rate.
109
+ q : float
110
+ The continuously compounded dividend yield.
111
+ t : float
112
+ The time to maturity of the option, in years.
113
+ call : bool, optional
114
+ True for a call option, False for a put. Defaults to True.
115
+
116
+ Returns
117
+ -------
118
+ float
119
+ The price of the European option.
120
+ """
121
+ lambda_ = self.params["lambda"]
122
+ mu_j = self.params["mu_j"]
123
+ sigma_j = self.params["sigma_j"]
124
+ max_sum_terms = int(self.params["max_sum_terms"])
125
+
126
+ k = math.exp(mu_j + 0.5 * sigma_j**2) - 1
127
+ lambda_prime = lambda_ * (1 + k)
128
+ y_mul = lambda_prime * t
129
+
130
+ interval = np.arange(max_sum_terms)
131
+ weights = np.exp(-y_mul) * (y_mul**interval) / factorial(interval)
132
+
133
+ r_n = r - lambda_ * k + (interval * (mu_j + 0.5 * sigma_j**2)) / t
134
+ sigma_n_sq = self.params["sigma"] ** 2 + (interval * sigma_j**2) / t
135
+ sigma_n = np.sqrt(np.maximum(sigma_n_sq, 1e-12))
136
+
137
+ total_price = 0.0
138
+ for i in range(max_sum_terms):
139
+ # Terminate sum early if weights become negligible
140
+ if weights[i] < 1e-12 and i > lambda_prime * t:
141
+ break
142
+
143
+ temp_bsm_solver = self.bsm_solver.with_params(sigma=sigma_n[i])
144
+
145
+ price_term = temp_bsm_solver.price_closed_form(
146
+ spot=spot, strike=strike, r=r_n[i], q=q, t=t, call=call
147
+ )
148
+ total_price += weights[i] * price_term
149
+
150
+ return total_price
151
+
152
+ def _cf_impl(
153
+ self,
154
+ *,
155
+ t: float,
156
+ spot: float,
157
+ r: float,
158
+ q: float,
159
+ **_: Any,
160
+ ) -> CF:
161
+ """
162
+ Merton characteristic function for the log-spot price log(S_t).
163
+
164
+ Parameters
165
+ ----------
166
+ t : float
167
+ The time to maturity of the option, in years.
168
+ spot : float
169
+ The current price of the underlying asset.
170
+ r : float
171
+ The continuously compounded risk-free rate.
172
+ q : float
173
+ The continuously compounded dividend yield.
174
+
175
+ Returns
176
+ -------
177
+ CF
178
+ The characteristic function.
179
+ """
180
+ p = self.params
181
+ sigma, lambda_, mu_j, sigma_j = p["sigma"], p["lambda"], p["mu_j"], p["sigma_j"]
182
+ drift = r - q - 0.5 * sigma**2
183
+ compensator = lambda_ * (np.exp(mu_j + 0.5 * sigma_j**2) - 1)
184
+
185
+ def phi(u: np.ndarray | complex) -> np.ndarray | complex:
186
+ bsm_part = (
187
+ 1j * u * (np.log(spot) + (drift - compensator) * t)
188
+ - 0.5 * u**2 * sigma**2 * t
189
+ )
190
+ jump_part = (
191
+ lambda_ * t * (np.exp(1j * u * mu_j - 0.5 * u**2 * sigma_j**2) - 1)
192
+ )
193
+ return np.exp(bsm_part + jump_part)
194
+
195
+ return phi
196
+
197
+ def _sde_impl(self):
198
+ p = self.params
199
+ sigma, mu_j, sigma_j, lambda_ = p["sigma"], p["mu_j"], p["sigma_j"], p["lambda"]
200
+ compensator = lambda_ * (np.exp(mu_j + 0.5 * sigma_j**2) - 1)
201
+
202
+ def stepper(
203
+ log_s_t: np.ndarray,
204
+ r: float,
205
+ q: float,
206
+ dt: float,
207
+ dw_t: np.ndarray,
208
+ jump_counts: np.ndarray,
209
+ rng: np.random.Generator,
210
+ ) -> np.ndarray:
211
+ drift_term = (r - q - 0.5 * sigma**2 - compensator) * dt
212
+ next_log_s = log_s_t + drift_term + sigma * dw_t
213
+ paths_with_jumps = np.where(jump_counts > 0)[0]
214
+ for path_idx in paths_with_jumps:
215
+ num_jumps_on_path = jump_counts[path_idx]
216
+ total_jump_size = np.sum(
217
+ rng.normal(loc=mu_j, scale=sigma_j, size=num_jumps_on_path)
218
+ )
219
+ next_log_s[path_idx] += total_jump_size
220
+ return next_log_s
221
+
222
+ return stepper
223
+
224
+ def _pde_impl(self) -> Any:
225
+ raise NotImplementedError(f"{self.name} PDE not implemented")