Skip to content

Commit 1de49c9

Browse files
Merge branch 'develop' into enh/shepard-multiple-opt
2 parents 110ef9e + a14e7e8 commit 1de49c9

File tree

3 files changed

+129
-40
lines changed

3 files changed

+129
-40
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ straightforward as possible.
3333
### Added
3434

3535
- ENH: Shepard Optimized Interpolation - Multiple Inputs Support [#515](https://github.com/RocketPy-Team/RocketPy/pull/515)
36+
- ENH: Argument for Optional Mutation on Function Discretize [#519](https://github.com/RocketPy-Team/RocketPy/pull/519)
3637

3738
### Changed
3839

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:
@@ -499,12 +500,16 @@ def set_discrete(
499500
interpolation="spline",
500501
extrapolation="constant",
501502
one_by_one=True,
503+
mutate_self=True,
502504
):
503-
"""This method transforms function defined Functions into list
504-
defined Functions. It evaluates the function at certain points
505-
(sampling range) and stores the results in a list, which is converted
506-
into a Function and then returned. The original Function object is
507-
replaced by the new one.
505+
"""This method discretizes a 1-D or 2-D Function by evaluating it at
506+
certain points (sampling range) and storing the results in a list,
507+
which is converted into a Function and then returned. By default, the
508+
original Function object is replaced by the new one, which can be
509+
changed by the attribute `mutate_self`.
510+
511+
This method is specially useful to change a dataset sampling or to
512+
convert a Function defined by a callable into a list based Function.
508513
509514
Parameters
510515
----------
@@ -528,18 +533,32 @@ def set_discrete(
528533
one_by_one : boolean, optional
529534
If True, evaluate Function in each sample point separately. If
530535
False, evaluates Function in vectorized form. Default is True.
536+
mutate_self : boolean, optional
537+
If True, the original Function object source will be replaced by
538+
the new one. If False, the original Function object source will
539+
remain unchanged, and the new one is simply returned.
540+
Default is True.
531541
532542
Returns
533543
-------
534544
self : Function
545+
546+
Notes
547+
-----
548+
1. This method performs by default in place replacement of the original
549+
Function object source. This can be changed by the attribute `mutate_self`.
550+
551+
2. Currently, this method only supports 1-D and 2-D Functions.
535552
"""
536-
if self.__dom_dim__ == 1:
553+
func = deepcopy(self) if not mutate_self else self
554+
555+
if func.__dom_dim__ == 1:
537556
xs = np.linspace(lower, upper, samples)
538-
ys = self.get_value(xs.tolist()) if one_by_one else self.get_value(xs)
539-
self.set_source(np.concatenate(([xs], [ys])).transpose())
540-
self.set_interpolation(interpolation)
541-
self.set_extrapolation(extrapolation)
542-
elif self.__dom_dim__ == 2:
557+
ys = func.get_value(xs.tolist()) if one_by_one else func.get_value(xs)
558+
func.set_source(np.concatenate(([xs], [ys])).transpose())
559+
func.set_interpolation(interpolation)
560+
func.set_extrapolation(extrapolation)
561+
elif func.__dom_dim__ == 2:
543562
lower = 2 * [lower] if isinstance(lower, (int, float)) else lower
544563
upper = 2 * [upper] if isinstance(upper, (int, float)) else upper
545564
sam = 2 * [samples] if isinstance(samples, (int, float)) else samples
@@ -548,22 +567,29 @@ def set_discrete(
548567
ys = np.linspace(lower[1], upper[1], sam[1])
549568
xs, ys = np.array(np.meshgrid(xs, ys)).reshape(2, xs.size * ys.size)
550569
# Evaluate function at all mesh nodes and convert it to matrix
551-
zs = np.array(self.get_value(xs, ys))
552-
self.__interpolation__ = "shepard"
553-
self.__extrapolation__ = "natural"
554-
self.set_source(np.concatenate(([xs], [ys], [zs])).transpose())
555-
return self
570+
zs = np.array(func.get_value(xs, ys))
571+
func.set_source(np.concatenate(([xs], [ys], [zs])).transpose())
572+
func.__interpolation__ = "shepard"
573+
func.__extrapolation__ = "natural"
574+
else:
575+
raise ValueError(
576+
"Discretization is only supported for 1-D and 2-D Functions."
577+
)
578+
return func
556579

557580
def set_discrete_based_on_model(
558-
self, model_function, one_by_one=True, keep_self=True
581+
self, model_function, one_by_one=True, keep_self=True, mutate_self=True
559582
):
560-
"""This method transforms the domain of Function instance into a list of
561-
discrete points based on the domain of a model Function instance. It
562-
does so by retrieving the domain, domain name, interpolation method and
563-
extrapolation method of the model Function instance. It then evaluates
564-
the original Function instance in all points of the retrieved domain to
565-
generate the list of discrete points that will be used for interpolation
566-
when this Function is called.
583+
"""This method transforms the domain of a 1-D or 2-D Function instance
584+
into a list of discrete points based on the domain of a model Function
585+
instance. It does so by retrieving the domain, domain name,
586+
interpolation method and extrapolation method of the model Function
587+
instance. It then evaluates the original Function instance in all
588+
points of the retrieved domain to generate the list of discrete points
589+
that will be used for interpolation when this Function is called.
590+
591+
By default, the original Function object is replaced by the new one,
592+
which can be changed by the attribute `mutate_self`.
567593
568594
Parameters
569595
----------
@@ -573,15 +599,17 @@ def set_discrete_based_on_model(
573599
Must be a Function whose source attribute is a list (i.e. a list
574600
based Function instance). Must have the same domain dimension as the
575601
Function to be discretized.
576-
577602
one_by_one : boolean, optional
578603
If True, evaluate Function in each sample point separately. If
579604
False, evaluates Function in vectorized form. Default is True.
580-
581-
keepSelf : boolean, optional
605+
keep_self : boolean, optional
582606
If True, the original Function interpolation and extrapolation
583607
methods will be kept. If False, those are substituted by the ones
584608
from the model Function. Default is True.
609+
mutate_self : boolean, optional
610+
If True, the original Function object source will be replaced by
611+
the new one. If False, the original Function object source will
612+
remain unchanged, and the new one is simply returned.
585613
586614
Returns
587615
-------
@@ -629,40 +657,48 @@ def set_discrete_based_on_model(
629657
630658
Notes
631659
-----
632-
1. This method performs in place replacement of the original Function
633-
object source.
660+
1. This method performs by default in place replacement of the original
661+
Function object source. This can be changed by the attribute `mutate_self`.
634662
635663
2. This method is similar to set_discrete, but it uses the domain of a
636664
model Function to define the domain of the new Function instance.
665+
666+
3. Currently, this method only supports 1-D and 2-D Functions.
637667
"""
638668
if not isinstance(model_function.source, np.ndarray):
639669
raise TypeError("model_function must be a list based Function.")
640670
if model_function.__dom_dim__ != self.__dom_dim__:
641671
raise ValueError("model_function must have the same domain dimension.")
642672

643-
if self.__dom_dim__ == 1:
673+
func = deepcopy(self) if not mutate_self else self
674+
675+
if func.__dom_dim__ == 1:
644676
xs = model_function.source[:, 0]
645-
ys = self.get_value(xs.tolist()) if one_by_one else self.get_value(xs)
646-
self.set_source(np.concatenate(([xs], [ys])).transpose())
647-
elif self.__dom_dim__ == 2:
677+
ys = func.get_value(xs.tolist()) if one_by_one else func.get_value(xs)
678+
func.set_source(np.concatenate(([xs], [ys])).transpose())
679+
elif func.__dom_dim__ == 2:
648680
# Create nodes to evaluate function
649681
xs = model_function.source[:, 0]
650682
ys = model_function.source[:, 1]
651683
# Evaluate function at all mesh nodes and convert it to matrix
652-
zs = np.array(self.get_value(xs, ys))
653-
self.set_source(np.concatenate(([xs], [ys], [zs])).transpose())
684+
zs = np.array(func.get_value(xs, ys))
685+
func.set_source(np.concatenate(([xs], [ys], [zs])).transpose())
686+
else:
687+
raise ValueError(
688+
"Discretization is only supported for 1-D and 2-D Functions."
689+
)
654690

655691
interp = (
656-
self.__interpolation__ if keep_self else model_function.__interpolation__
692+
func.__interpolation__ if keep_self else model_function.__interpolation__
657693
)
658694
extrap = (
659-
self.__extrapolation__ if keep_self else model_function.__extrapolation__
695+
func.__extrapolation__ if keep_self else model_function.__extrapolation__
660696
)
661697

662-
self.set_interpolation(interp)
663-
self.set_extrapolation(extrap)
698+
func.set_interpolation(interp)
699+
func.set_extrapolation(extrap)
664700

665-
return self
701+
return func
666702

667703
def reset(
668704
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)