1 package net.sf.openrocket.simulation;
3 import java.util.ArrayList;
4 import java.util.EventListener;
5 import java.util.EventObject;
7 import java.util.Random;
10 import net.sf.openrocket.aerodynamics.BarrowmanCalculator;
11 import net.sf.openrocket.logging.LogHelper;
12 import net.sf.openrocket.masscalc.BasicMassCalculator;
13 import net.sf.openrocket.models.atmosphere.AtmosphericModel;
14 import net.sf.openrocket.models.atmosphere.ExtendedISAModel;
15 import net.sf.openrocket.models.gravity.GravityModel;
16 import net.sf.openrocket.models.gravity.WGSGravityModel;
17 import net.sf.openrocket.models.wind.PinkNoiseWindModel;
18 import net.sf.openrocket.rocketcomponent.Rocket;
19 import net.sf.openrocket.startup.Application;
20 import net.sf.openrocket.util.BugException;
21 import net.sf.openrocket.util.ChangeSource;
22 import net.sf.openrocket.util.GeodeticComputationStrategy;
23 import net.sf.openrocket.util.MathUtil;
24 import net.sf.openrocket.util.StateChangeListener;
25 import net.sf.openrocket.util.Utils;
26 import net.sf.openrocket.util.WorldCoordinate;
29 * A class holding simulation options in basic parameter form and which functions
30 * as a ChangeSource. A SimulationConditions instance is generated from this class
31 * using {@link #toSimulationConditions()}.
33 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
35 public class SimulationOptions implements ChangeSource, Cloneable {
37 private static final LogHelper log = Application.getLogger();
39 public static final double MAX_LAUNCH_ROD_ANGLE = Math.PI / 3;
42 * The ISA standard atmosphere.
44 private static final AtmosphericModel ISA_ATMOSPHERIC_MODEL = new ExtendedISAModel();
47 private final Rocket rocket;
48 private String motorID = null;
52 * NOTE: When adding/modifying parameters, they must also be added to the
53 * equals and copyFrom methods!!
56 // TODO: HIGH: Fetch default values from Prefs!
58 private double launchRodLength = 1;
60 /** Launch rod angle > 0, radians from vertical */
61 private double launchRodAngle = 0;
63 /** Launch rod direction, 0 = upwind, PI = downwind. */
64 private double launchRodDirection = 0;
67 private double windAverage = 2.0;
68 private double windTurbulence = 0.1;
72 * SimulationOptions maintains the launch site parameters as separate double values,
73 * and converts them into a WorldCoordinate when converting to SimulationConditions.
75 private double launchAltitude = 0;
76 private double launchLatitude = 45;
77 private double launchLongitude = 0;
78 private GeodeticComputationStrategy geodeticComputation = GeodeticComputationStrategy.SPHERICAL;
80 private boolean useISA = true;
81 private double launchTemperature = ExtendedISAModel.STANDARD_TEMPERATURE;
82 private double launchPressure = ExtendedISAModel.STANDARD_PRESSURE;
85 private double timeStep = RK4SimulationStepper.RECOMMENDED_TIME_STEP;
86 private double maximumAngle = RK4SimulationStepper.RECOMMENDED_ANGLE_STEP;
88 private int randomSeed = new Random().nextInt();
90 private boolean calculateExtras = true;
93 private List<EventListener> listeners = new ArrayList<EventListener>();
97 public SimulationOptions(Rocket rocket) {
101 public Rocket getRocket() {
106 public String getMotorConfigurationID() {
111 * Set the motor configuration ID. This must be a valid motor configuration ID of
112 * the rocket, otherwise the configuration is set to <code>null</code>.
114 * @param id the configuration to set.
116 public void setMotorConfigurationID(String id) {
119 if (!rocket.isMotorConfigurationID(id))
128 public double getLaunchRodLength() {
129 return launchRodLength;
132 public void setLaunchRodLength(double launchRodLength) {
133 if (MathUtil.equals(this.launchRodLength, launchRodLength))
135 this.launchRodLength = launchRodLength;
140 public double getLaunchRodAngle() {
141 return launchRodAngle;
144 public void setLaunchRodAngle(double launchRodAngle) {
145 launchRodAngle = MathUtil.clamp(launchRodAngle, 0, MAX_LAUNCH_ROD_ANGLE);
146 if (MathUtil.equals(this.launchRodAngle, launchRodAngle))
148 this.launchRodAngle = launchRodAngle;
153 public double getLaunchRodDirection() {
154 return launchRodDirection;
157 public void setLaunchRodDirection(double launchRodDirection) {
158 launchRodDirection = MathUtil.reduce180(launchRodDirection);
159 if (MathUtil.equals(this.launchRodDirection, launchRodDirection))
161 this.launchRodDirection = launchRodDirection;
167 public double getWindSpeedAverage() {
171 public void setWindSpeedAverage(double windAverage) {
172 if (MathUtil.equals(this.windAverage, windAverage))
174 this.windAverage = MathUtil.max(windAverage, 0);
179 public double getWindSpeedDeviation() {
180 return windAverage * windTurbulence;
183 public void setWindSpeedDeviation(double windDeviation) {
184 if (windAverage < 0.1) {
187 setWindTurbulenceIntensity(windDeviation / windAverage);
192 * Return the wind turbulence intensity (standard deviation / average).
194 * @return the turbulence intensity
196 public double getWindTurbulenceIntensity() {
197 return windTurbulence;
201 * Set the wind standard deviation to match the given turbulence intensity.
203 * @param intensity the turbulence intensity
205 public void setWindTurbulenceIntensity(double intensity) {
206 // Does not check equality so that setWindSpeedDeviation can be sure of event firing
207 this.windTurbulence = intensity;
215 public double getLaunchAltitude() {
216 return launchAltitude;
219 public void setLaunchAltitude(double altitude) {
220 if (MathUtil.equals(this.launchAltitude, altitude))
222 this.launchAltitude = altitude;
227 public double getLaunchLatitude() {
228 return launchLatitude;
231 public void setLaunchLatitude(double launchLatitude) {
232 launchLatitude = MathUtil.clamp(launchLatitude, -90, 90);
233 if (MathUtil.equals(this.launchLatitude, launchLatitude))
235 this.launchLatitude = launchLatitude;
239 public double getLaunchLongitude() {
240 return launchLongitude;
243 public void setLaunchLongitude(double launchLongitude) {
244 launchLongitude = MathUtil.clamp(launchLongitude, -180, 180);
245 if (MathUtil.equals(this.launchLongitude, launchLongitude))
247 this.launchLongitude = launchLongitude;
252 public GeodeticComputationStrategy getGeodeticComputation() {
253 return geodeticComputation;
256 public void setGeodeticComputation(GeodeticComputationStrategy geodeticComputation) {
257 if (this.geodeticComputation == geodeticComputation)
259 if (geodeticComputation == null) {
260 throw new IllegalArgumentException("strategy cannot be null");
262 this.geodeticComputation = geodeticComputation;
267 public boolean isISAAtmosphere() {
271 public void setISAAtmosphere(boolean isa) {
279 public double getLaunchTemperature() {
280 return launchTemperature;
285 public void setLaunchTemperature(double launchTemperature) {
286 if (MathUtil.equals(this.launchTemperature, launchTemperature))
288 this.launchTemperature = launchTemperature;
294 public double getLaunchPressure() {
295 return launchPressure;
300 public void setLaunchPressure(double launchPressure) {
301 if (MathUtil.equals(this.launchPressure, launchPressure))
303 this.launchPressure = launchPressure;
309 * Returns an atmospheric model corresponding to the launch conditions. The
310 * atmospheric models may be shared between different calls.
312 * @return an AtmosphericModel object.
314 private AtmosphericModel getAtmosphericModel() {
316 return ISA_ATMOSPHERIC_MODEL;
318 return new ExtendedISAModel(getLaunchAltitude(), launchTemperature, launchPressure);
322 public double getTimeStep() {
326 public void setTimeStep(double timeStep) {
327 if (MathUtil.equals(this.timeStep, timeStep))
329 this.timeStep = timeStep;
333 public double getMaximumStepAngle() {
337 public void setMaximumStepAngle(double maximumAngle) {
338 maximumAngle = MathUtil.clamp(maximumAngle, 1 * Math.PI / 180, 20 * Math.PI / 180);
339 if (MathUtil.equals(this.maximumAngle, maximumAngle))
341 this.maximumAngle = maximumAngle;
347 public boolean getCalculateExtras() {
348 return calculateExtras;
353 public void setCalculateExtras(boolean calculateExtras) {
354 if (this.calculateExtras == calculateExtras)
356 this.calculateExtras = calculateExtras;
362 public int getRandomSeed() {
366 public void setRandomSeed(int randomSeed) {
367 if (this.randomSeed == randomSeed) {
370 this.randomSeed = randomSeed;
372 * This does not fire an event since we don't want to invalidate simulation results
373 * due to changing the seed value. This needs to be revisited if the user is ever
374 * allowed to select the seed value.
376 // fireChangeEvent();
380 * Randomize the random seed value.
382 public void randomizeSeed() {
383 this.randomSeed = new Random().nextInt();
384 // fireChangeEvent();
390 public SimulationOptions clone() {
392 SimulationOptions copy = (SimulationOptions) super.clone();
393 copy.listeners = new ArrayList<EventListener>();
395 } catch (CloneNotSupportedException e) {
396 throw new BugException(e);
401 public void copyFrom(SimulationOptions src) {
403 if (this.rocket == src.rocket) {
405 this.motorID = src.motorID;
409 if (src.rocket.hasMotors(src.motorID)) {
410 // Try to find a matching motor ID
411 String motorDesc = src.rocket.getMotorConfigurationDescription(src.motorID);
412 String matchID = null;
414 for (String id : this.rocket.getMotorConfigurationIDs()) {
415 if (motorDesc.equals(this.rocket.getMotorConfigurationDescription(id))) {
421 this.motorID = matchID;
427 this.launchAltitude = src.launchAltitude;
428 this.launchLatitude = src.launchLatitude;
429 this.launchLongitude = src.launchLongitude;
430 this.launchPressure = src.launchPressure;
431 this.launchRodAngle = src.launchRodAngle;
432 this.launchRodDirection = src.launchRodDirection;
433 this.launchRodLength = src.launchRodLength;
434 this.launchTemperature = src.launchTemperature;
435 this.maximumAngle = src.maximumAngle;
436 this.timeStep = src.timeStep;
437 this.windAverage = src.windAverage;
438 this.windTurbulence = src.windTurbulence;
439 this.calculateExtras = src.calculateExtras;
440 this.randomSeed = src.randomSeed;
448 * Compares whether the two simulation conditions are equal. The two are considered
449 * equal if the rocket, motor id and all variables are equal.
452 public boolean equals(Object other) {
453 if (!(other instanceof SimulationOptions))
455 SimulationOptions o = (SimulationOptions) other;
456 return ((this.rocket == o.rocket) &&
457 Utils.equals(this.motorID, o.motorID) &&
458 MathUtil.equals(this.launchAltitude, o.launchAltitude) &&
459 MathUtil.equals(this.launchLatitude, o.launchLatitude) &&
460 MathUtil.equals(this.launchLongitude, o.launchLongitude) &&
461 MathUtil.equals(this.launchPressure, o.launchPressure) &&
462 MathUtil.equals(this.launchRodAngle, o.launchRodAngle) &&
463 MathUtil.equals(this.launchRodDirection, o.launchRodDirection) &&
464 MathUtil.equals(this.launchRodLength, o.launchRodLength) &&
465 MathUtil.equals(this.launchTemperature, o.launchTemperature) &&
466 MathUtil.equals(this.maximumAngle, o.maximumAngle) &&
467 MathUtil.equals(this.timeStep, o.timeStep) &&
468 MathUtil.equals(this.windAverage, o.windAverage) &&
469 MathUtil.equals(this.windTurbulence, o.windTurbulence) &&
470 this.calculateExtras == o.calculateExtras && this.randomSeed == o.randomSeed);
474 * Hashcode method compatible with {@link #equals(Object)}.
477 public int hashCode() {
479 return rocket.hashCode();
480 return rocket.hashCode() + motorID.hashCode();
484 public void addChangeListener(EventListener listener) {
485 listeners.add(listener);
489 public void removeChangeListener(EventListener listener) {
490 listeners.remove(listener);
493 private final EventObject event = new EventObject(this);
495 private void fireChangeEvent() {
497 // Copy the list before iterating to prevent concurrent modification exceptions.
498 EventListener[] list = listeners.toArray(new EventListener[0]);
499 for (EventListener l : list) {
500 if ( l instanceof StateChangeListener ) {
501 ((StateChangeListener)l).stateChanged(event);
507 // TODO: HIGH: Clean up
508 public SimulationConditions toSimulationConditions() {
509 SimulationConditions conditions = new SimulationConditions();
511 conditions.setRocket((Rocket) getRocket().copy());
512 conditions.setMotorConfigurationID(getMotorConfigurationID());
513 conditions.setLaunchRodLength(getLaunchRodLength());
514 conditions.setLaunchRodAngle(getLaunchRodAngle());
515 conditions.setLaunchRodDirection(getLaunchRodDirection());
516 conditions.setLaunchSite(new WorldCoordinate(getLaunchLatitude(), getLaunchLongitude(), getLaunchAltitude()));
517 conditions.setGeodeticComputation(getGeodeticComputation());
518 conditions.setRandomSeed(randomSeed);
520 PinkNoiseWindModel windModel = new PinkNoiseWindModel(randomSeed);
521 windModel.setAverage(getWindSpeedAverage());
522 windModel.setStandardDeviation(getWindSpeedDeviation());
523 conditions.setWindModel(windModel);
525 conditions.setAtmosphericModel(getAtmosphericModel());
527 GravityModel gravityModel = new WGSGravityModel();
529 conditions.setGravityModel(gravityModel);
531 conditions.setAerodynamicCalculator(new BarrowmanCalculator());
532 conditions.setMassCalculator(new BasicMassCalculator());
534 conditions.setTimeStep(getTimeStep());
535 conditions.setMaximumAngleStep(getMaximumStepAngle());
537 conditions.setCalculateExtras(getCalculateExtras());