|
1 | 1 | # pylint: disable=too-many-lines |
| 2 | +import json |
2 | 3 | import math |
3 | 4 | import warnings |
4 | 5 | from copy import deepcopy |
5 | 6 | from functools import cached_property |
6 | 7 |
|
7 | 8 | import numpy as np |
| 9 | +import simplekml |
8 | 10 | from scipy.integrate import BDF, DOP853, LSODA, RK23, RK45, OdeSolver, Radau |
9 | 11 |
|
10 | | -from rocketpy.simulation.flight_data_exporter import FlightDataExporter |
11 | | - |
12 | 12 | from ..mathutils.function import Function, funcify_method |
13 | 13 | from ..mathutils.vector_matrix import Matrix, Vector |
14 | 14 | from ..plots.flight_plots import _FlightPlots |
15 | 15 | from ..prints.flight_prints import _FlightPrints |
16 | 16 | from ..tools import ( |
17 | 17 | calculate_cubic_hermite_coefficients, |
18 | | - deprecated, |
19 | 18 | euler313_to_quaternions, |
20 | 19 | find_closest, |
21 | 20 | find_root_linear_interpolation, |
@@ -1130,7 +1129,6 @@ def __init_solution_monitors(self): |
1130 | 1129 | self.out_of_rail_time_index = 0 |
1131 | 1130 | self.out_of_rail_state = np.array([0]) |
1132 | 1131 | self.apogee_state = np.array([0]) |
1133 | | - self.apogee = 0 |
1134 | 1132 | self.apogee_time = 0 |
1135 | 1133 | self.x_impact = 0 |
1136 | 1134 | self.y_impact = 0 |
@@ -2470,6 +2468,15 @@ def max_acceleration(self): |
2470 | 2468 | """Maximum acceleration reached by the rocket.""" |
2471 | 2469 | max_acceleration_time_index = np.argmax(self.acceleration[:, 1]) |
2472 | 2470 | return self.acceleration[max_acceleration_time_index, 1] |
| 2471 | + |
| 2472 | + @cached_property |
| 2473 | + def axial_acceleration(self): |
| 2474 | + """Axial acceleration magnitude as a function of time.""" |
| 2475 | + return ( |
| 2476 | + self.ax * self.attitude_vector_x |
| 2477 | + + self.ay * self.attitude_vector_y |
| 2478 | + + self.az * self.attitude_vector_z |
| 2479 | + ) |
2473 | 2480 |
|
2474 | 2481 | @funcify_method("Time (s)", "Horizontal Speed (m/s)") |
2475 | 2482 | def horizontal_speed(self): |
@@ -3266,72 +3273,270 @@ def calculate_stall_wind_velocity(self, stall_angle): # TODO: move to utilities |
3266 | 3273 | + f" of attack exceeds {stall_angle:.3f}°: {w_v:.3f} m/s" |
3267 | 3274 | ) |
3268 | 3275 |
|
3269 | | - @deprecated( |
3270 | | - reason="Moved to FlightDataExporter.export_pressures()", |
3271 | | - version="v1.12.0", |
3272 | | - alternative="rocketpy.simulation.flight_data_exporter.FlightDataExporter.export_pressures", |
3273 | | - ) |
3274 | | - def export_pressures(self, file_name, time_step): |
3275 | | - """ |
3276 | | - .. deprecated:: 1.11 |
3277 | | - Use :class:`rocketpy.simulation.flight_data_exporter.FlightDataExporter` |
3278 | | - and call ``.export_pressures(...)``. |
| 3276 | + def export_pressures(self, file_name, time_step): # TODO: move out |
| 3277 | + """Exports the pressure experienced by the rocket during the flight to |
| 3278 | + an external file, the '.csv' format is recommended, as the columns will |
| 3279 | + be separated by commas. It can handle flights with or without |
| 3280 | + parachutes, although it is not possible to get a noisy pressure signal |
| 3281 | + if no parachute is added. |
| 3282 | +
|
| 3283 | + If a parachute is added, the file will contain 3 columns: time in |
| 3284 | + seconds, clean pressure in Pascals and noisy pressure in Pascals. |
| 3285 | + For flights without parachutes, the third column will be discarded |
| 3286 | +
|
| 3287 | + This function was created especially for the 'Projeto Jupiter' |
| 3288 | + Electronics Subsystems team and aims to help in configuring |
| 3289 | + micro-controllers. |
| 3290 | +
|
| 3291 | + Parameters |
| 3292 | + ---------- |
| 3293 | + file_name : string |
| 3294 | + The final file name, |
| 3295 | + time_step : float |
| 3296 | + Time step desired for the final file |
| 3297 | +
|
| 3298 | + Return |
| 3299 | + ------ |
| 3300 | + None |
3279 | 3301 | """ |
3280 | | - return FlightDataExporter(self).export_pressures(file_name, time_step) |
| 3302 | + time_points = np.arange(0, self.t_final, time_step) |
| 3303 | + # pylint: disable=W1514, E1121 |
| 3304 | + with open(file_name, "w") as file: |
| 3305 | + if len(self.rocket.parachutes) == 0: |
| 3306 | + print("No parachutes in the rocket, saving static pressure.") |
| 3307 | + for t in time_points: |
| 3308 | + file.write(f"{t:f}, {self.pressure.get_value_opt(t):.5f}\n") |
| 3309 | + else: |
| 3310 | + for parachute in self.rocket.parachutes: |
| 3311 | + for t in time_points: |
| 3312 | + p_cl = parachute.clean_pressure_signal_function.get_value_opt(t) |
| 3313 | + p_ns = parachute.noisy_pressure_signal_function.get_value_opt(t) |
| 3314 | + file.write(f"{t:f}, {p_cl:.5f}, {p_ns:.5f}\n") |
| 3315 | + # We need to save only 1 parachute data |
| 3316 | + break |
3281 | 3317 |
|
3282 | | - @deprecated( |
3283 | | - reason="Moved to FlightDataExporter.export_data()", |
3284 | | - version="v1.12.0", |
3285 | | - alternative="rocketpy.simulation.flight_data_exporter.FlightDataExporter.export_data", |
3286 | | - ) |
3287 | 3318 | def export_data(self, file_name, *variables, time_step=None): |
| 3319 | + """Exports flight data to a comma separated value file (.csv). |
| 3320 | +
|
| 3321 | + Data is exported in columns, with the first column representing time |
| 3322 | + steps. The first line of the file is a header line, specifying the |
| 3323 | + meaning of each column and its units. |
| 3324 | +
|
| 3325 | + Parameters |
| 3326 | + ---------- |
| 3327 | + file_name : string |
| 3328 | + The file name or path of the exported file. Example: flight_data.csv |
| 3329 | + Do not use forbidden characters, such as / in Linux/Unix and |
| 3330 | + `<, >, :, ", /, \\, | ?, *` in Windows. |
| 3331 | + variables : strings, optional |
| 3332 | + Names of the data variables which shall be exported. Must be Flight |
| 3333 | + class attributes which are instances of the Function class. Usage |
| 3334 | + example: test_flight.export_data('test.csv', 'z', 'angle_of_attack', |
| 3335 | + 'mach_number'). |
| 3336 | + time_step : float, optional |
| 3337 | + Time step desired for the data. If None, all integration time steps |
| 3338 | + will be exported. Otherwise, linear interpolation is carried out to |
| 3339 | + calculate values at the desired time steps. Example: 0.001. |
3288 | 3340 | """ |
3289 | | - .. deprecated:: 1.11 |
3290 | | - Use :class:`rocketpy.simulation.flight_data_exporter.FlightDataExporter` |
3291 | | - and call ``.export_data(...)``. |
3292 | | - """ |
3293 | | - return FlightDataExporter(self).export_data( |
3294 | | - file_name, *variables, time_step=time_step |
| 3341 | + # TODO: we should move this method to outside of class. |
| 3342 | + |
| 3343 | + # Fast evaluation for the most basic scenario |
| 3344 | + if time_step is None and len(variables) == 0: |
| 3345 | + np.savetxt( |
| 3346 | + file_name, |
| 3347 | + self.solution, |
| 3348 | + fmt="%.6f", |
| 3349 | + delimiter=",", |
| 3350 | + header="" |
| 3351 | + "Time (s)," |
| 3352 | + "X (m)," |
| 3353 | + "Y (m)," |
| 3354 | + "Z (m)," |
| 3355 | + "E0," |
| 3356 | + "E1," |
| 3357 | + "E2," |
| 3358 | + "E3," |
| 3359 | + "W1 (rad/s)," |
| 3360 | + "W2 (rad/s)," |
| 3361 | + "W3 (rad/s)", |
| 3362 | + ) |
| 3363 | + return |
| 3364 | + |
| 3365 | + # Not so fast evaluation for general case |
| 3366 | + if variables is None: |
| 3367 | + variables = [ |
| 3368 | + "x", |
| 3369 | + "y", |
| 3370 | + "z", |
| 3371 | + "vx", |
| 3372 | + "vy", |
| 3373 | + "vz", |
| 3374 | + "e0", |
| 3375 | + "e1", |
| 3376 | + "e2", |
| 3377 | + "e3", |
| 3378 | + "w1", |
| 3379 | + "w2", |
| 3380 | + "w3", |
| 3381 | + ] |
| 3382 | + |
| 3383 | + if time_step is None: |
| 3384 | + time_points = self.time |
| 3385 | + else: |
| 3386 | + time_points = np.arange(self.t_initial, self.t_final, time_step) |
| 3387 | + |
| 3388 | + exported_matrix = [time_points] |
| 3389 | + exported_header = "Time (s)" |
| 3390 | + |
| 3391 | + # Loop through variables, get points and names (for the header) |
| 3392 | + for variable in variables: |
| 3393 | + if variable in self.__dict__: |
| 3394 | + variable_function = self.__dict__[variable] |
| 3395 | + # Deal with decorated Flight methods |
| 3396 | + else: |
| 3397 | + try: |
| 3398 | + obj = getattr(self.__class__, variable) |
| 3399 | + variable_function = obj.__get__(self, self.__class__) |
| 3400 | + except AttributeError as exc: |
| 3401 | + raise AttributeError( |
| 3402 | + f"Variable '{variable}' not found in Flight class" |
| 3403 | + ) from exc |
| 3404 | + variable_points = variable_function(time_points) |
| 3405 | + exported_matrix += [variable_points] |
| 3406 | + exported_header += f", {variable_function.__outputs__[0]}" |
| 3407 | + |
| 3408 | + exported_matrix = np.array(exported_matrix).T # Fix matrix orientation |
| 3409 | + |
| 3410 | + np.savetxt( |
| 3411 | + file_name, |
| 3412 | + exported_matrix, |
| 3413 | + fmt="%.6f", |
| 3414 | + delimiter=",", |
| 3415 | + header=exported_header, |
| 3416 | + encoding="utf-8", |
3295 | 3417 | ) |
3296 | 3418 |
|
3297 | | - @deprecated( |
3298 | | - reason="Moved to FlightDataExporter.export_sensor_data()", |
3299 | | - version="v1.12.0", |
3300 | | - alternative="rocketpy.simulation.flight_data_exporter.FlightDataExporter.export_sensor_data", |
3301 | | - ) |
3302 | 3419 | def export_sensor_data(self, file_name, sensor=None): |
| 3420 | + """Exports sensors data to a file. The file format can be either .csv or |
| 3421 | + .json. |
| 3422 | +
|
| 3423 | + Parameters |
| 3424 | + ---------- |
| 3425 | + file_name : str |
| 3426 | + The file name or path of the exported file. Example: flight_data.csv |
| 3427 | + Do not use forbidden characters, such as / in Linux/Unix and |
| 3428 | + `<, >, :, ", /, \\, | ?, *` in Windows. |
| 3429 | + sensor : Sensor, string, optional |
| 3430 | + The sensor to export data from. Can be given as a Sensor object or |
| 3431 | + as a string with the sensor name. If None, all sensors data will be |
| 3432 | + exported. Default is None. |
3303 | 3433 | """ |
3304 | | - .. deprecated:: 1.11 |
3305 | | - Use :class:`rocketpy.simulation.flight_data_exporter.FlightDataExporter` |
3306 | | - and call ``.export_sensor_data(...)``. |
3307 | | - """ |
3308 | | - return FlightDataExporter(self).export_sensor_data(file_name, sensor=sensor) |
| 3434 | + if sensor is None: |
| 3435 | + data_dict = {} |
| 3436 | + for used_sensor, measured_data in self.sensor_data.items(): |
| 3437 | + data_dict[used_sensor.name] = measured_data |
| 3438 | + else: |
| 3439 | + # export data of only that sensor |
| 3440 | + data_dict = {} |
| 3441 | + |
| 3442 | + if not isinstance(sensor, str): |
| 3443 | + data_dict[sensor.name] = self.sensor_data[sensor] |
| 3444 | + else: # sensor is a string |
| 3445 | + matching_sensors = [s for s in self.sensor_data if s.name == sensor] |
| 3446 | + |
| 3447 | + if len(matching_sensors) > 1: |
| 3448 | + data_dict[sensor] = [] |
| 3449 | + for s in matching_sensors: |
| 3450 | + data_dict[s.name].append(self.sensor_data[s]) |
| 3451 | + elif len(matching_sensors) == 1: |
| 3452 | + data_dict[sensor] = self.sensor_data[matching_sensors[0]] |
| 3453 | + else: |
| 3454 | + raise ValueError("Sensor not found in the Flight.sensor_data.") |
3309 | 3455 |
|
3310 | | - @deprecated( |
3311 | | - reason="Moved to FlightDataExporter.export_kml()", |
3312 | | - version="v1.12.0", |
3313 | | - alternative="rocketpy.simulation.flight_data_exporter.FlightDataExporter.export_kml", |
3314 | | - ) |
3315 | | - def export_kml( |
| 3456 | + with open(file_name, "w") as file: |
| 3457 | + json.dump(data_dict, file) |
| 3458 | + print("Sensor data exported to: ", file_name) |
| 3459 | + |
| 3460 | + def export_kml( # TODO: should be moved out of this class. |
3316 | 3461 | self, |
3317 | 3462 | file_name="trajectory.kml", |
3318 | 3463 | time_step=None, |
3319 | 3464 | extrude=True, |
3320 | 3465 | color="641400F0", |
3321 | 3466 | altitude_mode="absolute", |
3322 | 3467 | ): |
| 3468 | + """Exports flight data to a .kml file, which can be opened with Google |
| 3469 | + Earth to display the rocket's trajectory. |
| 3470 | +
|
| 3471 | + Parameters |
| 3472 | + ---------- |
| 3473 | + file_name : string |
| 3474 | + The file name or path of the exported file. Example: flight_data.csv |
| 3475 | + time_step : float, optional |
| 3476 | + Time step desired for the data. If None, all integration time steps |
| 3477 | + will be exported. Otherwise, linear interpolation is carried out to |
| 3478 | + calculate values at the desired time steps. Example: 0.001. |
| 3479 | + extrude: bool, optional |
| 3480 | + To be used if you want to project the path over ground by using an |
| 3481 | + extruded polygon. In case False only the linestring containing the |
| 3482 | + flight path will be created. Default is True. |
| 3483 | + color : str, optional |
| 3484 | + Color of your trajectory path, need to be used in specific kml |
| 3485 | + format. Refer to http://www.zonums.com/gmaps/kml_color/ for more |
| 3486 | + info. |
| 3487 | + altitude_mode: str |
| 3488 | + Select elevation values format to be used on the kml file. Use |
| 3489 | + 'relativetoground' if you want use Above Ground Level elevation, or |
| 3490 | + 'absolute' if you want to parse elevation using Above Sea Level. |
| 3491 | + Default is 'relativetoground'. Only works properly if the ground |
| 3492 | + level is flat. Change to 'absolute' if the terrain is to irregular |
| 3493 | + or contains mountains. |
3323 | 3494 | """ |
3324 | | - .. deprecated:: 1.11 |
3325 | | - Use :class:`rocketpy.simulation.flight_data_exporter.FlightDataExporter` |
3326 | | - and call ``.export_kml(...)``. |
3327 | | - """ |
3328 | | - return FlightDataExporter(self).export_kml( |
3329 | | - file_name=file_name, |
3330 | | - time_step=time_step, |
3331 | | - extrude=extrude, |
3332 | | - color=color, |
3333 | | - altitude_mode=altitude_mode, |
3334 | | - ) |
| 3495 | + # Define time points vector |
| 3496 | + if time_step is None: |
| 3497 | + time_points = self.time |
| 3498 | + else: |
| 3499 | + time_points = np.arange(self.t_initial, self.t_final + time_step, time_step) |
| 3500 | + # Open kml file with simplekml library |
| 3501 | + kml = simplekml.Kml(open=1) |
| 3502 | + trajectory = kml.newlinestring(name="Rocket Trajectory - Powered by RocketPy") |
| 3503 | + |
| 3504 | + if altitude_mode == "relativetoground": |
| 3505 | + # In this mode the elevation data will be the Above Ground Level |
| 3506 | + # elevation. Only works properly if the ground level is similar to |
| 3507 | + # a plane, i.e. it might not work well if the terrain has mountains |
| 3508 | + coords = [ |
| 3509 | + ( |
| 3510 | + self.longitude.get_value_opt(t), |
| 3511 | + self.latitude.get_value_opt(t), |
| 3512 | + self.altitude.get_value_opt(t), |
| 3513 | + ) |
| 3514 | + for t in time_points |
| 3515 | + ] |
| 3516 | + trajectory.coords = coords |
| 3517 | + trajectory.altitudemode = simplekml.AltitudeMode.relativetoground |
| 3518 | + else: # altitude_mode == 'absolute' |
| 3519 | + # In this case the elevation data will be the Above Sea Level elevation |
| 3520 | + # Ensure you use the correct value on self.env.elevation, otherwise |
| 3521 | + # the trajectory path can be offset from ground |
| 3522 | + coords = [ |
| 3523 | + ( |
| 3524 | + self.longitude.get_value_opt(t), |
| 3525 | + self.latitude.get_value_opt(t), |
| 3526 | + self.z.get_value_opt(t), |
| 3527 | + ) |
| 3528 | + for t in time_points |
| 3529 | + ] |
| 3530 | + trajectory.coords = coords |
| 3531 | + trajectory.altitudemode = simplekml.AltitudeMode.absolute |
| 3532 | + # Modify style of trajectory linestring |
| 3533 | + trajectory.style.linestyle.color = color |
| 3534 | + trajectory.style.polystyle.color = color |
| 3535 | + if extrude: |
| 3536 | + trajectory.extrude = 1 |
| 3537 | + # Save the KML |
| 3538 | + kml.save(file_name) |
| 3539 | + print("File ", file_name, " saved with success!") |
3335 | 3540 |
|
3336 | 3541 | def info(self): |
3337 | 3542 | """Prints out a summary of the data available about the Flight.""" |
|
0 commit comments