First on my to-do list in my last posting was writing a good function for evaluating how closely a particular Joukowski airfoil
fit an arbitrary user’s foil. I had done this before (the great crash
), and in that process learned
what is now obvious to me: in marching CCW around the points on an airfoil/circle, one must inevitably progress from the first
quadrant to the second to the third to the fourth. Obvious, yes? But unless you specifically code it this way, and instead, say,
rely on your airfoil coordinates to determine which root of the inverse Joukowski transformation to use, then you will often find
yourself dealing with some bizarre results, such as the lower surface of undercambered airfoils being erroneously assigned to the
first quadrant. That one took me a long time to figure out, and it was only
Neil Toronto’s new plot package that let me figure it out. In fact, those plotting routines
are so simple and useful, that they are integral not only to zfoil, the product, but
zfoil, the program, including
associated test code.
Case in point, my newest function, (airfoil-fit-error N a c0). N is a "normalized" vector of airfoil coordinates,
a the parameter defining the Joukowski transformation, and c0 the center of the circle that the transform of which
is being compared to N. Applying this function to the NACA 4412 gives us the graph:Although I am writing a more objective test, this graph was sufficient to give me confidence in my code, and insight into the next step in writing my program, coding an optimization routine to find the best value of c0, the best point to use for the center of a circle that will map to the user’s airfoil. The skew in the contour of the error function suggests that there’s some benefit in estimating the gradient, and as it happens,while getting my master’s degree, I came up with a 2D special case of Powell’s Method that should work quite well here. That’ll be a later post, for sure.
Re "explicitly coding it this way" (quadrant by quadrant), I ended up trying it two ways, via Racket’s "comprehensions" and "old school" with a sequence of named lets, basically unrolling the loop on the quadrant variable.
Method 1, using for/fold:
Method 2/0 (0 being the "lost version", as best I can recollect):
(define (airfoil-fit-error N a c0) (let[(R(R a c0)) (q 1)] (for/fold[(sum 0.0)] [(n N)] (cond [(= q 1) (cond[(< (real-part n)0) (set! q 2)])] [(= q 2) (cond[(< (imag-part n)0) (set! q 3)])] [(= q 3) (cond[(> (real-part n)0) (set! q 4)])]) (let*[(c(cond [(= q 1)(J^-1a a n)] [(= q 2)(J^-1b a n)] [(= q 3)(J^-1a a n)] [(= q 4)(J^-1b a n)])) (j(J a (+ c0 (make-polar R (angle (- c c0)))))) (k(- n j))] (+ sum (sqr (magnitude k)))))))
I first went for the comprehension to "modernize" my code, but IMHO it’s less readable. Truly "modern" Racket would probably use local defines to get rid of the indentation associated with the nested lets, but that never bothered me, so screw it.
(define (airfoil-fit-error N a c0) (let[(r(radius a c0))] (let loop1[(error 0.0) (i 0)] (let[(n(vector-ref N i))] (if(>(real-part n)0) (loop1(+ error (let*[(c(J^-1a a n)) (j(J a (+ c0 (make-polar r (angle (- c c0))))))] (sqr (magnitude (- n j))))) (add1 i)) (let loop2[(error error) (i i)] (let[(n(vector-ref N i))] (if(>(imag-part n)0) (loop2(+ error (let*[(c(J^-1b a n)) (j(J a (+ c0 (make-polar r (angle (- c c0))))))] (sqr (magnitude (- n j))))) (add1 i)) (let loop3[(error error) (i i)] (let[(n(vector-ref N i))] (if(<(real-part n)0) (loop3(+ error (let*[(c(J^-1a a n)) (j(J a (+ c0 (make-polar r (angle (- c c0))))))] (sqr (magnitude (- n j))))) (add1 i)) (let loop4[(error error) (i i)] (if(< i (vector-length N)) (let[(n(vector-ref N i))] (loop4(+ error (let*[(c(J^-1a a n)) (j(J a (+ c0 (make-polar r (angle (- c c0))))))] (sqr (magnitude (- n j))))) (add1 i))) error)))))))))))))


