1 package net.sf.openrocket.simulation;
3 import java.util.HashSet;
4 import java.util.Iterator;
7 import net.sf.openrocket.aerodynamics.FlightConditions;
8 import net.sf.openrocket.aerodynamics.Warning;
9 import net.sf.openrocket.logging.LogHelper;
10 import net.sf.openrocket.motor.Motor;
11 import net.sf.openrocket.motor.MotorId;
12 import net.sf.openrocket.motor.MotorInstance;
13 import net.sf.openrocket.motor.MotorInstanceConfiguration;
14 import net.sf.openrocket.rocketcomponent.Configuration;
15 import net.sf.openrocket.rocketcomponent.LaunchLug;
16 import net.sf.openrocket.rocketcomponent.MotorMount;
17 import net.sf.openrocket.rocketcomponent.RecoveryDevice;
18 import net.sf.openrocket.rocketcomponent.RocketComponent;
19 import net.sf.openrocket.simulation.exception.SimulationException;
20 import net.sf.openrocket.simulation.exception.SimulationLaunchException;
21 import net.sf.openrocket.simulation.listeners.SimulationListenerHelper;
22 import net.sf.openrocket.startup.Application;
23 import net.sf.openrocket.unit.UnitGroup;
24 import net.sf.openrocket.util.Coordinate;
25 import net.sf.openrocket.util.MathUtil;
26 import net.sf.openrocket.util.Pair;
27 import net.sf.openrocket.util.Quaternion;
30 public class BasicEventSimulationEngine implements SimulationEngine {
32 private static final LogHelper log = Application.getLogger();
34 // TODO: HIGH: Allow selecting steppers
35 private SimulationStepper flightStepper = new RK4SimulationStepper();
36 private SimulationStepper landingStepper = new BasicLandingStepper();
38 private SimulationStepper currentStepper;
40 private SimulationStatus status;
44 public FlightData simulate(SimulationConditions simulationConditions) throws SimulationException {
45 Set<MotorId> motorBurntOut = new HashSet<MotorId>();
48 FlightData flightData = new FlightData();
50 // Set up rocket configuration
51 Configuration configuration = setupConfiguration(simulationConditions);
52 MotorInstanceConfiguration motorConfiguration = setupMotorConfiguration(configuration);
53 if (motorConfiguration.getMotorIDs().isEmpty()) {
54 throw new SimulationLaunchException("No motors defined.");
57 // Initialize the simulation
58 currentStepper = flightStepper;
59 status = initialStatus(configuration, motorConfiguration, simulationConditions, flightData);
60 status = currentStepper.initialize(status);
63 SimulationListenerHelper.fireStartSimulation(status);
67 // Start the simulation
68 while (handleEvents()) {
71 double oldAlt = status.getRocketPosition().z;
73 if (SimulationListenerHelper.firePreStep(status)) {
74 // Step at most to the next event
75 double maxStepTime = Double.MAX_VALUE;
76 FlightEvent nextEvent = status.getEventQueue().peek();
77 if (nextEvent != null) {
78 maxStepTime = MathUtil.max(nextEvent.getTime() - status.getSimulationTime(), 0.001);
80 log.verbose("BasicEventSimulationEngine: Taking simulation step at t=" + status.getSimulationTime());
81 currentStepper.step(status, maxStepTime);
83 SimulationListenerHelper.firePostStep(status);
86 // Check for NaN values in the simulation status
90 addEvent(new FlightEvent(FlightEvent.Type.ALTITUDE, status.getSimulationTime(),
91 status.getConfiguration().getRocket(),
92 new Pair<Double, Double>(oldAlt, status.getRocketPosition().z)));
95 // Add appropriate events
96 if (!status.isLiftoff()) {
98 // Avoid sinking into ground before liftoff
99 if (status.getRocketPosition().z < 0) {
100 status.setRocketPosition(Coordinate.NUL);
101 status.setRocketVelocity(Coordinate.NUL);
104 if (status.getRocketPosition().z > 0.01) {
105 addEvent(new FlightEvent(FlightEvent.Type.LIFTOFF, status.getSimulationTime()));
110 // Check ground hit after liftoff
111 if (status.getRocketPosition().z < 0) {
112 status.setRocketPosition(status.getRocketPosition().setZ(0));
113 addEvent(new FlightEvent(FlightEvent.Type.GROUND_HIT, status.getSimulationTime()));
114 addEvent(new FlightEvent(FlightEvent.Type.SIMULATION_END, status.getSimulationTime()));
119 // Check for launch guide clearance
120 if (!status.isLaunchRodCleared() &&
121 status.getRocketPosition().length() > status.getSimulationConditions().getLaunchRodLength()) {
122 addEvent(new FlightEvent(FlightEvent.Type.LAUNCHROD, status.getSimulationTime(), null));
127 if (!status.isApogeeReached() && status.getRocketPosition().z < oldAlt - 0.001) {
128 addEvent(new FlightEvent(FlightEvent.Type.APOGEE, status.getSimulationTime(),
129 status.getConfiguration().getRocket()));
133 // Check for burnt out motors
134 for (MotorId motorId : status.getMotorConfiguration().getMotorIDs()) {
135 MotorInstance motor = status.getMotorConfiguration().getMotorInstance(motorId);
136 if (!motor.isActive() && motorBurntOut.add(motorId)) {
137 addEvent(new FlightEvent(FlightEvent.Type.BURNOUT, status.getSimulationTime(),
138 (RocketComponent) status.getMotorConfiguration().getMotorMount(motorId), motorId));
144 } catch (SimulationException e) {
145 SimulationListenerHelper.fireEndSimulation(status, e);
149 SimulationListenerHelper.fireEndSimulation(status, null);
151 flightData.addBranch(status.getFlightData());
153 log.info("Warnings at the end of simulation: " + flightData.getWarningSet());
155 // TODO: HIGH: Simulate branches
161 private SimulationStatus initialStatus(Configuration configuration,
162 MotorInstanceConfiguration motorConfiguration,
163 SimulationConditions simulationConditions, FlightData flightData) {
165 SimulationStatus init = new SimulationStatus();
166 init.setSimulationConditions(simulationConditions);
167 init.setConfiguration(configuration);
168 init.setMotorConfiguration(motorConfiguration);
170 init.setSimulationTime(0);
171 init.setPreviousTimeStep(simulationConditions.getTimeStep());
172 init.setRocketPosition(Coordinate.NUL);
173 init.setRocketVelocity(Coordinate.NUL);
176 // Initialize to roll angle with least stability w.r.t. the wind
178 FlightConditions cond = new FlightConditions(configuration);
179 simulationConditions.getAerodynamicCalculator().getWorstCP(configuration, cond, null);
180 double angle = -cond.getTheta() - simulationConditions.getLaunchRodDirection();
181 o = Quaternion.rotation(new Coordinate(0, 0, angle));
183 // Launch rod angle and direction
184 o = o.multiplyLeft(Quaternion.rotation(new Coordinate(0, simulationConditions.getLaunchRodAngle(), 0)));
185 o = o.multiplyLeft(Quaternion.rotation(new Coordinate(0, 0, simulationConditions.getLaunchRodDirection())));
187 init.setRocketOrientationQuaternion(o);
188 init.setRocketRotationVelocity(Coordinate.NUL);
192 * Calculate the effective launch rod length taking into account launch lugs.
193 * If no lugs are found, assume a tower launcher of full length.
195 double length = simulationConditions.getLaunchRodLength();
196 double lugPosition = Double.NaN;
197 for (RocketComponent c : configuration) {
198 if (c instanceof LaunchLug) {
199 double pos = c.toAbsolute(new Coordinate(c.getLength()))[0].x;
200 if (Double.isNaN(lugPosition) || pos > lugPosition) {
205 if (!Double.isNaN(lugPosition)) {
207 for (Coordinate c : configuration.getBounds()) {
211 if (maxX >= lugPosition) {
212 length = Math.max(0, length - (maxX - lugPosition));
215 init.setEffectiveLaunchRodLength(length);
219 init.setSimulationStartWallTime(System.nanoTime());
221 init.setMotorIgnited(false);
222 init.setLiftoff(false);
223 init.setLaunchRodCleared(false);
224 init.setApogeeReached(false);
226 init.getEventQueue().add(new FlightEvent(FlightEvent.Type.LAUNCH, 0, simulationConditions.getRocket()));
228 init.setFlightData(new FlightDataBranch("MAIN", FlightDataType.TYPE_TIME));
229 init.setWarnings(flightData.getWarningSet());
237 * Create a rocket configuration from the launch conditions.
239 * @param simulation the launch conditions.
240 * @return a rocket configuration with all stages attached.
242 private Configuration setupConfiguration(SimulationConditions simulation) {
243 Configuration configuration = new Configuration(simulation.getRocket());
244 configuration.setAllStages();
245 configuration.setMotorConfigurationID(simulation.getMotorConfigurationID());
247 return configuration;
253 * Create a new motor instance configuration for the rocket configuration.
255 * @param configuration the rocket configuration.
256 * @return a new motor instance configuration with all motors in place.
258 private MotorInstanceConfiguration setupMotorConfiguration(Configuration configuration) {
259 MotorInstanceConfiguration motors = new MotorInstanceConfiguration();
260 final String motorId = configuration.getMotorConfigurationID();
262 Iterator<MotorMount> iterator = configuration.motorIterator();
263 while (iterator.hasNext()) {
264 MotorMount mount = iterator.next();
265 RocketComponent component = (RocketComponent) mount;
266 Motor motor = mount.getMotor(motorId);
269 Coordinate[] positions = component.toAbsolute(mount.getMotorPosition(motorId));
270 for (int i = 0; i < positions.length; i++) {
271 Coordinate position = positions[i];
272 MotorId id = new MotorId(component.getID(), i + 1);
273 motors.addMotor(id, motor.getInstance(), mount, position);
281 * Handles events occurring during the flight from the event queue.
282 * Each event that has occurred before or at the current simulation time is
283 * processed. Suitable events are also added to the flight data.
285 private boolean handleEvents() throws SimulationException {
289 for (event = nextEvent(); event != null; event = nextEvent()) {
291 // Call simulation listeners, allow aborting event handling
292 if (!SimulationListenerHelper.fireHandleFlightEvent(status, event)) {
296 if (event.getType() != FlightEvent.Type.ALTITUDE) {
297 log.verbose("BasicEventSimulationEngine: Handling event " + event);
300 if (event.getType() == FlightEvent.Type.IGNITION) {
301 MotorMount mount = (MotorMount) event.getSource();
302 MotorId motorId = (MotorId) event.getData();
303 MotorInstance instance = status.getMotorConfiguration().getMotorInstance(motorId);
304 if (!SimulationListenerHelper.fireMotorIgnition(status, motorId, mount, instance)) {
309 if (event.getType() == FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT) {
310 RecoveryDevice device = (RecoveryDevice) event.getSource();
311 if (!SimulationListenerHelper.fireRecoveryDeviceDeployment(status, device)) {
318 // Check for motor ignition events, add ignition events to queue
319 for (MotorId id : status.getMotorConfiguration().getMotorIDs()) {
320 MotorMount mount = status.getMotorConfiguration().getMotorMount(id);
321 RocketComponent component = (RocketComponent) mount;
323 if (mount.getIgnitionEvent().isActivationEvent(event, component)) {
324 addEvent(new FlightEvent(FlightEvent.Type.IGNITION,
325 status.getSimulationTime() + mount.getIgnitionDelay(),
331 // Check for recovery device deployment, add events to queue
332 Iterator<RocketComponent> rci = status.getConfiguration().iterator();
333 while (rci.hasNext()) {
334 RocketComponent c = rci.next();
335 if (!(c instanceof RecoveryDevice))
337 if (((RecoveryDevice) c).getDeployEvent().isActivationEvent(event, c)) {
338 // Delay event by at least 1ms to allow stage separation to occur first
339 addEvent(new FlightEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT,
340 event.getTime() + Math.max(0.001, ((RecoveryDevice) c).getDeployDelay()), c));
346 switch (event.getType()) {
349 status.getFlightData().addEvent(event);
355 MotorMount mount = (MotorMount) event.getSource();
356 RocketComponent component = (RocketComponent) mount;
357 MotorId motorId = (MotorId) event.getData();
358 MotorInstanceConfiguration config = status.getMotorConfiguration();
359 config.setMotorIgnitionTime(motorId, event.getTime());
360 status.setMotorIgnited(true);
361 status.getFlightData().addEvent(event);
363 // Add stage separation event if appropriate
364 int n = component.getStageNumber();
365 if (n < component.getRocket().getStageCount() - 1) {
366 if (status.getConfiguration().isStageActive(n + 1)) {
367 addEvent(new FlightEvent(FlightEvent.Type.STAGE_SEPARATION, event.getTime(),
368 component.getStage()));
375 // Mark lift-off as occurred
376 status.setLiftoff(true);
377 status.getFlightData().addEvent(event);
382 // Mark launch rod as cleared
383 status.setLaunchRodCleared(true);
384 status.getFlightData().addEvent(event);
389 // If motor burnout occurs without lift-off, abort
390 if (!status.isLiftoff()) {
391 throw new SimulationLaunchException("Motor burnout without liftoff.");
393 // Add ejection charge event
394 String id = status.getConfiguration().getMotorConfigurationID();
395 MotorMount mount = (MotorMount) event.getSource();
396 double delay = mount.getMotorDelay(id);
397 if (delay != Motor.PLUGGED) {
398 addEvent(new FlightEvent(FlightEvent.Type.EJECTION_CHARGE, status.getSimulationTime() + delay,
399 event.getSource(), event.getData()));
401 status.getFlightData().addEvent(event);
405 case EJECTION_CHARGE: {
406 status.getFlightData().addEvent(event);
410 case STAGE_SEPARATION: {
411 // TODO: HIGH: Store lower stages to be simulated later
412 RocketComponent stage = event.getSource();
413 int n = stage.getStageNumber();
414 status.getConfiguration().setToStage(n);
415 status.getFlightData().addEvent(event);
420 // Mark apogee as reached
421 status.setApogeeReached(true);
422 status.getFlightData().addEvent(event);
425 case RECOVERY_DEVICE_DEPLOYMENT:
426 RocketComponent c = event.getSource();
427 int n = c.getStageNumber();
428 // Ignore event if stage not active
429 if (status.getConfiguration().isStageActive(n)) {
430 // TODO: HIGH: Check stage activeness for other events as well?
432 // Check whether any motor in the active stages is active anymore
433 for (MotorId motorId : status.getMotorConfiguration().getMotorIDs()) {
434 int stage = ((RocketComponent) status.getMotorConfiguration().
435 getMotorMount(motorId)).getStageNumber();
436 if (!status.getConfiguration().isStageActive(stage))
438 if (!status.getMotorConfiguration().getMotorInstance(motorId).isActive())
440 status.getWarnings().add(Warning.RECOVERY_DEPLOYMENT_WHILE_BURNING);
443 // Check for launch rod
444 if (!status.isLaunchRodCleared()) {
445 status.getWarnings().add(Warning.fromString("Recovery device device deployed while on " +
446 "the launch guide."));
449 // Check current velocity
450 if (status.getRocketVelocity().length() > 20) {
451 // TODO: LOW: Custom warning.
452 status.getWarnings().add(Warning.fromString("Recovery device deployment at high " +
454 + UnitGroup.UNITS_VELOCITY.toStringUnit(status.getRocketVelocity().length())
458 status.setLiftoff(true);
459 status.getDeployedRecoveryDevices().add((RecoveryDevice) c);
461 this.currentStepper = this.landingStepper;
462 this.status = currentStepper.initialize(status);
464 status.getFlightData().addEvent(event);
469 status.getFlightData().addEvent(event);
474 status.getFlightData().addEvent(event);
484 // If no motor has ignited, abort
485 if (!status.isMotorIgnited()) {
486 throw new SimulationLaunchException("No motors ignited.");
494 * Add a flight event to the event queue unless a listener aborts adding it.
496 * @param event the event to add to the queue.
498 private void addEvent(FlightEvent event) throws SimulationException {
499 if (SimulationListenerHelper.fireAddFlightEvent(status, event)) {
500 status.getEventQueue().add(event);
507 * Return the next flight event to handle, or null if no more events should be handled.
508 * This method jumps the simulation time forward in case no motors have been ignited.
509 * The flight event is removed from the event queue.
511 * @param status the simulation status
512 * @return the flight event to handle, or null
514 private FlightEvent nextEvent() {
515 EventQueue queue = status.getEventQueue();
516 FlightEvent event = queue.peek();
520 // Jump to event if no motors have been ignited
521 if (!status.isMotorIgnited() && event.getTime() > status.getSimulationTime()) {
522 status.setSimulationTime(event.getTime());
524 if (event.getTime() <= status.getSimulationTime()) {
533 private void checkNaN() throws SimulationException {
536 d += status.getSimulationTime();
537 d += status.getPreviousTimeStep();
538 b |= status.getRocketPosition().isNaN();
539 b |= status.getRocketVelocity().isNaN();
540 b |= status.getRocketOrientationQuaternion().isNaN();
541 b |= status.getRocketRotationVelocity().isNaN();
542 d += status.getEffectiveLaunchRodLength();
544 if (Double.isNaN(d) || b) {
545 log.error("Simulation resulted in NaN value:" +
546 " simulationTime=" + status.getSimulationTime() +
547 " previousTimeStep=" + status.getPreviousTimeStep() +
548 " rocketPosition=" + status.getRocketPosition() +
549 " rocketVelocity=" + status.getRocketVelocity() +
550 " rocketOrientationQuaternion=" + status.getRocketOrientationQuaternion() +
551 " rocketRotationVelocity=" + status.getRocketRotationVelocity() +
552 " effectiveLaunchRodLength=" + status.getEffectiveLaunchRodLength());
553 throw new SimulationException("Simulation resulted in not-a-number (NaN) value, please report a bug.");