package net.sf.openrocket.simulation; import java.util.Iterator; import net.sf.openrocket.aerodynamics.Warning; import net.sf.openrocket.l10n.Translator; import net.sf.openrocket.motor.Motor; import net.sf.openrocket.motor.MotorId; import net.sf.openrocket.motor.MotorInstance; import net.sf.openrocket.motor.MotorInstanceConfiguration; import net.sf.openrocket.rocketcomponent.Configuration; import net.sf.openrocket.rocketcomponent.DeploymentConfiguration; import net.sf.openrocket.rocketcomponent.IgnitionConfiguration; import net.sf.openrocket.rocketcomponent.MotorConfiguration; import net.sf.openrocket.rocketcomponent.MotorMount; import net.sf.openrocket.rocketcomponent.RecoveryDevice; import net.sf.openrocket.rocketcomponent.RocketComponent; import net.sf.openrocket.rocketcomponent.Stage; import net.sf.openrocket.rocketcomponent.StageSeparationConfiguration; import net.sf.openrocket.simulation.exception.MotorIgnitionException; import net.sf.openrocket.simulation.exception.SimulationException; import net.sf.openrocket.simulation.exception.SimulationLaunchException; import net.sf.openrocket.simulation.listeners.SimulationListenerHelper; import net.sf.openrocket.simulation.listeners.system.OptimumCoastListener; import net.sf.openrocket.startup.Application; import net.sf.openrocket.util.Coordinate; import net.sf.openrocket.util.MathUtil; import net.sf.openrocket.util.Pair; import net.sf.openrocket.util.SimpleStack; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class BasicEventSimulationEngine implements SimulationEngine { private static final Translator trans = Application.getTranslator(); private static final Logger log = LoggerFactory.getLogger(BasicEventSimulationEngine.class); // TODO: MEDIUM: Allow selecting steppers private SimulationStepper flightStepper = new RK4SimulationStepper(); private SimulationStepper landingStepper = new BasicLandingStepper(); private SimulationStepper tumbleStepper = new BasicTumbleStepper(); // 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: this is an arbitrary value private final static double THRUST_TUMBLE_CONDITION = 0.01; private SimulationStepper currentStepper; private SimulationStatus status; private String flightConfigurationId; private SimpleStack<SimulationStatus> stages = new SimpleStack<SimulationStatus>(); @Override public FlightData simulate(SimulationConditions simulationConditions) throws SimulationException { // Set up flight data FlightData flightData = new FlightData(); // Set up rocket configuration Configuration configuration = setupConfiguration(simulationConditions); flightConfigurationId = configuration.getFlightConfigurationID(); MotorInstanceConfiguration motorConfiguration = setupMotorConfiguration(configuration); if (motorConfiguration.getMotorIDs().isEmpty()) { throw new MotorIgnitionException(trans.get("BasicEventSimulationEngine.error.noMotorsDefined")); } status = new SimulationStatus(configuration, motorConfiguration, simulationConditions); status.getEventQueue().add(new FlightEvent(FlightEvent.Type.LAUNCH, 0, simulationConditions.getRocket())); { // main sustainer stage RocketComponent sustainer = configuration.getRocket().getChild(0); status.setFlightData(new FlightDataBranch(sustainer.getName(), FlightDataType.TYPE_TIME)); } stages.add(status); SimulationListenerHelper.fireStartSimulation(status); while (true) { if (stages.size() == 0) { break; } SimulationStatus stageStatus = stages.pop(); if (stageStatus == null) { break; } status = stageStatus; FlightDataBranch dataBranch = simulateLoop(); flightData.addBranch(dataBranch); flightData.getWarningSet().addAll(status.getWarnings()); } SimulationListenerHelper.fireEndSimulation(status, null); configuration.release(); if (!flightData.getWarningSet().isEmpty()) { log.info("Warnings at the end of simulation: " + flightData.getWarningSet()); } return flightData; } private FlightDataBranch simulateLoop() { // Initialize the simulation currentStepper = flightStepper; status = currentStepper.initialize(status); // Get originating position (in case listener has modified launch position) Coordinate origin = status.getRocketPosition(); Coordinate originVelocity = status.getRocketVelocity(); try { // Start the simulation while (handleEvents()) { // Take the step double oldAlt = status.getRocketPosition().z; if (SimulationListenerHelper.firePreStep(status)) { // Step at most to the next event double maxStepTime = Double.MAX_VALUE; FlightEvent nextEvent = status.getEventQueue().peek(); if (nextEvent != null) { maxStepTime = MathUtil.max(nextEvent.getTime() - status.getSimulationTime(), 0.001); } log.trace("BasicEventSimulationEngine: Taking simulation step at t=" + status.getSimulationTime()); currentStepper.step(status, maxStepTime); } SimulationListenerHelper.firePostStep(status); // Check for NaN values in the simulation status checkNaN(); // Add altitude event addEvent(new FlightEvent(FlightEvent.Type.ALTITUDE, status.getSimulationTime(), status.getConfiguration().getRocket(), new Pair<Double, Double>(oldAlt, status.getRocketPosition().z))); if (status.getRocketPosition().z > status.getMaxAlt()) { status.setMaxAlt(status.getRocketPosition().z); } // Position relative to start location Coordinate relativePosition = status.getRocketPosition().sub(origin); // Add appropriate events if (!status.isLiftoff()) { // Avoid sinking into ground before liftoff if (relativePosition.z < 0) { status.setRocketPosition(origin); status.setRocketVelocity(originVelocity); } // Detect lift-off if (relativePosition.z > 0.02) { addEvent(new FlightEvent(FlightEvent.Type.LIFTOFF, status.getSimulationTime())); } } else { // Check ground hit after liftoff if (status.getRocketPosition().z < 0) { status.setRocketPosition(status.getRocketPosition().setZ(0)); addEvent(new FlightEvent(FlightEvent.Type.GROUND_HIT, status.getSimulationTime())); addEvent(new FlightEvent(FlightEvent.Type.SIMULATION_END, status.getSimulationTime())); } } // Check for launch guide clearance if (!status.isLaunchRodCleared() && relativePosition.length() > status.getSimulationConditions().getLaunchRodLength()) { addEvent(new FlightEvent(FlightEvent.Type.LAUNCHROD, status.getSimulationTime(), null)); } // Check for apogee if (!status.isApogeeReached() && status.getRocketPosition().z < status.getMaxAlt() - 0.01) { status.setMaxAltTime(status.getSimulationTime()); addEvent(new FlightEvent(FlightEvent.Type.APOGEE, status.getSimulationTime(), status.getConfiguration().getRocket())); } // Check for burnt out motors for (MotorId motorId : status.getMotorConfiguration().getMotorIDs()) { MotorInstance motor = status.getMotorConfiguration().getMotorInstance(motorId); if (!motor.isActive() && status.addBurntOutMotor(motorId)) { addEvent(new FlightEvent(FlightEvent.Type.BURNOUT, status.getSimulationTime(), (RocketComponent) status.getMotorConfiguration().getMotorMount(motorId), motorId)); } } // Check for Tumbling // Conditions for transision are: // apogee reached (if sustainer stage) // and is not already tumbling // and not stable (cg > cp) // and aoa > AOA_TUMBLE_CONDITION threshold // and thrust < THRUST_TUMBLE_CONDITION threshold if (!status.isTumbling()) { final double t = status.getFlightData().getLast(FlightDataType.TYPE_THRUST_FORCE); final double cp = status.getFlightData().getLast(FlightDataType.TYPE_CP_LOCATION); final double cg = status.getFlightData().getLast(FlightDataType.TYPE_CG_LOCATION); final double aoa = status.getFlightData().getLast(FlightDataType.TYPE_AOA); final boolean wantToTumble = (cg > cp && aoa > AOA_TUMBLE_CONDITION); if (wantToTumble) { final boolean tooMuchThrust = t > THRUST_TUMBLE_CONDITION; final boolean isSustainer = status.getConfiguration().isStageActive(0); final boolean isApogee = status.isApogeeReached(); if (tooMuchThrust) { status.getWarnings().add(Warning.TUMBLE_UNDER_THRUST); } else if (isApogee) { addEvent(new FlightEvent(FlightEvent.Type.TUMBLE, status.getSimulationTime())); status.setTumbling(true); } } } } } catch (SimulationException e) { SimulationListenerHelper.fireEndSimulation(status, e); // Add FlightEvent for Abort. status.getFlightData().addEvent(new FlightEvent(FlightEvent.Type.EXCEPTION, status.getSimulationTime(), status.getConfiguration().getRocket(), e.getLocalizedMessage())); status.getWarnings().add(e.getLocalizedMessage()); } return status.getFlightData(); } /** * Create a rocket configuration from the launch conditions. * * @param simulation the launch conditions. * @return a rocket configuration with all stages attached. */ private Configuration setupConfiguration(SimulationConditions simulation) { Configuration configuration = new Configuration(simulation.getRocket()); configuration.setAllStages(); configuration.setFlightConfigurationID(simulation.getMotorConfigurationID()); return configuration; } /** * Create a new motor instance configuration for the rocket configuration. * * @param configuration the rocket configuration. * @return a new motor instance configuration with all motors in place. */ private MotorInstanceConfiguration setupMotorConfiguration(Configuration configuration) { MotorInstanceConfiguration motors = new MotorInstanceConfiguration(); final String flightConfigId = configuration.getFlightConfigurationID(); Iterator<MotorMount> iterator = configuration.motorIterator(); while (iterator.hasNext()) { MotorMount mount = iterator.next(); RocketComponent component = (RocketComponent) mount; MotorConfiguration motorConfig = mount.getMotorConfiguration().get(flightConfigId); IgnitionConfiguration ignitionConfig = mount.getIgnitionConfiguration().get(flightConfigId); Motor motor = motorConfig.getMotor(); if (motor != null) { Coordinate[] positions = component.toAbsolute(mount.getMotorPosition(flightConfigId)); for (int i = 0; i < positions.length; i++) { Coordinate position = positions[i]; MotorId id = new MotorId(component.getID(), i + 1); motors.addMotor(id, motor.getInstance(), motorConfig.getEjectionDelay(), mount, ignitionConfig.getIgnitionEvent(), ignitionConfig.getIgnitionDelay(), position); } } } return motors; } /** * Handles events occurring during the flight from the event queue. * Each event that has occurred before or at the current simulation time is * processed. Suitable events are also added to the flight data. */ private boolean handleEvents() throws SimulationException { boolean ret = true; FlightEvent event; log.trace("HandleEvents: current branch = " + status.getFlightData().getBranchName()); log.trace("EventQueue = " + status.getEventQueue().toString()); for (event = nextEvent(); event != null; event = nextEvent()) { // Ignore events for components that are no longer attached to the rocket if (event.getSource() != null && event.getSource().getParent() != null && !status.getConfiguration().isStageActive(event.getSource().getStageNumber())) { continue; } // Call simulation listeners, allow aborting event handling if (!SimulationListenerHelper.fireHandleFlightEvent(status, event)) { continue; } if (event.getType() != FlightEvent.Type.ALTITUDE) { log.trace("BasicEventSimulationEngine: Handling event " + event); } if (event.getType() == FlightEvent.Type.IGNITION) { MotorMount mount = (MotorMount) event.getSource(); MotorId motorId = (MotorId) event.getData(); MotorInstance instance = status.getMotorConfiguration().getMotorInstance(motorId); if (!SimulationListenerHelper.fireMotorIgnition(status, motorId, mount, instance)) { continue; } } if (event.getType() == FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT) { RecoveryDevice device = (RecoveryDevice) event.getSource(); if (!SimulationListenerHelper.fireRecoveryDeviceDeployment(status, device)) { continue; } } // Check for motor ignition events, add ignition events to queue for (MotorId id : status.getMotorConfiguration().getMotorIDs()) { IgnitionConfiguration.IgnitionEvent ignitionEvent = status.getMotorConfiguration().getMotorIgnitionEvent(id); MotorMount mount = status.getMotorConfiguration().getMotorMount(id); RocketComponent component = (RocketComponent) mount; if (ignitionEvent.isActivationEvent(event, component)) { double ignitionDelay = status.getMotorConfiguration().getMotorIgnitionDelay(id); addEvent(new FlightEvent(FlightEvent.Type.IGNITION, status.getSimulationTime() + ignitionDelay, component, id)); } } // Check for stage separation event for (int stageNo : status.getConfiguration().getActiveStages()) { if (stageNo == 0) continue; Stage stage = (Stage) status.getConfiguration().getRocket().getChild(stageNo); StageSeparationConfiguration separationConfig = stage.getStageSeparationConfiguration().get(flightConfigurationId); if (separationConfig.getSeparationEvent().isSeparationEvent(event, stage)) { addEvent(new FlightEvent(FlightEvent.Type.STAGE_SEPARATION, event.getTime() + separationConfig.getSeparationDelay(), stage)); } } // Check for recovery device deployment, add events to queue Iterator<RocketComponent> rci = status.getConfiguration().iterator(); while (rci.hasNext()) { RocketComponent c = rci.next(); if (!(c instanceof RecoveryDevice)) continue; DeploymentConfiguration deployConfig = ((RecoveryDevice) c).getDeploymentConfiguration().get(flightConfigurationId); if (deployConfig.isActivationEvent(event, c)) { // Delay event by at least 1ms to allow stage separation to occur first addEvent(new FlightEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, event.getTime() + Math.max(0.001, deployConfig.getDeployDelay()), c)); } } // Handle event switch (event.getType()) { case LAUNCH: { status.getFlightData().addEvent(event); break; } case IGNITION: { // Ignite the motor MotorId motorId = (MotorId) event.getData(); MotorInstanceConfiguration config = status.getMotorConfiguration(); config.setMotorIgnitionTime(motorId, event.getTime()); status.setMotorIgnited(true); status.getFlightData().addEvent(event); break; } case LIFTOFF: { // Mark lift-off as occurred status.setLiftoff(true); status.getFlightData().addEvent(event); break; } case LAUNCHROD: { // Mark launch rod as cleared status.setLaunchRodCleared(true); status.getFlightData().addEvent(event); break; } case BURNOUT: { // If motor burnout occurs without lift-off, abort if (!status.isLiftoff()) { throw new SimulationLaunchException(trans.get("BasicEventSimulationEngine.error.earlyMotorBurnout")); } // Add ejection charge event MotorId motorId = (MotorId) event.getData(); double delay = status.getMotorConfiguration().getEjectionDelay(motorId); if (delay != Motor.PLUGGED) { addEvent(new FlightEvent(FlightEvent.Type.EJECTION_CHARGE, status.getSimulationTime() + delay, event.getSource(), event.getData())); } status.getFlightData().addEvent(event); break; } case EJECTION_CHARGE: { status.getFlightData().addEvent(event); break; } case STAGE_SEPARATION: { // Record the event. status.getFlightData().addEvent(event); RocketComponent stage = event.getSource(); int n = stage.getStageNumber(); // Prepare the booster status for simulation. SimulationStatus boosterStatus = new SimulationStatus(status); boosterStatus.setFlightData(new FlightDataBranch(stage.getName(), FlightDataType.TYPE_TIME)); stages.add(boosterStatus); // Mark the status as having dropped the booster status.getConfiguration().setToStage(n - 1); // Mark the booster status as only having the booster. boosterStatus.getConfiguration().setOnlyStage(n); break; } case APOGEE: // Mark apogee as reached status.setApogeeReached(true); status.getFlightData().addEvent(event); // This apogee event might be the optimum if recovery has not already happened. if (status.getSimulationConditions().isCalculateExtras() && status.getDeployedRecoveryDevices().size() == 0) { status.getFlightData().setOptimumAltitude(status.getMaxAlt()); status.getFlightData().setTimeToOptimumAltitude(status.getMaxAltTime()); } break; case RECOVERY_DEVICE_DEPLOYMENT: RocketComponent c = event.getSource(); int n = c.getStageNumber(); // Ignore event if stage not active if (status.getConfiguration().isStageActive(n)) { // TODO: HIGH: Check stage activeness for other events as well? // Check whether any motor in the active stages is active anymore for (MotorId motorId : status.getMotorConfiguration().getMotorIDs()) { int stage = ((RocketComponent) status.getMotorConfiguration(). getMotorMount(motorId)).getStageNumber(); if (!status.getConfiguration().isStageActive(stage)) continue; if (!status.getMotorConfiguration().getMotorInstance(motorId).isActive()) continue; status.getWarnings().add(Warning.RECOVERY_DEPLOYMENT_WHILE_BURNING); } // Check for launch rod if (!status.isLaunchRodCleared()) { status.getWarnings().add(Warning.RECOVERY_LAUNCH_ROD); } // Check current velocity if (status.getRocketVelocity().length() > 20) { status.getWarnings().add(new Warning.HighSpeedDeployment(status.getRocketVelocity().length())); } status.setLiftoff(true); status.getDeployedRecoveryDevices().add((RecoveryDevice) c); // If we haven't already reached apogee, then we need to compute the actual coast time // to determine the optimum altitude. if (status.getSimulationConditions().isCalculateExtras() && !status.isApogeeReached()) { FlightData coastStatus = computeCoastTime(); status.getFlightData().setOptimumAltitude(coastStatus.getMaxAltitude()); status.getFlightData().setTimeToOptimumAltitude(coastStatus.getTimeToApogee()); } this.currentStepper = this.landingStepper; this.status = currentStepper.initialize(status); status.getFlightData().addEvent(event); } break; case GROUND_HIT: status.getFlightData().addEvent(event); break; case SIMULATION_END: ret = false; status.getFlightData().addEvent(event); break; case ALTITUDE: break; case TUMBLE: this.currentStepper = this.tumbleStepper; this.status = currentStepper.initialize(status); status.getFlightData().addEvent(event); break; } } // If no motor has ignited, abort if (!status.isMotorIgnited()) { throw new MotorIgnitionException(trans.get("BasicEventSimulationEngine.error.noIgnition")); } return ret; } /** * Add a flight event to the event queue unless a listener aborts adding it. * * @param event the event to add to the queue. */ private void addEvent(FlightEvent event) throws SimulationException { if (SimulationListenerHelper.fireAddFlightEvent(status, event)) { status.getEventQueue().add(event); } } /** * Return the next flight event to handle, or null if no more events should be handled. * This method jumps the simulation time forward in case no motors have been ignited. * The flight event is removed from the event queue. * * @return the flight event to handle, or null */ private FlightEvent nextEvent() { EventQueue queue = status.getEventQueue(); FlightEvent event = queue.peek(); if (event == null) return null; // Jump to event if no motors have been ignited if (!status.isMotorIgnited() && event.getTime() > status.getSimulationTime()) { status.setSimulationTime(event.getTime()); } if (event.getTime() <= status.getSimulationTime()) { return queue.poll(); } else { return null; } } private void checkNaN() throws SimulationException { double d = 0; boolean b = false; d += status.getSimulationTime(); d += status.getPreviousTimeStep(); b |= status.getRocketPosition().isNaN(); b |= status.getRocketVelocity().isNaN(); b |= status.getRocketOrientationQuaternion().isNaN(); b |= status.getRocketRotationVelocity().isNaN(); d += status.getEffectiveLaunchRodLength(); if (Double.isNaN(d) || b) { log.error("Simulation resulted in NaN value:" + " simulationTime=" + status.getSimulationTime() + " previousTimeStep=" + status.getPreviousTimeStep() + " rocketPosition=" + status.getRocketPosition() + " rocketVelocity=" + status.getRocketVelocity() + " rocketOrientationQuaternion=" + status.getRocketOrientationQuaternion() + " rocketRotationVelocity=" + status.getRocketRotationVelocity() + " effectiveLaunchRodLength=" + status.getEffectiveLaunchRodLength()); throw new SimulationException(trans.get("BasicEventSimulationEngine.error.NaNResult")); } } private FlightData computeCoastTime() { try { SimulationConditions conds = status.getSimulationConditions().clone(); conds.getSimulationListenerList().add(OptimumCoastListener.INSTANCE); BasicEventSimulationEngine e = new BasicEventSimulationEngine(); FlightData d = e.simulate(conds); return d; } catch (Exception e) { log.warn("Exception computing coast time: ", e); return null; } } }