From db8604d892e280409385f18dda3001b62692b95e Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Sat, 4 May 2024 23:38:47 -0400 Subject: [PATCH 1/9] ENH: make Function.get_value() accept complex numbers input --- rocketpy/mathutils/function.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rocketpy/mathutils/function.py b/rocketpy/mathutils/function.py index 6dc1c764b..fd84525fb 100644 --- a/rocketpy/mathutils/function.py +++ b/rocketpy/mathutils/function.py @@ -860,7 +860,7 @@ def get_value(self, *args): # if the function is 1-D: if self.__dom_dim__ == 1: # if the args is a simple number (int or float) - if isinstance(args[0], (int, float)): + if isinstance(args[0], (int, float, complex)): return self.source(args[0]) # if the arguments are iterable, we map and return a list if isinstance(args[0], Iterable): @@ -869,7 +869,7 @@ def get_value(self, *args): # if the function is n-D: else: # if each arg is a simple number (int or float) - if all(isinstance(arg, (int, float)) for arg in args): + if all(isinstance(arg, (int, float, complex)) for arg in args): return self.source(*args) # if each arg is iterable, we map and return a list if all(isinstance(arg, Iterable) for arg in args): From 1d011a62640d2062c84f86dab1eb24927b11bea3 Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Sat, 4 May 2024 23:39:20 -0400 Subject: [PATCH 2/9] ENH: add Function.differentiate_complex_step method --- rocketpy/mathutils/function.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/rocketpy/mathutils/function.py b/rocketpy/mathutils/function.py index fd84525fb..7994c355f 100644 --- a/rocketpy/mathutils/function.py +++ b/rocketpy/mathutils/function.py @@ -2427,6 +2427,30 @@ def differentiate(self, x, dx=1e-6, order=1): + self.get_value_opt(x - dx) ) / dx**2 + def differentiate_complex_step(self, x, dx=1e-6): + """Differentiate a Function object at a given point using the complex + step method. This method can be faster than ``Function.differentiate`` + since it requires only one evaluation of the function. However, the + evaluated function must accept complex numbers as input. + + Parameters + ---------- + x : float + Point at which to differentiate. + dx : float, optional + Step size to use for numerical differentiation, by default 1e-6. + + Returns + ------- + float + The real part of the derivative of the function at the given point. + + References + ---------- + [1] https://mdolab.engin.umich.edu/wiki/guide-complex-step-derivative-approximation + """ + return float(self.get_value_opt(x + dx * 1j).imag / dx) + def identity_function(self): """Returns a Function object that correspond to the identity mapping, i.e. f(x) = x. From eeaa694d2a5e0cb9b30db41f7096c0b91627e53a Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Sat, 4 May 2024 23:40:02 -0400 Subject: [PATCH 3/9] TST: Add tests for the new Function.differentiate_complex_step method --- tests/unit/test_function.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/unit/test_function.py b/tests/unit/test_function.py index 17da59498..45ef3a05e 100644 --- a/tests/unit/test_function.py +++ b/tests/unit/test_function.py @@ -79,6 +79,33 @@ def test_differentiate(func_input, derivative_input, expected_derivative): assert np.isclose(func.differentiate(derivative_input), expected_derivative) +@pytest.mark.parametrize( + "func_input, derivative_input, expected_derivative", + [ + (1, 0, 0), # Test case 1: Function(1) + (lambda x: x, 0, 1), # Test case 2: Function(lambda x: x) + (lambda x: x**2, 1, 2), # Test case 3: Function(lambda x: x**2) + ], +) +def test_differentiate_complex_step(func_input, derivative_input, expected_derivative): + """Test the differentiate_complex_step method of the Function class. + + Parameters + ---------- + func_input : function + A function object created from a list of values. + derivative_input : int + Point at which to differentiate. + expected_derivative : float + Expected value of the derivative. + """ + func = Function(func_input) + assert isinstance(func.differentiate_complex_step(derivative_input), float) + assert np.isclose( + func.differentiate_complex_step(derivative_input), expected_derivative + ) + + def test_get_value(): """Tests the get_value method of the Function class. Both with respect to return instances and expected behaviour. From 3aad9a205a639876dc101a67eacfd2404004c759 Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Sat, 4 May 2024 23:41:58 -0400 Subject: [PATCH 4/9] DEV: adds PR #594 to the CHANGELOG.md file --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5f0d2e80..fbe00cfa0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Added +- ENH: Complex step differentiation [#594](https://github.com/RocketPy-Team/RocketPy/pull/594) - ENH: Exponential backoff decorator (fix #449) [#588](https://github.com/RocketPy-Team/RocketPy/pull/588) - ENH: Function Validation Rework & Swap `np.searchsorted` to `bisect_left` [#582](https://github.com/RocketPy-Team/RocketPy/pull/582) - ENH: Add new stability margin properties to Flight class [#572](https://github.com/RocketPy-Team/RocketPy/pull/572) From 09f291f4342ab17b949d9b4ae935996a331f1a48 Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Sat, 4 May 2024 23:54:53 -0400 Subject: [PATCH 5/9] DOC: add complex step differentiation to the Function docs pags --- docs/user/function.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/user/function.rst b/docs/user/function.rst index b95496991..bd8e82d3c 100644 --- a/docs/user/function.rst +++ b/docs/user/function.rst @@ -346,6 +346,7 @@ d. Differentiation and Integration One of the most useful ``Function`` features for data analysis is easily differentiating and integrating the data source. These methods are divided as follow: - :meth:`rocketpy.Function.differentiate`: differentiate the ``Function`` at a given point, returning the derivative value as the result; +- :meth:`rocketpy.Function.differentiate_complex_step`: differentiate the ``Function`` at a given point using the complex step method, returning the derivative value as the result; - :meth:`rocketpy.Function.integral`: performs a definite integral over specified limits, returns the integral value (area under ``Function``); - :meth:`rocketpy.Function.derivative_function`: computes the derivative of the given `Function`, returning another `Function` that is the derivative of the original at each point; - :meth:`rocketpy.Function.integral_function`: calculates the definite integral of the function from a given point up to a variable, returns a ``Function``. @@ -363,6 +364,19 @@ Let's make a familiar example of differentiation: the derivative of the function # Differentiate it at x = 3 print(f.differentiate(3)) +RocketPy also supports the complex step method for differentiation, which is a very accurate method for numerical differentiation. Let's compare the results of the complex step method with the standard method: + +.. jupyter-execute:: + + # Define the function x^2 + f = Function(lambda x: x**2) + + # Differentiate it at x = 3 using the complex step method + print(f.differentiate_complex_step(3)) + +The complex step method can be as twice as faster as the standard method, but +it requires the function to be differentiable in the complex plane. + Also one may compute the derivative function: .. jupyter-execute:: From 13562483f646d8cb61f2105d1fa72f892aa0a37c Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Wed, 8 May 2024 02:09:38 -0400 Subject: [PATCH 6/9] ENH: Implement second order derivative complex step --- rocketpy/mathutils/function.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/rocketpy/mathutils/function.py b/rocketpy/mathutils/function.py index 7994c355f..ca7d1e093 100644 --- a/rocketpy/mathutils/function.py +++ b/rocketpy/mathutils/function.py @@ -2427,7 +2427,7 @@ def differentiate(self, x, dx=1e-6, order=1): + self.get_value_opt(x - dx) ) / dx**2 - def differentiate_complex_step(self, x, dx=1e-6): + def differentiate_complex_step(self, x, dx=1e-200, order=1): """Differentiate a Function object at a given point using the complex step method. This method can be faster than ``Function.differentiate`` since it requires only one evaluation of the function. However, the @@ -2438,7 +2438,10 @@ def differentiate_complex_step(self, x, dx=1e-6): x : float Point at which to differentiate. dx : float, optional - Step size to use for numerical differentiation, by default 1e-6. + Step size to use for numerical differentiation, by default 1e-200. + order : int, optional + Order of differentiation, by default 1. Right now, only first and + second order derivatives are supported. Returns ------- @@ -2449,7 +2452,19 @@ def differentiate_complex_step(self, x, dx=1e-6): ---------- [1] https://mdolab.engin.umich.edu/wiki/guide-complex-step-derivative-approximation """ - return float(self.get_value_opt(x + dx * 1j).imag / dx) + if order == 1: + return float(self.get_value_opt(x + dx * 1j).imag / dx) + elif order == 2: + return float( + 2 + * (self.get_value_opt(x).real - self.get_value_opt(x + dx * 1j).real) + / dx**2 + ) + else: + raise NotImplementedError( + "Only 1st and 2nd order derivatives are supported. " + "Set order=1 or order=2." + ) def identity_function(self): """Returns a Function object that correspond to the identity mapping, From dfc794bb90ee270ba907b66126c43d870ce4d1ff Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Wed, 8 May 2024 02:12:12 -0400 Subject: [PATCH 7/9] TST: improve tests for the complex step differentiation --- tests/unit/test_function.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tests/unit/test_function.py b/tests/unit/test_function.py index 45ef3a05e..c3671dc91 100644 --- a/tests/unit/test_function.py +++ b/tests/unit/test_function.py @@ -80,11 +80,12 @@ def test_differentiate(func_input, derivative_input, expected_derivative): @pytest.mark.parametrize( - "func_input, derivative_input, expected_derivative", + "func_input, derivative_input, expected_first_derivative, expected_second_derivative", [ - (1, 0, 0), # Test case 1: Function(1) - (lambda x: x, 0, 1), # Test case 2: Function(lambda x: x) - (lambda x: x**2, 1, 2), # Test case 3: Function(lambda x: x**2) + (1, 0, 0, 0), # Test case 1: Function(1) + (lambda x: x, 0, 1, 0), # Test case 2: Function(lambda x: x) + (lambda x: x**2, 1, 2, 2), # Test case 3: Function(lambda x: x**2) + (lambda x: (x**3 - 3*x**2 + 2*x + 10), 2, 22, 6), # Test case 4 ], ) def test_differentiate_complex_step(func_input, derivative_input, expected_derivative): @@ -104,6 +105,10 @@ def test_differentiate_complex_step(func_input, derivative_input, expected_deriv assert np.isclose( func.differentiate_complex_step(derivative_input), expected_derivative ) + assert np.isclose( + func.differentiate_complex_step(derivative_input, order=2), expected_derivative + ) + def test_get_value(): From 037fc69b4a91019a6c5ecd070220280fd4f616c7 Mon Sep 17 00:00:00 2001 From: Lint Action Date: Wed, 8 May 2024 06:13:13 +0000 Subject: [PATCH 8/9] Fix code style issues with Black --- tests/unit/test_function.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_function.py b/tests/unit/test_function.py index c3671dc91..faeeefe6d 100644 --- a/tests/unit/test_function.py +++ b/tests/unit/test_function.py @@ -83,9 +83,9 @@ def test_differentiate(func_input, derivative_input, expected_derivative): "func_input, derivative_input, expected_first_derivative, expected_second_derivative", [ (1, 0, 0, 0), # Test case 1: Function(1) - (lambda x: x, 0, 1, 0), # Test case 2: Function(lambda x: x) + (lambda x: x, 0, 1, 0), # Test case 2: Function(lambda x: x) (lambda x: x**2, 1, 2, 2), # Test case 3: Function(lambda x: x**2) - (lambda x: (x**3 - 3*x**2 + 2*x + 10), 2, 22, 6), # Test case 4 + (lambda x: (x**3 - 3 * x**2 + 2 * x + 10), 2, 22, 6), # Test case 4 ], ) def test_differentiate_complex_step(func_input, derivative_input, expected_derivative): @@ -110,7 +110,6 @@ def test_differentiate_complex_step(func_input, derivative_input, expected_deriv ) - def test_get_value(): """Tests the get_value method of the Function class. Both with respect to return instances and expected behaviour. From d20e7f7f4552ff8d6294cd2137eae3c6d202fe1d Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Wed, 8 May 2024 02:37:18 -0400 Subject: [PATCH 9/9] MNT: rollback second order derivative complex step (not working) --- rocketpy/mathutils/function.py | 14 ++++---------- tests/unit/test_function.py | 23 +++++++++++------------ 2 files changed, 15 insertions(+), 22 deletions(-) diff --git a/rocketpy/mathutils/function.py b/rocketpy/mathutils/function.py index ca7d1e093..9b95403ad 100644 --- a/rocketpy/mathutils/function.py +++ b/rocketpy/mathutils/function.py @@ -2440,8 +2440,8 @@ def differentiate_complex_step(self, x, dx=1e-200, order=1): dx : float, optional Step size to use for numerical differentiation, by default 1e-200. order : int, optional - Order of differentiation, by default 1. Right now, only first and - second order derivatives are supported. + Order of differentiation, by default 1. Right now, only first order + derivative is supported. Returns ------- @@ -2454,16 +2454,10 @@ def differentiate_complex_step(self, x, dx=1e-200, order=1): """ if order == 1: return float(self.get_value_opt(x + dx * 1j).imag / dx) - elif order == 2: - return float( - 2 - * (self.get_value_opt(x).real - self.get_value_opt(x + dx * 1j).real) - / dx**2 - ) else: raise NotImplementedError( - "Only 1st and 2nd order derivatives are supported. " - "Set order=1 or order=2." + "Only 1st order derivatives are supported yet. " + "Set order=1." ) def identity_function(self): diff --git a/tests/unit/test_function.py b/tests/unit/test_function.py index c3671dc91..a110520e4 100644 --- a/tests/unit/test_function.py +++ b/tests/unit/test_function.py @@ -80,15 +80,17 @@ def test_differentiate(func_input, derivative_input, expected_derivative): @pytest.mark.parametrize( - "func_input, derivative_input, expected_first_derivative, expected_second_derivative", + "func_input, derivative_input, expected_first_derivative", [ - (1, 0, 0, 0), # Test case 1: Function(1) - (lambda x: x, 0, 1, 0), # Test case 2: Function(lambda x: x) - (lambda x: x**2, 1, 2, 2), # Test case 3: Function(lambda x: x**2) - (lambda x: (x**3 - 3*x**2 + 2*x + 10), 2, 22, 6), # Test case 4 + (1, 0, 0), # Test case 1: Function(1) + (lambda x: x, 0, 1), # Test case 2: Function(lambda x: x) + (lambda x: x**2, 1, 2), # Test case 3: Function(lambda x: x**2) + (lambda x: -x**3, 2, -12), # Test case 4: Function(lambda x: -x**3) ], ) -def test_differentiate_complex_step(func_input, derivative_input, expected_derivative): +def test_differentiate_complex_step( + func_input, derivative_input, expected_first_derivative +): """Test the differentiate_complex_step method of the Function class. Parameters @@ -101,14 +103,11 @@ def test_differentiate_complex_step(func_input, derivative_input, expected_deriv Expected value of the derivative. """ func = Function(func_input) - assert isinstance(func.differentiate_complex_step(derivative_input), float) + assert isinstance(func.differentiate_complex_step(x=derivative_input), float) assert np.isclose( - func.differentiate_complex_step(derivative_input), expected_derivative + func.differentiate_complex_step(x=derivative_input, order=1), + expected_first_derivative, ) - assert np.isclose( - func.differentiate_complex_step(derivative_input, order=2), expected_derivative - ) - def test_get_value():