Skip to content
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
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 @@ def default(self, o):
return float(o)
elif isinstance(o, np.ndarray):
return o.tolist()
elif isinstance(o, datetime):
return [o.year, o.month, o.day, o.hour]
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)
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__)

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

return encoding
else:
return super().default(o)


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 = {
key: value
for key, value in obj.items()
if key in class_.__init__.__code__.co_varnames
}

return class_(**kwargs)
except (ImportError, AttributeError):
return obj
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 @@ def __initialize_constants(self):
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 @@ def set_atmospheric_model( # pylint: disable=too-many-statements
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 @@ def decimal_degrees_to_arc_seconds(angle):
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,
}

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

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(
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"]

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"]

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):
"""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