1 package net.sf.openrocket.aerodynamics;
3 import static net.sf.openrocket.util.MathUtil.pow2;
5 import java.util.Iterator;
8 import net.sf.openrocket.motor.Motor;
9 import net.sf.openrocket.rocketcomponent.Configuration;
10 import net.sf.openrocket.rocketcomponent.MotorMount;
11 import net.sf.openrocket.rocketcomponent.Rocket;
12 import net.sf.openrocket.rocketcomponent.RocketComponent;
13 import net.sf.openrocket.util.Coordinate;
14 import net.sf.openrocket.util.MathUtil;
18 * A class that is the base of all aerodynamical calculations.
20 * A {@link Configuration} object must be assigned to this class before any
21 * operations are allowed. This can be done using the constructor or using
22 * the {@link #setConfiguration(Configuration)} method. The default is a
23 * <code>null</code> configuration, in which case the calculation
24 * methods throw {@link NullPointerException}.
26 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
29 public abstract class AerodynamicCalculator {
31 private static final double MIN_MASS = 0.001 * MathUtil.EPSILON;
33 /** Number of divisions used when calculating worst CP. */
34 public static final int DIVISIONS = 360;
37 * A <code>WarningSet</code> that can be used if <code>null</code> is passed
38 * to a calculation method.
40 protected WarningSet ignoreWarningSet = new WarningSet();
43 * The <code>Rocket</code> currently being calculated.
45 protected Rocket rocket = null;
47 protected Configuration configuration = null;
51 * Cached data. All CG data is in absolute coordinates. All moments of inertia
52 * are relative to their respective CG.
54 private Coordinate[] cgCache = null;
55 private Coordinate[] origCG = null; // CG of non-overridden stage
56 private double longitudalInertiaCache[] = null;
57 private double rotationalInertiaCache[] = null;
60 // TODO: LOW: Do not void unnecessary data (mass/aero separately)
61 private int rocketModID = -1;
62 // private int aeroModID = -1;
63 // private int massModID = -1;
66 * No-options constructor. The rocket is left as <code>null</code>.
68 public AerodynamicCalculator() {
75 * A constructor that sets the Configuration to be used.
77 * @param config the configuration to use
79 public AerodynamicCalculator(Configuration config) {
80 setConfiguration(config);
85 public Configuration getConfiguration() {
89 public void setConfiguration(Configuration config) {
90 this.configuration = config;
91 this.rocket = config.getRocket();
95 public abstract AerodynamicCalculator newInstance();
98 ////////////////// Mass property calculations ///////////////////
102 * Get the CG and mass of the current configuration with motors at the specified
103 * time. The motor ignition times are taken from the configuration.
105 public Coordinate getCG(double time) {
108 totalCG = getEmptyCG();
110 Iterator<MotorMount> iterator = configuration.motorIterator();
111 while (iterator.hasNext()) {
112 MotorMount mount = iterator.next();
113 double ignition = configuration.getIgnitionTime(mount);
114 Motor motor = mount.getMotor(configuration.getMotorConfigurationID());
115 RocketComponent component = (RocketComponent) mount;
117 double position = (component.getLength() - motor.getLength()
118 + mount.getMotorOverhang());
120 for (Coordinate c: component.toAbsolute(motor.getCG(time-ignition).
121 add(position,0,0))) {
122 totalCG = totalCG.average(c);
131 * Get the CG and mass of the current configuration without motors.
133 * @return the CG of the configuration
135 public Coordinate getEmptyCG() {
138 if (cgCache == null) {
139 calculateStageCache();
142 Coordinate totalCG = null;
143 for (int stage: configuration.getActiveStages()) {
144 totalCG = cgCache[stage].average(totalCG);
148 totalCG = Coordinate.NUL;
155 * Return the longitudal inertia of the current configuration with motors at
156 * the specified time. The motor ignition times are taken from the configuration.
158 * @param time the time.
159 * @return the longitudal moment of inertia of the configuration.
161 public double getLongitudalInertia(double time) {
164 if (cgCache == null) {
165 calculateStageCache();
168 final Coordinate totalCG = getCG(time);
169 double totalInertia = 0;
172 for (int stage: configuration.getActiveStages()) {
173 Coordinate stageCG = cgCache[stage];
175 totalInertia += (longitudalInertiaCache[stage] +
176 stageCG.weight * MathUtil.pow2(stageCG.x - totalCG.x));
181 Iterator<MotorMount> iterator = configuration.motorIterator();
182 while (iterator.hasNext()) {
183 MotorMount mount = iterator.next();
184 double ignition = configuration.getIgnitionTime(mount);
185 Motor motor = mount.getMotor(configuration.getMotorConfigurationID());
186 RocketComponent component = (RocketComponent) mount;
188 double position = (component.getLength() - motor.getLength()
189 + mount.getMotorOverhang());
191 double inertia = motor.getLongitudalInertia(time - ignition);
192 for (Coordinate c: component.toAbsolute(motor.getCG(time-ignition).
193 add(position,0,0))) {
194 totalInertia += inertia + c.weight * MathUtil.pow2(c.x - totalCG.x);
203 * Return the rotational inertia of the configuration with motors at the specified time.
204 * The motor ignition times are taken from the configuration.
206 * @param time the time.
207 * @return the rotational moment of inertia of the configuration.
209 public double getRotationalInertia(double time) {
212 if (cgCache == null) {
213 calculateStageCache();
216 final Coordinate totalCG = getCG(time);
217 double totalInertia = 0;
220 for (int stage: configuration.getActiveStages()) {
221 Coordinate stageCG = cgCache[stage];
223 totalInertia += rotationalInertiaCache[stage] + stageCG.weight * (
224 MathUtil.pow2(stageCG.y-totalCG.y) + MathUtil.pow2(stageCG.z-totalCG.z)
230 Iterator<MotorMount> iterator = configuration.motorIterator();
231 while (iterator.hasNext()) {
232 MotorMount mount = iterator.next();
233 double ignition = configuration.getIgnitionTime(mount);
234 Motor motor = mount.getMotor(configuration.getMotorConfigurationID());
235 RocketComponent component = (RocketComponent) mount;
237 double position = (component.getLength() - motor.getLength()
238 + mount.getMotorOverhang());
240 double inertia = motor.getRotationalInertia(time - ignition);
241 for (Coordinate c: component.toAbsolute(motor.getCG(time-ignition).
242 add(position,0,0))) {
243 totalInertia += inertia + c.weight * (
244 MathUtil.pow2(c.y - totalCG.y) + MathUtil.pow2(c.z - totalCG.z)
254 private void calculateStageCache() {
255 int stages = rocket.getStageCount();
257 cgCache = new Coordinate[stages];
258 longitudalInertiaCache = new double[stages];
259 rotationalInertiaCache = new double[stages];
261 for (int i=0; i < stages; i++) {
262 RocketComponent stage = rocket.getChild(i);
263 MassData data = calculateAssemblyMassData(stage);
264 cgCache[i] = stage.toAbsolute(data.cg)[0];
265 longitudalInertiaCache[i] = data.longitudalInertia;
266 rotationalInertiaCache[i] = data.rotationalInetria;
273 // * Updates the stage CGs.
275 // private void calculateStageCGs() {
276 // int stages = rocket.getStageCount();
278 // cgCache = new Coordinate[stages];
279 // origCG = new Coordinate[stages];
281 // for (int i=0; i < stages; i++) {
282 // Stage stage = (Stage) rocket.getChild(i);
283 // Coordinate stageCG = null;
285 // Iterator<RocketComponent> iterator = stage.deepIterator();
286 // while (iterator.hasNext()) {
287 // RocketComponent component = iterator.next();
289 // for (Coordinate c: component.toAbsolute(component.getCG())) {
290 // stageCG = c.average(stageCG);
294 // if (stageCG == null)
295 // stageCG = Coordinate.NUL;
297 // origCG[i] = stageCG;
299 // if (stage.isMassOverridden()) {
300 // stageCG = stageCG.setWeight(stage.getOverrideMass());
302 // if (stage.isCGOverridden()) {
303 // stageCG = stageCG.setXYZ(stage.getOverrideCG());
306 //// System.out.println("Stage "+i+" CG:"+stageCG);
308 // cgCache[i] = stageCG;
313 // private Coordinate calculateCG(RocketComponent component) {
314 // Coordinate componentCG = Coordinate.NUL;
316 // // Compute CG of this component
317 // Coordinate cg = component.getCG();
318 // if (cg.weight < MIN_MASS)
319 // cg = cg.setWeight(MIN_MASS);
321 // for (Coordinate c: component.toAbsolute(cg)) {
322 // componentCG = componentCG.average(c);
325 // // Compute CG with subcomponents
326 // for (RocketComponent sibling: component.getChildren()) {
327 // componentCG = componentCG.average(calculateCG(sibling));
330 // // Override mass/CG if subcomponents are also overridden
331 // if (component.getOverrideSubcomponents()) {
332 // if (component.isMassOverridden()) {
333 // componentCG = componentCG.setWeight(
334 // MathUtil.max(component.getOverrideMass(), MIN_MASS));
336 // if (component.isCGOverridden()) {
337 // componentCG = componentCG.setXYZ(component.getOverrideCG());
341 // return componentCG;
346 // private void calculateStageInertias() {
347 // int stages = rocket.getStageCount();
349 // if (cgCache == null)
350 // calculateStageCGs();
352 // longitudalInertiaCache = new double[stages];
353 // rotationalInertiaCache = new double[stages];
355 // for (int i=0; i < stages; i++) {
356 // Coordinate stageCG = cgCache[i];
357 // double stageLongitudalInertia = 0;
358 // double stageRotationalInertia = 0;
360 // Iterator<RocketComponent> iterator = rocket.getChild(i).deepIterator();
361 // while (iterator.hasNext()) {
362 // RocketComponent component = iterator.next();
363 // double li = component.getLongitudalInertia();
364 // double ri = component.getRotationalInertia();
365 // double mass = component.getMass();
367 // for (Coordinate c: component.toAbsolute(component.getCG())) {
368 // stageLongitudalInertia += li + mass * MathUtil.pow2(c.x - stageCG.x);
369 // stageRotationalInertia += ri + mass * (MathUtil.pow2(c.y - stageCG.y) +
370 // MathUtil.pow2(c.z - stageCG.z));
374 // // Check for mass override of complete stage
375 // if ((origCG[i].weight != cgCache[i].weight) && origCG[i].weight > 0.0000001) {
376 // stageLongitudalInertia = (stageLongitudalInertia * cgCache[i].weight /
377 // origCG[i].weight);
378 // stageRotationalInertia = (stageRotationalInertia * cgCache[i].weight /
379 // origCG[i].weight);
382 // longitudalInertiaCache[i] = stageLongitudalInertia;
383 // rotationalInertiaCache[i] = stageRotationalInertia;
390 * Returns the mass and inertia data for this component and all subcomponents.
391 * The inertia is returned relative to the CG, and the CG is in the coordinates
392 * of the specified component, not global coordinates.
394 private MassData calculateAssemblyMassData(RocketComponent parent) {
395 MassData parentData = new MassData();
397 // Calculate data for this component
398 parentData.cg = parent.getComponentCG();
399 if (parentData.cg.weight < MIN_MASS)
400 parentData.cg = parentData.cg.setWeight(MIN_MASS);
403 // Override only this component's data
404 if (!parent.getOverrideSubcomponents()) {
405 if (parent.isMassOverridden())
406 parentData.cg = parentData.cg.setWeight(MathUtil.max(parent.getOverrideMass(),MIN_MASS));
407 if (parent.isCGOverridden())
408 parentData.cg = parentData.cg.setXYZ(parent.getOverrideCG());
411 parentData.longitudalInertia = parent.getLongitudalUnitInertia() * parentData.cg.weight;
412 parentData.rotationalInetria = parent.getRotationalUnitInertia() * parentData.cg.weight;
415 // Combine data for subcomponents
416 for (RocketComponent sibling: parent.getChildren()) {
417 Coordinate combinedCG;
420 // Compute data of sibling
421 MassData siblingData = calculateAssemblyMassData(sibling);
422 Coordinate[] siblingCGs = sibling.toRelative(siblingData.cg, parent);
424 for (Coordinate siblingCG: siblingCGs) {
426 // Compute CG of this + sibling
427 combinedCG = parentData.cg.average(siblingCG);
429 // Add effect of this CG change to parent inertia
430 dx2 = pow2(parentData.cg.x - combinedCG.x);
431 parentData.longitudalInertia += parentData.cg.weight * dx2;
433 dr2 = pow2(parentData.cg.y - combinedCG.y) + pow2(parentData.cg.z - combinedCG.z);
434 parentData.rotationalInetria += parentData.cg.weight * dr2;
437 // Add inertia of sibling
438 parentData.longitudalInertia += siblingData.longitudalInertia;
439 parentData.rotationalInetria += siblingData.rotationalInetria;
441 // Add effect of sibling CG change
442 dx2 = pow2(siblingData.cg.x - combinedCG.x);
443 parentData.longitudalInertia += siblingData.cg.weight * dx2;
445 dr2 = pow2(siblingData.cg.y - combinedCG.y) + pow2(siblingData.cg.z - combinedCG.z);
446 parentData.rotationalInetria += siblingData.cg.weight * dr2;
449 parentData.cg = combinedCG;
453 // Override total data
454 if (parent.getOverrideSubcomponents()) {
455 if (parent.isMassOverridden()) {
456 double oldMass = parentData.cg.weight;
457 double newMass = MathUtil.max(parent.getOverrideMass(), MIN_MASS);
458 parentData.longitudalInertia = parentData.longitudalInertia * newMass / oldMass;
459 parentData.rotationalInetria = parentData.rotationalInetria * newMass / oldMass;
460 parentData.cg = parentData.cg.setWeight(newMass);
462 if (parent.isCGOverridden()) {
463 double oldx = parentData.cg.x;
464 double newx = parent.getOverrideCGX();
465 parentData.longitudalInertia += parentData.cg.weight * pow2(oldx - newx);
466 parentData.cg = parentData.cg.setX(newx);
474 private static class MassData {
475 public Coordinate cg = Coordinate.NUL;
476 public double longitudalInertia = 0;
477 public double rotationalInetria = 0;
484 //////////////// Aerodynamic calculators ////////////////
486 public abstract Coordinate getCP(FlightConditions conditions, WarningSet warnings);
489 public abstract List<AerodynamicForces> getCPAnalysis(FlightConditions conditions,
490 WarningSet warnings);
493 public abstract Map<RocketComponent, AerodynamicForces>
494 getForceAnalysis(FlightConditions conditions, WarningSet warnings);
496 public abstract AerodynamicForces getAerodynamicForces(double time,
497 FlightConditions conditions, WarningSet warnings);
500 /* Calculate only axial forces (and do not warn about insane AOA etc) */
501 public abstract AerodynamicForces getAxialForces(double time,
502 FlightConditions conditions, WarningSet warnings);
506 public Coordinate getWorstCP() {
507 return getWorstCP(new FlightConditions(configuration), ignoreWarningSet);
511 * The worst theta angle is stored in conditions.
513 public Coordinate getWorstCP(FlightConditions conditions, WarningSet warnings) {
514 FlightConditions cond = conditions.clone();
515 Coordinate worst = new Coordinate(Double.MAX_VALUE);
519 for (int i=0; i < DIVISIONS; i++) {
520 cond.setTheta(2*Math.PI*i/DIVISIONS);
521 cp = getCP(cond, warnings);
522 if (cp.x < worst.x) {
524 theta = cond.getTheta();
528 conditions.setTheta(theta);
538 * Check the current cache consistency. This method must be called by all
539 * methods that may use any cached data before any other operations are
540 * performed. If the rocket has changed since the previous call to
541 * <code>checkCache()</code>, then either {@link #voidAerodynamicCache()} or
542 * {@link #voidMassCache()} (or both) are called.
544 * This method performs the checking based on the rocket's modification IDs,
545 * so that these method may be called from listeners of the rocket itself.
547 protected final void checkCache() {
548 if (rocketModID != rocket.getModID()) {
549 rocketModID = rocket.getModID();
551 voidAerodynamicCache();
557 * Void cached mass data. This method is called whenever a change occurs in
558 * the rocket structure that affects the mass of the rocket and when a new
559 * Rocket is set. This method must be overridden to void any cached data
560 * necessary. The method must call <code>super.voidMassCache()</code> during its
563 protected void voidMassCache() {
565 longitudalInertiaCache = null;
566 rotationalInertiaCache = null;
570 * Void cached aerodynamic data. This method is called whenever a change occurs in
571 * the rocket structure that affects the aerodynamics of the rocket and when a new
572 * Rocket is set. This method must be overridden to void any cached data
573 * necessary. The method must call <code>super.voidAerodynamicCache()</code> during
576 protected void voidAerodynamicCache() {