1 package net.sf.openrocket.aerodynamics.barrowman;
3 import static java.lang.Math.*;
4 import static net.sf.openrocket.util.MathUtil.pow2;
6 import java.util.Arrays;
8 import net.sf.openrocket.aerodynamics.AerodynamicForces;
9 import net.sf.openrocket.aerodynamics.FlightConditions;
10 import net.sf.openrocket.aerodynamics.Warning;
11 import net.sf.openrocket.aerodynamics.WarningSet;
12 import net.sf.openrocket.rocketcomponent.FinSet;
13 import net.sf.openrocket.rocketcomponent.RocketComponent;
14 import net.sf.openrocket.util.BugException;
15 import net.sf.openrocket.util.Coordinate;
16 import net.sf.openrocket.util.LinearInterpolator;
17 import net.sf.openrocket.util.MathUtil;
18 import net.sf.openrocket.util.PolyInterpolator;
21 public class FinSetCalc extends RocketComponentCalc {
22 private static final double STALL_ANGLE = (20 * Math.PI/180);
24 /** Number of divisions in the fin chords. */
25 protected static final int DIVISIONS = 48;
30 private FinSet component;
33 protected double macLength = Double.NaN; // MAC length
34 protected double macLead = Double.NaN; // MAC leading edge position
35 protected double macSpan = Double.NaN; // MAC spanwise position
36 protected double finArea = Double.NaN; // Fin area
37 protected double ar = Double.NaN; // Fin aspect ratio
38 protected double span = Double.NaN; // Fin span
39 protected double cosGamma = Double.NaN; // Cosine of midchord sweep angle
40 protected double cosGammaLead = Double.NaN; // Cosine of leading edge sweep angle
41 protected double rollSum = Double.NaN; // Roll damping sum term
43 private int interferenceFinCount = -1; // No. of fins in interference
45 protected double[] chordLead = new double[DIVISIONS];
46 protected double[] chordTrail = new double[DIVISIONS];
47 protected double[] chordLength = new double[DIVISIONS];
50 protected final WarningSet geometryWarnings = new WarningSet();
52 private double[] poly = new double[6];
55 public FinSetCalc(RocketComponent component) {
57 if (!(component instanceof FinSet)) {
58 throw new IllegalArgumentException("Illegal component type "+component);
60 this.component = (FinSet) component;
65 * Calculates the non-axial forces produced by the fins (normal and side forces,
66 * pitch, yaw and roll moments, CP position, CNa).
69 public void calculateNonaxialForces(FlightConditions conditions,
70 AerodynamicForces forces, WarningSet warnings) {
72 // Compute and cache the fin geometry
73 if (Double.isNaN(macLength)) {
74 calculateFinGeometry();
82 forces.cp = Coordinate.NUL;
85 forces.CrollForce = 0;
92 // Add warnings (radius/2 == diameter/4)
93 if (component.getThickness() > component.getBodyRadius()/2) {
94 warnings.add(Warning.THICK_FIN);
96 warnings.addAll(geometryWarnings);
100 //////// Calculate CNa. /////////
102 // One fin without interference (both sub- and supersonic):
103 double cna1 = calculateFinCNa1(conditions);
107 // Multiple fins with fin-fin interference
110 // TODO: MEDIUM: Take into account multiple fin sets
111 int fins = component.getFinCount();
112 int interferenceFins = getInterferenceFinCount();
113 double theta = conditions.getTheta();
114 double angle = component.getBaseRotation();
117 // Compute basic CNa without interference effects
118 if (fins == 1 || fins == 2) {
119 // Basic CNa from geometry
121 for (int i=0; i < fins; i++) {
122 mul += MathUtil.pow2(Math.sin(theta - angle));
123 angle += 2 * Math.PI / fins;
127 // Basic CNa assuming full efficiency
128 cna = cna1 * fins/2.0;
132 // Take into account fin-fin interference effects
133 switch (interferenceFins) {
138 // No interference effect
158 // Assume 75% efficiency
160 warnings.add("Too many parallel fins");
165 * Used in 0.9.5 and earlier. Takes into account rotation angle for three
166 * and four fins, does not take into account interference from other fin sets.
173 for (int i=0; i < fins; i++) {
174 mul += MathUtil.pow2(Math.sin(theta - angle));
175 angle += 2 * Math.PI / fins;
181 // multiplier 1.5, sinusoidal reduction of 15%
182 cna = cna1 * 1.5 * (1 - 0.15*pow2(Math.cos(1.5 * (theta-angle))));
186 // multiplier 2.0, sinusoidal reduction of 6%
187 cna = cna1 * 2.0 * (1 - 0.06*pow2(Math.sin(2 * (theta-angle))));
207 // Assume N/2 * 3/4 efficiency for more fins
208 cna = cna1 * fins * 3.0/8.0;
213 // Body-fin interference effect
214 double r = component.getBodyRadius();
215 double tau = r / (span+r);
216 if (Double.isNaN(tau) || Double.isInfinite(tau))
218 cna *= 1 + tau; // Classical Barrowman
219 // cna *= pow2(1 + tau); // Barrowman thesis (too optimistic??)
223 // TODO: LOW: check for fin tip mach cone interference
224 // (Barrowman thesis pdf-page 40)
226 // TODO: LOW: fin-fin mach cone effect, MIL-HDBK page 5-25
230 // Calculate CP position
231 double x = macLead + calculateCPPos(conditions) * macLength;
235 // Calculate roll forces, reduce forcing above stall angle
237 // Without body-fin interference effect:
238 // forces.CrollForce = fins * (macSpan+r) * cna1 * component.getCantAngle() /
239 // conditions.getRefLength();
240 // With body-fin interference effect:
241 forces.CrollForce = fins * (macSpan+r) * cna1 * (1+tau) * component.getCantAngle() /
242 conditions.getRefLength();
247 if (conditions.getAOA() > STALL_ANGLE) {
248 // System.out.println("Fin stalling in roll");
249 forces.CrollForce *= MathUtil.clamp(
250 1-(conditions.getAOA() - STALL_ANGLE)/(STALL_ANGLE/2), 0, 1);
252 forces.CrollDamp = calculateDampingMoment(conditions);
253 forces.Croll = forces.CrollForce - forces.CrollDamp;
257 // System.out.printf(component.getName() + ": roll rate:%.3f force:%.3f damp:%.3f " +
259 // conditions.getRollRate(), forces.CrollForce, forces.CrollDamp, forces.Croll);
262 forces.CN = cna * MathUtil.min(conditions.getAOA(), STALL_ANGLE);
263 forces.cp = new Coordinate(x, 0, 0, cna);
264 forces.Cm = forces.CN * x / conditions.getRefLength();
267 * TODO: HIGH: Compute actual side force and yaw moment.
268 * This is not currently performed because it produces strange results for
269 * stable rockets that have two fins in the front part of the fuselage,
270 * where the rocket flies at an ever-increasing angle of attack. This may
271 * be due to incorrect computation of pitch/yaw damping moments.
273 // if (fins == 1 || fins == 2) {
274 // forces.Cside = fins * cna1 * Math.cos(theta-angle) * Math.sin(theta-angle);
275 // forces.Cyaw = fins * forces.Cside * x / conditions.getRefLength();
288 * Returns the MAC length of the fin. This is required in the friction drag
291 * @return the MAC length of the fin.
293 public double getMACLength() {
294 // Compute and cache the fin geometry
295 if (Double.isNaN(macLength)) {
296 calculateFinGeometry();
303 public double getMidchordPos() {
304 // Compute and cache the fin geometry
305 if (Double.isNaN(macLength)) {
306 calculateFinGeometry();
310 return macLead + 0.5 * macLength;
316 * Pre-calculates the fin geometry values.
318 protected void calculateFinGeometry() {
320 span = component.getSpan();
321 finArea = component.getFinArea();
322 ar = 2 * pow2(span) / finArea;
324 Coordinate[] points = component.getFinPoints();
326 // Check for jagged edges
327 geometryWarnings.clear();
328 boolean down = false;
329 for (int i=1; i < points.length; i++) {
330 if ((points[i].y > points[i-1].y + 0.001) && down) {
331 geometryWarnings.add(Warning.JAGGED_EDGED_FIN);
334 if (points[i].y < points[i-1].y - 0.001) {
340 // Calculate the chord lead and trail positions and length
342 Arrays.fill(chordLead, Double.POSITIVE_INFINITY);
343 Arrays.fill(chordTrail, Double.NEGATIVE_INFINITY);
344 Arrays.fill(chordLength, 0);
346 for (int point=1; point < points.length; point++) {
347 double x1 = points[point-1].x;
348 double y1 = points[point-1].y;
349 double x2 = points[point].x;
350 double y2 = points[point].y;
352 if (MathUtil.equals(y1, y2))
355 int i1 = (int)(y1*1.0001/span*(DIVISIONS-1));
356 int i2 = (int)(y2*1.0001/span*(DIVISIONS-1));
357 i1 = MathUtil.clamp(i1, 0, DIVISIONS-1);
358 i2 = MathUtil.clamp(i2, 0, DIVISIONS-1);
365 for (int i = i1; i <= i2; i++) {
366 // Intersection point (x,y)
367 double y = i*span/(DIVISIONS-1);
368 double x = (y-y2)/(y1-y2)*x1 + (y1-y)/(y1-y2)*x2;
369 if (x < chordLead[i])
371 if (x > chordTrail[i])
374 // TODO: LOW: If fin point exactly on chord line, might be counted twice:
383 // Check and correct any inconsistencies
384 for (int i=0; i < DIVISIONS; i++) {
385 if (Double.isInfinite(chordLead[i]) || Double.isInfinite(chordTrail[i]) ||
386 Double.isNaN(chordLead[i]) || Double.isNaN(chordTrail[i])) {
390 if (chordLength[i] < 0 || Double.isNaN(chordLength[i])) {
393 if (chordLength[i] > chordTrail[i] - chordLead[i]) {
394 chordLength[i] = chordTrail[i] - chordLead[i];
399 /* Calculate fin properties:
401 * macLength // MAC length
402 * macLead // MAC leading edge position
403 * macSpan // MAC spanwise position
404 * ar // Fin aspect ratio (already set)
405 * span // Fin span (already set)
414 double radius = component.getBodyRadius();
416 final double dy = span/(DIVISIONS-1);
417 for (int i=0; i < DIVISIONS; i++) {
418 double length = chordTrail[i] - chordLead[i];
421 macLength += length * length;
422 macSpan += y * length;
423 macLead += chordLead[i] * length;
425 rollSum += chordLength[i] * pow2(radius + y);
428 double dx = (chordTrail[i]+chordLead[i])/2 - (chordTrail[i-1]+chordLead[i-1])/2;
429 cosGamma += dy/MathUtil.hypot(dx, dy);
431 dx = chordLead[i] - chordLead[i-1];
432 cosGammaLead += dy/MathUtil.hypot(dx, dy);
445 cosGamma /= (DIVISIONS-1);
446 cosGammaLead /= (DIVISIONS-1);
450 /////////////// CNa1 calculation ////////////////
452 private static final double CNA_SUBSONIC = 0.9;
453 private static final double CNA_SUPERSONIC = 1.5;
454 private static final double CNA_SUPERSONIC_B = pow(pow2(CNA_SUPERSONIC)-1, 1.5);
455 private static final double GAMMA = 1.4;
456 private static final LinearInterpolator K1, K2, K3;
457 private static final PolyInterpolator cnaInterpolator = new PolyInterpolator(
458 new double[] { CNA_SUBSONIC, CNA_SUPERSONIC },
459 new double[] { CNA_SUBSONIC, CNA_SUPERSONIC },
460 new double[] { CNA_SUBSONIC }
462 /* Pre-calculate the values for K1, K2 and K3 */
465 int n = (int)((5.0-CNA_SUPERSONIC)*10);
466 double[] x = new double[n];
467 double[] k1 = new double[n];
468 double[] k2 = new double[n];
469 double[] k3 = new double[n];
470 for (int i=0; i<n; i++) {
471 double M = CNA_SUPERSONIC + i*0.1;
472 double beta = sqrt(M*M - 1);
475 k2[i] = ((GAMMA+1)*pow(M, 4) - 4*pow2(beta)) / (4*pow(beta,4));
476 k3[i] = ((GAMMA+1)*pow(M, 8) + (2*pow2(GAMMA) - 7*GAMMA - 5) * pow(M,6) +
477 10*(GAMMA+1)*pow(M,4) + 8) / (6*pow(beta,7));
479 K1 = new LinearInterpolator(x,k1);
480 K2 = new LinearInterpolator(x,k2);
481 K3 = new LinearInterpolator(x,k3);
483 // System.out.println("K1[m="+CNA_SUPERSONIC+"] = "+k1[0]);
484 // System.out.println("K2[m="+CNA_SUPERSONIC+"] = "+k2[0]);
485 // System.out.println("K3[m="+CNA_SUPERSONIC+"] = "+k3[0]);
489 protected double calculateFinCNa1(FlightConditions conditions) {
490 double mach = conditions.getMach();
491 double ref = conditions.getRefArea();
492 double alpha = MathUtil.min(conditions.getAOA(),
493 Math.PI - conditions.getAOA(), STALL_ANGLE);
496 if (mach <= CNA_SUBSONIC) {
497 return 2*Math.PI*pow2(span)/(1 + sqrt(1 + (1-pow2(mach))*
498 pow2(pow2(span)/(finArea*cosGamma)))) / ref;
502 if (mach >= CNA_SUPERSONIC) {
503 return finArea * (K1.getValue(mach) + K2.getValue(mach)*alpha +
504 K3.getValue(mach)*pow2(alpha)) / ref;
507 // Transonic case, interpolate
511 double sq = sqrt(1 + (1-pow2(CNA_SUBSONIC)) * pow2(span*span/(finArea*cosGamma)));
512 subV = 2*Math.PI*pow2(span)/ref / (1+sq);
513 subD = 2*mach*Math.PI*pow(span,6) / (pow2(finArea*cosGamma) * ref *
516 superV = finArea * (K1.getValue(CNA_SUPERSONIC) + K2.getValue(CNA_SUPERSONIC)*alpha +
517 K3.getValue(CNA_SUPERSONIC)*pow2(alpha)) / ref;
518 superD = -finArea/ref * 2*CNA_SUPERSONIC / CNA_SUPERSONIC_B;
520 // System.out.println("subV="+subV+" superV="+superV+" subD="+subD+" superD="+superD);
522 return cnaInterpolator.interpolate(mach, subV, superV, subD, superD, 0);
528 private double calculateDampingMoment(FlightConditions conditions) {
529 double rollRate = conditions.getRollRate();
531 if (Math.abs(rollRate) < 0.1)
534 double mach = conditions.getMach();
535 double radius = component.getBodyRadius();
536 double absRate = Math.abs(rollRate);
540 * At low speeds and relatively large roll rates (i.e. near apogee) the
541 * fin tips rotate well above stall angle. In this case sum the chords
544 if (absRate * (radius + span) / conditions.getVelocity() > 15*Math.PI/180) {
546 for (int i=0; i < DIVISIONS; i++) {
547 double dist = radius + span*i/DIVISIONS;
548 double aoa = Math.min(absRate*dist/conditions.getVelocity(), 15*Math.PI/180);
549 sum += chordLength[i] * dist * aoa;
551 sum = sum * (span/DIVISIONS) * 2*Math.PI/conditions.getBeta() /
552 (conditions.getRefArea() * conditions.getRefLength());
554 // System.out.println("SPECIAL: " +
555 // (MathUtil.sign(rollRate) *component.getFinCount() * sum));
556 return MathUtil.sign(rollRate) * component.getFinCount() * sum;
561 if (mach <= CNA_SUBSONIC) {
562 // System.out.println("BASIC: "+
563 // (component.getFinCount() * 2*Math.PI * rollRate * rollSum /
564 // (conditions.getRefArea() * conditions.getRefLength() *
565 // conditions.getVelocity() * conditions.getBeta())));
567 return component.getFinCount() * 2*Math.PI * rollRate * rollSum /
568 (conditions.getRefArea() * conditions.getRefLength() *
569 conditions.getVelocity() * conditions.getBeta());
571 if (mach >= CNA_SUPERSONIC) {
573 double vel = conditions.getVelocity();
574 double k1 = K1.getValue(mach);
575 double k2 = K2.getValue(mach);
576 double k3 = K3.getValue(mach);
580 for (int i=0; i < DIVISIONS; i++) {
581 double y = i*span/(DIVISIONS-1);
582 double angle = rollRate * (radius+y) / vel;
584 sum += (k1 * angle + k2 * angle*angle + k3 * angle*angle*angle)
585 * chordLength[i] * (radius+y);
588 return component.getFinCount() * sum * span/(DIVISIONS-1) /
589 (conditions.getRefArea() * conditions.getRefLength());
592 // Transonic, do linear interpolation
594 FlightConditions cond = conditions.clone();
595 cond.setMach(CNA_SUBSONIC - 0.01);
596 double subsonic = calculateDampingMoment(cond);
597 cond.setMach(CNA_SUPERSONIC + 0.01);
598 double supersonic = calculateDampingMoment(cond);
600 return subsonic * (CNA_SUPERSONIC - mach)/(CNA_SUPERSONIC - CNA_SUBSONIC) +
601 supersonic * (mach - CNA_SUBSONIC)/(CNA_SUPERSONIC - CNA_SUBSONIC);
608 * Return the relative position of the CP along the mean aerodynamic chord.
609 * Below mach 0.5 it is at the quarter chord, above mach 2 calculated using an
610 * empirical formula, between these two using an interpolation polynomial.
612 * @param cond Mach speed used
613 * @return CP position along the MAC
615 private double calculateCPPos(FlightConditions cond) {
616 double m = cond.getMach();
618 // At subsonic speeds CP at quarter chord
622 // At supersonic speeds use empirical formula
623 double beta = cond.getBeta();
624 return (ar * beta - 0.67) / (2*ar*beta - 1);
627 // In between use interpolation polynomial
631 for (int i=0; i < poly.length; i++) {
640 * Calculate CP position interpolation polynomial coefficients from the
641 * fin geometry. This is a fifth order polynomial that satisfies
650 * where f(M) = (ar*sqrt(M^2-1) - 0.67) / (2*ar*sqrt(M^2-1) - 1).
652 * The values were calculated analytically in Mathematica. The coefficients
653 * are used as poly[0] + poly[1]*x + poly[2]*x^2 + ...
655 private void calculatePoly() {
656 double denom = pow2(1 - 3.4641*ar); // common denominator
658 poly[5] = (-1.58025 * (-0.728769 + ar) * (-0.192105 + ar)) / denom;
659 poly[4] = (12.8395 * (-0.725688 + ar) * (-0.19292 + ar)) / denom;
660 poly[3] = (-39.5062 * (-0.72074 + ar) * (-0.194245 + ar)) / denom;
661 poly[2] = (55.3086 * (-0.711482 + ar) * (-0.196772 + ar)) / denom;
662 poly[1] = (-31.6049 * (-0.705375 + ar) * (-0.198476 + ar)) / denom;
663 poly[0] = (9.16049 * (-0.588838 + ar) * (-0.20624 + ar)) / denom;
667 // @SuppressWarnings("null")
668 // public static void main(String arg[]) {
669 // Rocket rocket = TestRocket.makeRocket();
670 // FinSet finset = null;
672 // Iterator<RocketComponent> iter = rocket.deepIterator();
673 // while (iter.hasNext()) {
674 // RocketComponent c = iter.next();
675 // if (c instanceof FinSet) {
676 // finset = (FinSet)c;
681 // ((TrapezoidFinSet)finset).setHeight(0.10);
682 // ((TrapezoidFinSet)finset).setRootChord(0.10);
683 // ((TrapezoidFinSet)finset).setTipChord(0.10);
684 // ((TrapezoidFinSet)finset).setSweep(0.0);
687 // FinSetCalc calc = new FinSetCalc(finset);
689 // calc.calculateFinGeometry();
690 // FlightConditions cond = new FlightConditions(new Configuration(rocket));
691 // for (double m=0; m < 3; m+=0.05) {
693 // cond.setAOA(0.0*Math.PI/180);
694 // double cna = calc.calculateFinCNa1(cond);
695 // System.out.printf("%5.2f "+cna+"\n", m);
702 public double calculatePressureDragForce(FlightConditions conditions,
703 double stagnationCD, double baseCD, WarningSet warnings) {
705 // Compute and cache the fin geometry
706 if (Double.isNaN(cosGammaLead)) {
707 calculateFinGeometry();
712 FinSet.CrossSection profile = component.getCrossSection();
713 double mach = conditions.getMach();
716 // Pressure fore-drag
717 if (profile == FinSet.CrossSection.AIRFOIL ||
718 profile == FinSet.CrossSection.ROUNDED) {
720 // Round leading edge
722 drag = Math.pow(1 - pow2(mach), -0.417) - 1;
723 } else if (mach < 1) {
724 drag = 1 - 1.785 * (mach-0.9);
726 drag = 1.214 - 0.502/pow2(mach) + 0.1095/pow2(pow2(mach));
729 } else if (profile == FinSet.CrossSection.SQUARE) {
732 throw new UnsupportedOperationException("Unsupported fin profile: "+profile);
735 // Slanted leading edge
736 drag *= pow2(cosGammaLead);
738 // Trailing edge drag
739 if (profile == FinSet.CrossSection.SQUARE) {
741 } else if (profile == FinSet.CrossSection.ROUNDED) {
744 // Airfoil assumed to have zero base drag
747 // Scale to correct reference area
748 drag *= component.getFinCount() * component.getSpan() * component.getThickness() /
749 conditions.getRefArea();
755 private int getInterferenceFinCount() {
756 if (interferenceFinCount < 1) {
758 RocketComponent parent = component.getParent();
759 if (parent == null) {
760 throw new IllegalStateException("fin set without parent component");
763 double lead = component.toRelative(Coordinate.NUL, parent)[0].x;
764 double trail = component.toRelative(new Coordinate(component.getLength()),
767 interferenceFinCount = 0;
768 for (RocketComponent c: parent.getChildren()) {
769 if (c instanceof FinSet) {
770 double finLead = c.toRelative(Coordinate.NUL, parent)[0].x;
771 double finTrail = c.toRelative(new Coordinate(c.getLength()), parent)[0].x;
772 if ((finLead < trail - 0.005) && (finTrail > lead + 0.005)) {
773 interferenceFinCount += ((FinSet)c).getFinCount();
778 if (interferenceFinCount < component.getFinCount()) {
779 throw new BugException("Counted " + interferenceFinCount + " parallel fins, " +
780 "when component itself has " + component.getFinCount());
783 return interferenceFinCount;