diff --git a/.vscode/settings.json b/.vscode/settings.json index 98010d539..e36a259e0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -233,6 +233,7 @@ "polystyle", "powerseries", "Prandtl", + "pressurant", "prettytable", "Projeto", "prometheus", @@ -265,6 +266,7 @@ "SBMT", "scilimits", "searchsorted", + "seblm", "seealso", "setrail", "simplekml", diff --git a/CHANGELOG.md b/CHANGELOG.md index d89921da5..42dbd3b1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,9 +35,12 @@ Attention: The newest changes should be on top --> ### Changed +- MNT: Refactor Tank's testing Assertion with CAD data. [#678](https://github.com/RocketPy-Team/RocketPy/pull/678) ### Fixed +- BUG: Correctly update atmospheric conditions after changing date and location [#743](https://github.com/RocketPy-Team/RocketPy/pull/743) + ## [v1.7.0] - 2024-11-30 diff --git a/rocketpy/environment/environment.py b/rocketpy/environment/environment.py index 394275763..dbabeee78 100644 --- a/rocketpy/environment/environment.py +++ b/rocketpy/environment/environment.py @@ -688,7 +688,9 @@ def set_date(self, date, timezone="UTC"): "Ensemble", ]: self.set_atmospheric_model( - self.atmospheric_model_file, self.atmospheric_model_dict + type=self.atmospheric_model_type, + file=self.atmospheric_model_file, + dictionary=self.atmospheric_model_dict, ) def set_location(self, latitude, longitude): @@ -726,7 +728,9 @@ def set_location(self, latitude, longitude): "Ensemble", ]: self.set_atmospheric_model( - self.atmospheric_model_file, self.atmospheric_model_dict + type=self.atmospheric_model_type, + file=self.atmospheric_model_file, + dictionary=self.atmospheric_model_dict, ) def set_gravity_model(self, gravity=None): diff --git a/rocketpy/motors/tank.py b/rocketpy/motors/tank.py index 29aebfddc..982380891 100644 --- a/rocketpy/motors/tank.py +++ b/rocketpy/motors/tank.py @@ -23,7 +23,7 @@ class Tank(ABC): Name of the tank. Tank.geometry : TankGeometry Geometry of the tank. - Tank.flux_time : float, tuple of float, optional + Tank.flux_time : float, tuple of float Tank flux time in seconds. Tank.liquid : Fluid Liquid inside the tank as a Fluid object. @@ -84,13 +84,14 @@ def __init__(self, name, geometry, flux_time, liquid, gas, discretize=100): Name of the tank. geometry : TankGeometry Geometry of the tank. - flux_time : float, tuple of float, optional - Tank flux time in seconds. It is the time range in which the tank - flux is being analyzed. In general, during this time, the tank is - being filled or emptied.If a float is given, the flux time is - assumed to be between 0 and the given float, in seconds. If a tuple - of float is given, the flux time is assumed to be between the first - and second elements of the tuple. + flux_time : float, tuple of float + Tank flux time in seconds. Time interval that the fluid flux is + being analyzed. If a float is given, the flux time is assumed to + be between 0 and the given float, in seconds. If a tuple of float + is given, the flux time is assumed to be between the first and + second elements of the tuple. + Before the start time, the tank properties are kept at their + initial state. After the final time, their final state is kept. gas : Fluid Gas inside the tank as a Fluid object. liquid : Fluid @@ -335,7 +336,7 @@ def center_of_mass(self): return center_of_mass - @funcify_method("Time (s)", "Inertia tensor of liquid (kg*m²)") + @funcify_method("Time (s)", "Liquid Inertia (kg*m²)") def liquid_inertia(self): """ Returns the inertia tensor of the liquid portion of the tank @@ -361,7 +362,7 @@ def liquid_inertia(self): return self.liquid.density * Ix_volume - @funcify_method("Time (s)", "inertia tensor of gas (kg*m^2)") + @funcify_method("Time (s)", "Gas Inertia (kg*m^2)") def gas_inertia(self): """ Returns the inertia tensor of the gas portion of the tank @@ -387,7 +388,7 @@ def gas_inertia(self): return self.gas.density * inertia_volume - @funcify_method("Time (s)", "inertia tensor (kg*m^2)") + @funcify_method("Time (s)", "Fluid Inertia (kg*m^2)") def inertia(self): """ Returns the inertia tensor of the tank's fluids as a function of @@ -493,6 +494,17 @@ def draw(self, *, filename=None): """ self.plots.draw(filename=filename) + def info(self): + """Prints out a summary of the tank properties.""" + self.prints.all() + + def all_info(self): + """Prints out detailed information and plots of the tank + properties. + """ + self.prints.all() + self.plots.all() + class MassFlowRateBasedTank(Tank): """Class to define a tank based on mass flow rates inputs. This class @@ -527,14 +539,14 @@ def __init__( Name of the tank. geometry : TankGeometry Geometry of the tank. - flux_time : float, tuple of float, optional - Tank flux time in seconds. It is the time range in which the tank - flux is being analyzed. In general, during this time, the tank is - being filled or emptied. - If a float is given, the flux time is assumed to be between 0 and - the given float, in seconds. If a tuple of float is given, the flux - time is assumed to be between the first and second elements of the - tuple. + flux_time : float, tuple of float + Tank flux time in seconds. Time interval that the fluid flux is + being analyzed. If a float is given, the flux time is assumed to + be between 0 and the given float, in seconds. If a tuple of float + is given, the flux time is assumed to be between the first and + second elements of the tuple. + Before the start time, the tank properties are kept at their + initial state. After the final time, their final state is kept. liquid : Fluid Liquid inside the tank as a Fluid object. gas : Fluid @@ -586,28 +598,28 @@ def __init__( self.liquid_mass_flow_rate_in = Function( liquid_mass_flow_rate_in, inputs="Time (s)", - outputs="Mass Flow Rate (kg/s)", + outputs="Liquid Mass Flow Rate In (kg/s)", interpolation="linear", extrapolation="zero", ) self.gas_mass_flow_rate_in = Function( gas_mass_flow_rate_in, inputs="Time (s)", - outputs="Mass Flow Rate (kg/s)", + outputs="Gas Mass Flow Rate In (kg/s)", interpolation="linear", extrapolation="zero", ) self.liquid_mass_flow_rate_out = Function( liquid_mass_flow_rate_out, inputs="Time (s)", - outputs="Mass Flow Rate (kg/s)", + outputs="Liquid Mass Flow Rate Out (kg/s)", interpolation="linear", extrapolation="zero", ) self.gas_mass_flow_rate_out = Function( gas_mass_flow_rate_out, inputs="Time (s)", - outputs="Mass Flow Rate (kg/s)", + outputs="Gas Mass Flow Rate Out (kg/s)", interpolation="linear", extrapolation="zero", ) @@ -620,7 +632,7 @@ def __init__( self._check_volume_bounds() self._check_height_bounds() - @funcify_method("Time (s)", "Mass (kg)") + @funcify_method("Time (s)", "Fluid Mass (kg)") def fluid_mass(self): """ Returns the total mass of liquid and gases inside the tank as a @@ -633,7 +645,7 @@ def fluid_mass(self): """ return self.liquid_mass + self.gas_mass - @funcify_method("Time (s)", "Mass (kg)") + @funcify_method("Time (s)", "Liquid Mass (kg)") def liquid_mass(self): """ Returns the mass of the liquid as a function of time by integrating @@ -644,7 +656,9 @@ def liquid_mass(self): Function Mass of the liquid as a function of time. """ - liquid_flow = self.net_liquid_flow_rate.integral_function() + liquid_flow = self.net_liquid_flow_rate.integral_function( + datapoints=self.discretize + ) liquid_mass = self.initial_liquid_mass + liquid_flow if (liquid_mass < 0).any(): raise ValueError( @@ -657,7 +671,7 @@ def liquid_mass(self): ) return liquid_mass - @funcify_method("Time (s)", "Mass (kg)") + @funcify_method("Time (s)", "Gas Mass (kg)") def gas_mass(self): """ Returns the mass of the gas as a function of time by integrating @@ -668,7 +682,7 @@ def gas_mass(self): Function Mass of the gas as a function of time. """ - gas_flow = self.net_gas_flow_rate.integral_function() + gas_flow = self.net_gas_flow_rate.integral_function(datapoints=self.discretize) gas_mass = self.initial_gas_mass + gas_flow if (gas_mass < -1e-6).any(): # -1e-6 is to avoid numerical errors raise ValueError( @@ -682,7 +696,7 @@ def gas_mass(self): return gas_mass - @funcify_method("Time (s)", "liquid mass flow rate (kg/s)", extrapolation="zero") + @funcify_method("Time (s)", "Liquid Mass Flow Rate (kg/s)", extrapolation="zero") def net_liquid_flow_rate(self): """ Returns the net mass flow rate of liquid as a function of time. @@ -696,7 +710,7 @@ def net_liquid_flow_rate(self): """ return self.liquid_mass_flow_rate_in - self.liquid_mass_flow_rate_out - @funcify_method("Time (s)", "gas mass flow rate (kg/s)", extrapolation="zero") + @funcify_method("Time (s)", "Gas Mass Flow Rate (kg/s)", extrapolation="zero") def net_gas_flow_rate(self): """ Returns the net mass flow rate of gas as a function of time. @@ -710,7 +724,7 @@ def net_gas_flow_rate(self): """ return self.gas_mass_flow_rate_in - self.gas_mass_flow_rate_out - @funcify_method("Time (s)", "mass flow rate (kg/s)", extrapolation="zero") + @funcify_method("Time (s)", "Net Mass Flow Rate (kg/s)", extrapolation="zero") def net_mass_flow_rate(self): """ Returns the net mass flow rate of the tank as a function of time. @@ -724,7 +738,7 @@ def net_mass_flow_rate(self): """ return self.net_liquid_flow_rate + self.net_gas_flow_rate - @funcify_method("Time (s)", "Volume (m³)") + @funcify_method("Time (s)", "Fluid Volume (m³)") def fluid_volume(self): """ Returns the volume total fluid volume inside the tank as a @@ -738,7 +752,7 @@ def fluid_volume(self): """ return self.liquid_volume + self.gas_volume - @funcify_method("Time (s)", "Volume (m³)") + @funcify_method("Time (s)", "Liquid Volume (m³)") def liquid_volume(self): """ Returns the volume of the liquid as a function of time. @@ -750,7 +764,7 @@ def liquid_volume(self): """ return self.liquid_mass / self.liquid.density - @funcify_method("Time (s)", "Volume (m³)") + @funcify_method("Time (s)", "Gas Volume (m³)") def gas_volume(self): """ Returns the volume of the gas as a function of time. @@ -762,7 +776,7 @@ def gas_volume(self): """ return self.gas_mass / self.gas.density - @funcify_method("Time (s)", "Height (m)") + @funcify_method("Time (s)", "Liquid Height (m)") def liquid_height(self): """ Returns the liquid level as a function of time. This @@ -799,7 +813,7 @@ def liquid_height(self): return liquid_height - @funcify_method("Time (s)", "Height (m)") + @funcify_method("Time (s)", "Gas Height (m)") def gas_height(self): """ Returns the gas level as a function of time. This @@ -864,13 +878,14 @@ def __init__( Name of the tank. geometry : TankGeometry Geometry of the tank. - flux_time : float, tuple of float, optional - Tank flux time in seconds. It is the time range in which the tank - flux is being analyzed. In general, during this time, the tank is - being filled or emptied. If a float is given, the flux time is - assumed to be between 0 and the given float, in seconds. If a tuple - of float is given, the flux time is assumed to be between the first - and second elements of the tuple. + flux_time : float, tuple of float + Tank flux time in seconds. Time interval that the fluid flux is + being analyzed. If a float is given, the flux time is assumed to + be between 0 and the given float, in seconds. If a tuple of float + is given, the flux time is assumed to be between the first and + second elements of the tuple. + Before the start time, the tank properties are kept at their + initial state. After the final time, their final state is kept. liquid : Fluid Liquid inside the tank as a Fluid object. gas : Fluid @@ -902,7 +917,7 @@ def __init__( self._check_volume_bounds() self._check_height_bounds() - @funcify_method("Time (s)", "Mass (kg)") + @funcify_method("Time (s)", "Fluid Mass (kg)") def fluid_mass(self): """ Returns the total mass of liquid and gases inside the tank as a @@ -915,7 +930,7 @@ def fluid_mass(self): """ return self.liquid_mass + self.gas_mass - @funcify_method("Time (s)", "Mass flow rate (kg/s)") + @funcify_method("Time (s)", "Net Mass Flow Rate (kg/s)") def net_mass_flow_rate(self): """ Returns the net mass flow rate of the tank as a function of time by @@ -928,7 +943,7 @@ def net_mass_flow_rate(self): """ return self.fluid_mass.derivative_function() - @funcify_method("Time (s)", "Volume (m³)") + @funcify_method("Time (s)", "Fluid Volume (m³)") def fluid_volume(self): """ Returns the volume total fluid volume inside the tank as a @@ -944,7 +959,7 @@ def fluid_volume(self): self.gas_volume ) - @funcify_method("Time (s)", "Volume (m³)") + @funcify_method("Time (s)", "Liquid Volume (m³)") def liquid_volume(self): """ Returns the volume of the liquid as a function of time. The @@ -958,7 +973,7 @@ def liquid_volume(self): """ return -(self.ullage - self.geometry.total_volume) - @funcify_method("Time (s)", "Volume (m³)") + @funcify_method("Time (s)", "Gas Volume (m³)") def gas_volume(self): """ Returns the volume of the gas as a function of time. From the @@ -971,7 +986,7 @@ def gas_volume(self): """ return self.ullage - @funcify_method("Time (s)", "Mass (kg)") + @funcify_method("Time (s)", "Gas Mass (kg)") def gas_mass(self): """ Returns the mass of the gas as a function of time. @@ -983,7 +998,7 @@ def gas_mass(self): """ return self.gas_volume * self.gas.density - @funcify_method("Time (s)", "Mass (kg)") + @funcify_method("Time (s)", "Liquid Mass (kg)") def liquid_mass(self): """ Returns the mass of the liquid as a function of time. @@ -995,7 +1010,7 @@ def liquid_mass(self): """ return self.liquid_volume * self.liquid.density - @funcify_method("Time (s)", "Height (m)") + @funcify_method("Time (s)", "Liquid Height (m)") def liquid_height(self): """ Returns the liquid level as a function of time. This @@ -1009,7 +1024,7 @@ def liquid_height(self): """ return self.geometry.inverse_volume.compose(self.liquid_volume) - @funcify_method("Time (s)", "Height (m)", "linear") + @funcify_method("Time (s)", "Gas Height (m)", "linear") def gas_height(self): """ Returns the gas level as a function of time. This height is measured @@ -1059,13 +1074,14 @@ def __init__( Name of the tank. geometry : TankGeometry Geometry of the tank. - flux_time : float, tuple of float, optional - Tank flux time in seconds. It is the time range in which the tank - flux is being analyzed. In general, during this time, the tank is - being filled or emptied. If a float is given, the flux time is - assumed to be between 0 and the given float, in seconds. If a tuple - of float is given, the flux time is assumed to be between the first - and second elements of the tuple. + flux_time : float, tuple of float + Tank flux time in seconds. Time interval that the fluid flux is + being analyzed. If a float is given, the flux time is assumed to + be between 0 and the given float, in seconds. If a tuple of float + is given, the flux time is assumed to be between the first and + second elements of the tuple. + Before the start time, the tank properties are kept at their + initial state. After the final time, their final state is kept. liquid : Fluid Liquid inside the tank as a Fluid object. gas : Fluid @@ -1096,7 +1112,7 @@ def __init__( self._check_height_bounds() self._check_volume_bounds() - @funcify_method("Time (s)", "Mass (kg)") + @funcify_method("Time (s)", "Fluid Mass (kg)") def fluid_mass(self): """ Returns the total mass of liquid and gases inside the tank as a @@ -1112,7 +1128,7 @@ def fluid_mass(self): sum_mass.set_discrete_based_on_model(self.liquid_level) return sum_mass - @funcify_method("Time (s)", "Mass flow rate (kg/s)") + @funcify_method("Time (s)", "Net Mass Flow Rate (kg/s)") def net_mass_flow_rate(self): """ Returns the net mass flow rate of the tank as a function of time by @@ -1125,7 +1141,7 @@ def net_mass_flow_rate(self): """ return self.fluid_mass.derivative_function() - @funcify_method("Time (s)", "Volume (m³)") + @funcify_method("Time (s)", "Fluid Volume (m³)") def fluid_volume(self): """ Returns the volume total fluid volume inside the tank as a @@ -1148,7 +1164,7 @@ def fluid_volume(self): ) return volume - @funcify_method("Time (s)", "Volume (m³)") + @funcify_method("Time (s)", "Liquid Volume (m³)") def liquid_volume(self): """ Returns the volume of the liquid as a function of time. @@ -1160,7 +1176,7 @@ def liquid_volume(self): """ return self.geometry.volume.compose(self.liquid_height) - @funcify_method("Time (s)", "Volume (m³)") + @funcify_method("Time (s)", "Gas Volume (m³)") def gas_volume(self): """ Returns the volume of the gas as a function of time. The gas volume @@ -1178,7 +1194,7 @@ def gas_volume(self): func -= self.liquid_volume return func - @funcify_method("Time (s)", "Height (m)") + @funcify_method("Time (s)", "Liquid Height (m)") def liquid_height(self): """ Returns the liquid level as a function of time. This height is @@ -1191,7 +1207,7 @@ def liquid_height(self): """ return self.liquid_level - @funcify_method("Time (s)", "Mass (kg)") + @funcify_method("Time (s)", "Gas Mass (kg)") def gas_mass(self): """ Returns the mass of the gas as a function of time. @@ -1203,7 +1219,7 @@ def gas_mass(self): """ return self.gas_volume * self.gas.density - @funcify_method("Time (s)", "Mass (kg)") + @funcify_method("Time (s)", "Liquid Mass (kg)") def liquid_mass(self): """ Returns the mass of the liquid as a function of time. @@ -1215,7 +1231,7 @@ def liquid_mass(self): """ return self.liquid_volume * self.liquid.density - @funcify_method("Time (s)", "Height (m)", "linear") + @funcify_method("Time (s)", "Gas Height (m)", "linear") def gas_height(self): """ Returns the gas level as a function of time. This @@ -1268,13 +1284,14 @@ def __init__( Name of the tank. geometry : TankGeometry Geometry of the tank. - flux_time : float, tuple of float, optional - Tank flux time in seconds. It is the time range in which the tank - flux is being analyzed. In general, during this time, the tank is - being filled or emptied. If a float is given, the flux time is - assumed to be between 0 and the given float, in seconds. If a tuple - of float is given, the flux time is assumed to be between the first - and second elements of the tuple. + flux_time : float, tuple of float + Tank flux time in seconds. Time interval that the fluid flux is + being analyzed. If a float is given, the flux time is assumed to + be between 0 and the given float, in seconds. If a tuple of float + is given, the flux time is assumed to be between the first and + second elements of the tuple. + Before the start time, the tank properties are kept at their + initial state. After the final time, their final state is kept. liquid : Fluid Liquid inside the tank as a Fluid object. gas : Fluid @@ -1311,7 +1328,7 @@ def __init__( self._check_volume_bounds() self._check_height_bounds() - @funcify_method("Time (s)", "Mass (kg)") + @funcify_method("Time (s)", "Fluid Mass (kg)") def fluid_mass(self): """ Returns the total mass of liquid and gases inside the tank as @@ -1324,7 +1341,7 @@ def fluid_mass(self): """ return self.liquid_mass + self.gas_mass - @funcify_method("Time (s)", "Mass flow rate (kg/s)") + @funcify_method("Time (s)", "Net Mass Flow Rate (kg/s)") def net_mass_flow_rate(self): """ Returns the net mass flow rate of the tank as a function of time @@ -1337,7 +1354,7 @@ def net_mass_flow_rate(self): """ return self.fluid_mass.derivative_function() - @funcify_method("Time (s)", "Mass (kg)") + @funcify_method("Time (s)", "Liquid Mass (kg)") def liquid_mass(self): """ Returns the mass of the liquid as a function of time. @@ -1349,7 +1366,7 @@ def liquid_mass(self): """ return self.liquid_mass - @funcify_method("Time (s)", "Mass (kg)") + @funcify_method("Time (s)", "Gas Mass (kg)") def gas_mass(self): """ Returns the mass of the gas as a function of time. @@ -1361,7 +1378,7 @@ def gas_mass(self): """ return self.gas_mass - @funcify_method("Time (s)", "Volume (m³)") + @funcify_method("Time (s)", "Fluid Volume (m³)") def fluid_volume(self): """ Returns the volume total fluid volume inside the tank as a @@ -1389,7 +1406,7 @@ def fluid_volume(self): return fluid_volume - @funcify_method("Time (s)", "Volume (m³)") + @funcify_method("Time (s)", "Gas Volume (m³)") def gas_volume(self): """ Returns the volume of the gas as a function of time. @@ -1401,7 +1418,7 @@ def gas_volume(self): """ return self.gas_mass / self.gas.density - @funcify_method("Time (s)", "Volume (m³)") + @funcify_method("Time (s)", "Liquid Volume (m³)") def liquid_volume(self): """ Returns the volume of the liquid as a function of time. @@ -1413,7 +1430,7 @@ def liquid_volume(self): """ return self.liquid_mass / self.liquid.density - @funcify_method("Time (s)", "Height (m)") + @funcify_method("Time (s)", "Liquid Height (m)") def liquid_height(self): """ Returns the liquid level as a function of time. This @@ -1448,7 +1465,7 @@ def liquid_height(self): return liquid_height - @funcify_method("Time (s)", "Height (m)") + @funcify_method("Time (s)", "Gas Height (m)") def gas_height(self): """ Returns the gas level as a function of time. This diff --git a/rocketpy/plots/tank_geometry_plots.py b/rocketpy/plots/tank_geometry_plots.py index d9bf141bf..8c6ca9afa 100644 --- a/rocketpy/plots/tank_geometry_plots.py +++ b/rocketpy/plots/tank_geometry_plots.py @@ -40,6 +40,6 @@ def all(self): ------- None """ - self.radius() - self.area() - self.volume() + self.tank_geometry.radius.plot(equal_axis=True) + self.tank_geometry.area() + self.tank_geometry.volume() diff --git a/rocketpy/plots/tank_plots.py b/rocketpy/plots/tank_plots.py index 6f1c0e5f2..9c6ebb78a 100644 --- a/rocketpy/plots/tank_plots.py +++ b/rocketpy/plots/tank_plots.py @@ -2,6 +2,8 @@ import numpy as np from matplotlib.patches import Polygon +from rocketpy.mathutils.function import Function + from .plot_helpers import show_or_save_plot @@ -30,6 +32,7 @@ def __init__(self, tank): self.tank = tank self.name = tank.name + self.flux_time = tank.flux_time self.geometry = tank.geometry def _generate_tank(self, translate=(0, 0), csys=1): @@ -101,6 +104,82 @@ def draw(self, *, filename=None): ax.set_ylim(-1.5 * y_max, 1.5 * y_max) show_or_save_plot(filename) + def fluid_volume(self, filename=None): + """Plots both the liquid and gas fluid volumes. + + Parameters + ---------- + filename : str | None, optional + The path the plot should be saved to. By default None, in which case + the plot will be shown instead of saved. Supported file endings are: + eps, jpg, jpeg, pdf, pgf, png, ps, raw, rgba, svg, svgz, tif, tiff + and webp (these are the formats supported by matplotlib). + """ + _, ax = Function.compare_plots( + [self.tank.liquid_volume, self.tank.gas_volume], + *self.flux_time, + title="Fluid Volume (m^3) x Time (s)", + xlabel="Time (s)", + ylabel="Volume (m^3)", + show=False, + return_object=True, + ) + ax.legend(["Liquid", "Gas"]) + show_or_save_plot(filename) + + def fluid_height(self, filename=None): + """Plots both the liquid and gas fluid height. + + Parameters + ---------- + filename : str | None, optional + The path the plot should be saved to. By default None, in which case + the plot will be shown instead of saved. Supported file endings are: + eps, jpg, jpeg, pdf, pgf, png, ps, raw, rgba, svg, svgz, tif, tiff + and webp (these are the formats supported by matplotlib). + """ + _, ax = Function.compare_plots( + [self.tank.liquid_height, self.tank.gas_height], + *self.flux_time, + title="Fluid Height (m) x Time (s)", + xlabel="Time (s)", + ylabel="Height (m)", + show=False, + return_object=True, + ) + ax.legend(["Liquid", "Gas"]) + show_or_save_plot(filename) + + def fluid_center_of_mass(self, filename=None): + """Plots the gas, liquid and combined center of mass. + + Parameters + ---------- + filename : str | None, optional + The path the plot should be saved to. By default None, in which case + the plot will be shown instead of saved. Supported file endings are: + eps, jpg, jpeg, pdf, pgf, png, ps, raw, rgba, svg, svgz, tif, tiff + and webp (these are the formats supported by matplotlib). + """ + _, ax = Function.compare_plots( + [ + self.tank.liquid_center_of_mass, + self.tank.gas_center_of_mass, + self.tank.center_of_mass, + ], + *self.flux_time, + title="Fluid Center of Mass (m) x Time (s)", + xlabel="Time (s)", + ylabel="Center of Mass (m)", + show=False, + return_object=True, + ) + # Change style of lines + ax.lines[0].set_linestyle("--") + ax.lines[1].set_linestyle("-.") + ax.legend(["Liquid", "Gas", "Total"]) + show_or_save_plot(filename) + def all(self): """Prints out all graphs available about the Tank. It simply calls all the other plotter methods in this class. @@ -109,3 +188,10 @@ def all(self): ------- None """ + self.draw() + self.tank.fluid_mass.plot(*self.flux_time) + self.tank.net_mass_flow_rate.plot(*self.flux_time) + self.fluid_height() + self.fluid_volume() + self.fluid_center_of_mass() + self.tank.inertia.plot(*self.flux_time) diff --git a/rocketpy/prints/fluid_prints.py b/rocketpy/prints/fluid_prints.py index a90aac229..7b2d7cac8 100644 --- a/rocketpy/prints/fluid_prints.py +++ b/rocketpy/prints/fluid_prints.py @@ -32,3 +32,5 @@ def all(self): ------- None """ + print(f"Name: {self.fluid.name}") + print(f"Density: {self.fluid.density:.4f} kg/m^3") diff --git a/rocketpy/prints/tank_prints.py b/rocketpy/prints/tank_prints.py index 41732c6cf..7c61aa67f 100644 --- a/rocketpy/prints/tank_prints.py +++ b/rocketpy/prints/tank_prints.py @@ -25,6 +25,37 @@ def __init__( """ self.tank = tank + def fluid_parameters(self): + """Prints out the fluid parameters of the Tank. + + Returns + ------- + None + """ + print(f"Tank '{self.tank.name}' Fluid Parameters:") + print("\nLiquid Fluid") + self.tank.liquid.prints.all() + print("\nGas Fluid") + self.tank.gas.prints.all() + + def mass_flux(self): + """Prints out the mass flux of the Tank. + + Returns + ------- + None + """ + initial_time, final_time = self.tank.flux_time + print(f"\nTank '{self.tank.name}' Mass Flux Data:") + print(f"\nInitial Quantities at t = {initial_time:.2f} s:") + print(f"Initial Fluid Mass: {self.tank.fluid_mass(initial_time):.3e} kg") + print(f"Initial Liquid Volume: {self.tank.liquid_volume(initial_time):.3e} m^3") + print(f"Initial Liquid Level: {self.tank.liquid_height(initial_time):.3e} m") + print(f"\nFinal Quantities at t = {final_time:.2f} s:") + print(f"Final Fluid Mass: {self.tank.fluid_mass(final_time):.3e} kg") + print(f"Final Liquid Volume: {self.tank.liquid_volume(final_time):.3e} m^3") + print(f"Final Liquid Level: {self.tank.liquid_height(final_time):.3e} m") + def all(self): """Prints out all data available about the Tank. @@ -32,3 +63,7 @@ def all(self): ------- None """ + print(f"\nTank '{self.tank.name}' Data:\n") + self.tank.geometry.prints.all() + self.fluid_parameters() + self.mass_flux() diff --git a/tests/conftest.py b/tests/conftest.py index a23f5fd89..31f53484a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,6 +9,7 @@ "tests.fixtures.motor.hybrid_fixtures", "tests.fixtures.motor.solid_motor_fixtures", "tests.fixtures.motor.tanks_fixtures", + "tests.fixtures.motor.tank_geometry_fixtures", "tests.fixtures.motor.generic_motor_fixtures", "tests.fixtures.parachutes.parachute_fixtures", "tests.fixtures.rockets.rocket_fixtures", diff --git a/tests/fixtures/hybrid/__init__.py b/tests/fixtures/hybrid/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/fixtures/hybrid/hybrid_fixtures.py b/tests/fixtures/hybrid/hybrid_fixtures.py deleted file mode 100644 index 0f5a872fb..000000000 --- a/tests/fixtures/hybrid/hybrid_fixtures.py +++ /dev/null @@ -1,276 +0,0 @@ -import numpy as np -import pytest - -from rocketpy import ( - CylindricalTank, - Fluid, - Function, - HybridMotor, - LevelBasedTank, - LiquidMotor, - MassBasedTank, - SphericalTank, - UllageBasedTank, -) - - -@pytest.fixture -def pressurant_fluid(): - """An example of a pressurant fluid as N2 gas at - 273.15K and 30MPa. - - Returns - ------- - rocketpy.Fluid - An object of the Fluid class. - """ - return Fluid(name="N2", density=300) - - -@pytest.fixture -def fuel_pressurant(): - """An example of a pressurant fluid as N2 gas at - 273.15K and 2MPa. - - Returns - ------- - rocketpy.Fluid - An object of the Fluid class. - """ - return Fluid(name="N2", density=25) - - -@pytest.fixture -def oxidizer_pressurant(): - """An example of a pressurant fluid as N2 gas at - 273.15K and 3MPa. - - Returns - ------- - rocketpy.Fluid - An object of the Fluid class. - """ - return Fluid(name="N2", density=35) - - -@pytest.fixture -def fuel_fluid(): - """An example of propane as fuel fluid at - 273.15K and 2MPa. - - Returns - ------- - rocketpy.Fluid - An object of the Fluid class. - """ - return Fluid(name="Propane", density=500) - - -@pytest.fixture -def oxidizer_fluid(): - """An example of liquid oxygen as oxidizer fluid at - 100K and 3MPa. - - Returns - ------- - rocketpy.Fluid - An object of the Fluid class. - """ - return Fluid(name="O2", density=1000) - - -@pytest.fixture -def pressurant_tank(pressurant_fluid): - """An example of a pressurant cylindrical tank with spherical - caps. - - Parameters - ---------- - pressurant_fluid : rocketpy.Fluid - Pressurizing fluid. This is a pytest fixture. - - Returns - ------- - rocketpy.MassBasedTank - An object of the CylindricalTank class. - """ - geometry = CylindricalTank(0.135 / 2, 0.981, spherical_caps=True) - pressurant_tank = MassBasedTank( - name="Pressure Tank", - geometry=geometry, - liquid_mass=0, - flux_time=(8, 20), - gas_mass="data/rockets/berkeley/pressurantMassFiltered.csv", - gas=pressurant_fluid, - liquid=pressurant_fluid, - ) - - return pressurant_tank - - -@pytest.fixture -def fuel_tank(fuel_fluid, fuel_pressurant): - """An example of a fuel cylindrical tank with spherical - caps. - - Parameters - ---------- - fuel_fluid : rocketpy.Fluid - Fuel fluid of the tank. This is a pytest fixture. - fuel_pressurant : rocketpy.Fluid - Pressurizing fluid of the fuel tank. This is a pytest - fixture. - - Returns - ------- - rocketpy.UllageBasedTank - """ - geometry = CylindricalTank(0.0744, 0.8068, spherical_caps=True) - ullage = ( - -Function("data/rockets/berkeley/test124_Propane_Volume.csv") * 1e-3 - + geometry.total_volume - ) - fuel_tank = UllageBasedTank( - name="Propane Tank", - flux_time=(8, 20), - geometry=geometry, - liquid=fuel_fluid, - gas=fuel_pressurant, - ullage=ullage, - ) - - return fuel_tank - - -@pytest.fixture -def oxidizer_tank(oxidizer_fluid, oxidizer_pressurant): - """An example of a oxidizer cylindrical tank with spherical - caps. - - Parameters - ---------- - oxidizer_fluid : rocketpy.Fluid - Oxidizer fluid of the tank. This is a pytest fixture. - oxidizer_pressurant : rocketpy.Fluid - Pressurizing fluid of the oxidizer tank. This is a pytest - fixture. - - Returns - ------- - rocketpy.UllageBasedTank - """ - geometry = CylindricalTank(0.0744, 0.8068, spherical_caps=True) - ullage = ( - -Function("data/rockets/berkeley/test124_Lox_Volume.csv") * 1e-3 - + geometry.total_volume - ) - oxidizer_tank = UllageBasedTank( - name="Lox Tank", - flux_time=(8, 20), - geometry=geometry, - liquid=oxidizer_fluid, - gas=oxidizer_pressurant, - ullage=ullage, - ) - - return oxidizer_tank - - -@pytest.fixture -def liquid_motor(pressurant_tank, fuel_tank, oxidizer_tank): - """An example of a liquid motor with pressurant, fuel and oxidizer tanks. - - Parameters - ---------- - pressurant_tank : rocketpy.MassBasedTank - Tank that contains pressurizing fluid. This is a pytest fixture. - fuel_tank : rocketpy.UllageBasedTank - Tank that contains the motor fuel. This is a pytest fixture. - oxidizer_tank : rocketpy.UllageBasedTank - Tank that contains the motor oxidizer. This is a pytest fixture. - - Returns - ------- - rocketpy.LiquidMotor - """ - liquid_motor = LiquidMotor( - thrust_source="data/rockets/berkeley/test124_Thrust_Curve.csv", - burn_time=(8, 20), - dry_mass=10, - dry_inertia=(5, 5, 0.2), - center_of_dry_mass_position=0, - nozzle_position=-1.364, - nozzle_radius=0.069 / 2, - ) - liquid_motor.add_tank(pressurant_tank, position=2.007) - liquid_motor.add_tank(fuel_tank, position=-1.048) - liquid_motor.add_tank(oxidizer_tank, position=0.711) - - return liquid_motor - - -@pytest.fixture -def spherical_oxidizer_tank(oxidizer_fluid, oxidizer_pressurant): - """An example of a oxidizer spherical tank. - - Parameters - ---------- - oxidizer_fluid : rocketpy.Fluid - Oxidizer fluid of the tank. This is a pytest fixture. - oxidizer_pressurant : rocketpy.Fluid - Pressurizing fluid of the oxidizer tank. This is a pytest - fixture. - - Returns - ------- - rocketpy.UllageBasedTank - """ - geometry = SphericalTank(0.05) - liquid_level = Function(lambda t: 0.1 * np.exp(-t / 2) - 0.05) - oxidizer_tank = LevelBasedTank( - name="Lox Tank", - flux_time=10, - geometry=geometry, - liquid=oxidizer_fluid, - gas=oxidizer_pressurant, - liquid_height=liquid_level, - ) - - return oxidizer_tank - - -@pytest.fixture -def hybrid_motor(spherical_oxidizer_tank): - """An example of a hybrid motor with spherical oxidizer - tank and fuel grains. - - Parameters - ---------- - spherical_oxidizer_tank : rocketpy.LevelBasedTank - Example Tank that contains the motor oxidizer. This is a - pytest fixture. - - Returns - ------- - rocketpy.HybridMotor - """ - motor = HybridMotor( - thrust_source=lambda t: 2000 - 100 * t, - burn_time=10, - center_of_dry_mass_position=0, - dry_inertia=(4, 4, 0.1), - dry_mass=8, - grain_density=1700, - grain_number=4, - grain_initial_height=0.1, - grain_separation=0, - grain_initial_inner_radius=0.04, - grain_outer_radius=0.1, - nozzle_position=-0.4, - nozzle_radius=0.07, - grains_center_of_mass_position=-0.1, - ) - - motor.add_tank(spherical_oxidizer_tank, position=0.3) - - return motor diff --git a/tests/fixtures/motor/data/cylindrical_oxidizer_tank_expected.csv b/tests/fixtures/motor/data/cylindrical_oxidizer_tank_expected.csv new file mode 100644 index 000000000..22652a6ce --- /dev/null +++ b/tests/fixtures/motor/data/cylindrical_oxidizer_tank_expected.csv @@ -0,0 +1,26 @@ +level_height,volume,center_of_mass,inertia +-0.4034,0.0,0.0,0.0 +-0.3697833333333333,0.00022435619637196954,-0.38148562747252746,3.2817704908519726e-05 +-0.33616666666666667,0.0007382946059424683,-0.36099299529814066,9.719213246759829e-05 +-0.30255,0.0013224978119496303,-0.3425968277449047,0.00015763404974113978 +-0.26893333333333336,0.0019070864787702323,-0.3251687335359676,0.00020622875048451338 +-0.23531666666666667,0.002491675145590835,-0.30803145186305303,0.0002442533372601051 +-0.2017,0.0030762638124114438,-0.29101919163369117,0.00027302907431388793 +-0.16808333333333333,0.003660852479232046,-0.27407206014303415,0.00029387722589183405 +-0.13446666666666668,0.004245441146052648,-0.2571631531039504,0.0003081190562399166 +-0.10085,0.004830029812873251,-0.24027859135913576,0.0003170758296041081 +-0.06723333333333337,0.005414618479693852,-0.22341048959783028,0.0003220688102303815 +-0.033616666666666684,0.005999207146514448,-0.20655403602428474,0.0003244192623647094 +0.0,0.006583795813335056,-0.1897061278394083,0.00032544845025306453 +0.03361666666666663,0.007168384480155651,-0.1728646743891429,0.0003264776381414197 +0.06723333333333331,0.00775297314697626,-0.156028215576324,0.0003288280902757475 +0.10085,0.008337561813796869,-0.13919570080300325,0.0003338210709020209 +0.13446666666666662,0.008922150480617464,-0.1223663548163549,0.00034277784426621244 +0.16808333333333325,0.009506739147438074,-0.10553959305102469,0.00035701967461429507 +0.2017,0.010091327814258668,-0.08871496639669146,0.00037786782619224097 +0.23531666666666662,0.010675916481079278,-0.07189212411203103,0.00040664356324602403 +0.26893333333333325,0.011260505147899872,-0.05507078829060723,0.000444668150021615 +0.30255,0.011845093814720481,-0.038250735887836625,0.0004932628507649892 +0.3361666666666666,0.012429297020727635,-0.021442820198694704,0.0005537047680385298 +0.36978333333333324,0.012943235430298142,-0.006612617441073541,0.0006180791955976091 +0.4034,0.013167591626670111,-8.233868449333761e-18,0.0006508969005061287 diff --git a/tests/fixtures/motor/data/cylindrical_pressurant_tank_expected.csv b/tests/fixtures/motor/data/cylindrical_pressurant_tank_expected.csv new file mode 100644 index 000000000..caf9862e7 --- /dev/null +++ b/tests/fixtures/motor/data/cylindrical_pressurant_tank_expected.csv @@ -0,0 +1,26 @@ +level_height,volume,center_of_mass,inertia +-0.4905,0.0,0.0,0.0 +-0.449625,0.0002827826025953255,-0.46411144141531335,6.113435649768482e-05 +-0.40875,0.0008480974805291653,-0.4405110759493672,0.0001657333255803535 +-0.36787499999999995,0.0014331773879828432,-0.41920154494382006,0.00025470343771418223 +-0.32699999999999996,0.0020182572954365085,-0.3983976063829787,0.00032607798469747707 +-0.28612499999999996,0.0026033372028901733,-0.3777583762886599,0.00038181202932474384 +-0.24524999999999997,0.003188417110343852,-0.3571931818181816,0.00042386063439048673 +-0.20437499999999997,0.003773497017797517,-0.33666758534850627,0.00045417886268920747 +-0.16349999999999998,0.004358576925251182,-0.3161656403940886,0.00047472177701541056 +-0.12262499999999998,0.004943656832704848,-0.2956789495114007,0.00048744444016359965 +-0.08174999999999999,0.005528736740158513,-0.27520266990291287,0.0004943019149282782 +-0.040874999999999995,0.006113816647612191,-0.2547338125548726,0.0004972492641039496 +5.551115123125783e-17,0.006698896555065856,-0.23427043269230766,0.0004982415504851175 +0.04087500000000005,0.007283976462519522,-0.21381121039056752,0.0004992338368662853 +0.08175000000000004,0.0078690563699732,-0.19335521828103658,0.0005021811860419568 +0.12262500000000004,0.008454136277426865,-0.17290178571428558,0.0005090386608066354 +0.16350000000000003,0.009039216184880532,-0.15245041567695955,0.0005217613239548245 +0.20437500000000003,0.009624296092334197,-0.13200073201338547,0.0005423042382810275 +0.24525000000000002,0.010209375999787874,-0.11155244479495245,0.000572622466579749 +0.286125,0.010794455907241539,-0.09110532695176517,0.000614671071645491 +0.327,0.011379535814695205,-0.07065919811320753,0.0006704051162727579 +0.367875,0.01196461572214887,-0.050213913189771295,0.0007417796632560529 +0.40875,0.01254969562960255,-0.029769354148844927,0.0008307497753898814 +0.449625,0.013115010507536386,-0.010007055749004045,0.0009353487444725499 +0.4905,0.013397793110131711,0.0,0.0009964831009702348 diff --git a/tests/fixtures/motor/data/spherical_oxidizer_tank_expected.csv b/tests/fixtures/motor/data/spherical_oxidizer_tank_expected.csv new file mode 100644 index 000000000..9d1cd0c62 --- /dev/null +++ b/tests/fixtures/motor/data/spherical_oxidizer_tank_expected.csv @@ -0,0 +1,26 @@ +level_height,volume,center_of_mass,inertia +-0.05,0.0,0.0,0.0 +-0.04583333333333334,2.6513248185677844e-06,-0.047232142857142854,6.095088014828042e-09 +-0.04166666666666667,1.0302290723577582e-05,-0.044485294117647074,2.175980154667401e-08 +-0.037500000000000006,2.2498384888989377e-05,-0.04176136363636364,4.363855762203842e-08 +-0.03333333333333334,3.87850944887629e-05,-0.0390625,6.908594955810907e-08 +-0.02916666666666667,5.8707906696857845e-05,-0.036391129032258066,9.609572933369276e-08 +-0.025,8.181230868723432e-05,-0.03374999999999999,1.2322978996014713e-07 +-0.020833333333333336,0.00010764378763385171,-0.031142241379310345,1.4954714785231024e-07 +-0.01666666666666667,0.00013574783071067004,-0.028571428571428584,1.7453292519943379e-07 +-0.012500000000000004,0.0001656699250916491,-0.026041666666666678,1.9802733233611288e-07 +-0.008333333333333338,0.00019695555795074912,-0.0235576923076923,2.201546501132184e-07 +-0.004166666666666673,0.00022915021646192904,-0.02112500000000001,2.412522122688262e-07 +0.0,0.00026179938779914946,-0.018749999999999996,2.61799387799151e-07 +0.004166666666666666,0.0002944485591363695,-0.01644021739130436,2.8234656332947566e-07 +0.008333333333333331,0.00032664321764755015,-0.014204545454545458,3.034441254850839e-07 +0.012499999999999997,0.0003579288505066499,-0.012053571428571419,3.2557144326218885e-07 +0.016666666666666663,0.0003878509448876289,-0.009999999999999992,3.490658503988677e-07 +0.02083333333333333,0.0004159549879644476,-0.008059210526315778,3.740516277459912e-07 +0.024999999999999994,0.0004417864669110648,-0.00624999999999999,4.0036898563815394e-07 +0.02916666666666666,0.0004648908689014413,-0.004595588235294098,4.2750304626460787e-07 +0.033333333333333326,0.0004848136811095362,-0.0031249999999999993,4.54512826040191e-07 +0.03749999999999999,0.0005011003907093094,-0.0018750000000000101,4.799602179762609e-07 +0.04166666666666666,0.000513296484874721,-0.0008928571428571531,5.018389740516248e-07 +0.04583333333333332,0.000520947450779731,-0.0002403846153846192,5.17503687583471e-07 +0.05,0.0005235987755982989,6.229927327055379e-19,5.235987755982989e-07 diff --git a/tests/fixtures/motor/data/tank_cad_validation_data_generator.ipynb b/tests/fixtures/motor/data/tank_cad_validation_data_generator.ipynb new file mode 100644 index 000000000..63131969d --- /dev/null +++ b/tests/fixtures/motor/data/tank_cad_validation_data_generator.ipynb @@ -0,0 +1,265 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# CadQuery Validation of Tank Geometry Parameters" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This notebook is an example of using CadQuery to compute properties, such as volume, center of mass and inertia of geometric shapes. Therefore, its main purpose is to validate the `rocketpy.TankGeometry` calculations for different fluid heights." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext autoreload\n", + "%autoreload 2" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "import cadquery as cq\n", + "import numpy as np\n", + "import csv\n", + "from rocketpy import CylindricalTank, SphericalTank" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Warning: Adding spherical caps to the tank will not modify the total height of the tank 0.8068 m. Its cylindrical portion height will be reduced to 0.6579999999999999 m.\n", + "Warning: Adding spherical caps to the tank will not modify the total height of the tank 0.981 m. Its cylindrical portion height will be reduced to 0.846 m.\n" + ] + } + ], + "source": [ + "# Create fixtures geometries\n", + "geometry_map = {}\n", + "geometry_map[\"sphere\"] = {\"spherical_oxidizer_tank\": SphericalTank(0.05)}\n", + "geometry_map[\"cylinder\"] = {\n", + " \"cylindrical_oxidizer_tank\": CylindricalTank(0.0744, 0.8068, True),\n", + " \"cylindrical_pressurant_tank\": CylindricalTank(0.135 / 2, 0.981, True),\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def export_expected_parameters(name, datapoints):\n", + " with open(f'{name}_expected.csv', mode='w') as output_file:\n", + " writer = csv.writer(output_file)\n", + " writer.writerow(['level_height', 'volume', 'center_of_mass', 'inertia'])\n", + " for data_row in datapoints:\n", + " writer.writerow([*data_row])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## CadQuery of a Spherical Tank" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "A spherical shape is easily defined in CadQuery by using the `sphere` function. Furthermore, different levels of fluid height can be defined by using the `cut` function, i.e., subtracting a cylinder from the desired height and above." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "def evaluate_sphere_parameters(radius, level_height):\n", + " \"\"\"Computes the volume, center of mass and inertia\n", + " (with respect to the origin) of a sphere 'filled' up\n", + " to a certain level height.\n", + "\n", + " Parameters\n", + " ----------\n", + " radius : float\n", + " The radius of the sphere.\n", + " level_height : float\n", + " The height of the liquid inside the sphere.\n", + "\n", + " Returns\n", + " -------\n", + " volume : float\n", + " The volume of the sphere.\n", + " center_of_mass : float\n", + " The center of mass of the sphere.\n", + " inertia : float\n", + " The inertia of the sphere with respect to the origin.\n", + " \"\"\"\n", + " sphere = cq.Workplane(\"XY\").sphere(radius)\n", + "\n", + " # Cut the sphere to the level height\n", + " drill_height = 10 * radius\n", + " sphere = sphere.cut(\n", + " cq.Workplane(\"XY\")\n", + " .cylinder(drill_height, radius)\n", + " .translate((0, 0, drill_height / 2 + level_height))\n", + " )\n", + "\n", + " # Uncomment to display the CAD\n", + " # display(sphere)\n", + "\n", + " primitive = sphere.val()\n", + "\n", + " volume = primitive.Volume()\n", + " center_of_mass = primitive.centerOfMass(primitive)\n", + " inertia_tensor = primitive.matrixOfInertia(primitive)\n", + "\n", + " return volume, center_of_mass.z, inertia_tensor[0][0] + volume * center_of_mass.z**2" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "for name, sphere_geometry in geometry_map[\"sphere\"].items():\n", + " radius = sphere_geometry.total_height / 2\n", + " datapoints = []\n", + " for h in np.linspace(-radius, radius, 25):\n", + " datapoints.append([h, *evaluate_sphere_parameters(radius, h)])\n", + "\n", + " export_expected_parameters(name, datapoints)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## CadQuery of a Cylindrical Tank with Caps" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "A cylindrical shape with caps is defined by using the `cylinder` function for the main body, the `sphere` function for the caps and uniting the three shapes into one. Similarly to the sphere case, the fluid height is defined by using the `cut` function, i.e., subtracting a cylinder from the desired height and above." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def evaluate_cylinder_with_caps(total_length, radius, level_height):\n", + " \"\"\"Computes the volume, center of mass and inertia (with respect\n", + " to the origin) of a cylinder with spherical caps 'filled' up to a\n", + " certain level height.\n", + "\n", + " Parameters\n", + " ----------\n", + " total_length : float\n", + " The total length of the cylinder (with caps).\n", + " radius : float\n", + " The radius of the cylinder.\n", + "\n", + " Returns\n", + " -------\n", + " volume : float\n", + " The volume of the cylinder with caps.\n", + " center_of_mass : float\n", + " The z-coordinate of the center of mass.\n", + " inertia : float\n", + " The inertia of the cylinder with caps with respect to the origin.\n", + " \"\"\"\n", + " cylinder_height = total_length - 2 * radius\n", + "\n", + " cylinder = cq.Workplane(\"XY\").cylinder(cylinder_height, radius)\n", + " top_cap = (\n", + " cq.Workplane(\"XY\").sphere(radius).translate((0, 0, total_length / 2 - radius))\n", + " )\n", + " bottom_cap = (\n", + " cq.Workplane(\"XY\").sphere(radius).translate((0, 0, -total_length / 2 + radius))\n", + " )\n", + "\n", + " solid = cylinder.union(top_cap).union(bottom_cap)\n", + "\n", + " # Remove solid above the level_height\n", + " drill_height = 10 * total_length\n", + " solid = solid.cut(\n", + " cq.Workplane(\"XY\")\n", + " .cylinder(drill_height, radius)\n", + " .translate((0, 0, drill_height / 2 + level_height))\n", + " )\n", + " # Uncomment to display the CAD\n", + " # display(solid)\n", + "\n", + " primitive = solid.val()\n", + "\n", + " volume = primitive.Volume()\n", + " center_of_mass = primitive.centerOfMass(primitive)\n", + " inertia_tensor = primitive.matrixOfInertia(primitive)\n", + "\n", + " return volume, center_of_mass.z, inertia_tensor[0][0] + volume * center_of_mass.z**2" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "for name, cylinder_geometry in geometry_map[\"cylinder\"].items():\n", + " radius = cylinder_geometry.radius(0)\n", + " length = cylinder_geometry.total_height\n", + " datapoints = []\n", + " for h in np.linspace(-length / 2, length / 2, 25):\n", + " datapoints.append([h, *evaluate_cylinder_with_caps(length, radius, h)])\n", + "\n", + " export_expected_parameters(name, datapoints)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "venv312", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/tests/fixtures/motor/liquid_fixtures.py b/tests/fixtures/motor/liquid_fixtures.py index 7cd708a48..0711f56d6 100644 --- a/tests/fixtures/motor/liquid_fixtures.py +++ b/tests/fixtures/motor/liquid_fixtures.py @@ -68,6 +68,32 @@ def oxidizer_fluid(): return Fluid(name="O2", density=1000) +@pytest.fixture +def lox_fluid_seblm(): + """A liquid oxygen fixture whose density comes + from testing data. + + Returns + ------- + rocketpy.Fluid + An object of the Fluid class. + """ + return Fluid(name="O2", density=1141.7) + + +@pytest.fixture +def nitrogen_fluid_seblm(): + """A nitrogen gas fixture whose density comes + from testing data. + + Returns + ------- + rocketpy.Fluid + An object of the Fluid class. + """ + return Fluid(name="N2", density=51.75) + + @pytest.fixture def liquid_motor(pressurant_tank, fuel_tank, oxidizer_tank): """An example of a liquid motor with pressurant, fuel and oxidizer tanks. diff --git a/tests/fixtures/motor/tank_geometry_fixtures.py b/tests/fixtures/motor/tank_geometry_fixtures.py new file mode 100644 index 000000000..c664fec21 --- /dev/null +++ b/tests/fixtures/motor/tank_geometry_fixtures.py @@ -0,0 +1,41 @@ +import pytest + +from rocketpy import CylindricalTank, SphericalTank + + +@pytest.fixture +def pressurant_tank_geometry(): + """An example of a pressurant cylindrical tank with spherical + caps. + + Returns + ------- + rocketpy.CylindricalTank + An object of the CylindricalTank class. + """ + return CylindricalTank(0.135 / 2, 0.981, spherical_caps=True) + + +@pytest.fixture +def propellant_tank_geometry(): + """An example of a cylindrical tank with spherical + caps. + + Returns + ------- + rocketpy.CylindricalTank + An object of the CylindricalTank class. + """ + return CylindricalTank(0.0744, 0.8068, spherical_caps=True) + + +@pytest.fixture +def spherical_oxidizer_geometry(): + """An example of a spherical tank. + + Returns + ------- + rocketpy.SphericalTank + An object of the SphericalTank class. + """ + return SphericalTank(0.05) diff --git a/tests/fixtures/motor/tanks_fixtures.py b/tests/fixtures/motor/tanks_fixtures.py index 7dc1904b5..40417c8a4 100644 --- a/tests/fixtures/motor/tanks_fixtures.py +++ b/tests/fixtures/motor/tanks_fixtures.py @@ -3,16 +3,334 @@ from rocketpy import ( CylindricalTank, + Fluid, Function, LevelBasedTank, MassBasedTank, - SphericalTank, + MassFlowRateBasedTank, + TankGeometry, UllageBasedTank, ) @pytest.fixture -def pressurant_tank(pressurant_fluid): +def sample_full_mass_flow_rate_tank(): + """An example of a full MassFlowRateBasedTank. + + Returns + ------- + rocketpy.MassFlowRateBasedTank + An object of the MassFlowRateBasedTank class. + """ + full_tank = MassFlowRateBasedTank( + name="Full Tank", + geometry=CylindricalTank(0.1, 1 / np.pi), + initial_liquid_mass=9, + initial_gas_mass=0.001, + liquid=Fluid("Water", 1000), + gas=Fluid("Air", 1), + liquid_mass_flow_rate_in=0, + gas_mass_flow_rate_in=0, + gas_mass_flow_rate_out=0, + liquid_mass_flow_rate_out=0, + flux_time=(0, 10), + ) + + return full_tank + + +@pytest.fixture +def sample_empty_mass_flow_rate_tank(): + """An example of an empty MassFlowRateBasedTank. + + Returns + ------- + rocketpy.MassFlowRateBasedTank + An object of the MassFlowRateBasedTank class. + """ + empty_tank = MassFlowRateBasedTank( + name="Empty Tank", + geometry=CylindricalTank(0.1, 1 / np.pi), + initial_liquid_mass=0, + initial_gas_mass=0, + liquid=Fluid("Water", 1000), + gas=Fluid("Air", 1), + liquid_mass_flow_rate_in=0, + gas_mass_flow_rate_in=0, + gas_mass_flow_rate_out=0, + liquid_mass_flow_rate_out=0, + flux_time=(0, 10), + ) + + return empty_tank + + +@pytest.fixture +def sample_full_ullage_tank(): + """An example of a UllageBasedTank full of liquid. + + Returns + ------- + rocketpy.UllageBasedTank + An object of the UllageBasedTank class. + """ + full_tank = UllageBasedTank( + name="Full Tank", + geometry=CylindricalTank(0.1, 1 / np.pi), + liquid=Fluid("Water", 1000), + gas=Fluid("Air", 1), + ullage=0, + flux_time=(0, 10), + ) + + return full_tank + + +@pytest.fixture +def sample_empty_ullage_tank(): + """An example of a UllageBasedTank with no liquid. + + Returns + ------- + rocketpy.UllageBasedTank + An object of the UllageBasedTank class. + """ + empty_tank = UllageBasedTank( + name="Empty Tank", + geometry=CylindricalTank(0.1, 1 / np.pi), + liquid=Fluid("Water", 1000), + gas=Fluid("Air", 1), + ullage=0.01, + flux_time=(0, 10), + ) + + return empty_tank + + +@pytest.fixture +def sample_full_level_tank(): + """An example of a LevelBasedTank full of liquid. + + Returns + ------- + rocketpy.LevelBasedTank + An object of the LevelBasedTank class. + """ + full_tank = LevelBasedTank( + name="Full Tank", + geometry=CylindricalTank(0.1, 1 / np.pi), + liquid=Fluid("Water", 1000), + gas=Fluid("Air", 1), + liquid_height=1 / (2 * np.pi), + flux_time=(0, 10), + ) + + return full_tank + + +@pytest.fixture +def sample_empty_level_tank(): + """An example of a LevelBasedTank with no liquid. + + Returns + ------- + rocketpy.LevelBasedTank + An object of the LevelBasedTank class. + """ + empty_tank = LevelBasedTank( + name="Empty Tank", + geometry=CylindricalTank(0.1, 1 / np.pi), + liquid=Fluid("Water", 1000), + gas=Fluid("Air", 1), + liquid_height=0, + flux_time=(0, 10), + ) + + return empty_tank + + +@pytest.fixture +def sample_full_mass_tank(): + """An example of a full MassBasedTank. + + Returns + ------- + rocketpy.MassBasedTank + An object of the MassBasedTank class. + """ + full_tank = MassBasedTank( + name="Full Tank", + geometry=CylindricalTank(0.1, 1 / np.pi), + liquid=Fluid("Water", 1000), + gas=Fluid("Air", 1), + liquid_mass=9, + gas_mass=0.001, + flux_time=(0, 10), + ) + + return full_tank + + +@pytest.fixture +def sample_empty_mass_tank(): + """An example of an empty MassBasedTank. + + Returns + ------- + rocketpy.MassBasedTank + An object of the MassBasedTank class. + """ + empty_tank = MassBasedTank( + name="Empty Tank", + geometry=CylindricalTank(0.1, 1 / np.pi), + liquid=Fluid("Water", 1000), + gas=Fluid("Air", 1), + liquid_mass=0, + gas_mass=0, + flux_time=(0, 10), + ) + + return empty_tank + + +@pytest.fixture +def real_mass_based_tank_seblm(lox_fluid_seblm, nitrogen_fluid_seblm): + """An instance of a real cylindrical tank with spherical caps. + + Parameters + ---------- + lox_fluid_seblm : rocketpy.Fluid + Liquid oxygen fluid. This is a pytest fixture. + nitrogen_fluid_seblm : rocketpy.Fluid + Nitrogen gas fluid. This is a pytest fixture. + + Returns + ------- + rocketpy.MassBasedTank + An object of the MassBasedTank class. + """ + geometry = CylindricalTank(0.0744, 0.8698, True) + + lox_tank = MassBasedTank( + name="Real Tank", + geometry=geometry, + flux_time=(0, 15.583), + liquid_mass="./data/rockets/berkeley/Test135LoxMass.csv", + gas_mass="./data/rockets/berkeley/Test135GasMass.csv", + liquid=lox_fluid_seblm, + gas=nitrogen_fluid_seblm, + discretize=200, + ) + + return lox_tank + + +@pytest.fixture +def example_mass_based_tank_seblm(lox_fluid_seblm, nitrogen_fluid_seblm): + """Example data of a cylindrical tank with spherical caps. + + Parameters + ---------- + lox_fluid_seblm : rocketpy.Fluid + Liquid oxygen fluid. This is a pytest fixture. + nitrogen_fluid_seblm : rocketpy.Fluid + Nitrogen gas fluid. This is a pytest fixture. + + Returns + ------- + rocketpy.MassBasedTank + An object of the MassBasedTank class. + """ + geometry = TankGeometry({(0, 5): 1}) + + example_tank = MassBasedTank( + name="Example Tank", + geometry=geometry, + flux_time=(0, 10), + liquid_mass="./data/rockets/berkeley/ExampleTankLiquidMassData.csv", + gas_mass="./data/rockets/berkeley/ExampleTankGasMassData.csv", + liquid=lox_fluid_seblm, + gas=nitrogen_fluid_seblm, + discretize=None, + ) + + return example_tank + + +@pytest.fixture +def real_level_based_tank_seblm(lox_fluid_seblm, nitrogen_fluid_seblm): + """An instance of a real cylindrical tank with spherical caps. + + Parameters + ---------- + lox_fluid_seblm : rocketpy.Fluid + Liquid oxygen fluid. This is a pytest fixture. + nitrogen_fluid_seblm : rocketpy.Fluid + Nitrogen gas fluid. This is a pytest fixture. + + Returns + ------- + rocketpy.LevelBasedTank + An object of the LevelBasedTank class. + """ + geometry = TankGeometry( + { + (0, 0.0559): lambda h: np.sqrt(0.0775**2 - (0.0775 - h) ** 2), + (0.0559, 0.7139): 0.0744, + (0.7139, 0.7698): lambda h: np.sqrt(0.0775**2 - (h - 0.6924) ** 2), + } + ) + + level_tank = LevelBasedTank( + name="Level Tank", + geometry=geometry, + flux_time=(0, 15.583), + gas=nitrogen_fluid_seblm, + liquid=lox_fluid_seblm, + liquid_height="./data/rockets/berkeley/loxUllage.csv", + discretize=None, + ) + + return level_tank + + +@pytest.fixture +def example_mass_flow_rate_based_tank_seblm(lox_fluid_seblm, nitrogen_fluid_seblm): + """An instance of a example cylindrical tank whose flux + is given by mass flow rates. + + Parameters + ---------- + lox_fluid_seblm : rocketpy.Fluid + Liquid oxygen fluid. This is a pytest fixture. + nitrogen_fluid_seblm : rocketpy.Fluid + Nitrogen gas fluid. This is a pytest fixture. + + Returns + ------- + rocketpy.MassFlowRateBasedTank + An object of the MassFlowRateBasedTank class. + """ + mass_flow_rate_tank = MassFlowRateBasedTank( + name="Test Tank", + geometry=TankGeometry({(0, 1): 1}), + flux_time=(0, 10), + initial_liquid_mass=5, + initial_gas_mass=0.1, + liquid_mass_flow_rate_in=0.1, + gas_mass_flow_rate_in=0.01, + liquid_mass_flow_rate_out=0.2, + gas_mass_flow_rate_out=0.02, + liquid=lox_fluid_seblm, + gas=nitrogen_fluid_seblm, + discretize=11, + ) + + return mass_flow_rate_tank + + +@pytest.fixture +def pressurant_tank(pressurant_fluid, pressurant_tank_geometry): """An example of a pressurant cylindrical tank with spherical caps. @@ -20,16 +338,17 @@ def pressurant_tank(pressurant_fluid): ---------- pressurant_fluid : rocketpy.Fluid Pressurizing fluid. This is a pytest fixture. + pressurant_tank_geometry : rocketpy.CylindricalTank + Geometry of the pressurant tank. This is a pytest fixture. Returns ------- rocketpy.MassBasedTank An object of the CylindricalTank class. """ - geometry = CylindricalTank(0.135 / 2, 0.981, spherical_caps=True) pressurant_tank = MassBasedTank( name="Pressure Tank", - geometry=geometry, + geometry=pressurant_tank_geometry, liquid_mass=0, flux_time=(8, 20), gas_mass="data/rockets/berkeley/pressurantMassFiltered.csv", @@ -41,7 +360,7 @@ def pressurant_tank(pressurant_fluid): @pytest.fixture -def fuel_tank(fuel_fluid, fuel_pressurant): +def fuel_tank(fuel_fluid, fuel_pressurant, propellant_tank_geometry): """An example of a fuel cylindrical tank with spherical caps. @@ -52,20 +371,21 @@ def fuel_tank(fuel_fluid, fuel_pressurant): fuel_pressurant : rocketpy.Fluid Pressurizing fluid of the fuel tank. This is a pytest fixture. + propellant_tank_geometry : rocketpy.CylindricalTank + Geometry of the fuel tank. This is a pytest fixture. Returns ------- rocketpy.UllageBasedTank """ - geometry = CylindricalTank(0.0744, 0.8068, spherical_caps=True) ullage = ( -Function("data/rockets/berkeley/test124_Propane_Volume.csv") * 1e-3 - + geometry.total_volume + + propellant_tank_geometry.total_volume ) fuel_tank = UllageBasedTank( name="Propane Tank", flux_time=(8, 20), - geometry=geometry, + geometry=propellant_tank_geometry, liquid=fuel_fluid, gas=fuel_pressurant, ullage=ullage, @@ -75,7 +395,7 @@ def fuel_tank(fuel_fluid, fuel_pressurant): @pytest.fixture -def oxidizer_tank(oxidizer_fluid, oxidizer_pressurant): +def oxidizer_tank(oxidizer_fluid, oxidizer_pressurant, propellant_tank_geometry): """An example of a oxidizer cylindrical tank with spherical caps. @@ -86,20 +406,21 @@ def oxidizer_tank(oxidizer_fluid, oxidizer_pressurant): oxidizer_pressurant : rocketpy.Fluid Pressurizing fluid of the oxidizer tank. This is a pytest fixture. + propellant_tank_geometry : rocketpy.CylindricalTank + Geometry of the oxidizer tank. This is a pytest fixture. Returns ------- rocketpy.UllageBasedTank """ - geometry = CylindricalTank(0.0744, 0.8068, spherical_caps=True) ullage = ( -Function("data/rockets/berkeley/test124_Lox_Volume.csv") * 1e-3 - + geometry.total_volume + + propellant_tank_geometry.total_volume ) oxidizer_tank = UllageBasedTank( name="Lox Tank", flux_time=(8, 20), - geometry=geometry, + geometry=propellant_tank_geometry, liquid=oxidizer_fluid, gas=oxidizer_pressurant, ullage=ullage, @@ -109,7 +430,9 @@ def oxidizer_tank(oxidizer_fluid, oxidizer_pressurant): @pytest.fixture -def spherical_oxidizer_tank(oxidizer_fluid, oxidizer_pressurant): +def spherical_oxidizer_tank( + oxidizer_fluid, oxidizer_pressurant, spherical_oxidizer_geometry +): """An example of a oxidizer spherical tank. Parameters @@ -122,14 +445,13 @@ def spherical_oxidizer_tank(oxidizer_fluid, oxidizer_pressurant): Returns ------- - rocketpy.UllageBasedTank + rocketpy.LevelBasedTank """ - geometry = SphericalTank(0.05) liquid_level = Function(lambda t: 0.1 * np.exp(-t / 2) - 0.05) oxidizer_tank = LevelBasedTank( name="Lox Tank", flux_time=10, - geometry=geometry, + geometry=spherical_oxidizer_geometry, liquid=oxidizer_fluid, gas=oxidizer_pressurant, liquid_height=liquid_level, diff --git a/tests/integration/test_tank.py b/tests/integration/test_tank.py new file mode 100644 index 000000000..f79d670ab --- /dev/null +++ b/tests/integration/test_tank.py @@ -0,0 +1,35 @@ +# pylint: disable=unused-argument + +from unittest.mock import patch + +import pytest + + +@patch("matplotlib.pyplot.show") +@pytest.mark.parametrize( + "fixture_name", + [ + "sample_full_mass_flow_rate_tank", + "sample_empty_mass_flow_rate_tank", + "sample_full_ullage_tank", + "sample_empty_ullage_tank", + "sample_full_level_tank", + "sample_empty_level_tank", + "sample_full_mass_tank", + "sample_empty_mass_tank", + "real_mass_based_tank_seblm", + "pressurant_tank", + "fuel_tank", + "oxidizer_tank", + "spherical_oxidizer_tank", + ], +) +def test_tank_all_info(mock_show, fixture_name, request): + tank = request.getfixturevalue(fixture_name) + + assert tank.info() is None + assert tank.all_info() is None + + assert (tank.gas_height <= tank.geometry.top).all + assert (tank.liquid_height <= tank.geometry.top).all + assert (tank.fluid_volume <= tank.geometry.total_volume).all diff --git a/tests/unit/test_tank.py b/tests/unit/test_tank.py index 3002bee43..a313caf20 100644 --- a/tests/unit/test_tank.py +++ b/tests/unit/test_tank.py @@ -1,959 +1,357 @@ -# TODO: This file must be refactored to improve readability and maintainability. -# pylint: disable=too-many-statements -import os from math import isclose +from pathlib import Path import numpy as np import pytest +import scipy.integrate as spi -from rocketpy import ( - Fluid, - Function, - LevelBasedTank, - MassBasedTank, - MassFlowRateBasedTank, - TankGeometry, -) - -pressurant_params = (0.135 / 2, 0.981) -fuel_params = (0.0744, 0.8068) -oxidizer_params = (0.0744, 0.8068) +BASE_PATH = Path("./data/rockets/berkeley/") -parametrize_fixtures = pytest.mark.parametrize( +@pytest.mark.parametrize( "params", [ - ("pressurant_tank", pressurant_params), - ("fuel_tank", fuel_params), - ("oxidizer_tank", oxidizer_params), + ( + "real_mass_based_tank_seblm", + BASE_PATH / "Test135LoxMass.csv", + BASE_PATH / "Test135GasMass.csv", + ), + ( + "example_mass_based_tank_seblm", + BASE_PATH / "ExampleTankLiquidMassData.csv", + BASE_PATH / "ExampleTankGasMassData.csv", + ), ], ) +def test_mass_based_tank_fluid_mass(params, request): + """Test the fluid_mass property of the MassBasedTank subclass of Tank + class. - -@parametrize_fixtures -def test_tank_bounds(params, request): - """Test basic geometric properties of the tanks.""" - tank, (expected_radius, expected_height) = params - tank = request.getfixturevalue(tank) - - expected_total_height = expected_height - - assert tank.geometry.radius(0) == pytest.approx(expected_radius, abs=1e-6) - assert tank.geometry.total_height == pytest.approx(expected_total_height, abs=1e-6) - - -@parametrize_fixtures -def test_tank_coordinates(params, request): - """Test basic coordinate values of the tanks.""" - tank, (_, height) = params - tank = request.getfixturevalue(tank) - - expected_bottom = -height / 2 - expected_top = height / 2 - - assert tank.geometry.bottom == pytest.approx(expected_bottom, abs=1e-6) - assert tank.geometry.top == pytest.approx(expected_top, abs=1e-6) - - -@parametrize_fixtures -def test_tank_total_volume(params, request): - """Test the total volume of the tanks comparing to the analytically - calculated values. + Parameters + ---------- + params : tuple + A tuple containing test parameters. + request : _pytest.fixtures.FixtureRequest + A pytest fixture request object. """ - tank, (radius, height) = params + tank, liq_path, gas_path = params tank = request.getfixturevalue(tank) - - expected_total_volume = ( - np.pi * radius**2 * (height - 2 * radius) + 4 / 3 * np.pi * radius**3 + expected_liquid_mass = np.loadtxt(liq_path, skiprows=1, delimiter=",") + expected_gas_mass = np.loadtxt(gas_path, skiprows=1, delimiter=",") + + assert np.allclose( + expected_liquid_mass[:, 1], + tank.liquid_mass(expected_liquid_mass[:, 0]), + rtol=1e-2, + atol=1e-4, + ) + assert np.allclose( + expected_gas_mass[:, 1], + tank.gas_mass(expected_gas_mass[:, 0]), + rtol=1e-1, + atol=1e-4, ) - assert tank.geometry.total_volume == pytest.approx(expected_total_volume, abs=1e-6) - - -@parametrize_fixtures -def test_tank_volume(params, request): - """Test the volume of the tanks at different heights comparing to the - analytically calculated values. - """ - tank, (radius, height) = params - tank = request.getfixturevalue(tank) - - total_height = height - bottom = -height / 2 - top = height / 2 - - expected_volume = tank_volume_function(radius, total_height, bottom) - - for h in np.linspace(bottom, top, 101): - assert tank.geometry.volume(h) == pytest.approx(expected_volume(h), abs=1e-6) - - -@parametrize_fixtures -def test_tank_centroid(params, request): - """Test the centroid of the tanks at different heights comparing to the - analytically calculated values. - """ - tank, (radius, height) = params - tank = request.getfixturevalue(tank) - - total_height = height - bottom = -height / 2 - - expected_centroid = tank_centroid_function(radius, total_height, bottom) - - for h, liquid_com in zip( - tank.liquid_height.y_array, tank.liquid_center_of_mass.y_array - ): - # Loss of accuracy to 1e-3 when liquid height is close to zero - assert liquid_com == pytest.approx(expected_centroid(h), abs=1e-3) +@pytest.mark.parametrize( + "params", + [ + ( + "real_mass_based_tank_seblm", + BASE_PATH / "Test135LoxMass.csv", + BASE_PATH / "Test135GasMass.csv", + ), + ( + "example_mass_based_tank_seblm", + BASE_PATH / "ExampleTankLiquidMassData.csv", + BASE_PATH / "ExampleTankGasMassData.csv", + ), + ], +) +def test_mass_based_tank_net_mass_flow_rate(params, request): + """Test the net_mass_flow_rate property of the MassBasedTank + subclass of Tank. -@parametrize_fixtures -def test_tank_inertia(params, request): - """Test the inertia of the tanks at different heights comparing to the - analytically calculated values. + Parameters + ---------- + params : tuple + A tuple containing test parameters. + request : _pytest.fixtures.FixtureRequest + A pytest fixture request object. """ - tank, (radius, height) = params + tank, liq_path, gas_path = params tank = request.getfixturevalue(tank) + expected_liquid_mass = np.loadtxt(liq_path, skiprows=1, delimiter=",") + expected_gas_mass = np.loadtxt(gas_path, skiprows=1, delimiter=",") - total_height = height - bottom = -height / 2 - - expected_inertia = tank_inertia_function(radius, total_height, bottom) - - for h in tank.liquid_height.y_array: - assert tank.geometry.Ix_volume(tank.geometry.bottom, h)(h) == pytest.approx( - expected_inertia(h)[0], abs=1e-5 - ) - - -def test_mass_based_tank(): - """Tests the MassBasedTank subclass of Tank regarding its mass and - net mass flow rate properties. The test is performed on both a real - tank and a simplified tank. - """ - lox = Fluid(name="LOx", density=1141.7) - n2 = Fluid( - name="Nitrogen Gas", - density=51.75, - ) # density value may be estimate - - def top_endcap(y): - """Calculate the top endcap based on hemisphere equation. - - Parameters: - y (float): The y-coordinate. - - Returns: - float: The result of the hemisphere equation for the top endcap. - """ - return np.sqrt(0.0775**2 - (y - 0.7924) ** 2) - - def bottom_endcap(y): - """Calculate the bottom endcap based on hemisphere equation. - - Parameters: - y (float): The y-coordinate. - - Returns: - float: The result of the hemisphere equation for the bottom endcap. - """ - return np.sqrt(0.0775**2 - (0.0775 - y) ** 2) - - # Generate tank geometry {radius: height, ...} - real_geometry = TankGeometry( - { - (0, 0.0559): bottom_endcap, - (0.0559, 0.8039): lambda y: 0.0744, - (0.8039, 0.8698): top_endcap, - } + # Noisy derivatives, assert integrals + initial_mass = expected_liquid_mass[0, 1] + expected_gas_mass[0, 1] + expected_mass_variation = ( + expected_liquid_mass[-1, 1] + expected_gas_mass[-1, 1] - initial_mass ) - - # Import liquid mass data - lox_masses = "./data/rockets/berkeley/Test135LoxMass.csv" - example_liquid_masses = "./data/rockets/berkeley/ExampleTankLiquidMassData.csv" - - # Import gas mass data - gas_masses = "./data/rockets/berkeley/Test135GasMass.csv" - example_gas_masses = "./data/rockets/berkeley/ExampleTankGasMassData.csv" - - # Generate tanks based on Berkeley SEB team's real tank geometries - real_tank_lox = MassBasedTank( - name="Real Tank", - geometry=real_geometry, - flux_time=(0, 10), - liquid_mass=lox_masses, - gas_mass=gas_masses, - liquid=lox, - gas=n2, + computed_final_mass = spi.simpson( + tank.net_mass_flow_rate.y_array, + x=tank.net_mass_flow_rate.x_array, ) - # Generate tank geometry {radius: height, ...} - example_geometry = TankGeometry({(0, 5): 1}) - - # Generate tanks based on simplified tank geometry - example_tank_lox = MassBasedTank( - name="Example Tank", - geometry=example_geometry, - flux_time=(0, 10), - liquid_mass=example_liquid_masses, - gas_mass=example_gas_masses, - liquid=lox, - gas=n2, - discretize=None, - ) - - # Assert volume bounds - # pylint: disable=comparison-with-callable - assert (real_tank_lox.gas_height <= real_tank_lox.geometry.top).all - assert (real_tank_lox.fluid_volume <= real_tank_lox.geometry.total_volume).all - assert (example_tank_lox.gas_height <= example_tank_lox.geometry.top).all - assert (example_tank_lox.fluid_volume <= example_tank_lox.geometry.total_volume).all - - initial_liquid_mass = 5 - initial_gas_mass = 0 - liquid_mass_flow_rate_in = 0.1 - gas_mass_flow_rate_in = 0.1 - liquid_mass_flow_rate_out = 0.2 - gas_mass_flow_rate_out = 0.05 - - def test(calculated, expected, t, real=False): - """Iterate over time range and test that calculated value is close to actual value""" - j = 0 - for i in np.arange(0, t, 0.1): - try: - print(calculated.get_value(i), expected(i)) - assert isclose(calculated.get_value(i), expected(i), rel_tol=5e-2) - except IndexError: - break - - if real: - j += 4 - else: - j += 1 - - def test_mass(): - """Test mass function of MassBasedTank subclass of Tank""" - - def example_expected(t): - return ( - initial_liquid_mass - + t * (liquid_mass_flow_rate_in - liquid_mass_flow_rate_out) - + initial_gas_mass - + t * (gas_mass_flow_rate_in - gas_mass_flow_rate_out) - ) - - example_calculated = example_tank_lox.fluid_mass - - lox_vals = Function(lox_masses).y_array - - def real_expected(t): - return lox_vals[t] - - real_calculated = real_tank_lox.fluid_mass - - test(example_calculated, example_expected, 5) - test(real_calculated, real_expected, 15.5, real=True) - - def test_net_mfr(): - """Test net_mass_flow_rate function of MassBasedTank subclass of Tank""" - - def example_expected(_): - return ( - liquid_mass_flow_rate_in - - liquid_mass_flow_rate_out - + gas_mass_flow_rate_in - - gas_mass_flow_rate_out - ) - - example_calculated = example_tank_lox.net_mass_flow_rate + assert isclose(expected_mass_variation, computed_final_mass, rel_tol=1e-2) - liquid_mfrs = Function(example_liquid_masses).y_array - gas_mfrs = Function(example_gas_masses).y_array - - def real_expected(t): - return (liquid_mfrs[t] + gas_mfrs[t]) / t - - real_calculated = real_tank_lox.net_mass_flow_rate - - test(example_calculated, example_expected, 10) - test(real_calculated, real_expected, 15.5, real=True) - - test_mass() - test_net_mfr() - - -def test_level_based_tank(): - """Test LevelBasedTank subclass of Tank class using Berkeley SEB team's - tank data of fluid level. - """ - lox = Fluid(name="LOx", density=1141.7) - n2 = Fluid(name="Nitrogen Gas", density=51.75) - - test_dir = "./data/rockets/berkeley/" - - def top_endcap(y): - return np.sqrt(0.0775**2 - (y - 0.692300000000001) ** 2) - - def bottom_endcap(y): - return np.sqrt(0.0775**2 - (0.0775 - y) ** 2) - - tank_geometry = TankGeometry( - { - (0, 0.0559): bottom_endcap, - (0.0559, 0.7139): lambda y: 0.0744, - (0.7139, 0.7698): top_endcap, - } - ) - - ullage_data = Function(os.path.abspath(test_dir + "loxUllage.csv")).get_source() - level_tank = LevelBasedTank( - name="LevelTank", - geometry=tank_geometry, - flux_time=(0, 10), - gas=n2, - liquid=lox, - liquid_height=ullage_data, - discretize=None, - ) - - mass_data = Function(test_dir + "loxMass.csv").get_source() - mass_flow_rate_data = Function(test_dir + "loxMFR.csv").get_source() - - def align_time_series(small_source, large_source): - assert isinstance(small_source, np.ndarray) and isinstance( - large_source, np.ndarray - ), "Must be np.ndarrays" - if small_source.shape[0] > large_source.shape[0]: - small_source, large_source = large_source, small_source - - result_larger_source = np.ndarray(small_source.shape) - result_smaller_source = np.ndarray(small_source.shape) - tolerance = 0.1 - curr_ind = 0 - for val in small_source: - time = val[0] - delta_time_vector = abs(time - large_source[:, 0]) - large_index = np.argmin(delta_time_vector) - delta_time = abs(time - large_source[large_index][0]) - - if delta_time < tolerance: - result_larger_source[curr_ind] = large_source[large_index] - result_smaller_source[curr_ind] = val - curr_ind += 1 - return result_larger_source, result_smaller_source - - assert np.allclose(level_tank.liquid_height, ullage_data) - - calculated_mass = level_tank.liquid_mass.set_discrete( - mass_data[0][0], mass_data[0][-1], len(mass_data[0]) - ) - calculated_mass, mass_data = align_time_series( - calculated_mass.get_source(), mass_data - ) - assert np.allclose(calculated_mass, mass_data, rtol=1, atol=2) - - calculated_mfr = level_tank.net_mass_flow_rate.set_discrete( - mass_flow_rate_data[0][0], - mass_flow_rate_data[0][-1], - len(mass_flow_rate_data[0]), - ) - calculated_mfr, _ = align_time_series( - calculated_mfr.get_source(), mass_flow_rate_data - ) +def test_level_based_tank_liquid_level(real_level_based_tank_seblm): + """Test the liquid_level property of LevelBasedTank + subclass of Tank. - -def test_mfr_tank_basic(): - """Test MassFlowRateTank subclass of Tank class regarding its properties, - such as net_mass_flow_rate, fluid_mass, center_of_mass and inertia. + Parameters + ---------- + real_level_based_tank_seblm : LevelBasedTank + The LevelBasedTank to be tested. """ + tank = real_level_based_tank_seblm + level_data = np.loadtxt(BASE_PATH / "loxUllage.csv", delimiter=",") - def test(t, a, tol=1e-4): - for i in np.arange(0, 10, 1): - print(t.get_value(i), a(i)) - assert isclose(t.get_value(i), a(i), abs_tol=tol) - - def test_nmfr(): - def nmfr(_): - return ( - liquid_mass_flow_rate_in - + gas_mass_flow_rate_in - - liquid_mass_flow_rate_out - - gas_mass_flow_rate_out - ) - - test(t.net_mass_flow_rate, nmfr) - - def test_mass(): - def m(x): - return ( - initial_liquid_mass - + (liquid_mass_flow_rate_in - liquid_mass_flow_rate_out) * x - ) + ( - initial_gas_mass + (gas_mass_flow_rate_in - gas_mass_flow_rate_out) * x - ) - - lm = t.fluid_mass - test(lm, m) - - def test_liquid_height(): - def alv(x): - return ( - initial_liquid_mass - + (liquid_mass_flow_rate_in - liquid_mass_flow_rate_out) * x - ) / lox.density - - def alh(x): - return alv(x) / (np.pi) - - tlh = t.liquid_height - test(tlh, alh) - - def test_com(): - def liquid_mass(x): - return ( - initial_liquid_mass - + (liquid_mass_flow_rate_in - liquid_mass_flow_rate_out) * x - ) - - def liquid_volume(x): - return liquid_mass(x) / lox.density - - def liquid_height(x): - return liquid_volume(x) / (np.pi) - - def gas_mass(x): - return ( - initial_gas_mass + (gas_mass_flow_rate_in - gas_mass_flow_rate_out) * x - ) - - def gas_volume(x): - return gas_mass(x) / n2.density - - def gas_height(x): - return gas_volume(x) / np.pi + liquid_height(x) - - def liquid_com(x): - return liquid_height(x) / 2 - - def gas_com(x): - return (gas_height(x) - liquid_height(x)) / 2 + liquid_height(x) - - def acom(x): - return (liquid_mass(x) * liquid_com(x) + gas_mass(x) * gas_com(x)) / ( - liquid_mass(x) + gas_mass(x) - ) - - tcom = t.center_of_mass - test(tcom, acom) - - def test_inertia(): - def liquid_mass(x): - return ( - initial_liquid_mass - + (liquid_mass_flow_rate_in - liquid_mass_flow_rate_out) * x - ) - - def liquid_volume(x): - return liquid_mass(x) / lox.density + assert np.allclose(level_data, tank.liquid_height.get_source()) - def liquid_height(x): - return liquid_volume(x) / (np.pi) - def gas_mass(x): - return ( - initial_gas_mass + (gas_mass_flow_rate_in - gas_mass_flow_rate_out) * x - ) - - def gas_volume(x): - return gas_mass(x) / n2.density - - def gas_height(x): - return gas_volume(x) / np.pi + liquid_height(x) - - def liquid_com(x): - return liquid_height(x) / 2 - - def gas_com(x): - return (gas_height(x) - liquid_height(x)) / 2 + liquid_height(x) - - def acom(x): - return (liquid_mass(x) * liquid_com(x) + gas_mass(x) * gas_com(x)) / ( - liquid_mass(x) + gas_mass(x) - ) - - r = 1 - - def ixy_gas(x): - return ( - 1 / 4 * gas_mass(x) * r**2 - + 1 / 12 * gas_mass(x) * (gas_height(x) - liquid_height(x)) ** 2 - + gas_mass(x) * (gas_com(x) - acom(x)) ** 2 - ) - - def ixy_liq(x): - return ( - 1 / 4 * liquid_mass(x) * r**2 - + 1 / 12 * liquid_mass(x) * (liquid_height(x) - t.geometry.bottom) ** 2 - + liquid_mass(x) * (liquid_com(x) - acom(x)) ** 2 - ) - - def ixy(x): - return ixy_gas(x) + ixy_liq(x) - - test(t.gas_inertia, ixy_gas, tol=1e-3) - test(t.liquid_inertia, ixy_liq, tol=1e-3) - test(t.inertia, ixy, tol=1e-3) - - tank_radius_function = TankGeometry({(0, 5): 1}) - lox = Fluid( - name="LOx", - density=1141, - ) - n2 = Fluid( - name="Nitrogen Gas", - density=51.75, - ) # density value may be estimate - initial_liquid_mass = 5 - initial_gas_mass = 0.1 - liquid_mass_flow_rate_in = 0.1 - gas_mass_flow_rate_in = 0.01 - liquid_mass_flow_rate_out = 0.2 - gas_mass_flow_rate_out = 0.02 - - t = MassFlowRateBasedTank( - name="Test Tank", - geometry=tank_radius_function, - flux_time=(0, 10), - initial_liquid_mass=initial_liquid_mass, - initial_gas_mass=initial_gas_mass, - liquid_mass_flow_rate_in=Function(0.1).set_discrete(0, 10, 1000), - gas_mass_flow_rate_in=Function(0.01).set_discrete(0, 10, 1000), - liquid_mass_flow_rate_out=Function(0.2).set_discrete(0, 10, 1000), - gas_mass_flow_rate_out=Function(0.02).set_discrete(0, 10, 1000), - liquid=lox, - gas=n2, - discretize=None, - ) - - test_nmfr() - test_mass() - test_liquid_height() - test_com() - test_inertia() - - -# Auxiliary testing functions - - -def cylinder_volume(radius, height): - """Returns the volume of a cylinder with the given radius and height. +def test_level_based_tank_mass(real_level_based_tank_seblm): + """Test the mass property of LevelBasedTank subclass of Tank. Parameters ---------- - radius : float - The radius of the cylinder. - height : float - The height of the cylinder. - - Returns - ------- - float - The volume of the cylinder. + real_level_based_tank_seblm : LevelBasedTank + The LevelBasedTank to be tested. """ - return np.pi * radius**2 * height + tank = real_level_based_tank_seblm + mass_data = np.loadtxt(BASE_PATH / "loxMass.csv", delimiter=",") + # Soft tolerances for the whole curve + assert np.allclose(mass_data, tank.fluid_mass.get_source(), rtol=1e-1, atol=6e-1) -def lower_spherical_cap_volume(radius, height=None): - """Returns the volume of a spherical cap with the given radius and filled - height that is filled from its convex side. - - Parameters - ---------- - radius : float - The radius of the spherical cap. - height : float, optional - The height of the spherical cap. If not given, the radius is used. - - Returns - ------- - float - The volume of the spherical cap. - """ - if height is None: - height = radius - return np.pi / 3 * height**2 * (3 * radius - height) + # Tighter tolerances for middle of the curve + assert np.allclose( + mass_data[100:401], tank.fluid_mass.get_source()[100:401], rtol=5e-2, atol=1e-1 + ) -def upper_spherical_cap_volume(radius, height=None): - """Returns the volume of a spherical cap with the given radius and filled - height that is filled from its concave side. +def test_mass_flow_rate_tank_mass_flow_rate(example_mass_flow_rate_based_tank_seblm): + """Test the mass_flow_rate property of the MassFlowRateBasedTank + subclass of Tank. Parameters ---------- - radius : float - The radius of the spherical cap. - height : float, optional - The height of the spherical cap. If not given, the radius is used. - - Returns - ------- - float - The volume of the spherical cap. + example_mass_flow_rate_based_tank_seblm : MassFlowRateBasedTank + The MassFlowRateBasedTank to be tested. """ - if height is None: - height = radius - return np.pi / 3 * height * (3 * radius**2 - height**2) + tank = example_mass_flow_rate_based_tank_seblm + expected_mass_flow_rate = 0.1 - 0.2 + 0.01 - 0.02 -def tank_volume_function(tank_radius, tank_height, zero_height=0): - """Returns a function that calculates the volume of a cylindrical tank - with spherical caps. + assert np.allclose(expected_mass_flow_rate, tank.net_mass_flow_rate.y_array) - Parameters - ---------- - tank_radius : float - The radius of the cylindrical part of the tank. - tank_height : float - The height of the tank including caps. - zero_height : float, optional - The coordinate of the bottom of the tank. Defaults to 0. - - Returns - ------- - function - A function that calculates the volume of the tank for a given height. - """ - def tank_volume(h): - # Coordinate shift to the bottom of the tank - h = h - zero_height - if h < tank_radius: - return lower_spherical_cap_volume(tank_radius, h) - elif tank_radius <= h < tank_height - tank_radius: - return lower_spherical_cap_volume(tank_radius) + cylinder_volume( - tank_radius, h - tank_radius - ) - else: - return ( - lower_spherical_cap_volume(tank_radius) - + cylinder_volume(tank_radius, tank_height - 2 * tank_radius) - + upper_spherical_cap_volume( - tank_radius, h - (tank_height - tank_radius) - ) - ) - - return tank_volume - - -def cylinder_centroid(height): - """Returns the centroid of a cylinder with the given height. +def test_mass_flow_rate_tank_fluid_mass(example_mass_flow_rate_based_tank_seblm): + """Test the fluid_mass property of the MassFlowRateBasedTank + subclass of Tank. Parameters ---------- - height : float - The height of the cylinder. - - Returns - ------- - float - The centroid of the cylinder. + example_mass_flow_rate_based_tank_seblm : MassFlowRateBasedTank + The MassFlowRateBasedTank to be tested. """ - return height / 2 + tank = example_mass_flow_rate_based_tank_seblm + expected_initial_liquid_mass = 5 + expected_initial_gas_mass = 0.1 + expected_initial_mass = expected_initial_liquid_mass + expected_initial_gas_mass + expected_liquid_mass_flow = 0.1 - 0.2 + expected_gas_mass_flow = 0.01 - 0.02 + expected_total_mass_flow = expected_liquid_mass_flow + expected_gas_mass_flow -def lower_spherical_cap_centroid(radius, height=None): - """Returns the centroid of a spherical cap with the given radius and filled - height that is filled from its convex side. + times = np.linspace(0, 10, 11) - Parameters - ---------- - radius : float - The radius of the spherical cap. - height : float, optional - The height of the spherical cap. If not given, the radius is used. - - Returns - ------- - float - The centroid of the spherical cap. - """ - if height is None: - height = radius - return radius - (0.75 * (2 * radius - height) ** 2 / (3 * radius - height)) + assert np.allclose( + expected_initial_liquid_mass + expected_liquid_mass_flow * times, + tank.liquid_mass.y_array, + ) + assert np.allclose( + expected_initial_gas_mass + expected_gas_mass_flow * times, + tank.gas_mass.y_array, + ) + assert np.allclose( + expected_initial_mass + expected_total_mass_flow * times, + tank.fluid_mass.y_array, + ) -def upper_spherical_cap_centroid(radius, height=None): - """Returns the centroid of a spherical cap with the given radius and filled - height that is filled from its concave side. +def test_mass_flow_rate_tank_liquid_height( + example_mass_flow_rate_based_tank_seblm, lox_fluid_seblm, nitrogen_fluid_seblm +): + """Test the liquid height properties of the MassFlowRateBasedTank + subclass of Tank. Parameters ---------- - radius : float - The radius of the spherical cap. - height : float, optional - The height of the spherical cap. If not given, the radius is used. - - Returns - ------- - float - The centroid of the spherical cap. + example_mass_flow_rate_based_tank_seblm : MassFlowRateBasedTank + The MassFlowRateBasedTank to be tested. + lox_fluid_seblm : Fluid + The Fluid object representing liquid oxygen. + nitrogen_fluid_seblm : Fluid + The Fluid object representing nitrogen gas. """ - if height is None: - height = radius - return 0.75 * (height**3 - 2 * height * radius**2) / (height**2 - 3 * radius**2) + tank = example_mass_flow_rate_based_tank_seblm + def expected_liquid_volume(t): + return (5 + (0.1 - 0.2) * t) / lox_fluid_seblm.density -def tank_centroid_function(tank_radius, tank_height, zero_height=0): - """Returns a function that calculates the centroid of a cylindrical tank - with spherical caps. + def expected_gas_volume(t): + return (0.1 + (0.01 - 0.02) * t) / nitrogen_fluid_seblm.density - Parameters - ---------- - tank_radius : float - The radius of the cylindrical part of the tank. - tank_height : float - The height of the tank including caps. - zero_height : float, optional - The coordinate of the bottom of the tank. Defaults to 0. - - Returns - ------- - function - A function that calculates the centroid of the tank for a given height. - """ + times = np.linspace(0, 10, 11) - def tank_centroid(h): - # Coordinate shift to the bottom of the tank - h = h - zero_height - cylinder_height = tank_height - 2 * tank_radius - - if h < tank_radius: - centroid = lower_spherical_cap_centroid(tank_radius, h) - - elif tank_radius <= h < tank_height - tank_radius: - # Fluid height from cylinder base - base = tank_radius - height = h - base - - balance = lower_spherical_cap_volume( - tank_radius - ) * lower_spherical_cap_centroid(tank_radius) + cylinder_volume( - tank_radius, height - ) * ( - cylinder_centroid(height) + tank_radius - ) - volume = lower_spherical_cap_volume(tank_radius) + cylinder_volume( - tank_radius, height - ) - centroid = balance / volume - - else: - # Fluid height from upper cap base - base = tank_height - tank_radius - height = h - base - - balance = ( - lower_spherical_cap_volume(tank_radius) - * lower_spherical_cap_centroid(tank_radius) - + cylinder_volume(tank_radius, cylinder_height) - * (cylinder_centroid(cylinder_height) + tank_radius) - + upper_spherical_cap_volume(tank_radius, height) - * (upper_spherical_cap_centroid(tank_radius, height) + base) - ) - volume = ( - lower_spherical_cap_volume(tank_radius) - + cylinder_volume(tank_radius, cylinder_height) - + upper_spherical_cap_volume(tank_radius, height) - ) - centroid = balance / volume - - return centroid + zero_height - - return tank_centroid - - -def cylinder_inertia(radius, height, reference=0): - """Returns the inertia of a cylinder with the given radius and height. - - Parameters - ---------- - radius : float - The radius of the cylinder. - height : float - The height of the cylinder. - reference : float, optional - The coordinate of the axis of rotation. - - Returns - ------- - numpy.ndarray - The inertia of the cylinder in the x, y, and z directions. - """ - # Evaluate inertia and perform coordinate shift to the reference point - inertia_x = cylinder_volume(radius, height) * ( - radius**2 / 4 + height**2 / 12 + (height / 2 - reference) ** 2 + assert np.allclose(expected_liquid_volume(times), tank.liquid_volume.y_array) + assert np.allclose( + expected_liquid_volume(times) / tank.geometry.area(0), + tank.liquid_height.y_array, + ) + assert np.allclose( + expected_gas_volume(times), + tank.gas_volume.y_array, + ) + assert np.allclose( + (expected_gas_volume(times) + expected_liquid_volume(times)) + / tank.geometry.area(0), + tank.gas_height.y_array, ) - inertia_y = inertia_x - inertia_z = cylinder_volume(radius, height) * radius**2 / 2 - - return np.array([inertia_x, inertia_y, inertia_z]) -def lower_spherical_cap_inertia(radius, height=None, reference=0): - """Returns the inertia of a spherical cap with the given radius and filled - height that is filled from its convex side. +def test_mass_flow_rate_tank_center_of_mass( + example_mass_flow_rate_based_tank_seblm, lox_fluid_seblm, nitrogen_fluid_seblm +): + """Test the center of mass properties of the MassFlowRateBasedTank + subclass of Tank. Parameters ---------- - radius : float - The radius of the spherical cap. - height : float - The height of the spherical cap. If not given, the radius is used. - reference : float, optional - The coordinate of the axis of rotation. - - Returns - ------- - numpy.ndarray - The inertia of the spherical cap in the x, y, and z directions. + example_mass_flow_rate_based_tank_seblm : MassFlowRateBasedTank + The MassFlowRateBasedTank to be tested. + lox_fluid_seblm : Fluid + The Fluid object representing liquid oxygen. + nitrogen_fluid_seblm : Fluid + The Fluid object representing nitrogen gas. """ - if height is None: - height = radius + # TODO: improve code context and repetition + tank = example_mass_flow_rate_based_tank_seblm - centroid = lower_spherical_cap_centroid(radius, height) + def expected_liquid_center_of_mass(t): + liquid_height = (5 + (0.1 - 0.2) * t) / lox_fluid_seblm.density / np.pi - # Evaluate inertia and perform coordinate shift to the reference point - inertia_x = lower_spherical_cap_volume(radius, height) * ( - ( - np.pi - * height**2 - * ( - -9 * height**3 - + 45 * height**2 * radius - - 80 * height * radius**2 - + 60 * radius**3 - ) - / 60 - ) - - (radius - centroid) ** 2 - + (centroid - reference) ** 2 - ) - inertia_y = inertia_x - inertia_z = lower_spherical_cap_volume(radius, height) * ( - np.pi * height**3 * (3 * height**2 - 15 * height * radius + 20 * radius**2) / 30 - ) - return np.array([inertia_x, inertia_y, inertia_z]) + return liquid_height / 2 + def expected_gas_center_of_mass(t): + liquid_height = (5 + (0.1 - 0.2) * t) / lox_fluid_seblm.density / np.pi + gas_height = (0.1 + (0.01 - 0.02) * t) / nitrogen_fluid_seblm.density / np.pi -def upper_spherical_cap_inertia(radius, height=None, reference=0): - """Returns the inertia of a spherical cap with the given radius and filled - height that is filled from its concave side. + return gas_height / 2 + liquid_height - Parameters - ---------- - radius : float - The radius of the spherical cap. - height : float - The height of the spherical cap. If not given, the radius is used. - reference : float, optional - The coordinate of the axis of rotation. - - Returns - ------- - numpy.ndarray - The inertia of the spherical cap in the x, y, and z directions. - """ - if height is None: - height = radius + def expected_center_of_mass(t): + liquid_mass = 5 + (0.1 - 0.2) * t + gas_mass = 0.1 + (0.01 - 0.02) * t - centroid = upper_spherical_cap_centroid(radius, height) + return ( + liquid_mass * expected_liquid_center_of_mass(t) + + gas_mass * expected_gas_center_of_mass(t) + ) / (liquid_mass + gas_mass) - # Evaluate inertia and perform coordinate shift to the reference point - inertia_x = upper_spherical_cap_volume(radius, height) * ( - ( - ( - np.pi - * height - * (-9 * height**4 + 10 * height**2 * radius**2 + 15 * radius**4) - ) - / 60 - ) - - centroid**2 - + (centroid - reference) ** 2 + times = np.linspace(0, 10, 11) + + assert np.allclose( + expected_liquid_center_of_mass(times), + tank.liquid_center_of_mass.y_array, + atol=1e-4, + rtol=1e-3, + ) + assert np.allclose( + expected_gas_center_of_mass(times), + tank.gas_center_of_mass.y_array, + atol=1e-4, + rtol=1e-3, ) - inertia_y = inertia_x - inertia_z = upper_spherical_cap_volume(radius, height) * ( - np.pi - * height - * (3 * height**4 - 10 * height**2 * radius**2 + 15 * radius**4) - / 30 + assert np.allclose( + expected_center_of_mass(times), + tank.center_of_mass.y_array, + atol=1e-4, + rtol=1e-3, ) - return np.array([inertia_x, inertia_y, inertia_z]) -def tank_inertia_function(tank_radius, tank_height, zero_height=0): - """Returns a function that calculates the inertia of a cylindrical tank - with spherical caps. The reference point is the tank centroid. +def test_mass_flow_rate_tank_inertia( + example_mass_flow_rate_based_tank_seblm, lox_fluid_seblm, nitrogen_fluid_seblm +): + """Test the inertia properties of the MassFlowRateBasedTank + subclass of Tank. Parameters ---------- - tank_radius : float - The radius of the cylindrical part of the tank. - tank_height : float - The height of the tank including caps. - zero_height : float, optional - The coordinate of the bottom of the tank. Defaults to 0. - - Returns - ------- - function - A function that calculates the inertia of the tank for a given height. + example_mass_flow_rate_based_tank_seblm : MassFlowRateBasedTank + The MassFlowRateBasedTank to be tested. + lox_fluid_seblm : Fluid + The Fluid object representing liquid oxygen. + nitrogen_fluid_seblm : Fluid + The Fluid object representing nitrogen gas. """ + # TODO: improve code context and repetition + tank = example_mass_flow_rate_based_tank_seblm - def tank_inertia(h): - # Coordinate shift to the bottom of the tank - h = h - zero_height - center = tank_height / 2 - cylinder_height = tank_height - 2 * tank_radius - - if h < tank_radius: - inertia = lower_spherical_cap_inertia(tank_radius, h, center) - - elif tank_radius <= h < tank_height - tank_radius: - # Fluid height from cylinder base - base = tank_radius - height = h - base + def expected_center_of_mass(t): + liquid_mass = 5 + (0.1 - 0.2) * t + gas_mass = 0.1 + (0.01 - 0.02) * t + liquid_height = liquid_mass / lox_fluid_seblm.density / np.pi + gas_height = gas_mass / nitrogen_fluid_seblm.density / np.pi - lower_centroid = lower_spherical_cap_centroid(tank_radius) - cyl_centroid = cylinder_centroid(height) + base - lower_inertia = lower_spherical_cap_inertia(tank_radius, reference=center) - cyl_inertia = cylinder_inertia(tank_radius, height, reference=center - base) + return ( + liquid_mass * liquid_height / 2 + + gas_mass * (gas_height / 2 + liquid_height) + ) / (liquid_mass + gas_mass) - inertia = lower_inertia + cyl_inertia - - else: - # Fluid height from upper cap base - base = tank_height - tank_radius - height = h - base - - lower_centroid = lower_spherical_cap_centroid(tank_radius) - cyl_centroid = cylinder_centroid(cylinder_height) + tank_radius - upper_centroid = upper_spherical_cap_centroid(tank_radius, height) + base - - lower_inertia = lower_spherical_cap_inertia( - tank_radius, reference=lower_centroid - center - ) - cyl_inertia = cylinder_inertia( - tank_radius, cylinder_height, cyl_centroid - tank_radius - ) - upper_inertia = upper_spherical_cap_inertia( - tank_radius, height, upper_centroid - base - ) + def expected_liquid_inertia(t): + r = 1 + liquid_mass = 5 + (0.1 - 0.2) * t + liquid_height = liquid_mass / lox_fluid_seblm.density / np.pi + liquid_com = liquid_height / 2 + + return ( + 1 / 4 * liquid_mass * r**2 + + 1 / 12 * liquid_mass * liquid_height**2 + + liquid_mass * (liquid_com - expected_center_of_mass(t)) ** 2 + ) - inertia = lower_inertia + cyl_inertia + upper_inertia + def expected_gas_inertia(t): + r = 1 + liquid_mass = 5 + (0.1 - 0.2) * t + gas_mass = 0.1 + (0.01 - 0.02) * t + liquid_height = liquid_mass / lox_fluid_seblm.density / np.pi + gas_height = gas_mass / nitrogen_fluid_seblm.density / np.pi + gas_com = gas_height / 2 + liquid_height + + return ( + 1 / 4 * gas_mass * r**2 + + 1 / 12 * gas_mass * (gas_height - liquid_height) ** 2 + + gas_mass * (gas_com - expected_center_of_mass(t)) ** 2 + ) - return inertia + times = np.linspace(0, 10, 11) - return tank_inertia + assert np.allclose( + expected_liquid_inertia(times), + tank.liquid_inertia.y_array, + atol=1e-3, + rtol=1e-2, + ) + assert np.allclose( + expected_gas_inertia(times), tank.gas_inertia.y_array, atol=1e-3, rtol=1e-2 + ) + assert np.allclose( + expected_liquid_inertia(times) + expected_gas_inertia(times), + tank.inertia.y_array, + atol=1e-3, + rtol=1e-2, + ) diff --git a/tests/unit/test_tank_geometry.py b/tests/unit/test_tank_geometry.py new file mode 100644 index 000000000..54e3671c2 --- /dev/null +++ b/tests/unit/test_tank_geometry.py @@ -0,0 +1,126 @@ +from pathlib import Path + +import numpy as np +import pytest + +PRESSURANT_PARAMS = (0.135 / 2, 0.981) +PROPELLANT_PARAMS = (0.0744, 0.8068) +SPHERICAL_PARAMS = (0.05, 0.1) + +BASE_PATH = Path("tests/fixtures/motor/data/") + +parametrize_fixtures = pytest.mark.parametrize( + "params", + [ + ( + "pressurant_tank_geometry", + PRESSURANT_PARAMS, + BASE_PATH / "cylindrical_pressurant_tank_expected.csv", + ), + ( + "propellant_tank_geometry", + PROPELLANT_PARAMS, + BASE_PATH / "cylindrical_oxidizer_tank_expected.csv", + ), + ( + "spherical_oxidizer_geometry", + SPHERICAL_PARAMS, + BASE_PATH / "spherical_oxidizer_tank_expected.csv", + ), + ], +) + + +@parametrize_fixtures +def test_tank_bounds(params, request): + """Test basic geometric properties of the tanks.""" + geometry, (expected_radius, expected_height), _ = params + geometry = request.getfixturevalue(geometry) + + expected_total_height = expected_height + + assert np.isclose(geometry.radius(0), expected_radius) + assert np.isclose(geometry.total_height, expected_total_height) + + +@parametrize_fixtures +def test_tank_coordinates(params, request): + """Test basic coordinate values of the tanks.""" + geometry, (_, height), _ = params + geometry = request.getfixturevalue(geometry) + + expected_bottom = -height / 2 + expected_top = height / 2 + + assert np.isclose(geometry.bottom, expected_bottom) + assert np.isclose(geometry.top, expected_top) + + +@parametrize_fixtures +def test_tank_total_volume(params, request): + """Test the total volume of the tanks comparing to the analytically + calculated values. + """ + geometry, (radius, height), _ = params + geometry = request.getfixturevalue(geometry) + + expected_total_volume = ( + np.pi * radius**2 * (height - 2 * radius) + 4 / 3 * np.pi * radius**3 + ) + + assert np.isclose(geometry.total_volume, expected_total_volume) + + +@parametrize_fixtures +def test_tank_volume(params, request): + """Test the volume of the tanks at different heights comparing to the + CAD generated values. + """ + geometry, *_, file_path = params + geometry = request.getfixturevalue(geometry) + + heights, expected_volumes = np.loadtxt( + file_path, delimiter=",", skiprows=1, usecols=(0, 1), unpack=True + ) + + assert np.allclose(expected_volumes, geometry.volume(heights)) + + +@parametrize_fixtures +def test_tank_centroid(params, request): + """Test the centroid of the tanks at different heights comparing to the + analytically calculated values. + """ + geometry, *_, file_path = params + geometry = request.getfixturevalue(geometry) + + heights, expected_volumes, expected_centroids = np.loadtxt( + file_path, delimiter=",", skiprows=1, usecols=(0, 1, 2), unpack=True + ) + + # For higher accuracy: geometry.volume_moment(geometry.bottom, h)(h) + assert np.allclose( + expected_centroids * expected_volumes, + geometry.volume_moment(geometry.bottom, geometry.top)(heights), + ) + + +@parametrize_fixtures +def test_tank_inertia(params, request): + """Test the inertia of the tanks at different heights comparing to the + analytically calculated values. + """ + geometry, *_, file_path = params + geometry = request.getfixturevalue(geometry) + + heights, expected_inertia = np.loadtxt( + file_path, delimiter=",", skiprows=1, usecols=(0, 3), unpack=True + ) + + # For higher accuracy: geometry.Ix_volume(geometry.bottom, h)(h) + assert np.allclose( + expected_inertia[1:], + geometry.Ix_volume(geometry.bottom, geometry.top)(heights[1:]), + rtol=1e-5, + atol=1e-9, + )