diff --git a/core/src/main/java/info/openrocket/core/aerodynamics/AbstractAerodynamicCalculator.java b/core/src/main/java/info/openrocket/core/aerodynamics/AbstractAerodynamicCalculator.java index 59538069c..e82b982d8 100644 --- a/core/src/main/java/info/openrocket/core/aerodynamics/AbstractAerodynamicCalculator.java +++ b/core/src/main/java/info/openrocket/core/aerodynamics/AbstractAerodynamicCalculator.java @@ -32,6 +32,18 @@ public abstract class AbstractAerodynamicCalculator implements AerodynamicCalcul private ModID rocketAeroModID = new ModID(); private ModID rocketTreeModID = new ModID(); + /** + * Determine whether calculations are suspect because we are stalling + * + * @return whether we are stalling, and the margin + * between our AOA and a stall + * If the return is positive we aren't; + * If it's negative we are. + * + */ + @Override + public abstract double getStallMargin(); + //////////////// Aerodynamic calculators //////////////// @Override diff --git a/core/src/main/java/info/openrocket/core/aerodynamics/AerodynamicCalculator.java b/core/src/main/java/info/openrocket/core/aerodynamics/AerodynamicCalculator.java index 982f248ae..79d6878b9 100644 --- a/core/src/main/java/info/openrocket/core/aerodynamics/AerodynamicCalculator.java +++ b/core/src/main/java/info/openrocket/core/aerodynamics/AerodynamicCalculator.java @@ -15,6 +15,17 @@ import info.openrocket.core.util.Monitorable; */ public interface AerodynamicCalculator extends Monitorable { + /** + * Determine whether calculations are suspect because we are stalling + * + * @return whether we are stalling, and the margin + * between our AOA and a stall + * If the return is positive we aren't; + * If it's negative we are. + * + */ + public double getStallMargin(); + /** * Calculate the CP of the specified configuration. * diff --git a/core/src/main/java/info/openrocket/core/aerodynamics/BarrowmanCalculator.java b/core/src/main/java/info/openrocket/core/aerodynamics/BarrowmanCalculator.java index d4369374c..9f1cb46a4 100644 --- a/core/src/main/java/info/openrocket/core/aerodynamics/BarrowmanCalculator.java +++ b/core/src/main/java/info/openrocket/core/aerodynamics/BarrowmanCalculator.java @@ -56,6 +56,9 @@ public class BarrowmanCalculator extends AbstractAerodynamicCalculator { private double cacheDiameter = -1; private double cacheLength = -1; + private final double stallAngle = 17.5 * Math.PI / 180; + private double stallMargin; + public BarrowmanCalculator() { } @@ -65,7 +68,20 @@ public class BarrowmanCalculator extends AbstractAerodynamicCalculator { public BarrowmanCalculator newInstance() { return new BarrowmanCalculator(); } - + + /** + * Determine whether calculations are suspect because we are stalling + * + * @return whether we are stalling, and the margin + * between our AOA and a stall + * If the return is positive we aren't; + * If it's negative we are. + * + */ + @Override + public double getStallMargin() { + return stallMargin; + } /** * Calculate the CP according to the extended Barrowman method. @@ -215,6 +231,9 @@ public class BarrowmanCalculator extends AbstractAerodynamicCalculator { total.setCm(total.getCm() - total.getPitchDampingMoment()); total.setCyaw(total.getCyaw() - total.getYawDampingMoment()); + // How far are we from stalling? + stallMargin = stallAngle - conditions.getAOA(); + return total; } @@ -258,9 +277,6 @@ public class BarrowmanCalculator extends AbstractAerodynamicCalculator { if (warnings == null) warnings = ignoreWarningSet; - if (conditions.getAOA() > 17.5 * Math.PI / 180) - warnings.add(new Warning.LargeAOA(conditions.getAOA())); - if (calcMap == null) buildCalcMap(configuration); diff --git a/core/src/main/java/info/openrocket/core/logging/Warning.java b/core/src/main/java/info/openrocket/core/logging/Warning.java index 7361f7180..7d5805503 100644 --- a/core/src/main/java/info/openrocket/core/logging/Warning.java +++ b/core/src/main/java/info/openrocket/core/logging/Warning.java @@ -57,7 +57,8 @@ public abstract class Warning extends Message { return trans.get("Warning.LargeAOA.str1"); //// Large angle of attack encountered ( return (trans.get("Warning.LargeAOA.str2") + - UnitGroup.UNITS_ANGLE.getDefaultUnit().toString(aoa) + ")."); + // UnitGroup.UNITS_ANGLE.getDefaultUnit().toString(aoa) + ")."); + UnitGroup.UNITS_ANGLE.toStringUnit(aoa) + ")."); } @Override diff --git a/core/src/main/java/info/openrocket/core/simulation/AbstractSimulationStepper.java b/core/src/main/java/info/openrocket/core/simulation/AbstractSimulationStepper.java index 474d6c474..5875025ae 100644 --- a/core/src/main/java/info/openrocket/core/simulation/AbstractSimulationStepper.java +++ b/core/src/main/java/info/openrocket/core/simulation/AbstractSimulationStepper.java @@ -246,9 +246,6 @@ public abstract class AbstractSimulationStepper implements SimulationStepper { public Coordinate launchRodDirection = null; - public double maxZvelocity = Double.NaN; - public double startWarningTime = Double.NaN; - // set by calculateFlightConditions and calculateAcceleration: public AerodynamicForces forces; public Coordinate windVelocity = new Coordinate(Double.NaN, Double.NaN, Double.NaN); diff --git a/core/src/main/java/info/openrocket/core/simulation/BasicEventSimulationEngine.java b/core/src/main/java/info/openrocket/core/simulation/BasicEventSimulationEngine.java index 97dd2b334..3afa1c157 100644 --- a/core/src/main/java/info/openrocket/core/simulation/BasicEventSimulationEngine.java +++ b/core/src/main/java/info/openrocket/core/simulation/BasicEventSimulationEngine.java @@ -41,10 +41,6 @@ public class BasicEventSimulationEngine implements SimulationEngine { private final SimulationStepper landingStepper = new BasicLandingStepper(); private final SimulationStepper tumbleStepper = new BasicTumbleStepper(); private final SimulationStepper groundStepper = new GroundStepper(); - - // Constant holding 20 degrees in radians. This is the AOA condition - // necessary to transition to tumbling. - private final static double AOA_TUMBLE_CONDITION = Math.PI / 9.0; // The thrust must be below this value for the transition to tumbling. // TODO HIGH: this is an arbitrary value @@ -111,7 +107,7 @@ public class BasicEventSimulationEngine implements SimulationEngine { currentStatus.addWarning(Warning.NO_RECOVERY_DEVICE); } - currentStatus.getEventQueue().add(new FlightEvent(FlightEvent.Type.LAUNCH, 0, simulationConditions.getRocket())); + currentStatus.addEvent(new FlightEvent(FlightEvent.Type.LAUNCH, 0, simulationConditions.getRocket())); toSimulate.push(currentStatus); SimulationListenerHelper.fireStartSimulation(currentStatus); @@ -261,20 +257,32 @@ public class BasicEventSimulationEngine implements SimulationEngine { // } // } - // Check for Tumbling - // Conditions for transition are: - // is not already tumbling - // and not stable (cg > cp) - // and aoa > AOA_TUMBLE_CONDITION threshold - - if (!currentStatus.isTumbling()) { + // Check for fin stall and either set tumbling or LargeAOA warning depending on + // rocket stability margin + // Inhibited if already tumbling, parachutes deployed, or on the ground + if (!currentStatus.isTumbling() && + (currentStatus.getDeployedRecoveryDevices().size() == 0) && + !currentStatus.isLanded()) { final double cp = currentStatus.getFlightDataBranch().getLast(FlightDataType.TYPE_CP_LOCATION); final double cg = currentStatus.getFlightDataBranch().getLast(FlightDataType.TYPE_CG_LOCATION); final double aoa = currentStatus.getFlightDataBranch().getLast(FlightDataType.TYPE_AOA); - - if (cg > cp && aoa > AOA_TUMBLE_CONDITION) { - currentStatus.addEvent(new FlightEvent(FlightEvent.Type.TUMBLE, currentStatus.getSimulationTime())); - } + final double margin = + currentStatus.getSimulationConditions().getAerodynamicCalculator().getStallMargin(); + + // large AOA -- stalling. + if (margin < 0) { + // If we're stable, put a warning about large AOA + // note -- if cp is NaN (which it is while on the rod) cg > cp is false + if (cg > cp) { + // Not stable, so transition to tumbling + currentStatus.addEvent(new FlightEvent(FlightEvent.Type.TUMBLE, currentStatus.getSimulationTime())); + } else { + // Stable, so warning about AOA + if (currentStatus.recordWarnings()) { + currentStatus.addWarning(new Warning.LargeAOA(aoa)); + } + } + } } // If I'm on the ground and have no events in the queue, I'm done @@ -551,6 +559,7 @@ public class BasicEventSimulationEngine implements SimulationEngine { case RECOVERY_DEVICE_DEPLOYMENT: RocketComponent c = event.getSource(); int n = c.getStageNumber(); + // Ignore event if stage not active if (currentStatus.getConfiguration().isStageActive(n)) { // TODO: HIGH: Check stage activeness for other events as well? diff --git a/core/src/main/java/info/openrocket/core/simulation/RK4SimulationStepper.java b/core/src/main/java/info/openrocket/core/simulation/RK4SimulationStepper.java index b37733e48..b3872aab4 100644 --- a/core/src/main/java/info/openrocket/core/simulation/RK4SimulationStepper.java +++ b/core/src/main/java/info/openrocket/core/simulation/RK4SimulationStepper.java @@ -401,22 +401,7 @@ public class RK4SimulationStepper extends AbstractSimulationStepper { * launch rod or 0.25 seconds after departure, and when the velocity has dropped * below 20% of the max. velocity. */ - WarningSet warnings = new WarningSet(); - store.maxZvelocity = MathUtil.max(store.maxZvelocity, status.getRocketVelocity().z); - - if (!status.isLaunchRodCleared()) { - warnings = null; - } else { - if (status.getRocketVelocity().z < 0.2 * store.maxZvelocity) { - warnings = null; - } - if (Double.isNaN(store.startWarningTime)) { - store.startWarningTime = status.getSimulationTime() + 0.25; - } - } - - if (!(status.getSimulationTime() > store.startWarningTime)) - warnings = null; + WarningSet warnings = status.recordWarnings() ? new WarningSet() : null; // Calculate aerodynamic forces store.forces = status.getSimulationConditions().getAerodynamicCalculator() diff --git a/core/src/main/java/info/openrocket/core/simulation/SimulationStatus.java b/core/src/main/java/info/openrocket/core/simulation/SimulationStatus.java index 54c279bfc..597f25f7e 100644 --- a/core/src/main/java/info/openrocket/core/simulation/SimulationStatus.java +++ b/core/src/main/java/info/openrocket/core/simulation/SimulationStatus.java @@ -38,6 +38,13 @@ import org.slf4j.LoggerFactory; public class SimulationStatus implements Cloneable, Monitorable { + // time after leaving launch rod before recording flight event warnings + private final double WARNINGS_WAIT = 0.25; + + // when our z velocity decreases to this proportion of max z velocity, stop recording + // most flight event warnings + private final double WARNINGS_VEL = 0.2; + private static final Logger log = LoggerFactory.getLogger(BasicEventSimulationEngine.class); private SimulationConditions simulationConditions; @@ -54,6 +61,9 @@ public class SimulationStatus implements Cloneable, Monitorable { private Quaternion orientation; private Coordinate rotationVelocity; + private double maxZVelocity = Double.NEGATIVE_INFINITY; + private double startWarningsTime = 1200; + private double effectiveLaunchRodLength; // Set of all motors @@ -189,7 +199,9 @@ public class SimulationStatus implements Cloneable, Monitorable { this.apogeeReached = orig.apogeeReached; this.tumbling = orig.tumbling; this.landed = orig.landed; - + this.maxZVelocity = orig.maxZVelocity; + this.startWarningsTime = orig.startWarningsTime; + this.configuration.copyStages(orig.configuration); this.deployedRecoveryDevices.clear(); @@ -357,6 +369,9 @@ public class SimulationStatus implements Cloneable, Monitorable { public void setLaunchRodCleared(boolean launchRod) { this.launchRodCleared = launchRod; + if (launchRod) { + startWarningsTime = getSimulationTime() + WARNINGS_WAIT; + } modID = new ModID(); } @@ -550,6 +565,8 @@ public class SimulationStatus implements Cloneable, Monitorable { flightDataBranch.setValue(FlightDataType.TYPE_VELOCITY_XY, MathUtil.hypot(getRocketVelocity().x, getRocketVelocity().y)); flightDataBranch.setValue(FlightDataType.TYPE_VELOCITY_Z, getRocketVelocity().z); + setMaxZVelocity(Math.max(getRocketVelocity().z, getMaxZVelocity())); + flightDataBranch.setValue(FlightDataType.TYPE_VELOCITY_TOTAL, getRocketVelocity().length()); Coordinate c = getRocketOrientationQuaternion().rotateZ(); @@ -563,6 +580,46 @@ public class SimulationStatus implements Cloneable, Monitorable { (System.nanoTime() - getSimulationStartWallTime()) / 1000000000.0); } + /** + * Get max Z velocity so far in flight + * @return max Z velocity so far + */ + public double getMaxZVelocity() { + return maxZVelocity; + } + + /** + * Set max Z velocity so far + * @param zVel current z velocity + */ + private void setMaxZVelocity(double zVel) { + if (zVel > maxZVelocity) { + maxZVelocity = zVel; + modID = new ModID(); + } + } + + /** + * Determine whether (most) flight event warnings are currently being saved. + * Warnings are not saved until 0.25 seconds after leaving the rail, and again + * after Z velocity is reduced to 20% of the max. + */ + boolean recordWarnings() { + if (!launchRodCleared) { + return false; + } + + if (getSimulationTime() < startWarningsTime) { + return false; + } + + if (getRocketVelocity().z < getMaxZVelocity() * 0.2) { + return false; + } + + return true; + } + /** * Add a flight event to the event queue unless a listener aborts adding it. * @@ -570,6 +627,7 @@ public class SimulationStatus implements Cloneable, Monitorable { */ public void addEvent(FlightEvent event) throws SimulationException { if (SimulationListenerHelper.fireAddFlightEvent(this, event)) { + if (event.getType() != FlightEvent.Type.ALTITUDE) { log.trace("Adding event to queue: " + event); } diff --git a/core/src/test/java/info/openrocket/core/simulation/FlightEventsTest.java b/core/src/test/java/info/openrocket/core/simulation/FlightEventsTest.java index 3654f5eee..cfedef013 100644 --- a/core/src/test/java/info/openrocket/core/simulation/FlightEventsTest.java +++ b/core/src/test/java/info/openrocket/core/simulation/FlightEventsTest.java @@ -138,10 +138,10 @@ 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.SIM_WARN, 2.01, null, warn), - new FlightEvent(FlightEvent.Type.TUMBLE, 2.85, null), + new FlightEvent(FlightEvent.Type.TUMBLE, 2.73, null), new FlightEvent(FlightEvent.Type.APOGEE, 3.78, rocket), - new FlightEvent(FlightEvent.Type.GROUND_HIT, 9.0, null), - new FlightEvent(FlightEvent.Type.SIMULATION_END, 9.0, null) + new FlightEvent(FlightEvent.Type.GROUND_HIT, 8.94, null), + new FlightEvent(FlightEvent.Type.SIMULATION_END, 8.94, null) }; // Side Boosters @@ -170,10 +170,26 @@ public class FlightEventsTest extends BaseTestCase { // time, from the right sources for (int i = 0; i < Math.min(expectedEvents.length, actualEvents.length); i++) { final FlightEvent expected = expectedEvents[i]; + Warning expectedWarning = null; + if (expected.getType() == FlightEvent.Type.SIM_WARN) { + expectedWarning = (Warning) expected.getData(); + } + final FlightEvent actual = actualEvents[i]; + Warning actualWarning = null; + if (actual.getType() == FlightEvent.Type.SIM_WARN) { + actualWarning = (Warning) actual.getData(); + } + + assertTrue(((expectedWarning == null) && (actualWarning == null)) || + ((expectedWarning != null) && expectedWarning.equals(actualWarning)) || + ((actualWarning != null) && actualWarning.equals(expectedWarning)), + "Branch " + branchNo + " FlightEvent " + i + ": " + expectedWarning + + " not found; " + actualWarning + " found instead"); + assertSame(expected.getType(), actual.getType(), - "Branch " + branchNo + " FlightEvent " + i + " type " + expected.getType() - + " not found; FlightEvent " + actual.getType() + " found instead"); + "Branch " + branchNo + " FlightEvent " + i + ": type " + expected.getType() + + " not found; FlightEvent " + actual.getType() + " found instead"); if (1200 != expected.getTime()) { // event times that are dependent on simulation step time shouldn't be held to diff --git a/swing/src/main/java/info/openrocket/swing/gui/plot/EventGraphics.java b/swing/src/main/java/info/openrocket/swing/gui/plot/EventGraphics.java index 27005f4da..ebc78e0b7 100644 --- a/swing/src/main/java/info/openrocket/swing/gui/plot/EventGraphics.java +++ b/swing/src/main/java/info/openrocket/swing/gui/plot/EventGraphics.java @@ -67,6 +67,7 @@ public class EventGraphics { "pix/eventicons/event-recovery-device-deployment.png"); loadImage(EVENT_IMAGES, FlightEvent.Type.GROUND_HIT, "pix/eventicons/event-ground-hit.png"); loadImage(EVENT_IMAGES, FlightEvent.Type.SIMULATION_END, "pix/eventicons/event-simulation-end.png"); + loadImage(EVENT_IMAGES, FlightEvent.Type.TUMBLE, "pix/eventicons/event-tumble.png"); loadImage(EVENT_IMAGES, FlightEvent.Type.EXCEPTION, "pix/eventicons/event-exception.png"); loadImage(EVENT_IMAGES, FlightEvent.Type.SIM_ABORT, "pix/eventicons/event-exception.png"); } diff --git a/swing/src/main/java/info/openrocket/swing/gui/plot/Plot.java b/swing/src/main/java/info/openrocket/swing/gui/plot/Plot.java index 7c24334aa..491808473 100644 --- a/swing/src/main/java/info/openrocket/swing/gui/plot/Plot.java +++ b/swing/src/main/java/info/openrocket/swing/gui/plot/Plot.java @@ -353,7 +353,9 @@ public abstract class Plot, C extend eventStr = eventStr + event.getType(); } } - sb.append(eventStr + "
"); + if (eventStr != "") { + sb.append(eventStr + "
"); + } } // Valid Y data? diff --git a/swing/src/main/resources/pix/eventicons/event-tumble.png b/swing/src/main/resources/pix/eventicons/event-tumble.png new file mode 100644 index 000000000..682c8d478 Binary files /dev/null and b/swing/src/main/resources/pix/eventicons/event-tumble.png differ