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: Expansion of Encoders Implementation for Full Flights. #679

Merged
merged 3 commits into from
Dec 21, 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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ Attention: The newest changes should be on top -->
- ENH: create a dataset of pre-registered motors. See #664 [#744](https://github.com/RocketPy-Team/RocketPy/pull/744)
- DOC: add Defiance flight example [#742](https://github.com/RocketPy-Team/RocketPy/pull/742)
- ENH: Allow for Alternative and Custom ODE Solvers. [#748](https://github.com/RocketPy-Team/RocketPy/pull/748)
- ENH: Expansion of Encoders Implementation for Full Flights. [#679](https://github.com/RocketPy-Team/RocketPy/pull/679)



### Changed
Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

167 changes: 90 additions & 77 deletions docs/notebooks/monte_carlo_analysis/monte_carlo_class_usage.ipynb

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ netCDF4>=1.6.4
requests
pytz
simplekml
dill
Gui-FernandesBR marked this conversation as resolved.
Show resolved Hide resolved
138 changes: 130 additions & 8 deletions rocketpy/_encoders.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
"""Defines a custom JSON encoder for RocketPy objects."""

import json
import types
from datetime import datetime
from importlib import import_module

import numpy as np

from rocketpy.mathutils.function import Function


class RocketPyEncoder(json.JSONEncoder):
"""NOTE: This is still under construction, please don't use it yet."""
"""Custom JSON encoder for RocketPy objects. It defines how to encode
different types of objects to a JSON supported format."""

def __init__(self, *args, **kwargs):
self.include_outputs = kwargs.pop("include_outputs", False)
self.include_function_data = kwargs.pop("include_function_data", True)
super().__init__(*args, **kwargs)

def default(self, o):
if isinstance(
Expand All @@ -33,11 +40,126 @@
return float(o)
elif isinstance(o, np.ndarray):
return o.tolist()
elif isinstance(o, datetime):
return [o.year, o.month, o.day, o.hour]

Check warning on line 44 in rocketpy/_encoders.py

View check run for this annotation

Codecov / codecov/patch

rocketpy/_encoders.py#L44

Added line #L44 was not covered by tests
elif hasattr(o, "__iter__") and not isinstance(o, str):
return list(o)
elif isinstance(o, Function):
if not self.include_function_data:
return str(o)

Check warning on line 49 in rocketpy/_encoders.py

View check run for this annotation

Codecov / codecov/patch

rocketpy/_encoders.py#L49

Added line #L49 was not covered by tests
else:
encoding = o.to_dict(self.include_outputs)
encoding["signature"] = get_class_signature(o)
return encoding
elif hasattr(o, "to_dict"):
return o.to_dict()
# elif isinstance(o, Function):
# return o.__dict__()
elif isinstance(o, (Function, types.FunctionType)):
return repr(o)
encoding = o.to_dict(self.include_outputs)
encoding = remove_circular_references(encoding)

encoding["signature"] = get_class_signature(o)

return encoding

elif hasattr(o, "__dict__"):
encoding = remove_circular_references(o.__dict__)

Check warning on line 63 in rocketpy/_encoders.py

View check run for this annotation

Codecov / codecov/patch

rocketpy/_encoders.py#L62-L63

Added lines #L62 - L63 were not covered by tests

if "rocketpy" in o.__class__.__module__:
encoding["signature"] = get_class_signature(o)

Check warning on line 66 in rocketpy/_encoders.py

View check run for this annotation

Codecov / codecov/patch

rocketpy/_encoders.py#L65-L66

Added lines #L65 - L66 were not covered by tests

return encoding

Check warning on line 68 in rocketpy/_encoders.py

View check run for this annotation

Codecov / codecov/patch

rocketpy/_encoders.py#L68

Added line #L68 was not covered by tests
else:
return super().default(o)

Check warning on line 70 in rocketpy/_encoders.py

View check run for this annotation

Codecov / codecov/patch

rocketpy/_encoders.py#L70

Added line #L70 was not covered by tests


class RocketPyDecoder(json.JSONDecoder):
"""Custom JSON decoder for RocketPy objects. It defines how to decode
different types of objects from a JSON supported format."""

def __init__(self, *args, **kwargs):
super().__init__(object_hook=self.object_hook, *args, **kwargs)

def object_hook(self, obj):
if "signature" in obj:
signature = obj.pop("signature")

try:
class_ = get_class_from_signature(signature)

if hasattr(class_, "from_dict"):
return class_.from_dict(obj)
else:
# Filter keyword arguments
kwargs = {

Check warning on line 91 in rocketpy/_encoders.py

View check run for this annotation

Codecov / codecov/patch

rocketpy/_encoders.py#L91

Added line #L91 was not covered by tests
key: value
for key, value in obj.items()
if key in class_.__init__.__code__.co_varnames
}

return class_(**kwargs)
except (ImportError, AttributeError):
return obj

Check warning on line 99 in rocketpy/_encoders.py

View check run for this annotation

Codecov / codecov/patch

rocketpy/_encoders.py#L97-L99

Added lines #L97 - L99 were not covered by tests
else:
return json.JSONEncoder.default(self, o)
return obj


def get_class_signature(obj):
"""Returns the signature of a class so it can be identified on
decoding. The signature is a dictionary with the module and
name of the object's class as strings.


Parameters
----------
obj : object
Object to get the signature from.

Returns
-------
dict
Signature of the class.
"""
class_ = obj.__class__
name = getattr(class_, '__qualname__', class_.__name__)

return {"module": class_.__module__, "name": name}


def get_class_from_signature(signature):
"""Returns the class from its signature dictionary by
importing the module and loading the class.

Parameters
----------
signature : dict
Signature of the class.

Returns
-------
type
Class defined by the signature.
"""
module = import_module(signature["module"])
inner_class = None

for class_ in signature["name"].split("."):
inner_class = getattr(module, class_)

return inner_class


def remove_circular_references(obj_dict):
"""Removes circular references from a dictionary.

Parameters
----------
obj_dict : dict
Dictionary to remove circular references from.

Returns
-------
dict
Dictionary without circular references.
"""
obj_dict.pop("prints", None)
obj_dict.pop("plots", None)

return obj_dict
110 changes: 103 additions & 7 deletions rocketpy/environment/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -366,12 +366,15 @@
self.standard_g = 9.80665
self.__weather_model_map = WeatherModelMapping()
self.__atm_type_file_to_function_map = {
("forecast", "GFS"): fetch_gfs_file_return_dataset,
("forecast", "NAM"): fetch_nam_file_return_dataset,
("forecast", "RAP"): fetch_rap_file_return_dataset,
("forecast", "HIRESW"): fetch_hiresw_file_return_dataset,
("ensemble", "GEFS"): fetch_gefs_ensemble,
# ("ensemble", "CMC"): fetch_cmc_ensemble,
"forecast": {
"GFS": fetch_gfs_file_return_dataset,
"NAM": fetch_nam_file_return_dataset,
"RAP": fetch_rap_file_return_dataset,
"HIRESW": fetch_hiresw_file_return_dataset,
},
"ensemble": {
"GEFS": fetch_gefs_ensemble,
},
}
self.__standard_atmosphere_layers = {
"geopotential_height": [ # in geopotential m
Expand Down Expand Up @@ -1270,7 +1273,10 @@
self.process_windy_atmosphere(file)
elif type in ["forecast", "reanalysis", "ensemble"]:
dictionary = self.__validate_dictionary(file, dictionary)
fetch_function = self.__atm_type_file_to_function_map.get((type, file))
try:
fetch_function = self.__atm_type_file_to_function_map[type][file]
except KeyError:
fetch_function = None

# Fetches the dataset using OpenDAP protocol or uses the file path
dataset = fetch_function() if fetch_function is not None else file
Expand Down Expand Up @@ -2748,6 +2754,96 @@
arc_seconds = (remainder * 60 - arc_minutes) * 60
return degrees, arc_minutes, arc_seconds

def to_dict(self, include_outputs=False):
env_dict = {
"gravity": self.gravity,
"date": self.date,
"latitude": self.latitude,
"longitude": self.longitude,
"elevation": self.elevation,
"datum": self.datum,
"timezone": self.timezone,
"max_expected_height": self.max_expected_height,
"atmospheric_model_type": self.atmospheric_model_type,
"pressure": self.pressure,
"temperature": self.temperature,
"wind_velocity_x": self.wind_velocity_x,
"wind_velocity_y": self.wind_velocity_y,
"wind_heading": self.wind_heading,
"wind_direction": self.wind_direction,
"wind_speed": self.wind_speed,
Gui-FernandesBR marked this conversation as resolved.
Show resolved Hide resolved
}

if include_outputs:
env_dict["density"] = self.density
env_dict["barometric_height"] = self.barometric_height
env_dict["speed_of_sound"] = self.speed_of_sound
env_dict["dynamic_viscosity"] = self.dynamic_viscosity

Check warning on line 2781 in rocketpy/environment/environment.py

View check run for this annotation

Codecov / codecov/patch

rocketpy/environment/environment.py#L2778-L2781

Added lines #L2778 - L2781 were not covered by tests

return env_dict

@classmethod
def from_dict(cls, data): # pylint: disable=too-many-statements
env = cls(
gravity=data["gravity"],
date=data["date"],
latitude=data["latitude"],
longitude=data["longitude"],
elevation=data["elevation"],
datum=data["datum"],
timezone=data["timezone"],
max_expected_height=data["max_expected_height"],
)
atmospheric_model = data["atmospheric_model_type"]

if atmospheric_model == "standard_atmosphere":
env.set_atmospheric_model("standard_atmosphere")
elif atmospheric_model == "custom_atmosphere":
env.set_atmospheric_model(

Check warning on line 2802 in rocketpy/environment/environment.py

View check run for this annotation

Codecov / codecov/patch

rocketpy/environment/environment.py#L2802

Added line #L2802 was not covered by tests
type="custom_atmosphere",
pressure=data["pressure"],
temperature=data["temperature"],
wind_u=data["wind_velocity_x"],
wind_v=data["wind_velocity_y"],
)
else:
env.__set_pressure_function(data["pressure"])
env.__set_temperature_function(data["temperature"])
env.__set_wind_velocity_x_function(data["wind_velocity_x"])
env.__set_wind_velocity_y_function(data["wind_velocity_y"])
env.__set_wind_heading_function(data["wind_heading"])
env.__set_wind_direction_function(data["wind_direction"])
env.__set_wind_speed_function(data["wind_speed"])
env.elevation = data["elevation"]
env.max_expected_height = data["max_expected_height"]

if atmospheric_model in ("windy", "forecast", "reanalysis", "ensemble"):
env.atmospheric_model_init_date = data["atmospheric_model_init_date"]
env.atmospheric_model_end_date = data["atmospheric_model_end_date"]
env.atmospheric_model_interval = data["atmospheric_model_interval"]
env.atmospheric_model_init_lat = data["atmospheric_model_init_lat"]
env.atmospheric_model_end_lat = data["atmospheric_model_end_lat"]
env.atmospheric_model_init_lon = data["atmospheric_model_init_lon"]
env.atmospheric_model_end_lon = data["atmospheric_model_end_lon"]

Check warning on line 2827 in rocketpy/environment/environment.py

View check run for this annotation

Codecov / codecov/patch

rocketpy/environment/environment.py#L2821-L2827

Added lines #L2821 - L2827 were not covered by tests

if atmospheric_model == "ensemble":
env.level_ensemble = data["level_ensemble"]
env.height_ensemble = data["height_ensemble"]
env.temperature_ensemble = data["temperature_ensemble"]
env.wind_u_ensemble = data["wind_u_ensemble"]
env.wind_v_ensemble = data["wind_v_ensemble"]
env.wind_heading_ensemble = data["wind_heading_ensemble"]
env.wind_direction_ensemble = data["wind_direction_ensemble"]
env.wind_speed_ensemble = data["wind_speed_ensemble"]
env.num_ensemble_members = data["num_ensemble_members"]

Check warning on line 2838 in rocketpy/environment/environment.py

View check run for this annotation

Codecov / codecov/patch

rocketpy/environment/environment.py#L2830-L2838

Added lines #L2830 - L2838 were not covered by tests

env.__reset_barometric_height_function()
env.calculate_density_profile()
env.calculate_speed_of_sound_profile()
env.calculate_dynamic_viscosity()

return env


if __name__ == "__main__":
import doctest
Expand Down
5 changes: 4 additions & 1 deletion rocketpy/environment/environment_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,10 @@ def __check_coordinates_inside_grid(
or lat_index > len(lat_array) - 1
):
raise ValueError(
f"Latitude and longitude pair {(self.latitude, self.longitude)} is outside the grid available in the given file, which is defined by {(lat_array[0], lon_array[0])} and {(lat_array[-1], lon_array[-1])}."
f"Latitude and longitude pair {(self.latitude, self.longitude)} "
"is outside the grid available in the given file, which "
f"is defined by {(lat_array[0], lon_array[0])} and "
f"{(lat_array[-1], lon_array[-1])}."
)

def __localize_input_dates(self):
Expand Down
52 changes: 49 additions & 3 deletions rocketpy/mathutils/function.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
RBFInterpolator,
)

from rocketpy.tools import from_hex_decode, to_hex_encode

from ..plots.plot_helpers import show_or_save_plot

# Numpy 1.x compatibility,
Expand Down Expand Up @@ -711,9 +713,9 @@ def set_discrete(
if func.__dom_dim__ == 1:
xs = np.linspace(lower, upper, samples)
ys = func.get_value(xs.tolist()) if one_by_one else func.get_value(xs)
func.set_source(np.concatenate(([xs], [ys])).transpose())
func.set_interpolation(interpolation)
func.set_extrapolation(extrapolation)
func.__interpolation__ = interpolation
func.__extrapolation__ = extrapolation
func.set_source(np.column_stack((xs, ys)))
elif func.__dom_dim__ == 2:
lower = 2 * [lower] if isinstance(lower, NUMERICAL_TYPES) else lower
upper = 2 * [upper] if isinstance(upper, NUMERICAL_TYPES) else upper
Expand Down Expand Up @@ -3418,6 +3420,50 @@ def __validate_extrapolation(self, extrapolation):
extrapolation = "natural"
return extrapolation

def to_dict(self, include_outputs=False): # pylint: disable=unused-argument
"""Serializes the Function instance to a dictionary.

Returns
-------
dict
A dictionary containing the Function's attributes.
"""
source = self.source

if callable(source):
source = to_hex_encode(source)

return {
"source": source,
"title": self.title,
"inputs": self.__inputs__,
"outputs": self.__outputs__,
"interpolation": self.__interpolation__,
"extrapolation": self.__extrapolation__,
}

@classmethod
def from_dict(cls, func_dict):
Gui-FernandesBR marked this conversation as resolved.
Show resolved Hide resolved
"""Creates a Function instance from a dictionary.

Parameters
----------
func_dict
The JSON like Function dictionary.
"""
source = func_dict["source"]
if func_dict["interpolation"] is None and func_dict["extrapolation"] is None:
source = from_hex_decode(source)

return cls(
source=source,
interpolation=func_dict["interpolation"],
extrapolation=func_dict["extrapolation"],
inputs=func_dict["inputs"],
outputs=func_dict["outputs"],
title=func_dict["title"],
)


def funcify_method(*args, **kwargs): # pylint: disable=too-many-statements
"""Decorator factory to wrap methods as Function objects and save them as
Expand Down
Loading
Loading