diff --git a/CHANGELOG.md b/CHANGELOG.md index 311189757..0efb2f8fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,11 +36,12 @@ straightforward as possible. ### Changed -- +- MNT: Add repr method to Parachute class [#490](https://github.com/RocketPy-Team/RocketPy/pull/490) +- ENH: Function Reverse Arithmetic Priority [#488](https://github.com/RocketPy-Team/RocketPy/pull/488) ### Fixed -- +- ENH: Parachute trigger doesn't work if "Apogee" is used instead of "apogee" [#489](https://github.com/RocketPy-Team/RocketPy/pull/489) ## [v1.1.2] - 2023-11-25 diff --git a/CODEOWNERS b/CODEOWNERS index 177b566ec..912580dc7 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,3 +1,4 @@ # These owners will be the default owners for everything in # the repo. -* @giovaniceotto +* @RocketPy-Team/code-owners + diff --git a/docs/development/style_guide.rst b/docs/development/style_guide.rst index b77e4e06b..fd2f12365 100644 --- a/docs/development/style_guide.rst +++ b/docs/development/style_guide.rst @@ -69,6 +69,7 @@ So here are a couple of **guidelines** to help you when creating new branches to #. ``enh``: when you add new features and enhancements #. ``maint``: when your branch is all about refactoring, fixing typos, etc. #. ``rel``: when your branch makes changes related to creating new releases + #. ``tst``: when your branch makes changes related to tests * Use ``-`` instead of spaces for the description text. * Keep branch names with lowercase letters. @@ -80,6 +81,7 @@ Here are a couple of example branch names: - ``bug/issue-98-upside-down-rockets`` - ``enh/hybrid-motor-feature`` - ``maint/typos-flight-class`` +- ``tst/refactor-tests-flight-class`` Once you are ready to create a Pull Request for your branch, we advise you to merge with the ``develop`` branch instead of the default ``master`` branch. This way, we keep the ``master`` branch stable and use the ``develop`` branch to test out new features! diff --git a/rocketpy/mathutils/function.py b/rocketpy/mathutils/function.py index e52fbb2fb..8bdad9d2b 100644 --- a/rocketpy/mathutils/function.py +++ b/rocketpy/mathutils/function.py @@ -24,6 +24,9 @@ class Function: extrapolation, plotting and algebra. """ + # Arithmetic priority + __array_ufunc__ = None + def __init__( self, source, @@ -1837,7 +1840,9 @@ def __add__(self, other): return Function(lambda x: (self.get_value(x) + other(x))) # If other is Float except... except AttributeError: - if isinstance(other, (float, int, complex)): + if isinstance( + other, (float, int, complex, np.ndarray, np.integer, np.floating) + ): # Check if Function object source is array or callable if isinstance(self.source, np.ndarray): # Operate on grid values @@ -1967,7 +1972,9 @@ def __mul__(self, other): return Function(lambda x: (self.get_value(x) * other(x))) # If other is Float except... except AttributeError: - if isinstance(other, (float, int, complex)): + if isinstance( + other, (float, int, complex, np.ndarray, np.integer, np.floating) + ): # Check if Function object source is array or callable if isinstance(self.source, np.ndarray): # Operate on grid values @@ -2056,7 +2063,9 @@ def __truediv__(self, other): return Function(lambda x: (self.get_value_opt(x) / other(x))) # If other is Float except... except AttributeError: - if isinstance(other, (float, int, complex)): + if isinstance( + other, (float, int, complex, np.ndarray, np.integer, np.floating) + ): # Check if Function object source is array or callable if isinstance(self.source, np.ndarray): # Operate on grid values @@ -2095,7 +2104,9 @@ def __rtruediv__(self, other): A Function object which gives the result of other(x)/self(x). """ # Check if Function object source is array and other is float - if isinstance(other, (float, int, complex)): + if isinstance( + other, (float, int, complex, np.ndarray, np.integer, np.floating) + ): if isinstance(self.source, np.ndarray): # Operate on grid values ys = other / self.y_array @@ -2163,7 +2174,9 @@ def __pow__(self, other): return Function(lambda x: (self.get_value_opt(x) ** other(x))) # If other is Float except... except AttributeError: - if isinstance(other, (float, int, complex)): + if isinstance( + other, (float, int, complex, np.ndarray, np.integer, np.floating) + ): # Check if Function object source is array or callable if isinstance(self.source, np.ndarray): # Operate on grid values @@ -2202,7 +2215,9 @@ def __rpow__(self, other): A Function object which gives the result of other(x)**self(x). """ # Check if Function object source is array and other is float - if isinstance(other, (float, int, complex)): + if isinstance( + other, (float, int, complex, np.ndarray, np.integer, np.floating) + ): if isinstance(self.source, np.ndarray): # Operate on grid values ys = other**self.y_array diff --git a/rocketpy/rocket/parachute.py b/rocketpy/rocket/parachute.py index 07c44e158..31f9252ed 100644 --- a/rocketpy/rocket/parachute.py +++ b/rocketpy/rocket/parachute.py @@ -38,7 +38,7 @@ class Parachute: case, the parachute will be ejected when the rocket reaches this height above ground level. - - The string "apogee," which triggers the parachute at apogee, i.e., + - The string "apogee" which triggers the parachute at apogee, i.e., when the rocket reaches its highest point and starts descending. Note: The function will be called according to the sampling rate @@ -126,7 +126,7 @@ def __init__( case, the parachute will be ejected when the rocket reaches this height above ground level. - - The string "apogee," which triggers the parachute at apogee, i.e., + - The string "apogee" which triggers the parachute at apogee, i.e., when the rocket reaches its highest point and starts descending. Note: The function will be called according to the sampling rate @@ -171,11 +171,17 @@ def __init__( self.prints = _ParachutePrints(self) - # evaluate the trigger + self.__evaluate_trigger_function(trigger) + + def __evaluate_trigger_function(self, trigger): + """This is used to set the triggerfunc attribute that will be used to + interact with the Flight class. + """ if callable(trigger): self.triggerfunc = trigger + elif isinstance(trigger, (int, float)): - # trigger is interpreted as the absolute height at which the parachute will be ejected + # The parachute is deployed at a given height def triggerfunc(p, h, y): # p = pressure considering parachute noise signal # h = height above ground level considering parachute noise signal @@ -184,8 +190,8 @@ def triggerfunc(p, h, y): self.triggerfunc = triggerfunc - elif trigger == "apogee": - # trigger for apogee + elif trigger.lower() == "apogee": + # The parachute is deployed at apogee def triggerfunc(p, h, y): # p = pressure considering parachute noise signal # h = height above ground level considering parachute noise signal @@ -194,7 +200,12 @@ def triggerfunc(p, h, y): self.triggerfunc = triggerfunc - return None + else: + raise ValueError( + f"Unable to set the trigger function for parachute '{self.name}'. " + + "Trigger must be a callable, a float value or the string 'apogee'. " + + "See the Parachute class documentation for more information." + ) def __str__(self): """Returns a string representation of the Parachute class. @@ -209,6 +220,13 @@ def __str__(self): self.cd_s, ) + def __repr__(self): + """Representation method for the class, useful when debugging.""" + return ( + f"" + ) + def info(self): """Prints information about the Parachute class.""" self.prints.all() diff --git a/rocketpy/rocket/rocket.py b/rocketpy/rocket/rocket.py index 947c82acd..62f61d99b 100644 --- a/rocketpy/rocket/rocket.py +++ b/rocketpy/rocket/rocket.py @@ -1092,7 +1092,7 @@ def add_parachute( case, the parachute will be ejected when the rocket reaches this height above ground level. - - The string "apogee," which triggers the parachute at apogee, i.e., + - The string "apogee" which triggers the parachute at apogee, i.e., when the rocket reaches its highest point and starts descending. Note: The function will be called according to the sampling rate diff --git a/tests/test_function.py b/tests/test_function.py index c67a21b30..5362b0486 100644 --- a/tests/test_function.py +++ b/tests/test_function.py @@ -390,3 +390,83 @@ def test_shepard_interpolation(x, y, z_expected): func = Function(source=source, inputs=["x", "y"], outputs=["z"]) z = func(x, y) assert np.isclose(z, z_expected, atol=1e-8).all() + + +@pytest.mark.parametrize("other", [1, 0.1, np.int_(1), np.float_(0.1), np.array([1])]) +def test_sum_arithmetic_priority(other): + """Test the arithmetic priority of the add operation of the Function class, + specially comparing to the numpy array operations. + """ + func_lambda = Function(lambda x: x**2) + func_array = Function([(0, 0), (1, 1), (2, 4)]) + + assert isinstance(func_lambda + func_array, Function) + assert isinstance(func_array + func_lambda, Function) + assert isinstance(func_lambda + other, Function) + assert isinstance(other + func_lambda, Function) + assert isinstance(func_array + other, Function) + assert isinstance(other + func_array, Function) + + +@pytest.mark.parametrize("other", [1, 0.1, np.int_(1), np.float_(0.1), np.array([1])]) +def test_sub_arithmetic_priority(other): + """Test the arithmetic priority of the sub operation of the Function class, + specially comparing to the numpy array operations. + """ + func_lambda = Function(lambda x: x**2) + func_array = Function([(0, 0), (1, 1), (2, 4)]) + + assert isinstance(func_lambda - func_array, Function) + assert isinstance(func_array - func_lambda, Function) + assert isinstance(func_lambda - other, Function) + assert isinstance(other - func_lambda, Function) + assert isinstance(func_array - other, Function) + assert isinstance(other - func_array, Function) + + +@pytest.mark.parametrize("other", [1, 0.1, np.int_(1), np.float_(0.1), np.array([1])]) +def test_mul_arithmetic_priority(other): + """Test the arithmetic priority of the mul operation of the Function class, + specially comparing to the numpy array operations. + """ + func_lambda = Function(lambda x: x**2) + func_array = Function([(0, 0), (1, 1), (2, 4)]) + + assert isinstance(func_lambda * func_array, Function) + assert isinstance(func_array * func_lambda, Function) + assert isinstance(func_lambda * other, Function) + assert isinstance(other * func_lambda, Function) + assert isinstance(func_array * other, Function) + assert isinstance(other * func_array, Function) + + +@pytest.mark.parametrize("other", [1, 0.1, np.int_(1), np.float_(0.1), np.array([1])]) +def test_truediv_arithmetic_priority(other): + """Test the arithmetic priority of the truediv operation of the Function class, + specially comparing to the numpy array operations. + """ + func_lambda = Function(lambda x: x**2) + func_array = Function([(1, 1), (2, 4)]) + + assert isinstance(func_lambda / func_array, Function) + assert isinstance(func_array / func_lambda, Function) + assert isinstance(func_lambda / other, Function) + assert isinstance(other / func_lambda, Function) + assert isinstance(func_array / other, Function) + assert isinstance(other / func_array, Function) + + +@pytest.mark.parametrize("other", [1, 0.1, np.int_(1), np.float_(0.1), np.array([1])]) +def test_pow_arithmetic_priority(other): + """Test the arithmetic priority of the pow operation of the Function class, + specially comparing to the numpy array operations. + """ + func_lambda = Function(lambda x: x**2) + func_array = Function([(0, 0), (1, 1), (2, 4)]) + + assert isinstance(func_lambda**func_array, Function) + assert isinstance(func_array**func_lambda, Function) + assert isinstance(func_lambda**other, Function) + assert isinstance(other**func_lambda, Function) + assert isinstance(func_array**other, Function) + assert isinstance(other**func_array, Function)