|
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 ..motors.point_mass_motor import PointMassMotor |
|
17 | 17 | from ..rocket import PointMassRocket |
18 | 18 | from ..tools import ( |
19 | 19 | calculate_cubic_hermite_coefficients, |
20 | | - deprecated, |
21 | 20 | euler313_to_quaternions, |
22 | 21 | find_closest, |
23 | 22 | find_root_linear_interpolation, |
@@ -1136,7 +1135,6 @@ def __init_solution_monitors(self): |
1136 | 1135 | self.out_of_rail_time_index = 0 |
1137 | 1136 | self.out_of_rail_state = np.array([0]) |
1138 | 1137 | self.apogee_state = np.array([0]) |
1139 | | - self.apogee = 0 |
1140 | 1138 | self.apogee_time = 0 |
1141 | 1139 | self.x_impact = 0 |
1142 | 1140 | self.y_impact = 0 |
@@ -2619,6 +2617,15 @@ def max_acceleration(self): |
2619 | 2617 | """Maximum acceleration reached by the rocket.""" |
2620 | 2618 | max_acceleration_time_index = np.argmax(self.acceleration[:, 1]) |
2621 | 2619 | return self.acceleration[max_acceleration_time_index, 1] |
| 2620 | + |
| 2621 | + @cached_property |
| 2622 | + def axial_acceleration(self): |
| 2623 | + """Axial acceleration magnitude as a function of time.""" |
| 2624 | + return ( |
| 2625 | + self.ax * self.attitude_vector_x |
| 2626 | + + self.ay * self.attitude_vector_y |
| 2627 | + + self.az * self.attitude_vector_z |
| 2628 | + ) |
2622 | 2629 |
|
2623 | 2630 | @funcify_method("Time (s)", "Horizontal Speed (m/s)") |
2624 | 2631 | def horizontal_speed(self): |
@@ -3415,72 +3422,270 @@ def calculate_stall_wind_velocity(self, stall_angle): # TODO: move to utilities |
3415 | 3422 | + f" of attack exceeds {stall_angle:.3f}°: {w_v:.3f} m/s" |
3416 | 3423 | ) |
3417 | 3424 |
|
3418 | | - @deprecated( |
3419 | | - reason="Moved to FlightDataExporter.export_pressures()", |
3420 | | - version="v1.12.0", |
3421 | | - alternative="rocketpy.simulation.flight_data_exporter.FlightDataExporter.export_pressures", |
3422 | | - ) |
3423 | | - def export_pressures(self, file_name, time_step): |
3424 | | - """ |
3425 | | - .. deprecated:: 1.11 |
3426 | | - Use :class:`rocketpy.simulation.flight_data_exporter.FlightDataExporter` |
3427 | | - and call ``.export_pressures(...)``. |
| 3425 | + def export_pressures(self, file_name, time_step): # TODO: move out |
| 3426 | + """Exports the pressure experienced by the rocket during the flight to |
| 3427 | + an external file, the '.csv' format is recommended, as the columns will |
| 3428 | + be separated by commas. It can handle flights with or without |
| 3429 | + parachutes, although it is not possible to get a noisy pressure signal |
| 3430 | + if no parachute is added. |
| 3431 | +
|
| 3432 | + If a parachute is added, the file will contain 3 columns: time in |
| 3433 | + seconds, clean pressure in Pascals and noisy pressure in Pascals. |
| 3434 | + For flights without parachutes, the third column will be discarded |
| 3435 | +
|
| 3436 | + This function was created especially for the 'Projeto Jupiter' |
| 3437 | + Electronics Subsystems team and aims to help in configuring |
| 3438 | + micro-controllers. |
| 3439 | +
|
| 3440 | + Parameters |
| 3441 | + ---------- |
| 3442 | + file_name : string |
| 3443 | + The final file name, |
| 3444 | + time_step : float |
| 3445 | + Time step desired for the final file |
| 3446 | +
|
| 3447 | + Return |
| 3448 | + ------ |
| 3449 | + None |
3428 | 3450 | """ |
3429 | | - return FlightDataExporter(self).export_pressures(file_name, time_step) |
| 3451 | + time_points = np.arange(0, self.t_final, time_step) |
| 3452 | + # pylint: disable=W1514, E1121 |
| 3453 | + with open(file_name, "w") as file: |
| 3454 | + if len(self.rocket.parachutes) == 0: |
| 3455 | + print("No parachutes in the rocket, saving static pressure.") |
| 3456 | + for t in time_points: |
| 3457 | + file.write(f"{t:f}, {self.pressure.get_value_opt(t):.5f}\n") |
| 3458 | + else: |
| 3459 | + for parachute in self.rocket.parachutes: |
| 3460 | + for t in time_points: |
| 3461 | + p_cl = parachute.clean_pressure_signal_function.get_value_opt(t) |
| 3462 | + p_ns = parachute.noisy_pressure_signal_function.get_value_opt(t) |
| 3463 | + file.write(f"{t:f}, {p_cl:.5f}, {p_ns:.5f}\n") |
| 3464 | + # We need to save only 1 parachute data |
| 3465 | + break |
3430 | 3466 |
|
3431 | | - @deprecated( |
3432 | | - reason="Moved to FlightDataExporter.export_data()", |
3433 | | - version="v1.12.0", |
3434 | | - alternative="rocketpy.simulation.flight_data_exporter.FlightDataExporter.export_data", |
3435 | | - ) |
3436 | 3467 | def export_data(self, file_name, *variables, time_step=None): |
| 3468 | + """Exports flight data to a comma separated value file (.csv). |
| 3469 | +
|
| 3470 | + Data is exported in columns, with the first column representing time |
| 3471 | + steps. The first line of the file is a header line, specifying the |
| 3472 | + meaning of each column and its units. |
| 3473 | +
|
| 3474 | + Parameters |
| 3475 | + ---------- |
| 3476 | + file_name : string |
| 3477 | + The file name or path of the exported file. Example: flight_data.csv |
| 3478 | + Do not use forbidden characters, such as / in Linux/Unix and |
| 3479 | + `<, >, :, ", /, \\, | ?, *` in Windows. |
| 3480 | + variables : strings, optional |
| 3481 | + Names of the data variables which shall be exported. Must be Flight |
| 3482 | + class attributes which are instances of the Function class. Usage |
| 3483 | + example: test_flight.export_data('test.csv', 'z', 'angle_of_attack', |
| 3484 | + 'mach_number'). |
| 3485 | + time_step : float, optional |
| 3486 | + Time step desired for the data. If None, all integration time steps |
| 3487 | + will be exported. Otherwise, linear interpolation is carried out to |
| 3488 | + calculate values at the desired time steps. Example: 0.001. |
3437 | 3489 | """ |
3438 | | - .. deprecated:: 1.11 |
3439 | | - Use :class:`rocketpy.simulation.flight_data_exporter.FlightDataExporter` |
3440 | | - and call ``.export_data(...)``. |
3441 | | - """ |
3442 | | - return FlightDataExporter(self).export_data( |
3443 | | - file_name, *variables, time_step=time_step |
| 3490 | + # TODO: we should move this method to outside of class. |
| 3491 | + |
| 3492 | + # Fast evaluation for the most basic scenario |
| 3493 | + if time_step is None and len(variables) == 0: |
| 3494 | + np.savetxt( |
| 3495 | + file_name, |
| 3496 | + self.solution, |
| 3497 | + fmt="%.6f", |
| 3498 | + delimiter=",", |
| 3499 | + header="" |
| 3500 | + "Time (s)," |
| 3501 | + "X (m)," |
| 3502 | + "Y (m)," |
| 3503 | + "Z (m)," |
| 3504 | + "E0," |
| 3505 | + "E1," |
| 3506 | + "E2," |
| 3507 | + "E3," |
| 3508 | + "W1 (rad/s)," |
| 3509 | + "W2 (rad/s)," |
| 3510 | + "W3 (rad/s)", |
| 3511 | + ) |
| 3512 | + return |
| 3513 | + |
| 3514 | + # Not so fast evaluation for general case |
| 3515 | + if variables is None: |
| 3516 | + variables = [ |
| 3517 | + "x", |
| 3518 | + "y", |
| 3519 | + "z", |
| 3520 | + "vx", |
| 3521 | + "vy", |
| 3522 | + "vz", |
| 3523 | + "e0", |
| 3524 | + "e1", |
| 3525 | + "e2", |
| 3526 | + "e3", |
| 3527 | + "w1", |
| 3528 | + "w2", |
| 3529 | + "w3", |
| 3530 | + ] |
| 3531 | + |
| 3532 | + if time_step is None: |
| 3533 | + time_points = self.time |
| 3534 | + else: |
| 3535 | + time_points = np.arange(self.t_initial, self.t_final, time_step) |
| 3536 | + |
| 3537 | + exported_matrix = [time_points] |
| 3538 | + exported_header = "Time (s)" |
| 3539 | + |
| 3540 | + # Loop through variables, get points and names (for the header) |
| 3541 | + for variable in variables: |
| 3542 | + if variable in self.__dict__: |
| 3543 | + variable_function = self.__dict__[variable] |
| 3544 | + # Deal with decorated Flight methods |
| 3545 | + else: |
| 3546 | + try: |
| 3547 | + obj = getattr(self.__class__, variable) |
| 3548 | + variable_function = obj.__get__(self, self.__class__) |
| 3549 | + except AttributeError as exc: |
| 3550 | + raise AttributeError( |
| 3551 | + f"Variable '{variable}' not found in Flight class" |
| 3552 | + ) from exc |
| 3553 | + variable_points = variable_function(time_points) |
| 3554 | + exported_matrix += [variable_points] |
| 3555 | + exported_header += f", {variable_function.__outputs__[0]}" |
| 3556 | + |
| 3557 | + exported_matrix = np.array(exported_matrix).T # Fix matrix orientation |
| 3558 | + |
| 3559 | + np.savetxt( |
| 3560 | + file_name, |
| 3561 | + exported_matrix, |
| 3562 | + fmt="%.6f", |
| 3563 | + delimiter=",", |
| 3564 | + header=exported_header, |
| 3565 | + encoding="utf-8", |
3444 | 3566 | ) |
3445 | 3567 |
|
3446 | | - @deprecated( |
3447 | | - reason="Moved to FlightDataExporter.export_sensor_data()", |
3448 | | - version="v1.12.0", |
3449 | | - alternative="rocketpy.simulation.flight_data_exporter.FlightDataExporter.export_sensor_data", |
3450 | | - ) |
3451 | 3568 | def export_sensor_data(self, file_name, sensor=None): |
| 3569 | + """Exports sensors data to a file. The file format can be either .csv or |
| 3570 | + .json. |
| 3571 | +
|
| 3572 | + Parameters |
| 3573 | + ---------- |
| 3574 | + file_name : str |
| 3575 | + The file name or path of the exported file. Example: flight_data.csv |
| 3576 | + Do not use forbidden characters, such as / in Linux/Unix and |
| 3577 | + `<, >, :, ", /, \\, | ?, *` in Windows. |
| 3578 | + sensor : Sensor, string, optional |
| 3579 | + The sensor to export data from. Can be given as a Sensor object or |
| 3580 | + as a string with the sensor name. If None, all sensors data will be |
| 3581 | + exported. Default is None. |
3452 | 3582 | """ |
3453 | | - .. deprecated:: 1.11 |
3454 | | - Use :class:`rocketpy.simulation.flight_data_exporter.FlightDataExporter` |
3455 | | - and call ``.export_sensor_data(...)``. |
3456 | | - """ |
3457 | | - return FlightDataExporter(self).export_sensor_data(file_name, sensor=sensor) |
| 3583 | + if sensor is None: |
| 3584 | + data_dict = {} |
| 3585 | + for used_sensor, measured_data in self.sensor_data.items(): |
| 3586 | + data_dict[used_sensor.name] = measured_data |
| 3587 | + else: |
| 3588 | + # export data of only that sensor |
| 3589 | + data_dict = {} |
| 3590 | + |
| 3591 | + if not isinstance(sensor, str): |
| 3592 | + data_dict[sensor.name] = self.sensor_data[sensor] |
| 3593 | + else: # sensor is a string |
| 3594 | + matching_sensors = [s for s in self.sensor_data if s.name == sensor] |
| 3595 | + |
| 3596 | + if len(matching_sensors) > 1: |
| 3597 | + data_dict[sensor] = [] |
| 3598 | + for s in matching_sensors: |
| 3599 | + data_dict[s.name].append(self.sensor_data[s]) |
| 3600 | + elif len(matching_sensors) == 1: |
| 3601 | + data_dict[sensor] = self.sensor_data[matching_sensors[0]] |
| 3602 | + else: |
| 3603 | + raise ValueError("Sensor not found in the Flight.sensor_data.") |
3458 | 3604 |
|
3459 | | - @deprecated( |
3460 | | - reason="Moved to FlightDataExporter.export_kml()", |
3461 | | - version="v1.12.0", |
3462 | | - alternative="rocketpy.simulation.flight_data_exporter.FlightDataExporter.export_kml", |
3463 | | - ) |
3464 | | - def export_kml( |
| 3605 | + with open(file_name, "w") as file: |
| 3606 | + json.dump(data_dict, file) |
| 3607 | + print("Sensor data exported to: ", file_name) |
| 3608 | + |
| 3609 | + def export_kml( # TODO: should be moved out of this class. |
3465 | 3610 | self, |
3466 | 3611 | file_name="trajectory.kml", |
3467 | 3612 | time_step=None, |
3468 | 3613 | extrude=True, |
3469 | 3614 | color="641400F0", |
3470 | 3615 | altitude_mode="absolute", |
3471 | 3616 | ): |
| 3617 | + """Exports flight data to a .kml file, which can be opened with Google |
| 3618 | + Earth to display the rocket's trajectory. |
| 3619 | +
|
| 3620 | + Parameters |
| 3621 | + ---------- |
| 3622 | + file_name : string |
| 3623 | + The file name or path of the exported file. Example: flight_data.csv |
| 3624 | + time_step : float, optional |
| 3625 | + Time step desired for the data. If None, all integration time steps |
| 3626 | + will be exported. Otherwise, linear interpolation is carried out to |
| 3627 | + calculate values at the desired time steps. Example: 0.001. |
| 3628 | + extrude: bool, optional |
| 3629 | + To be used if you want to project the path over ground by using an |
| 3630 | + extruded polygon. In case False only the linestring containing the |
| 3631 | + flight path will be created. Default is True. |
| 3632 | + color : str, optional |
| 3633 | + Color of your trajectory path, need to be used in specific kml |
| 3634 | + format. Refer to http://www.zonums.com/gmaps/kml_color/ for more |
| 3635 | + info. |
| 3636 | + altitude_mode: str |
| 3637 | + Select elevation values format to be used on the kml file. Use |
| 3638 | + 'relativetoground' if you want use Above Ground Level elevation, or |
| 3639 | + 'absolute' if you want to parse elevation using Above Sea Level. |
| 3640 | + Default is 'relativetoground'. Only works properly if the ground |
| 3641 | + level is flat. Change to 'absolute' if the terrain is to irregular |
| 3642 | + or contains mountains. |
3472 | 3643 | """ |
3473 | | - .. deprecated:: 1.11 |
3474 | | - Use :class:`rocketpy.simulation.flight_data_exporter.FlightDataExporter` |
3475 | | - and call ``.export_kml(...)``. |
3476 | | - """ |
3477 | | - return FlightDataExporter(self).export_kml( |
3478 | | - file_name=file_name, |
3479 | | - time_step=time_step, |
3480 | | - extrude=extrude, |
3481 | | - color=color, |
3482 | | - altitude_mode=altitude_mode, |
3483 | | - ) |
| 3644 | + # Define time points vector |
| 3645 | + if time_step is None: |
| 3646 | + time_points = self.time |
| 3647 | + else: |
| 3648 | + time_points = np.arange(self.t_initial, self.t_final + time_step, time_step) |
| 3649 | + # Open kml file with simplekml library |
| 3650 | + kml = simplekml.Kml(open=1) |
| 3651 | + trajectory = kml.newlinestring(name="Rocket Trajectory - Powered by RocketPy") |
| 3652 | + |
| 3653 | + if altitude_mode == "relativetoground": |
| 3654 | + # In this mode the elevation data will be the Above Ground Level |
| 3655 | + # elevation. Only works properly if the ground level is similar to |
| 3656 | + # a plane, i.e. it might not work well if the terrain has mountains |
| 3657 | + coords = [ |
| 3658 | + ( |
| 3659 | + self.longitude.get_value_opt(t), |
| 3660 | + self.latitude.get_value_opt(t), |
| 3661 | + self.altitude.get_value_opt(t), |
| 3662 | + ) |
| 3663 | + for t in time_points |
| 3664 | + ] |
| 3665 | + trajectory.coords = coords |
| 3666 | + trajectory.altitudemode = simplekml.AltitudeMode.relativetoground |
| 3667 | + else: # altitude_mode == 'absolute' |
| 3668 | + # In this case the elevation data will be the Above Sea Level elevation |
| 3669 | + # Ensure you use the correct value on self.env.elevation, otherwise |
| 3670 | + # the trajectory path can be offset from ground |
| 3671 | + coords = [ |
| 3672 | + ( |
| 3673 | + self.longitude.get_value_opt(t), |
| 3674 | + self.latitude.get_value_opt(t), |
| 3675 | + self.z.get_value_opt(t), |
| 3676 | + ) |
| 3677 | + for t in time_points |
| 3678 | + ] |
| 3679 | + trajectory.coords = coords |
| 3680 | + trajectory.altitudemode = simplekml.AltitudeMode.absolute |
| 3681 | + # Modify style of trajectory linestring |
| 3682 | + trajectory.style.linestyle.color = color |
| 3683 | + trajectory.style.polystyle.color = color |
| 3684 | + if extrude: |
| 3685 | + trajectory.extrude = 1 |
| 3686 | + # Save the KML |
| 3687 | + kml.save(file_name) |
| 3688 | + print("File ", file_name, " saved with success!") |
3484 | 3689 |
|
3485 | 3690 | def info(self): |
3486 | 3691 | """Prints out a summary of the data available about the Flight.""" |
|
0 commit comments