Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BUG: Function breaks if a header is present in the csv file #485

Merged
merged 10 commits into from
Nov 27, 2023
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,18 @@ straightforward as possible.

-

## [v1.1.2] - 2023-11-25

You can install this version by running `pip install rocketpy==1.1.2`

### Fixed

- BUG: Function breaks if a header is present in the csv file [#485](https://github.com/RocketPy-Team/RocketPy/pull/485)

## [v1.1.1] - 2023-11-23

You can install this version by running `pip install rocketpy==1.1.1`


### Added

- DOC: Added this changelog file [#472](https://github.com/RocketPy-Team/RocketPy/pull/472)
Expand Down
4 changes: 2 additions & 2 deletions docs/user/motors/liquidmotor.rst
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,15 @@ Then we must first define the tanks:
fuel_gas = Fluid(name="ethanol_g", density=1.59)

# Define tanks geometry
tanks_shape = CylindricalTank(radius = 0.1, height = 1, spherical_caps = True)
tanks_shape = CylindricalTank(radius = 0.1, height = 1.2, spherical_caps = True)

# Define tanks
oxidizer_tank = MassFlowRateBasedTank(
name="oxidizer tank",
geometry=tanks_shape,
flux_time=5,
initial_liquid_mass=32,
initial_gas_mass=0.1,
initial_gas_mass=0.01,
liquid_mass_flow_rate_in=0,
liquid_mass_flow_rate_out=lambda t: 32 / 3 * exp(-0.25 * t),
gas_mass_flow_rate_in=0,
Expand Down
87 changes: 52 additions & 35 deletions rocketpy/mathutils/function.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,9 @@

Parameters
----------
source : function, scalar, ndarray, string
The actual function. If type is function, it will be called for
evaluation. If type is int or float, it will be treated as a
constant function. If ndarray, its points will be used for
interpolation. An ndarray should be as [(x0, y0, z0), (x1, y1, z1),
(x2, y2, z2), ...] where x0 and y0 are inputs and z0 is output. If
string, imports file named by the string and treats it as csv.
The file is converted into ndarray and should not have headers.
source : function, scalar, ndarray, string, Function
The data source to be used for the function. See the ``set_source``
method's documentation for more details.
MateusStano marked this conversation as resolved.
Show resolved Hide resolved
MateusStano marked this conversation as resolved.
Show resolved Hide resolved
inputs : string, sequence of strings, optional
The name of the inputs of the function. Will be used for
representation and graphing (axis names). 'Scalar' is default.
Expand Down Expand Up @@ -133,25 +128,39 @@
return self

def set_source(self, source):
"""Set the source which defines the output of the function giving a
certain input.
"""Sets the data source for the function, defining how the function
produces output from a given input.

Parameters
----------
source : function, scalar, ndarray, string, Function
The actual function. If type is function, it will be called for
evaluation. If type is int or float, it will be treated as a
constant function. If ndarray, its points will be used for
interpolation. An ndarray should be as [(x0, y0, z0), (x1, y1, z1),
(x2, y2, z2), ...] where x0 and y0 are inputs and z0 is output. If
string, imports file named by the string and treats it as csv.
The file is converted into ndarray and should not have headers.
If the source is a Function, its source will be copied and another
Function will be created following the new inputs and outputs.
source : function, scalar, ndarray, string, or Function
The data source to be used for the function:

- Python function: Called for evaluation with input values.
MateusStano marked this conversation as resolved.
Show resolved Hide resolved

- int or float: Treated as a constant value.
MateusStano marked this conversation as resolved.
Show resolved Hide resolved

- ndarray: Used for interpolation. Format as [(x0, y0, z0),
(x1, y1, z1), ..., (xn, yn, zn)], where 'x' and 'y' are inputs,
and 'z' is the output.

- string: Path to a CSV file. The file is read and converted into an
ndarray. The file can optionally contain a single header line.

- Function: Copies the source of the provided Function object,
creating a new Function with adjusted inputs and outputs.

Notes
-----
(I) CSV files can optionally contain a single header line. If present,
the header is ignored during processing.
(II) Fields in CSV files may be enclosed in double quotes. If fields are
not quoted, double quotes should not appear inside them.

Returns
-------
self : Function
Returns the Function instance.
"""
_ = self._check_user_input(
source,
Expand All @@ -165,20 +174,20 @@
source = source.get_source()
# Import CSV if source is a string or Path and convert values to ndarray
if isinstance(source, (str, Path)):
# Read file and check for headers
with open(source, mode="r") as f:
first_line = f.readline()
# If headers are found...
if first_line[0] in ['"', "'"]:
# Headers available
first_line = first_line.replace('"', " ").replace("'", " ")
first_line = first_line.split(" , ")
self.set_inputs(first_line[0])
self.set_outputs(first_line[1:])
source = np.loadtxt(source, delimiter=",", skiprows=1, dtype=float)
# if headers are not found
else:
source = np.loadtxt(source, delimiter=",", dtype=float)
with open(source, "r") as file:
first_line = file.readline()
try:
# Try to convert the first line to floats
np.array([float(item) for item in first_line.split(",")])
# If successful, no headers are present
file.seek(0) # Reset file read position
source = np.loadtxt(file, delimiter=",", dtype=float)
MateusStano marked this conversation as resolved.
Show resolved Hide resolved
except ValueError:
# If an error occurs, headers are present
source = np.genfromtxt(
source, delimiter=",", dtype=float, skip_header=1
)
MateusStano marked this conversation as resolved.
Show resolved Hide resolved

# Convert to ndarray if source is a list
if isinstance(source, (list, tuple)):
source = np.array(source, dtype=np.float64)
Expand Down Expand Up @@ -2830,7 +2839,15 @@
# Deal with csv or txt
if isinstance(source, (str, Path)):
# Convert to numpy array
source = np.loadtxt(source, delimiter=",", dtype=float)
try:
source = np.loadtxt(source, delimiter=",", dtype=float)
except ValueError:
# Skip header
source = np.loadtxt(source, delimiter=",", dtype=float, skiprows=1)
except Exception as e:
raise ValueError(

Check warning on line 2848 in rocketpy/mathutils/function.py

View check run for this annotation

Codecov / codecov/patch

rocketpy/mathutils/function.py#L2847-L2848

Added lines #L2847 - L2848 were not covered by tests
"The source file is not a valid csv or txt file."
) from e

else:
# this will also trigger an error if the source is not a list of
Expand Down
4 changes: 4 additions & 0 deletions tests/fixtures/function/1d_no_quotes.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
time,value
0,100
1,200
2,300
4 changes: 4 additions & 0 deletions tests/fixtures/function/1d_quotes.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"time","value"
0,100
1,200
2,300
17 changes: 17 additions & 0 deletions tests/test_function.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,23 @@ def test_function_from_csv(func_from_csv, func_2d_from_csv):
)


@pytest.mark.parametrize(
"csv_file",
[
"tests/fixtures/function/1d_quotes.csv",
"tests/fixtures/function/1d_no_quotes.csv",
],
)
def test_func_from_csv_with_header(csv_file):
"""Tests if a Function can be created from a CSV file with a single header
line. It tests cases where the fields are separated by quotes and without
quotes."""
f = Function(csv_file)
assert f.__repr__() == "'Function from R1 to R1 : (Scalar) → (Scalar)'"
assert np.isclose(f(0), 100)
assert np.isclose(f(0) + f(1), 300), "Error summing the values of the function"


def test_getters(func_from_csv, func_2d_from_csv):
"""Test the different getters of the Function class.

Expand Down
Loading