Skip to content

Commit fba6c8c

Browse files
Merge pull request #594 from RocketPy-Team/enh/complex-step-differentiation
ENH: Complex step differentiation
2 parents dc99084 + fc6804c commit fba6c8c

File tree

4 files changed

+80
-2
lines changed

4 files changed

+80
-2
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
3232

3333
### Added
3434

35+
- ENH: Complex step differentiation [#594](https://github.com/RocketPy-Team/RocketPy/pull/594)
3536
- ENH: Exponential backoff decorator (fix #449) [#588](https://github.com/RocketPy-Team/RocketPy/pull/588)
3637
- ENH: Function Validation Rework & Swap `np.searchsorted` to `bisect_left` [#582](https://github.com/RocketPy-Team/RocketPy/pull/582)
3738
- ENH: Add new stability margin properties to Flight class [#572](https://github.com/RocketPy-Team/RocketPy/pull/572)

docs/user/function.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,7 @@ d. Differentiation and Integration
346346
One of the most useful ``Function`` features for data analysis is easily differentiating and integrating the data source. These methods are divided as follow:
347347

348348
- :meth:`rocketpy.Function.differentiate`: differentiate the ``Function`` at a given point, returning the derivative value as the result;
349+
- :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;
349350
- :meth:`rocketpy.Function.integral`: performs a definite integral over specified limits, returns the integral value (area under ``Function``);
350351
- :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;
351352
- :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
363364
# Differentiate it at x = 3
364365
print(f.differentiate(3))
365366

367+
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:
368+
369+
.. jupyter-execute::
370+
371+
# Define the function x^2
372+
f = Function(lambda x: x**2)
373+
374+
# Differentiate it at x = 3 using the complex step method
375+
print(f.differentiate_complex_step(3))
376+
377+
The complex step method can be as twice as faster as the standard method, but
378+
it requires the function to be differentiable in the complex plane.
379+
366380
Also one may compute the derivative function:
367381

368382
.. jupyter-execute::

rocketpy/mathutils/function.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -860,7 +860,7 @@ def get_value(self, *args):
860860
# if the function is 1-D:
861861
if self.__dom_dim__ == 1:
862862
# if the args is a simple number (int or float)
863-
if isinstance(args[0], (int, float)):
863+
if isinstance(args[0], (int, float, complex)):
864864
return self.source(args[0])
865865
# if the arguments are iterable, we map and return a list
866866
if isinstance(args[0], Iterable):
@@ -869,7 +869,7 @@ def get_value(self, *args):
869869
# if the function is n-D:
870870
else:
871871
# if each arg is a simple number (int or float)
872-
if all(isinstance(arg, (int, float)) for arg in args):
872+
if all(isinstance(arg, (int, float, complex)) for arg in args):
873873
return self.source(*args)
874874
# if each arg is iterable, we map and return a list
875875
if all(isinstance(arg, Iterable) for arg in args):
@@ -2427,6 +2427,38 @@ def differentiate(self, x, dx=1e-6, order=1):
24272427
+ self.get_value_opt(x - dx)
24282428
) / dx**2
24292429

2430+
def differentiate_complex_step(self, x, dx=1e-200, order=1):
2431+
"""Differentiate a Function object at a given point using the complex
2432+
step method. This method can be faster than ``Function.differentiate``
2433+
since it requires only one evaluation of the function. However, the
2434+
evaluated function must accept complex numbers as input.
2435+
2436+
Parameters
2437+
----------
2438+
x : float
2439+
Point at which to differentiate.
2440+
dx : float, optional
2441+
Step size to use for numerical differentiation, by default 1e-200.
2442+
order : int, optional
2443+
Order of differentiation, by default 1. Right now, only first order
2444+
derivative is supported.
2445+
2446+
Returns
2447+
-------
2448+
float
2449+
The real part of the derivative of the function at the given point.
2450+
2451+
References
2452+
----------
2453+
[1] https://mdolab.engin.umich.edu/wiki/guide-complex-step-derivative-approximation
2454+
"""
2455+
if order == 1:
2456+
return float(self.get_value_opt(x + dx * 1j).imag / dx)
2457+
else:
2458+
raise NotImplementedError(
2459+
"Only 1st order derivatives are supported yet. " "Set order=1."
2460+
)
2461+
24302462
def identity_function(self):
24312463
"""Returns a Function object that correspond to the identity mapping,
24322464
i.e. f(x) = x.

tests/unit/test_function.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,37 @@ def test_differentiate(func_input, derivative_input, expected_derivative):
7979
assert np.isclose(func.differentiate(derivative_input), expected_derivative)
8080

8181

82+
@pytest.mark.parametrize(
83+
"func_input, derivative_input, expected_first_derivative",
84+
[
85+
(1, 0, 0), # Test case 1: Function(1)
86+
(lambda x: x, 0, 1), # Test case 2: Function(lambda x: x)
87+
(lambda x: x**2, 1, 2), # Test case 3: Function(lambda x: x**2)
88+
(lambda x: -(x**3), 2, -12), # Test case 4: Function(lambda x: -x**3)
89+
],
90+
)
91+
def test_differentiate_complex_step(
92+
func_input, derivative_input, expected_first_derivative
93+
):
94+
"""Test the differentiate_complex_step method of the Function class.
95+
96+
Parameters
97+
----------
98+
func_input : function
99+
A function object created from a list of values.
100+
derivative_input : int
101+
Point at which to differentiate.
102+
expected_derivative : float
103+
Expected value of the derivative.
104+
"""
105+
func = Function(func_input)
106+
assert isinstance(func.differentiate_complex_step(x=derivative_input), float)
107+
assert np.isclose(
108+
func.differentiate_complex_step(x=derivative_input, order=1),
109+
expected_first_derivative,
110+
)
111+
112+
82113
def test_get_value():
83114
"""Tests the get_value method of the Function class.
84115
Both with respect to return instances and expected behaviour.

0 commit comments

Comments
 (0)