├── Manual.pdf ├── tests ├── test_pickle_idouble.py ├── test_with_passive.py ├── test_scalars.py ├── test_record_function.py ├── test_simple_recording.py ├── test_external_function.py ├── test_idouble.py ├── test_array_arg.py ├── test_overrides.py ├── test_vector_function_with_ad.py ├── test_linear_interpolation.py ├── test_statistical_functions.py ├── test_idouble_with_recording.py ├── test_manipulation_functions.py └── test_aadc_array.py ├── benchmarks ├── environment.yml ├── settings.py ├── aadc_bench.py ├── jax_bench.py ├── pytorch_bench.py └── tensorflow_bench.py ├── examples ├── py_basic.py ├── example_monkeypatching_numpy.py ├── CurveExample.py ├── differential_machine_learning.py ├── interpolation.ipynb ├── GBM.py ├── splines.py ├── vector_function_with_ad.ipynb ├── solver.py ├── sabr.ipynb ├── 01-PyAADCBasic.ipynb ├── opt.py └── opt_splines.py ├── getting-started ├── 06-stoch_interp.ipynb ├── 01-hello-world.ipynb └── 05-scipy-interop.ipynb ├── QuantLib ├── 06-Serialization.ipynb └── 07-LiveRisk.ipynb └── README.md /Manual.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matlogica/AADC-Python/HEAD/Manual.pdf -------------------------------------------------------------------------------- /tests/test_pickle_idouble.py: -------------------------------------------------------------------------------- 1 | import pickle 2 | 3 | import aadc 4 | 5 | 6 | def test_pickle_idouble() -> None: 7 | pickle.dumps(aadc.idouble(1)) 8 | -------------------------------------------------------------------------------- /benchmarks/environment.yml: -------------------------------------------------------------------------------- 1 | name: aadc_benchmarks 2 | channels: 3 | - conda-forge 4 | - defaults 5 | dependencies: 6 | - python=3.11 7 | - jax 8 | - pytorch-cpu 9 | - tensorflow-cpu 10 | - pip: 11 | - mtalg 12 | -------------------------------------------------------------------------------- /benchmarks/settings.py: -------------------------------------------------------------------------------- 1 | # Simulation parameters 2 | NUM_TIME_STEPS = 500 # Number of time steps 3 | NUM_PATHS = 50000 # Number of paths 4 | 5 | # Model parameters 6 | SPOT = 100.0 7 | STRIKE = 100.0 8 | RISK_FREE_RATE = 0.05 9 | VOLATILITY = 0.2 10 | BARRIER = 90.0 11 | EXPIRY = 1.0 12 | 13 | RANDOM_SEED = 1234 14 | -------------------------------------------------------------------------------- /tests/test_with_passive.py: -------------------------------------------------------------------------------- 1 | import aadc 2 | 3 | 4 | def test_with_passive() -> None: 5 | funcs = aadc.Functions() 6 | funcs.start_recording() 7 | x = aadc.idouble(1.0) 8 | y = aadc.idouble(2.0) 9 | z = aadc.idouble(3.0) 10 | x.mark_as_input() 11 | y.mark_as_input() 12 | z.mark_as_input() 13 | f = aadc.math.exp(x / y + z) + x 14 | with aadc.passive(): 15 | print(float(f)) 16 | f.mark_as_output() 17 | funcs.stop_recording() 18 | funcs.print_passive_extract_locations() 19 | -------------------------------------------------------------------------------- /tests/test_scalars.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import aadc 4 | 5 | 6 | def test_iand_ibool_with_bool() -> None: 7 | funcs = aadc.Functions() 8 | 9 | funcs.start_recording() 10 | 11 | x = aadc.array([1.0, aadc.idouble(2.0)]) 12 | x[1].mark_as_input() 13 | 14 | np.all(x > 0.0) 15 | 16 | 17 | def test_ior_ibool_with_bool() -> None: 18 | funcs = aadc.Functions() 19 | 20 | funcs.start_recording() 21 | 22 | x = aadc.array([1.0, aadc.idouble(2.0)]) 23 | x[1].mark_as_input() 24 | 25 | np.any(x > 0.0) 26 | -------------------------------------------------------------------------------- /tests/test_record_function.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import aadc 4 | 5 | 6 | def test_record() -> None: 7 | mat = np.array([[1.0, 2.0], [-3.0, -4.0], [5.0, 6.0]]) 8 | 9 | def analytics(x, a): 10 | return np.exp(-a * np.dot(mat, x)) 11 | 12 | rec = aadc.record(analytics, np.zeros((2,)), [0.0]) 13 | 14 | test_set = [ 15 | ((1.0, 2.0), 0.0), 16 | ((1.0, 3.0), np.log(2.0)), 17 | ((2.0, -1.0), np.log(3.0)), 18 | ] 19 | 20 | for x, a in test_set: 21 | x = np.array(x) 22 | r = analytics(x, a) 23 | rec.set_params(a) 24 | 25 | assert np.allclose(rec.func(x), r) 26 | assert np.allclose(rec.jac(x), -a * (mat.T * r).T) 27 | -------------------------------------------------------------------------------- /tests/test_simple_recording.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import aadc 4 | from aadc.evaluate_wrappers import evaluate_kernel 5 | from aadc.recording_ctx import record_kernel 6 | 7 | 8 | def test_recording_with_scalar_eval() -> None: 9 | funcs = aadc.Functions() 10 | funcs.start_recording() 11 | x = aadc.idouble(1.0) 12 | y = aadc.idouble(2.0) 13 | z = aadc.idouble(3.0) 14 | xin = x.mark_as_input() 15 | f = aadc.math.exp(x / y + z) + x 16 | fout = f.mark_as_output() 17 | funcs.stop_recording() 18 | 19 | inputs = {xin: 1.0} 20 | request = {fout: [xin]} 21 | 22 | workers = aadc.ThreadPool(1) 23 | aadc.evaluate(funcs, request, inputs, workers) 24 | 25 | 26 | def test_recording_with_scalar_eval_ctx_and_evaluate_args() -> None: 27 | with record_kernel() as kernel: 28 | x = aadc.idouble(1.0) 29 | y = aadc.idouble(2.0) 30 | z = aadc.idouble(3.0) 31 | xin = x.mark_as_input() 32 | f = aadc.math.exp(x / y + z) + x 33 | fout = f.mark_as_output() 34 | 35 | output = evaluate_kernel(kernel, request={fout: [xin]}, inputs={xin: 1.0}, num_threads=1) 36 | 37 | assert np.isclose(output.values[fout].item(), 34.11545196) 38 | assert np.isclose(output.derivs[fout][xin].item(), 17.55772598) 39 | -------------------------------------------------------------------------------- /examples/py_basic.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import numpy as np 4 | 5 | import aadc 6 | 7 | #math.exp = aadc.math.exp 8 | 9 | 10 | def f(x, y, z): 11 | return np.exp(x / y + z) 12 | 13 | 14 | funcs = aadc.Functions() 15 | x = aadc.idouble(1.0) 16 | y = aadc.idouble(2.0) 17 | z = aadc.idouble(3.0) 18 | 19 | funcs.start_recording() 20 | x_arg = x.mark_as_input() 21 | y_arg = y.mark_as_input() 22 | z_arg = z.mark_as_input() 23 | 24 | f = f(x, y, z) + x 25 | 26 | f_res = f.mark_as_output() 27 | funcs.stop_recording() 28 | funcs.print_passive_extract_locations() 29 | 30 | print("f=", f) 31 | 32 | inputs = { 33 | x_arg: (1.0 * np.ones((120))), 34 | y_arg: (2.0), 35 | z_arg: (3.0), 36 | } 37 | 38 | request = {f_res: [x_arg, y_arg, z_arg]} ## key: what output, value: what gradients are needed 39 | 40 | workers = aadc.ThreadPool(4) 41 | res = aadc.evaluate(funcs, request, inputs, workers) 42 | 43 | print("Result", res[0][f_res]) 44 | print("df/dx", res[1][f_res][x_arg]) 45 | print("df/dy", res[1][f_res][y_arg]) 46 | print("df/dz", res[1][f_res][z_arg]) 47 | 48 | if False: 49 | res = aadc.evaluate(funcs, request, inputs) 50 | print("Result", res[0][f_res]) 51 | print("df/dx", res[1][f_res][x_arg]) 52 | print("df/dy", res[1][f_res][y_arg]) 53 | print("df/dz", res[1][f_res][z_arg]) 54 | 55 | print("Done!") 56 | -------------------------------------------------------------------------------- /examples/example_monkeypatching_numpy.py: -------------------------------------------------------------------------------- 1 | import time 2 | from threading import Thread 3 | 4 | import numpy as np 5 | 6 | import aadc.overrides 7 | from aadc import Functions 8 | from aadc.ndarray import AADCArray 9 | 10 | 11 | def problematic_code(log_returns): 12 | asset_price_movements = np.ones((T, assets)) 13 | for t in range(T): 14 | asset_price_movements[t, :] = asset_price_movements[t - 1, :] * np.exp(log_returns) 15 | return asset_price_movements 16 | 17 | 18 | if __name__ == "__main__": 19 | rng = np.random.default_rng(1234) 20 | 21 | assets = 10 22 | T = 100 23 | 24 | log_returns = AADCArray(rng.standard_normal(assets)) 25 | funcs = Functions() 26 | funcs.start_recording() 27 | log_returns.mark_as_input() 28 | 29 | def without_overrides(): 30 | time.sleep(0.5) 31 | print("Executing thread without context manager:") 32 | try: 33 | problematic_code(log_returns) 34 | except Exception as e: 35 | print("Exception occured:", e) 36 | 37 | thread = Thread(target=without_overrides) 38 | thread.start() 39 | 40 | with aadc.overrides.aadc_overrides(): # Could specify a subset here 41 | print("Enabled overrides.") 42 | time.sleep(1.0) 43 | print("Executing within context manager:") 44 | asset = problematic_code(log_returns) 45 | print(type(asset)) 46 | 47 | funcs.stop_recording() 48 | thread.join() 49 | -------------------------------------------------------------------------------- /examples/CurveExample.py: -------------------------------------------------------------------------------- 1 | # --- 2 | # jupyter: 3 | # jupytext: 4 | # formats: py:percent 5 | # text_representation: 6 | # extension: .py 7 | # format_name: percent 8 | # format_version: '1.3' 9 | # jupytext_version: 1.6.0 10 | # kernelspec: 11 | # display_name: Python 3 12 | # language: python 13 | # name: python3 14 | # --- 15 | 16 | # %% [markdown] 17 | # # Cash-flow analysis 18 | # 19 | # Copyright (©) 2020 StatPro Italia srl 20 | # 21 | # This file is part of QuantLib, a free-software/open-source library 22 | # for financial quantitative analysts and developers - https://www.quantlib.org/ 23 | # 24 | # QuantLib is free software: you can redistribute it and/or modify it under the 25 | # terms of the QuantLib license. You should have received a copy of the 26 | # license along with this program; if not, please email 27 | # . The license is also available online at 28 | # . 29 | # 30 | # This program is distributed in the hope that it will be useful, but WITHOUT 31 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 32 | # FOR A PARTICULAR PURPOSE. See the license for more details. 33 | 34 | # %% 35 | import numpy as np 36 | 37 | import aadc 38 | 39 | dates = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 40 | rates = [0.01, 0.02, 0.03, 0.04, 0.05, 0.06, 0.07, 0.08, 0.09, 0.1] # aadc.idouble(0.1)] 41 | print(type(rates)) 42 | irates = [0.01, 0.02, 0.03, 0.04, 0.05, 0.06, 0.07, 0.08, aadc.idouble(0.09), aadc.idouble(0.1)] 43 | print(type(irates)) 44 | iirates = [ 45 | aadc.idouble(0.01), 46 | aadc.idouble(0.02), 47 | aadc.idouble(0.03), 48 | aadc.idouble(0.04), 49 | aadc.idouble(0.05), 50 | aadc.idouble(0.06), 51 | aadc.idouble(0.07), 52 | aadc.idouble(0.08), 53 | aadc.idouble(0.09), 54 | aadc.idouble(0.1), 55 | ] 56 | 57 | nrates = np.ones(10) 58 | 59 | forecast_curve = aadc.MyDemoCurve(nrates, nrates) 60 | nrates[9] = aadc.idouble(0.1) # automatically converts to double and this is OK 61 | forecast_curve = aadc.MyDemoCurve(nrates, nrates) 62 | 63 | 64 | forecast_curve = aadc.MyDemoCurve(dates, rates) 65 | forecast_curve_ii = aadc.MyDemoCurve(dates, iirates) 66 | forecast_curve = aadc.MyDemoCurve(dates, irates) 67 | 68 | print("Start recording") 69 | funcs = aadc.Functions() 70 | funcs.start_recording() 71 | arg1 = irates[9].mark_as_input() 72 | 73 | # nRates[9] = iRates[9] # Exception - Can we store idouble in numpy array? 74 | # nn_forecast_curve = aadc.MyDemoCurve(nRates, nRates) 75 | 76 | forecast_curve = aadc.MyDemoCurve(dates, irates) 77 | forecast_curve_passive = aadc.MyDemoCurve(dates, rates) 78 | 79 | print(forecast_curve(1)) # returns active double 80 | print(forecast_curve_passive(1)) # returns double 81 | print(forecast_curve_passive(aadc.idouble(1))) # returns double 82 | print(forecast_curve_passive(irates[9])) # returns active double 83 | 84 | exp1 = irates[9] + irates[9] 85 | print(exp1) # returns active idouble 86 | 87 | print(irates[8] + irates[8]) # returns native double? 88 | print(float(irates[8] + irates[8])) # returns native double 89 | 90 | try: 91 | print(float(exp1)) # returns exception 92 | except Exception as e: 93 | print(e) 94 | 95 | funcs.stop_recording() 96 | 97 | print("Stop recording") 98 | 99 | 100 | # forecast_curve = aadc.MyDemoCurve(aadc.idouble(0.03)) 101 | 102 | print(forecast_curve(1)) 103 | -------------------------------------------------------------------------------- /examples/differential_machine_learning.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import aadc 3 | 4 | # Example of AADC interface use for Differential Machine Learning. Here we need intermediate state variables with values and adjoints 5 | # See https://github.com/differential-machine-learning 6 | 7 | # Regular python code to calculate payoff of a simple Monte Carlo simulation on just one path 8 | 9 | state = np.array([ 100.0, 10.0]) # initial state of the model. Here we assume we have 2 "assets" 10 | 11 | T = 5 # number of time steps 12 | 13 | payoff = 0.0 14 | for t in range(1, T): 15 | new_state = state + np.random.normal(0, 0.1, state.__len__()) 16 | state = new_state 17 | # if t == T - 1: 18 | # payoff = np.maximum(0, 100 - state) 19 | payoff += np.sum(state) 20 | 21 | print(f"Payoff: {payoff}") 22 | 23 | # AADC version 24 | state = aadc.array([aadc.idouble(100.0), aadc.idouble(10.0)]) 25 | 26 | funcs = aadc.Functions() # object to hold valuation graph as compiled kernel 27 | 28 | funcs.start_recording() # Record 1 MC path 29 | 30 | t0_state_arg = [val.mark_as_input() for val in state] # here we define t0 state as input. I.e. we can change asset levels and we calc adjoints 31 | 32 | randoms_arg = [] # array to collect arguments for random numbers that drive the path 33 | 34 | print(t0_state_arg) 35 | 36 | payoff = 0.0 37 | int_state_arg = [] # array to collect arguments(handles) for state variables that are calculated in the path. We use them to access adjoints 38 | int_state_res = [] # array to collect results(handles) for state variables that are calculated in the path. We use them to access values 39 | 40 | for t in range(1, T): 41 | random = aadc.array(np.random.normal(0, 0.1, len(state))) 42 | randoms_arg.append(random.mark_as_input_no_diff()) # we do not need adjoints for random numbers 43 | 44 | new_state = state + random 45 | int_state_arg = int_state_arg + [val.mark_as_diff() for val in new_state] # collect variable handles 46 | int_state_res = int_state_res + [val.mark_as_output() for val in new_state] # collect variable handles. Actually same ids, just different type 47 | 48 | state = new_state 49 | 50 | # if t == T - 1: 51 | # payoff = np.sum(np.maximum(0, 100.0 - state)) 52 | payoff = payoff + np.sum(state) 53 | 54 | payoff_res = payoff.mark_as_output() 55 | 56 | funcs.stop_recording() 57 | 58 | #print(randoms_arg) 59 | 60 | # Prepare inputs for the kernel 61 | 62 | NumMC = 10 63 | inputs = {} 64 | for args in randoms_arg: 65 | for arg in args: 66 | inputs.update({arg: np.random.normal(0, 0.1, NumMC)}) 67 | 68 | for arg in t0_state_arg: 69 | # we can reuse aadc kernel for arbitrary state values. I.e. do scenarios etc. 70 | inputs.update({arg: 100.0}) 71 | 72 | #print(inputs) 73 | 74 | # return payoff and gradients w.r.t. state variables 75 | request = {payoff_res: t0_state_arg + int_state_arg} 76 | for res in int_state_res: 77 | # return intermediate state variables 78 | request.update({res: []}) 79 | 80 | workers = aadc.ThreadPool(4) 81 | res = aadc.evaluate(funcs, request, inputs, workers) # run the kernel 82 | 83 | print("Result", res[0][payoff_res]) 84 | for state_i in range(len(state)): 85 | print("dPayoff/dState[0][", state_i, "]", res[1][payoff_res][int_state_arg[state_i]]) 86 | 87 | for t in range(1, T): 88 | for state_i in range(len(state)): 89 | print("State[", t, "][", state_i, "]", res[0][int_state_res[state_i + (t-1)*len(state)]]) 90 | print("dPayoff/dState[", t, "][", state_i, "]", res[1][payoff_res][int_state_arg[state_i + (t-1)*len(state)]]) 91 | 92 | -------------------------------------------------------------------------------- /tests/test_external_function.py: -------------------------------------------------------------------------------- 1 | import math 2 | import time 3 | 4 | import numpy as np 5 | import pytest 6 | 7 | import aadc 8 | import aadc.record_function 9 | import aadc.vectorize 10 | 11 | 12 | @pytest.fixture() 13 | def monkeypatch_mathexp(): 14 | orig = math.exp 15 | math.exp = aadc.math.exp 16 | yield 17 | math.exp = orig 18 | 19 | 20 | def test_external_function(monkeypatch_mathexp) -> None: 21 | class MyFunc(aadc.VectorFunction): 22 | def func(self, x): 23 | print("func in python") 24 | return x + 100.0 25 | 26 | mf = MyFunc() 27 | 28 | x = np.ones(10) 29 | 30 | print(mf.func(x)) 31 | print(x) 32 | 33 | print(np.array(aadc.ExternalFunction(mf, x)) + 1.0) 34 | 35 | funcs = aadc.Functions() 36 | 37 | funcs.start_recording() 38 | inputs = [aadc.idouble(i) for i in range(10)] 39 | inputs_args = [i.mark_as_input() for i in inputs] 40 | 41 | inputs = np.array(inputs) + 0.001 42 | 43 | outputs = aadc.ExternalFunction(mf, inputs) 44 | 45 | outputs = np.array(outputs) + 10000 46 | 47 | outputs_arg = [outputs[i].mark_as_output() for i in range(10)] 48 | 49 | funcs.stop_recording() 50 | funcs.print_passive_extract_locations() 51 | 52 | request = {} # outputsArg[0] : [] } 53 | 54 | for i in range(len(outputs_arg)): 55 | request[outputs_arg[i]] = [inputs_args[i]] # inputsArgs 56 | 57 | workers = aadc.ThreadPool(1) 58 | 59 | # inputs = [{ inputsArgs[i] : np.array([ float(i) ]) } for i in range(10)] 60 | inputs = {} 61 | for i in range(10): 62 | inputs[inputs_args[i]] = [float(i)] 63 | 64 | print(inputs) 65 | 66 | res = aadc.evaluate(funcs, request, inputs, workers) 67 | 68 | for i in range(len(outputs_arg)): 69 | print(res[0][outputs_arg[i]]) 70 | # print(res[1][outputsArg[i]]) 71 | 72 | 73 | def test_vector_function_with_jac() -> None: 74 | rng = np.random.default_rng(1234) 75 | 76 | a = rng.standard_normal((10, 10)) 77 | 78 | def func_a(x): 79 | return a @ x 80 | 81 | x0 = np.ones(10) 82 | 83 | vf = aadc.record_function.record(func_a, x0, bump_size=1e-3) 84 | 85 | res = vf.func(x0) 86 | jac = vf.jac(x0) 87 | 88 | assert np.allclose(res, func_a(x0)) 89 | assert np.allclose(jac, a) 90 | 91 | 92 | def test_vectorize() -> None: 93 | rng = np.random.default_rng(1234) 94 | 95 | def func(x): 96 | return np.exp(x[0] * x[1] + x[2]) 97 | 98 | def func_np(x): 99 | return np.exp(x[:, 0] * x[:, 1] + x[:, 2]) 100 | 101 | x0 = rng.standard_normal((1_000_000, 3)) 102 | vf = aadc.vectorize.VectorizedFunction(func, num_threads=12) 103 | 104 | start = time.time() 105 | y_np = func_np(x0) 106 | end = time.time() 107 | print(f"Elapsed time pure np: {end - start:.6f} seconds") 108 | 109 | eps = 1e-8 110 | fd0_np = (func_np(x0 + np.array([eps, 0.0, 0.0])) - func_np(x0 - np.array([eps, 0.0, 0.0]))) / (2 * eps) 111 | fd1_np = (func_np(x0 + np.array([0.0, eps, 0.0])) - func_np(x0 - np.array([0.0, eps, 0.0]))) / (2 * eps) 112 | fd2_np = (func_np(x0 + np.array([0.0, 0.0, eps])) - func_np(x0 - np.array([0.0, 0.0, eps]))) / (2 * eps) 113 | fd_grad = np.stack((fd0_np, fd1_np, fd2_np)) 114 | 115 | start = time.time() 116 | y, y_grads = vf(x0) 117 | end = time.time() 118 | print(f"Elapsed time aadc: {end - start:.6f} seconds") 119 | 120 | start = time.time() 121 | y, y_grads = vf(x0) 122 | end = time.time() 123 | print(f"Elapsed time aadc no recording: {end - start:.6f} seconds") 124 | 125 | assert np.allclose(y, y_np) 126 | assert np.allclose(y_grads, fd_grad, atol=1e-5) 127 | -------------------------------------------------------------------------------- /benchmarks/aadc_bench.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | import time 3 | 4 | import aadc.evaluate_wrappers 5 | import numpy as np 6 | import numpy.typing as npt 7 | from mtalg.random import MultithreadedRNG 8 | 9 | import aadc 10 | from aadc.recording_ctx import record_kernel 11 | import settings 12 | 13 | 14 | @dataclass(slots=True) 15 | class BlackScholesModel: 16 | sigma: float 17 | rfr: float 18 | s0: float 19 | 20 | def simulate(self, final_time: float, normal_samples: npt.NDArray[np.double]) -> tuple[npt.NDArray[np.double], npt.NDArray[np.double]]: 21 | num_steps = normal_samples.shape[1] 22 | dt = final_time / num_steps 23 | time = aadc.array(np.zeros(num_steps)) 24 | gbm = aadc.array(np.zeros_like(normal_samples)) 25 | 26 | gbm[:, 0] = self.s0 27 | bm_curr = 0. 28 | 29 | for i in range(1, num_steps): 30 | time[i] = i * dt 31 | bm_curr = bm_curr + normal_samples[:, i] * np.sqrt(dt) 32 | gbm[:, i] = self.s0 * np.exp((self.rfr - self.sigma**2/2) * time[i] + self.sigma * bm_curr) 33 | 34 | return time, gbm 35 | 36 | 37 | @dataclass(slots=True) 38 | class DownAndOutCallPayoff: 39 | barrier: float 40 | strike: float 41 | 42 | def evaluate(self, paths: npt.NDArray[np.double]) -> npt.NDArray[np.double] | float: 43 | scaling_factor = 1e+1 44 | barrier_distances = (paths - self.barrier) * scaling_factor 45 | survival_probs = np.prod((1 + np.tanh(barrier_distances)) / 2, axis=1) 46 | call_payoff = np.maximum(paths[:, -1] - self.strike, 0.0) 47 | payoffs = survival_probs * call_payoff 48 | return payoffs 49 | 50 | 51 | M = settings.NUM_TIME_STEPS 52 | N = settings.NUM_PATHS 53 | 54 | 55 | if __name__ == '__main__': 56 | start_time = time.time() 57 | rng = MultithreadedRNG(settings.RANDOM_SEED, num_threads=12) 58 | normal_samples = rng.standard_normal((1, M)) 59 | param_names = ['spot', 'strike', 'rfr', 'expiry', 'vol', 'barrier'] 60 | params_np = aadc.array([ 61 | settings.SPOT, 62 | settings.STRIKE, 63 | settings.RISK_FREE_RATE, 64 | settings.EXPIRY, 65 | settings.VOLATILITY, 66 | settings.BARRIER 67 | ]) 68 | 69 | with record_kernel() as kernel: 70 | params = aadc.array(params_np) 71 | pin = params.mark_as_input() 72 | active_samples = aadc.array(normal_samples) 73 | asin = active_samples.mark_as_input_no_diff() 74 | 75 | model = BlackScholesModel(params[4], params[2], params[0]) 76 | payoff = DownAndOutCallPayoff(params[5], params[1]) 77 | times, paths = model.simulate(params[3], active_samples) 78 | price_undiscounted = payoff.evaluate(paths) 79 | price_discounted = price_undiscounted*np.exp(-params[2]*params[3]) 80 | pout = price_discounted.mark_as_output() 81 | 82 | recording_end_time = time.time() 83 | print(f"Recording time: {recording_end_time - start_time}") 84 | 85 | normal_samples_eval = rng.standard_normal((M, N)) 86 | 87 | rng_end_time = time.time() 88 | print(f"RNG time: {rng_end_time - recording_end_time}") 89 | 90 | inputs = { 91 | **{param: param_value for param, param_value in zip(pin, params_np)}, 92 | **{sample: samples_row for sample, samples_row in zip(np.squeeze(asin), normal_samples_eval)} 93 | } 94 | request = {pout.item(): pin.tolist()} 95 | 96 | request_build_end = time.time() 97 | print(f"Request build time: {request_build_end-rng_end_time}") 98 | 99 | workers = aadc.ThreadPool(12) 100 | values, derivs = aadc.evaluate(kernel, request, inputs, workers) 101 | 102 | price = values[pout.item()].mean() 103 | print(f"--Calculated price: {price}") 104 | print(f"--Calculated gradients:") 105 | for param_name, param in zip(param_names, derivs[pout.item()].values()): 106 | grad = param.mean() 107 | print(f"----Grad w.r.t. to {param_name}: {grad}") 108 | 109 | evaluate_end_time = time.time() 110 | print(f"RNG + evaluate + compilation time: {evaluate_end_time - start_time}") 111 | print(f"RNG + evaluate time: {evaluate_end_time - recording_end_time}") 112 | print(f"Evaluate only time: {evaluate_end_time - request_build_end}") 113 | 114 | -------------------------------------------------------------------------------- /examples/interpolation.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 24, 6 | "metadata": {}, 7 | "outputs": [ 8 | { 9 | "name": "stdout", 10 | "output_type": "stream", 11 | "text": [ 12 | "You are using evaluation version of AADC. Expire date is 20250201\n" 13 | ] 14 | } 15 | ], 16 | "source": [ 17 | "import numpy as np\n", 18 | "\n", 19 | "import aadc\n", 20 | "from aadc.scipy.interpolate import interp1d, CubicSpline\n", 21 | "\n", 22 | "N = 10\n", 23 | "\n", 24 | "def func(t, xs, ys):\n", 25 | " yt = np.interp(t, xs, ys)\n", 26 | " t2 = 0.25\n", 27 | " yt2 = np.interp(t2, xs, ys)\n", 28 | " return yt+yt2\n", 29 | "\n", 30 | "with aadc.record_kernel() as kernel:\n", 31 | " xs_numpy = np.linspace(0,1,N)\n", 32 | " ys_numpy = np.linspace(0,1,N)\n", 33 | " xs = aadc.array(xs_numpy)\n", 34 | " ys = aadc.array(ys_numpy)\n", 35 | " xs_args = xs.mark_as_input()\n", 36 | " ys_args = ys.mark_as_input()\n", 37 | " t = xs.mean()\n", 38 | " arg_t = t.mark_as_input()\n", 39 | " res = func(t, xs, ys)\n", 40 | " out_res = res.mark_as_output()\n", 41 | "\n", 42 | "t_test = 0.5\n", 43 | "\n", 44 | "f = aadc.VectorFunctionWithAD(kernel, args=[arg_t], res=[out_res], param_args=np.r_[xs_args, ys_args])\n", 45 | "f.set_params(np.r_[xs_numpy, ys_numpy])\n", 46 | "value, jacobian = f.evaluate(np.array([t_test]))\n", 47 | "\n", 48 | "assert np.allclose(value.squeeze(), func(t_test, xs_numpy, ys_numpy))\n", 49 | "\n", 50 | "eps = 1e-3\n", 51 | "fd_derivative = (func(t_test + eps, xs_numpy, ys_numpy) - func(t_test - eps, xs_numpy, ys_numpy)) / (2 * eps)\n", 52 | "assert np.allclose(fd_derivative, jacobian.squeeze())" 53 | ] 54 | }, 55 | { 56 | "cell_type": "code", 57 | "execution_count": 25, 58 | "metadata": {}, 59 | "outputs": [ 60 | { 61 | "name": "stdout", 62 | "output_type": "stream", 63 | "text": [ 64 | "You are using evaluation version of AADC. Expire date is 20250201\n" 65 | ] 66 | } 67 | ], 68 | "source": [ 69 | "def func_scipy(t, xs, ys):\n", 70 | " linear = interp1d(xs, ys, kind='linear')\n", 71 | " cubic_legacy = interp1d(xs, ys, kind='cubic')\n", 72 | " cubic_new = CubicSpline(xs, ys, bc_type='clamped')\n", 73 | " return linear(t) + cubic_legacy(t) + cubic_new(t)\n", 74 | "\n", 75 | "# Main kernel recording with context manager\n", 76 | "with aadc.record_kernel() as kernel:\n", 77 | " xs_numpy = np.linspace(0, 1, N)\n", 78 | " ys_numpy = np.linspace(0, 1, N)\n", 79 | " xs = aadc.array(xs_numpy)\n", 80 | " ys = aadc.array(ys_numpy)\n", 81 | " xs_args = xs.mark_as_input()\n", 82 | " ys_args = ys.mark_as_input()\n", 83 | " t = xs.mean()\n", 84 | " arg_t = t.mark_as_input()\n", 85 | " res = func_scipy(t, xs, ys)\n", 86 | " out_res = res.mark_as_output()\n", 87 | "\n", 88 | "# Test the function\n", 89 | "t_test = 0.5\n", 90 | "f = aadc.VectorFunctionWithAD(kernel, args=[arg_t], res=[out_res], param_args=np.r_[xs_args, ys_args])\n", 91 | "f.set_params(np.r_[xs_numpy, ys_numpy])\n", 92 | "value, jacobian = f.evaluate(np.array([t_test]))\n", 93 | "\n", 94 | "# Verify results\n", 95 | "assert np.allclose(value.squeeze(), func_scipy(t_test, xs_numpy, ys_numpy))\n", 96 | "\n", 97 | "# Check derivatives\n", 98 | "eps = 1e-3\n", 99 | "fd_derivative = (func_scipy(t_test + eps, xs_numpy, ys_numpy) - func_scipy(t_test - eps, xs_numpy, ys_numpy)) / (2 * eps)\n", 100 | "assert np.allclose(fd_derivative, jacobian.squeeze())" 101 | ] 102 | } 103 | ], 104 | "metadata": { 105 | "kernelspec": { 106 | "display_name": "aadcpy", 107 | "language": "python", 108 | "name": "python3" 109 | }, 110 | "language_info": { 111 | "codemirror_mode": { 112 | "name": "ipython", 113 | "version": 3 114 | }, 115 | "file_extension": ".py", 116 | "mimetype": "text/x-python", 117 | "name": "python", 118 | "nbconvert_exporter": "python", 119 | "pygments_lexer": "ipython3", 120 | "version": "3.11.11" 121 | } 122 | }, 123 | "nbformat": 4, 124 | "nbformat_minor": 2 125 | } 126 | -------------------------------------------------------------------------------- /benchmarks/jax_bench.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import jax 4 | import jax.numpy as jnp 5 | from jax import random 6 | 7 | import settings 8 | 9 | jax.config.update('jax_platform_name', 'cpu') 10 | 11 | 12 | def simulate_paths(sigma: jnp.ndarray, rfr: jnp.ndarray, s0: jnp.ndarray, 13 | num_paths: int, num_tsteps: int, final_time: jnp.ndarray, 14 | key: random.PRNGKey) -> jnp.ndarray: 15 | dt = final_time/num_tsteps 16 | gbm = jnp.zeros((num_paths, num_tsteps)) 17 | 18 | gbm = gbm.at[:, 0].set(s0) 19 | bm_curr = jnp.zeros(num_paths) 20 | 21 | def body_fun(i, state): 22 | gbm, bm_curr, key = state 23 | key, subkey = random.split(key) 24 | t = i * dt 25 | bm_curr = bm_curr + random.normal(subkey, (num_paths,)) * jnp.sqrt(dt) 26 | gbm = gbm.at[:, i].set( 27 | s0 * jnp.exp((rfr - 0.5*sigma**2)*t + sigma*bm_curr) 28 | ) 29 | return gbm, bm_curr, key 30 | 31 | # Use scan for the loop since JAX doesn't support traditional loops 32 | gbm, _, _ = jax.lax.fori_loop( 33 | 1, num_tsteps, 34 | lambda i, state: body_fun(i, state), 35 | (gbm, bm_curr, key) 36 | ) 37 | 38 | return gbm 39 | 40 | def evaluate_payoff(paths: jnp.ndarray, barrier: jnp.ndarray, 41 | strike: jnp.ndarray) -> jnp.ndarray: 42 | scaling_factor = 1e+1 43 | barrier_distances = (paths - barrier) * scaling_factor 44 | survival_probs = jnp.prod((1 + jnp.tanh(barrier_distances)) / 2, axis=1) 45 | call_payoff = jnp.maximum(paths[:, -1] - strike, 0.) 46 | payoffs = survival_probs * call_payoff 47 | return payoffs 48 | 49 | def get_price(spot: jnp.ndarray, strike: jnp.ndarray, rfr: jnp.ndarray, 50 | vol: jnp.ndarray, barrier: jnp.ndarray, expiry: jnp.ndarray, 51 | num_paths: int, num_tsteps: int, key: random.PRNGKey) -> jnp.ndarray: 52 | paths = simulate_paths(vol, rfr, spot, num_paths, num_tsteps, expiry, key) 53 | payoffs = evaluate_payoff(paths, barrier, strike) 54 | price = jnp.mean(payoffs) 55 | return jnp.exp(-rfr*expiry) * price 56 | 57 | if __name__ == '__main__': 58 | M = settings.NUM_TIME_STEPS 59 | N = settings.NUM_PATHS 60 | 61 | spot = jnp.array(settings.SPOT) 62 | strike = jnp.array(settings.STRIKE) 63 | rfr = jnp.array(settings.RISK_FREE_RATE) 64 | vol = jnp.array(settings.VOLATILITY) 65 | barrier = jnp.array(settings.BARRIER) 66 | expiry = jnp.array(settings.EXPIRY) 67 | key = random.PRNGKey(settings.RANDOM_SEED) 68 | 69 | get_price_jit = jax.jit(get_price, static_argnums=(6, 7)) # only num_paths and num_tsteps are static 70 | get_price_and_grad = jax.value_and_grad(get_price, argnums=(0, 1, 2, 3, 4, 5)) 71 | 72 | print("Running in JIT mode...") 73 | 74 | # First pass (compilation) 75 | print("First pass (compilation)...") 76 | start_time = time.time() 77 | price, grads = get_price_and_grad(spot, strike, rfr, vol, barrier, expiry, N, M, key) 78 | print(f"--Price: {price}") 79 | print("--Calculated gradients:") 80 | print(f"----Grad w.r.t. spot: {grads[0]}") 81 | print(f"----Grad w.r.t. strike: {grads[1]}") 82 | print(f"----Grad w.r.t. rfr: {grads[2]}") 83 | print(f"----Grad w.r.t. expiry: {grads[5]}") 84 | print(f"----Grad w.r.t. vol: {grads[3]}") 85 | print(f"----Grad w.r.t. barrier: {grads[4]}") 86 | elapsed_time = time.time() - start_time # JAX uses lazy evaluation for gradients 87 | print(f"--Total execution time (1st pass: RNG + evaluate + compilation): {elapsed_time:.4f}s") 88 | 89 | # Second pass (after compilation) 90 | print("\nSecond pass...") 91 | start_time = time.time() 92 | key, subkey = random.split(key) 93 | price, grads = get_price_and_grad(spot, strike, rfr, vol, barrier, expiry, N, M, subkey) 94 | print(f"--Price: {price}") 95 | print("--Calculated gradients:") 96 | print(f"----Grad w.r.t. spot: {grads[0]}") 97 | print(f"----Grad w.r.t. strike: {grads[1]}") 98 | print(f"----Grad w.r.t. rfr: {grads[2]}") 99 | print(f"----Grad w.r.t. expiry: {grads[5]}") 100 | print(f"----Grad w.r.t. vol: {grads[3]}") 101 | print(f"----Grad w.r.t. barrier: {grads[4]}") 102 | elapsed_time = time.time() - start_time # JAX uses lazy evaluation for gradients 103 | print(f"--Total execution time (2nd pass: RNG + evaluate): {elapsed_time:.4f}s") -------------------------------------------------------------------------------- /benchmarks/pytorch_bench.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import torch 4 | 5 | import settings 6 | 7 | torch.set_default_dtype(torch.double) 8 | torch.set_default_device('cpu') 9 | torch.manual_seed(settings.RANDOM_SEED) 10 | 11 | @torch.jit.script 12 | def simulate_paths(sigma: torch.Tensor, rfr: torch.Tensor, s0: torch.Tensor, 13 | num_paths: int, num_tsteps: int, final_time: torch.Tensor) -> torch.Tensor: 14 | dt = final_time/num_tsteps 15 | gbm = torch.zeros((num_paths, num_tsteps)) 16 | 17 | gbm[:, 0] = s0 18 | bm_curr = torch.zeros(num_paths) 19 | 20 | for i in range(1, num_tsteps): 21 | t = i * dt 22 | bm_curr = bm_curr + torch.randn(num_paths) * torch.sqrt(dt) 23 | gbm[:, i] = s0 * torch.exp((rfr - 0.5*sigma**2)*t + sigma*bm_curr) 24 | 25 | return gbm 26 | 27 | @torch.jit.script 28 | def evaluate_payoff(paths: torch.Tensor, barrier: torch.Tensor, 29 | strike: torch.Tensor) -> torch.Tensor: 30 | scaling_factor = 1e+1 31 | barrier_distances = (paths - barrier) * scaling_factor 32 | survival_probs = torch.prod((1 + torch.tanh(barrier_distances)) / 2, dim=1) 33 | call_payoff = torch.maximum(paths[:, -1] - strike, torch.tensor(0.)) 34 | payoffs = survival_probs * call_payoff 35 | return payoffs 36 | 37 | @torch.jit.script 38 | def get_price(spot: torch.Tensor, strike: torch.Tensor, rfr: torch.Tensor, 39 | vol: torch.Tensor, barrier: torch.Tensor, expiry: torch.Tensor, 40 | num_paths: int, num_tsteps: int) -> torch.Tensor: 41 | paths = simulate_paths(vol, rfr, spot, num_paths, num_tsteps, expiry) 42 | payoffs = evaluate_payoff(paths, barrier, strike) 43 | price = torch.mean(payoffs) 44 | return torch.exp(-rfr*expiry) * price 45 | 46 | M = settings.NUM_TIME_STEPS 47 | N = settings.NUM_PATHS 48 | 49 | # Parameters 50 | spot = torch.tensor(settings.SPOT, requires_grad=True) 51 | strike = torch.tensor(settings.STRIKE, requires_grad=True) 52 | rfr = torch.tensor(settings.RISK_FREE_RATE, requires_grad=True) 53 | vol = torch.tensor(settings.VOLATILITY, requires_grad=True) 54 | barrier = torch.tensor(settings.BARRIER, requires_grad=True) 55 | expiry = torch.tensor(settings.EXPIRY, requires_grad=True) 56 | 57 | PARAMS = { 58 | "spot": spot, 59 | "strike": strike, 60 | "rfr": rfr, 61 | "vol": vol, 62 | } 63 | 64 | if __name__ == '__main__': 65 | M = settings.NUM_TIME_STEPS 66 | N = settings.NUM_PATHS 67 | 68 | spot = torch.tensor(settings.SPOT, requires_grad=True) 69 | strike = torch.tensor(settings.STRIKE, requires_grad=True) 70 | rfr = torch.tensor(settings.RISK_FREE_RATE, requires_grad=True) 71 | vol = torch.tensor(settings.VOLATILITY, requires_grad=True) 72 | barrier = torch.tensor(settings.BARRIER, requires_grad=True) 73 | expiry = torch.tensor(settings.EXPIRY, requires_grad=True) 74 | 75 | print("Running in JIT mode...") 76 | 77 | print("First pass (compilation)...") 78 | start_time = time.time() 79 | price = get_price(spot, strike, rfr, vol, barrier, expiry, N, M) 80 | price.backward() 81 | elapsed_time = time.time() - start_time 82 | print(f"--Price: {price.item()}") 83 | print("--Calculated gradients:") 84 | print(f"----Grad w.r.t. spot: {spot.grad.item()}") 85 | print(f"----Grad w.r.t. strike: {strike.grad.item()}") 86 | print(f"----Grad w.r.t. rfr: {rfr.grad.item()}") 87 | print(f"----Grad w.r.t. expiry: {expiry.grad.item()}") 88 | print(f"----Grad w.r.t. vol: {vol.grad.item()}") 89 | print(f"----Grad w.r.t. barrier: {barrier.grad.item()}") 90 | print(f"--Total execution time (1st pass: RNG + evaluate + compilation): {elapsed_time:.4f}s") 91 | 92 | # Reset gradients 93 | for param in [spot, strike, rfr, vol, barrier, expiry]: 94 | param.grad = None 95 | 96 | # Second pass 97 | print("\nSecond pass...") 98 | start_time = time.time() 99 | price = get_price(spot, strike, rfr, vol, barrier, expiry, N, M) 100 | price.backward() 101 | elapsed_time = time.time() - start_time 102 | print(f"--Price: {price.item()}") 103 | print("--Calculated gradients:") 104 | print(f"----Grad w.r.t. spot: {spot.grad.item()}") 105 | print(f"----Grad w.r.t. strike: {strike.grad.item()}") 106 | print(f"----Grad w.r.t. rfr: {rfr.grad.item()}") 107 | print(f"----Grad w.r.t. expiry: {expiry.grad.item()}") 108 | print(f"----Grad w.r.t. vol: {vol.grad.item()}") 109 | print(f"----Grad w.r.t. barrier: {barrier.grad.item()}") 110 | print(f"--Total execution time (2nd pass: RNG + evaluate): {elapsed_time:.4f}s") -------------------------------------------------------------------------------- /tests/test_idouble.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from aadc import idouble 4 | 5 | 6 | def test_add_array_to_idouble() -> None: 7 | result = idouble(4.0) 8 | result += np.array([3.0, 4.0]) 9 | assert type(result) == np.ndarray, "Result should be an np.ndarray" 10 | assert np.all(result == np.array([7.0, 8.0])), "Result should match expected values" 11 | 12 | 13 | def test_add_idouble_to_array() -> None: 14 | result = np.array([3.0, 4.0]) 15 | result += idouble(4.0) 16 | assert type(result) == np.ndarray, "Result should be an np.ndarray" 17 | assert np.all(result == np.array([7.0, 8.0])), "Result should match expected values" 18 | 19 | 20 | def test_sub_idouble_from_array() -> None: 21 | result = np.array([3.0, 4.0]) 22 | result -= idouble(4.0) 23 | assert type(result) == np.ndarray, "Result should be an np.ndarray" 24 | assert np.all(result == np.array([-1.0, 0.0])), "Result should match expected values" 25 | 26 | 27 | def test_sub_array_from_idouble() -> None: 28 | result = idouble(4.0) 29 | result -= np.array([3.0, 4.0]) 30 | assert type(result) == np.ndarray, "Result should be an np.ndarray" 31 | assert np.all(result == np.array([1.0, 0.0])), "Result should match expected values" 32 | 33 | 34 | def test_mul_idouble_with_array() -> None: 35 | result = idouble(4.0) * np.array([3.0, 4.0]) 36 | assert type(result) == np.ndarray, "Result should be an np.ndarray" 37 | assert np.all(result == np.array([12.0, 16.0])), "Result should match expected values" 38 | 39 | 40 | def test_div_idouble_by_array() -> None: 41 | result = idouble(4.0) / np.array([3.0, 4.0]) 42 | assert type(result) == np.ndarray, "Result should be an np.ndarray" 43 | assert np.all(result == np.array([4.0 / 3.0, 1.0])), "Result should match expected values" 44 | 45 | 46 | def test_eq_idouble_with_array() -> None: 47 | result = idouble(4.0) == np.array([3.0, 4.0]) 48 | assert type(result) == np.ndarray, "Result should be an np.ndarray" 49 | assert np.all(result == np.array([False, True])), "Result should match expected values" 50 | 51 | 52 | def test_gt_idouble_with_array() -> None: 53 | result = idouble(4.0) > np.array([3.0, 4.0]) 54 | assert type(result) == np.ndarray, "Result should be an np.ndarray" 55 | assert np.all(result == np.array([True, False])), "Result should match expected values" 56 | 57 | 58 | def test_le_idouble_with_array() -> None: 59 | result = idouble(4.0) <= np.array([3.0, 4.0]) 60 | assert type(result) == np.ndarray, "Result should be an np.ndarray" 61 | assert np.all(result == np.array([False, True])), "Result should match expected values" 62 | 63 | 64 | def test_ge_idouble_with_array() -> None: 65 | result = idouble(4.0) >= np.array([3.0, 4.0]) 66 | assert type(result) == np.ndarray, "Result should be an np.ndarray" 67 | assert np.all(result == np.array([True, True])), "Result should match expected values" 68 | 69 | 70 | def test_mul_array_with_idouble() -> None: 71 | result = np.array([3.0, 4.0]) * idouble(4.0) 72 | assert type(result) == np.ndarray, "Result should be an np.ndarray" 73 | assert np.all(result == np.array([12.0, 16.0])), "Result should match expected values" 74 | 75 | 76 | def test_div_array_by_idouble() -> None: 77 | result = np.array([3.0, 4.0]) / idouble(4.0) 78 | assert type(result) == np.ndarray, "Result should be an np.ndarray" 79 | assert np.all(result == np.array([3.0 / 4.0, 1.0])), "Result should match expected values" 80 | 81 | 82 | def test_eq_array_with_idouble() -> None: 83 | result = np.array([3.0, 4.0]) == idouble(4.0) 84 | assert type(result) == np.ndarray, "Result should be an np.ndarray" 85 | assert np.all(result == np.array([False, True])), "Result should match expected values" 86 | 87 | 88 | def test_gt_array_with_idouble() -> None: 89 | result = np.array([3.0, 4.0]) > idouble(4.0) 90 | assert type(result) == np.ndarray, "Result should be an np.ndarray" 91 | assert np.all(result == np.array([False, False])), "Result should match expected values" 92 | 93 | 94 | def test_le_array_with_idouble() -> None: 95 | result = np.array([3.0, 4.0]) <= idouble(4.0) 96 | assert type(result) == np.ndarray, "Result should be an np.ndarray" 97 | assert np.all(result == np.array([True, True])), "Result should match expected values" 98 | 99 | 100 | def test_ge_array_with_idouble() -> None: 101 | result = np.array([3.0, 4.0]) >= idouble(4.0) 102 | assert type(result) == np.ndarray, "Result should be an np.ndarray" 103 | assert np.all(result == np.array([False, True])), "Result should match expected values" 104 | -------------------------------------------------------------------------------- /benchmarks/tensorflow_bench.py: -------------------------------------------------------------------------------- 1 | import tensorflow as tf 2 | import time 3 | import settings 4 | 5 | tf.random.set_seed(settings.RANDOM_SEED) 6 | dtype = 'float64' 7 | 8 | class BlackScholesModel(tf.Module): 9 | 10 | def __init__(self, sigma: tf.Tensor, rfr: tf.Tensor, s0: tf.Tensor): 11 | self.sigma: tf.Tensor = sigma 12 | self.rfr: tf.Tensor = rfr 13 | self.s0: tf.Tensor = s0 14 | 15 | 16 | def simulate(self, num_paths: int, num_tsteps: int, final_time: float) -> tuple[tf.Tensor, tf.Tensor]: 17 | num_tsteps = num_tsteps 18 | dt = final_time/num_tsteps 19 | times = tf.zeros(num_tsteps, dtype=dtype) 20 | gbm = tf.zeros((num_paths, num_tsteps), dtype=dtype) 21 | 22 | # Initialize first values 23 | gbm = tf.tensor_scatter_nd_update(gbm, [[i, 0] for i in range(num_paths)], tf.fill([num_paths], self.s0)) 24 | bm_curr = tf.zeros(num_paths, dtype=dtype) 25 | 26 | # Time stepping loop 27 | for i in range(1, num_tsteps): 28 | times = tf.tensor_scatter_nd_update(times, [[i]], [i * dt]) 29 | bm_curr = bm_curr + tf.random.normal((num_paths,), dtype=dtype) * tf.sqrt(dt) 30 | curr_gbm = self.s0 * tf.exp((self.rfr - 0.5 * self.sigma**2) * (i * dt) + self.sigma * bm_curr) 31 | gbm = tf.tensor_scatter_nd_update(gbm, [[j, i] for j in range(num_paths)], curr_gbm) 32 | return gbm, times 33 | 34 | 35 | 36 | class DownAndOutCallPayoff(tf.Module): 37 | 38 | def __init__(self, barrier: tf.Tensor, strike: tf.Tensor): 39 | self.barrier: tf.Tensor = barrier 40 | self.strike: tf.Tensor = strike 41 | 42 | 43 | def evaluate(self, paths: tf.Tensor) -> tf.Tensor: 44 | # Using tanh for smoother barrier transition 45 | scaling_factor = 1e+1 # Controls the sharpness of the transition 46 | barrier_distances = (paths - self.barrier) * scaling_factor 47 | survival_probs = tf.reduce_prod((1 + tf.tanh(barrier_distances)) / 2, axis=1) 48 | 49 | # Regular max function for the call payoff 50 | call_payoff = tf.maximum(paths[:, -1] - self.strike, 0.0) 51 | 52 | # Combine smoothed barrier and payoff 53 | payoffs = survival_probs * call_payoff 54 | return payoffs 55 | 56 | 57 | spot = tf.Variable(settings.SPOT, dtype=dtype) 58 | strike = tf.Variable(settings.STRIKE, dtype=dtype) 59 | rfr = tf.Variable(settings.RISK_FREE_RATE, dtype=dtype) 60 | vol = tf.Variable(settings.VOLATILITY, dtype=dtype) 61 | barrier = tf.Variable(settings.BARRIER, dtype=dtype) 62 | expiry = tf.constant(settings.EXPIRY, dtype=dtype) 63 | 64 | PARAMS = { 65 | "spot": spot, 66 | "strike": strike, 67 | "rfr": rfr, 68 | "vol": vol, 69 | "barrier": barrier, 70 | "expiry": expiry 71 | } 72 | 73 | M = settings.NUM_TIME_STEPS 74 | N = settings.NUM_PATHS 75 | 76 | 77 | def get_price(spot, strike, rfr, vol, barrier, expiry): 78 | model = BlackScholesModel(vol, rfr, spot) 79 | payoff = DownAndOutCallPayoff(barrier, strike) 80 | paths, times = model.simulate(N, M, expiry) 81 | payoffs = payoff.evaluate(paths) 82 | 83 | price = tf.reduce_mean(payoffs) 84 | return tf.exp(-rfr*expiry) * price 85 | 86 | 87 | if __name__ == '__main__': 88 | print("Running in compiled mode - 1st pass...") 89 | 90 | start_time_compile = time.time() 91 | get_price_compiled = tf.function(get_price) 92 | 93 | start_time_compile_forward = time.time() 94 | with tf.GradientTape() as tape: 95 | price = get_price_compiled(**PARAMS) 96 | grad = tape.gradient(price, [*PARAMS.values()]) 97 | 98 | print(f"--Calculated gradients:") 99 | for i, (param_name, param) in enumerate(PARAMS.items()): 100 | print(f"----Grad w.r.t. to {param_name}: {grad[i]}") 101 | param.grad = None 102 | print(f"--Calculated price: {price}") 103 | 104 | elapsed_time_1st_pass = time.time() - start_time_compile 105 | print(f"--Total execution time (1st pass: RNG + evaluate + compilation): {elapsed_time_1st_pass:.4f}s") 106 | 107 | print("Running in compiled mode - 2nd pass...") 108 | start_time_2nd_pass = time.time() 109 | with tf.GradientTape() as tape: 110 | price = get_price_compiled(**PARAMS) 111 | grad = tape.gradient(price, [*PARAMS.values()]) 112 | 113 | print(f"--Calculated gradients:") 114 | for i, (param_name, param) in enumerate(PARAMS.items()): 115 | print(f"----Grad w.r.t. to {param_name}: {grad[i]}") 116 | param.grad = None 117 | print(f"--Calculated price: {price}") 118 | 119 | elapsed_time_2nd_pass = time.time() - start_time_2nd_pass 120 | print(f"--Total execution time (2nd pass: RNG + evaluate): {elapsed_time_2nd_pass:.4f}s") 121 | 122 | -------------------------------------------------------------------------------- /tests/test_array_arg.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import aadc 4 | from aadc.evaluate_wrappers import evaluate_matrix_inputs 5 | from aadc.recording_ctx import record_kernel 6 | 7 | 8 | def test_array_arg_all_derivatives() -> None: 9 | batch_size = 2 10 | rng = np.random.default_rng(1234) 11 | a_np = rng.standard_normal((3, 3)) 12 | 13 | def func(a, x): 14 | return (a @ x).mean() 15 | 16 | with record_kernel() as kernel: 17 | a = aadc.array(a_np) 18 | a_arg = a.mark_as_input() 19 | 20 | x = aadc.array(np.ones(3)) 21 | x_arg = x.mark_as_input() 22 | 23 | y = func(a, x) 24 | y_arg = y.mark_as_output() 25 | 26 | request = [(y_arg, [a_arg, x_arg])] 27 | inputs = [(x_arg, rng.standard_normal((batch_size, 3))), (a_arg, np.tile(a_np, (batch_size, 1, 1)))] 28 | 29 | output_values, output_grads = evaluate_matrix_inputs(kernel, request, inputs, 4) 30 | 31 | assert output_values.shape == (batch_size, 1), "Forward pass shape correct" 32 | 33 | expected_results = [ 34 | # result, grad a, grad x 35 | ( 36 | 0.22142861151869342, 37 | np.array([[0.1146, -0.1708, 0.4413], [0.1146, -0.1708, 0.4413], [0.1146, -0.1708, 0.4413]]), 38 | np.array([-0.9767, 0.6244, 0.6626]), 39 | ), 40 | ( 41 | 0.32630306664349257, 42 | np.array([[-0.2868, 0.1732, -0.4217], [-0.2868, 0.1732, -0.4217], [-0.2868, 0.1732, -0.4217]]), 43 | np.array([-0.9767, 0.6244, 0.6626]), 44 | ), 45 | ] 46 | 47 | for i, (res, grad_a, grad_x) in enumerate(expected_results): 48 | assert np.isclose(res, output_values[i]), f"For {i}-th batch element, forward pass results correct" 49 | assert np.allclose(grad_a, output_grads[y_arg][0][i], atol=1e-4), f"Grads correct for a, {i}-th batch element" 50 | assert np.allclose(grad_x, output_grads[y_arg][1][i], atol=1e-4), f"Grads correct for x, {i}-th batch element" 51 | 52 | 53 | def test_array_arg_params_constant_across_batch() -> None: 54 | rng = np.random.default_rng(1234) 55 | batch_size = 2 56 | a_np = rng.standard_normal((3, 3)) 57 | 58 | def func(a, x): 59 | return (a @ x).mean() 60 | 61 | with record_kernel() as kernel: 62 | a = aadc.array(a_np) 63 | a_arg = a.mark_as_input() 64 | 65 | x = aadc.array(np.ones(3)) 66 | x_arg = x.mark_as_input() 67 | 68 | y = func(a, x) 69 | y_arg = y.mark_as_output() 70 | 71 | request = [(y_arg, [a_arg, x_arg])] 72 | inputs = [(x_arg, rng.standard_normal((batch_size, 3))), (a_arg, a_np[np.newaxis, :])] 73 | 74 | output_values, output_grads = evaluate_matrix_inputs(kernel, request, inputs, 4) 75 | 76 | assert output_values.shape == (batch_size, 1), "Forward pass shape correct" 77 | 78 | expected_results = [ 79 | # result, grad a, grad x 80 | ( 81 | 0.22142861151869342, 82 | np.array([[0.1146, -0.1708, 0.4413], [0.1146, -0.1708, 0.4413], [0.1146, -0.1708, 0.4413]]), 83 | np.array([-0.9767, 0.6244, 0.6626]), 84 | ), 85 | ( 86 | 0.32630306664349257, 87 | np.array([[-0.2868, 0.1732, -0.4217], [-0.2868, 0.1732, -0.4217], [-0.2868, 0.1732, -0.4217]]), 88 | np.array([-0.9767, 0.6244, 0.6626]), 89 | ), 90 | ] 91 | 92 | for i, (res, grad_a, grad_x) in enumerate(expected_results): 93 | assert np.isclose(res, output_values[i]), f"For {i}-th batch element, forward pass results correct" 94 | assert np.allclose(grad_a, output_grads[y_arg][0][i], atol=1e-4), f"Grads correct for a, {i}-th batch element" 95 | assert np.allclose(grad_x, output_grads[y_arg][1][i], atol=1e-4), f"Grads correct for x, {i}-th batch element" 96 | 97 | 98 | def test_array_arg_params_only_in_request() -> None: 99 | rng = np.random.default_rng(1234) 100 | batch_size = 2 101 | a_np = rng.standard_normal((3, 3)) 102 | 103 | def func(a, x): 104 | return (a @ x).mean() 105 | 106 | with record_kernel() as kernel: 107 | a = aadc.array(a_np) 108 | a_arg = a.mark_as_input() 109 | 110 | x = aadc.array(np.ones(3)) 111 | x_arg = x.mark_as_input_no_diff() 112 | 113 | y = func(a, x) 114 | y_arg = y.mark_as_output() 115 | 116 | request = [(y_arg, [a_arg])] 117 | inputs = [(x_arg, rng.standard_normal((batch_size, 3))), (a_arg, a_np[np.newaxis, :])] 118 | 119 | output_values, output_grads = evaluate_matrix_inputs(kernel, request, inputs, 1) 120 | 121 | assert output_values.shape == (batch_size, 1), "Forward pass shape correct" 122 | 123 | expected_results = [ 124 | # result, grad a 125 | ( 126 | 0.22142861151869342, 127 | np.array([[0.1146, -0.1708, 0.4413], [0.1146, -0.1708, 0.4413], [0.1146, -0.1708, 0.4413]]), 128 | ), 129 | ( 130 | 0.32630306664349257, 131 | np.array([[-0.2868, 0.1732, -0.4217], [-0.2868, 0.1732, -0.4217], [-0.2868, 0.1732, -0.4217]]), 132 | ), 133 | ] 134 | 135 | for i, (res, grad_a) in enumerate(expected_results): 136 | assert np.isclose(res, output_values[i]), f"For {i}-th batch element, forward pass results correct" 137 | assert np.allclose(grad_a, output_grads[y_arg][0][i], atol=1e-4), f"Grads correct for a, {i}-th batch element" 138 | -------------------------------------------------------------------------------- /tests/test_overrides.py: -------------------------------------------------------------------------------- 1 | import builtins 2 | import time 3 | from threading import Thread 4 | 5 | import numpy as np 6 | import pytest 7 | from scipy.stats import norm 8 | 9 | import aadc 10 | import aadc.overrides 11 | from aadc import Functions 12 | from aadc.ndarray import AADCArray 13 | from aadc.overrides import float, int, isinstance 14 | 15 | 16 | @pytest.fixture 17 | def problematic_code(): 18 | def func(log_returns, bigt, assets): 19 | funcs = Functions() 20 | funcs.start_recording() 21 | log_returns.mark_as_input() 22 | 23 | try: 24 | asset_price_movements = np.ones((bigt, assets)) 25 | for t in range(bigt): 26 | asset_price_movements[t, :] = asset_price_movements[t - 1, :] * np.exp(log_returns) 27 | finally: 28 | funcs.stop_recording() 29 | 30 | return asset_price_movements 31 | 32 | return func 33 | 34 | 35 | def test_call_float_on_float() -> None: 36 | value = 3.14 37 | regular_float = float(value) 38 | assert builtins.isinstance(regular_float, builtins.float), "Regular float should be an instance of builtins.float" 39 | 40 | 41 | def test_call_float_on_idouble() -> None: 42 | value = aadc.idouble(3.14) 43 | aadc_float = float(value) 44 | assert builtins.isinstance(aadc_float, aadc.idouble), "idouble should pass through float with noop" 45 | 46 | 47 | def test_call_isinstace_on_float() -> None: 48 | assert isinstance(3.14, float), "regular float should be treated as the new float" 49 | assert isinstance(3.14, builtins.float), "regular float should be treated as a builtins.float" 50 | 51 | 52 | def test_call_isinstace_on_idouble() -> None: 53 | value = aadc.idouble(10.0) 54 | assert isinstance(value, float), "aadc.idouble should be treated as the new float" 55 | assert isinstance(value, builtins.float), "aadc.idouble should be treated as a builtins.float" 56 | 57 | 58 | def test_call_int_on_int() -> None: 59 | value = 10 60 | regular_int = int(value) 61 | assert isinstance(regular_int, builtins.int), "Regular int should be an instance of builtins.int" 62 | 63 | 64 | def test_call_int_on_iint() -> None: 65 | value = aadc.iint(20) 66 | aadc_int = int(value) 67 | assert isinstance(aadc_int, aadc.iint), "iint should pass through int with noop" 68 | 69 | 70 | def test_call_isinstace_on_int() -> None: 71 | assert isinstance(5, int), "Regular int should be treated as the new int" 72 | assert isinstance(5, builtins.int), "Regular int should be treated as a builtins.int" 73 | 74 | 75 | def test_call_isinstace_on_iint() -> None: 76 | value = aadc.iint(30) 77 | assert isinstance(value, int), "aadc.iint should be treated as the new int" 78 | assert isinstance(value, builtins.int), "aadc.iint should be treated as a builtins.int" 79 | 80 | 81 | def test_scipy_norm_cdf() -> None: 82 | rng = np.random.default_rng(1234) 83 | np_randoms = rng.standard_normal(10) 84 | 85 | funcs = Functions() 86 | funcs.start_recording() 87 | randoms = AADCArray(np_randoms) 88 | randoms.mark_as_input() 89 | 90 | with aadc.overrides.aadc_overrides(): 91 | out_aadc = norm.cdf(randoms) 92 | out_np = norm.cdf(np_randoms) 93 | 94 | funcs.stop_recording() 95 | 96 | assert isinstance(out_aadc, AADCArray) 97 | assert np.allclose(out_aadc, out_np) 98 | 99 | 100 | def test_full_like() -> None: 101 | funcs = Functions() 102 | funcs.start_recording() 103 | value = aadc.idouble(42.0) 104 | value.mark_as_input() 105 | 106 | test_array = np.array([1.0, 2.0, 3.0]) 107 | 108 | with aadc.overrides.aadc_overrides(): 109 | output = np.full_like(test_array, value) 110 | 111 | funcs.stop_recording() 112 | 113 | assert isinstance(output, AADCArray) 114 | assert np.allclose(output, 42.0) 115 | 116 | 117 | def test_numpy_monkeypatch(problematic_code) -> None: 118 | rng = np.random.default_rng(1234) 119 | 120 | assets = 10 121 | bigt = 100 122 | 123 | log_returns = AADCArray(rng.standard_normal(assets)) 124 | 125 | def without_overrides(): 126 | time.sleep(0.05) 127 | with pytest.raises(ValueError): 128 | problematic_code(log_returns, bigt, assets) 129 | 130 | thread = Thread(target=without_overrides) 131 | thread.start() 132 | 133 | with aadc.overrides.aadc_overrides(): # Could specify a subset here 134 | time.sleep(0.1) 135 | problematic_code(log_returns, bigt, assets) 136 | 137 | thread.join() 138 | 139 | 140 | def test_numpy_monkeypatch_gbm(problematic_code) -> None: 141 | s0 = 100 142 | rfr = 0.01 143 | sigma = 0.2 144 | final_time = 1 145 | num_tsteps = 100 146 | seed = 42 147 | rng = np.random.default_rng(seed) 148 | dt = final_time / num_tsteps 149 | num_paths = 1 150 | 151 | time = np.linspace(start=0.0, stop=final_time, num=num_tsteps) 152 | 153 | funcs = aadc.Functions() 154 | funcs.start_recording() 155 | 156 | normal_samples_numpy = rng.standard_normal((num_paths, num_tsteps - 1)) 157 | normal_samples = AADCArray(normal_samples_numpy) 158 | normal_samples.mark_as_input() 159 | 160 | with aadc.overrides.aadc_overrides(): 161 | bm = np.concatenate((np.atleast_2d(np.zeros(num_paths)).T, np.cumsum(normal_samples * dt, axis=1)), axis=1) 162 | 163 | s0 * np.exp((rfr - sigma**2 / 2) * time + sigma * bm) 164 | funcs.stop_recording() 165 | -------------------------------------------------------------------------------- /tests/test_vector_function_with_ad.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import aadc 4 | import aadc.ndarray 5 | from aadc.ndarray import AADCArray 6 | from aadc.recording_ctx import record_kernel 7 | 8 | 9 | def test_vector_function_with_ad() -> None: 10 | mat = np.array([[1.0, 2.0], [-3.0, -4.0], [5.0, 6.0]]) 11 | 12 | def analytics(x: AADCArray | np.ndarray, a: aadc.idouble | float) -> AADCArray | np.ndarray: 13 | if x.ndim == 1: 14 | return np.exp(-a * np.dot(mat, x)) 15 | else: 16 | # For batched inputs 17 | return np.exp(-a * np.dot(mat, x.T)).T 18 | 19 | # Record the function 20 | with record_kernel() as kernel: 21 | x = aadc.array(np.ones(2)) 22 | x_args = x.mark_as_input() 23 | 24 | a = aadc.idouble(1.0) 25 | a_arg = a.mark_as_input_no_diff() 26 | 27 | f = analytics(x, a) 28 | assert isinstance(f, AADCArray) # Keep mypy happy 29 | f_res = f.mark_as_output() 30 | 31 | # Create VectorFunctionWithAD instance 32 | vec_func = aadc.VectorFunctionWithAD(kernel, x_args, f_res, param_args=[a_arg]) 33 | 34 | # Test single input case 35 | test_set = [ 36 | (np.array([1.0, 2.0]), 0.0), 37 | (np.array([1.0, 3.0]), np.log(2.0)), 38 | (np.array([2.0, -1.0]), np.log(3.0)), 39 | ] 40 | 41 | for x_it, a_it in test_set: 42 | r = analytics(x_it, a_it) 43 | vec_func.set_params([a_it]) 44 | 45 | # Get both function value and jacobian in one call 46 | f_val, jac = vec_func.evaluate(x_it) 47 | 48 | # Test function value 49 | assert np.allclose(f_val.squeeze(), r) 50 | 51 | # Test jacobian (gradient) 52 | expected_jac = -a_it * (mat.T * r).T 53 | assert np.allclose(jac.squeeze(), expected_jac) 54 | 55 | # Test batched input case 56 | x_batch = np.array([[1.0, 2.0], [1.0, 3.0], [2.0, -1.0]]) 57 | a_value = 0.5 58 | 59 | vec_func.set_params([a_value]) 60 | 61 | # Expected results for batch 62 | r_batch = analytics(x_batch, a_value) 63 | expected_jac_batch = np.array([-a_value * (mat.T * r).T for r in r_batch]) 64 | 65 | # Get both function values and jacobians for batch 66 | f_batch, jac_batch = vec_func.evaluate(x_batch) 67 | 68 | assert np.allclose(f_batch, r_batch) 69 | assert np.allclose(jac_batch, expected_jac_batch) 70 | 71 | 72 | def test_vector_function_with_ad_threading() -> None: 73 | # Test with multiple threads 74 | n = 1000 # Large batch size to make threading worthwhile 75 | dim = 5 76 | 77 | # Create a simple quadratic function 78 | mat = np.random.randn(3, dim) 79 | 80 | def analytics(x: AADCArray | np.ndarray, a: aadc.idouble | float) -> AADCArray | np.ndarray: 81 | if x.ndim == 1: 82 | return np.exp(-a * np.dot(mat, x)) 83 | else: 84 | return np.exp(-a * np.dot(mat, x.T)).T 85 | 86 | with record_kernel() as kernel: 87 | x = aadc.array(np.zeros(dim)) 88 | x_args = x.mark_as_input_no_diff() 89 | 90 | a = aadc.idouble(0.0) 91 | a_arg = a.mark_as_input_no_diff() 92 | 93 | f = analytics(x, a) 94 | 95 | assert isinstance(f, AADCArray) # Keep mypy happy 96 | f_res = f.mark_as_output() 97 | 98 | # Create instances with different thread counts 99 | vec_func_single = aadc.VectorFunctionWithAD(kernel, x_args, f_res, param_args=[a_arg], num_threads=1) 100 | vec_func_multi = aadc.VectorFunctionWithAD(kernel, x_args, f_res, param_args=[a_arg], num_threads=4) 101 | 102 | # Test with large batch 103 | x_batch = np.random.randn(n, dim) 104 | a_value = 0.5 105 | 106 | vec_func_single.set_params([a_value]) 107 | vec_func_multi.set_params([a_value]) 108 | 109 | # Results should be the same regardless of thread count 110 | f_single, jac_single = vec_func_single.evaluate(x_batch) 111 | f_multi, jac_multi = vec_func_multi.evaluate(x_batch) 112 | 113 | assert np.allclose(f_single, f_multi) 114 | assert np.allclose(jac_single, jac_multi) 115 | 116 | 117 | def test_vector_function_with_ad_threading_batch_size_equal_1() -> None: 118 | # Test with multiple threads 119 | n = 1 # Batch size equal to 1 120 | dim = 5 121 | 122 | # Create a simple quadratic function 123 | mat = np.random.randn(3, dim) 124 | 125 | def analytics(x: AADCArray | np.ndarray, a: aadc.idouble | float) -> AADCArray | np.ndarray: 126 | if x.ndim == 1: 127 | return np.exp(-a * np.dot(mat, x)) 128 | else: 129 | return np.exp(-a * np.dot(mat, x.T)).T 130 | 131 | # Record the function 132 | with record_kernel() as kernel: 133 | x = aadc.array(np.zeros(dim)) 134 | x_args = x.mark_as_input_no_diff() 135 | 136 | a = aadc.idouble(0.0) 137 | a_arg = a.mark_as_input_no_diff() 138 | 139 | f = analytics(x, a) 140 | 141 | assert isinstance(f, AADCArray) # Keep mypy happy 142 | f_res = f.mark_as_output() 143 | 144 | # Create instances with different thread counts 145 | vec_func_single = aadc.VectorFunctionWithAD(kernel, x_args, f_res, param_args=[a_arg], num_threads=1) 146 | vec_func_multi = aadc.VectorFunctionWithAD(kernel, x_args, f_res, param_args=[a_arg], num_threads=4) 147 | 148 | x_batch = np.random.randn(n, dim) 149 | a_value = 0.5 150 | 151 | vec_func_single.set_params([a_value]) 152 | vec_func_multi.set_params([a_value]) 153 | 154 | # Results should be the same regardless of thread count 155 | f_single, jac_single = vec_func_single.evaluate(x_batch) 156 | f_multi, jac_multi = vec_func_multi.evaluate(x_batch) 157 | 158 | assert np.allclose(f_single, f_multi) 159 | assert np.allclose(jac_single, jac_multi) 160 | -------------------------------------------------------------------------------- /getting-started/06-stoch_interp.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "\"Open" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": 13, 13 | "metadata": {}, 14 | "outputs": [], 15 | "source": [ 16 | "import sys\n", 17 | "#!pip install https://matlogica.com/DemoReleases/aadc-1.7.5.30-cp3{sys.version_info.minor}-cp3{sys.version_info.minor}-linux_x86_64.whl\n", 18 | "import aadc\n", 19 | "import numpy as np\n", 20 | "import random\n" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": 14, 26 | "metadata": {}, 27 | "outputs": [ 28 | { 29 | "name": "stdout", 30 | "output_type": "stream", 31 | "text": [ 32 | "You are using evaluation version of AADC. Expire date is 20240901\n", 33 | "idouble([AAD[rv] [adj] :83,9.72e-01])\n", 34 | "Number active to passive conversions: 0 while recording Python\n" 35 | ] 36 | } 37 | ], 38 | "source": [ 39 | "random.seed(42)\n", 40 | "from datetime import date, timedelta\n", 41 | "from dateutil.relativedelta import relativedelta\n", 42 | "\n", 43 | "# Get today's date\n", 44 | "today_date = date.today()\n", 45 | "dates = np.array([today_date + relativedelta(years=i) for i in range(21)])\n", 46 | "times = aadc.array([(date - today_date).days / 365 for date in dates])\n", 47 | "\n", 48 | "kernel = aadc.Kernel()\n", 49 | "kernel.start_recording()\n", 50 | "\n", 51 | "zero_rates = aadc.array([0.0025 + 0.005 * 0.02 * random.randint(0, 99) for i in range(len(dates))])\n", 52 | "zero_args = zero_rates.mark_as_input()\n", 53 | "\n", 54 | "dfs = np.exp(-zero_rates * times)\n", 55 | "t = aadc.idouble(5.0)\n", 56 | "arg_t = t.mark_as_input()\n", 57 | "df = np.interp(t, times, dfs)\n", 58 | "\n", 59 | "df_result = df.mark_as_output()\n", 60 | "print(df)\n", 61 | "\n", 62 | "kernel.stop_recording()\n", 63 | "kernel.print_passive_extract_locations()\n" 64 | ] 65 | }, 66 | { 67 | "cell_type": "code", 68 | "execution_count": 15, 69 | "metadata": {}, 70 | "outputs": [ 71 | { 72 | "data": { 73 | "text/plain": [ 74 | "[0.0,\n", 75 | " 0.0,\n", 76 | " 0.0,\n", 77 | " 0.0,\n", 78 | " -0.010966410208294638,\n", 79 | " -4.989033589791705,\n", 80 | " 0.0,\n", 81 | " 0.0,\n", 82 | " 0.0,\n", 83 | " 0.0,\n", 84 | " 0.0,\n", 85 | " 0.0,\n", 86 | " 0.0,\n", 87 | " 0.0,\n", 88 | " 0.0,\n", 89 | " 0.0,\n", 90 | " 0.0,\n", 91 | " 0.0,\n", 92 | " 0.0,\n", 93 | " 0.0,\n", 94 | " 0.0]" 95 | ] 96 | }, 97 | "execution_count": 15, 98 | "metadata": {}, 99 | "output_type": "execute_result" 100 | } 101 | ], 102 | "source": [ 103 | "def get_aadc_risks(t):\n", 104 | " request = { df_result: zero_args }\n", 105 | " r = aadc.evaluate(kernel, request, { arg_t: [t] }, aadc.ThreadPool(1))\n", 106 | " return [item for subdict in r[1].values() for sublist in subdict.values() for item in sublist]\n", 107 | "\n", 108 | "get_aadc_risks(5.0)" 109 | ] 110 | }, 111 | { 112 | "cell_type": "code", 113 | "execution_count": 16, 114 | "metadata": {}, 115 | "outputs": [ 116 | { 117 | "data": { 118 | "application/vnd.jupyter.widget-view+json": { 119 | "model_id": "69a17088e3ef47e8b76a65d8700832e0", 120 | "version_major": 2, 121 | "version_minor": 0 122 | }, 123 | "text/plain": [ 124 | "interactive(children=(FloatSlider(value=5.0, description='Time: ', max=20.0, step=0.2), Output(layout=Layout(h…" 125 | ] 126 | }, 127 | "execution_count": 16, 128 | "metadata": {}, 129 | "output_type": "execute_result" 130 | } 131 | ], 132 | "source": [ 133 | "import matplotlib.pyplot as plt\n", 134 | "import ipywidgets as widgets\n", 135 | "from ipywidgets import interactive\n", 136 | "\n", 137 | "\n", 138 | "def risk_plot(t):\n", 139 | " risks = get_aadc_risks(t)\n", 140 | "\n", 141 | " plt.figure(figsize=(10, 6))\n", 142 | " plt.bar(range(len(times)), risks)\n", 143 | " plt.xlabel('Years')\n", 144 | " plt.ylabel('Sensitivity')\n", 145 | " plt.title('Interpolated curve delta')\n", 146 | " plt.show()\n", 147 | "\n", 148 | " # Create interactive sliders for level and slope\n", 149 | "t_slider = widgets.FloatSlider(\n", 150 | " value=5.0,\n", 151 | " min=0.0,\n", 152 | " max=20.0,\n", 153 | " step=0.2,\n", 154 | " description='Time: ',\n", 155 | " continuous_update=True\n", 156 | ")\n", 157 | "\n", 158 | "# Use interactive function to update the plot\n", 159 | "interactive_plot = interactive(risk_plot, t=t_slider)\n", 160 | "output = interactive_plot.children[-1]\n", 161 | "output.layout.height = '600px'\n", 162 | "interactive_plot\n" 163 | ] 164 | } 165 | ], 166 | "metadata": { 167 | "kernelspec": { 168 | "display_name": "Python 3", 169 | "language": "python", 170 | "name": "python3" 171 | }, 172 | "language_info": { 173 | "codemirror_mode": { 174 | "name": "ipython", 175 | "version": 3 176 | }, 177 | "file_extension": ".py", 178 | "mimetype": "text/x-python", 179 | "name": "python", 180 | "nbconvert_exporter": "python", 181 | "pygments_lexer": "ipython3", 182 | "version": "3.11.8" 183 | } 184 | }, 185 | "nbformat": 4, 186 | "nbformat_minor": 2 187 | } 188 | -------------------------------------------------------------------------------- /examples/GBM.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import aadc 3 | import math 4 | import time 5 | 6 | class Params: 7 | def __init__(self): 8 | self.NumberOfScenarios = 10000 9 | self.NumberOfTimesteps = 252 10 | 11 | class Trade: 12 | def __init__(self): 13 | self.time_to_maturity = 1.0 14 | self.risk_free_rate = 0.02 15 | self.volatility = 0.2 16 | self.stock_price = 100.0 17 | 18 | class AsianOption: 19 | # simulate risk factors using GBM stochastic differential equation 20 | def SimulateRiskFactor(self, trade): 21 | prices = [] 22 | timestep = trade.time_to_maturity / self.Configuration.NumberOfTimesteps 23 | for scenarioNumber in range(self.Configuration.NumberOfScenarios): 24 | running_sum_price = 0.0 25 | stock_price = trade.stock_price 26 | for timestepNumber in range(self.Configuration.NumberOfTimesteps): 27 | drift = (trade.risk_free_rate - 0.5 * (trade.volatility ** 2)) * timestep 28 | uncertainty = trade.volatility * np.sqrt(timestep) * self.random_normals[scenarioNumber][timestepNumber] # np.random.normal(0, 1) 29 | stock_price = stock_price * np.exp(drift + uncertainty) 30 | running_sum_price += stock_price 31 | prices.append(running_sum_price/self.Configuration.NumberOfTimesteps) 32 | return prices 33 | 34 | 35 | 36 | 37 | option = AsianOption() 38 | option.Configuration = Params() 39 | trade = Trade() 40 | # fix seed 41 | np.random.seed(17) 42 | 43 | # simulate risk factors 44 | timer_start = time.time() 45 | option.random_normals = np.random.normal(0, 1, (option.Configuration.NumberOfScenarios, option.Configuration.NumberOfTimesteps)) 46 | print("Time to simulate risk factors:", time.time() - timer_start) 47 | 48 | timer_start = time.time() 49 | prices = option.SimulateRiskFactor(trade) 50 | print(np.average(prices)) 51 | print("Time to price:", time.time() - timer_start) 52 | # bump-and-revalue risk 53 | 54 | h = 1e-6 55 | trade.stock_price += h 56 | print("dP/dS:", (np.average(option.SimulateRiskFactor(trade)) - np.average(prices)) / h) 57 | trade.stock_price -= h 58 | trade.risk_free_rate += h 59 | print("dP/dR:", (np.average(option.SimulateRiskFactor(trade)) - np.average(prices)) / h) 60 | trade.risk_free_rate -= h 61 | trade.volatility += h 62 | print("dP/dV:", (np.average(option.SimulateRiskFactor(trade)) - np.average(prices)) / h) 63 | trade.volatility -= h 64 | print("Time to bump-and-revalue:", time.time() - timer_start) 65 | 66 | # Now lets do AADC way 67 | 68 | option.Configuration.NumberOfScenarios_copy = option.Configuration.NumberOfScenarios 69 | option.Configuration.NumberOfScenarios = 1 70 | 71 | option.random_normals_copy = option.random_normals 72 | 73 | option.random_normals = aadc.array(np.random.normal(0, 1, (option.Configuration.NumberOfScenarios, option.Configuration.NumberOfTimesteps))) 74 | 75 | timer_start = time.time() 76 | kernel = aadc.Functions() 77 | kernel.start_recording() 78 | 79 | random_normals_arg = option.random_normals.mark_as_input_no_diff() # we can set new values, but we don't want to differentiate it 80 | 81 | trade.stock_price = aadc.idouble(trade.stock_price) 82 | stock_price_arg = trade.stock_price.mark_as_input() # we can set new values and differentiate it 83 | 84 | trade.risk_free_rate = aadc.idouble(trade.risk_free_rate) 85 | risk_free_rate_arg = trade.risk_free_rate.mark_as_input() 86 | 87 | trade.volatility = aadc.idouble(trade.volatility) 88 | volatility_arg = trade.volatility.mark_as_input() 89 | 90 | one_path_prices = option.SimulateRiskFactor(trade) 91 | #print(random_normals_arg) 92 | print(one_path_prices[0]) 93 | 94 | one_path_prices_res = one_path_prices[0].mark_as_output() 95 | 96 | kernel.stop_recording() 97 | #print(kernel.recording_stats("GBM")) 98 | print("Time to do AADC recording:", time.time() - timer_start) 99 | 100 | # restore original values 101 | trade.stock_price = 100.0 102 | trade.risk_free_rate = 0.02 103 | trade.volatility = 0.2 104 | option.random_normals = option.random_normals_copy 105 | option.Configuration.NumberOfScenarios = option.Configuration.NumberOfScenarios_copy 106 | 107 | 108 | inputs = {} 109 | 110 | inputs[stock_price_arg] = trade.stock_price 111 | inputs[risk_free_rate_arg] = trade.risk_free_rate 112 | inputs[volatility_arg] = trade.volatility 113 | 114 | for random_i in range(random_normals_arg[0].size): 115 | inputs[random_normals_arg[0][random_i]] = option.random_normals_copy[:, random_i].copy() 116 | 117 | # AADC pricing only: 118 | timer_start = time.time() 119 | request = { 120 | one_path_prices_res : [] 121 | } 122 | 123 | results = aadc.evaluate(kernel, request, inputs, aadc.ThreadPool(1)) 124 | print("AADC time to price:", time.time() - timer_start) 125 | 126 | print("price:", np.average(results[0][one_path_prices_res])) 127 | 128 | # AADC price + AAD first order risk 129 | timer_start = time.time() 130 | request = { 131 | one_path_prices_res : [stock_price_arg, risk_free_rate_arg, volatility_arg] 132 | } 133 | 134 | results = aadc.evaluate(kernel, request, inputs, aadc.ThreadPool(1)) 135 | print("AADC time to price + AAD:", time.time() - timer_start) 136 | 137 | print("price:", np.average(results[0][one_path_prices_res])) 138 | print("dP/dS:", np.average(results[1][one_path_prices_res][stock_price_arg])) 139 | print("dP/dR:", np.average(results[1][one_path_prices_res][risk_free_rate_arg])) 140 | print("dP/dV:", np.average(results[1][one_path_prices_res][volatility_arg])) 141 | 142 | # Use bump-and-revalue of AAD derivatives for second order 143 | 144 | inputs[stock_price_arg] = trade.stock_price + h 145 | results_up = aadc.evaluate(kernel, request, inputs, aadc.ThreadPool(1)) 146 | inputs[stock_price_arg] = trade.stock_price - h 147 | results_down = aadc.evaluate(kernel, request, inputs, aadc.ThreadPool(1)) 148 | 149 | print("d2P/dS2:", (np.average(results_up[0][one_path_prices_res]) - 2 * np.average(results[0][one_path_prices_res]) + np.average(results_down[0][one_path_prices_res])) / (h ** 2)) 150 | 151 | 152 | # kernel can be pickled and saved for later use. For example to do intra-day pricing 153 | 154 | -------------------------------------------------------------------------------- /tests/test_linear_interpolation.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pytest 3 | 4 | import aadc 5 | from aadc import idouble, record_kernel 6 | 7 | 8 | def test_interp1d_scalar() -> None: 9 | with record_kernel(): 10 | x_args = aadc.array([1.0, 2.0, 3.0, 4.0]) 11 | x0_val = idouble(2.5) 12 | x0_val.mark_as_input() 13 | y_args = aadc.array([1.0, 4.0, 9.0, 16.0]) 14 | y_args.mark_as_input() 15 | y0_val = np.interp(x0_val, x_args, y_args) 16 | assert np.isclose(y0_val, 6.5) 17 | 18 | 19 | def test_interp1d_inactive_scalar() -> None: 20 | with record_kernel(): 21 | x_args = np.array([1.0, 2.0, 3.0, 4.0]) 22 | x0_val = 2.5 23 | y_args = aadc.array([1.0, 4.0, 9.0, 16.0]) 24 | y_args.mark_as_input() 25 | y0_val = np.interp(x0_val, x_args, y_args) 26 | assert np.isclose(y0_val, 6.5) 27 | 28 | 29 | def test_interp1d_vector() -> None: 30 | with record_kernel(): 31 | x_args = aadc.array([1.0, 2.0, 3.0, 4.0]) 32 | x0_vals = aadc.array([0.5, 2.5, 3.0, 4.5]) 33 | x0_vals.mark_as_input() 34 | y_args = aadc.array([1.0, 4.0, 9.0, 16.0]) 35 | y_args.mark_as_input() 36 | y0_vals = np.interp(x0_vals, x_args, y_args) 37 | expected = np.array([1.0, 6.5, 9.0, 16.0]) 38 | assert np.allclose(y0_vals, expected) 39 | 40 | 41 | def test_interp1d_vector_left_right() -> None: 42 | with record_kernel(): 43 | x_args = aadc.array([1.0, 2.0, 3.0, 4.0]) 44 | x0_vals = aadc.array([0.5, 2.5, 3.0, 4.5]) 45 | x0_vals.mark_as_input() 46 | y_args = aadc.array([1.0, 4.0, 9.0, 16.0]) 47 | y_args.mark_as_input() 48 | 49 | # Test 'left' behavior 50 | y0_vals_left = np.interp(x0_vals, x_args, y_args, left=0.0) 51 | expected_left = np.array([0.0, 6.5, 9.0, 16.0]) 52 | 53 | y0_vals_right = np.interp(x0_vals, x_args, y_args, right=20.0) 54 | expected_right = np.array([1.0, 6.5, 9.0, 20.0]) 55 | 56 | assert np.allclose(y0_vals_left, expected_left) 57 | assert np.allclose(y0_vals_right, expected_right) 58 | 59 | 60 | def test_interp1d_inactive_vector() -> None: 61 | with record_kernel(): 62 | x_args = aadc.array([1.0, 2.0, 3.0, 4.0]) 63 | x0_vals = aadc.array([0.5, 2.5, 3.0, 4.5]) 64 | y_args = aadc.array([1.0, 4.0, 9.0, 16.0]) 65 | y_args.mark_as_input() 66 | y0_vals = np.interp(x0_vals, x_args, y_args) 67 | expected = np.array([1.0, 6.5, 9.0, 16.0]) 68 | assert np.allclose(y0_vals, expected) 69 | 70 | 71 | def test_interp1d_nonaadc_xargs() -> None: 72 | with record_kernel(): 73 | times = np.array([1.0, 4.0, 9.0, 16.0]) 74 | 75 | kernel = aadc.Kernel() 76 | kernel.start_recording() 77 | 78 | zero_rates = aadc.array([0.0025 + 0.005 * 0.02 * i for i in range(len(times))]) 79 | zero_rates.mark_as_input() 80 | 81 | dfs = np.exp(-zero_rates * times) 82 | t = aadc.idouble(5.0) 83 | t.mark_as_input() 84 | np.interp(t, times, dfs) 85 | 86 | 87 | @pytest.mark.parametrize("seed", np.arange(20)) 88 | def test_interpolation_vector_function(seed) -> None: 89 | np.random.seed(seed) 90 | num_pts = 10 91 | xs_np = np.linspace(0, 1, num_pts) 92 | ys_np = np.random.randn(num_pts) 93 | 94 | def func(xs, ys, bump=0.0): 95 | xs_np = xs + bump 96 | ys_np = ys 97 | t = xs_np.mean() 98 | yt = np.interp(t, xs_np, ys_np) 99 | t2 = 0.25 100 | yt2 = np.interp(t2, xs_np, ys_np) 101 | return yt + yt2 102 | 103 | with aadc.record_kernel() as kernel: 104 | xs = aadc.array(xs_np) 105 | ys = aadc.array(ys_np) 106 | xs_args = xs.mark_as_input() 107 | ys_args = ys.mark_as_input() 108 | yout = func(xs, ys) 109 | result = yout.mark_as_output() 110 | 111 | f = aadc.VectorFunctionWithAD(kernel, args=xs_args, res=[result], param_args=ys_args) 112 | f.set_params(np.asarray(ys)) 113 | 114 | _, ad_jacobian = f.evaluate(np.asarray(xs)) 115 | ad_jacobian = ad_jacobian[0] # Extract the jacobian from the tuple 116 | 117 | bump_size = 1e-5 118 | fd_jacobian = np.zeros(xs.shape) 119 | 120 | for i in range(xs.shape[0]): 121 | bump = np.zeros(xs.shape) 122 | bump[i] = bump_size 123 | fd_jacobian[i] = (func(xs_np, ys_np, bump) - func(xs_np, ys_np, -bump)) / (2 * bump_size) 124 | 125 | assert np.allclose(ad_jacobian.flatten(), fd_jacobian, rtol=1e-6, atol=1e-6) 126 | 127 | 128 | @pytest.mark.skip("Limitation: cannot have xs different from those used at recording time as input") 129 | def test_interpolation_vector_function_different_xs() -> None: 130 | np.random.seed(42) 131 | num_pts = 10 132 | xs_np = np.linspace(0, 1, num_pts) 133 | ys_np = np.random.randn(num_pts) 134 | # Create different evaluation points 135 | xs_eval = np.linspace(-0.5, 1.5, num_pts) # Wider range than original xs 136 | 137 | def func(xs, ys, bump=0.0): 138 | xs_np = xs + bump 139 | ys_np = ys 140 | t = xs_np.mean() 141 | yt = np.interp(t, xs_np, ys_np) 142 | t2 = 0.25 143 | yt2 = np.interp(t2, xs_np, ys_np) 144 | return yt + yt2 145 | 146 | with aadc.record_kernel() as kernel: 147 | xs = aadc.array(xs_np) 148 | ys = aadc.array(ys_np) 149 | xs_args = xs.mark_as_input() 150 | ys_args = ys.mark_as_input() 151 | yout = func(xs, ys) 152 | result = yout.mark_as_output() 153 | 154 | f = aadc.VectorFunctionWithAD(kernel, args=xs_args, res=[result], param_args=ys_args) 155 | f.set_params(np.asarray(ys)) 156 | 157 | # Evaluate at different x values 158 | _, ad_jacobian = f.evaluate(np.asarray(xs_eval)) 159 | ad_jacobian = ad_jacobian[0] # Extract the jacobian from the tuple 160 | 161 | bump_size = 1e-5 162 | fd_jacobian = np.zeros(xs_eval.shape) 163 | 164 | for i in range(xs_eval.shape[0]): 165 | bump = np.zeros(xs_eval.shape) 166 | bump[i] = bump_size 167 | fd_jacobian[i] = (func(xs_eval, ys_np, bump) - func(xs_eval, ys_np, -bump)) / (2 * bump_size) 168 | 169 | assert np.allclose(ad_jacobian.flatten(), fd_jacobian, rtol=1e-6, atol=1e-6) 170 | -------------------------------------------------------------------------------- /QuantLib/06-Serialization.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "f058977c", 6 | "metadata": {}, 7 | "source": [ 8 | "\"Open" 9 | ] 10 | }, 11 | { 12 | "cell_type": "markdown", 13 | "id": "47c84254-692a-4aa3-804a-72bcdca0e600", 14 | "metadata": {}, 15 | "source": [ 16 | "# Serialization example\n", 17 | "\n", 18 | "This example uses results from 05-OIS-QuantLib-UserCurve.ipynb. Make sure you run it and file 05-Kernel.pkl is available." 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": null, 24 | "id": "771b1930", 25 | "metadata": {}, 26 | "outputs": [], 27 | "source": [ 28 | "import sys\n", 29 | "#!pip install https://matlogica.com/DemoReleases/aadc-1.7.5.40-cp3{sys.version_info.minor}-cp3{sys.version_info.minor}-linux_x86_64.whl" 30 | ] 31 | }, 32 | { 33 | "cell_type": "code", 34 | "execution_count": 2, 35 | "id": "b28e1c9f-cbbc-4864-beea-3c68997ece5d", 36 | "metadata": {}, 37 | "outputs": [], 38 | "source": [ 39 | "# Note that now QuantLib is not longer required\n", 40 | "import pickle\n", 41 | "import aadc\n", 42 | "import numpy as np" 43 | ] 44 | }, 45 | { 46 | "cell_type": "code", 47 | "execution_count": 3, 48 | "id": "c454ad96-7f1d-4825-b895-ca6f2fadc7ac", 49 | "metadata": {}, 50 | "outputs": [], 51 | "source": [ 52 | "with open('05-Kernel.pkl', 'rb') as f:\n", 53 | " Trade = pickle.load(f)" 54 | ] 55 | }, 56 | { 57 | "cell_type": "code", 58 | "execution_count": 4, 59 | "id": "2cfb3539-87b4-4e3c-b8b3-e2e7a66063fc", 60 | "metadata": {}, 61 | "outputs": [ 62 | { 63 | "data": { 64 | "text/plain": [ 65 | "" 66 | ] 67 | }, 68 | "execution_count": 4, 69 | "metadata": {}, 70 | "output_type": "execute_result" 71 | } 72 | ], 73 | "source": [ 74 | "Trade\n", 75 | "forecast_ratesArgs = Trade[\"Inputs\"][\"forecast_rates\"]\n", 76 | "discount_spreadArg = Trade[\"Inputs\"][\"discount_spread\"]\n", 77 | "swapNPVRes = Trade[\"Outputs\"][\"swapNPV\"]\n", 78 | "Trade[\"Kernel\"]" 79 | ] 80 | }, 81 | { 82 | "cell_type": "code", 83 | "execution_count": 5, 84 | "id": "6a7ee57e-686b-46bd-b129-876f3e995f68", 85 | "metadata": {}, 86 | "outputs": [], 87 | "source": [ 88 | "# Same as in 05, but inputs can take arbitrary levels\n", 89 | "forecast_rates = [ -0.004, -0.002, 0.001, 0.005, 0.009, 0.010, 0.010, 0.012, 0.017, 0.019, 0.028, 0.032]\n", 90 | "discount_spread = - 0.001" 91 | ] 92 | }, 93 | { 94 | "cell_type": "code", 95 | "execution_count": 6, 96 | "id": "3f4e1b90-6dc8-4d77-951f-49de089fc6d4", 97 | "metadata": {}, 98 | "outputs": [], 99 | "source": [ 100 | "vol=0.01\n", 101 | "num_scenarios=10000\n", 102 | "\n", 103 | "inputs = {}\n", 104 | "for rArg, r in zip(forecast_ratesArgs, forecast_rates):\n", 105 | " inputs[rArg] = float(r) * np.random.normal(1, vol, num_scenarios) \n", 106 | "inputs[discount_spreadArg] = discount_spread * np.random.normal(1, vol, num_scenarios)" 107 | ] 108 | }, 109 | { 110 | "cell_type": "code", 111 | "execution_count": 7, 112 | "id": "941f4d50-c8e3-4d1b-92fd-cd6ad57edb1e", 113 | "metadata": {}, 114 | "outputs": [], 115 | "source": [ 116 | "request = {swapNPVRes : forecast_ratesArgs + [ discount_spreadArg ] }" 117 | ] 118 | }, 119 | { 120 | "cell_type": "code", 121 | "execution_count": 8, 122 | "id": "2a40cef4-d6f8-4dac-9b31-f13f9f97da9b", 123 | "metadata": {}, 124 | "outputs": [], 125 | "source": [ 126 | "Res = aadc.evaluate(Trade[\"Kernel\"], request, inputs, aadc.ThreadPool(4))" 127 | ] 128 | }, 129 | { 130 | "cell_type": "code", 131 | "execution_count": 9, 132 | "id": "fe91bbfe-8d3c-418c-bf48-3859c289c9ed", 133 | "metadata": {}, 134 | "outputs": [ 135 | { 136 | "name": "stdout", 137 | "output_type": "stream", 138 | "text": [ 139 | "Result [0.07790587 0.07877351 0.0781491 ... 0.07963079 0.07660915 0.07739835]\n" 140 | ] 141 | } 142 | ], 143 | "source": [ 144 | "# Now you can compare results to 05\n", 145 | "print(\"Result\", Res[0][Trade[\"Outputs\"][\"swapNPV\"]])" 146 | ] 147 | }, 148 | { 149 | "cell_type": "code", 150 | "execution_count": 10, 151 | "id": "42c1acc2-bfe5-47ea-be67-91935931518f", 152 | "metadata": {}, 153 | "outputs": [ 154 | { 155 | "name": "stdout", 156 | "output_type": "stream", 157 | "text": [ 158 | "Forecast rates risk : \n", 159 | "dNPV/dR -0.29027402552836484\n", 160 | "dNPV/dR -0.28541916520475397\n", 161 | "dNPV/dR 0.0\n", 162 | "dNPV/dR 0.0\n", 163 | "dNPV/dR -0.010097730747421036\n", 164 | "dNPV/dR -0.00011219700830467769\n", 165 | "dNPV/dR -0.02718146237262195\n", 166 | "dNPV/dR -0.07095611330754761\n", 167 | "dNPV/dR 5.321481496855116\n", 168 | "dNPV/dR 0.0058657512707737565\n", 169 | "dNPV/dR 0.0\n", 170 | "dNPV/dR 0.0\n" 171 | ] 172 | } 173 | ], 174 | "source": [ 175 | "# Bucketed sensitivities of swapNPV to the zero rates\n", 176 | "print(\"Forecast rates risk : \")\n", 177 | "for rArg in forecast_ratesArgs:\n", 178 | " print(\"dNPV/dR\", np.average(Res[1][Trade[\"Outputs\"][\"swapNPV\"]][rArg]))" 179 | ] 180 | } 181 | ], 182 | "metadata": { 183 | "kernelspec": { 184 | "display_name": "Python 3 (ipykernel)", 185 | "language": "python", 186 | "name": "python3" 187 | }, 188 | "language_info": { 189 | "codemirror_mode": { 190 | "name": "ipython", 191 | "version": 3 192 | }, 193 | "file_extension": ".py", 194 | "mimetype": "text/x-python", 195 | "name": "python", 196 | "nbconvert_exporter": "python", 197 | "pygments_lexer": "ipython3", 198 | "version": "3.11.2" 199 | } 200 | }, 201 | "nbformat": 4, 202 | "nbformat_minor": 5 203 | } 204 | -------------------------------------------------------------------------------- /examples/splines.py: -------------------------------------------------------------------------------- 1 | # Interactive AADC Spline Visualization (Static Version) 2 | # 3 | # This notebook demonstrates visualization of cubic spline interpolation using AADC (Algorithmic Automatic Differentiation for C++). 4 | # Originally interactive, this version shows static plots of: 5 | # 6 | # 1. Different knot point configurations 7 | # 2. Various function shapes 8 | # 3. Different evaluation points 9 | # 4. Sensitivities to parameters 10 | 11 | # Setup and Imports 12 | import matplotlib.pyplot as plt 13 | import numpy as np 14 | from matplotlib.gridspec import GridSpec 15 | from scipy.interpolate import CubicSpline as ScipySpline 16 | 17 | import aadc 18 | from aadc.scipy.interpolate import CubicSpline 19 | 20 | # Define the AADC spline functions 21 | def create_spline_kernel(x_values, y_values, x0_value, bc_type="not-a-knot"): 22 | with aadc.record_kernel() as kernel: 23 | # Convert inputs to AADC arrays 24 | x_active = aadc.array(x_values) 25 | x_arg = x_active.mark_as_input() 26 | 27 | y_active = aadc.array(y_values) 28 | y_arg = y_active.mark_as_input() 29 | 30 | # Create spline 31 | spline = CubicSpline(x_active, y_active, bc_type=bc_type) 32 | 33 | # Evaluation point 34 | x0 = aadc.idouble(x0_value) 35 | x0_arg = x0.mark_as_input() 36 | 37 | # Evaluate spline 38 | f = spline(x0) 39 | f_arg = f.mark_as_output() 40 | 41 | return kernel, x_arg, y_arg, x0_arg, f_arg 42 | 43 | def evaluate_spline(kernel, x_values, y_values, x0_value, x_arg, y_arg, x0_arg, f_arg): 44 | # Prepare inputs 45 | inputs = {} 46 | inputs[x0_arg] = x0_value 47 | 48 | for i, val in enumerate(x_values): 49 | inputs[x_arg[i]] = val 50 | 51 | for i, val in enumerate(y_values): 52 | inputs[y_arg[i]] = val 53 | 54 | # Evaluate kernel 55 | request = {f_arg: list(x_arg) + list(y_arg) + [x0_arg]} 56 | result = aadc.evaluate(kernel, request, inputs) 57 | 58 | # Extract results and convert to proper format 59 | value = result[0][f_arg] 60 | # Handle scalar or array differently 61 | if isinstance(value, np.ndarray) and value.size == 1: 62 | value = value.item() # Extract the scalar value from a size-1 array 63 | 64 | d_dx0 = result[1][f_arg][x0_arg] 65 | if isinstance(d_dx0, np.ndarray) and d_dx0.size == 1: 66 | d_dx0 = d_dx0.item() 67 | 68 | # Extract derivatives as lists 69 | d_dx = [] 70 | for x_i in x_arg: 71 | deriv = result[1][f_arg][x_i] 72 | if isinstance(deriv, np.ndarray) and deriv.size == 1: 73 | d_dx.append(deriv.item()) 74 | else: 75 | d_dx.append(deriv) 76 | 77 | d_dy = [] 78 | for y_i in y_arg: 79 | deriv = result[1][f_arg][y_i] 80 | if isinstance(deriv, np.ndarray) and deriv.size == 1: 81 | d_dy.append(deriv.item()) 82 | else: 83 | d_dy.append(deriv) 84 | 85 | return value, d_dx0, d_dx, d_dy 86 | 87 | # Function to create visualization 88 | def create_spline_plot(x_values, y_values, x0_value, bc_type="not-a-knot", title=None): 89 | # Create kernel 90 | kernel, x_arg, y_arg, x0_arg, f_arg = create_spline_kernel(x_values, y_values, x0_value, bc_type) 91 | 92 | # Evaluate the spline at x0 93 | value, d_dx0, d_dx, d_dy = evaluate_spline(kernel, x_values, y_values, x0_value, x_arg, y_arg, x0_arg, f_arg) 94 | 95 | # Create a fine grid for plotting the spline 96 | x_fine = np.linspace(min(x_values) - 0.5, max(x_values) + 0.5, 200) 97 | 98 | # Use SciPy for visualization (faster than evaluating AADC at each point) 99 | scipy_spline = ScipySpline(x_values, y_values, bc_type=bc_type) 100 | y_fine = scipy_spline(x_fine) 101 | 102 | # Set up the figure with GridSpec for better layout 103 | fig = plt.figure(figsize=(14, 10)) 104 | gs = GridSpec(2, 2, height_ratios=[2, 1]) 105 | 106 | # Main plot - spline curve and points 107 | ax1 = plt.subplot(gs[0, :]) 108 | ax1.plot(x_fine, y_fine, 'b-', label='Cubic Spline') 109 | ax1.plot(x_values, y_values, 'ro', markersize=8, label='Knot Points') 110 | ax1.axvline(x=x0_value, color='g', linestyle='--', label='Evaluation Point') 111 | ax1.axhline(y=value, color='g', linestyle='--') 112 | ax1.plot(x0_value, value, 'g*', markersize=10, label=f'Spline(x0) = {value:.4f}') 113 | 114 | # Add derivative visualization as a tangent line 115 | tangent_width = 1.0 116 | x_tangent = np.array([x0_value - tangent_width/2, x0_value + tangent_width/2]) 117 | y_tangent = value + d_dx0 * (x_tangent - x0_value) 118 | ax1.plot(x_tangent, y_tangent, 'g-', linewidth=2, label=f'Derivative at x0: {d_dx0:.4f}') 119 | 120 | ax1.set_xlabel('x') 121 | ax1.set_ylabel('y') 122 | if title: 123 | ax1.set_title(f'Cubic Spline Interpolation: {title}') 124 | else: 125 | ax1.set_title('Cubic Spline Interpolation') 126 | ax1.legend() 127 | ax1.grid(True) 128 | 129 | # Lower left - sensitivity to x 130 | ax2 = plt.subplot(gs[1, 0]) 131 | ax2.bar(range(len(x_values)), d_dx, color='purple') 132 | ax2.set_xlabel('Knot Point Index') 133 | ax2.set_ylabel('Sensitivity ∂f/∂x') 134 | ax2.set_title('Sensitivity to X Values') 135 | ax2.set_xticks(range(len(x_values))) 136 | ax2.grid(True, axis='y') 137 | 138 | # Lower right - sensitivity to y 139 | ax3 = plt.subplot(gs[1, 1]) 140 | ax3.bar(range(len(y_values)), d_dy, color='orange') 141 | ax3.set_xlabel('Knot Point Index') 142 | ax3.set_ylabel('Sensitivity ∂f/∂y') 143 | ax3.set_title('Sensitivity to Y Values') 144 | ax3.set_xticks(range(len(y_values))) 145 | ax3.grid(True, axis='y') 146 | 147 | plt.tight_layout() 148 | return fig 149 | 150 | # Create a series of static plots demonstrating different aspects 151 | 152 | # 1. Basic example with sin(x) 153 | num_points = 5 154 | x_default = np.linspace(0, 10, num_points) 155 | y_default = np.sin(x_default) 156 | x0_default = 6.5 157 | 158 | fig1 = create_spline_plot(x_default, y_default, x0_default, bc_type="not-a-knot", 159 | title="Sin(x) Function with Not-a-knot Boundary Condition") 160 | plt.show() 161 | 162 | # 2. Different boundary conditions 163 | bc_types = ["natural", "clamped", "not-a-knot"] 164 | for bc_type in bc_types: 165 | fig = create_spline_plot(x_default, y_default, x0_default, bc_type=bc_type, 166 | title=f"Sin(x) with {bc_type} Boundary Condition") 167 | plt.show() 168 | 169 | # 3. Different function types 170 | # Quadratic 171 | y_quadratic = x_default**2 172 | fig3 = create_spline_plot(x_default, y_quadratic, x0_default, title="Quadratic Function") 173 | plt.show() 174 | 175 | # Exponential 176 | y_exponential = np.exp(x_default/5) 177 | fig4 = create_spline_plot(x_default, y_exponential, x0_default, title="Exponential Function") 178 | plt.show() 179 | 180 | -------------------------------------------------------------------------------- /examples/vector_function_with_ad.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 4, 6 | "metadata": {}, 7 | "outputs": [ 8 | { 9 | "name": "stdout", 10 | "output_type": "stream", 11 | "text": [ 12 | "Running cmake --build & --install in /home/ocmob/dev/aadc/AADC-Python-Bindings/build\n", 13 | "You are using evaluation version of AADC. Expire date is 20250201\n" 14 | ] 15 | } 16 | ], 17 | "source": [ 18 | "from dataclasses import dataclass\n", 19 | "from typing import Tuple\n", 20 | "\n", 21 | "import aadc.evaluate_wrappers\n", 22 | "import numpy as np\n", 23 | "import numpy.typing as npt\n", 24 | "\n", 25 | "import aadc\n", 26 | "from aadc import VectorFunctionWithAD\n", 27 | "from aadc.recording_ctx import record_kernel\n", 28 | "\n", 29 | "\n", 30 | "@dataclass\n", 31 | "class BlackScholesModel:\n", 32 | " sigma: float\n", 33 | " rfr: float\n", 34 | " s0: float\n", 35 | " \n", 36 | " def simulate(self, final_time: float, normal_samples: npt.NDArray[np.double]) -> Tuple[npt.NDArray[np.double], npt.NDArray[np.double]]:\n", 37 | " num_steps = normal_samples.shape[1]\n", 38 | " dt = final_time / num_steps\n", 39 | " time = aadc.array(np.zeros(num_steps))\n", 40 | " gbm = aadc.array(np.zeros_like(normal_samples))\n", 41 | " \n", 42 | " gbm[:, 0] = self.s0\n", 43 | " bm_curr = 0.\n", 44 | " \n", 45 | " for i in range(1, num_steps):\n", 46 | " time[i] = i * dt\n", 47 | " bm_curr = bm_curr + normal_samples[:, i] * np.sqrt(dt)\n", 48 | " gbm[:, i] = self.s0 * np.exp((self.rfr - self.sigma**2/2) * time[i] + self.sigma * bm_curr)\n", 49 | " \n", 50 | " return time, gbm\n", 51 | " \n", 52 | "\n", 53 | "@dataclass\n", 54 | "class DownAndOutCallPayoff:\n", 55 | " barrier: float\n", 56 | " strike: float\n", 57 | " \n", 58 | " def evaluate(self, paths: npt.NDArray[np.double]) -> npt.NDArray[np.double]:\n", 59 | " scaling_factor = 1e+1\n", 60 | " barrier_distances = (paths - self.barrier) * scaling_factor\n", 61 | " survival_probs = np.prod((1 + np.tanh(barrier_distances)) / 2, axis=1)\n", 62 | " call_payoff = np.maximum(paths[:, -1] - self.strike, 0.0)\n", 63 | " payoffs = survival_probs * call_payoff\n", 64 | " return payoffs\n", 65 | "\n", 66 | "M = 500\n", 67 | "N = 1000\n", 68 | "RANDOM_SEED = 2137\n", 69 | "SPOT = 100.0\n", 70 | "STRIKE = 100.0\n", 71 | "RISK_FREE_RATE = 0.05\n", 72 | "EXPIRY = 1.0\n", 73 | "VOLATILITY = 0.2\n", 74 | "BARRIER = 90.0\n", 75 | "\n", 76 | "rng = np.random.default_rng(RANDOM_SEED)\n", 77 | "normal_samples = rng.standard_normal((1, M))\n", 78 | "params_np = aadc.array([\n", 79 | " SPOT, \n", 80 | " STRIKE, \n", 81 | " BARRIER,\n", 82 | " EXPIRY, \n", 83 | " RISK_FREE_RATE, \n", 84 | " VOLATILITY\n", 85 | "])\n", 86 | "\n", 87 | "with record_kernel() as kernel:\n", 88 | " params = aadc.array(params_np)\n", 89 | " pin = params.mark_as_input()\n", 90 | " active_samples = aadc.array(normal_samples)\n", 91 | " asin = active_samples.mark_as_input_no_diff()\n", 92 | "\n", 93 | " model = BlackScholesModel(params[-1], params[-2], params[0])\n", 94 | " payoff = DownAndOutCallPayoff(params[2], params[1])\n", 95 | " times, paths = model.simulate(params[3], active_samples)\n", 96 | " price_undiscounted = payoff.evaluate(paths)\n", 97 | " pundout = price_undiscounted.mark_as_output()\n", 98 | " price_discounted = price_undiscounted*np.exp(-params[-2]*params[3])\n", 99 | " pout = price_discounted.mark_as_output()\n", 100 | "\n", 101 | "normal_samples_eval = rng.standard_normal((N, M))\n" 102 | ] 103 | }, 104 | { 105 | "cell_type": "code", 106 | "execution_count": 5, 107 | "metadata": {}, 108 | "outputs": [ 109 | { 110 | "name": "stdout", 111 | "output_type": "stream", 112 | "text": [ 113 | "9.541824644245017 [59.25275641 14.62033378]\n" 114 | ] 115 | } 116 | ], 117 | "source": [ 118 | "calibration_vf = VectorFunctionWithAD(kernel, pin[4:], pout, batch_param_args=np.squeeze(asin), param_args=pin[:4], num_threads=12)\n", 119 | "calibration_vf.set_params(params_np[:4])\n", 120 | "calibration_vf.set_batch_params(normal_samples_eval)\n", 121 | "values, grads = calibration_vf.evaluate(params_np[4:])\n", 122 | "\n", 123 | "value_calib = values.mean()\n", 124 | "dvalue_d_bs_params = grads.mean(axis=0).squeeze()\n", 125 | "\n", 126 | "print(value_calib, dvalue_d_bs_params)" 127 | ] 128 | }, 129 | { 130 | "cell_type": "code", 131 | "execution_count": 6, 132 | "metadata": {}, 133 | "outputs": [ 134 | { 135 | "name": "stdout", 136 | "output_type": "stream", 137 | "text": [ 138 | "9.541824644245017 [ 0.83005581 -0.42013721 -0.3493186 4.4246712 59.25275641 14.62033378]\n" 139 | ] 140 | } 141 | ], 142 | "source": [ 143 | "evaluation_vf = VectorFunctionWithAD(kernel, pin, pout, batch_param_args=np.squeeze(asin), num_threads=12)\n", 144 | "evaluation_vf.set_batch_params(normal_samples_eval)\n", 145 | "values, grads = evaluation_vf.evaluate(params_np)\n", 146 | "\n", 147 | "value_eval = values.mean()\n", 148 | "dvalue_dparams = grads.mean(axis=0).squeeze()\n", 149 | "\n", 150 | "print(value_eval, dvalue_dparams)" 151 | ] 152 | }, 153 | { 154 | "cell_type": "code", 155 | "execution_count": 7, 156 | "metadata": {}, 157 | "outputs": [], 158 | "source": [ 159 | "inputs = {\n", 160 | " **{param: param_value for param, param_value in zip(pin, params_np)},\n", 161 | " **{sample: samples_row for sample, samples_row in zip(np.squeeze(asin), normal_samples_eval)}\n", 162 | "}\n", 163 | "request = {\n", 164 | " pout.item(): pin.tolist(),\n", 165 | " pundout.item(): pin.tolist()\n", 166 | "}\n", 167 | "\n", 168 | "workers = aadc.ThreadPool(12)\n", 169 | "values, derivs = aadc.evaluate(kernel, request, inputs, workers)" 170 | ] 171 | }, 172 | { 173 | "cell_type": "code", 174 | "execution_count": null, 175 | "metadata": {}, 176 | "outputs": [], 177 | "source": [] 178 | } 179 | ], 180 | "metadata": { 181 | "kernelspec": { 182 | "display_name": "aadcpy", 183 | "language": "python", 184 | "name": "python3" 185 | }, 186 | "language_info": { 187 | "codemirror_mode": { 188 | "name": "ipython", 189 | "version": 3 190 | }, 191 | "file_extension": ".py", 192 | "mimetype": "text/x-python", 193 | "name": "python", 194 | "nbconvert_exporter": "python", 195 | "pygments_lexer": "ipython3", 196 | "version": "3.11.11" 197 | } 198 | }, 199 | "nbformat": 4, 200 | "nbformat_minor": 2 201 | } 202 | -------------------------------------------------------------------------------- /tests/test_statistical_functions.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import aadc 4 | from aadc import Functions, idouble 5 | from aadc.ndarray import AADCArray 6 | 7 | 8 | def test_cumsum() -> None: 9 | func = Functions() 10 | func.start_recording() 11 | val = AADCArray([1.0, 2.0, 3.0]) 12 | val.mark_as_input() 13 | val = np.cumsum(val) 14 | func.stop_recording() 15 | assert isinstance(val, AADCArray) 16 | assert val == np.array([1.0, 3.0, 6.0]) 17 | 18 | 19 | def test_max() -> None: 20 | func = Functions() 21 | func.start_recording() 22 | val = AADCArray([1.0, 2.0, 3.0]) 23 | val.mark_as_input() 24 | val = np.max(val) 25 | func.stop_recording() 26 | assert isinstance(val, idouble) 27 | assert val == 3.0 28 | 29 | 30 | def test_mean() -> None: 31 | func = Functions() 32 | func.start_recording() 33 | val = AADCArray([1.0, 2.0, 3.0]) 34 | val.mark_as_input() 35 | val = np.mean(val) 36 | func.stop_recording() 37 | assert isinstance(val, idouble) 38 | assert val == 2.0 39 | 40 | 41 | def test_min() -> None: 42 | func = Functions() 43 | func.start_recording() 44 | val = AADCArray([1.0, 2.0, 3.0]) 45 | val.mark_as_input() 46 | val = np.min(val) 47 | func.stop_recording() 48 | assert isinstance(val, idouble) 49 | assert val == 1.0 50 | 51 | 52 | def test_prod() -> None: 53 | func = Functions() 54 | func.start_recording() 55 | val = AADCArray([1.0, 2.0, 3.0]) 56 | val.mark_as_input() 57 | val = np.prod(val) 58 | func.stop_recording() 59 | assert isinstance(val, idouble) 60 | assert val == 6.0 61 | 62 | 63 | def test_std() -> None: 64 | func = Functions() 65 | func.start_recording() 66 | val = AADCArray([1.0, 2.0, 3.0]) 67 | val.mark_as_input() 68 | val = np.std(val) 69 | func.stop_recording() 70 | assert isinstance(val, idouble) 71 | assert round(val, 2) == 0.82 72 | 73 | 74 | def test_sum() -> None: 75 | func = Functions() 76 | func.start_recording() 77 | val = AADCArray([1.0, 2.0, 3.0]) 78 | val.mark_as_input() 79 | val = np.sum(val) 80 | func.stop_recording() 81 | assert isinstance(val, idouble) 82 | assert val == 6.0 83 | 84 | 85 | def test_sum_2d() -> None: 86 | func = Functions() 87 | func.start_recording() 88 | val = AADCArray([[1.0, 2.0], [3.0, 4.0]]) 89 | val.mark_as_input() 90 | val = np.sum(val, axis=0) 91 | func.stop_recording() 92 | assert isinstance(val, AADCArray) 93 | assert np.all(val == [4.0, 6.0]) 94 | 95 | 96 | def test_sum_3d() -> None: 97 | func = Functions() 98 | func.start_recording() 99 | val = AADCArray([[[1.0, 2.0], [3.0, 4.0]], [[5.0, 6.0], [7.0, 8.0]]]) 100 | val.mark_as_input() 101 | val = np.sum(val, axis=2) 102 | func.stop_recording() 103 | assert isinstance(val, AADCArray) 104 | assert np.all(val == np.array([[3.0, 7.0], [11.0, 15.0]])) 105 | 106 | 107 | def test_var() -> None: 108 | func = Functions() 109 | func.start_recording() 110 | val = AADCArray([1.0, 2.0, 3.0]) 111 | val.mark_as_input() 112 | val = np.var(val) 113 | func.stop_recording() 114 | assert isinstance(val, idouble) 115 | assert round(val, 2) == 0.67 116 | 117 | 118 | def test_diff() -> None: 119 | func = Functions() 120 | func.start_recording() 121 | val = AADCArray([2.0, 1.0, 2.0, 3.0]) 122 | val.mark_as_input() 123 | val = np.diff(val) 124 | func.stop_recording() 125 | assert isinstance(val, AADCArray) 126 | assert val == np.array([1.0, -1.0, -4.0]) 127 | 128 | 129 | def test_average_without_weights() -> None: 130 | func = Functions() 131 | func.start_recording() 132 | val = AADCArray([1.0, 2.0, 3.0]) 133 | val.mark_as_input() 134 | val = np.average(val) 135 | func.stop_recording() 136 | assert isinstance(val, idouble) 137 | assert val == 2.0 138 | 139 | 140 | def test_average_without_weights_2d() -> None: 141 | func = Functions() 142 | func.start_recording() 143 | val = AADCArray([[1.0, 2.0], [3.0, 4.0]]) 144 | val.mark_as_input() 145 | val = np.average(val, axis=1) 146 | func.stop_recording() 147 | assert isinstance(val, AADCArray) 148 | assert np.allclose(val, [1.5, 3.5]) 149 | 150 | 151 | def test_average_with_weights() -> None: 152 | func = Functions() 153 | func.start_recording() 154 | val = AADCArray([1.0, 2.0, 3.0]) 155 | weights = AADCArray([1.0, 1.0, 1.0]) 156 | val.mark_as_input() 157 | weights.mark_as_input() 158 | val = np.average(val, weights=weights) 159 | func.stop_recording() 160 | assert isinstance(val, idouble) 161 | assert val == 2.0 162 | 163 | 164 | def test_average_with_weights_2d() -> None: 165 | func = Functions() 166 | func.start_recording() 167 | val_np = AADCArray([[1.0, 2.0], [3.0, 4.0]]) 168 | weights_np = AADCArray([[1.0, 2.0], [3.0, 4.0]]) 169 | val = AADCArray(val_np) 170 | weights = AADCArray(weights_np) 171 | val.mark_as_input() 172 | weights.mark_as_input() 173 | val = np.average(val, weights=weights, axis=1) 174 | func.stop_recording() 175 | assert isinstance(val, AADCArray) 176 | assert np.allclose(val, np.average(val_np, weights=weights_np, axis=1)) 177 | 178 | 179 | def test_cov() -> None: 180 | funcs = aadc.Functions() 181 | funcs.start_recording() 182 | xnp = np.array([1.0, 2.0]) 183 | x = aadc.array(xnp) 184 | x.mark_as_input() 185 | z = np.cov(x, x) 186 | funcs.stop_recording() 187 | assert np.allclose(z, np.cov(xnp, xnp)) 188 | 189 | 190 | def test_cov_with_y() -> None: 191 | funcs = aadc.Functions() 192 | funcs.start_recording() 193 | xnp = np.array([1.0, 2.0]) 194 | ynp = np.array([3.0, 4.0]) 195 | x = aadc.array(xnp) 196 | y = aadc.array(ynp) 197 | x.mark_as_input() 198 | y.mark_as_input() 199 | z = np.cov(x, y) 200 | funcs.stop_recording() 201 | assert np.allclose(z, np.cov(xnp, ynp)) 202 | 203 | 204 | def test_cov_rowvar_false() -> None: 205 | funcs = aadc.Functions() 206 | funcs.start_recording() 207 | xnp = np.array([[1.0, 2.0], [3.0, 4.0]]) 208 | x = aadc.array(xnp) 209 | x.mark_as_input() 210 | z = np.cov(x, rowvar=False) 211 | funcs.stop_recording() 212 | assert np.allclose(z, np.cov(xnp, rowvar=False)) 213 | 214 | 215 | def test_cov_with_bias() -> None: 216 | funcs = aadc.Functions() 217 | funcs.start_recording() 218 | xnp = np.array([1.0, 2.0, 3.0]) 219 | x = aadc.array(xnp) 220 | x.mark_as_input() 221 | z = np.cov(x, bias=True) 222 | funcs.stop_recording() 223 | assert np.allclose(z, np.cov(xnp, bias=True)) 224 | 225 | 226 | def test_cov_with_fweights() -> None: 227 | funcs = aadc.Functions() 228 | funcs.start_recording() 229 | xnp = np.array([1.0, 2.0, 3.0]) 230 | fweights_np = np.array([1.0, 2.0, 3.0]) 231 | x = aadc.array(xnp) 232 | x.mark_as_input() 233 | z = np.cov(x, fweights=fweights_np) 234 | funcs.stop_recording() 235 | assert np.allclose(z, np.cov(xnp, fweights=fweights_np)) 236 | -------------------------------------------------------------------------------- /tests/test_idouble_with_recording.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from aadc import Functions, idouble 4 | from aadc.ndarray import AADCArray 5 | 6 | 7 | def test_add_array_to_idouble() -> None: 8 | func = Functions() 9 | func.start_recording() 10 | 11 | val = idouble(4.0) 12 | val.mark_as_input() 13 | result = val + np.array([3.0, 4.0]) 14 | 15 | func.stop_recording() 16 | 17 | assert type(result) == AADCArray, "Result should be an AADCArray" 18 | assert np.all(result == np.array([7.0, 8.0])), "Result should match expected values" 19 | 20 | 21 | def test_add_idouble_to_array() -> None: 22 | func = Functions() 23 | func.start_recording() 24 | 25 | val = idouble(4.0) 26 | val.mark_as_input() 27 | result = np.array([3.0, 4.0]) 28 | result += val 29 | 30 | func.stop_recording() 31 | 32 | assert type(result) == AADCArray, "Result should be an AADCArray" 33 | assert np.all(result == np.array([7.0, 8.0])), "Result should match expected values" 34 | 35 | 36 | def test_sub_idouble_from_array() -> None: 37 | func = Functions() 38 | func.start_recording() 39 | 40 | val = idouble(4.0) 41 | val.mark_as_input() 42 | result = np.array([3.0, 4.0]) 43 | result -= val 44 | 45 | func.stop_recording() 46 | 47 | assert type(result) == AADCArray, "Result should be an AADCArray" 48 | assert np.all(result == np.array([-1.0, 0.0])), "Result should match expected values" 49 | 50 | 51 | def test_sub_array_from_idouble() -> None: 52 | func = Functions() 53 | func.start_recording() 54 | 55 | val = idouble(4.0) 56 | val.mark_as_input() 57 | result = val - np.array([3.0, 4.0]) 58 | 59 | func.stop_recording() 60 | 61 | assert type(result) == AADCArray, "Result should be an AADCArray" 62 | assert np.all(result == np.array([1.0, 0.0])), "Result should match expected values" 63 | 64 | 65 | def test_mul_idouble_with_array() -> None: 66 | func = Functions() 67 | func.start_recording() 68 | 69 | val = idouble(4.0) 70 | val.mark_as_input() 71 | result = val * np.array([3.0, 4.0]) 72 | 73 | func.stop_recording() 74 | 75 | assert type(result) == AADCArray, "Result should be an AADCArray" 76 | assert np.all(result == np.array([12.0, 16.0])), "Result should match expected values" 77 | 78 | 79 | def test_div_idouble_by_array() -> None: 80 | func = Functions() 81 | func.start_recording() 82 | 83 | val = idouble(4.0) 84 | val.mark_as_input() 85 | result = val / np.array([3.0, 4.0]) 86 | 87 | func.stop_recording() 88 | 89 | assert type(result) == AADCArray, "Result should be an AADCArray" 90 | assert np.all(result == np.array([4.0 / 3.0, 1.0])), "Result should match expected values" 91 | 92 | 93 | def test_eq_idouble_with_array() -> None: 94 | func = Functions() 95 | func.start_recording() 96 | 97 | val = idouble(4.0) 98 | val.mark_as_input() 99 | result = val == np.array([3.0, 4.0]) 100 | 101 | func.stop_recording() 102 | 103 | assert type(result) == AADCArray, "Result should be an AADCArray" 104 | assert np.all(result == np.array([False, True])), "Result should match expected values" 105 | 106 | 107 | def test_gt_idouble_with_array() -> None: 108 | func = Functions() 109 | func.start_recording() 110 | 111 | val = idouble(4.0) 112 | val.mark_as_input() 113 | result = val > np.array([3.0, 4.0]) 114 | 115 | func.stop_recording() 116 | 117 | assert type(result) == AADCArray, "Result should be an AADCArray" 118 | assert np.all(result == np.array([True, False])), "Result should match expected values" 119 | 120 | 121 | def test_le_idouble_with_array() -> None: 122 | func = Functions() 123 | func.start_recording() 124 | 125 | val = idouble(4.0) 126 | val.mark_as_input() 127 | result = val <= np.array([3.0, 4.0]) 128 | 129 | func.stop_recording() 130 | 131 | assert type(result) == AADCArray, "Result should be an AADCArray" 132 | assert np.all(result == np.array([False, True])), "Result should match expected values" 133 | 134 | 135 | def test_ge_idouble_with_array() -> None: 136 | func = Functions() 137 | func.start_recording() 138 | 139 | val = idouble(4.0) 140 | val.mark_as_input() 141 | result = val >= np.array([3.0, 4.0]) 142 | 143 | func.stop_recording() 144 | 145 | assert type(result) == AADCArray, "Result should be an AADCArray" 146 | assert np.all(result == np.array([True, True])), "Result should match expected values" 147 | 148 | 149 | def test_mul_array_with_idouble() -> None: 150 | func = Functions() 151 | func.start_recording() 152 | 153 | val = idouble(4.0) 154 | val.mark_as_input() 155 | result = np.array([3.0, 4.0]) * val 156 | 157 | func.stop_recording() 158 | 159 | assert type(result) == AADCArray, "Result should be an AADCArray" 160 | assert np.all(result == np.array([12.0, 16.0])), "Result should match expected values" 161 | 162 | 163 | def test_div_array_by_idouble() -> None: 164 | func = Functions() 165 | func.start_recording() 166 | 167 | val = idouble(4.0) 168 | val.mark_as_input() 169 | result = np.array([3.0, 4.0]) / val 170 | 171 | func.stop_recording() 172 | 173 | assert type(result) == AADCArray, "Result should be an AADCArray" 174 | assert np.all(result == np.array([3.0 / 4.0, 1.0])), "Result should match expected values" 175 | 176 | 177 | def test_eq_array_with_idouble() -> None: 178 | func = Functions() 179 | func.start_recording() 180 | 181 | val = idouble(4.0) 182 | val.mark_as_input() 183 | result = np.array([3.0, 4.0]) == val 184 | 185 | func.stop_recording() 186 | 187 | assert type(result) == AADCArray, "Result should be an AADCArray" 188 | assert np.all(result == np.array([False, True])), "Result should match expected values" 189 | 190 | 191 | def test_gt_array_with_idouble() -> None: 192 | func = Functions() 193 | func.start_recording() 194 | 195 | val = idouble(4.0) 196 | val.mark_as_input() 197 | result = np.array([3.0, 4.0]) > val 198 | 199 | func.stop_recording() 200 | 201 | assert type(result) == AADCArray, "Result should be an AADCArray" 202 | assert np.all(result == np.array([False, False])), "Result should match expected values" 203 | 204 | 205 | def test_le_array_with_idouble() -> None: 206 | func = Functions() 207 | func.start_recording() 208 | 209 | val = idouble(4.0) 210 | val.mark_as_input() 211 | result = np.array([3.0, 4.0]) <= val 212 | 213 | func.stop_recording() 214 | 215 | assert type(result) == AADCArray, "Result should be an AADCArray" 216 | assert np.all(result == np.array([True, True])), "Result should match expected values" 217 | 218 | 219 | def test_ge_array_with_idouble() -> None: 220 | func = Functions() 221 | func.start_recording() 222 | 223 | val = idouble(4.0) 224 | val.mark_as_input() 225 | result = np.array([3.0, 4.0]) >= val 226 | 227 | func.stop_recording() 228 | 229 | assert type(result) == AADCArray, "Result should be an AADCArray" 230 | assert np.all(result == np.array([False, True])), "Result should match expected values" 231 | -------------------------------------------------------------------------------- /tests/test_manipulation_functions.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from aadc import Functions, idouble 4 | from aadc.ndarray import AADCArray 5 | 6 | 7 | class TestBroadcastArrays: 8 | def test_two_active_arrays(self): 9 | func = Functions() 10 | func.start_recording() 11 | val1 = AADCArray([1.0, 2.0]) 12 | val1.mark_as_input() 13 | val2 = AADCArray([[1.0], [2.0]]) 14 | val2.mark_as_input() 15 | val1, val2 = np.broadcast_arrays(val1, val2) 16 | func.stop_recording() 17 | assert isinstance(val1, AADCArray) 18 | assert isinstance(val2, AADCArray) 19 | assert np.all(val1 == np.array([[1.0, 2.0], [1.0, 2.0]])) 20 | assert np.all(val2 == np.array([[1.0, 1.0], [2.0, 2.0]])) 21 | 22 | def test_active_idouble_inactive_aadcarray(self): 23 | func = Functions() 24 | func.start_recording() 25 | val1 = idouble(1.0) 26 | val1.mark_as_input() 27 | val2 = AADCArray(np.array([[1.0], [2.0]])) 28 | val1, val2 = np.broadcast_arrays(val1, val2) 29 | func.stop_recording() 30 | assert isinstance(val1, AADCArray) 31 | assert isinstance(val2, AADCArray) 32 | assert np.all(val1 == np.array([[1.0], [1.0]])) 33 | assert np.all(val2 == np.array([[1.0], [2.0]])) 34 | 35 | def test_active_idouble_numpy_array(self): 36 | func = Functions() 37 | func.start_recording() 38 | val1 = idouble(1.0) 39 | val1.mark_as_input() 40 | val2 = np.array([[1.0], [2.0]]) 41 | val1, val2 = np.broadcast_arrays(val1, val2) 42 | func.stop_recording() 43 | assert isinstance(val1, AADCArray) 44 | assert isinstance(val2, AADCArray) 45 | assert np.all(val1 == np.array([[1.0], [1.0]])) 46 | assert np.all(val2 == np.array([[1.0], [2.0]])) 47 | 48 | def test_inactive_idouble_numpy_array(self): 49 | func = Functions() 50 | func.start_recording() 51 | val1 = idouble(1.0) 52 | val2 = np.array([[1.0], [2.0]]) 53 | val1, val2 = np.broadcast_arrays(val1, val2) 54 | func.stop_recording() 55 | assert isinstance(val1, AADCArray) 56 | assert isinstance(val2, AADCArray) 57 | assert np.all(val1 == np.array([[1.0], [1.0]])) 58 | assert np.all(val2 == np.array([[1.0], [2.0]])) 59 | 60 | 61 | def test_broadcast_to() -> None: 62 | func = Functions() 63 | func.start_recording() 64 | val = AADCArray([1.0, 2.0, 3.0]) 65 | val.mark_as_input() 66 | val = np.broadcast_to(val, (3, 3)) 67 | func.stop_recording() 68 | assert isinstance(val, AADCArray) 69 | assert np.all(val == np.array([[1.0, 2.0, 3.0], [1.0, 2.0, 3.0], [1.0, 2.0, 3.0]])) 70 | 71 | 72 | def test_concatenate() -> None: 73 | func = Functions() 74 | func.start_recording() 75 | val1 = AADCArray([[1.0, 2.0], [3.0, 4.0]]) 76 | val2 = AADCArray([[5.0, 6.0]]) 77 | val1.mark_as_input() 78 | val2.mark_as_input() 79 | val = np.concatenate((val1, val2), axis=0) 80 | func.stop_recording() 81 | assert isinstance(val, AADCArray) 82 | assert np.all(val == np.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]])) 83 | 84 | 85 | def test_concatenate_active_inactive() -> None: 86 | func = Functions() 87 | func.start_recording() 88 | val1 = AADCArray([[1.0, 2.0], [3.0, 4.0]]) 89 | val2 = AADCArray([[5.0, 6.0]]) 90 | val1.mark_as_input() 91 | val = np.concatenate((val1, val2), axis=0) 92 | func.stop_recording() 93 | assert isinstance(val, AADCArray) 94 | assert np.all(val == np.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]])) 95 | 96 | 97 | def test_concatenate_numpy_array() -> None: 98 | func = Functions() 99 | func.start_recording() 100 | val1 = AADCArray([[1.0, 2.0], [3.0, 4.0]]) 101 | val2 = np.array([[5.0, 6.0]]) 102 | val1.mark_as_input() 103 | val = np.concatenate((val1, val2), axis=0) 104 | func.stop_recording() 105 | assert isinstance(val, AADCArray) 106 | assert np.all(val == np.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]])) 107 | 108 | 109 | def test_expand_dims() -> None: 110 | func = Functions() 111 | func.start_recording() 112 | val = AADCArray([1.0, 2.0]) 113 | val.mark_as_input() 114 | val = np.expand_dims(val, axis=1) 115 | func.stop_recording() 116 | assert isinstance(val, AADCArray) 117 | assert np.all(val == np.array([[1.0], [2.0]])) 118 | 119 | 120 | def test_flip() -> None: 121 | func = Functions() 122 | func.start_recording() 123 | val = AADCArray([[1.0, 2.0], [3.0, 4.0]]) 124 | val.mark_as_input() 125 | val = np.flip(val) 126 | func.stop_recording() 127 | assert isinstance(val, AADCArray) 128 | assert np.all(val == np.array([[4.0, 3.0], [2.0, 1.0]])) 129 | 130 | 131 | def test_moveaxis() -> None: 132 | func = Functions() 133 | func.start_recording() 134 | val = AADCArray(np.ones((3, 4, 5))) 135 | val.mark_as_input() 136 | val = np.moveaxis(val, 0, -1) 137 | func.stop_recording() 138 | assert isinstance(val, AADCArray) 139 | assert val.shape == (4, 5, 3) 140 | 141 | 142 | def test_repeat() -> None: 143 | func = Functions() 144 | func.start_recording() 145 | val = AADCArray([1.0, 2.0, 3.0]) 146 | val.mark_as_input() 147 | val = np.repeat(val, 3) 148 | func.stop_recording() 149 | assert isinstance(val, AADCArray) 150 | assert np.all(val == np.array([1.0, 1.0, 1.0, 2.0, 2.0, 2.0, 3.0, 3.0, 3.0])) 151 | 152 | 153 | def test_reshape() -> None: 154 | func = Functions() 155 | func.start_recording() 156 | val = AADCArray([1.0, 2.0, 3.0, 4.0, 5.0, 6.0]) 157 | val.mark_as_input() 158 | val = np.reshape(val, (3, 2)) 159 | func.stop_recording() 160 | assert isinstance(val, AADCArray) 161 | assert np.all(val == np.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]])) 162 | 163 | 164 | def test_roll() -> None: 165 | func = Functions() 166 | func.start_recording() 167 | val = AADCArray([1.0, 2.0, 3.0]) 168 | val.mark_as_input() 169 | val = np.roll(val, 1) 170 | func.stop_recording() 171 | assert isinstance(val, AADCArray) 172 | assert np.all(val == np.array([3.0, 1.0, 2.0])) 173 | 174 | 175 | def test_squeeze() -> None: 176 | func = Functions() 177 | func.start_recording() 178 | val = AADCArray([[[1.0], [2.0], [3.0]]]) 179 | val.mark_as_input() 180 | val = np.squeeze(val) 181 | func.stop_recording() 182 | assert isinstance(val, AADCArray) 183 | assert np.all(val == np.array([1.0, 2.0, 3.0])) 184 | 185 | 186 | def test_stack() -> None: 187 | func = Functions() 188 | func.start_recording() 189 | val1 = AADCArray([1.0, 2.0, 3.0]) 190 | val2 = AADCArray([4.0, 5.0, 6.0]) 191 | val1.mark_as_input() 192 | val2.mark_as_input() 193 | val = np.stack((val1, val2)) 194 | func.stop_recording() 195 | assert isinstance(val, AADCArray) 196 | assert np.all(val == np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])) 197 | 198 | 199 | def test_tile() -> None: 200 | func = Functions() 201 | func.start_recording() 202 | val = AADCArray([1.0, 2.0, 3.0]) 203 | val.mark_as_input() 204 | val = np.tile(val, 2) 205 | func.stop_recording() 206 | assert isinstance(val, AADCArray) 207 | assert np.all(val == np.array([1.0, 2.0, 3.0, 1.0, 2.0, 3.0])) 208 | -------------------------------------------------------------------------------- /examples/solver.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import numpy as np 3 | from scipy.optimize import root_scalar 4 | 5 | import aadc 6 | 7 | # %% [markdown] 8 | # # Simple Chained Root Scalar Example 9 | # 10 | # This example demonstrates using two sequential root_scalar calls, where the 11 | # second call depends on the result of the first call. 12 | 13 | # %% [markdown] 14 | # ## 1. Using SciPy's root_scalar 15 | 16 | # %% 17 | # Define our first objective function 18 | def objective1(x, target=10.0): 19 | """Find where x^2 + 5 = target""" 20 | return x**2 + 5 - target 21 | 22 | # Solve for x where x^2 + 5 = 10 23 | result1 = root_scalar(objective1, x0=2.0, method='newton') 24 | x1 = result1.root 25 | 26 | print(f"First root (x₁): {x1:.6f}") 27 | print(f"Verification: {x1}² + 5 = {x1**2 + 5:.6f}") 28 | 29 | # Define our second objective function, which depends on the first result 30 | def objective2(x, prev_result=x1): 31 | """Find where x*prev_result - x^3 = 20""" 32 | return x*prev_result - x**3 - 20 33 | 34 | # Solve for x where x*x1 - x^3 = 20 35 | result2 = root_scalar(objective2, x0=2.0, method='newton') 36 | x2 = result2.root 37 | 38 | print(f"Second root (x₂): {x2:.6f}") 39 | print(f"Verification: {x2}*{x1} - {x2}³ = {x2*x1 - x2**3:.6f}") 40 | 41 | # %% [markdown] 42 | # ## 2. Using AADC's root_scalar with automatic differentiation 43 | 44 | # %% 45 | # Create an AADC kernel for recording the computation 46 | func = aadc.Kernel() 47 | func.start_recording() 48 | 49 | # Create an AADC input variable for the target value 50 | target = aadc.idouble(10.0) 51 | target_arg = target.mark_as_input() 52 | 53 | # Define the first objective function 54 | def aadc_objective1(x): 55 | """Find where x^2 + 5 = target""" 56 | return x**2 + 5 - target 57 | 58 | # Solve using AADC root_scalar 59 | aadc_result1 = aadc.root_scalar(aadc_objective1, x0=2.0) 60 | print(f"First AADC root: ~{aadc_result1}") 61 | 62 | # Define the second objective function using the first result 63 | def aadc_objective2(x): 64 | """Find where x*result1 - x^3 = 20""" 65 | return x*aadc_result1 - x**3 - 20 66 | 67 | # Solve the second equation 68 | aadc_result2 = aadc.root_scalar(aadc_objective2, x0=2.0) 69 | print(f"Second AADC root: ~{aadc_result2}") 70 | 71 | # Mark the results as outputs for later analysis 72 | result1_out = aadc_result1.mark_as_output() 73 | result2_out = aadc_result2.mark_as_output() 74 | 75 | # Stop recording 76 | func.stop_recording() 77 | 78 | # %% [markdown] 79 | # ## 3. Analysis of dependencies and sensitivities 80 | 81 | # %% 82 | # Set up inputs and requests for AADC evaluation 83 | inputs = {target_arg: 10.0} 84 | request = { 85 | result1_out: [target_arg], 86 | result2_out: [target_arg] 87 | } 88 | 89 | # Evaluate the kernel 90 | res = aadc.evaluate(func, request, inputs) 91 | 92 | # Extract results 93 | aadc_x1 = res[0][result1_out] 94 | aadc_x2 = res[0][result2_out] 95 | 96 | # Extract sensitivities 97 | sensitivity_x1_to_target = res[1][result1_out][target_arg][0] 98 | sensitivity_x2_to_target = res[1][result2_out][target_arg][0] 99 | 100 | print("\nAADC Results:") 101 | print(f"First root (x₁): {aadc_x1}") 102 | print(f"Second root (x₂): {aadc_x2}") 103 | 104 | print("\nComparison with SciPy results:") 105 | print(f"First root error: {abs(aadc_x1 - x1)}") 106 | print(f"Second root error: {abs(aadc_x2 - x2)}") 107 | 108 | print("\nSensitivity Analysis:") 109 | print(f"∂x₁/∂target = {sensitivity_x1_to_target}") 110 | print(f"∂x₂/∂target = {sensitivity_x2_to_target}") 111 | 112 | # %% [markdown] 113 | # ## 4. Verification with finite differences 114 | 115 | # %% 116 | # Function to compute both roots for a given target 117 | def compute_roots(target_val): 118 | # First root: x^2 + 5 = target 119 | def obj1(x): 120 | return x**2 + 5 - target_val 121 | 122 | res1 = root_scalar(obj1, x0=2.0, method='newton') 123 | x1_val = res1.root 124 | 125 | # Second root: x*x1 - x^3 = 20 126 | def obj2(x): 127 | return x*x1_val - x**3 - 20 128 | 129 | res2 = root_scalar(obj2, x0=2.0, method='newton') 130 | x2_val = res2.root 131 | 132 | return x1_val, x2_val 133 | 134 | # Compute finite difference approximations 135 | epsilon = 1e-6 136 | target_base = 10.0 137 | x1_base, x2_base = compute_roots(target_base) 138 | x1_bumped, x2_bumped = compute_roots(target_base + epsilon) 139 | 140 | fd_sensitivity_x1 = (x1_bumped - x1_base) / epsilon 141 | fd_sensitivity_x2 = (x2_bumped - x2_base) / epsilon 142 | 143 | print("\nFinite Difference Verification:") 144 | print(f"∂x₁/∂target (FD) = {fd_sensitivity_x1}") 145 | print(f"∂x₂/∂target (FD) = {fd_sensitivity_x2}") 146 | 147 | print("\nSensitivity Comparison:") 148 | print(f"First root sensitivity error: {abs(sensitivity_x1_to_target - fd_sensitivity_x1)}") 149 | print(f"Second root sensitivity error: {abs(sensitivity_x2_to_target - fd_sensitivity_x2)}") 150 | 151 | # %% [markdown] 152 | # ## 5. Visualizing the relationship 153 | 154 | # %% 155 | # Compute roots for a range of target values 156 | target_values = np.linspace(5.0, 15.0, 50) 157 | x1_values = [] 158 | x2_values = [] 159 | 160 | for t in target_values: 161 | x1, x2 = compute_roots(t) 162 | x1_values.append(x1) 163 | x2_values.append(x2) 164 | 165 | # Plot the relationships 166 | plt.figure(figsize=(12, 6)) 167 | 168 | # Compute AADC results for the same range of target values as SciPy 169 | aadc_targets = np.linspace(5.0, 15.0, 50) 170 | 171 | # Evaluate AADC kernel for all targets at once 172 | inputs = {target_arg: aadc_targets} 173 | res_all_targets = aadc.evaluate(func, request, inputs) 174 | 175 | # Extract results for all target values 176 | aadc_x1_values = res_all_targets[0][result1_out] 177 | aadc_x2_values = res_all_targets[0][result2_out] 178 | 179 | plt.subplot(1, 2, 1) 180 | plt.plot(target_values, x1_values, 'b-', linewidth=2, label='SciPy') 181 | plt.plot(aadc_targets, aadc_x1_values, 'r--', linewidth=2, label='AADC') 182 | plt.xlabel('Target Value') 183 | plt.ylabel('x₁ (First Root)') 184 | plt.title('First Root vs Target') 185 | plt.legend() 186 | plt.grid(True, alpha=0.3) 187 | 188 | plt.subplot(1, 2, 2) 189 | plt.plot(target_values, x2_values, 'b-', linewidth=2, label='SciPy') 190 | plt.plot(aadc_targets, aadc_x2_values, 'r--', linewidth=2, label='AADC') 191 | plt.xlabel('Target Value') 192 | plt.ylabel('x₂ (Second Root)') 193 | plt.title('Second Root vs Target') 194 | plt.legend() 195 | plt.grid(True, alpha=0.3) 196 | 197 | plt.tight_layout() 198 | plt.show() 199 | 200 | # %% [markdown] 201 | # ## Key Insights 202 | # 203 | # 1. **Chained Calculations**: The second root_scalar call depends directly on the result of the first call, 204 | # creating a chain of calculations. 205 | # 206 | # 2. **Hidden Parameters**: The objective function `objective2` has a hidden dependency on the result of 207 | # the first calculation (`x1`). AADC automatically tracks this dependency. 208 | # 209 | # 3. **Sensitivities**: We can compute how changes in the initial target value affect both the first and 210 | # second roots, even though the dependency chain is complex. 211 | # 212 | # 4. **Verification**: The sensitivities computed by AADC match those computed using finite differences, 213 | # confirming that AADC correctly tracks dependencies through multiple root_scalar calls. 214 | 215 | # %% [markdown] 216 | 217 | # # Automatic Implicit Function Theorem - Summary 218 | 219 | # ## Paper Details 220 | # - **Title**: Automatic Implicit Function Theorem 221 | # - **Authors**: Dmitri Goloubentsev, Evgeny Lakshtanov, Vladimir Piterbarg 222 | # - **Affiliation**: Matlogica, Universidade de Aveiro, NatWest Markets, Imperial College London 223 | # - **Published**: December 14, 2021 (Revised: May 31, 2022) 224 | # - **SSRN ID**: 3984964 225 | # - **URL**: [https://papers.ssrn.com/sol3/papers.cfm?abstract_id=3984964](https://papers.ssrn.com/sol3/papers.cfm?abstract_id=3984964) 226 | -------------------------------------------------------------------------------- /examples/sabr.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import numpy as np\n", 10 | "import aadc\n", 11 | "\n", 12 | "def sabr_normal_vol_atm(fwd, expiry, beta, sig0, rho, nu):\n", 13 | " F = fwd\n", 14 | " t = expiry\n", 15 | " t = np.where(abs(t) < 1e-10, 1e-10, t)\n", 16 | "\n", 17 | " c1 = 1 + ((2 - 3 * rho ** 2) / 24) * (nu ** 2) * t\n", 18 | " c2 = (rho * beta * nu * t) / (4 * F ** (1 - beta))\n", 19 | " c3 = beta * (beta - 2) * t / (24 * F ** (2 - 2 * beta))\n", 20 | "\n", 21 | " sig_n = (c1 * sig0 + c2 * sig0 ** 2 + c3 * sig0 ** 3) / (F ** (-beta))\n", 22 | "\n", 23 | " return sig_n\n", 24 | "\n", 25 | "\n", 26 | "def sabr_normal_vol_otm(fwd, strike, expiry, beta, sig0, rho, nu):\n", 27 | " F = fwd\n", 28 | " K = strike\n", 29 | " t = expiry\n", 30 | " t = np.where(abs(t) < 1e-10, 1e-10, t)\n", 31 | "\n", 32 | " k = K / F\n", 33 | " alpha = sig0 / (F ** (1 - beta))\n", 34 | "\n", 35 | " beta_close_to_one = np.isclose(beta, 1, 1e-10)\n", 36 | " q = np.where(beta_close_to_one, np.log(k), (k ** (1 - beta) - 1) / (1 - beta))\n", 37 | "\n", 38 | " z = q * nu / alpha\n", 39 | " z_close_to_zero = np.isclose(z, 0, 1e-10)\n", 40 | " z = np.where(z_close_to_zero, np.nan, z)\n", 41 | "\n", 42 | " _H = z / np.log((np.sqrt(1 + 2 * rho * z + z ** 2) + z + rho) / (1 + rho))\n", 43 | "\n", 44 | " H = np.where(z_close_to_zero, 1, _H)\n", 45 | "\n", 46 | " _B = np.log((q * k ** (beta / 2)) / (k - 1)) * (alpha ** 2) / (q ** 2)\n", 47 | " _B += (rho / 4) * ((k ** beta - 1) / (k - 1)) * alpha * nu\n", 48 | " _B += ((2 - 3 * rho ** 2) / 24) * (nu ** 2)\n", 49 | "\n", 50 | " B = ((k - 1) / q) * (1 + _B * t)\n", 51 | "\n", 52 | " sig_n = sig0 * (F ** beta) * H * B\n", 53 | "\n", 54 | " return sig_n\n", 55 | "\n", 56 | "\n", 57 | "def sabr_normal_vol(fwd, strike, expiry, beta, sig0, rho, nu):\n", 58 | " F, K, expiry, beta, sig0, rho, nu = np.broadcast_arrays(fwd, strike, expiry, beta, sig0, rho, nu)\n", 59 | "\n", 60 | " return np.where(np.isclose(F, K, 1e-6),\n", 61 | " sabr_normal_vol_atm(F, expiry, beta, sig0, rho, nu),\n", 62 | " sabr_normal_vol_otm(F, K, expiry, beta, sig0, rho, nu))\n" 63 | ] 64 | }, 65 | { 66 | "cell_type": "code", 67 | "execution_count": null, 68 | "metadata": {}, 69 | "outputs": [], 70 | "source": [ 71 | "ncalibrations = 1000\n", 72 | "\n", 73 | "# Generate market data\n", 74 | "fwd = np.linspace(0.01, 0.05, ncalibrations)\n", 75 | "beta = np.tile([0.5, 0.6, 0.7, 0.8, 0.9], -(-ncalibrations // 5))[:ncalibrations]\n", 76 | "expiry = np.tile(np.linspace(1, 10, 10), -(-ncalibrations // 10))[:ncalibrations]\n", 77 | "\n", 78 | "np.random.seed(42)\n", 79 | "sig0 = np.random.uniform(0.01, 0.05, ncalibrations)\n", 80 | "rho = np.random.uniform(-0.5, 0., ncalibrations)\n", 81 | "nu = np.random.uniform(0.2, 0.6, ncalibrations)\n", 82 | "\n", 83 | "# 7 strikes, each row is an independent calibration scenario\n", 84 | "strikes = np.linspace(0.25, 1.75, 7).reshape(-1, 1) * fwd\n", 85 | "vols = sabr_normal_vol(fwd, strikes, expiry, beta, sig0, rho, nu)\n", 86 | "strikes,vols" 87 | ] 88 | }, 89 | { 90 | "cell_type": "code", 91 | "execution_count": null, 92 | "metadata": {}, 93 | "outputs": [], 94 | "source": [ 95 | "from scipy.optimize import least_squares\n", 96 | "vol_weights = [1., 1., 1., 100., 1., 1., 1.]\n", 97 | "\n", 98 | "def residual(x, fwd, beta, expiry, strikes, vols):\n", 99 | " sig0, rho, nu = x\n", 100 | " rho = np.broadcast_to(rho, vols.shape)\n", 101 | "\n", 102 | " return np.where(np.abs(rho) > 0.9999,\n", 103 | " np.ones_like(vols) * 1e6,\n", 104 | " vol_weights * (sabr_normal_vol(fwd, strikes, expiry, beta, sig0, rho, nu) - vols))\n", 105 | "\n", 106 | "x0 = np.array([0.02, -0.25, 0.03])\n", 107 | "\n", 108 | "def sabr_normal_smile_fit(scen):\n", 109 | " results = least_squares(\n", 110 | " residual,\n", 111 | " x0,\n", 112 | " args=(fwd[scen], beta[scen], expiry[scen], strikes[:, scen], vols[:, scen]),\n", 113 | " method=\"lm\",\n", 114 | " xtol=1e-6)\n", 115 | "\n", 116 | " return results.x" 117 | ] 118 | }, 119 | { 120 | "cell_type": "markdown", 121 | "metadata": {}, 122 | "source": [ 123 | "Solve for sig0, rho, nu for each scenario:" 124 | ] 125 | }, 126 | { 127 | "cell_type": "code", 128 | "execution_count": null, 129 | "metadata": {}, 130 | "outputs": [], 131 | "source": [ 132 | "sig0_bar, rho_bar, nu_bar = np.transpose([sabr_normal_smile_fit(j) for j in range(ncalibrations)])\n", 133 | "\n", 134 | "rho_bar, nu_bar = np.where(nu_bar < 0, -rho_bar, rho_bar), np.abs(nu_bar)" 135 | ] 136 | }, 137 | { 138 | "cell_type": "code", 139 | "execution_count": null, 140 | "metadata": {}, 141 | "outputs": [], 142 | "source": [ 143 | "assert np.allclose(sig0_bar, sig0, atol=1e-10)\n", 144 | "assert np.allclose(rho_bar, rho, atol=1e-10)\n", 145 | "assert np.allclose(nu_bar, nu, atol=1e-10)\n", 146 | "# sig0_bar - sig0, rho_bar - rho, nu_bar - nu\n", 147 | "# np.argmax(np.abs(sig0_bar - sig0)), np.argmax(np.abs(rho_bar - rho)), np.argmax(np.abs(nu_bar - nu))\n" 148 | ] 149 | }, 150 | { 151 | "cell_type": "markdown", 152 | "metadata": {}, 153 | "source": [ 154 | "Now AADC the whole thing" 155 | ] 156 | }, 157 | { 158 | "cell_type": "code", 159 | "execution_count": null, 160 | "metadata": {}, 161 | "outputs": [], 162 | "source": [ 163 | "kernel = aadc.record(residual, x0, params=(fwd[0], beta[0], expiry[0], strikes[:, 0], vols[:, 0]), bump_size=1e-10)" 164 | ] 165 | }, 166 | { 167 | "cell_type": "code", 168 | "execution_count": null, 169 | "metadata": {}, 170 | "outputs": [], 171 | "source": [ 172 | "def sabr_normal_smile_fit_aadc(scen):\n", 173 | " kernel.set_params(fwd[scen], beta[scen], expiry[scen], strikes[:, scen], vols[:, scen])\n", 174 | " results = least_squares(\n", 175 | " kernel.func,\n", 176 | " x0,\n", 177 | " jac=kernel.jac,\n", 178 | " method=\"lm\",\n", 179 | " xtol=1e-6)\n", 180 | "\n", 181 | " return results.x" 182 | ] 183 | }, 184 | { 185 | "cell_type": "markdown", 186 | "metadata": {}, 187 | "source": [ 188 | "Moment of truth:" 189 | ] 190 | }, 191 | { 192 | "cell_type": "code", 193 | "execution_count": null, 194 | "metadata": {}, 195 | "outputs": [], 196 | "source": [ 197 | "sig0_star, rho_star, nu_star = np.transpose([sabr_normal_smile_fit_aadc(j) for j in range(ncalibrations)])\n", 198 | "rho_star, nu_star = np.where(nu_star < 0, -rho_star, rho_star), np.abs(nu_star)\n", 199 | "\n", 200 | "assert np.allclose(sig0_bar, sig0_star, atol=1e-10)\n", 201 | "assert np.allclose(rho_bar, rho_star, atol=1e-10)\n", 202 | "assert np.allclose(nu_bar, nu_star, atol=1e-10)\n", 203 | "\n", 204 | "# sig0_star - sig0_bar, rho_star - rho_bar, nu_star - nu_bar\n", 205 | "# np.argmax(np.abs(sig0_star - sig0_bar)), np.argmax(np.abs(rho_star - rho_bar)), np.argmax(np.abs(nu_star - nu_bar))\n", 206 | "\n" 207 | ] 208 | } 209 | ], 210 | "metadata": { 211 | "kernelspec": { 212 | "display_name": "Python 3", 213 | "language": "python", 214 | "name": "python3" 215 | }, 216 | "language_info": { 217 | "codemirror_mode": { 218 | "name": "ipython", 219 | "version": 3 220 | }, 221 | "file_extension": ".py", 222 | "mimetype": "text/x-python", 223 | "name": "python", 224 | "nbconvert_exporter": "python", 225 | "pygments_lexer": "ipython3", 226 | "version": "3.11.8" 227 | } 228 | }, 229 | "nbformat": 4, 230 | "nbformat_minor": 2 231 | } 232 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AADC-Python 2 | 3 | ## Introduction 4 | 5 | This library provides high-performance components leveraging the hardware acceleration support and automatic differentiation. It uses MatLogica's specialised run-time compiler, AADC (AAD Compiler) to generate efficient binary kernels for execution of the original and the adjoint functions on the fly. 6 | 7 | The solution has two main aspects (that can be used separately): 8 | 9 | Accelerating simulations, such as Monte-Carlo simulations, historical analysis, and "what if" scenario analysis, bump & revalue, etc. 10 | Automatic Differentiation: AADC can speed up the AAD method itself, and deliver pricing and scenario analysis simply and effectively, in a way that is unattainable with competing products. 11 | AADC uses Code Generation AAD™ approach that combines Code Transformation and Operator Overloading to efficiently extract the valuation graph and, at runtime, generate efficient binary kernels replicating the original program, and where required, it's adjoint. AADC utilizes native CPU vectorization and multi-threading, delivering performance comparable to a GPU. This results in faster computing of the model and its first and higher-order derivatives. The approach is particularly useful for models with many parameters that require frequent gradient updates during training. 12 | 13 | The performance of the runtime graph compilation is crucial, because it's now part of the overall model execution. This is why a specialised graph compiler needs to be used - any off-the-shelf compiler would introduce substantial delay, making the approach not practically viable. This is where TensorFlow for finance project failed. 14 | 15 | Please join our discord: https://discord.gg/YqYDfWj6 16 | 17 | ## Use Cases 18 | 19 | The solution can be used for greenfield or existing projects. It allows developers to focus on modeling, rather than performance optimisations, greatly improving time-to-market for new features, simplifying IT architecture and infrastructure. 20 | 21 | ## In Finance 22 | 23 | The solution is used for speeding up and computing derivatives for various financial models, including pricing exotic derivatives, payoff languages, curve building, XVA, EPE, Loss-given-default, and others. 24 | 25 | It enables transitioning to Live Risk from Batch processing by applying the Automated Implicit Function Theorem. 26 | 27 | Stress-Testing, Back-Testing, What-if analysis, VaR can be accelerated with the solution. 28 | 29 | ## Neural Networks 30 | 31 | The solution can be used to develop new Neural Network Architectures. Refer to research: 32 | 33 | https://arxiv.org/abs/2207.03577 34 | 35 | 36 | 37 | ## Other applications 38 | 39 | Life science, physics, drug research, disease diagnosis (https://elib.uni-stuttgart.de/bitstream/11682/13787/7/PhD_Thesis_Ivan.pdf) benefit from simplifying development, automatic differentiation and improving performance of simulations. 40 | 41 | * Topology Optimization 101: How to Use Algorithmic Models to Create Lightweight Design: https://formlabs.com/blog/topology-optimization/ 42 | 43 | * AuTO: a framework for Automatic differentiation in Topology Optimization: https://link.springer.com/article/10.1007/s00158-021-03025-8 44 | 45 | * A set of examples that use AD for several purposes with simulation: https://www.dolfin-adjoint.org/en/stable/documentation/examples.html. 46 | 47 | 48 | ## Package contents 49 | 50 | The package includes 2 projects: basic examples and QuantLib(https://www.quantlib.org/) examples. 51 | 52 | Please refer to Manual.pdf on the functionality and uses. 53 | 54 | ## Installation and API reference 55 | 56 | To install the `aadc` package, use pip: 57 | 58 | ```sh 59 | pip install aadc 60 | ``` 61 | 62 | ## Usage 63 | 64 | You can use most of your existing code without any modifications. Only the inputs to the evaluation graph you want to record using AADC have to be explicitly initialized as active floating point types. 65 | 66 | A stand-alone `aadc.idboule` type stores a single active double, while `aadc.array()` function returns a multi-dimensional `AADCArray` of `idboule`s and can be used as a drop-in replacement of NumPy's `np.array()`. 67 | 68 | --- 69 | 70 | ### `evaluate` 71 | 72 | ```python 73 | aadc.evaluate(funcs: Kernel, request: dict, inputs: dict, workers: ThreadPool) -> list 74 | ``` 75 | 76 | **Description:** 77 | The `evaluate` function executes the recorded kernel `funcs` for multiple inputs, and returns the evaluated results and sensitivities according to `request`. 78 | 79 | **Parameters:** 80 | - `funcs` (Kernel): The recorded kernel object. 81 | - `request` (dict): A dictionary specifying the outputs and the gradients required. The key (`AADCResult`) is the workspace index of an output (see below), and the value is a list of `AADCArgument` - indices of inputs whose adjoints are requested, 82 | - `inputs` (dict): A dictionary of inputs for the evaluation. The key (`AADCArgument`) identifies the input node, and the value is an `np.array` of all scenarios for this input. 83 | - `workers` (ThreadPool): A thread pool to manage parallel execution. 84 | 85 | **Returns:** 86 | - `list`: [{ 87 | outputArg -> outputValues 88 | }, 89 | { 90 | outputArg -> { 91 | inputArg -> adjoints 92 | } 93 | }] 94 | 95 | **Example:** 96 | 97 | ```python 98 | results = aadc.evaluate(funcs, request, inputs, workers) 99 | ``` 100 | 101 | --- 102 | 103 | ## Classes and Methods 104 | 105 | ### `Kernel` 106 | 107 | ```python 108 | class aadc.Kernel: 109 | def start_recording(self) -> None 110 | def stop_recording(self) -> None 111 | ``` 112 | 113 | **Description:** 114 | The `Kernel` class provides methods to start and stop recording the calculation graph and stores the JIT-compiled AADC machine code kernels for forward and reverse (adjoint) passes 115 | 116 | **Methods:** 117 | 118 | #### `start_recording` 119 | 120 | ```python 121 | def start_recording() -> None 122 | ``` 123 | 124 | **Description:** 125 | Starts recording. 126 | 127 | **Returns:** 128 | - `None` 129 | 130 | **Example:** 131 | 132 | ```python 133 | funcs = aadc.Kernel() 134 | funcs.start_recording() 135 | ``` 136 | 137 | #### `stop_recording` 138 | 139 | ```python 140 | def stop_recording() -> None 141 | ``` 142 | 143 | **Description:** 144 | Stops recording the operations. 145 | 146 | **Returns:** 147 | - `None` 148 | 149 | **Example:** 150 | 151 | ```python 152 | funcs.stop_recording() 153 | ``` 154 | 155 | --- 156 | 157 | ### `ThreadPool` 158 | 159 | ```python 160 | class aadc.ThreadPool: 161 | def __init__(self, num_threads: int) 162 | ``` 163 | 164 | **Description:** 165 | The `ThreadPool` class manages a pool of threads for parallel execution. 166 | 167 | **Parameters:** 168 | - `num_threads` (int): The number of threads to include in the pool. 169 | 170 | **Example:** 171 | 172 | ```python 173 | workers = aadc.ThreadPool(4) 174 | ``` 175 | 176 | --- 177 | 178 | ## Methods 179 | 180 | ### `mark_as_input` 181 | 182 | ```python 183 | def mark_as_input() -> AADCArgument 184 | ``` 185 | 186 | **Description:** 187 | Marks the variable as an input of the computation being recorded. Both `idouble` and `AADCArray` implement this method 188 | 189 | **Returns:** 190 | - `AADCArgument`: An index into the kernel workspace where the input is stored before the forward pass / adjoints can be read after the reverse pass; or a list of `AADCArgument` in the array case. 191 | 192 | **Example:** 193 | 194 | ```python 195 | stock_arg = stock_price.mark_as_input() 196 | ``` 197 | 198 | ### `mark_as_output` 199 | 200 | ```python 201 | def mark_as_output() -> AADCResult 202 | ``` 203 | 204 | **Description:** 205 | Marks the variable as an output of the computation being recorded. 206 | 207 | **Returns:** 208 | - `AADCResult`: An index into the kernel workspace where the output can be read after the forward pass / adjoint should be set to 1.0 before the reverse pass 209 | 210 | **Example:** 211 | 212 | ```python 213 | price_res = price.mark_as_output() 214 | ``` 215 | 216 | --- 217 | 218 | ### record 219 | 220 | ```python 221 | aadc.record(computation: Callable, x0: NDArray, params: tuple, bump_size: float) -> CurreidRecordedFunction 222 | ``` 223 | 224 | **Description:** 225 | Given `computation` - a function `f(x, *params)` `record` records the computation passing `x0` and `params` 226 | 227 | **Returns:** 228 | - `CurreidRecordedFunction`: A structure with 3 class methods 229 | - `func`: the JIT-compiled version of `computation` closed over `params` with the signature `f(x)` 230 | - `jac`: a function returning `computation`s Jacobian w.r.t. `x`, calculated using finite differences with `bump_size` 231 | - `set_params`: use this method to change the `params` in `func`s closure 232 | 233 | **Example:** 234 | 235 | ```python 236 | rec = aadc.record(f, np.zeros(shape), params=[0.0, 0.0], bump_size=1e-10) 237 | x = np.ones(shape) 238 | rec.set_params(a) 239 | 240 | assert np.allclose(rec.func(x), f(x, a)) 241 | ``` 242 | -------------------------------------------------------------------------------- /examples/01-PyAADCBasic.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "id": "4d2988b1-4dea-4032-b2c6-864d1f35c67a", 7 | "metadata": {}, 8 | "outputs": [ 9 | { 10 | "name": "stdout", 11 | "output_type": "stream", 12 | "text": [ 13 | "bind_swig2pybind_linearalgebra_i\n", 14 | "Adding extend interface from SWIG to Pybind11 for class FixedLocalVolSurface\n", 15 | "Adding extend interface from SWIG to Pybind11 for class GridModelLocalVolSurface\n" 16 | ] 17 | } 18 | ], 19 | "source": [ 20 | "# AADC basic \"Hello World\" example\n", 21 | "# This example demonstrates how to use the AADC library to record a simple function and calculate its derivatives\n", 22 | "import aadc\n", 23 | "import numpy as np\n", 24 | "import math" 25 | ] 26 | }, 27 | { 28 | "cell_type": "code", 29 | "execution_count": 2, 30 | "id": "965df2e1-ce4a-411f-be40-8bc644db21b2", 31 | "metadata": {}, 32 | "outputs": [], 33 | "source": [ 34 | "# Override defalt math.exp with aadc.math.exp to hand active types\n", 35 | "math.exp = aadc.math.exp" 36 | ] 37 | }, 38 | { 39 | "cell_type": "code", 40 | "execution_count": 3, 41 | "id": "40bbfc28-b257-4efe-b2f5-a8f77c911eba", 42 | "metadata": {}, 43 | "outputs": [], 44 | "source": [ 45 | "# Simple function to record\n", 46 | "# The function can take arbitrary input types and \n", 47 | "# can be defined in external file\n", 48 | "def F(x,y,z):\n", 49 | " return math.exp(x / y + z)" 50 | ] 51 | }, 52 | { 53 | "cell_type": "code", 54 | "execution_count": 4, 55 | "id": "f3b87119-b2a3-4dfb-b9cf-eebd4060d569", 56 | "metadata": {}, 57 | "outputs": [], 58 | "source": [ 59 | "# Object to hold recorded AADC Kernels that\n", 60 | "# allow to speed up the calculation of the function itself\n", 61 | "# and AAD for its derivatives\n", 62 | "funcs = aadc.Functions()" 63 | ] 64 | }, 65 | { 66 | "cell_type": "code", 67 | "execution_count": 5, 68 | "id": "5d39ca34-fb9b-40ed-b944-1cda7ff7f141", 69 | "metadata": {}, 70 | "outputs": [], 71 | "source": [ 72 | "# Define function arguments.\n", 73 | "# Note you need to use aadc.idouble instead of float\n", 74 | "x = aadc.idouble(1.0)\n", 75 | "y = aadc.idouble(2.0)\n", 76 | "z = aadc.idouble(3.0)" 77 | ] 78 | }, 79 | { 80 | "cell_type": "code", 81 | "execution_count": 6, 82 | "id": "3300d637-4725-4405-b9d5-d7eadd8cc8bc", 83 | "metadata": {}, 84 | "outputs": [ 85 | { 86 | "name": "stdout", 87 | "output_type": "stream", 88 | "text": [ 89 | "You are using evaluation version of AADC. Expire date is 20240901\n" 90 | ] 91 | } 92 | ], 93 | "source": [ 94 | "# Trigger recording of the function\n", 95 | "funcs.start_recording()" 96 | ] 97 | }, 98 | { 99 | "cell_type": "code", 100 | "execution_count": 7, 101 | "id": "d6cd2d27-abb4-4231-84e5-8865eabba328", 102 | "metadata": {}, 103 | "outputs": [], 104 | "source": [ 105 | "# Mark the function arguments as input and save reference argument ids\n", 106 | "xArg = x.mark_as_input()\n", 107 | "yArg = y.mark_as_input()\n", 108 | "zArg = z.mark_as_input()" 109 | ] 110 | }, 111 | { 112 | "cell_type": "code", 113 | "execution_count": 8, 114 | "id": "a6a70fbe-8cc7-4868-ae04-2a7121be6355", 115 | "metadata": {}, 116 | "outputs": [], 117 | "source": [ 118 | "# Call the function and add some operations\n", 119 | "f = F(x,y,z) + x" 120 | ] 121 | }, 122 | { 123 | "cell_type": "code", 124 | "execution_count": 9, 125 | "id": "3a709740-8437-4dd3-b1ae-50a6f6fccec5", 126 | "metadata": {}, 127 | "outputs": [], 128 | "source": [ 129 | "# Mark the result as output\n", 130 | "fRes = f.mark_as_output()" 131 | ] 132 | }, 133 | { 134 | "cell_type": "code", 135 | "execution_count": 10, 136 | "id": "9020ad7b-0b31-4615-ac01-fdaca21f709a", 137 | "metadata": {}, 138 | "outputs": [], 139 | "source": [ 140 | "# Stop recording\n", 141 | "# After this step AADC Kernel containing native machine CPU code\n", 142 | "# will be generated and stored in the funcs object\n", 143 | "funcs.stop_recording()" 144 | ] 145 | }, 146 | { 147 | "cell_type": "code", 148 | "execution_count": 11, 149 | "id": "07f8caa2-2d3e-4acf-adc2-42a96f90fc08", 150 | "metadata": {}, 151 | "outputs": [ 152 | { 153 | "name": "stdout", 154 | "output_type": "stream", 155 | "text": [ 156 | "Number active to passive conversions: 0 while recording Python\n" 157 | ] 158 | } 159 | ], 160 | "source": [ 161 | "# Check if the function is recorded properly\n", 162 | "# and can be used for arbitrary input values\n", 163 | "# This should return 0 if everything is OK, indicating\n", 164 | "# that no branches in the function are not supported\n", 165 | "funcs.print_passive_extract_locations()" 166 | ] 167 | }, 168 | { 169 | "cell_type": "code", 170 | "execution_count": 12, 171 | "id": "56f45689-ef0c-4f60-87d4-2f31030670bd", 172 | "metadata": {}, 173 | "outputs": [], 174 | "source": [ 175 | "# New samples of input values to calculate the function and its derivatives at\n", 176 | "# Note that the x input is a vector of 20 samples\n", 177 | "# and the y and z are scalars\n", 178 | "inputs = {\n", 179 | " xArg:(1.0 * np.random.normal(1, 0.2, 20)),\n", 180 | " yArg:(2.0),\n", 181 | " zArg:(3.0),\n", 182 | "}" 183 | ] 184 | }, 185 | { 186 | "cell_type": "code", 187 | "execution_count": 13, 188 | "id": "a4fe2659-9ea2-49ba-ab97-c1282ae30e9c", 189 | "metadata": {}, 190 | "outputs": [], 191 | "source": [ 192 | "# Key: what output, value: what gradients are needed\n", 193 | "\n", 194 | "request = {fRes:[xArg,yArg,zArg]} " 195 | ] 196 | }, 197 | { 198 | "cell_type": "code", 199 | "execution_count": 14, 200 | "id": "e7afc718-1ba6-4bd5-a96d-a05d88802c8f", 201 | "metadata": {}, 202 | "outputs": [], 203 | "source": [ 204 | "# Run AADC Kernel for array inputs using 4 CPU threads and avx2\n", 205 | "Res = aadc.evaluate(funcs, request, inputs, aadc.ThreadPool(4))" 206 | ] 207 | }, 208 | { 209 | "cell_type": "code", 210 | "execution_count": 15, 211 | "id": "00ef0d56-99f7-4bb1-b38c-72c410b99123", 212 | "metadata": {}, 213 | "outputs": [ 214 | { 215 | "data": { 216 | "text/plain": [ 217 | "array([33.24205433, 34.6915409 , 37.52129798, 37.69674374, 32.36243554,\n", 218 | " 35.50886523, 44.42939322, 32.24723311, 39.13724023, 31.05824264,\n", 219 | " 37.46866464, 31.34508762, 29.13052269, 34.96900331, 34.22029599,\n", 220 | " 34.67305414, 35.39841509, 43.32715 , 33.4486223 , 32.64016329])" 221 | ] 222 | }, 223 | "execution_count": 15, 224 | "metadata": {}, 225 | "output_type": "execute_result" 226 | } 227 | ], 228 | "source": [ 229 | "# Function output\n", 230 | "Res[0][fRes]" 231 | ] 232 | }, 233 | { 234 | "cell_type": "code", 235 | "execution_count": 16, 236 | "id": "dafa8ec7-cd5c-40a5-b8e6-25c32e398907", 237 | "metadata": {}, 238 | "outputs": [ 239 | { 240 | "name": "stdout", 241 | "output_type": "stream", 242 | "text": [ 243 | "df/dx\n" 244 | ] 245 | }, 246 | { 247 | "data": { 248 | "text/plain": [ 249 | "array([17.14619553, 17.82949053, 19.16784835, 19.25100457, 16.7323514 ,\n", 250 | " 18.21547665, 22.45556536, 16.67819827, 19.93448792, 16.11995851,\n", 251 | " 19.14290554, 16.25452073, 15.21763133, 17.96046918, 17.60716649,\n", 252 | " 17.82076571, 18.1632877 , 21.92927875, 17.24347281, 16.86294837])" 253 | ] 254 | }, 255 | "execution_count": 16, 256 | "metadata": {}, 257 | "output_type": "execute_result" 258 | } 259 | ], 260 | "source": [ 261 | "print(\"df/dx\")\n", 262 | "Res[1][fRes][xArg]" 263 | ] 264 | }, 265 | { 266 | "cell_type": "code", 267 | "execution_count": 18, 268 | "id": "f84ae8e2-3d5b-4ecf-9a3e-f42151cab1aa", 269 | "metadata": {}, 270 | "outputs": [ 271 | { 272 | "name": "stdout", 273 | "output_type": "stream", 274 | "text": [ 275 | "df/dy\n", 276 | "[ -7.66672437 -8.68872797 -10.7699121 -10.90255339 -7.06172349\n", 277 | " -9.27838383 -16.28759015 -6.98335615 -12.00696843 -6.18652467\n", 278 | " -10.7302003 -6.37674175 -4.94247543 -8.88783664 -8.3530975\n", 279 | " -8.67550096 -9.19814652 -15.36829103 -7.81048443 -7.2514816 ]\n" 280 | ] 281 | } 282 | ], 283 | "source": [ 284 | "print(\"df/dy\")\n", 285 | "print(Res[1][fRes][yArg])\n" 286 | ] 287 | } 288 | ], 289 | "metadata": { 290 | "kernelspec": { 291 | "display_name": "Python 3 (ipykernel)", 292 | "language": "python", 293 | "name": "python3" 294 | }, 295 | "language_info": { 296 | "codemirror_mode": { 297 | "name": "ipython", 298 | "version": 3 299 | }, 300 | "file_extension": ".py", 301 | "mimetype": "text/x-python", 302 | "name": "python", 303 | "nbconvert_exporter": "python", 304 | "pygments_lexer": "ipython3", 305 | "version": "3.11.2" 306 | } 307 | }, 308 | "nbformat": 4, 309 | "nbformat_minor": 5 310 | } 311 | -------------------------------------------------------------------------------- /examples/opt.py: -------------------------------------------------------------------------------- 1 | # %% [markdown] 2 | # # Levenberg-Marquardt Optimization with AADC Derivatives 3 | # This notebook demonstrates using Levenberg-Marquardt optimization with both SciPy and the 4 | # AADC implementation. 5 | 6 | # %% [markdown] 7 | # ## 1. Standard Implementation with SciPy 8 | # First, let's import the necessary libraries: 9 | 10 | # %% 11 | import matplotlib.pyplot as plt 12 | import numpy as np 13 | from scipy.optimize import least_squares 14 | 15 | import aadc 16 | 17 | # %matplotlib inline 18 | plt.ioff() # Turn off interactive mode 19 | 20 | # %% [markdown] 21 | # Now we'll define our residual function: 22 | 23 | # %% 24 | # Define the function to be minimized (residuals) 25 | def residual_func(params, x_data, y_data): 26 | # Example: fitting y = a * exp(-b * x) + c 27 | a, b, c = params 28 | y_model = a * np.exp(-b * x_data) + c 29 | return y_model - y_data 30 | 31 | # %% [markdown] 32 | # Let's generate some sample data for our optimization problem: 33 | 34 | # %% 35 | # Generate some sample data 36 | np.random.seed(42) # For reproducibility 37 | x_data = np.linspace(0, 10, 10) 38 | true_params = [5.0, 0.5, 1.0] 39 | y_true = true_params[0] * np.exp(-true_params[1] * x_data) + true_params[2] 40 | noise = np.random.normal(0, 0.2, x_data.shape) 41 | y_data = y_true + noise 42 | print("x_data:", x_data) 43 | print("y_data:", y_data) 44 | 45 | # %% [markdown] 46 | # Now we'll create an optimization function: 47 | 48 | # %% 49 | def run_optimization(x_data, y_data): 50 | # Initial guess for parameters 51 | initial_params = [1.0, 1.0, 0.0] 52 | 53 | # Perform Levenberg-Marquardt optimization 54 | result = least_squares( 55 | residual_func, 56 | initial_params, 57 | method='lm', 58 | args=(x_data, y_data), 59 | verbose=0 60 | ) 61 | 62 | return result, initial_params 63 | 64 | # Run the optimization 65 | result, initial_params = run_optimization(x_data, y_data) 66 | 67 | # Print results 68 | print("True parameters:", true_params) 69 | print("Optimal parameters:", result.x) 70 | print("Cost:", result.cost) 71 | print("Success:", result.success) 72 | 73 | # Plot the optimization results for SciPy implementation 74 | plt.figure(figsize=(10, 6)) 75 | 76 | # Plot the noisy data points 77 | plt.scatter(x_data, y_data, color='blue', alpha=0.6, label='Noisy data') 78 | 79 | # Plot the true function 80 | plt.plot(x_data, y_true, 'g-', linewidth=2, label='True function') 81 | 82 | # Plot the initial guess 83 | initial_guess = initial_params[0] * np.exp(-initial_params[1] * x_data) + initial_params[2] 84 | plt.plot(x_data, initial_guess, 'r--', linewidth=2, label='Initial guess') 85 | 86 | # Plot the optimized function 87 | a_opt, b_opt, c_opt = result.x 88 | y_opt = a_opt * np.exp(-b_opt * x_data) + c_opt 89 | plt.plot(x_data, y_opt, 'k-', linewidth=2, label='Optimized fit') 90 | 91 | # Add labels and legend 92 | plt.xlabel('x') 93 | plt.ylabel('y') 94 | plt.title('SciPy Levenberg-Marquardt Optimization Results') 95 | plt.legend() 96 | plt.grid(True, alpha=0.3) 97 | 98 | # Add text box with optimized parameters 99 | param_text = f'Optimized parameters:\na = {a_opt:.4f}\nb = {b_opt:.4f}\nc = {c_opt:.4f}' 100 | plt.text(6, 4, param_text, bbox=dict(facecolor='white', alpha=0.8)) 101 | plt.tight_layout() 102 | plt.show() 103 | 104 | # %% [markdown] 105 | # ## 2. Implementation with AADC 106 | # Note that objective function is using y_data internally and derivatives must be calculated with 107 | # respect to y_data. AADC.least_squares() function doesn't require Jacobian w.r.t. y_data, it will 108 | # be calculated automatically. See Automatic IFT publication for more details. 109 | 110 | # %% 111 | # AADC version 112 | 113 | func = aadc.Kernel() 114 | func.start_recording() 115 | 116 | # Define the residual function with AADC inputs 117 | iy_data = aadc.ndarray.AADCArray([aadc.idouble(y) for y in y_data]) 118 | iy_data_arg = [y.mark_as_input() for y in iy_data] 119 | 120 | def objective(params): 121 | return residual_func(params, x_data, iy_data) 122 | 123 | aadc_result = aadc.least_squares(objective, initial_params) 124 | print("AADC Optimal parameters:", aadc_result.x) 125 | 126 | x_res = [a.mark_as_output() for a in aadc_result.x] 127 | func.stop_recording() 128 | func.print_passive_extract_locations() 129 | 130 | inputs = {y_arg: y for y_arg, y in zip(iy_data_arg, y_data)} 131 | request = {x_res[i]: iy_data_arg for i in range(len(x_res))} 132 | res = aadc.evaluate(func, request, inputs) 133 | 134 | print("True parameters:", true_params) 135 | print("Scipy Optimal parameters:", result.x) 136 | print("AADC Kernel Optimal parameters:", [res[0][x_res[i]] for i in range(len(x_res))]) 137 | 138 | # %% [markdown] 139 | # ## 3. Comparing Derivatives 140 | # Let's compare the derivatives computed by both methods: 141 | 142 | # %% 143 | print("AADC Kernel Derivatives param 0:", res[1][x_res[0]]) 144 | print("AADC Kernel Derivatives param 1:", res[1][x_res[1]]) 145 | print("AADC Kernel Derivatives param 2:", res[1][x_res[2]]) 146 | 147 | # %% [markdown] 148 | # Now let's verify the derivatives using the finite difference method: 149 | 150 | # %% 151 | # Define the function to calculate derivatives using finite differences 152 | def calculate_finite_diff_derivatives(params, x_data, y_data, epsilon=1e-6): 153 | derivatives = [] 154 | 155 | for i in range(len(params)): 156 | param_derivatives = [] 157 | 158 | for j in range(len(y_data)): 159 | # Create modified y_data with a bump in one element 160 | y_bumped = y_data.copy() 161 | y_bumped[j] += epsilon 162 | 163 | # Run optimization with bumped data 164 | bumped_result, _ = run_optimization(x_data, y_bumped) 165 | 166 | # Calculate derivative (change in parameter / change in y) 167 | derivative = (bumped_result.x[i] - params[i]) / epsilon 168 | param_derivatives.append(derivative) 169 | 170 | derivatives.append(param_derivatives) 171 | 172 | return derivatives 173 | 174 | # Calculate finite difference derivatives for all parameters 175 | # Use %%capture to suppress the output 176 | print("Calculating finite difference derivatives (this may take some time)...") 177 | fd_derivatives = calculate_finite_diff_derivatives(result.x, x_data, y_data) 178 | 179 | # Extract AADC derivatives in a format suitable for comparison 180 | aadc_derivatives = [] 181 | for i in range(len(result.x)): 182 | # Extract derivatives from the AADC result 183 | aadc_deriv_i = np.array([res[1][x_res[i]][y_arg][0] for y_arg in iy_data_arg]) 184 | aadc_derivatives.append(aadc_deriv_i) 185 | 186 | print(f"Parameter {i} derivatives:") 187 | print(f" AADC: {aadc_deriv_i}") 188 | print(f" Finite diff: {fd_derivatives[i]}") 189 | print(f" Mean difference: {np.mean(np.abs(aadc_deriv_i - np.array(fd_derivatives[i])))}") 190 | print() 191 | 192 | # %% [markdown] 193 | # ## 4. Combined Visualization 194 | # Let's visualize both the optimization results and parameter sensitivities in a single figure: 195 | 196 | # %% 197 | # Create a combined figure with optimization results and parameter sensitivities 198 | fig = plt.figure(figsize=(15, 12)) 199 | 200 | # Plot 1: Optimization Results (Top) 201 | ax1 = plt.subplot2grid((4, 1), (0, 0), rowspan=1) 202 | 203 | # Plot the noisy data points 204 | ax1.scatter(x_data, y_data, color='blue', alpha=0.6, label='Noisy data') 205 | 206 | # Plot the true function 207 | ax1.plot(x_data, y_true, 'g-', linewidth=2, label='True function') 208 | 209 | # Plot the initial guess 210 | initial_guess = initial_params[0] * np.exp(-initial_params[1] * x_data) + initial_params[2] 211 | ax1.plot(x_data, initial_guess, 'r--', linewidth=2, label='Initial guess') 212 | 213 | # Plot the optimized function 214 | a_opt, b_opt, c_opt = result.x 215 | y_opt = a_opt * np.exp(-b_opt * x_data) + c_opt 216 | ax1.plot(x_data, y_opt, 'k-', linewidth=2, label='Optimized fit') 217 | 218 | # Add labels and legend 219 | ax1.set_xlabel('x') 220 | ax1.set_ylabel('y') 221 | ax1.set_title('Levenberg-Marquardt Optimization Results') 222 | ax1.legend(loc='upper right') 223 | ax1.grid(True, alpha=0.3) 224 | 225 | # Add text box with optimized parameters 226 | param_text = f'Optimized parameters:\na = {a_opt:.4f}\nb = {b_opt:.4f}\nc = {c_opt:.4f}' 227 | ax1.text(0.05, 0.5, param_text, transform=ax1.transAxes, 228 | bbox=dict(facecolor='white', alpha=0.8)) 229 | 230 | # Plots 2-4: Parameter Sensitivities (Bottom three rows) 231 | param_names = ['a', 'b', 'c'] 232 | for i in range(len(result.x)): 233 | ax = plt.subplot2grid((4, 1), (i+1, 0), rowspan=1) 234 | 235 | ax.plot(x_data, aadc_derivatives[i], 'bo-', label='AADC derivative') 236 | ax.plot(x_data, fd_derivatives[i], 'ro--', label='Finite diff derivative') 237 | 238 | ax.set_title(f'Parameter {param_names[i]} sensitivity to data points') 239 | ax.set_xlabel('x') 240 | ax.set_ylabel(f'd{param_names[i]}/dy') 241 | ax.grid(True, alpha=0.3) 242 | ax.legend() 243 | 244 | plt.tight_layout() 245 | plt.show() 246 | 247 | # %% [markdown] 248 | # # Automatic Implicit Function Theorem - Summary 249 | 250 | # %% [markdown] 251 | # ## Paper Details 252 | # 253 | # * Title: Automatic Implicit Function Theorem 254 | # * Authors: Dmitri Goloubentsev, Evgeny Lakshtanov, Vladimir Piterbarg 255 | # * Affiliation: Matlogica, Universidade de Aveiro, NatWest Markets, Imperial College London 256 | # * Published: December 14, 2021 (Revised: May 31, 2022) 257 | # * SSRN ID: 3984964 258 | # * URL: https://papers.ssrn.com/sol3/papers.cfm?abstract_id=3984964 259 | -------------------------------------------------------------------------------- /examples/opt_splines.py: -------------------------------------------------------------------------------- 1 | # %% [markdown] 2 | # # Levenberg-Marquardt Optimization with AADC Derivatives 3 | # 4 | # This notebook demonstrates using Levenberg-Marquardt optimization with both SciPy and the AADC implementation. 5 | 6 | # %% [markdown] 7 | # ## 1. Standard Implementation with SciPy 8 | # 9 | # First, let's import the necessary libraries: 10 | 11 | # %% 12 | import matplotlib.pyplot as plt 13 | import numpy as np 14 | from scipy.optimize import least_squares 15 | 16 | import aadc 17 | import aadc.ndarray 18 | from aadc.scipy.interpolate import CubicSpline 19 | 20 | # %matplotlib inline 21 | plt.ioff() # Turn off interactive mode 22 | 23 | # %% [markdown] 24 | # Now we'll define our residual function: 25 | 26 | # %% 27 | # Define the function to be minimized (residuals) 28 | def residual_func(params, x_data, y_data): 29 | # Example: fitting y = a * exp(-b * x) + c 30 | a, b, c = params 31 | y_model = a * np.exp(-b * x_data) + c 32 | return y_model - y_data 33 | 34 | # %% [markdown] 35 | # Let's generate some sample data for our optimization problem: 36 | 37 | # %% 38 | # Generate some sample data 39 | np.random.seed(42) # For reproducibility 40 | x_data = np.linspace(0, 10, 10) 41 | true_params = [5.0, 0.5, 1.0] 42 | y_true = true_params[0] * np.exp(-true_params[1] * x_data) + true_params[2] 43 | noise = np.random.normal(0, 0.2, x_data.shape) 44 | y_data = y_true + noise 45 | 46 | print("x_data:", x_data) 47 | print("y_data:", y_data) 48 | 49 | # Define data for spline fitting 50 | spline_x_data = np.linspace(0, 10, 5) 51 | initial_params = np.ones(np.shape(spline_x_data)) 52 | 53 | def spline_residual_func(params, x_data, y_data): 54 | spline = CubicSpline(spline_x_data, aadc.ndarray.AADCArray(params)) 55 | return spline(x_data) - y_data 56 | 57 | # %% [markdown] 58 | # Now we'll create an optimization function: 59 | 60 | # %% 61 | def run_optimization(x_data, y_data): 62 | # Perform Levenberg-Marquardt optimization 63 | result = least_squares( 64 | spline_residual_func, 65 | initial_params, 66 | method='lm', 67 | args=(x_data, y_data), 68 | verbose=0 69 | ) 70 | 71 | return result, initial_params 72 | 73 | # Run the optimization 74 | result, initial_params = run_optimization(x_data, y_data) 75 | 76 | # Print results 77 | print("True parameters:", true_params) 78 | print("Optimal parameters:", result.x) 79 | print("Cost:", result.cost) 80 | print("Success:", result.success) 81 | 82 | # Plot the optimization results for SciPy implementation 83 | plt.figure(figsize=(10, 6)) 84 | 85 | # Plot the noisy data points 86 | plt.scatter(x_data, y_data, color='blue', alpha=0.6, label='Noisy data') 87 | 88 | # Plot the true function 89 | plt.plot(x_data, y_true, 'g-', linewidth=2, label='True function') 90 | 91 | # Plot the initial guess 92 | plt.plot(spline_x_data, initial_params, 'r--', linewidth=2, label='Initial guess') 93 | 94 | # Plot the optimized function 95 | fitted_spline = CubicSpline(spline_x_data, result.x) 96 | y_opt = fitted_spline(x_data) 97 | plt.plot(x_data, y_opt, 'k-', linewidth=2, label='Optimized fit') 98 | 99 | # Add labels and legend 100 | plt.xlabel('x') 101 | plt.ylabel('y') 102 | plt.title('SciPy Levenberg-Marquardt Optimization Results') 103 | plt.legend() 104 | plt.grid(True, alpha=0.3) 105 | 106 | # Add text box with optimized parameters 107 | param_text = 'Optimized parameters:\n' 108 | plt.text(6, 4, param_text, bbox=dict(facecolor='white', alpha=0.8)) 109 | 110 | plt.tight_layout() 111 | plt.show() 112 | 113 | # %% [markdown] 114 | # ## 2. Implementation with AADC 115 | # 116 | # Note that objective function is using y_data internally and derivatives must be calculated with respect to y_data. 117 | # AADC.least_squares() function doesn't require Jacobian w.r.t. y_data, it will be calculated automatically. See Automatic IFT publication for more details. 118 | 119 | # %% 120 | # AADC version 121 | 122 | func = aadc.Kernel() 123 | func.start_recording() 124 | 125 | # Define the residual function with AADC inputs 126 | iy_data = aadc.ndarray.AADCArray([aadc.idouble(y) for y in y_data]) 127 | iy_data_arg = [y.mark_as_input() for y in iy_data] 128 | 129 | def objective(params): 130 | return spline_residual_func(params, x_data, iy_data) 131 | 132 | initial_params = aadc.ndarray.AADCArray([aadc.idouble(y) for y in initial_params]) 133 | aadc_result = aadc.least_squares(objective, initial_params) 134 | 135 | print("AADC Optimal parameters:", aadc_result.x) 136 | 137 | x_res = [a.mark_as_output() for a in aadc_result.x] 138 | 139 | func.stop_recording() 140 | 141 | func.print_passive_extract_locations() 142 | 143 | inputs = {y_arg: y for y_arg, y in zip(iy_data_arg, y_data)} 144 | request = {x_res[i]: iy_data_arg for i in range(len(x_res))} 145 | 146 | res = aadc.evaluate(func, request, inputs) 147 | 148 | print("True parameters:", true_params) 149 | print("Scipy Optimal parameters:", result.x) 150 | print("AADC Kernel Optimal parameters:", [res[0][x_res[i]] for i in range(len(x_res))]) 151 | 152 | # %% [markdown] 153 | # ## 3. Comparing Derivatives 154 | # 155 | # Let's compare the derivatives computed by both methods: 156 | 157 | # %% 158 | print("AADC Kernel Derivatives param 0:", res[1][x_res[0]]) 159 | print("AADC Kernel Derivatives param 1:", res[1][x_res[1]]) 160 | print("AADC Kernel Derivatives param 2:", res[1][x_res[2]]) 161 | 162 | # %% [markdown] 163 | # Now let's verify the derivatives using the finite difference method: 164 | 165 | # %% 166 | # Define the function to calculate derivatives using finite differences 167 | def calculate_finite_diff_derivatives(params, x_data, y_data, epsilon=1e-6): 168 | derivatives = [] 169 | 170 | for i in range(len(params)): 171 | param_derivatives = [] 172 | 173 | for j in range(len(y_data)): 174 | # Create modified y_data with a bump in one element 175 | y_bumped = y_data.copy() 176 | y_bumped[j] += epsilon 177 | 178 | # Run optimization with bumped data 179 | bumped_result, _ = run_optimization(x_data, y_bumped) 180 | 181 | # Calculate derivative (change in parameter / change in y) 182 | derivative = (bumped_result.x[i] - params[i]) / epsilon 183 | param_derivatives.append(derivative) 184 | 185 | derivatives.append(param_derivatives) 186 | 187 | return derivatives 188 | 189 | # Calculate finite difference derivatives for all parameters 190 | print("Calculating finite difference derivatives (this may take some time)...") 191 | fd_derivatives = calculate_finite_diff_derivatives(result.x, x_data, y_data) 192 | 193 | # Extract AADC derivatives in a format suitable for comparison 194 | aadc_derivatives = [] 195 | for i in range(len(result.x)): 196 | # Extract derivatives from the AADC result 197 | aadc_deriv_i = np.array([res[1][x_res[i]][y_arg][0] for y_arg in iy_data_arg]) 198 | aadc_derivatives.append(aadc_deriv_i) 199 | 200 | print(f"Parameter {i} derivatives:") 201 | print(f" AADC: {aadc_deriv_i}") 202 | print(f" Finite diff: {fd_derivatives[i]}") 203 | print(f" Mean difference: {np.mean(np.abs(aadc_deriv_i - np.array(fd_derivatives[i])))}") 204 | print() 205 | 206 | # %% [markdown] 207 | # ## 4. Combined Visualization 208 | # 209 | # Let's visualize both the optimization results and parameter sensitivities in a single figure: 210 | 211 | # %% 212 | # Create a combined figure with optimization results and parameter sensitivities 213 | fig = plt.figure(figsize=(15, 12)) 214 | 215 | # Plot 1: Optimization Results (Top) 216 | ax1 = plt.subplot2grid((initial_params.size+1, 1), (0, 0), rowspan=1) 217 | 218 | # Plot the noisy data points 219 | ax1.scatter(x_data, y_data, color='blue', alpha=0.6, label='Noisy data') 220 | 221 | # Plot the true function 222 | ax1.plot(x_data, y_true, 'g-', linewidth=2, label='True function') 223 | 224 | # Plot the initial guess 225 | fitted_spline = CubicSpline(spline_x_data, initial_params) 226 | initial_guess = fitted_spline(x_data) 227 | ax1.plot(x_data, initial_guess, 'r--', linewidth=2, label='Initial guess') 228 | 229 | # Plot the optimized function 230 | fitted_spline = CubicSpline(spline_x_data, result.x) 231 | y_opt = fitted_spline(x_data) 232 | ax1.plot(x_data, y_opt, 'k-', linewidth=2, label='Optimized fit') 233 | 234 | # Add labels and legend 235 | ax1.set_xlabel('x') 236 | ax1.set_ylabel('y') 237 | ax1.set_title('Levenberg-Marquardt Optimization Results') 238 | ax1.legend(loc='upper right') 239 | ax1.grid(True, alpha=0.3) 240 | 241 | # Add text box with optimized parameters 242 | param_text = 'Optimized parameters:\n' 243 | ax1.text(0.05, 0.5, param_text, transform=ax1.transAxes, 244 | bbox=dict(facecolor='white', alpha=0.8)) 245 | 246 | # Plots 2-4: Parameter Sensitivities (Bottom rows) 247 | for i in range(len(result.x)): 248 | ax = plt.subplot2grid((initial_params.size+1, 1), (i+1, 0), rowspan=1) 249 | 250 | ax.plot(x_data, aadc_derivatives[i], 'bo-', label='AADC derivative') 251 | ax.plot(x_data, fd_derivatives[i], 'ro--', label='Finite diff derivative') 252 | 253 | ax.set_title(f'Parameter {i} sensitivity to data points') 254 | ax.set_xlabel('x') 255 | ax.set_ylabel(f'd{i}/dy') 256 | ax.grid(True, alpha=0.3) 257 | ax.legend() 258 | 259 | plt.tight_layout() 260 | plt.show() 261 | 262 | # %% [markdown] 263 | 264 | # # Automatic Implicit Function Theorem - Summary 265 | 266 | # ## Paper Details 267 | # - **Title**: Automatic Implicit Function Theorem 268 | # - **Authors**: Dmitri Goloubentsev, Evgeny Lakshtanov, Vladimir Piterbarg 269 | # - **Affiliation**: Matlogica, Universidade de Aveiro, NatWest Markets, Imperial College London 270 | # - **Published**: December 14, 2021 (Revised: May 31, 2022) 271 | # - **SSRN ID**: 3984964 272 | # - **URL**: [https://papers.ssrn.com/sol3/papers.cfm?abstract_id=3984964](https://papers.ssrn.com/sol3/papers.cfm?abstract_id=3984964) 273 | -------------------------------------------------------------------------------- /tests/test_aadc_array.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pytest 3 | import scipy 4 | 5 | import aadc 6 | import aadc.overrides 7 | from aadc import Functions, ibool, idouble 8 | from aadc.ndarray import AADCArray 9 | from aadc.recording_ctx import record_kernel 10 | 11 | 12 | @pytest.fixture() 13 | def test_array() -> None: 14 | return AADCArray(np.array([[2.0, 9.0], [8.0, 4.0]])) 15 | 16 | 17 | def test_slicing(test_array) -> None: 18 | assert isinstance(test_array[1, :2], AADCArray) 19 | assert isinstance(test_array[:2][1, 0], float) 20 | 21 | 22 | def test_binary_comparisons(test_array) -> None: 23 | test_array > 4.0 24 | test_array >= 4.0 25 | test_array < 4.0 26 | test_array <= 4.0 27 | 28 | 29 | def test_np_where(test_array) -> None: 30 | np.where((test_array <= 4.0), test_array, 0.0) 31 | 32 | 33 | def test_np_where_scalars(test_array) -> None: 34 | res = np.where(aadc.ibool(True), aadc.array(np.ones(3)), np.zeros(3)) 35 | assert np.array_equal(res, np.ones(3)) 36 | 37 | 38 | def test_isfinite(test_array) -> None: 39 | np.isfinite(test_array) 40 | 41 | 42 | def test_scipy_functions(test_array) -> None: 43 | funcs = Functions() 44 | funcs.start_recording() 45 | test_array.mark_as_input() 46 | scipy.special.erf(test_array) 47 | scipy.special.erfc(test_array) 48 | funcs.stop_recording() 49 | 50 | 51 | def test_normal_cdf(test_array) -> None: 52 | unrecorded_cdf = scipy.special.ndtr(test_array) 53 | funcs = Functions() 54 | funcs.start_recording() 55 | test_array.mark_as_input() 56 | recorded_cdf = scipy.special.ndtr(test_array) 57 | funcs.stop_recording() 58 | 59 | assert np.array_equal(unrecorded_cdf, recorded_cdf) 60 | 61 | 62 | def test_matmul(test_array) -> None: 63 | unrecorded_matmul = np.matmul(test_array, test_array) 64 | funcs = Functions() 65 | funcs.start_recording() 66 | test_array.mark_as_input() 67 | recorded_matmul = np.matmul(test_array, test_array) 68 | funcs.stop_recording() 69 | 70 | assert np.array_equal(unrecorded_matmul, recorded_matmul) 71 | 72 | 73 | def test_dot(test_array) -> None: 74 | unrecorded_dot = np.dot(test_array, test_array) 75 | funcs = Functions() 76 | funcs.start_recording() 77 | test_array.mark_as_input() 78 | recorded_dot = np.dot(test_array, test_array) 79 | funcs.stop_recording() 80 | 81 | assert np.array_equal(unrecorded_dot, recorded_dot) 82 | 83 | 84 | def test_recording(test_array) -> None: 85 | funcs = Functions() 86 | funcs.start_recording() 87 | inputs = test_array.mark_as_input() 88 | 89 | def f(x): 90 | return np.exp(x).sum() 91 | 92 | f = f(test_array) 93 | output = f.mark_as_output() 94 | 95 | funcs.stop_recording() 96 | 97 | aadc_inputs = {func_input: 10 for func_input in inputs.ravel()} 98 | request = {output: inputs.ravel().tolist()} 99 | 100 | workers = aadc.ThreadPool(4) 101 | aadc.evaluate(funcs, request, aadc_inputs, workers) 102 | 103 | 104 | def test_array_of_arrays(test_array) -> None: 105 | funcs = Functions() 106 | funcs.start_recording() 107 | test_array.mark_as_input() 108 | new_array = aadc.array([test_array, test_array]) 109 | funcs.stop_recording() 110 | assert np.array_equal(new_array, np.stack((test_array, test_array))) 111 | 112 | 113 | @pytest.mark.parametrize("convertion_function", [np.asarray, np.array]) 114 | def test_convert_active_array_to_numpy(test_array, convertion_function) -> None: 115 | funcs = Functions() 116 | funcs.start_recording() 117 | test_array.mark_as_input() 118 | with pytest.raises(ValueError) as exc_info: 119 | convertion_function(test_array) 120 | funcs.stop_recording() 121 | 122 | assert exc_info.type is ValueError 123 | assert exc_info.value.args[0] == "Cannot convert an active AADCArray to a numpy array" 124 | 125 | 126 | @pytest.mark.parametrize("convertion_function", [np.asarray, np.array]) 127 | def test_convert_active_array_to_numpy_without_recording(test_array, convertion_function) -> None: 128 | numpy_test_array = np.asarray(test_array) 129 | funcs = Functions() 130 | funcs.start_recording() 131 | test_array.mark_as_input() 132 | funcs.stop_recording() 133 | assert np.allclose(convertion_function(test_array), numpy_test_array) 134 | 135 | 136 | def test_scalar_mul_with_1el_array() -> None: 137 | kernel = aadc.Functions() 138 | kernel.start_recording() 139 | 140 | s0_1 = aadc.idouble(100.0) 141 | s0_1.mark_as_input() 142 | 143 | s1 = s0_1 * np.ones(1) 144 | print(s1[:]) # should not fail 145 | 146 | with aadc.overrides.aadc_overrides(): 147 | s1 = s0_1 * np.ones(1) 148 | 149 | print(s1[:]) # should not fail 150 | kernel.stop_recording() 151 | 152 | 153 | def test_scalar_mul_with_nel_array() -> None: 154 | kernel = aadc.Functions() 155 | kernel.start_recording() 156 | 157 | s0_1 = aadc.idouble(100.0) 158 | s0_1.mark_as_input() 159 | 160 | s1 = s0_1 * np.ones(5) 161 | print(s1[:]) # should not fail 162 | 163 | with aadc.overrides.aadc_overrides(): 164 | s1 = s0_1 * np.ones(5) 165 | 166 | print(s1[:]) # should not fail 167 | kernel.stop_recording() 168 | 169 | 170 | def test_iall_with_extra_args() -> None: 171 | with record_kernel(): 172 | a = aadc.array([ibool(True), ibool(False), ibool(True)]) 173 | result = not np.all(a) 174 | assert result 175 | 176 | 177 | def test_iall_axis_1() -> None: 178 | with record_kernel(): 179 | a = aadc.array([[ibool(True), ibool(False)], [ibool(True), ibool(True)]]) 180 | result = np.all(a, axis=1) 181 | assert np.array_equal(result, np.array([False, True])) 182 | 183 | 184 | def test_iall_out() -> None: 185 | with record_kernel(): 186 | a = aadc.array([ibool(True), ibool(False), ibool(True)]) 187 | result = np.array(ibool(False)) 188 | np.all(a, out=result) 189 | assert not result 190 | 191 | 192 | def test_iall_keepdims() -> None: 193 | with record_kernel(): 194 | a = aadc.array([[ibool(True), ibool(False)], [ibool(True), ibool(True)]]) 195 | result = np.all(a, axis=1, keepdims=True) 196 | assert np.array_equal(result, np.array([[False], [True]])) 197 | 198 | 199 | def test_iall_where() -> None: 200 | with record_kernel(): 201 | a = aadc.array([[ibool(True), ibool(False)], [ibool(True), ibool(False)]]) 202 | cond = np.array([[True, False], [False, True]]) 203 | result = (not np.all(a, where=cond),) 204 | assert result 205 | 206 | 207 | def test_iand_with_extra_args() -> None: 208 | with record_kernel(): 209 | a = aadc.array([ibool(True), ibool(False), ibool(True)]) 210 | result = np.any(a) 211 | assert result 212 | 213 | 214 | def test_iand_axis_1() -> None: 215 | with record_kernel(): 216 | a = aadc.array([[ibool(True), ibool(False)], [ibool(True), ibool(True)]]) 217 | result = np.any(a, axis=1) 218 | assert np.array_equal(result, np.array([True, True])) 219 | 220 | 221 | def test_iand_out() -> None: 222 | with record_kernel(): 223 | a = aadc.array([ibool(True), ibool(False), ibool(True)]) 224 | result = np.array(ibool(True)) 225 | np.any(a, out=result) 226 | assert result 227 | 228 | 229 | def test_iand_keepdims() -> None: 230 | with record_kernel(): 231 | a = aadc.array([[ibool(True), ibool(False)], [ibool(True), ibool(True)]]) 232 | result = np.any(a, axis=1, keepdims=True) 233 | assert np.array_equal(result, np.array([[True], [True]])) 234 | 235 | 236 | def test_iand_where() -> None: 237 | with record_kernel(): 238 | a = aadc.array([[ibool(True), ibool(False)], [ibool(True), ibool(False)]]) 239 | cond = np.array([[True, False], [False, True]]) 240 | result = np.any(a, where=cond) 241 | assert result 242 | 243 | 244 | def test_searchsorted_scalar() -> None: 245 | with record_kernel(): 246 | x_args = np.array([1.0, 2.0, 3.0, 4.0]) 247 | x0_val = idouble(2.5) 248 | x0_val.mark_as_input() 249 | idx = np.searchsorted(x_args, x0_val) 250 | assert idx == 2 251 | 252 | 253 | def test_searchsorted_array() -> None: 254 | with record_kernel(): 255 | x_args = np.array([1.0, 2.0, 3.0, 4.0]) 256 | x0_vals = aadc.array([0.5, 2.5, 3.0, 4.5]) 257 | x0_vals.mark_as_input() 258 | idx = np.searchsorted(x_args, x0_vals) 259 | expected = np.array([0, 2, 2, 4]) 260 | assert np.array_equal(idx, expected) 261 | 262 | 263 | def test_cholesky_factorization_single_matrix() -> None: 264 | with record_kernel(): 265 | a = aadc.array([[4.0, 12.0, -16.0], [12.0, 37.0, -43.0], [-16.0, -43.0, 98.0]]) 266 | a.mark_as_input() 267 | 268 | l_aadc = np.linalg.cholesky(a) 269 | 270 | l_np = np.linalg.cholesky(a) 271 | 272 | assert np.allclose(l_aadc, l_np) 273 | assert np.allclose(l_aadc @ l_aadc.T, a) 274 | 275 | 276 | def test_cholesky_factorization_vectorized() -> None: 277 | with record_kernel(): 278 | a = aadc.array( 279 | [ 280 | [ 281 | [[4.0, 12.0, -16.0], [12.0, 37.0, -43.0], [-16.0, -43.0, 98.0]], 282 | [[1.0, 2.0, 3.0], [2.0, 5.0, 6.0], [3.0, 6.0, 9.0]], 283 | ], 284 | [ 285 | [[2.0, -1.0, 0.0], [-1.0, 2.0, -1.0], [0.0, -1.0, 2.0]], 286 | [[2.0, -1.0, 0.0], [-1.0, 2.0, -1.0], [0.0, -1.0, 2.0]], 287 | ], 288 | ] 289 | ) 290 | a.mark_as_input() 291 | 292 | l_aadc = np.linalg.cholesky(a) 293 | 294 | l_np = np.linalg.cholesky(a) 295 | 296 | assert np.allclose(l_aadc, l_np) 297 | assert np.allclose(l_aadc @ np.transpose(l_aadc, (0, 1, 3, 2)), a) 298 | 299 | 300 | def test_aadc_array_from_empty_array() -> None: 301 | output = aadc.array(np.full(0, 1, dtype=float)).mark_as_input() 302 | assert output.size == 0 303 | assert output.dtype == "O" 304 | -------------------------------------------------------------------------------- /getting-started/01-hello-world.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "f058977c", 6 | "metadata": {}, 7 | "source": [ 8 | "\"Open" 9 | ] 10 | }, 11 | { 12 | "cell_type": "markdown", 13 | "id": "908dd67a", 14 | "metadata": {}, 15 | "source": [ 16 | "# AADC basic \"Hello World\" example\n", 17 | "This example demonstrates how to use the AADC library to record a simple function and calculate its derivatives" 18 | ] 19 | }, 20 | { 21 | "cell_type": "code", 22 | "execution_count": 1, 23 | "id": "cc93f60c-c87c-4b57-bbf2-5fda74847c45", 24 | "metadata": {}, 25 | "outputs": [], 26 | "source": [ 27 | "import sys" 28 | ] 29 | }, 30 | { 31 | "cell_type": "markdown", 32 | "id": "9d9f0b76-7c97-4fdb-b869-7be9650e21e3", 33 | "metadata": {}, 34 | "source": [ 35 | "### Please uncomment next line if you don't have AADC installed locally" 36 | ] 37 | }, 38 | { 39 | "cell_type": "code", 40 | "execution_count": 2, 41 | "id": "e28e92e3-8fc6-45d9-b5af-7659caa9f0c1", 42 | "metadata": {}, 43 | "outputs": [], 44 | "source": [ 45 | "#!pip install https://matlogica.com/DemoReleases/aadc-1.7.5.30-cp3{sys.version_info.minor}-cp3{sys.version_info.minor}-linux_x86_64.whl" 46 | ] 47 | }, 48 | { 49 | "cell_type": "code", 50 | "execution_count": 3, 51 | "id": "4d2988b1-4dea-4032-b2c6-864d1f35c67a", 52 | "metadata": {}, 53 | "outputs": [], 54 | "source": [ 55 | "import aadc\n", 56 | "import numpy as np" 57 | ] 58 | }, 59 | { 60 | "cell_type": "markdown", 61 | "id": "3ab0d54e", 62 | "metadata": {}, 63 | "source": [ 64 | "# Simple function to record\n", 65 | "The function can take arbitrary input types and can be defined in external file" 66 | ] 67 | }, 68 | { 69 | "cell_type": "code", 70 | "execution_count": 4, 71 | "id": "40bbfc28-b257-4efe-b2f5-a8f77c911eba", 72 | "metadata": {}, 73 | "outputs": [], 74 | "source": [ 75 | "def F(x,y,z):\n", 76 | " return np.exp(x / y + z)" 77 | ] 78 | }, 79 | { 80 | "cell_type": "markdown", 81 | "id": "13fa580c", 82 | "metadata": {}, 83 | "source": [ 84 | "# Object to hold recorded AADC Kernels\n", 85 | "allow to speed up the calculation of the function itself and AAD for its derivatives\n" 86 | ] 87 | }, 88 | { 89 | "cell_type": "code", 90 | "execution_count": 5, 91 | "id": "f3b87119-b2a3-4dfb-b9cf-eebd4060d569", 92 | "metadata": {}, 93 | "outputs": [], 94 | "source": [ 95 | "funcs = aadc.Functions()" 96 | ] 97 | }, 98 | { 99 | "cell_type": "markdown", 100 | "id": "04f729a8", 101 | "metadata": {}, 102 | "source": [ 103 | "# Define function arguments.\n", 104 | "Note you need to use aadc.idouble instead of float" 105 | ] 106 | }, 107 | { 108 | "cell_type": "code", 109 | "execution_count": 6, 110 | "id": "5d39ca34-fb9b-40ed-b944-1cda7ff7f141", 111 | "metadata": {}, 112 | "outputs": [], 113 | "source": [ 114 | "x = aadc.idouble(1.0)\n", 115 | "y = aadc.idouble(2.0)\n", 116 | "z = aadc.idouble(3.0)" 117 | ] 118 | }, 119 | { 120 | "cell_type": "markdown", 121 | "id": "333a1e2c", 122 | "metadata": {}, 123 | "source": [ 124 | "# Trigger recording of the function" 125 | ] 126 | }, 127 | { 128 | "cell_type": "code", 129 | "execution_count": 7, 130 | "id": "3300d637-4725-4405-b9d5-d7eadd8cc8bc", 131 | "metadata": {}, 132 | "outputs": [ 133 | { 134 | "name": "stdout", 135 | "output_type": "stream", 136 | "text": [ 137 | "You are using evaluation version of AADC. Expire date is 20240901\n" 138 | ] 139 | } 140 | ], 141 | "source": [ 142 | "funcs.start_recording()" 143 | ] 144 | }, 145 | { 146 | "cell_type": "markdown", 147 | "id": "99d411cb", 148 | "metadata": {}, 149 | "source": [ 150 | "# Mark the function arguments as input and save reference argument ids" 151 | ] 152 | }, 153 | { 154 | "cell_type": "code", 155 | "execution_count": 8, 156 | "id": "d6cd2d27-abb4-4231-84e5-8865eabba328", 157 | "metadata": {}, 158 | "outputs": [], 159 | "source": [ 160 | "xArg = x.mark_as_input()\n", 161 | "yArg = y.mark_as_input()\n", 162 | "zArg = z.mark_as_input()" 163 | ] 164 | }, 165 | { 166 | "cell_type": "markdown", 167 | "id": "a2f0bbed", 168 | "metadata": {}, 169 | "source": [ 170 | "# Call the function and add some operations" 171 | ] 172 | }, 173 | { 174 | "cell_type": "code", 175 | "execution_count": 9, 176 | "id": "a6a70fbe-8cc7-4868-ae04-2a7121be6355", 177 | "metadata": {}, 178 | "outputs": [], 179 | "source": [ 180 | "f = F(x,y,z) + x" 181 | ] 182 | }, 183 | { 184 | "cell_type": "markdown", 185 | "id": "4636cd88", 186 | "metadata": {}, 187 | "source": [ 188 | "# Mark the result as output" 189 | ] 190 | }, 191 | { 192 | "cell_type": "code", 193 | "execution_count": 10, 194 | "id": "3a709740-8437-4dd3-b1ae-50a6f6fccec5", 195 | "metadata": {}, 196 | "outputs": [], 197 | "source": [ 198 | "fRes = f.mark_as_output()" 199 | ] 200 | }, 201 | { 202 | "cell_type": "markdown", 203 | "id": "c7ea381f", 204 | "metadata": {}, 205 | "source": [ 206 | "# Stop recording\n", 207 | "After this step AADC Kernel containing native machine CPU code will be generated and stored in the funcs object" 208 | ] 209 | }, 210 | { 211 | "cell_type": "code", 212 | "execution_count": 11, 213 | "id": "9020ad7b-0b31-4615-ac01-fdaca21f709a", 214 | "metadata": {}, 215 | "outputs": [], 216 | "source": [ 217 | "funcs.stop_recording()" 218 | ] 219 | }, 220 | { 221 | "cell_type": "markdown", 222 | "id": "92f13bb3", 223 | "metadata": {}, 224 | "source": [ 225 | "# Check if the function is recorded properly\n", 226 | "So it can be used for arbitrary input values. This should return 0 if everything is OK, indicating that no branches in the function are not supported.\n" 227 | ] 228 | }, 229 | { 230 | "cell_type": "code", 231 | "execution_count": 12, 232 | "id": "07f8caa2-2d3e-4acf-adc2-42a96f90fc08", 233 | "metadata": {}, 234 | "outputs": [ 235 | { 236 | "name": "stdout", 237 | "output_type": "stream", 238 | "text": [ 239 | "Number active to passive conversions: 0 while recording Python\n" 240 | ] 241 | } 242 | ], 243 | "source": [ 244 | "funcs.print_passive_extract_locations()" 245 | ] 246 | }, 247 | { 248 | "cell_type": "markdown", 249 | "id": "6b85f15e", 250 | "metadata": {}, 251 | "source": [ 252 | "# New samples of input values\n", 253 | "To calculate the function and its derivatives at. Note that the x input is a vector of 20 samples and the y and z are scalars" 254 | ] 255 | }, 256 | { 257 | "cell_type": "code", 258 | "execution_count": 13, 259 | "id": "56f45689-ef0c-4f60-87d4-2f31030670bd", 260 | "metadata": {}, 261 | "outputs": [], 262 | "source": [ 263 | "inputs = {\n", 264 | " xArg:(1.0 * np.random.normal(1, 0.2, 20)),\n", 265 | " yArg:(2.0),\n", 266 | " zArg:(3.0),\n", 267 | "}" 268 | ] 269 | }, 270 | { 271 | "cell_type": "markdown", 272 | "id": "f60bc787", 273 | "metadata": {}, 274 | "source": [ 275 | "Key: what output, value: what gradients are needed" 276 | ] 277 | }, 278 | { 279 | "cell_type": "code", 280 | "execution_count": 14, 281 | "id": "a4fe2659-9ea2-49ba-ab97-c1282ae30e9c", 282 | "metadata": {}, 283 | "outputs": [], 284 | "source": [ 285 | "request = {fRes:[xArg,yArg,zArg]} " 286 | ] 287 | }, 288 | { 289 | "cell_type": "markdown", 290 | "id": "b3c87901", 291 | "metadata": {}, 292 | "source": [ 293 | "Run AADC Kernel for array inputs using 4 CPU threads and avx2" 294 | ] 295 | }, 296 | { 297 | "cell_type": "code", 298 | "execution_count": 15, 299 | "id": "e7afc718-1ba6-4bd5-a96d-a05d88802c8f", 300 | "metadata": {}, 301 | "outputs": [], 302 | "source": [ 303 | "Res = aadc.evaluate(funcs, request, inputs, aadc.ThreadPool(4))" 304 | ] 305 | }, 306 | { 307 | "cell_type": "markdown", 308 | "id": "b3ab6f97", 309 | "metadata": {}, 310 | "source": [ 311 | "## Function output" 312 | ] 313 | }, 314 | { 315 | "cell_type": "code", 316 | "execution_count": 16, 317 | "id": "00ef0d56-99f7-4bb1-b38c-72c410b99123", 318 | "metadata": {}, 319 | "outputs": [ 320 | { 321 | "data": { 322 | "text/plain": [ 323 | "array([32.08581563, 31.38530127, 31.62739357, 36.80152087, 33.69603014,\n", 324 | " 29.05901561, 34.70163523, 39.10171386, 34.41085558, 27.75783793,\n", 325 | " 34.35402649, 35.74232235, 33.15115711, 34.43709469, 38.82291664,\n", 326 | " 33.6335681 , 36.29140435, 29.58695409, 30.00289371, 31.50297821])" 327 | ] 328 | }, 329 | "execution_count": 16, 330 | "metadata": {}, 331 | "output_type": "execute_result" 332 | } 333 | ], 334 | "source": [ 335 | "Res[0][fRes]" 336 | ] 337 | }, 338 | { 339 | "cell_type": "markdown", 340 | "id": "094a7d9b", 341 | "metadata": {}, 342 | "source": [ 343 | "## df/dx" 344 | ] 345 | }, 346 | { 347 | "cell_type": "code", 348 | "execution_count": 17, 349 | "id": "dafa8ec7-cd5c-40a5-b8e6-25c32e398907", 350 | "metadata": {}, 351 | "outputs": [ 352 | { 353 | "data": { 354 | "text/plain": [ 355 | "array([16.60233975, 16.27339127, 16.38702499, 18.82690445, 17.36002691,\n", 356 | " 15.18422985, 17.83425465, 19.91761619, 17.69704864, 14.57736753,\n", 357 | " 17.67024091, 18.32581633, 17.10340089, 17.70942711, 19.7852397 ,\n", 358 | " 17.33059644, 18.58548135, 15.43095436, 15.62553098, 16.32862024])" 359 | ] 360 | }, 361 | "execution_count": 17, 362 | "metadata": {}, 363 | "output_type": "execute_result" 364 | } 365 | ], 366 | "source": [ 367 | "Res[1][fRes][xArg]" 368 | ] 369 | }, 370 | { 371 | "cell_type": "markdown", 372 | "id": "a128854c", 373 | "metadata": {}, 374 | "source": [ 375 | "## df/dy" 376 | ] 377 | }, 378 | { 379 | "cell_type": "code", 380 | "execution_count": 18, 381 | "id": "f84ae8e2-3d5b-4ecf-9a3e-f42151cab1aa", 382 | "metadata": {}, 383 | "outputs": [ 384 | { 385 | "data": { 386 | "text/plain": [ 387 | "array([ -6.87389267, -6.40351229, -6.56520967, -10.23007577,\n", 388 | " -7.98349938, -4.89750186, -8.69595239, -11.97940531,\n", 389 | " -8.48843149, -4.09427466, -8.44801682, -9.44854459,\n", 390 | " -7.60366624, -8.50710751, -11.76366688, -7.93973367,\n", 391 | " -9.85175277, -5.23154837, -5.49796933, -6.48199625])" 392 | ] 393 | }, 394 | "execution_count": 18, 395 | "metadata": {}, 396 | "output_type": "execute_result" 397 | } 398 | ], 399 | "source": [ 400 | "Res[1][fRes][yArg]\n" 401 | ] 402 | } 403 | ], 404 | "metadata": { 405 | "kernelspec": { 406 | "display_name": "Python 3 (ipykernel)", 407 | "language": "python", 408 | "name": "python3" 409 | }, 410 | "language_info": { 411 | "codemirror_mode": { 412 | "name": "ipython", 413 | "version": 3 414 | }, 415 | "file_extension": ".py", 416 | "mimetype": "text/x-python", 417 | "name": "python", 418 | "nbconvert_exporter": "python", 419 | "pygments_lexer": "ipython3", 420 | "version": "3.11.2" 421 | } 422 | }, 423 | "nbformat": 4, 424 | "nbformat_minor": 5 425 | } 426 | -------------------------------------------------------------------------------- /getting-started/05-scipy-interop.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "\"Open" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "# AADC Example how to use recorded objective functions with numerical optimizations methods from SciPy" 15 | ] 16 | }, 17 | { 18 | "cell_type": "markdown", 19 | "metadata": {}, 20 | "source": [ 21 | "This notebook shows how to use AADC to achieve a 30x speedup for SABR model calibration across a volatility cube\n", 22 | "We start with a traditional approach. The calibration objective function is just a vector of calibration errors for a vector of strikes, given `sig0`, `rho`, and `nu`\n", 23 | "We then use SciPy's least_squares method to calibrate the 3 parameters for 1000 scenarios (representing a hypothetical vol cube) using 7 strikes for each calibration.\n", 24 | "We then proceed to use AADC to record the calibration objective function. Note, that thanks to using AVX2 we are able not only to calculate the vector of errors, but at the same time (and with the same computation effort) also a full jacobian of the objective function: the finite difference numerical derivatives of the errors wrt to the 3 parameters" 25 | ] 26 | }, 27 | { 28 | "cell_type": "code", 29 | "execution_count": 1, 30 | "metadata": {}, 31 | "outputs": [], 32 | "source": [ 33 | "import warnings\n", 34 | "\n", 35 | "import numpy as np\n", 36 | "\n", 37 | "def sabr_normal_vol_atm(fwd, expiry, beta, sig0, rho, nu):\n", 38 | " F = fwd\n", 39 | " t = expiry\n", 40 | " t = np.where(abs(t) < 1e-10, 1e-10, t)\n", 41 | "\n", 42 | " c1 = 1 + ((2 - 3 * rho ** 2) / 24) * (nu ** 2) * t\n", 43 | " c2 = (rho * beta * nu * t) / (4 * F ** (1 - beta))\n", 44 | " c3 = beta * (beta - 2) * t / (24 * F ** (2 - 2 * beta))\n", 45 | "\n", 46 | " sig_n = (c1 * sig0 + c2 * sig0 ** 2 + c3 * sig0 ** 3) / (F ** (-beta))\n", 47 | "\n", 48 | " return sig_n\n", 49 | "\n", 50 | "\n", 51 | "def sabr_normal_vol_otm(fwd, strike, expiry, beta, sig0, rho, nu):\n", 52 | " F = fwd\n", 53 | " K = strike\n", 54 | " t = expiry\n", 55 | " t = np.where(abs(t) < 1e-10, 1e-10, t)\n", 56 | "\n", 57 | " k = K / F\n", 58 | " alpha = sig0 / (F ** (1 - beta))\n", 59 | "\n", 60 | " beta_close_to_one = np.isclose(beta, 1, 1e-10)\n", 61 | " q = np.where(beta_close_to_one, np.log(k), (k ** (1 - beta) - 1) / (1 - beta))\n", 62 | "\n", 63 | " z = q * nu / alpha\n", 64 | " z_close_to_zero = np.isclose(z, 0, 1e-10)\n", 65 | " z = np.where(z_close_to_zero, np.nan, z)\n", 66 | "\n", 67 | " _H = z / np.log((np.sqrt(1 + 2 * rho * z + z ** 2) + z + rho) / (1 + rho))\n", 68 | "\n", 69 | " H = np.where(z_close_to_zero, 1, _H)\n", 70 | "\n", 71 | " _B = np.log((q * k ** (beta / 2)) / (k - 1)) * (alpha ** 2) / (q ** 2)\n", 72 | " _B += (rho / 4) * ((k ** beta - 1) / (k - 1)) * alpha * nu\n", 73 | " _B += ((2 - 3 * rho ** 2) / 24) * (nu ** 2)\n", 74 | "\n", 75 | " B = ((k - 1) / q) * (1 + _B * t)\n", 76 | "\n", 77 | " sig_n = sig0 * (F ** beta) * H * B\n", 78 | "\n", 79 | " return sig_n\n", 80 | "\n", 81 | "\n", 82 | "def sabr_normal_vol(fwd, strike, expiry, beta, sig0, rho, nu):\n", 83 | " F, K, expiry, beta, sig0, rho, nu = np.broadcast_arrays(fwd, strike, expiry, beta, sig0, rho, nu)\n", 84 | "\n", 85 | " return np.where(np.isclose(F, K, 1e-6),\n", 86 | " sabr_normal_vol_atm(F, expiry, beta, sig0, rho, nu),\n", 87 | " sabr_normal_vol_otm(F, K, expiry, beta, sig0, rho, nu))\n" 88 | ] 89 | }, 90 | { 91 | "cell_type": "markdown", 92 | "metadata": {}, 93 | "source": [ 94 | "Here we generate parameters for our 1000 scenarios" 95 | ] 96 | }, 97 | { 98 | "cell_type": "code", 99 | "execution_count": 2, 100 | "metadata": {}, 101 | "outputs": [ 102 | { 103 | "data": { 104 | "text/plain": [ 105 | "(array([[0.0025 , 0.00251001, 0.00252002, ..., 0.01247998, 0.01248999,\n", 106 | " 0.0125 ],\n", 107 | " [0.005 , 0.00502002, 0.00504004, ..., 0.02495996, 0.02497998,\n", 108 | " 0.025 ],\n", 109 | " [0.0075 , 0.00753003, 0.00756006, ..., 0.03743994, 0.03746997,\n", 110 | " 0.0375 ],\n", 111 | " ...,\n", 112 | " [0.0125 , 0.01255005, 0.0126001 , ..., 0.0623999 , 0.06244995,\n", 113 | " 0.0625 ],\n", 114 | " [0.015 , 0.01506006, 0.01512012, ..., 0.07487988, 0.07493994,\n", 115 | " 0.075 ],\n", 116 | " [0.0175 , 0.01757007, 0.01764014, ..., 0.08735986, 0.08742993,\n", 117 | " 0.0875 ]]),\n", 118 | " array([[0.00250636, 0.00262104, 0.00220094, ..., 0.00478576, 0.00637934,\n", 119 | " 0.00728143],\n", 120 | " [0.00252886, 0.00278514, 0.0020041 , ..., 0.00404589, 0.00591273,\n", 121 | " 0.0060057 ],\n", 122 | " [0.00251032, 0.00290853, 0.00176953, ..., 0.00306579, 0.00518306,\n", 123 | " 0.00427347],\n", 124 | " ...,\n", 125 | " [0.00252301, 0.00320189, 0.0019685 , ..., 0.00232997, 0.00447891,\n", 126 | " 0.00362963],\n", 127 | " [0.00260126, 0.00339511, 0.00242466, ..., 0.00335633, 0.00530268,\n", 128 | " 0.00564105],\n", 129 | " [0.00273087, 0.00361596, 0.00291311, ..., 0.00437557, 0.00636866,\n", 130 | " 0.00755968]]))" 131 | ] 132 | }, 133 | "execution_count": 2, 134 | "metadata": {}, 135 | "output_type": "execute_result" 136 | } 137 | ], 138 | "source": [ 139 | "warnings.filterwarnings('ignore')\n", 140 | "ncalibrations = 1000\n", 141 | "\n", 142 | "# Generate market data\n", 143 | "fwd = np.linspace(0.01, 0.05, ncalibrations)\n", 144 | "beta = np.tile([0.5, 0.6, 0.7, 0.8, 0.9], -(-ncalibrations // 5))[:ncalibrations]\n", 145 | "expiry = np.tile(np.linspace(1, 10, 10), -(-ncalibrations // 10))[:ncalibrations]\n", 146 | "\n", 147 | "np.random.seed(42)\n", 148 | "sig0 = np.random.uniform(0.01, 0.05, ncalibrations)\n", 149 | "rho = np.random.uniform(-0.5, 0., ncalibrations)\n", 150 | "nu = np.random.uniform(0.2, 0.6, ncalibrations)\n", 151 | "\n", 152 | "# 7 strikes, each row is an independent calibration scenario\n", 153 | "strikes = np.linspace(0.25, 1.75, 7).reshape(-1, 1) * fwd\n", 154 | "vols = sabr_normal_vol(fwd, strikes, expiry, beta, sig0, rho, nu)\n", 155 | "strikes,vols" 156 | ] 157 | }, 158 | { 159 | "cell_type": "markdown", 160 | "metadata": {}, 161 | "source": [ 162 | "The `residual` function is the calibration objective. Note how we're using \"walls\" to constrain `rho` within `(-1,1)`" 163 | ] 164 | }, 165 | { 166 | "cell_type": "code", 167 | "execution_count": 3, 168 | "metadata": {}, 169 | "outputs": [], 170 | "source": [ 171 | "from scipy.optimize import least_squares\n", 172 | "vol_weights = [1., 1., 1., 100., 1., 1., 1.]\n", 173 | "\n", 174 | "def residual(x, fwd, beta, expiry, strikes, vols):\n", 175 | " sig0, rho, nu = x\n", 176 | " rho = np.broadcast_to(rho, vols.shape)\n", 177 | "\n", 178 | " return np.where(np.abs(rho) > 0.9999,\n", 179 | " np.ones_like(vols) * 1e6,\n", 180 | " vol_weights * (sabr_normal_vol(fwd, strikes, expiry, beta, sig0, rho, nu) - vols))\n", 181 | "\n", 182 | "x0 = np.array([0.02, -0.25, 0.03])\n", 183 | "\n", 184 | "def sabr_normal_smile_fit(scen):\n", 185 | " results = least_squares(\n", 186 | " residual,\n", 187 | " x0,\n", 188 | " args=(fwd[scen], beta[scen], expiry[scen], strikes[:, scen], vols[:, scen]),\n", 189 | " method=\"lm\",\n", 190 | " xtol=1e-6)\n", 191 | "\n", 192 | " return results.x" 193 | ] 194 | }, 195 | { 196 | "cell_type": "markdown", 197 | "metadata": {}, 198 | "source": [ 199 | "Solve for sig0, rho, nu for each scenario: `sig0_bar`, `rho_bar`, `nu_bar` are calibrated parameters so far without any use of AADC. Note the cell computation time." 200 | ] 201 | }, 202 | { 203 | "cell_type": "code", 204 | "execution_count": 4, 205 | "metadata": {}, 206 | "outputs": [ 207 | { 208 | "name": "stdout", 209 | "output_type": "stream", 210 | "text": [ 211 | "CPU times: user 9.73 s, sys: 0 ns, total: 9.73 s\n", 212 | "Wall time: 9.78 s\n" 213 | ] 214 | } 215 | ], 216 | "source": [ 217 | "%%time\n", 218 | "sig0_bar, rho_bar, nu_bar = np.transpose([sabr_normal_smile_fit(j) for j in range(ncalibrations)])\n", 219 | "\n", 220 | "rho_bar, nu_bar = np.where(nu_bar < 0, -rho_bar, rho_bar), np.abs(nu_bar)" 221 | ] 222 | }, 223 | { 224 | "cell_type": "code", 225 | "execution_count": 5, 226 | "metadata": {}, 227 | "outputs": [], 228 | "source": [ 229 | "assert np.allclose(sig0_bar, sig0, atol=1e-10)\n", 230 | "assert np.allclose(rho_bar, rho, atol=1e-10)\n", 231 | "assert np.allclose(nu_bar, nu, atol=1e-10)\n", 232 | "# sig0_bar - sig0, rho_bar - rho, nu_bar - nu\n", 233 | "# np.argmax(np.abs(sig0_bar - sig0)), np.argmax(np.abs(rho_bar - rho)), np.argmax(np.abs(nu_bar - nu))\n" 234 | ] 235 | }, 236 | { 237 | "cell_type": "markdown", 238 | "metadata": {}, 239 | "source": [ 240 | "Now AADC the whole thing. `aadc.record` returns the recorded AADC kernel for `residual` function, and has 3 members of interest:\n", 241 | "- `kernel.func` is the AADC JIT version of `residual` and can be substituted for it in `least_squares`, except for a small detail:\n", 242 | "- `kernel.set_params` should be used to curry the extra arguments of the objective function that are not part of the calibration\n", 243 | "- `kernel.jac` is the numerical jacobian, and it can be fed to `least_squares` to achieve faster convergence" 244 | ] 245 | }, 246 | { 247 | "cell_type": "markdown", 248 | "metadata": {}, 249 | "source": [ 250 | "### Please uncomment next line if you don't have AADC installed locally" 251 | ] 252 | }, 253 | { 254 | "cell_type": "code", 255 | "execution_count": 6, 256 | "metadata": {}, 257 | "outputs": [], 258 | "source": [ 259 | "import sys\n", 260 | "#!pip install https://matlogica.com/DemoReleases/aadc-1.7.5.30-cp3{sys.version_info.minor}-cp3{sys.version_info.minor}-linux_x86_64.whl" 261 | ] 262 | }, 263 | { 264 | "cell_type": "code", 265 | "execution_count": 7, 266 | "metadata": {}, 267 | "outputs": [ 268 | { 269 | "name": "stdout", 270 | "output_type": "stream", 271 | "text": [ 272 | "You are using evaluation version of AADC. Expire date is 20240901\n" 273 | ] 274 | } 275 | ], 276 | "source": [ 277 | "import aadc\n", 278 | "warnings.filterwarnings('ignore')\n", 279 | "\n", 280 | "kernel = aadc.record(residual, x0, params=(fwd[0], beta[0], expiry[0], strikes[:, 0], vols[:, 0]), bump_size=1e-10)" 281 | ] 282 | }, 283 | { 284 | "cell_type": "code", 285 | "execution_count": 8, 286 | "metadata": {}, 287 | "outputs": [], 288 | "source": [ 289 | "def sabr_normal_smile_fit_aadc(scen):\n", 290 | " kernel.set_params(fwd[scen], beta[scen], expiry[scen], strikes[:, scen], vols[:, scen])\n", 291 | " results = least_squares(\n", 292 | " kernel.func,\n", 293 | " x0,\n", 294 | " jac=kernel.jac,\n", 295 | " method=\"lm\",\n", 296 | " xtol=1e-6)\n", 297 | "\n", 298 | " return results.x" 299 | ] 300 | }, 301 | { 302 | "cell_type": "markdown", 303 | "metadata": {}, 304 | "source": [ 305 | "Moment of truth. Let's check the solution we obtain with the JIT kernel is the same" 306 | ] 307 | }, 308 | { 309 | "cell_type": "code", 310 | "execution_count": 9, 311 | "metadata": {}, 312 | "outputs": [ 313 | { 314 | "name": "stdout", 315 | "output_type": "stream", 316 | "text": [ 317 | "CPU times: user 340 ms, sys: 3.03 ms, total: 343 ms\n", 318 | "Wall time: 342 ms\n" 319 | ] 320 | } 321 | ], 322 | "source": [ 323 | "%%time\n", 324 | "sig0_star, rho_star, nu_star = np.transpose([sabr_normal_smile_fit_aadc(j) for j in range(ncalibrations)])\n", 325 | "rho_star, nu_star = np.where(nu_star < 0, -rho_star, rho_star), np.abs(nu_star)\n", 326 | "\n", 327 | "assert np.allclose(sig0_bar, sig0_star, atol=1e-10)\n", 328 | "assert np.allclose(rho_bar, rho_star, atol=1e-10)\n", 329 | "assert np.allclose(nu_bar, nu_star, atol=1e-10)\n", 330 | "\n", 331 | "# sig0_star - sig0_bar, rho_star - rho_bar, nu_star - nu_bar\n", 332 | "# np.argmax(np.abs(sig0_star - sig0_bar)), np.argmax(np.abs(rho_star - rho_bar)), np.argmax(np.abs(nu_star - nu_bar))\n", 333 | "\n" 334 | ] 335 | } 336 | ], 337 | "metadata": { 338 | "kernelspec": { 339 | "display_name": "Python 3 (ipykernel)", 340 | "language": "python", 341 | "name": "python3" 342 | }, 343 | "language_info": { 344 | "codemirror_mode": { 345 | "name": "ipython", 346 | "version": 3 347 | }, 348 | "file_extension": ".py", 349 | "mimetype": "text/x-python", 350 | "name": "python", 351 | "nbconvert_exporter": "python", 352 | "pygments_lexer": "ipython3", 353 | "version": "3.11.8" 354 | } 355 | }, 356 | "nbformat": 4, 357 | "nbformat_minor": 4 358 | } 359 | -------------------------------------------------------------------------------- /QuantLib/07-LiveRisk.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "\"Open" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "# AADC Live risk server approach\n", 15 | "We start with generating a random portfolio of 1000 IR swaps with random start dates, notionals and maturities.\n", 16 | "We then record an AADC kernel for a single portfolio price, marking the IR zero rates as inputs.\n", 17 | "We then simulate random \"market updates\" and demonstrate fast repricing of the portfolio along with bucketed AAD deltas.\n", 18 | "Kernel execution is so fast (within tens of ms), that it can be used as a source of a ticking \"live risk\" for the portfolio" 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": 17, 24 | "metadata": {}, 25 | "outputs": [], 26 | "source": [ 27 | "import sys\n", 28 | "#!pip install https://matlogica.com/DemoReleases/aadcquantlib-1.7.5.30-cp3{sys.version_info.minor}-cp3{sys.version_info.minor}-linux_x86_64.whl\n", 29 | "import aadc\n", 30 | "import aadc.quantlib as ql\n", 31 | "import numpy as np\n", 32 | "import matplotlib.pyplot as plt\n", 33 | "import ipywidgets as widgets\n", 34 | "from ipywidgets import interactive\n", 35 | "import random\n", 36 | "import time" 37 | ] 38 | }, 39 | { 40 | "cell_type": "markdown", 41 | "metadata": {}, 42 | "source": [ 43 | "Random portfolio generation:" 44 | ] 45 | }, 46 | { 47 | "cell_type": "code", 48 | "execution_count": 18, 49 | "metadata": {}, 50 | "outputs": [], 51 | "source": [ 52 | "def createRandomSwaps(num_trades, todays_date, calendar):\n", 53 | " random.seed(42)\n", 54 | " portfolio = []\n", 55 | "\n", 56 | " for _ in range(num_trades):\n", 57 | " discounting_term_structure = ql.RelinkableYieldTermStructureHandle()\n", 58 | " forecasting_term_structure = ql.RelinkableYieldTermStructureHandle()\n", 59 | "\n", 60 | " nominal = 1000000.0 * (100.0 + random.randint(0, 399)) * (-1.0 + 2 * random.randint(0, 1))\n", 61 | "\n", 62 | " # Fixed leg\n", 63 | " fixed_leg_frequency = ql.Bimonthly\n", 64 | " fixed_leg_convention = ql.Unadjusted\n", 65 | " floating_leg_convention = ql.ModifiedFollowing\n", 66 | " fixed_leg_day_counter = ql.Thirty360(ql.Thirty360.European)\n", 67 | " fixed_rate = 0.03\n", 68 | " floating_leg_day_counter = ql.Actual360()\n", 69 | " start_date = todays_date + ql.Period(10 + random.randint(0, 359), ql.Days)\n", 70 | "\n", 71 | " # Floating leg\n", 72 | " floating_leg_frequency = ql.Semiannual\n", 73 | " euribor_index = ql.Euribor6M(forecasting_term_structure)\n", 74 | " spread = 0.0\n", 75 | "\n", 76 | " length_in_years = 2 + random.randint(0, 25)\n", 77 | "\n", 78 | " swap_type = ql.VanillaSwap.Payer\n", 79 | "\n", 80 | " maturity_date = start_date + ql.Period(length_in_years, ql.Years)\n", 81 | " fixed_schedule = ql.Schedule(start_date, maturity_date, ql.Period(fixed_leg_frequency), calendar,\n", 82 | " fixed_leg_convention, fixed_leg_convention, ql.DateGeneration.Forward, False)\n", 83 | " floating_schedule = ql.Schedule(start_date, maturity_date, ql.Period(floating_leg_frequency), calendar,\n", 84 | " floating_leg_convention, floating_leg_convention, ql.DateGeneration.Forward, False)\n", 85 | "\n", 86 | " random_vanilla_swap = ql.VanillaSwap(\n", 87 | " swap_type, nominal, fixed_schedule, fixed_rate, fixed_leg_day_counter,\n", 88 | " floating_schedule, euribor_index, spread, floating_leg_day_counter\n", 89 | " )\n", 90 | "\n", 91 | " swap_engine = ql.DiscountingSwapEngine(discounting_term_structure)\n", 92 | " random_vanilla_swap.setPricingEngine(swap_engine)\n", 93 | "\n", 94 | " portfolio.append((random_vanilla_swap, discounting_term_structure, forecasting_term_structure))\n", 95 | "\n", 96 | " return portfolio" 97 | ] 98 | }, 99 | { 100 | "cell_type": "markdown", 101 | "metadata": {}, 102 | "source": [ 103 | "Pricing using original Quantlib code" 104 | ] 105 | }, 106 | { 107 | "cell_type": "code", 108 | "execution_count": 19, 109 | "metadata": {}, 110 | "outputs": [], 111 | "source": [ 112 | "def price_portfolio(portfolio, dates, zero_rates):\n", 113 | " total_npv = aadc.idouble(0.0)\n", 114 | "\n", 115 | " log_linear_curve = ql.ZeroCurve(dates, zero_rates.tolist(), ql.Actual360(), ql.TARGET())\n", 116 | " log_linear_curve.enableExtrapolation()\n", 117 | "\n", 118 | " for (swap, discounting_term_structure, forecasting_term_structure) in portfolio:\n", 119 | " discounting_term_structure.linkTo(log_linear_curve)\n", 120 | " forecasting_term_structure.linkTo(log_linear_curve)\n", 121 | " total_npv += swap.NPV()\n", 122 | "\n", 123 | " return total_npv" 124 | ] 125 | }, 126 | { 127 | "cell_type": "markdown", 128 | "metadata": {}, 129 | "source": [ 130 | "Recording the AADC kernel" 131 | ] 132 | }, 133 | { 134 | "cell_type": "code", 135 | "execution_count": 20, 136 | "metadata": {}, 137 | "outputs": [], 138 | "source": [ 139 | "\n", 140 | "\n", 141 | "def record_kernel(portfolio, dates):\n", 142 | " kernel = aadc.Kernel()\n", 143 | " kernel.start_recording()\n", 144 | "\n", 145 | " zero_rates = aadc.array(np.zeros(len(dates)))\n", 146 | " zero_args = zero_rates.mark_as_input()\n", 147 | "\n", 148 | " total_npv = price_portfolio(portfolio, dates, zero_rates)\n", 149 | "\n", 150 | " res = total_npv.mark_as_output()\n", 151 | " kernel.stop_recording()\n", 152 | "\n", 153 | " return (kernel, { res: zero_args })" 154 | ] 155 | }, 156 | { 157 | "cell_type": "markdown", 158 | "metadata": {}, 159 | "source": [ 160 | "Set up everything and run the recording. This could be done at the beginning of a trading day in a live risk server setting" 161 | ] 162 | }, 163 | { 164 | "cell_type": "code", 165 | "execution_count": 21, 166 | "metadata": {}, 167 | "outputs": [ 168 | { 169 | "name": "stdout", 170 | "output_type": "stream", 171 | "text": [ 172 | "You are using evaluation version of AADC. Expire date is 20240901\n", 173 | "Recording time: 4.090595722198486\n", 174 | "Number active to passive conversions: 0 while recording Python\n" 175 | ] 176 | } 177 | ], 178 | "source": [ 179 | "\n", 180 | "num_trades = 1000\n", 181 | "todays_date = ql.Date(12, ql.June, 2024)\n", 182 | "calendar = ql.TARGET()\n", 183 | "\n", 184 | "portfolio = createRandomSwaps(num_trades, todays_date, calendar)\n", 185 | "dates = [todays_date] + [todays_date + ql.Period(i, ql.Years) for i in range(1, 30)]\n", 186 | "\n", 187 | "mark_time = time.time()\n", 188 | "(kernel, request) = record_kernel(portfolio, dates)\n", 189 | "\n", 190 | "print(\"Recording time: \", time.time() - mark_time)\n", 191 | "\n", 192 | "kernel.print_passive_extract_locations()" 193 | ] 194 | }, 195 | { 196 | "cell_type": "markdown", 197 | "metadata": {}, 198 | "source": [ 199 | "Time one portfolio pricing with original Quantlib code" 200 | ] 201 | }, 202 | { 203 | "cell_type": "code", 204 | "execution_count": 22, 205 | "metadata": {}, 206 | "outputs": [ 207 | { 208 | "name": "stdout", 209 | "output_type": "stream", 210 | "text": [ 211 | "CPU times: user 70.1 ms, sys: 33 ms, total: 103 ms\n", 212 | "Wall time: 103 ms\n" 213 | ] 214 | }, 215 | { 216 | "data": { 217 | "text/plain": [ 218 | "-4826369999.999888" 219 | ] 220 | }, 221 | "execution_count": 22, 222 | "metadata": {}, 223 | "output_type": "execute_result" 224 | } 225 | ], 226 | "source": [ 227 | "%%time\n", 228 | "price_portfolio(portfolio, dates, np.zeros(len(dates)))" 229 | ] 230 | }, 231 | { 232 | "cell_type": "markdown", 233 | "metadata": {}, 234 | "source": [ 235 | "Execute the recorded kernel for a given market update. The random \"market update\" is simulated by providing different seeds to the random numbers generator and subsequently using the generator to produce zero rates for the IR curves." 236 | ] 237 | }, 238 | { 239 | "cell_type": "code", 240 | "execution_count": 23, 241 | "metadata": {}, 242 | "outputs": [ 243 | { 244 | "name": "stdout", 245 | "output_type": "stream", 246 | "text": [ 247 | "Calculation time: 0.05907607078552246\n" 248 | ] 249 | }, 250 | { 251 | "data": { 252 | "text/plain": [ 253 | "(array([0.0106, 0.0039, 0.0028, 0.0119, 0.006 , 0.0056, 0.0053, 0.0042,\n", 254 | " 0.0119, 0.0038, 0.0111, 0.0119, 0.0094, 0.0036, 0.01 , 0.0079,\n", 255 | " 0.0029, 0.0028, 0.0036, 0.0052, 0.0054, 0.0089, 0.0102, 0.0028,\n", 256 | " 0.0096, 0.005 , 0.0116, 0.0108, 0.0114, 0.0094]),\n", 257 | " ({Res(593280): array([-3.06171282e+09])},\n", 258 | " {Res(593280): {Arg(7): array([-2.71154029e+08]),\n", 259 | " Arg(8): array([-2.33599052e+09]),\n", 260 | " Arg(9): array([95473624.52143878]),\n", 261 | " Arg(10): array([-2.67159216e+09]),\n", 262 | " Arg(11): array([-1.13067702e+10]),\n", 263 | " Arg(12): array([-7.7708617e+09]),\n", 264 | " Arg(13): array([-1.68327627e+09]),\n", 265 | " Arg(14): array([-5.02848577e+08]),\n", 266 | " Arg(15): array([1.40243253e+09]),\n", 267 | " Arg(16): array([1.88911676e+09]),\n", 268 | " Arg(17): array([7.14903345e+09]),\n", 269 | " Arg(18): array([3.85957032e+09]),\n", 270 | " Arg(19): array([-1.30158466e+10]),\n", 271 | " Arg(20): array([-3.09489203e+09]),\n", 272 | " Arg(21): array([-2.93155413e+10]),\n", 273 | " Arg(22): array([2.19427717e+10]),\n", 274 | " Arg(23): array([1.77834842e+10]),\n", 275 | " Arg(24): array([3.46343595e+09]),\n", 276 | " Arg(25): array([2.28373239e+10]),\n", 277 | " Arg(26): array([2.23932045e+10]),\n", 278 | " Arg(27): array([2.55189772e+10]),\n", 279 | " Arg(28): array([1.68716069e+10]),\n", 280 | " Arg(29): array([1.24651855e+10]),\n", 281 | " Arg(30): array([-2.22644554e+10]),\n", 282 | " Arg(31): array([1.89700615e+10]),\n", 283 | " Arg(32): array([2.37012977e+10]),\n", 284 | " Arg(33): array([1.15514375e+10]),\n", 285 | " Arg(34): array([4.96470892e+10]),\n", 286 | " Arg(35): array([2.04907595e+10]),\n", 287 | " Arg(36): array([0.])}}))" 288 | ] 289 | }, 290 | "execution_count": 23, 291 | "metadata": {}, 292 | "output_type": "execute_result" 293 | } 294 | ], 295 | "source": [ 296 | "import time\n", 297 | "\n", 298 | "def calc_risks(seed):\n", 299 | " random.seed(seed)\n", 300 | " zero_rates = np.zeros(len(dates))\n", 301 | " for i in range(len(dates)):\n", 302 | " zero_rates[i] = 0.0025 + 0.005 * 0.02 * random.randint(0, 99)\n", 303 | "\n", 304 | " args = list(request.values())[0]\n", 305 | "\n", 306 | " time_mark = time.time()\n", 307 | "\n", 308 | " r = aadc.evaluate(kernel, request, { a: [x] for a, x in zip(args, zero_rates) }, aadc.ThreadPool(1))\n", 309 | "\n", 310 | " print(\"Calculation time: \", time.time() - time_mark)\n", 311 | "\n", 312 | " return (zero_rates, r)\n", 313 | "\n", 314 | "calc_risks(42)" 315 | ] 316 | }, 317 | { 318 | "cell_type": "markdown", 319 | "metadata": {}, 320 | "source": [ 321 | "Plot the results interactively" 322 | ] 323 | }, 324 | { 325 | "cell_type": "code", 326 | "execution_count": 24, 327 | "metadata": {}, 328 | "outputs": [ 329 | { 330 | "data": { 331 | "application/vnd.jupyter.widget-view+json": { 332 | "model_id": "6da54eba40624bbbb96786128f971999", 333 | "version_major": 2, 334 | "version_minor": 0 335 | }, 336 | "text/plain": [ 337 | "interactive(children=(FloatSlider(value=42.0, continuous_update=False, description='Seed:', min=1.0, step=1.0)…" 338 | ] 339 | }, 340 | "execution_count": 24, 341 | "metadata": {}, 342 | "output_type": "execute_result" 343 | } 344 | ], 345 | "source": [ 346 | "def plot_risks(seed):\n", 347 | " (zero_rates, r) = calc_risks(seed)\n", 348 | "\n", 349 | " risks = [item for subdict in r[1].values() for sublist in subdict.values() for item in sublist]\n", 350 | " assert(len(risks) == len(dates))\n", 351 | "\n", 352 | " plot_years = range(0, 30)\n", 353 | " assert(len(plot_years) == len(dates))\n", 354 | "\n", 355 | " plt.figure(figsize=(14, 6))\n", 356 | " plt.subplot(1, 2, 1)\n", 357 | " plt.plot(plot_years, zero_rates, marker='o', linestyle='-', color='b')\n", 358 | " plt.xlabel('Year')\n", 359 | " plt.ylabel('Zero Rate')\n", 360 | " plt.title('Zero Rate Curve')\n", 361 | " plt.grid(True)\n", 362 | " plt.ylim(0, max(zero_rates) * 1.5)\n", 363 | "\n", 364 | " # Plotting the riks\n", 365 | " plt.subplot(1, 2, 2)\n", 366 | " plt.plot(plot_years, risks, marker='o', linestyle='-', color='r')\n", 367 | " plt.xlabel('Year')\n", 368 | " plt.ylabel('Zero risk')\n", 369 | " plt.title('Zero risks')\n", 370 | " plt.grid(True)\n", 371 | " plt.ylim(min(risks) * 1.5, max(risks) * 1.5)\n", 372 | "\n", 373 | " plt.tight_layout()\n", 374 | " plt.show()\n", 375 | "\n", 376 | "\n", 377 | "\n", 378 | "# Create interactive sliders for level and slope\n", 379 | "seed_slider = widgets.FloatSlider(\n", 380 | " value=42,\n", 381 | " min=1,\n", 382 | " max=100,\n", 383 | " step=1,\n", 384 | " description='Seed:',\n", 385 | " continuous_update=False\n", 386 | ")\n", 387 | "\n", 388 | "# Use interactive function to update the plot\n", 389 | "interactive_plot = interactive(plot_risks, seed=seed_slider)\n", 390 | "output = interactive_plot.children[-1]\n", 391 | "output.layout.height = '650px'\n", 392 | "interactive_plot\n" 393 | ] 394 | } 395 | ], 396 | "metadata": { 397 | "kernelspec": { 398 | "display_name": "Python 3", 399 | "language": "python", 400 | "name": "python3" 401 | }, 402 | "language_info": { 403 | "codemirror_mode": { 404 | "name": "ipython", 405 | "version": 3 406 | }, 407 | "file_extension": ".py", 408 | "mimetype": "text/x-python", 409 | "name": "python", 410 | "nbconvert_exporter": "python", 411 | "pygments_lexer": "ipython3", 412 | "version": "3.11.8" 413 | } 414 | }, 415 | "nbformat": 4, 416 | "nbformat_minor": 2 417 | } 418 | --------------------------------------------------------------------------------