From 3f909934f75e779afc0dc166cbc5be1886ce71f2 Mon Sep 17 00:00:00 2001 From: Pedro Bressan Date: Wed, 28 Aug 2024 10:29:06 -0300 Subject: [PATCH 1/5] MNT: Add CAD data for tank geometry validation. --- rocketpy/motors/tank_geometry.py | 1 - tests/conftest.py | 1 + .../motor/{ => data}/Cesaroni_M1670.eng | 0 .../{ => data}/Cesaroni_M1670_shifted.eng | 0 .../motor/data/cad_tank_geometry.ipynb | 230 ++++++++ .../cylindrical_oxidizer_tank_expected.csv | 26 + .../cylindrical_pressurant_tank_expected.csv | 26 + .../data/spherical_oxidizer_tank_expected.csv | 26 + tests/fixtures/motor/solid_motor_fixtures.py | 2 +- .../fixtures/motor/tank_geometry_fixtures.py | 42 ++ tests/fixtures/motor/tanks_fixtures.py | 44 +- tests/unit/test_solidmotor.py | 2 +- tests/unit/test_tank.py | 527 ------------------ tests/unit/test_tank_geometry.py | 131 +++++ 14 files changed, 505 insertions(+), 553 deletions(-) rename tests/fixtures/motor/{ => data}/Cesaroni_M1670.eng (100%) rename tests/fixtures/motor/{ => data}/Cesaroni_M1670_shifted.eng (100%) create mode 100644 tests/fixtures/motor/data/cad_tank_geometry.ipynb create mode 100644 tests/fixtures/motor/data/cylindrical_oxidizer_tank_expected.csv create mode 100644 tests/fixtures/motor/data/cylindrical_pressurant_tank_expected.csv create mode 100644 tests/fixtures/motor/data/spherical_oxidizer_tank_expected.csv create mode 100644 tests/fixtures/motor/tank_geometry_fixtures.py create mode 100644 tests/unit/test_tank_geometry.py diff --git a/rocketpy/motors/tank_geometry.py b/rocketpy/motors/tank_geometry.py index 272f8fc93..38272dfc4 100644 --- a/rocketpy/motors/tank_geometry.py +++ b/rocketpy/motors/tank_geometry.py @@ -1,7 +1,6 @@ from functools import cached_property import numpy as np - from ..mathutils.function import Function, PiecewiseFunction, funcify_method from ..plots.tank_geometry_plots import _TankGeometryPlots from ..prints.tank_geometry_prints import _TankGeometryPrints diff --git a/tests/conftest.py b/tests/conftest.py index a1e4b7f99..019273ff8 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/motor/Cesaroni_M1670.eng b/tests/fixtures/motor/data/Cesaroni_M1670.eng similarity index 100% rename from tests/fixtures/motor/Cesaroni_M1670.eng rename to tests/fixtures/motor/data/Cesaroni_M1670.eng diff --git a/tests/fixtures/motor/Cesaroni_M1670_shifted.eng b/tests/fixtures/motor/data/Cesaroni_M1670_shifted.eng similarity index 100% rename from tests/fixtures/motor/Cesaroni_M1670_shifted.eng rename to tests/fixtures/motor/data/Cesaroni_M1670_shifted.eng diff --git a/tests/fixtures/motor/data/cad_tank_geometry.ipynb b/tests/fixtures/motor/data/cad_tank_geometry.ipynb new file mode 100644 index 000000000..7dec9cd9b --- /dev/null +++ b/tests/fixtures/motor/data/cad_tank_geometry.ipynb @@ -0,0 +1,230 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# CadQuery Validation of Tank Geometry Parameters" + ] + }, + { + "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": 4, + "metadata": {}, + "outputs": [], + "source": [ + "def export_expected_parameters(name, datapoints):\n", + " with open(f'{name}_expected.csv', mode='w') as file:\n", + " writer = csv.writer(file)\n", + " writer.writerow(['level_height', 'volume', 'center_of_mass', 'inertia'])\n", + " for data_row in datapoints:\n", + " writer.writerow([*data_row])" + ] + }, + { + "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": "code", + "execution_count": 7, + "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.\n", + " center_of_mass : float\n", + " The z-coordinate of the center of mass.\n", + " inertia : float\n", + " The inertia of the cylinder 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/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/solid_motor_fixtures.py b/tests/fixtures/motor/solid_motor_fixtures.py index eff7d65d5..ba776e6b0 100644 --- a/tests/fixtures/motor/solid_motor_fixtures.py +++ b/tests/fixtures/motor/solid_motor_fixtures.py @@ -52,7 +52,7 @@ def cesaroni_m1670_shifted(): # old name: solid_motor A simple object of the SolidMotor class """ example_motor = SolidMotor( - thrust_source="tests/fixtures/motor/Cesaroni_M1670_shifted.eng", + thrust_source="tests/fixtures/motor/data/Cesaroni_M1670_shifted.eng", burn_time=3.9, dry_mass=1.815, dry_inertia=(0.125, 0.125, 0.002), diff --git a/tests/fixtures/motor/tank_geometry_fixtures.py b/tests/fixtures/motor/tank_geometry_fixtures.py new file mode 100644 index 000000000..b56ee1971 --- /dev/null +++ b/tests/fixtures/motor/tank_geometry_fixtures.py @@ -0,0 +1,42 @@ +import numpy as np +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 4238e1e1e..23790adf8 100644 --- a/tests/fixtures/motor/tanks_fixtures.py +++ b/tests/fixtures/motor/tanks_fixtures.py @@ -1,18 +1,11 @@ import numpy as np import pytest -from rocketpy import ( - CylindricalTank, - Function, - LevelBasedTank, - MassBasedTank, - SphericalTank, - UllageBasedTank, -) +from rocketpy import Function, LevelBasedTank, MassBasedTank, UllageBasedTank @pytest.fixture -def pressurant_tank(pressurant_fluid): +def pressurant_tank(pressurant_fluid, pressurant_tank_geometry): """An example of a pressurant cylindrical tank with spherical caps. @@ -20,16 +13,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/SEBLM/pressurantMassFiltered.csv", @@ -41,7 +35,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 +46,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/SEBLM/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 +70,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,19 +81,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/SEBLM/test124_Lox_Volume.csv") * 1e-3 + geometry.total_volume + -Function("data/SEBLM/test124_Lox_Volume.csv") * 1e-3 + + 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, @@ -108,7 +105,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 @@ -121,14 +120,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/unit/test_solidmotor.py b/tests/unit/test_solidmotor.py index 064c8210e..a064113af 100644 --- a/tests/unit/test_solidmotor.py +++ b/tests/unit/test_solidmotor.py @@ -94,7 +94,7 @@ def tests_import_eng_asserts_read_values_correctly(cesaroni_m1670): The SolidMotor object to be used in the tests. """ comments, description, data_points = cesaroni_m1670.import_eng( - "tests/fixtures/motor/Cesaroni_M1670.eng" + "tests/fixtures/motor/data/Cesaroni_M1670.eng" ) assert comments == [";this motor is COTS", ";3.9 burnTime", ";"] diff --git a/tests/unit/test_tank.py b/tests/unit/test_tank.py index 3a77a8bca..a793dab20 100644 --- a/tests/unit/test_tank.py +++ b/tests/unit/test_tank.py @@ -4,7 +4,6 @@ from math import isclose import numpy as np -import pytest from rocketpy import ( Fluid, @@ -15,117 +14,6 @@ TankGeometry, ) -pressurant_params = (0.135 / 2, 0.981) -fuel_params = (0.0744, 0.8068) -oxidizer_params = (0.0744, 0.8068) - - -parametrize_fixtures = pytest.mark.parametrize( - "params", - [ - ("pressurant_tank", pressurant_params), - ("fuel_tank", fuel_params), - ("oxidizer_tank", oxidizer_params), - ], -) - - -@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. - """ - tank, (radius, height) = params - tank = request.getfixturevalue(tank) - - expected_total_volume = ( - np.pi * radius**2 * (height - 2 * radius) + 4 / 3 * np.pi * radius**3 - ) - - 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) - - -@parametrize_fixtures -def test_tank_inertia(params, request): - """Test the inertia 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_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 @@ -542,418 +430,3 @@ def ixy(x): 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. - - Parameters - ---------- - radius : float - The radius of the cylinder. - height : float - The height of the cylinder. - - Returns - ------- - float - The volume of the cylinder. - """ - return np.pi * radius**2 * height - - -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) - - -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. - - 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 * (3 * radius**2 - height**2) - - -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. - - 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. - - Parameters - ---------- - height : float - The height of the cylinder. - - Returns - ------- - float - The centroid of the cylinder. - """ - return height / 2 - - -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. - - 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)) - - -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. - - 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 0.75 * (height**3 - 2 * height * radius**2) / (height**2 - 3 * radius**2) - - -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. - - 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. - """ - - 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 - ) - 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. - - 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 - - centroid = lower_spherical_cap_centroid(radius, height) - - # 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]) - - -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. - - 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 - - centroid = upper_spherical_cap_centroid(radius, height) - - # 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 - ) - 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 - ) - 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. - - 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. - """ - - 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 - - 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) - - 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 - ) - - inertia = lower_inertia + cyl_inertia + upper_inertia - - return inertia - - return tank_inertia diff --git a/tests/unit/test_tank_geometry.py b/tests/unit/test_tank_geometry.py new file mode 100644 index 000000000..4d4649510 --- /dev/null +++ b/tests/unit/test_tank_geometry.py @@ -0,0 +1,131 @@ +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, atol=1e-6) + assert np.isclose(geometry.total_height, expected_total_height, atol=1e-6) + + +@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, atol=1e-6) + assert np.isclose(geometry.top, expected_top, atol=1e-6) + + +@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, atol=1e-6) + + +@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) + + expected_data = np.loadtxt(file_path, delimiter=",", skiprows=1) + + heights = expected_data[:, 0] + expected_volumes = expected_data[:, 1] + + assert np.allclose(expected_volumes, geometry.volume(heights), atol=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. + """ + geometry, *_, file_path = params + geometry = request.getfixturevalue(geometry) + + expected_data = np.loadtxt(file_path, delimiter=",", skiprows=1) + + heights = expected_data[:, 0] + expected_volumes = expected_data[:, 1] + expected_centroids = expected_data[:, 2] + + for i, h in enumerate(heights[1:], 1): # Avoid empty geometry + # Loss of accuracy when volume is close to zero + assert np.isclose( + expected_centroids[i], + geometry.volume_moment(geometry.bottom, h)(h) / expected_volumes[i], + atol=1e-3, + ) + + +@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) + + expected_data = np.loadtxt(file_path, delimiter=",", skiprows=1) + + heights = expected_data[:, 0] + expected_inertia = expected_data[:, 3] + + for i, h in enumerate(heights): # Avoid empty geometry + assert np.isclose( + expected_inertia[i], + geometry.Ix_volume(geometry.bottom, h)(h), + atol=1e-5, + ) From 626f46dcab4b7171faef807eb42ccb026e99be61 Mon Sep 17 00:00:00 2001 From: Pedro Bressan Date: Wed, 28 Aug 2024 20:14:48 -0300 Subject: [PATCH 2/5] MNT: Refactor tank testing and improve file structure. --- .vscode/settings.json | 1 + rocketpy/motors/tank.py | 6 +- rocketpy/motors/tank_geometry.py | 1 + tests/fixtures/motor/liquid_fixtures.py | 26 + tests/fixtures/motor/tanks_fixtures.py | 327 +++++++++++- tests/integration/test_tank.py | 33 ++ tests/unit/test_tank.py | 667 +++++++++++------------- 7 files changed, 687 insertions(+), 374 deletions(-) create mode 100644 tests/integration/test_tank.py diff --git a/.vscode/settings.json b/.vscode/settings.json index bc43e427c..d3f282d0b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -248,6 +248,7 @@ "SBMT", "scilimits", "searchsorted", + "seblm", "seealso", "setrail", "simplekml", diff --git a/rocketpy/motors/tank.py b/rocketpy/motors/tank.py index 6fabaa341..33152b742 100644 --- a/rocketpy/motors/tank.py +++ b/rocketpy/motors/tank.py @@ -631,7 +631,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( @@ -655,7 +657,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( diff --git a/rocketpy/motors/tank_geometry.py b/rocketpy/motors/tank_geometry.py index 38272dfc4..272f8fc93 100644 --- a/rocketpy/motors/tank_geometry.py +++ b/rocketpy/motors/tank_geometry.py @@ -1,6 +1,7 @@ from functools import cached_property import numpy as np + from ..mathutils.function import Function, PiecewiseFunction, funcify_method from ..plots.tank_geometry_plots import _TankGeometryPlots from ..prints.tank_geometry_prints import _TankGeometryPrints diff --git a/tests/fixtures/motor/liquid_fixtures.py b/tests/fixtures/motor/liquid_fixtures.py index 85a17e3a4..1d4fb1792 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/tanks_fixtures.py b/tests/fixtures/motor/tanks_fixtures.py index 23790adf8..885e060f8 100644 --- a/tests/fixtures/motor/tanks_fixtures.py +++ b/tests/fixtures/motor/tanks_fixtures.py @@ -1,7 +1,332 @@ import numpy as np import pytest -from rocketpy import Function, LevelBasedTank, MassBasedTank, UllageBasedTank +from rocketpy import ( + CylindricalTank, + Fluid, + Function, + LevelBasedTank, + MassBasedTank, + MassFlowRateBasedTank, + TankGeometry, + UllageBasedTank, +) + + +@pytest.fixture +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/berkeley/Test135LoxMass.csv", + gas_mass="./data/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/berkeley/ExampleTankLiquidMassData.csv", + gas_mass="./data/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/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 diff --git a/tests/integration/test_tank.py b/tests/integration/test_tank.py new file mode 100644 index 000000000..e5bfcc127 --- /dev/null +++ b/tests/integration/test_tank.py @@ -0,0 +1,33 @@ +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.prints.all() is None + assert tank.plots.all() 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 a793dab20..01e24b82e 100644 --- a/tests/unit/test_tank.py +++ b/tests/unit/test_tank.py @@ -1,432 +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 - -from rocketpy import ( - Fluid, - Function, - LevelBasedTank, - MassBasedTank, - MassFlowRateBasedTank, - TankGeometry, +import pytest +import scipy.integrate as spi + +BASE_PATH = Path("./data/berkeley/") + + +@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(): - """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. +def test_mass_based_tank_fluid_mass(params, request): + """Test the fluid_mass property of the MassBasedTank subclass of Tank + class. + + Parameters + ---------- + params : tuple + A tuple containing test parameters. + request : _pytest.fixtures.FixtureRequest + A pytest fixture request object. """ - 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, - } - ) - - # Import liquid mass data - lox_masses = "./data/berkeley/Test135LoxMass.csv" - example_liquid_masses = "./data/berkeley/ExampleTankLiquidMassData.csv" - - # Import gas mass data - gas_masses = "./data/berkeley/Test135GasMass.csv" - example_gas_masses = "./data/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, + 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=",") + + assert np.allclose( + expected_liquid_mass[:, 1], + tank.liquid_mass(expected_liquid_mass[:, 0]), + rtol=1e-2, + atol=1e-4, ) - - # 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 np.allclose( + expected_gas_mass[:, 1], + tank.gas_mass(expected_gas_mass[:, 0]), + rtol=1e-1, + atol=1e-4, ) - # 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 +@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. + + Parameters + ---------- + params : tuple + A tuple containing test parameters. + request : _pytest.fixtures.FixtureRequest + A pytest fixture request object. + """ + 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=",") + + # 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 + ) + computed_final_mass = spi.simpson( + tank.net_mass_flow_rate.y_array, + x=tank.net_mass_flow_rate.x_array, + ) - def test_mass(): - """Test mass function of MassBasedTank subclass of Tank""" + assert isclose(expected_mass_variation, computed_final_mass, rel_tol=1e-2) - 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 +def test_level_based_tank_liquid_level(real_level_based_tank_seblm): + """Test the liquid_level property of LevelBasedTank + subclass of Tank. - lox_vals = Function(lox_masses).y_array + 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 real_expected(t): - return lox_vals[t] + assert np.allclose(level_data, tank.liquid_height.get_source()) - real_calculated = real_tank_lox.fluid_mass - test(example_calculated, example_expected, 5) - test(real_calculated, real_expected, 15.5, real=True) +def test_level_based_tank_mass(real_level_based_tank_seblm): + """Test the mass property of LevelBasedTank subclass of Tank. - def test_net_mfr(): - """Test net_mass_flow_rate function of MassBasedTank subclass of Tank""" + Parameters + ---------- + real_level_based_tank_seblm : LevelBasedTank + The LevelBasedTank to be tested. + """ + tank = real_level_based_tank_seblm + mass_data = np.loadtxt(BASE_PATH / "loxMass.csv", delimiter=",") - def example_expected(_): - return ( - liquid_mass_flow_rate_in - - liquid_mass_flow_rate_out - + gas_mass_flow_rate_in - - gas_mass_flow_rate_out - ) + # Soft tolerances for the whole curve + assert np.allclose(mass_data, tank.fluid_mass.get_source(), rtol=1e-1, atol=6e-1) - example_calculated = example_tank_lox.net_mass_flow_rate + # 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 + ) - liquid_mfrs = Function(example_liquid_masses).y_array - gas_mfrs = Function(example_gas_masses).y_array +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. - def real_expected(t): - return (liquid_mfrs[t] + gas_mfrs[t]) / t + Parameters + ---------- + example_mass_flow_rate_based_tank_seblm : MassFlowRateBasedTank + The MassFlowRateBasedTank to be tested. + """ + tank = example_mass_flow_rate_based_tank_seblm - real_calculated = real_tank_lox.net_mass_flow_rate + expected_mass_flow_rate = 0.1 - 0.2 + 0.01 - 0.02 - test(example_calculated, example_expected, 10) - test(real_calculated, real_expected, 15.5, real=True) + assert np.allclose(expected_mass_flow_rate, tank.net_mass_flow_rate.y_array) - test_mass() - test_net_mfr() +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. -def test_level_based_tank(): - """Test LevelBasedTank subclass of Tank class using Berkeley SEB team's - tank data of fluid level. + Parameters + ---------- + example_mass_flow_rate_based_tank_seblm : MassFlowRateBasedTank + The MassFlowRateBasedTank to be tested. """ - lox = Fluid(name="LOx", density=1141.7) - n2 = Fluid(name="Nitrogen Gas", density=51.75) - - test_dir = "./data/berkeley/" + tank = example_mass_flow_rate_based_tank_seblm - def top_endcap(y): - return np.sqrt(0.0775**2 - (y - 0.692300000000001) ** 2) + 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 bottom_endcap(y): - return np.sqrt(0.0775**2 - (0.0775 - y) ** 2) + times = np.linspace(0, 10, 11) - tank_geometry = TankGeometry( - { - (0, 0.0559): bottom_endcap, - (0.0559, 0.7139): lambda y: 0.0744, - (0.7139, 0.7698): top_endcap, - } + assert np.allclose( + expected_initial_liquid_mass + expected_liquid_mass_flow * times, + tank.liquid_mass.y_array, ) - - 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]) + assert np.allclose( + expected_initial_gas_mass + expected_gas_mass_flow * times, + tank.gas_mass.y_array, ) - 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 + assert np.allclose( + expected_initial_mass + expected_total_mass_flow * times, + tank.fluid_mass.y_array, ) -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. +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 + ---------- + 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. """ + tank = example_mass_flow_rate_based_tank_seblm - 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 - ) + def expected_liquid_volume(t): + return (5 + (0.1 - 0.2) * t) / lox_fluid_seblm.density - lm = t.fluid_mass - test(lm, m) + def expected_gas_volume(t): + return (0.1 + (0.01 - 0.02) * t) / nitrogen_fluid_seblm.density - def test_liquid_height(): - def alv(x): - return ( - initial_liquid_mass - + (liquid_mass_flow_rate_in - liquid_mass_flow_rate_out) * x - ) / lox.density + times = np.linspace(0, 10, 11) - 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 - ) + 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, + ) - def liquid_volume(x): - return liquid_mass(x) / lox.density - def liquid_height(x): - return liquid_volume(x) / (np.pi) +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 + ---------- + 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 gas_mass(x): - return ( - initial_gas_mass + (gas_mass_flow_rate_in - gas_mass_flow_rate_out) * x - ) + def expected_liquid_center_of_mass(t): + liquid_height = (5 + (0.1 - 0.2) * t) / lox_fluid_seblm.density / np.pi - def gas_volume(x): - return gas_mass(x) / n2.density + return liquid_height / 2 - def gas_height(x): - return gas_volume(x) / np.pi + liquid_height(x) + 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 liquid_com(x): - return liquid_height(x) / 2 + return gas_height / 2 + liquid_height - def gas_com(x): - return (gas_height(x) - liquid_height(x)) / 2 + liquid_height(x) + def expected_center_of_mass(t): + liquid_mass = 5 + (0.1 - 0.2) * t + gas_mass = 0.1 + (0.01 - 0.02) * t - def acom(x): - return (liquid_mass(x) * liquid_com(x) + gas_mass(x) * gas_com(x)) / ( - liquid_mass(x) + gas_mass(x) - ) + return ( + liquid_mass * expected_liquid_center_of_mass(t) + + gas_mass * expected_gas_center_of_mass(t) + ) / (liquid_mass + gas_mass) - tcom = t.center_of_mass - test(tcom, acom) + times = np.linspace(0, 10, 11) - def test_inertia(): - def liquid_mass(x): - return ( - initial_liquid_mass - + (liquid_mass_flow_rate_in - liquid_mass_flow_rate_out) * x - ) + 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, + ) + assert np.allclose( + expected_center_of_mass(times), + tank.center_of_mass.y_array, + atol=1e-4, + rtol=1e-3, + ) - def liquid_volume(x): - return liquid_mass(x) / lox.density - def liquid_height(x): - return liquid_volume(x) / (np.pi) +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 + ---------- + 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 gas_mass(x): - return ( - initial_gas_mass + (gas_mass_flow_rate_in - gas_mass_flow_rate_out) * x - ) + 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 - def gas_volume(x): - return gas_mass(x) / n2.density + return ( + liquid_mass * liquid_height / 2 + + gas_mass * (gas_height / 2 + liquid_height) + ) / (liquid_mass + gas_mass) - 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 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 - def acom(x): - return (liquid_mass(x) * liquid_com(x) + gas_mass(x) * gas_com(x)) / ( - liquid_mass(x) + gas_mass(x) - ) + return ( + 1 / 4 * liquid_mass * r**2 + + 1 / 12 * liquid_mass * liquid_height**2 + + liquid_mass * (liquid_com - expected_center_of_mass(t)) ** 2 + ) + def expected_gas_inertia(t): 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, + 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 + ) + + times = np.linspace(0, 10, 11) + + assert np.allclose( + expected_liquid_inertia(times), + tank.liquid_inertia.y_array, + atol=1e-3, + rtol=1e-2, ) - 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, + 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, ) - - test_nmfr() - test_mass() - test_liquid_height() - test_com() - test_inertia() From 64b74a0a9d2ca9a0b3c224737cd95ed38be54a6b Mon Sep 17 00:00:00 2001 From: Pedro Bressan Date: Wed, 28 Aug 2024 20:17:04 -0300 Subject: [PATCH 3/5] STY: fix pylint suggestions. --- tests/integration/test_tank.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/integration/test_tank.py b/tests/integration/test_tank.py index e5bfcc127..621983503 100644 --- a/tests/integration/test_tank.py +++ b/tests/integration/test_tank.py @@ -1,3 +1,5 @@ +# pylint: disable=unused-argument + from unittest.mock import patch import pytest From 6cb7d9bdbb9dc0bfc8804f7ef0e7fd0ead54783d Mon Sep 17 00:00:00 2001 From: Pedro Bressan Date: Thu, 29 Aug 2024 10:05:56 -0300 Subject: [PATCH 4/5] ENH: implement Tanks prints and plots. --- rocketpy/motors/tank.py | 90 +++++++++++++-------------- rocketpy/plots/tank_geometry_plots.py | 6 +- rocketpy/plots/tank_plots.py | 59 ++++++++++++++++++ rocketpy/prints/fluid_prints.py | 2 + rocketpy/prints/tank_prints.py | 35 +++++++++++ 5 files changed, 144 insertions(+), 48 deletions(-) diff --git a/rocketpy/motors/tank.py b/rocketpy/motors/tank.py index 33152b742..1e8947c7f 100644 --- a/rocketpy/motors/tank.py +++ b/rocketpy/motors/tank.py @@ -335,7 +335,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 +361,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 +387,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 @@ -573,28 +573,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", ) @@ -607,7 +607,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 @@ -620,7 +620,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 @@ -646,7 +646,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 @@ -671,7 +671,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. @@ -685,7 +685,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. @@ -699,7 +699,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. @@ -713,7 +713,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 @@ -727,7 +727,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. @@ -739,7 +739,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. @@ -751,7 +751,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 @@ -788,7 +788,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 @@ -891,7 +891,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 @@ -904,7 +904,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 @@ -917,7 +917,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 @@ -933,7 +933,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 @@ -947,7 +947,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 @@ -960,7 +960,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. @@ -972,7 +972,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. @@ -984,7 +984,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 @@ -998,7 +998,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 @@ -1085,7 +1085,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 @@ -1101,7 +1101,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 @@ -1114,7 +1114,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 @@ -1137,7 +1137,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. @@ -1149,7 +1149,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 @@ -1167,7 +1167,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 @@ -1180,7 +1180,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. @@ -1192,7 +1192,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. @@ -1204,7 +1204,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 @@ -1300,7 +1300,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 @@ -1313,7 +1313,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 @@ -1326,7 +1326,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. @@ -1338,7 +1338,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. @@ -1350,7 +1350,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 @@ -1378,7 +1378,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. @@ -1390,7 +1390,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. @@ -1402,7 +1402,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 @@ -1437,7 +1437,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 7c0541eb2..6d94421a8 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 import Function + class _TankPlots: """Class that holds plot methods for Tank class. @@ -28,6 +30,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): @@ -90,6 +93,55 @@ def draw(self): ax.set_xlim(-1.2 * x_max, 1.2 * x_max) ax.set_ylim(-1.5 * y_max, 1.5 * y_max) + def fluid_volume(self): + """Plots both the liquid and gas fluid volumes.""" + _, 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"]) + plt.show() + + def fluid_height(self): + """Plots both the liquid and gas fluid height.""" + _, 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"]) + plt.show() + + def fluid_center_of_mass(self): + """Plots the gas, liquid and combined center of mass.""" + _, 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"]) + plt.show() + def all(self): """Prints out all graphs available about the Tank. It simply calls all the other plotter methods in this class. @@ -98,3 +150,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..463d7cc91 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\n:") + 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):.4f} kg") + print(f"Initial Liquid Volume: {self.tank.liquid_volume(initial_time):.4f} m^3") + print(f"Initial Liquid Level: {self.tank.liquid_height(initial_time):.4f} m") + print(f"\nFinal Quantities at t = {final_time:.2f} s:") + print(f"Final Fluid Mass: {self.tank.fluid_mass(final_time):.4f} kg") + print(f"Final Liquid Volume: {self.tank.liquid_volume(final_time):.4f} m^3") + print(f"Final Liquid Level: {self.tank.liquid_height(final_time):.4f} m") + def all(self): """Prints out all data available about the Tank. @@ -32,3 +63,7 @@ def all(self): ------- None """ + print(f"Tank '{self.tank.name}' Data\n:") + self.tank.geometry.prints.all() + self.fluid_parameters() + self.mass_flux() From 8c6eefafc9c24ce1a82dac0f2e859fd25670b1f5 Mon Sep 17 00:00:00 2001 From: Pedro Bressan Date: Fri, 30 Aug 2024 18:05:27 -0300 Subject: [PATCH 5/5] MNT: add recent changes to CHANGELOG. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c084940e1..8967ffc29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ 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) - DOC: Fix documentation dependencies [#651](https://github.com/RocketPy-Team/RocketPy/pull/651) - DOC: Fix documentation warnings [#645](https://github.com/RocketPy-Team/RocketPy/pull/645) - DOC: New Environment class docs pages [#644](https://github.com/RocketPy-Team/RocketPy/pull/644)