From 03f5c8924e85a83189cb1e54e7b1406d2ed32a1f Mon Sep 17 00:00:00 2001 From: JoePfeiffer Date: Sat, 10 Feb 2024 06:59:48 -0700 Subject: [PATCH 1/3] allow sustainer to tumble before apogee. I don't know if it's actually possible for the sustainer to tumble when not under thrust (I guess it would need a tractor motor and very careful CG-CP design so it would tumble immediately after burnout?) but the conditions are checked separately just in case --- .../simulation/BasicEventSimulationEngine.java | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/core/src/net/sf/openrocket/simulation/BasicEventSimulationEngine.java b/core/src/net/sf/openrocket/simulation/BasicEventSimulationEngine.java index 232da2090..821bab500 100644 --- a/core/src/net/sf/openrocket/simulation/BasicEventSimulationEngine.java +++ b/core/src/net/sf/openrocket/simulation/BasicEventSimulationEngine.java @@ -258,21 +258,16 @@ public class BasicEventSimulationEngine implements SimulationEngine { // Check for Tumbling // Conditions for transition are: - // apogee reached (if sustainer stage) - // and is not already tumbling + // is not already tumbling // and not stable (cg > cp) // and aoa > AOA_TUMBLE_CONDITION threshold - // and thrust < THRUST_TUMBLE_CONDITION threshold if (!currentStatus.isTumbling()) { final double cp = currentStatus.getFlightData().getLast(FlightDataType.TYPE_CP_LOCATION); final double cg = currentStatus.getFlightData().getLast(FlightDataType.TYPE_CG_LOCATION); final double aoa = currentStatus.getFlightData().getLast(FlightDataType.TYPE_AOA); - final boolean wantToTumble = (cg > cp && aoa > AOA_TUMBLE_CONDITION); - final boolean isSustainer = currentStatus.getConfiguration().isStageActive(0); - final boolean isApogee = currentStatus.isApogeeReached(); - if (wantToTumble && (isApogee || !isSustainer)) { + if (cg > cp && aoa > AOA_TUMBLE_CONDITION) { currentStatus.addEvent(new FlightEvent(FlightEvent.Type.TUMBLE, currentStatus.getSimulationTime())); } } From 02cd098d517fd95d76a1a67bf16e038a70bae37e Mon Sep 17 00:00:00 2001 From: JoePfeiffer Date: Mon, 12 Feb 2024 14:48:50 -0700 Subject: [PATCH 2/3] Update unit tests The Falcon 9 Heavy test rocket isn't aerodynamically stable, so allowing the sustainer to tumble before apogee causes an immediate TUMBLE_UNDER_THRUST abort. So, this commit adds a new rocket with a sustainer, core booster, and two side boosters that is stable. --- .../net/sf/openrocket/util/TestRockets.java | 157 ++++++++++++++++++ .../simulation/FlightEventsTest.java | 109 +++++++----- 2 files changed, 221 insertions(+), 45 deletions(-) diff --git a/core/src/net/sf/openrocket/util/TestRockets.java b/core/src/net/sf/openrocket/util/TestRockets.java index 3f165245f..500bacaa7 100644 --- a/core/src/net/sf/openrocket/util/TestRockets.java +++ b/core/src/net/sf/openrocket/util/TestRockets.java @@ -10,12 +10,14 @@ import net.sf.openrocket.database.Databases; import net.sf.openrocket.document.OpenRocketDocument; import net.sf.openrocket.document.OpenRocketDocumentFactory; import net.sf.openrocket.document.Simulation; +import net.sf.openrocket.document.StorageOptions; import net.sf.openrocket.file.openrocket.OpenRocketSaver; import net.sf.openrocket.logging.ErrorSet; import net.sf.openrocket.logging.WarningSet; import net.sf.openrocket.material.Material; import net.sf.openrocket.material.Material.Type; import net.sf.openrocket.motor.Manufacturer; +import net.sf.openrocket.motor.IgnitionEvent; import net.sf.openrocket.motor.Motor; import net.sf.openrocket.motor.MotorConfiguration; import net.sf.openrocket.motor.ThrustCurveMotor; @@ -137,6 +139,45 @@ public class TestRockets { .build(); } + // This function is used for unit, integration tests, DO NOT CHANGE (without updating tests). + private static Motor generateMotor_A10_13mm(){ + return new ThrustCurveMotor.Builder() + .setManufacturer(Manufacturer.getManufacturer("Estes")) + .setDesignation("A10") + .setDescription(" SU Black Powder") + .setCaseInfo("SU 13.0x45.0") + .setMotorType(Motor.Type.SINGLE) + .setStandardDelays(new double[] {0,3}) + .setDiameter(0.013) + .setLength(0.045) + .setTimePoints(new double[] {0.0, 0.026, 0.055, 0.093, 0.124, 0.146, 0.166, 0.179, 0.194, 0.203, 0.209, 0.225, 0.26, 0.333, 0.456, 0.575, 0.663, 0.76, 0.811, 0.828, 0.85}) + . setThrustPoints(new double[] {0.0, 0.478, 1.919, 4.513, 8.165, 10.956, 12.64, 11.046, 7.966, 6.042, 3.154, 1.421, 1.225, 1.41, 1.206, 1.195, 1.282, 1.273, 1.268, 0.689, 0.0}) + .setCGPoints(new Coordinate[] { + new Coordinate(0.0225, 0, 0, 3.8), + new Coordinate(0.0225, 0, 0, 3.78818), + new Coordinate(0.0225, 0, 0, 3.72207), + new Coordinate(0.0225, 0, 0, 3.48963), + new Coordinate(0.0225, 0, 0, 3.11587), + new Coordinate(0.0225, 0, 0, 2.71582), + new Coordinate(0.0225, 0, 0, 2.26703), + new Coordinate(0.0225, 0, 0, 1.97419), + new Coordinate(0.0225, 0, 0, 1.70299), + new Coordinate(0.0225, 0, 0, 1.58309), + new Coordinate(0.0225, 0, 0, 1.53062), + new Coordinate(0.0225, 0, 0, 1.46101), + new Coordinate(0.0225, 0, 0, 1.37293), + new Coordinate(0.0225, 0, 0, 1.19), + new Coordinate(0.0225, 0, 0, 0.884002), + new Coordinate(0.0225, 0, 0, 0.612283), + new Coordinate(0.0225, 0, 0, 0.404987), + new Coordinate(0.0225, 0, 0, 0.169296), + new Coordinate(0.0225, 0, 0, 0.0460542), + new Coordinate(0.0225, 0, 0, 0.0144153), + new Coordinate(0.0225, 0, 0, 0.0)}) + .setDigest("digest A10 test") + .build(); + } + // This function is used for unit, integration tests, DO NOT CHANGE (without updating tests). private static Motor generateMotor_B4_18mm(){ return new ThrustCurveMotor.Builder() @@ -1131,7 +1172,123 @@ public class TestRockets { rocket.getChild(1).getChild(0).addChild(finSet); } + + // This is a rocket with two axial stages and side boosters for multi-stage event tests. + // It's like a Falcon 9 Heavy (see above), but vastly simplified so it can be checked by hand + // if needed, and (more importantly) aerodynamically stable. It lacks a lot of the internal + // structure that's required by a real rocket + public static Rocket makeMultiStageEventTestRocket() { + + Rocket rocket = new Rocket(); + + FlightConfigurationId selFCID = rocket.createFlightConfiguration(new FlightConfigurationId()).getFlightConfigurationID(); + + final double THICKNESS = 0.002; + final double CORE_RADIUS = 0.01; + final int NUM_FINS = 4; + final double ROOT_CHORD = 0.04; + final double TIP_CHORD = 0.02; + final double SWEEP = 0.015; + final double HEIGHT = 0.03; + // Sustainer + AxialStage sustainer = new AxialStage(); + sustainer.setName("Sustainer"); + rocket.addChild(sustainer); + { + final double NC_LENGTH = 0.1; + NoseCone noseCone = new NoseCone(Transition.Shape.OGIVE, NC_LENGTH, CORE_RADIUS); + noseCone.setName("Sustainer Nose Cone"); + sustainer.addChild(noseCone); + + final double BT_LENGTH = 0.2; + BodyTube bodyTube = new BodyTube(BT_LENGTH, CORE_RADIUS, THICKNESS); + bodyTube.setName("Sustainer Body Tube"); + bodyTube.setMotorMount(true); + sustainer.addChild(bodyTube); + + MotorConfiguration motorConfig = new MotorConfiguration(bodyTube, selFCID); + motorConfig.setMotor(TestRockets.generateMotor_C6_18mm()); + motorConfig.setEjectionDelay(5.0); + motorConfig.setIgnitionEvent(IgnitionEvent.BURNOUT); + bodyTube.setMotorConfig(motorConfig, selFCID); + + final double CHUTE_DIAM = 0.3; + Parachute parachute = new Parachute(); + parachute.setName("Sustainer Parachute"); + parachute.setDiameter(CHUTE_DIAM); + bodyTube.addChild(parachute); + + final double POSITION = 0.0; + TrapezoidFinSet finSet = new TrapezoidFinSet(NUM_FINS, ROOT_CHORD, TIP_CHORD, SWEEP, HEIGHT); + finSet.setName("Sustainer Fin Set"); + finSet.setAxialMethod(AxialMethod.BOTTOM); + finSet.setAxialOffset(POSITION); + bodyTube.addChild(finSet); + } + + // Center Booster + AxialStage centerBooster = new AxialStage(); + centerBooster.setName("Center Booster"); + rocket.addChild(centerBooster); + { + final double BT_LENGTH = 0.09; + + BodyTube bodyTube = new BodyTube(BT_LENGTH, CORE_RADIUS, THICKNESS); + bodyTube.setName("Center Booster Body Tube"); + bodyTube.setMotorMount(true); + centerBooster.addChild(bodyTube); + + MotorConfiguration motorConfig = new MotorConfiguration(bodyTube, selFCID); + motorConfig.setMotor(TestRockets.generateMotor_C6_18mm()); + motorConfig.setEjectionDelay(0.0); + motorConfig.setIgnitionEvent(IgnitionEvent.LAUNCH); + motorConfig.setIgnitionDelay(0.01); + bodyTube.setMotorConfig(motorConfig, selFCID); + + final double POSITION = -0.015; + TrapezoidFinSet finSet = new TrapezoidFinSet(NUM_FINS, ROOT_CHORD, TIP_CHORD, SWEEP, HEIGHT); + finSet.setName("Center Booster Fin Set"); + finSet.setAxialMethod(AxialMethod.BOTTOM); + finSet.setAxialOffset(POSITION); + bodyTube.addChild(finSet); + } + + // Side Boosters + final double SIDE_RADIUS = 0.007; + ParallelStage sideBoosters = new ParallelStage(2); + sideBoosters.setName("Side boosters"); + rocket.getChild(1).getChild(0).addChild(sideBoosters); + sideBoosters.setAngleOffset(Math.PI/4); + { + final double NC_LENGTH = 0.05; + NoseCone noseCone = new NoseCone(Transition.Shape.OGIVE, NC_LENGTH, SIDE_RADIUS); + noseCone.setName("Side Booster Nose Cones"); + sideBoosters.addChild(noseCone); + + final double BT_LENGTH = 0.09; + BodyTube bodyTube = new BodyTube(BT_LENGTH, SIDE_RADIUS, THICKNESS); + bodyTube.setName("Side Booster Body Tubes"); + bodyTube.setMotorMount(true); + sideBoosters.addChild(bodyTube); + + MotorConfiguration motorConfig = new MotorConfiguration(bodyTube, selFCID); + motorConfig.setMotor(TestRockets.generateMotor_A10_13mm()); + motorConfig.setEjectionDelay(0.0); + motorConfig.setIgnitionEvent(IgnitionEvent.LAUNCH); + bodyTube.setMotorConfig(motorConfig, selFCID); + + StageSeparationConfiguration stageSepConfig = new StageSeparationConfiguration(); + stageSepConfig.setSeparationEvent(StageSeparationConfiguration.SeparationEvent.BURNOUT); + sideBoosters.getSeparationConfigurations().set(selFCID, stageSepConfig); + } + + rocket.enableEvents(); + rocket.setSelectedConfiguration(selFCID); + + return rocket; + } + // This is a simple four-fin rocket with large endplates on the // fins, for testing CG and CP calculations with fins on pods. // not a complete rocket (no motor mount nor recovery system) diff --git a/core/test/net/sf/openrocket/simulation/FlightEventsTest.java b/core/test/net/sf/openrocket/simulation/FlightEventsTest.java index 5e097d5d8..04618655c 100644 --- a/core/test/net/sf/openrocket/simulation/FlightEventsTest.java +++ b/core/test/net/sf/openrocket/simulation/FlightEventsTest.java @@ -68,8 +68,7 @@ public class FlightEventsTest extends BaseTestCase { */ @Test public void testMultiStage() throws SimulationException { - final Rocket rocket = TestRockets.makeFalcon9Heavy(); - TestRockets.addCoreFins(rocket); + final Rocket rocket = TestRockets.makeMultiStageEventTestRocket(); final Simulation sim = new Simulation(rocket); sim.getOptions().setISAAtmosphere(true); @@ -80,65 +79,85 @@ public class FlightEventsTest extends BaseTestCase { sim.simulate(); + /* + for (int b = 0; b < sim.getSimulatedData().getBranchCount(); b++) { + System.out.println("branch " + b); + for (FlightEvent event : sim.getSimulatedData().getBranch(b).getEvents()) { + System.out.println(" " + event); + } + } + */ + // Test branch count final int branchCount = sim.getSimulatedData().getBranchCount(); assertEquals(" Multi-stage simulation invalid branch count ", 3, branchCount); - final AxialStage coreStage = rocket.getStage(1); - final ParallelStage boosterStage = (ParallelStage) rocket.getStage(2); - final InnerTube boosterMotorTubes = (InnerTube) boosterStage.getChild(1).getChild(0); - final BodyTube coreBody = (BodyTube) coreStage.getChild(0); - + final AxialStage sustainer = rocket.getStage(0); + final BodyTube sustainerBody = (BodyTube) sustainer.getChild(1); + final Parachute sustainerChute = (Parachute) sustainerBody.getChild(0); + + final AxialStage centerBooster = rocket.getStage(1); + final BodyTube centerBoosterBody = (BodyTube) centerBooster.getChild(0); + + final ParallelStage sideBoosters = (ParallelStage) centerBoosterBody.getChild(1); + final BodyTube sideBoosterBodies = (BodyTube) sideBoosters.getChild(1); + // events whose time is too variable to check are given a time of 1200 - for (int b = 0; b < 3; b++) { + for (int b = 0; b < 2; b++) { FlightEvent[] expectedEvents; switch (b) { - // Sustainer (payload fairing stage) + // Sustainer case 0: expectedEvents = new FlightEvent[] { new FlightEvent(FlightEvent.Type.LAUNCH, 0.0, rocket), - new FlightEvent(FlightEvent.Type.IGNITION, 0.0, boosterMotorTubes), - new FlightEvent(FlightEvent.Type.IGNITION, 0.0, coreBody), - new FlightEvent(FlightEvent.Type.LIFTOFF, 0.1275, null), - new FlightEvent(FlightEvent.Type.LAUNCHROD, 0.130, null), - new FlightEvent(FlightEvent.Type.BURNOUT, 2.0, boosterMotorTubes), - new FlightEvent(FlightEvent.Type.EJECTION_CHARGE, 2.0, boosterStage), - new FlightEvent(FlightEvent.Type.STAGE_SEPARATION, 2.0, boosterStage), - new FlightEvent(FlightEvent.Type.BURNOUT, 2.0, coreBody), - new FlightEvent(FlightEvent.Type.EJECTION_CHARGE, 2.0, coreStage), - new FlightEvent(FlightEvent.Type.STAGE_SEPARATION, 2.0, coreStage), - new FlightEvent(FlightEvent.Type.APOGEE, 2.12, rocket), - new FlightEvent(FlightEvent.Type.TUMBLE, 2.202, null), - new FlightEvent(FlightEvent.Type.GROUND_HIT, 14.12, null), - new FlightEvent(FlightEvent.Type.SIMULATION_END, 14.12, null) + new FlightEvent(FlightEvent.Type.IGNITION, 0.0, sideBoosterBodies), + new FlightEvent(FlightEvent.Type.IGNITION, 0.01, centerBoosterBody), + new FlightEvent(FlightEvent.Type.LIFTOFF, 0.8255, null), + new FlightEvent(FlightEvent.Type.LAUNCHROD, 0.828, null), + new FlightEvent(FlightEvent.Type.BURNOUT, 0.85, sideBoosterBodies), + new FlightEvent(FlightEvent.Type.EJECTION_CHARGE, 0.85, sideBoosters), + new FlightEvent(FlightEvent.Type.STAGE_SEPARATION, 0.85, sideBoosters), + new FlightEvent(FlightEvent.Type.BURNOUT, 2.01, centerBoosterBody), + new FlightEvent(FlightEvent.Type.EJECTION_CHARGE, 2.01, centerBooster), + new FlightEvent(FlightEvent.Type.STAGE_SEPARATION, 2.01, centerBooster), + new FlightEvent(FlightEvent.Type.IGNITION, 2.01, sustainerBody), + new FlightEvent(FlightEvent.Type.BURNOUT, 4.01, sustainerBody), + new FlightEvent(FlightEvent.Type.APOGEE, 8.5, rocket), + new FlightEvent(FlightEvent.Type.EJECTION_CHARGE, 9.01, sustainer), + new FlightEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, 9.01, sustainerChute), + new FlightEvent(FlightEvent.Type.GROUND_HIT, 83.27, null), + new FlightEvent(FlightEvent.Type.SIMULATION_END, 83.27, null) }; break; - // Core stage + + // Center Booster case 1: expectedEvents = new FlightEvent[] { - new FlightEvent(FlightEvent.Type.IGNITION, 0.0, coreBody), - new FlightEvent(FlightEvent.Type.BURNOUT, 2.0, coreBody), - new FlightEvent(FlightEvent.Type.EJECTION_CHARGE, 2.0, coreStage), - new FlightEvent(FlightEvent.Type.STAGE_SEPARATION, 2.0, coreStage), - new FlightEvent(FlightEvent.Type.APOGEE, 2.1, rocket), - new FlightEvent(FlightEvent.Type.TUMBLE, 2.15, null), - new FlightEvent(FlightEvent.Type.GROUND_HIT, 7.26, null), - new FlightEvent(FlightEvent.Type.SIMULATION_END, 7.26, null) - }; - break; - // Booster stage - case 2: - expectedEvents = new FlightEvent[] { - new FlightEvent(FlightEvent.Type.IGNITION, 0.0, boosterMotorTubes), - new FlightEvent(FlightEvent.Type.BURNOUT, 2.0, boosterMotorTubes), - new FlightEvent(FlightEvent.Type.EJECTION_CHARGE, 2.0, boosterStage), - new FlightEvent(FlightEvent.Type.STAGE_SEPARATION, 2.0, boosterStage), - new FlightEvent(FlightEvent.Type.TUMBLE, 2.05, null), - new FlightEvent(FlightEvent.Type.APOGEE, 2.06, rocket), - new FlightEvent(FlightEvent.Type.GROUND_HIT, 12.0, null), - new FlightEvent(FlightEvent.Type.SIMULATION_END, 12.0, null) + new FlightEvent(FlightEvent.Type.IGNITION, 0.01, centerBoosterBody), + new FlightEvent(FlightEvent.Type.BURNOUT, 2.01, centerBoosterBody), + new FlightEvent(FlightEvent.Type.EJECTION_CHARGE, 2.01, centerBooster), + new FlightEvent(FlightEvent.Type.STAGE_SEPARATION, 2.01, centerBooster), + new FlightEvent(FlightEvent.Type.TUMBLE, 2.85, null), + new FlightEvent(FlightEvent.Type.APOGEE, 3.87, rocket), + new FlightEvent(FlightEvent.Type.GROUND_HIT, 9.0, null), + new FlightEvent(FlightEvent.Type.SIMULATION_END, 9.0, null) }; break; + + // Side Boosters + case 2: + expectedEvents = new FlightEvent[] { + new FlightEvent(FlightEvent.Type.IGNITION, 0.0, sideBoosterBodies), + new FlightEvent(FlightEvent.Type.BURNOUT, 0.85, sideBoosterBodies), + new FlightEvent(FlightEvent.Type.EJECTION_CHARGE, 0.85, sideBoosters), + new FlightEvent(FlightEvent.Type.STAGE_SEPARATION, 0.85, sideBoosters), + new FlightEvent(FlightEvent.Type.TUMBLE, 1.0, null), + new FlightEvent(FlightEvent.Type.APOGEE, 1.01, rocket), + new FlightEvent(FlightEvent.Type.GROUND_HIT, 1.21, null), + new FlightEvent(FlightEvent.Type.SIMULATION_END, 1.21, null) + }; + break; + default: throw new IllegalStateException("Invalid branch number " + b); } From bbc31fc266bca0e35a63ebe7c830aacb782546e2 Mon Sep 17 00:00:00 2001 From: JoePfeiffer Date: Tue, 13 Feb 2024 12:34:57 -0700 Subject: [PATCH 3/3] There's more variation in the apogee and ground hit times than I realized for the tumbling center booster --- .../test/net/sf/openrocket/simulation/FlightEventsTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/test/net/sf/openrocket/simulation/FlightEventsTest.java b/core/test/net/sf/openrocket/simulation/FlightEventsTest.java index 04618655c..4a6818bc4 100644 --- a/core/test/net/sf/openrocket/simulation/FlightEventsTest.java +++ b/core/test/net/sf/openrocket/simulation/FlightEventsTest.java @@ -138,9 +138,9 @@ public class FlightEventsTest extends BaseTestCase { new FlightEvent(FlightEvent.Type.EJECTION_CHARGE, 2.01, centerBooster), new FlightEvent(FlightEvent.Type.STAGE_SEPARATION, 2.01, centerBooster), new FlightEvent(FlightEvent.Type.TUMBLE, 2.85, null), - new FlightEvent(FlightEvent.Type.APOGEE, 3.87, rocket), - new FlightEvent(FlightEvent.Type.GROUND_HIT, 9.0, null), - new FlightEvent(FlightEvent.Type.SIMULATION_END, 9.0, null) + new FlightEvent(FlightEvent.Type.APOGEE, 1200, rocket), + new FlightEvent(FlightEvent.Type.GROUND_HIT, 1200, null), + new FlightEvent(FlightEvent.Type.SIMULATION_END, 1200, null) }; break;