1 package net.sf.openrocket.aerodynamics.barrowman;
3 import static java.lang.Math.pow;
4 import static java.lang.Math.sqrt;
5 import static net.sf.openrocket.util.MathUtil.pow2;
7 import java.util.Arrays;
8 import java.util.Iterator;
10 import net.sf.openrocket.aerodynamics.AerodynamicForces;
11 import net.sf.openrocket.aerodynamics.FlightConditions;
12 import net.sf.openrocket.aerodynamics.Warning;
13 import net.sf.openrocket.aerodynamics.WarningSet;
14 import net.sf.openrocket.rocketcomponent.Configuration;
15 import net.sf.openrocket.rocketcomponent.FinSet;
16 import net.sf.openrocket.rocketcomponent.Rocket;
17 import net.sf.openrocket.rocketcomponent.RocketComponent;
18 import net.sf.openrocket.rocketcomponent.TrapezoidFinSet;
19 import net.sf.openrocket.util.Coordinate;
20 import net.sf.openrocket.util.LinearInterpolator;
21 import net.sf.openrocket.util.MathUtil;
22 import net.sf.openrocket.util.PolyInterpolator;
23 import net.sf.openrocket.util.Test;
26 public class FinSetCalc extends RocketComponentCalc {
27 private static final double STALL_ANGLE = (20 * Math.PI/180);
29 /** Number of divisions in the fin chords. */
30 protected static final int DIVISIONS = 48;
35 private FinSet component;
38 protected double macLength = Double.NaN; // MAC length
39 protected double macLead = Double.NaN; // MAC leading edge position
40 protected double macSpan = Double.NaN; // MAC spanwise position
41 protected double finArea = Double.NaN; // Fin area
42 protected double ar = Double.NaN; // Fin aspect ratio
43 protected double span = Double.NaN; // Fin span
44 protected double cosGamma = Double.NaN; // Cosine of midchord sweep angle
45 protected double cosGammaLead = Double.NaN; // Cosine of leading edge sweep angle
46 protected double rollSum = Double.NaN; // Roll damping sum term
48 protected double[] chordLead = new double[DIVISIONS];
49 protected double[] chordTrail = new double[DIVISIONS];
50 protected double[] chordLength = new double[DIVISIONS];
52 protected final WarningSet geometryWarnings = new WarningSet();
54 private double[] poly = new double[6];
57 public FinSetCalc(RocketComponent component) {
59 if (!(component instanceof FinSet)) {
60 throw new IllegalArgumentException("Illegal component type "+component);
62 this.component = (FinSet) component;
67 * Calculates the non-axial forces produced by the fins (normal and side forces,
68 * pitch, yaw and roll moments, CP position, CNa).
71 public void calculateNonaxialForces(FlightConditions conditions,
72 AerodynamicForces forces, WarningSet warnings) {
74 // Compute and cache the fin geometry
75 if (Double.isNaN(macLength)) {
76 calculateFinGeometry();
84 forces.cp = Coordinate.NUL;
87 forces.CrollForce = 0;
94 // Add warnings (radius/2 == diameter/4)
95 if (component.getThickness() > component.getBodyRadius()/2) {
96 warnings.add(Warning.THICK_FIN);
98 warnings.addAll(geometryWarnings);
102 //////// Calculate CNa. /////////
104 // One fin without interference (both sub- and supersonic):
105 double cna1 = calculateFinCNa1(conditions);
109 // Multiple fins with fin-fin interference
112 // TODO: MEDIUM: Take into account multiple fin sets
113 int fins = component.getFinCount();
114 double theta = conditions.getTheta();
115 double angle = component.getBaseRotation();
122 for (int i=0; i < fins; i++) {
123 mul += MathUtil.pow2(Math.sin(theta - angle));
124 angle += 2 * Math.PI / fins;
130 // multiplier 1.5, sinusoidal reduction of 15%
131 cna = cna1 * 1.5 * (1 - 0.15*pow2(Math.cos(1.5 * (theta-angle))));
135 // multiplier 2.0, sinusoidal reduction of 6%
136 cna = cna1 * 2.0 * (1 - 0.06*pow2(Math.sin(2 * (theta-angle))));
156 // Assume N/2 * 3/4 efficiency for more fins
157 cna = cna1 * fins * 3.0/8.0;
162 // Body-fin interference effect
163 double r = component.getBodyRadius();
164 double tau = r / (span+r);
165 if (Double.isNaN(tau) || Double.isInfinite(tau))
167 cna *= 1 + tau; // Classical Barrowman
168 // cna *= pow2(1 + tau); // Barrowman thesis (too optimistic??)
172 // TODO: LOW: check for fin tip mach cone interference
173 // (Barrowman thesis pdf-page 40)
175 // TODO: LOW: fin-fin mach cone effect, MIL-HDBK page 5-25
179 // Calculate CP position
180 double x = macLead + calculateCPPos(conditions) * macLength;
184 // Calculate roll forces, reduce forcing above stall angle
186 // Without body-fin interference effect:
187 // forces.CrollForce = fins * (macSpan+r) * cna1 * component.getCantAngle() /
188 // conditions.getRefLength();
189 // With body-fin interference effect:
190 forces.CrollForce = fins * (macSpan+r) * cna1 * (1+tau) * component.getCantAngle() /
191 conditions.getRefLength();
196 if (conditions.getAOA() > STALL_ANGLE) {
197 // System.out.println("Fin stalling in roll");
198 forces.CrollForce *= MathUtil.clamp(
199 1-(conditions.getAOA() - STALL_ANGLE)/(STALL_ANGLE/2), 0, 1);
201 forces.CrollDamp = calculateDampingMoment(conditions);
202 forces.Croll = forces.CrollForce - forces.CrollDamp;
206 // System.out.printf(component.getName() + ": roll rate:%.3f force:%.3f damp:%.3f " +
208 // conditions.getRollRate(), forces.CrollForce, forces.CrollDamp, forces.Croll);
211 forces.CN = cna * MathUtil.min(conditions.getAOA(), STALL_ANGLE);
212 forces.cp = new Coordinate(x, 0, 0, cna);
213 forces.Cm = forces.CN * x / conditions.getRefLength();
216 forces.Cside = cna1 * Math.cos(theta-angle) * Math.sin(theta-angle);
217 forces.Cyaw = forces.Cside * x / conditions.getRefLength();
227 * Returns the MAC length of the fin. This is required in the friction drag
230 * @return the MAC length of the fin.
232 public double getMACLength() {
233 // Compute and cache the fin geometry
234 if (Double.isNaN(macLength)) {
235 calculateFinGeometry();
242 public double getMidchordPos() {
243 // Compute and cache the fin geometry
244 if (Double.isNaN(macLength)) {
245 calculateFinGeometry();
249 return macLead + 0.5 * macLength;
255 * Pre-calculates the fin geometry values.
257 protected void calculateFinGeometry() {
259 span = component.getSpan();
260 finArea = component.getFinArea();
261 ar = 2 * pow2(span) / finArea;
263 Coordinate[] points = component.getFinPoints();
265 // Check for jagged edges
266 geometryWarnings.clear();
267 boolean down = false;
268 for (int i=1; i < points.length; i++) {
269 if ((points[i].y > points[i-1].y + 0.001) && down) {
270 geometryWarnings.add(Warning.JAGGED_EDGED_FIN);
273 if (points[i].y < points[i-1].y - 0.001) {
279 // Calculate the chord lead and trail positions and length
281 Arrays.fill(chordLead, Double.POSITIVE_INFINITY);
282 Arrays.fill(chordTrail, Double.NEGATIVE_INFINITY);
283 Arrays.fill(chordLength, 0);
285 for (int point=1; point < points.length; point++) {
286 double x1 = points[point-1].x;
287 double y1 = points[point-1].y;
288 double x2 = points[point].x;
289 double y2 = points[point].y;
291 if (MathUtil.equals(y1, y2))
294 int i1 = (int)(y1*1.0001/span*(DIVISIONS-1));
295 int i2 = (int)(y2*1.0001/span*(DIVISIONS-1));
296 i1 = MathUtil.clamp(i1, 0, DIVISIONS-1);
297 i2 = MathUtil.clamp(i2, 0, DIVISIONS-1);
304 for (int i = i1; i <= i2; i++) {
305 // Intersection point (x,y)
306 double y = i*span/(DIVISIONS-1);
307 double x = (y-y2)/(y1-y2)*x1 + (y1-y)/(y1-y2)*x2;
308 if (x < chordLead[i])
310 if (x > chordTrail[i])
313 // TODO: LOW: If fin point exactly on chord line, might be counted twice:
322 // Check and correct any inconsistencies
323 for (int i=0; i < DIVISIONS; i++) {
324 if (Double.isInfinite(chordLead[i]) || Double.isInfinite(chordTrail[i]) ||
325 Double.isNaN(chordLead[i]) || Double.isNaN(chordTrail[i])) {
329 if (chordLength[i] < 0 || Double.isNaN(chordLength[i])) {
332 if (chordLength[i] > chordTrail[i] - chordLead[i]) {
333 chordLength[i] = chordTrail[i] - chordLead[i];
338 /* Calculate fin properties:
340 * macLength // MAC length
341 * macLead // MAC leading edge position
342 * macSpan // MAC spanwise position
343 * ar // Fin aspect ratio (already set)
344 * span // Fin span (already set)
353 double radius = component.getBodyRadius();
355 final double dy = span/(DIVISIONS-1);
356 for (int i=0; i < DIVISIONS; i++) {
357 double length = chordTrail[i] - chordLead[i];
360 macLength += length * length;
361 macSpan += y * length;
362 macLead += chordLead[i] * length;
364 rollSum += chordLength[i] * pow2(radius + y);
367 double dx = (chordTrail[i]+chordLead[i])/2 - (chordTrail[i-1]+chordLead[i-1])/2;
368 cosGamma += dy/MathUtil.hypot(dx, dy);
370 dx = chordLead[i] - chordLead[i-1];
371 cosGammaLead += dy/MathUtil.hypot(dx, dy);
384 cosGamma /= (DIVISIONS-1);
385 cosGammaLead /= (DIVISIONS-1);
389 /////////////// CNa1 calculation ////////////////
391 private static final double CNA_SUBSONIC = 0.9;
392 private static final double CNA_SUPERSONIC = 1.5;
393 private static final double CNA_SUPERSONIC_B = pow(pow2(CNA_SUPERSONIC)-1, 1.5);
394 private static final double GAMMA = 1.4;
395 private static final LinearInterpolator K1, K2, K3;
396 private static final PolyInterpolator cnaInterpolator = new PolyInterpolator(
397 new double[] { CNA_SUBSONIC, CNA_SUPERSONIC },
398 new double[] { CNA_SUBSONIC, CNA_SUPERSONIC },
399 new double[] { CNA_SUBSONIC }
401 /* Pre-calculate the values for K1, K2 and K3 */
404 int n = (int)((5.0-CNA_SUPERSONIC)*10);
405 double[] x = new double[n];
406 double[] k1 = new double[n];
407 double[] k2 = new double[n];
408 double[] k3 = new double[n];
409 for (int i=0; i<n; i++) {
410 double M = CNA_SUPERSONIC + i*0.1;
411 double beta = sqrt(M*M - 1);
414 k2[i] = ((GAMMA+1)*pow(M, 4) - 4*pow2(beta)) / (4*pow(beta,4));
415 k3[i] = ((GAMMA+1)*pow(M, 8) + (2*pow2(GAMMA) - 7*GAMMA - 5) * pow(M,6) +
416 10*(GAMMA+1)*pow(M,4) + 8) / (6*pow(beta,7));
418 K1 = new LinearInterpolator(x,k1);
419 K2 = new LinearInterpolator(x,k2);
420 K3 = new LinearInterpolator(x,k3);
422 // System.out.println("K1[m="+CNA_SUPERSONIC+"] = "+k1[0]);
423 // System.out.println("K2[m="+CNA_SUPERSONIC+"] = "+k2[0]);
424 // System.out.println("K3[m="+CNA_SUPERSONIC+"] = "+k3[0]);
428 protected double calculateFinCNa1(FlightConditions conditions) {
429 double mach = conditions.getMach();
430 double ref = conditions.getRefArea();
431 double alpha = MathUtil.min(conditions.getAOA(),
432 Math.PI - conditions.getAOA(), STALL_ANGLE);
435 if (mach <= CNA_SUBSONIC) {
436 return 2*Math.PI*pow2(span)/(1 + sqrt(1 + (1-pow2(mach))*
437 pow2(pow2(span)/(finArea*cosGamma)))) / ref;
441 if (mach >= CNA_SUPERSONIC) {
442 return finArea * (K1.getValue(mach) + K2.getValue(mach)*alpha +
443 K3.getValue(mach)*pow2(alpha)) / ref;
446 // Transonic case, interpolate
450 double sq = sqrt(1 + (1-pow2(CNA_SUBSONIC)) * pow2(span*span/(finArea*cosGamma)));
451 subV = 2*Math.PI*pow2(span)/ref / (1+sq);
452 subD = 2*mach*Math.PI*pow(span,6) / (pow2(finArea*cosGamma) * ref *
455 superV = finArea * (K1.getValue(CNA_SUPERSONIC) + K2.getValue(CNA_SUPERSONIC)*alpha +
456 K3.getValue(CNA_SUPERSONIC)*pow2(alpha)) / ref;
457 superD = -finArea/ref * 2*CNA_SUPERSONIC / CNA_SUPERSONIC_B;
459 // System.out.println("subV="+subV+" superV="+superV+" subD="+subD+" superD="+superD);
461 return cnaInterpolator.interpolate(mach, subV, superV, subD, superD, 0);
467 private double calculateDampingMoment(FlightConditions conditions) {
468 double rollRate = conditions.getRollRate();
470 if (Math.abs(rollRate) < 0.1)
473 double mach = conditions.getMach();
474 double radius = component.getBodyRadius();
475 double absRate = Math.abs(rollRate);
479 * At low speeds and relatively large roll rates (i.e. near apogee) the
480 * fin tips rotate well above stall angle. In this case sum the chords
483 if (absRate * (radius + span) / conditions.getVelocity() > 15*Math.PI/180) {
485 for (int i=0; i < DIVISIONS; i++) {
486 double dist = radius + span*i/DIVISIONS;
487 double aoa = Math.min(absRate*dist/conditions.getVelocity(), 15*Math.PI/180);
488 sum += chordLength[i] * dist * aoa;
490 sum = sum * (span/DIVISIONS) * 2*Math.PI/conditions.getBeta() /
491 (conditions.getRefArea() * conditions.getRefLength());
493 // System.out.println("SPECIAL: " +
494 // (MathUtil.sign(rollRate) *component.getFinCount() * sum));
495 return MathUtil.sign(rollRate) * component.getFinCount() * sum;
500 if (mach <= CNA_SUBSONIC) {
501 // System.out.println("BASIC: "+
502 // (component.getFinCount() * 2*Math.PI * rollRate * rollSum /
503 // (conditions.getRefArea() * conditions.getRefLength() *
504 // conditions.getVelocity() * conditions.getBeta())));
506 return component.getFinCount() * 2*Math.PI * rollRate * rollSum /
507 (conditions.getRefArea() * conditions.getRefLength() *
508 conditions.getVelocity() * conditions.getBeta());
510 if (mach >= CNA_SUPERSONIC) {
512 double vel = conditions.getVelocity();
513 double k1 = K1.getValue(mach);
514 double k2 = K2.getValue(mach);
515 double k3 = K3.getValue(mach);
519 for (int i=0; i < DIVISIONS; i++) {
520 double y = i*span/(DIVISIONS-1);
521 double angle = rollRate * (radius+y) / vel;
523 sum += (k1 * angle + k2 * angle*angle + k3 * angle*angle*angle)
524 * chordLength[i] * (radius+y);
527 return component.getFinCount() * sum * span/(DIVISIONS-1) /
528 (conditions.getRefArea() * conditions.getRefLength());
531 // Transonic, do linear interpolation
533 FlightConditions cond = conditions.clone();
534 cond.setMach(CNA_SUBSONIC - 0.01);
535 double subsonic = calculateDampingMoment(cond);
536 cond.setMach(CNA_SUPERSONIC + 0.01);
537 double supersonic = calculateDampingMoment(cond);
539 return subsonic * (CNA_SUPERSONIC - mach)/(CNA_SUPERSONIC - CNA_SUBSONIC) +
540 supersonic * (mach - CNA_SUBSONIC)/(CNA_SUPERSONIC - CNA_SUBSONIC);
547 * Return the relative position of the CP along the mean aerodynamic chord.
548 * Below mach 0.5 it is at the quarter chord, above mach 2 calculated using an
549 * empirical formula, between these two using an interpolation polynomial.
551 * @param cond Mach speed used
552 * @return CP position along the MAC
554 private double calculateCPPos(FlightConditions cond) {
555 double m = cond.getMach();
557 // At subsonic speeds CP at quarter chord
561 // At supersonic speeds use empirical formula
562 double beta = cond.getBeta();
563 return (ar * beta - 0.67) / (2*ar*beta - 1);
566 // In between use interpolation polynomial
570 for (int i=0; i < poly.length; i++) {
579 * Calculate CP position interpolation polynomial coefficients from the
580 * fin geometry. This is a fifth order polynomial that satisfies
589 * where f(M) = (ar*sqrt(M^2-1) - 0.67) / (2*ar*sqrt(M^2-1) - 1).
591 * The values were calculated analytically in Mathematica. The coefficients
592 * are used as poly[0] + poly[1]*x + poly[2]*x^2 + ...
594 private void calculatePoly() {
595 double denom = pow2(1 - 3.4641*ar); // common denominator
597 poly[5] = (-1.58025 * (-0.728769 + ar) * (-0.192105 + ar)) / denom;
598 poly[4] = (12.8395 * (-0.725688 + ar) * (-0.19292 + ar)) / denom;
599 poly[3] = (-39.5062 * (-0.72074 + ar) * (-0.194245 + ar)) / denom;
600 poly[2] = (55.3086 * (-0.711482 + ar) * (-0.196772 + ar)) / denom;
601 poly[1] = (-31.6049 * (-0.705375 + ar) * (-0.198476 + ar)) / denom;
602 poly[0] = (9.16049 * (-0.588838 + ar) * (-0.20624 + ar)) / denom;
606 @SuppressWarnings("null")
607 public static void main(String arg[]) {
608 Rocket rocket = Test.makeRocket();
609 FinSet finset = null;
611 Iterator<RocketComponent> iter = rocket.deepIterator();
612 while (iter.hasNext()) {
613 RocketComponent c = iter.next();
614 if (c instanceof FinSet) {
620 ((TrapezoidFinSet)finset).setHeight(0.10);
621 ((TrapezoidFinSet)finset).setRootChord(0.10);
622 ((TrapezoidFinSet)finset).setTipChord(0.10);
623 ((TrapezoidFinSet)finset).setSweep(0.0);
626 FinSetCalc calc = new FinSetCalc(finset);
628 calc.calculateFinGeometry();
629 FlightConditions cond = new FlightConditions(new Configuration(rocket));
630 for (double m=0; m < 3; m+=0.05) {
632 cond.setAOA(0.0*Math.PI/180);
633 double cna = calc.calculateFinCNa1(cond);
634 System.out.printf("%5.2f "+cna+"\n", m);
641 public double calculatePressureDragForce(FlightConditions conditions,
642 double stagnationCD, double baseCD, WarningSet warnings) {
644 // Compute and cache the fin geometry
645 if (Double.isNaN(cosGammaLead)) {
646 calculateFinGeometry();
651 FinSet.CrossSection profile = component.getCrossSection();
652 double mach = conditions.getMach();
655 // Pressure fore-drag
656 if (profile == FinSet.CrossSection.AIRFOIL ||
657 profile == FinSet.CrossSection.ROUNDED) {
659 // Round leading edge
661 drag = Math.pow(1 - pow2(mach), -0.417) - 1;
662 } else if (mach < 1) {
663 drag = 1 - 1.785 * (mach-0.9);
665 drag = 1.214 - 0.502/pow2(mach) + 0.1095/pow2(pow2(mach));
668 } else if (profile == FinSet.CrossSection.SQUARE) {
671 throw new UnsupportedOperationException("Unsupported fin profile: "+profile);
674 // Slanted leading edge
675 drag *= pow2(cosGammaLead);
677 // Trailing edge drag
678 if (profile == FinSet.CrossSection.SQUARE) {
680 } else if (profile == FinSet.CrossSection.ROUNDED) {
683 // Airfoil assumed to have zero base drag
686 // Scale to correct reference area
687 drag *= component.getFinCount() * component.getSpan() * component.getThickness() /
688 conditions.getRefArea();