Initial i18n support, bug fixes
[debian/openrocket] / src / net / sf / openrocket / simulation / BasicEventSimulationEngine.java
1 package net.sf.openrocket.simulation;
2
3 import java.util.HashSet;
4 import java.util.Iterator;
5 import java.util.Set;
6
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;
28
29
30 public class BasicEventSimulationEngine implements SimulationEngine {
31         
32         private static final LogHelper log = Application.getLogger();
33         
34         // TODO: HIGH: Allow selecting steppers
35         private SimulationStepper flightStepper = new RK4SimulationStepper();
36         private SimulationStepper landingStepper = new BasicLandingStepper();
37         
38         private SimulationStepper currentStepper;
39         
40         private SimulationStatus status;
41         
42         
43         @Override
44         public FlightData simulate(SimulationConditions simulationConditions) throws SimulationException {
45                 Set<MotorId> motorBurntOut = new HashSet<MotorId>();
46                 
47                 // Set up flight data
48                 FlightData flightData = new FlightData();
49                 
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.");
55                 }
56                 
57                 // Initialize the simulation
58                 currentStepper = flightStepper;
59                 status = initialStatus(configuration, motorConfiguration, simulationConditions, flightData);
60                 status = currentStepper.initialize(status);
61                 
62
63                 SimulationListenerHelper.fireStartSimulation(status);
64                 
65                 try {
66                         
67                         // Start the simulation
68                         while (handleEvents()) {
69                                 
70                                 // Take the step
71                                 double oldAlt = status.getRocketPosition().z;
72                                 
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);
79                                         }
80                                         log.verbose("BasicEventSimulationEngine: Taking simulation step at t=" + status.getSimulationTime());
81                                         currentStepper.step(status, maxStepTime);
82                                 }
83                                 SimulationListenerHelper.firePostStep(status);
84                                 
85
86                                 // Check for NaN values in the simulation status
87                                 checkNaN();
88                                 
89                                 // Add altitude event
90                                 addEvent(new FlightEvent(FlightEvent.Type.ALTITUDE, status.getSimulationTime(),
91                                                 status.getConfiguration().getRocket(),
92                                                 new Pair<Double, Double>(oldAlt, status.getRocketPosition().z)));
93                                 
94
95                                 // Add appropriate events
96                                 if (!status.isLiftoff()) {
97                                         
98                                         // Avoid sinking into ground before liftoff
99                                         if (status.getRocketPosition().z < 0) {
100                                                 status.setRocketPosition(Coordinate.NUL);
101                                                 status.setRocketVelocity(Coordinate.NUL);
102                                         }
103                                         // Detect lift-off
104                                         if (status.getRocketPosition().z > 0.01) {
105                                                 addEvent(new FlightEvent(FlightEvent.Type.LIFTOFF, status.getSimulationTime()));
106                                         }
107                                         
108                                 } else {
109                                         
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()));
115                                         }
116                                         
117                                 }
118                                 
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));
123                                 }
124                                 
125
126                                 // Check for apogee
127                                 if (!status.isApogeeReached() && status.getRocketPosition().z < oldAlt - 0.001) {
128                                         addEvent(new FlightEvent(FlightEvent.Type.APOGEE, status.getSimulationTime(),
129                                                         status.getConfiguration().getRocket()));
130                                 }
131                                 
132
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));
139                                         }
140                                 }
141                                 
142                         }
143                         
144                 } catch (SimulationException e) {
145                         SimulationListenerHelper.fireEndSimulation(status, e);
146                         throw e;
147                 }
148                 
149                 SimulationListenerHelper.fireEndSimulation(status, null);
150                 
151                 flightData.addBranch(status.getFlightData());
152                 
153                 log.info("Warnings at the end of simulation:  " + flightData.getWarningSet());
154                 
155                 // TODO: HIGH: Simulate branches
156                 return flightData;
157         }
158         
159         
160
161         private SimulationStatus initialStatus(Configuration configuration,
162                         MotorInstanceConfiguration motorConfiguration,
163                         SimulationConditions simulationConditions, FlightData flightData) {
164                 
165                 SimulationStatus init = new SimulationStatus();
166                 init.setSimulationConditions(simulationConditions);
167                 init.setConfiguration(configuration);
168                 init.setMotorConfiguration(motorConfiguration);
169                 
170                 init.setSimulationTime(0);
171                 init.setPreviousTimeStep(simulationConditions.getTimeStep());
172                 init.setRocketPosition(Coordinate.NUL);
173                 init.setRocketVelocity(Coordinate.NUL);
174                 
175
176                 // Initialize to roll angle with least stability w.r.t. the wind
177                 Quaternion o;
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));
182                 
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())));
186                 
187                 init.setRocketOrientationQuaternion(o);
188                 init.setRocketRotationVelocity(Coordinate.NUL);
189                 
190
191                 /*
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.
194                  */
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) {
201                                         lugPosition = pos;
202                                 }
203                         }
204                 }
205                 if (!Double.isNaN(lugPosition)) {
206                         double maxX = 0;
207                         for (Coordinate c : configuration.getBounds()) {
208                                 if (c.x > maxX)
209                                         maxX = c.x;
210                         }
211                         if (maxX >= lugPosition) {
212                                 length = Math.max(0, length - (maxX - lugPosition));
213                         }
214                 }
215                 init.setEffectiveLaunchRodLength(length);
216                 
217
218
219                 init.setSimulationStartWallTime(System.nanoTime());
220                 
221                 init.setMotorIgnited(false);
222                 init.setLiftoff(false);
223                 init.setLaunchRodCleared(false);
224                 init.setApogeeReached(false);
225                 
226                 init.getEventQueue().add(new FlightEvent(FlightEvent.Type.LAUNCH, 0, simulationConditions.getRocket()));
227                 
228                 init.setFlightData(new FlightDataBranch("MAIN", FlightDataType.TYPE_TIME));
229                 init.setWarnings(flightData.getWarningSet());
230                 
231                 return init;
232         }
233         
234         
235
236         /**
237          * Create a rocket configuration from the launch conditions.
238          * 
239          * @param simulation    the launch conditions.
240          * @return                              a rocket configuration with all stages attached.
241          */
242         private Configuration setupConfiguration(SimulationConditions simulation) {
243                 Configuration configuration = new Configuration(simulation.getRocket());
244                 configuration.setAllStages();
245                 configuration.setMotorConfigurationID(simulation.getMotorConfigurationID());
246                 
247                 return configuration;
248         }
249         
250         
251
252         /**
253          * Create a new motor instance configuration for the rocket configuration.
254          * 
255          * @param configuration         the rocket configuration.
256          * @return                                      a new motor instance configuration with all motors in place.
257          */
258         private MotorInstanceConfiguration setupMotorConfiguration(Configuration configuration) {
259                 MotorInstanceConfiguration motors = new MotorInstanceConfiguration();
260                 final String motorId = configuration.getMotorConfigurationID();
261                 
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);
267                         
268                         if (motor != null) {
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);
274                                 }
275                         }
276                 }
277                 return motors;
278         }
279         
280         /**
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.
284          */
285         private boolean handleEvents() throws SimulationException {
286                 boolean ret = true;
287                 FlightEvent event;
288                 
289                 for (event = nextEvent(); event != null; event = nextEvent()) {
290                         
291                         // Call simulation listeners, allow aborting event handling
292                         if (!SimulationListenerHelper.fireHandleFlightEvent(status, event)) {
293                                 continue;
294                         }
295                         
296                         if (event.getType() != FlightEvent.Type.ALTITUDE) {
297                                 log.verbose("BasicEventSimulationEngine:  Handling event " + event);
298                         }
299                         
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)) {
305                                         continue;
306                                 }
307                         }
308                         
309                         if (event.getType() == FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT) {
310                                 RecoveryDevice device = (RecoveryDevice) event.getSource();
311                                 if (!SimulationListenerHelper.fireRecoveryDeviceDeployment(status, device)) {
312                                         continue;
313                                 }
314                         }
315                         
316
317
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;
322                                 
323                                 if (mount.getIgnitionEvent().isActivationEvent(event, component)) {
324                                         addEvent(new FlightEvent(FlightEvent.Type.IGNITION,
325                                                         status.getSimulationTime() + mount.getIgnitionDelay(),
326                                                         component, id));
327                                 }
328                         }
329                         
330
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))
336                                         continue;
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));
341                                 }
342                         }
343                         
344
345                         // Handle event
346                         switch (event.getType()) {
347                         
348                         case LAUNCH: {
349                                 status.getFlightData().addEvent(event);
350                                 break;
351                         }
352                                 
353                         case IGNITION: {
354                                 // Ignite the motor
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);
362                                 
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()));
369                                         }
370                                 }
371                                 break;
372                         }
373                                 
374                         case LIFTOFF: {
375                                 // Mark lift-off as occurred
376                                 status.setLiftoff(true);
377                                 status.getFlightData().addEvent(event);
378                                 break;
379                         }
380                                 
381                         case LAUNCHROD: {
382                                 // Mark launch rod as cleared
383                                 status.setLaunchRodCleared(true);
384                                 status.getFlightData().addEvent(event);
385                                 break;
386                         }
387                                 
388                         case BURNOUT: {
389                                 // If motor burnout occurs without lift-off, abort
390                                 if (!status.isLiftoff()) {
391                                         throw new SimulationLaunchException("Motor burnout without liftoff.");
392                                 }
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()));
400                                 }
401                                 status.getFlightData().addEvent(event);
402                                 break;
403                         }
404                                 
405                         case EJECTION_CHARGE: {
406                                 status.getFlightData().addEvent(event);
407                                 break;
408                         }
409                                 
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);
416                                 break;
417                         }
418                                 
419                         case APOGEE:
420                                 // Mark apogee as reached
421                                 status.setApogeeReached(true);
422                                 status.getFlightData().addEvent(event);
423                                 break;
424                         
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?
431                                         
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))
437                                                         continue;
438                                                 if (!status.getMotorConfiguration().getMotorInstance(motorId).isActive())
439                                                         continue;
440                                                 status.getWarnings().add(Warning.RECOVERY_DEPLOYMENT_WHILE_BURNING);
441                                         }
442                                         
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."));
447                                         }
448                                         
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 " +
453                                                                 "speed ("
454                                                                 + UnitGroup.UNITS_VELOCITY.toStringUnit(status.getRocketVelocity().length())
455                                                                 + ")."));
456                                         }
457                                         
458                                         status.setLiftoff(true);
459                                         status.getDeployedRecoveryDevices().add((RecoveryDevice) c);
460                                         
461                                         this.currentStepper = this.landingStepper;
462                                         this.status = currentStepper.initialize(status);
463                                         
464                                         status.getFlightData().addEvent(event);
465                                 }
466                                 break;
467                         
468                         case GROUND_HIT:
469                                 status.getFlightData().addEvent(event);
470                                 break;
471                         
472                         case SIMULATION_END:
473                                 ret = false;
474                                 status.getFlightData().addEvent(event);
475                                 break;
476                         
477                         case ALTITUDE:
478                                 break;
479                         }
480                         
481                 }
482                 
483
484                 // If no motor has ignited, abort
485                 if (!status.isMotorIgnited()) {
486                         throw new SimulationLaunchException("No motors ignited.");
487                 }
488                 
489                 return ret;
490         }
491         
492         
493         /**
494          * Add a flight event to the event queue unless a listener aborts adding it.
495          * 
496          * @param event         the event to add to the queue.
497          */
498         private void addEvent(FlightEvent event) throws SimulationException {
499                 if (SimulationListenerHelper.fireAddFlightEvent(status, event)) {
500                         status.getEventQueue().add(event);
501                 }
502         }
503         
504         
505
506         /**
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.
510          * 
511          * @param status        the simulation status
512          * @return                      the flight event to handle, or null
513          */
514         private FlightEvent nextEvent() {
515                 EventQueue queue = status.getEventQueue();
516                 FlightEvent event = queue.peek();
517                 if (event == null)
518                         return null;
519                 
520                 // Jump to event if no motors have been ignited
521                 if (!status.isMotorIgnited() && event.getTime() > status.getSimulationTime()) {
522                         status.setSimulationTime(event.getTime());
523                 }
524                 if (event.getTime() <= status.getSimulationTime()) {
525                         return queue.poll();
526                 } else {
527                         return null;
528                 }
529         }
530         
531         
532
533         private void checkNaN() throws SimulationException {
534                 double d = 0;
535                 boolean b = false;
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();
543                 
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.");
554                 }
555         }
556         
557
558 }