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

ENH: Function inputs from CSV file header. #542

Merged
merged 6 commits into from
Feb 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ straightforward as possible.

### Added

- ENH: Function Support for CSV Header Inputs [#542](https://github.com/RocketPy-Team/RocketPy/pull/542)
- ENH: Shepard Optimized Interpolation - Multiple Inputs Support [#515](https://github.com/RocketPy-Team/RocketPy/pull/515)
- ENH: adds new Function.savetxt method [#514](https://github.com/RocketPy-Team/RocketPy/pull/514)
- ENH: Argument for Optional Mutation on Function Discretize [#519](https://github.com/RocketPy-Team/RocketPy/pull/519)
Expand Down
8 changes: 4 additions & 4 deletions rocketpy/environment/environment_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -901,10 +901,10 @@ def __parse_surface_data(self):
# Extract data from weather file
indices = (time_index, lon_index, lat_index)
for key, value in surface_file_dict.items():
dictionary[date_string][hour_string][
key
] = self.__extract_surface_data_value(
surface_data, value, indices, lon_array, lat_array
dictionary[date_string][hour_string][key] = (
self.__extract_surface_data_value(
surface_data, value, indices, lon_array, lat_array
)
)

# Get elevation, time index does not matter, use last one
Expand Down
58 changes: 43 additions & 15 deletions rocketpy/mathutils/function.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
and more. This is a core class of our package, and should be maintained
carefully as it may impact all the rest of the project.
"""

import warnings
from collections.abc import Iterable
from copy import deepcopy
Expand Down Expand Up @@ -58,7 +59,8 @@ def __init__(
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.
ndarray. The file can optionally contain a single header line, see
notes below for more information.

- Function: Copies the source of the provided Function object,
creating a new Function with adjusted inputs and outputs.
Expand Down Expand Up @@ -93,12 +95,19 @@ def __init__(

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.
(I) CSV files may include an optional single header line. If this
header line is present and contains names for each data column, those
names will be used to label the inputs and outputs unless specified
otherwise by the `inputs` and `outputs` arguments.
If the header is specified for only a few columns, it is ignored.

Gui-FernandesBR marked this conversation as resolved.
Show resolved Hide resolved
Commas in a header will be interpreted as a delimiter, which may cause
undesired input or output labeling. To avoid this, specify each input
and output name using the `inputs` and `outputs` arguments.

(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:
inputs = ["Scalar"]
if outputs is None:
Expand Down Expand Up @@ -183,10 +192,18 @@ def set_source(self, source):

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.
(I) CSV files may include an optional single header line. If this
header line is present and contains names for each data column, those
names will be used to label the inputs and outputs unless specified
otherwise. If the header is specified for only a few columns, it is
ignored.

Commas in a header will be interpreted as a delimiter, which may cause
undesired input or output labeling. To avoid this, specify each input
and output name using the `inputs` and `outputs` arguments.

(II) Fields in CSV files may be enclosed in double quotes. If fields
are not quoted, double quotes should not appear inside them.

Returns
-------
Expand Down Expand Up @@ -3018,7 +3035,7 @@ def _check_user_input(
if isinstance(inputs, str):
inputs = [inputs]

elif len(outputs) > 1:
if len(outputs) > 1:
raise ValueError(
"Output must either be a string or have dimension 1, "
+ f"it currently has dimension ({len(outputs)})."
Expand All @@ -3035,8 +3052,19 @@ def _check_user_input(
try:
source = np.loadtxt(source, delimiter=",", dtype=float)
except ValueError:
# Skip header
source = np.loadtxt(source, delimiter=",", dtype=float, skiprows=1)
with open(source, "r") as file:
header, *data = file.read().splitlines()

header = [
label.strip("'").strip('"') for label in header.split(",")
]
source = np.loadtxt(data, delimiter=",", dtype=float)

if len(source[0]) == len(header):
if inputs == ["Scalar"]:
inputs = header[:-1]
if outputs == ["Scalar"]:
outputs = [header[-1]]
except Exception as e:
raise ValueError(
"The source file is not a valid csv or txt file."
Expand All @@ -3054,7 +3082,7 @@ def _check_user_input(

## single dimension
if source_dim == 2:
# possible interpolation values: llinear, polynomial, akima and spline
# possible interpolation values: linear, polynomial, akima and spline
if interpolation is None:
interpolation = "spline"
elif interpolation.lower() not in [
Expand Down Expand Up @@ -3105,7 +3133,7 @@ def _check_user_input(
in_out_dim = len(inputs) + len(outputs)
if source_dim != in_out_dim:
raise ValueError(
"Source dimension ({source_dim}) does not match input "
f"Source dimension ({source_dim}) does not match input "
+ f"and output dimension ({in_out_dim})."
)
return inputs, outputs, interpolation, extrapolation
Expand Down
40 changes: 25 additions & 15 deletions rocketpy/plots/flight_plots.py
Original file line number Diff line number Diff line change
Expand Up @@ -400,9 +400,11 @@ def rail_buttons_forces(self):
)
ax1.set_xlim(
0,
self.flight.out_of_rail_time
if self.flight.out_of_rail_time > 0
else self.flight.tFinal,
(
self.flight.out_of_rail_time
if self.flight.out_of_rail_time > 0
else self.flight.tFinal
),
)
ax1.legend()
ax1.grid(True)
Expand Down Expand Up @@ -431,9 +433,11 @@ def rail_buttons_forces(self):
)
ax2.set_xlim(
0,
self.flight.out_of_rail_time
if self.flight.out_of_rail_time > 0
else self.flight.tFinal,
(
self.flight.out_of_rail_time
if self.flight.out_of_rail_time > 0
else self.flight.tFinal
),
)
ax2.legend()
ax2.grid(True)
Expand Down Expand Up @@ -557,9 +561,11 @@ def energy_data(self):
)
ax1.set_xlim(
0,
self.flight.apogee_time
if self.flight.apogee_time != 0.0
else self.flight.t_final,
(
self.flight.apogee_time
if self.flight.apogee_time != 0.0
else self.flight.t_final
),
)
ax1.ticklabel_format(style="sci", axis="y", scilimits=(0, 0))
ax1.set_title("Kinetic Energy Components")
Expand Down Expand Up @@ -587,9 +593,11 @@ def energy_data(self):
)
ax2.set_xlim(
0,
self.flight.apogee_time
if self.flight.apogee_time != 0.0
else self.flight.t_final,
(
self.flight.apogee_time
if self.flight.apogee_time != 0.0
else self.flight.t_final
),
)
ax2.ticklabel_format(style="sci", axis="y", scilimits=(0, 0))
ax2.set_title("Total Mechanical Energy Components")
Expand Down Expand Up @@ -620,9 +628,11 @@ def energy_data(self):
)
ax4.set_xlim(
0,
self.flight.apogee_time
if self.flight.apogee_time != 0.0
else self.flight.t_final,
(
self.flight.apogee_time
if self.flight.apogee_time != 0.0
else self.flight.t_final
),
)
ax3.ticklabel_format(style="sci", axis="y", scilimits=(0, 0))
ax4.set_title("Drag Absolute Power")
Expand Down
8 changes: 2 additions & 6 deletions rocketpy/rocket/aero_surface.py
Original file line number Diff line number Diff line change
Expand Up @@ -1180,10 +1180,7 @@ def evaluate_geometrical_parameters(self):
* (self.root_chord + 2 * self.tip_chord)
* self.rocket_radius
* self.span**2
+ 6
* (self.root_chord + self.tip_chord)
* self.span
* self.rocket_radius**2
+ 6 * (self.root_chord + self.tip_chord) * self.span * self.rocket_radius**2
) / 12
roll_damping_interference_factor = 1 + (
((tau - λ) / (tau)) - ((1 - λ) / (tau - 1)) * np.log(tau)
Expand Down Expand Up @@ -1506,8 +1503,7 @@ def evaluate_geometrical_parameters(self):
* self.rocket_radius**2
* np.sqrt(-self.span**2 + self.rocket_radius**2)
* np.arctan(
(self.span)
/ (np.sqrt(-self.span**2 + self.rocket_radius**2))
(self.span) / (np.sqrt(-self.span**2 + self.rocket_radius**2))
)
- np.pi
* self.rocket_radius**2
Expand Down
59 changes: 26 additions & 33 deletions rocketpy/simulation/flight.py
Original file line number Diff line number Diff line change
Expand Up @@ -683,9 +683,9 @@ def __init__(
node.time_bound = phase.TimeNodes[node_index + 1].t
phase.solver.t_bound = node.time_bound
phase.solver._lsoda_solver._integrator.rwork[0] = phase.solver.t_bound
phase.solver._lsoda_solver._integrator.call_args[
4
] = phase.solver._lsoda_solver._integrator.rwork
phase.solver._lsoda_solver._integrator.call_args[4] = (
phase.solver._lsoda_solver._integrator.rwork
)
phase.solver.status = "running"

# Feed required parachute and discrete controller triggers
Expand Down Expand Up @@ -1275,9 +1275,7 @@ def udot_rail1(self, t, u, post_processing=False):
R3 = -0.5 * rho * (free_stream_speed**2) * self.rocket.area * (drag_coeff)

# Calculate Linear acceleration
a3 = (R3 + thrust) / M - (
e0**2 - e1**2 - e2**2 + e3**2
) * self.env.gravity(z)
a3 = (R3 + thrust) / M - (e0**2 - e1**2 - e2**2 + e3**2) * self.env.gravity(z)
if a3 > 0:
ax = 2 * (e1 * e3 + e0 * e2) * a3
ay = 2 * (e2 * e3 - e0 * e1) * a3
Expand Down Expand Up @@ -1469,9 +1467,7 @@ def u_dot(self, t, u, post_processing=False):
0.5 * rho * (comp_stream_speed**2) * reference_area * c_lift
)
# component lift force components
lift_dir_norm = (
comp_stream_vx_b**2 + comp_stream_vy_b**2
) ** 0.5
lift_dir_norm = (comp_stream_vx_b**2 + comp_stream_vy_b**2) ** 0.5
comp_lift_xb = comp_lift * (comp_stream_vx_b / lift_dir_norm)
comp_lift_yb = comp_lift * (comp_stream_vy_b / lift_dir_norm)
# add to total lift force
Expand Down Expand Up @@ -1750,9 +1746,7 @@ def u_dot_generalized(self, t, u, post_processing=False):
0.5 * rho * (comp_stream_speed**2) * reference_area * c_lift
)
# Component lift force components
lift_dir_norm = (
comp_stream_vx_b**2 + comp_stream_vy_b**2
) ** 0.5
lift_dir_norm = (comp_stream_vx_b**2 + comp_stream_vy_b**2) ** 0.5
comp_lift_xb = comp_lift * (comp_stream_vx_b / lift_dir_norm)
comp_lift_yb = comp_lift * (comp_stream_vy_b / lift_dir_norm)
# Add to total lift force
Expand Down Expand Up @@ -1896,8 +1890,7 @@ def u_dot_parachute(self, t, u, post_processing=False):
freestream_z = vz
# Determine drag force
pseudoD = (
-0.5 * rho * cd_s * free_stream_speed
- ka * rho * 4 * np.pi * (R**2) * Rdot
-0.5 * rho * cd_s * free_stream_speed - ka * rho * 4 * np.pi * (R**2) * Rdot
)
Dx = pseudoD * freestream_x
Dy = pseudoD * freestream_y
Expand Down Expand Up @@ -2486,9 +2479,7 @@ def translational_energy(self):
# Redefine total_mass time grid to allow for efficient Function algebra
total_mass = deepcopy(self.rocket.total_mass)
total_mass.set_discrete_based_on_model(self.vz)
translational_energy = (
0.5 * total_mass * (self.vx**2 + self.vy**2 + self.vz**2)
)
translational_energy = 0.5 * total_mass * (self.vx**2 + self.vy**2 + self.vz**2)
return translational_energy

@funcify_method("Time (s)", "Kinetic Energy (J)", "spline", "zero")
Expand Down Expand Up @@ -3400,29 +3391,31 @@ def add(self, flight_phase, index=None):
)
if flight_phase.t == previous_phase.t
else (
"Trying to add flight phase starting *together* with the one *proceeding* it. ",
"This may be caused by multiple parachutes being triggered simultaneously.",
)
if flight_phase.t == next_phase.t
else (
"Trying to add flight phase starting *before* the one *preceding* it. ",
"This may be caused by multiple parachutes being triggered simultaneously",
" or by having a negative parachute lag.",
)
if flight_phase.t < previous_phase.t
else (
"Trying to add flight phase starting *after* the one *proceeding* it.",
"This may be caused by multiple parachutes being triggered simultaneously.",
(
"Trying to add flight phase starting *together* with the one *proceeding* it. ",
"This may be caused by multiple parachutes being triggered simultaneously.",
)
if flight_phase.t == next_phase.t
else (
(
"Trying to add flight phase starting *before* the one *preceding* it. ",
"This may be caused by multiple parachutes being triggered simultaneously",
" or by having a negative parachute lag.",
)
if flight_phase.t < previous_phase.t
else (
"Trying to add flight phase starting *after* the one *proceeding* it.",
"This may be caused by multiple parachutes being triggered simultaneously.",
)
)
)
)
self.display_warning(*warning_msg)
adjust = 1e-7 if flight_phase.t in {previous_phase.t, next_phase.t} else 0
new_index = (
index - 1
if flight_phase.t < previous_phase.t
else index + 1
if flight_phase.t > next_phase.t
else index
else index + 1 if flight_phase.t > next_phase.t else index
)
flight_phase.t += adjust
self.add(flight_phase, new_index)
Expand Down
1 change: 1 addition & 0 deletions rocketpy/simulation/flight_data_importer.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Starts with .csv or .txt file containing the rocket's collected flight data
and build a rocketpy.Flight object from it.
"""

import warnings
from os import listdir
from os.path import isfile, join
Expand Down
1 change: 1 addition & 0 deletions tests/test_flight_data_importer.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Tests the FlightDataImporter class from rocketpy.simulation module.
"""

import numpy as np

from rocketpy.simulation import FlightDataImporter
Expand Down
2 changes: 1 addition & 1 deletion tests/test_function.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def test_func_from_csv_with_header(csv_file):
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 f.__repr__() == "'Function from R1 to R1 : (time) → (value)'"
assert np.isclose(f(0), 100)
assert np.isclose(f(0) + f(1), 300), "Error summing the values of the function"

Expand Down
4 changes: 1 addition & 3 deletions tests/test_genericmotor.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,7 @@ def test_generic_motor_inertia(generic_motor):
# Tests the inertia formulation from the propellant mass
propellant_mass = generic_motor.propellant_mass.set_discrete(*burn_time, 50).y_array

propellant_I_11 = propellant_mass * (
chamber_radius**2 / 4 + chamber_height**2 / 12
)
propellant_I_11 = propellant_mass * (chamber_radius**2 / 4 + chamber_height**2 / 12)
propellant_I_22 = propellant_I_11
propellant_I_33 = propellant_mass * (chamber_radius**2 / 2)

Expand Down
Loading