Skip to content

Commit a14e7e8

Browse files
Merge pull request #519 from RocketPy-Team/enh/optional-discretize-mutation
ENH: Argument for Optional Mutation on Function Discretize
2 parents fe90f77 + 2205fdf commit a14e7e8

File tree

3 files changed

+129
-41
lines changed

3 files changed

+129
-41
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ straightforward as possible.
3232

3333
### Added
3434

35-
-
35+
- ENH: Argument for Optional Mutation on Function Discretize [#519](https://github.com/RocketPy-Team/RocketPy/pull/519)
3636

3737
### Changed
3838

rocketpy/mathutils/function.py

Lines changed: 76 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import matplotlib.pyplot as plt
1212
import numpy as np
13+
from copy import deepcopy
1314
from scipy import integrate, linalg, optimize
1415

1516
try:
@@ -515,12 +516,16 @@ def set_discrete(
515516
interpolation="spline",
516517
extrapolation="constant",
517518
one_by_one=True,
519+
mutate_self=True,
518520
):
519-
"""This method transforms function defined Functions into list
520-
defined Functions. It evaluates the function at certain points
521-
(sampling range) and stores the results in a list, which is converted
522-
into a Function and then returned. The original Function object is
523-
replaced by the new one.
521+
"""This method discretizes a 1-D or 2-D Function by evaluating it at
522+
certain points (sampling range) and storing the results in a list,
523+
which is converted into a Function and then returned. By default, the
524+
original Function object is replaced by the new one, which can be
525+
changed by the attribute `mutate_self`.
526+
527+
This method is specially useful to change a dataset sampling or to
528+
convert a Function defined by a callable into a list based Function.
524529
525530
Parameters
526531
----------
@@ -544,18 +549,32 @@ def set_discrete(
544549
one_by_one : boolean, optional
545550
If True, evaluate Function in each sample point separately. If
546551
False, evaluates Function in vectorized form. Default is True.
552+
mutate_self : boolean, optional
553+
If True, the original Function object source will be replaced by
554+
the new one. If False, the original Function object source will
555+
remain unchanged, and the new one is simply returned.
556+
Default is True.
547557
548558
Returns
549559
-------
550560
self : Function
561+
562+
Notes
563+
-----
564+
1. This method performs by default in place replacement of the original
565+
Function object source. This can be changed by the attribute `mutate_self`.
566+
567+
2. Currently, this method only supports 1-D and 2-D Functions.
551568
"""
552-
if self.__dom_dim__ == 1:
569+
func = deepcopy(self) if not mutate_self else self
570+
571+
if func.__dom_dim__ == 1:
553572
xs = np.linspace(lower, upper, samples)
554-
ys = self.get_value(xs.tolist()) if one_by_one else self.get_value(xs)
555-
self.set_source(np.concatenate(([xs], [ys])).transpose())
556-
self.set_interpolation(interpolation)
557-
self.set_extrapolation(extrapolation)
558-
elif self.__dom_dim__ == 2:
573+
ys = func.get_value(xs.tolist()) if one_by_one else func.get_value(xs)
574+
func.set_source(np.concatenate(([xs], [ys])).transpose())
575+
func.set_interpolation(interpolation)
576+
func.set_extrapolation(extrapolation)
577+
elif func.__dom_dim__ == 2:
559578
lower = 2 * [lower] if isinstance(lower, (int, float)) else lower
560579
upper = 2 * [upper] if isinstance(upper, (int, float)) else upper
561580
sam = 2 * [samples] if isinstance(samples, (int, float)) else samples
@@ -564,22 +583,29 @@ def set_discrete(
564583
ys = np.linspace(lower[1], upper[1], sam[1])
565584
xs, ys = np.array(np.meshgrid(xs, ys)).reshape(2, xs.size * ys.size)
566585
# Evaluate function at all mesh nodes and convert it to matrix
567-
zs = np.array(self.get_value(xs, ys))
568-
self.__interpolation__ = "shepard"
569-
self.__extrapolation__ = "natural"
570-
self.set_source(np.concatenate(([xs], [ys], [zs])).transpose())
571-
return self
586+
zs = np.array(func.get_value(xs, ys))
587+
func.set_source(np.concatenate(([xs], [ys], [zs])).transpose())
588+
func.__interpolation__ = "shepard"
589+
func.__extrapolation__ = "natural"
590+
else:
591+
raise ValueError(
592+
"Discretization is only supported for 1-D and 2-D Functions."
593+
)
594+
return func
572595

573596
def set_discrete_based_on_model(
574-
self, model_function, one_by_one=True, keep_self=True
597+
self, model_function, one_by_one=True, keep_self=True, mutate_self=True
575598
):
576-
"""This method transforms the domain of Function instance into a list of
577-
discrete points based on the domain of a model Function instance. It
578-
does so by retrieving the domain, domain name, interpolation method and
579-
extrapolation method of the model Function instance. It then evaluates
580-
the original Function instance in all points of the retrieved domain to
581-
generate the list of discrete points that will be used for interpolation
582-
when this Function is called.
599+
"""This method transforms the domain of a 1-D or 2-D Function instance
600+
into a list of discrete points based on the domain of a model Function
601+
instance. It does so by retrieving the domain, domain name,
602+
interpolation method and extrapolation method of the model Function
603+
instance. It then evaluates the original Function instance in all
604+
points of the retrieved domain to generate the list of discrete points
605+
that will be used for interpolation when this Function is called.
606+
607+
By default, the original Function object is replaced by the new one,
608+
which can be changed by the attribute `mutate_self`.
583609
584610
Parameters
585611
----------
@@ -589,15 +615,17 @@ def set_discrete_based_on_model(
589615
Must be a Function whose source attribute is a list (i.e. a list
590616
based Function instance). Must have the same domain dimension as the
591617
Function to be discretized.
592-
593618
one_by_one : boolean, optional
594619
If True, evaluate Function in each sample point separately. If
595620
False, evaluates Function in vectorized form. Default is True.
596-
597-
keepSelf : boolean, optional
621+
keep_self : boolean, optional
598622
If True, the original Function interpolation and extrapolation
599623
methods will be kept. If False, those are substituted by the ones
600624
from the model Function. Default is True.
625+
mutate_self : boolean, optional
626+
If True, the original Function object source will be replaced by
627+
the new one. If False, the original Function object source will
628+
remain unchanged, and the new one is simply returned.
601629
602630
Returns
603631
-------
@@ -645,40 +673,48 @@ def set_discrete_based_on_model(
645673
646674
Notes
647675
-----
648-
1. This method performs in place replacement of the original Function
649-
object source.
676+
1. This method performs by default in place replacement of the original
677+
Function object source. This can be changed by the attribute `mutate_self`.
650678
651679
2. This method is similar to set_discrete, but it uses the domain of a
652680
model Function to define the domain of the new Function instance.
681+
682+
3. Currently, this method only supports 1-D and 2-D Functions.
653683
"""
654684
if not isinstance(model_function.source, np.ndarray):
655685
raise TypeError("model_function must be a list based Function.")
656686
if model_function.__dom_dim__ != self.__dom_dim__:
657687
raise ValueError("model_function must have the same domain dimension.")
658688

659-
if self.__dom_dim__ == 1:
689+
func = deepcopy(self) if not mutate_self else self
690+
691+
if func.__dom_dim__ == 1:
660692
xs = model_function.source[:, 0]
661-
ys = self.get_value(xs.tolist()) if one_by_one else self.get_value(xs)
662-
self.set_source(np.concatenate(([xs], [ys])).transpose())
663-
elif self.__dom_dim__ == 2:
693+
ys = func.get_value(xs.tolist()) if one_by_one else func.get_value(xs)
694+
func.set_source(np.concatenate(([xs], [ys])).transpose())
695+
elif func.__dom_dim__ == 2:
664696
# Create nodes to evaluate function
665697
xs = model_function.source[:, 0]
666698
ys = model_function.source[:, 1]
667699
# Evaluate function at all mesh nodes and convert it to matrix
668-
zs = np.array(self.get_value(xs, ys))
669-
self.set_source(np.concatenate(([xs], [ys], [zs])).transpose())
700+
zs = np.array(func.get_value(xs, ys))
701+
func.set_source(np.concatenate(([xs], [ys], [zs])).transpose())
702+
else:
703+
raise ValueError(
704+
"Discretization is only supported for 1-D and 2-D Functions."
705+
)
670706

671707
interp = (
672-
self.__interpolation__ if keep_self else model_function.__interpolation__
708+
func.__interpolation__ if keep_self else model_function.__interpolation__
673709
)
674710
extrap = (
675-
self.__extrapolation__ if keep_self else model_function.__extrapolation__
711+
func.__extrapolation__ if keep_self else model_function.__extrapolation__
676712
)
677713

678-
self.set_interpolation(interp)
679-
self.set_extrapolation(extrap)
714+
func.set_interpolation(interp)
715+
func.set_extrapolation(extrap)
680716

681-
return self
717+
return func
682718

683719
def reset(
684720
self,

tests/unit/test_function.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,3 +158,55 @@ def test_get_value_opt(x, y, z):
158158
func = Function(source, interpolation="shepard", extrapolation="natural")
159159
assert isinstance(func.get_value_opt(x, y), float)
160160
assert np.isclose(func.get_value_opt(x, y), z, atol=1e-6)
161+
162+
163+
@pytest.mark.parametrize("samples", [2, 50, 1000])
164+
def test_set_discrete_mutator(samples):
165+
"""Tests the set_discrete method of the Function class."""
166+
func = Function(lambda x: x**3)
167+
discretized_func = func.set_discrete(-10, 10, samples, mutate_self=True)
168+
169+
assert isinstance(discretized_func, Function)
170+
assert isinstance(func, Function)
171+
assert discretized_func.source.shape == (samples, 2)
172+
assert func.source.shape == (samples, 2)
173+
174+
175+
@pytest.mark.parametrize("samples", [2, 50, 1000])
176+
def test_set_discrete_non_mutator(samples):
177+
"""Tests the set_discrete method of the Function class.
178+
The mutator argument is set to False.
179+
"""
180+
func = Function(lambda x: x**3)
181+
discretized_func = func.set_discrete(-10, 10, samples, mutate_self=False)
182+
183+
assert isinstance(discretized_func, Function)
184+
assert isinstance(func, Function)
185+
assert discretized_func.source.shape == (samples, 2)
186+
assert callable(func.source)
187+
188+
189+
def test_set_discrete_based_on_model_mutator(linear_func):
190+
"""Tests the set_discrete_based_on_model method of the Function class.
191+
The mutator argument is set to True.
192+
"""
193+
func = Function(lambda x: x**3)
194+
discretized_func = func.set_discrete_based_on_model(linear_func, mutate_self=True)
195+
196+
assert isinstance(discretized_func, Function)
197+
assert isinstance(func, Function)
198+
assert discretized_func.source.shape == (4, 2)
199+
assert func.source.shape == (4, 2)
200+
201+
202+
def test_set_discrete_based_on_model_non_mutator(linear_func):
203+
"""Tests the set_discrete_based_on_model method of the Function class.
204+
The mutator argument is set to False.
205+
"""
206+
func = Function(lambda x: x**3)
207+
discretized_func = func.set_discrete_based_on_model(linear_func, mutate_self=False)
208+
209+
assert isinstance(discretized_func, Function)
210+
assert isinstance(func, Function)
211+
assert discretized_func.source.shape == (4, 2)
212+
assert callable(func.source)

0 commit comments

Comments
 (0)