diff --git a/core/resources/l10n/messages.properties b/core/resources/l10n/messages.properties index d1db4362c..cdc770065 100644 --- a/core/resources/l10n/messages.properties +++ b/core/resources/l10n/messages.properties @@ -574,9 +574,11 @@ SimuRunDlg.lbl.Altitude = Altitude: SimuRunDlg.lbl.Velocity = Velocity: SimuRunDlg.msg.Unabletosim = Unable to simulate: SimuRunDlg.msg.errorOccurred = An error occurred during the simulation: +SimuRunDlg.msg.branchErrorOccurred = An error occurred during simulation branch BasicEventSimulationEngine.error.noMotorsDefined = No motors defined in the simulation. -BasicEventSimulationEngine.error.cantCalculateStability = Can't calculate rocket stability. +BasicEventSimulationEngine.error.activeLengthZero = Active airframe has length 0 +BasicEventSimulationEngine.error.cantCalculateStability = Can't calculate stability BasicEventSimulationEngine.error.earlyMotorBurnout = Motor burnout without liftoff. BasicEventSimulationEngine.error.noConfiguredIgnition = No motors configured to ignite at liftoff BasicEventSimulationEngine.error.noIgnition = No motors ignited. diff --git a/core/resources/pix/eventicons/event-exception.png b/core/resources/pix/eventicons/event-exception.png new file mode 100644 index 000000000..0cfd58596 Binary files /dev/null and b/core/resources/pix/eventicons/event-exception.png differ diff --git a/core/src/net/sf/openrocket/simulation/BasicEventSimulationEngine.java b/core/src/net/sf/openrocket/simulation/BasicEventSimulationEngine.java index c903addbf..91c76e596 100644 --- a/core/src/net/sf/openrocket/simulation/BasicEventSimulationEngine.java +++ b/core/src/net/sf/openrocket/simulation/BasicEventSimulationEngine.java @@ -86,13 +86,6 @@ public class BasicEventSimulationEngine implements SimulationEngine { throw new MotorIgnitionException(trans.get("BasicEventSimulationEngine.error.noMotorsDefined")); } - // Can't calculate stability - if (currentStatus.getSimulationConditions().getAerodynamicCalculator() - .getCP(currentStatus.getConfiguration(), - new FlightConditions(currentStatus.getConfiguration()), - new WarningSet()).weight < MathUtil.EPSILON) - throw new SimulationException(trans.get("BasicEventSimulationEngine.error.cantCalculateStability")); - // Problems that let us simulate, but result is likely bad // No recovery device @@ -157,6 +150,8 @@ public class BasicEventSimulationEngine implements SimulationEngine { Coordinate originVelocity = currentStatus.getRocketVelocity(); try { + + checkGeometry(currentStatus); // Start the simulation while (handleEvents()) { @@ -284,6 +279,7 @@ public class BasicEventSimulationEngine implements SimulationEngine { flightData.getWarningSet().addAll(currentStatus.getWarnings()); e.setFlightData(flightData); + e.setFlightDataBranch(currentStatus.getFlightData()); throw e; } @@ -495,7 +491,8 @@ public class BasicEventSimulationEngine implements SimulationEngine { SimulationStatus boosterStatus = new SimulationStatus(currentStatus); // Prepare the new simulation branch - boosterStatus.setFlightData(new FlightDataBranch(boosterStage.getName(), FlightDataType.TYPE_TIME)); + boosterStatus.setFlightData(new FlightDataBranch(boosterStage.getName(), currentStatus.getFlightData())); + boosterStatus.getFlightData().addEvent(event); // Mark the current status as having dropped the current stage and all stages below it currentStatus.getConfiguration().clearStagesBelow( stageNumber); @@ -504,6 +501,10 @@ public class BasicEventSimulationEngine implements SimulationEngine { boosterStatus.getConfiguration().clearStagesAbove(stageNumber); toSimulate.push(boosterStatus); + + // Make sure upper stages can still be simulated + checkGeometry(currentStatus); + log.info(String.format("==>> @ %g; from Branch: %s ---- Branching: %s ---- \n", currentStatus.getSimulationTime(), currentStatus.getFlightData().getBranchName(), boosterStatus.getFlightData().getBranchName())); @@ -665,8 +666,24 @@ public class BasicEventSimulationEngine implements SimulationEngine { return null; } } - - + + // we need to check geometry to make sure we can simulation the active + // stages in a simulation branch when the branch starts executing, and + // whenever a stage separation occurs + private void checkGeometry(SimulationStatus currentStatus) throws SimulationException { + + // Active stages have total length of 0. + if (currentStatus.getConfiguration().getLengthAerodynamic() < MathUtil.EPSILON) { + throw new SimulationException(trans.get("BasicEventSimulationEngine.error.activeLengthZero")); + } + + // Can't calculate stability + if (currentStatus.getSimulationConditions().getAerodynamicCalculator() + .getCP(currentStatus.getConfiguration(), + new FlightConditions(currentStatus.getConfiguration()), + new WarningSet()).weight < MathUtil.EPSILON) + throw new SimulationException(trans.get("BasicEventSimulationEngine.error.cantCalculateStability")); + } private void checkNaN() throws SimulationException { double d = 0; diff --git a/core/src/net/sf/openrocket/simulation/FlightDataBranch.java b/core/src/net/sf/openrocket/simulation/FlightDataBranch.java index c7f71f7fc..77bc60f56 100644 --- a/core/src/net/sf/openrocket/simulation/FlightDataBranch.java +++ b/core/src/net/sf/openrocket/simulation/FlightDataBranch.java @@ -75,6 +75,26 @@ public class FlightDataBranch implements Monitorable { maxValues.put(t, Double.NaN); } } + + /** + * Make a flight data branch with one data point copied from its parent. Intended for use + * when creating a new branch upon stage separation, so the data at separation is present + * in both branches (and if the new branch has an immediate exception, it can be plotted) + */ + public FlightDataBranch(String branchName, FlightDataBranch parent) { + this.branchName = branchName; + + // need to have at least one type to set up values + values.put(FlightDataType.TYPE_TIME, new ArrayList()); + minValues.put(FlightDataType.TYPE_TIME, Double.NaN); + maxValues.put(FlightDataType.TYPE_TIME, Double.NaN); + + // copy all values into new FlightDataBranch + this.addPoint(); + for (FlightDataType t : parent.getTypes()) { + this.setValue(t, parent.getLast(t)); + } + } /** * Makes an 'empty' flight data branch which has no data but all built in data types are defined. diff --git a/core/src/net/sf/openrocket/simulation/exception/SimulationException.java b/core/src/net/sf/openrocket/simulation/exception/SimulationException.java index e0e8ee8ec..720eadce0 100644 --- a/core/src/net/sf/openrocket/simulation/exception/SimulationException.java +++ b/core/src/net/sf/openrocket/simulation/exception/SimulationException.java @@ -1,10 +1,12 @@ package net.sf.openrocket.simulation.exception; import net.sf.openrocket.simulation.FlightData; +import net.sf.openrocket.simulation.FlightDataBranch; public class SimulationException extends Exception { private FlightData flightData = null; + private FlightDataBranch flightDataBranch = null; public SimulationException() { @@ -29,4 +31,13 @@ public class SimulationException extends Exception { public FlightData getFlightData() { return flightData; } + + public void setFlightDataBranch(FlightDataBranch f) { + flightDataBranch = f; + } + + public FlightDataBranch getFlightDataBranch() { + return flightDataBranch; + } + } diff --git a/core/src/net/sf/openrocket/util/TestRockets.java b/core/src/net/sf/openrocket/util/TestRockets.java index 2471997b2..ea2fc8a8d 100644 --- a/core/src/net/sf/openrocket/util/TestRockets.java +++ b/core/src/net/sf/openrocket/util/TestRockets.java @@ -1104,6 +1104,26 @@ public class TestRockets { return rocket; } + // Several simulations need the Falcon9Heavy, but with fins added to the + // core stage (without them, there is a simulation exception at stage separation + // This method is intended to add those fins to the F9H, but will in fact + // add them to the second stage of a rocket + public static void addCoreFins(Rocket rocket) { + final int bodyFinCount = 4; + final double bodyFinRootChord = 0.05; + final double bodyFinTipChord = bodyFinRootChord; + final double bodyFinHeight = 0.025; + final double bodyFinSweep = 0.0; + final AxialMethod bodyFinAxialMethod = AxialMethod.BOTTOM; + final double bodyFinAxialOffset = 0.0; + + final TrapezoidFinSet finSet = new TrapezoidFinSet(bodyFinCount, bodyFinRootChord, bodyFinTipChord, bodyFinSweep, bodyFinHeight); + finSet.setName("Body Tube FinSet"); + finSet.setAxialMethod(bodyFinAxialMethod); + + rocket.getChild(1).getChild(0).addChild(finSet); + } + // 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/DisableStageTest.java b/core/test/net/sf/openrocket/simulation/DisableStageTest.java index f92c634ea..42b0f77b7 100644 --- a/core/test/net/sf/openrocket/simulation/DisableStageTest.java +++ b/core/test/net/sf/openrocket/simulation/DisableStageTest.java @@ -148,9 +148,13 @@ public class DisableStageTest extends BaseTestCase { @Test public void testBooster1() { //// Test disabling the stage - Rocket rocketRemoved = TestRockets.makeFalcon9Heavy(); // Rocket with the last stage removed - Rocket rocketDisabled = TestRockets.makeFalcon9Heavy(); // Rocket with the last stage disabled + Rocket rocketRemoved = TestRockets.makeFalcon9Heavy(); // Rocket with the last stage removed + TestRockets.addCoreFins(rocketRemoved); + + Rocket rocketDisabled = TestRockets.makeFalcon9Heavy(); // Rocket with the last stage disabled + TestRockets.addCoreFins(rocketDisabled); + FlightConfigurationId fcid = new FlightConfigurationId(TestRockets.FALCON_9H_FCID_1); int stageNr = 2; // Stage 2 is the Parallel Booster Stage rocketRemoved.getChild(1).getChild(0).removeChild(0); // Remove the Parallel Booster Stage @@ -171,6 +175,8 @@ public class DisableStageTest extends BaseTestCase { //// Test re-enableing the stage. Rocket rocketOriginal = TestRockets.makeFalcon9Heavy(); + TestRockets.addCoreFins(rocketOriginal); + Simulation simOriginal = new Simulation(rocketOriginal); simOriginal.setFlightConfigurationId(fcid); simOriginal.getOptions().setISAAtmosphere(true); @@ -188,7 +194,10 @@ public class DisableStageTest extends BaseTestCase { public void testBooster2() { //// Test disabling the stage Rocket rocketRemoved = TestRockets.makeFalcon9Heavy(); // Rocket with the last stage removed + TestRockets.addCoreFins(rocketRemoved); + Rocket rocketDisabled = TestRockets.makeFalcon9Heavy(); // Rocket with the last stage disabled + TestRockets.addCoreFins(rocketDisabled); FlightConfigurationId fid = new FlightConfigurationId(TestRockets.FALCON_9H_FCID_1); int stageNr = 1; // Stage 1 is the Parallel Booster Stage's parent stage @@ -225,6 +234,8 @@ public class DisableStageTest extends BaseTestCase { //// Test re-enableing the stage. Rocket rocketOriginal = TestRockets.makeFalcon9Heavy(); + TestRockets.addCoreFins(rocketOriginal); + Simulation simOriginal = new Simulation(rocketOriginal); simOriginal.setFlightConfigurationId(fid); simOriginal.getOptions().setISAAtmosphere(true); diff --git a/core/test/net/sf/openrocket/simulation/FlightEventsTest.java b/core/test/net/sf/openrocket/simulation/FlightEventsTest.java index 74f573b98..ddb6993ca 100644 --- a/core/test/net/sf/openrocket/simulation/FlightEventsTest.java +++ b/core/test/net/sf/openrocket/simulation/FlightEventsTest.java @@ -85,6 +85,8 @@ public class FlightEventsTest extends BaseTestCase { @Test public void testMultiStage() throws SimulationException { final Rocket rocket = TestRockets.makeFalcon9Heavy(); + TestRockets.addCoreFins(rocket); + final Simulation sim = new Simulation(rocket); sim.getOptions().setISAAtmosphere(true); sim.getOptions().setTimeStep(0.05); @@ -98,32 +100,49 @@ public class FlightEventsTest extends BaseTestCase { 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); + + // events whose time is too variable to check are given a time of 1200 for (int b = 0; b < 3; b++) { - final FlightEvent.Type[] expectedEventTypes; - final double[] expectedEventTimes; + FlightEvent[] expectedEvents; final RocketComponent[] expectedSources; switch (b) { case 0: - expectedEventTypes = new FlightEvent.Type[]{FlightEvent.Type.LAUNCH, FlightEvent.Type.IGNITION, FlightEvent.Type.IGNITION, - FlightEvent.Type.LIFTOFF, FlightEvent.Type.LAUNCHROD, FlightEvent.Type.APOGEE, - FlightEvent.Type.BURNOUT, FlightEvent.Type.EJECTION_CHARGE, FlightEvent.Type.STAGE_SEPARATION, - FlightEvent.Type.BURNOUT, FlightEvent.Type.EJECTION_CHARGE, FlightEvent.Type.STAGE_SEPARATION, - FlightEvent.Type.TUMBLE, FlightEvent.Type.GROUND_HIT, FlightEvent.Type.SIMULATION_END}; - expectedEventTimes = new double[]{0.0, 0.0, 0.0, 0.1225, 0.125, 1.735, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0}; // Tumble and ground hit time are too variable, so don't include it - 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); - expectedSources = new RocketComponent[]{rocket, boosterMotorTubes, coreBody, null, null, rocket, - boosterMotorTubes, boosterStage, boosterStage, coreBody, coreStage, coreStage, - null, null, null}; + 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.1225, null), + new FlightEvent(FlightEvent.Type.LAUNCHROD, 0.125, null), + new FlightEvent(FlightEvent.Type.APOGEE, 1.86, rocket), + 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.TUMBLE, 2.4127, null), + new FlightEvent(FlightEvent.Type.GROUND_HIT, 1200, null), + new FlightEvent(FlightEvent.Type.SIMULATION_END, 1200, null) + }; break; case 1: + expectedEvents = new FlightEvent[] { + new FlightEvent(FlightEvent.Type.STAGE_SEPARATION, 2.0, coreStage), + new FlightEvent(FlightEvent.Type.GROUND_HIT, 1200, null), + new FlightEvent(FlightEvent.Type.SIMULATION_END, 1200, null) + }; + break; case 2: - expectedEventTypes = new FlightEvent.Type[]{FlightEvent.Type.TUMBLE, FlightEvent.Type.GROUND_HIT, - FlightEvent.Type.SIMULATION_END}; - expectedEventTimes = new double[]{}; // Tumble and ground hit time are too variable, so don't include it - expectedSources = new RocketComponent[]{null, null, null}; + expectedEvents = new FlightEvent[] { + new FlightEvent(FlightEvent.Type.STAGE_SEPARATION, 2.0, boosterStage), + new FlightEvent(FlightEvent.Type.TUMBLE, 3.551, null), + new FlightEvent(FlightEvent.Type.GROUND_HIT, 1200, null), + new FlightEvent(FlightEvent.Type.SIMULATION_END, 1200, null) + }; break; default: throw new IllegalStateException("Invalid branch number " + b); @@ -131,28 +150,28 @@ public class FlightEventsTest extends BaseTestCase { // Test event count final FlightDataBranch branch = sim.getSimulatedData().getBranch(b); - final List eventList = branch.getEvents(); - final List eventTypes = eventList.stream().map(FlightEvent::getType).collect(Collectors.toList()); - assertEquals(" Multi-stage simulation, branch " + b + " invalid number of events", expectedEventTypes.length, eventTypes.size()); + final FlightEvent[] events = (FlightEvent[]) branch.getEvents().toArray(new FlightEvent[0]); + for (int i = 0; i < events.length; i++) { + System.out.println("branch " + b + " index " + i + " event " + events[i]); + } + assertEquals(" Multi-stage simulation, branch " + b + " invalid number of events", expectedEvents.length, events.length); - // Test that all expected events are present, and in the right order - for (int i = 0; i < expectedEventTypes.length; i++) { - assertSame(" Flight type " + expectedEventTypes[i] + ", branch " + b + " not found in multi-stage simulation", - expectedEventTypes[i], eventTypes.get(i)); - } + // Test that all expected events are present, in the right order, at the right time, from the right sources + for (int i = 0; i < events.length; i++) { + final FlightEvent expected = expectedEvents[i]; + final FlightEvent actual = events[i]; + assertSame("Branch " + b + " FlightEvent " + i + " type " + expected.getType() + " not found; FlightEvent " + actual.getType() + " found instead", + expected.getType(), actual.getType()); - // Test that the event times are correct - for (int i = 0; i < expectedEventTimes.length; i++) { - assertEquals(" Flight type " + expectedEventTypes[i] + " has wrong time", - expectedEventTimes[i], eventList.get(i).getTime(), EPSILON); - } + if (1200 != expected.getTime()) { + assertEquals("Branch " + b + " FlightEvent " + i + " type " + expected.getType() + " has wrong time", + expected.getTime(), actual.getTime(), EPSILON); + } - // Test that the event sources are correct - for (int i = 0; i < expectedSources.length; i++) { - assertEquals(" Flight type " + expectedEventTypes[i] + " has wrong source", - expectedSources[i], eventList.get(i).getSource()); + // Test that the event sources are correct + assertEquals("Branch " + b + " FlightEvent " + i + " type " + expected.getType() + " has wrong source", + expected.getSource(), actual.getSource()); } } } - } diff --git a/swing/src/net/sf/openrocket/gui/plot/EventGraphics.java b/swing/src/net/sf/openrocket/gui/plot/EventGraphics.java index 3443e9be5..6dd647b2b 100644 --- a/swing/src/net/sf/openrocket/gui/plot/EventGraphics.java +++ b/swing/src/net/sf/openrocket/gui/plot/EventGraphics.java @@ -57,6 +57,7 @@ public class EventGraphics { "pix/eventicons/event-recovery-device-deployment.png"); loadImage(FlightEvent.Type.GROUND_HIT, "pix/eventicons/event-ground-hit.png"); loadImage(FlightEvent.Type.SIMULATION_END, "pix/eventicons/event-simulation-end.png"); + loadImage(FlightEvent.Type.EXCEPTION, "pix/eventicons/event-exception.png"); } private static void loadImage(FlightEvent.Type type, String file) { diff --git a/swing/src/net/sf/openrocket/gui/simulation/SimulationRunDialog.java b/swing/src/net/sf/openrocket/gui/simulation/SimulationRunDialog.java index 63ac79c29..2ff3c7101 100644 --- a/swing/src/net/sf/openrocket/gui/simulation/SimulationRunDialog.java +++ b/swing/src/net/sf/openrocket/gui/simulation/SimulationRunDialog.java @@ -38,6 +38,7 @@ import net.sf.openrocket.l10n.Translator; import net.sf.openrocket.motor.IgnitionEvent; import net.sf.openrocket.motor.MotorConfiguration; import net.sf.openrocket.rocketcomponent.FlightConfiguration; +import net.sf.openrocket.simulation.FlightDataBranch; import net.sf.openrocket.simulation.FlightEvent; import net.sf.openrocket.simulation.SimulationStatus; import net.sf.openrocket.simulation.customexpression.CustomExpression; @@ -433,12 +434,20 @@ public class SimulationRunDialog extends JDialog { null, simulation.getName(), JOptionPane.ERROR_MESSAGE); } else if (t instanceof SimulationException) { + String title = simulation.getName(); + FlightDataBranch dataBranch = ((SimulationException) t).getFlightDataBranch(); + String message; + if (dataBranch != null) { + message = trans.get("SimuRunDlg.msg.branchErrorOccurred") + "\"" + dataBranch.getBranchName() + "\""; + } else { + message = trans.get("SimuRunDlg.msg.errorOccurred"); + } DetailDialog.showDetailedMessageDialog(SimulationRunDialog.this, new Object[] { //// A error occurred during the simulation: - trans.get("SimuRunDlg.msg.errorOccurred"), t.getMessage() }, - null, simulation.getName(), JOptionPane.ERROR_MESSAGE); + message, t.getMessage() }, + null, simulation.getName(), JOptionPane.ERROR_MESSAGE); } else {