diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d0506df6..311189757 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/docs/conf.py b/docs/conf.py index 9331a2f41..9a056cbcc 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -24,7 +24,7 @@ author = "RocketPy Team" # The full version, including alpha/beta/rc tags -release = "1.1.1" +release = "1.1.2" # -- General configuration --------------------------------------------------- diff --git a/docs/user/installation.rst b/docs/user/installation.rst index 8454156ca..1945af799 100644 --- a/docs/user/installation.rst +++ b/docs/user/installation.rst @@ -19,7 +19,7 @@ If you want to choose a specific version to guarantee compatibility, you may ins .. code-block:: shell - pip install rocketpy==1.1.1 + pip install rocketpy==1.1.2 Optional Installation Method: ``conda`` diff --git a/docs/user/motors/liquidmotor.rst b/docs/user/motors/liquidmotor.rst index 5a1b4e8d5..401ab62c6 100644 --- a/docs/user/motors/liquidmotor.rst +++ b/docs/user/motors/liquidmotor.rst @@ -40,7 +40,7 @@ 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( @@ -48,7 +48,7 @@ Then we must first define the tanks: 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, diff --git a/rocketpy/mathutils/function.py b/rocketpy/mathutils/function.py index f07960f6c..e52fbb2fb 100644 --- a/rocketpy/mathutils/function.py +++ b/rocketpy/mathutils/function.py @@ -39,14 +39,26 @@ def __init__( 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 : callable, scalar, ndarray, string, or Function + The data source to be used for the function: + + - Callable: Called for evaluation with input values. Must have the + desired inputs as arguments and return a single output value. + Input order is important. Example: Python functions, classes, and + methods. + + - int or float: Treated as a constant value function. + + - 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. + 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. @@ -74,6 +86,13 @@ def __init__( Returns ------- None + + 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. """ # Set input and output if inputs is None: @@ -133,25 +152,42 @@ def set_outputs(self, outputs): 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 : callable, scalar, ndarray, string, or Function + The data source to be used for the function: + + - Callable: Called for evaluation with input values. Must have the + desired inputs as arguments and return a single output value. + Input order is important. Example: Python functions, classes, and + methods. + + - int or float: Treated as a constant value function. + + - 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, @@ -165,20 +201,17 @@ def set_source(self, source): 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: + try: + source = np.loadtxt(file, delimiter=",", dtype=float) + except ValueError: + # If an error occurs, headers are present + source = np.loadtxt(source, delimiter=",", dtype=float, skiprows=1) + except Exception as e: + raise ValueError( + "The source file is not a valid csv or txt file." + ) from e + # Convert to ndarray if source is a list if isinstance(source, (list, tuple)): source = np.array(source, dtype=np.float64) @@ -2830,7 +2863,15 @@ def _check_user_input( # 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( + "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 diff --git a/setup.py b/setup.py index 85e184a02..4632bd009 100644 --- a/setup.py +++ b/setup.py @@ -23,7 +23,7 @@ setuptools.setup( name="rocketpy", - version="1.1.1", + version="1.1.2", install_requires=necessary_require, extras_require={ "env_analysis": env_analysis_require, diff --git a/tests/fixtures/function/1d_no_quotes.csv b/tests/fixtures/function/1d_no_quotes.csv new file mode 100644 index 000000000..239f38654 --- /dev/null +++ b/tests/fixtures/function/1d_no_quotes.csv @@ -0,0 +1,4 @@ +time,value +0,100 +1,200 +2,300 \ No newline at end of file diff --git a/tests/fixtures/function/1d_quotes.csv b/tests/fixtures/function/1d_quotes.csv new file mode 100644 index 000000000..0779c8c74 --- /dev/null +++ b/tests/fixtures/function/1d_quotes.csv @@ -0,0 +1,4 @@ +"time","value" +0,100 +1,200 +2,300 \ No newline at end of file diff --git a/tests/test_function.py b/tests/test_function.py index 1a5b4f189..c67a21b30 100644 --- a/tests/test_function.py +++ b/tests/test_function.py @@ -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.