libacfutils
A general purpose library of utility functions designed to make it easier to develop addons for the X-Plane flight simulator.
Loading...
Searching...
No Matches
perf.c
1/*
2 * CDDL HEADER START
3 *
4 * The contents of this file are subject to the terms of the
5 * Common Development and Distribution License, Version 1.0 only
6 * (the "License"). You may not use this file except in compliance
7 * with the License.
8 *
9 * You can obtain a copy of the license in the file COPYING
10 * or http://www.opensource.org/licenses/CDDL-1.0.
11 * See the License for the specific language governing permissions
12 * and limitations under the License.
13 *
14 * When distributing Covered Code, include this CDDL HEADER in each
15 * file and include the License file COPYING.
16 * If applicable, add the following below this CDDL HEADER, with the
17 * fields enclosed by brackets "[]" replaced with your own identifying
18 * information: Portions Copyright [yyyy] [name of copyright owner]
19 *
20 * CDDL HEADER END
21 */
22/*
23 * Copyright 2021 Saso Kiselkov. All rights reserved.
24 */
25
26#include <stdio.h>
27#include <stdlib.h>
28#include <math.h>
29#include <string.h>
30#include <stddef.h>
31
32#include <acfutils/assert.h>
33#include <acfutils/avl.h>
34#include <acfutils/geom.h>
35#include <acfutils/math.h>
36#include <acfutils/list.h>
37#include <acfutils/log.h>
38#include <acfutils/helpers.h>
39#include <acfutils/perf.h>
40#include <acfutils/safe_alloc.h>
41
42#define SECS_PER_HR 3600 /* Number of seconds in an hour */
43
44#define ACFT_PERF_MIN_VERSION 1
45#define ACFT_PERF_MAX_VERSION 1
46#define MAX_LINE_COMPS 2
47
48/*
49 * Simulation step for accelclb2dist in seconds. 5 seems to be a good
50 * compromise between performance and accuracy (~1% error vs running
51 * 1-second steps, but 5x faster).
52 */
53#define SECS_PER_STEP 10
54/*
55 * Higher accuracy in the departure segment.
56 */
57#define SECS_PER_STEP_TAKEOFF 1
58/*
59 * Higher accuracy in the deceleration phase.
60 */
61#define SECS_PER_STEP_DECEL 1
62/*
63 * Cruise phase doesn't need high accuracy.
64 */
65#define SECS_PER_STEP_CRZ 10
66#define ALT_THRESH 1
67#define KCAS_THRESH 0.1
68#define KCAS_TABLE_THRESH 5
69
70#define MAX_ITER_STEPS 100000
71
72typedef struct {
73 double vs; /* vertical speed in m/s */
74 double fused; /* fuel used in kg */
75 double fused_t; /* fuel used time in seconds */
76 double ff; /* fuel flow in kg/s */
78
79typedef struct {
80 double isa; /* ISA deviation in degrees C */
81 double ias; /* IAS in m/s */
82 double mach; /* mach limit */
83 unsigned num_wts; /* number of weights */
84 unsigned num_alts; /* number of altitudes */
85 double *wts; /* weights in kg */
86 double *alts; /* altitudes in meters */
87 perf_table_cell_t **rows; /* one pointer set per alt */
88
89 avl_node_t ias_node;
90 avl_node_t mach_node;
91 list_node_t isa_node;
93
95 avl_tree_t by_isa;
96};
97
98typedef struct {
99 double isa;
100 avl_tree_t by_ias;
101 avl_tree_t by_mach;
102 list_t tables;
103 avl_node_t ts_node;
105
106static bool_t step_debug = B_FALSE;
107
108static bool_t perf_table_parse(FILE *fp, perf_table_set_t *set,
109 unsigned num_eng, double ff_corr, unsigned *line_num);
110static void perf_table_free(perf_table_t *table);
111
112void
113lacf_set_perf_step_debug(bool_t flag)
114{
115 step_debug = flag;
116}
117
118bool_t
119lacf_get_perf_step_debug(void)
120{
121 return (step_debug);
122}
123
124static int
125perf_isas_compar(const void *a, const void *b)
126{
127 const perf_table_isa_t *ia = a, *ib = b;
128
129 if (ia->isa < ib->isa)
130 return (-1);
131 if (ia->isa > ib->isa)
132 return (1);
133
134 return (0);
135}
136
137static int
138perf_tables_ias_compar(const void *a, const void *b)
139{
140 const perf_table_t *ta = a, *tb = b;
141
142 ASSERT(!isnan(ta->ias));
143 ASSERT(!isnan(tb->ias));
144 if (ta->ias < tb->ias)
145 return (-1);
146 if (ta->ias > tb->ias)
147 return (1);
148
149 return (0);
150}
151
152static int
153perf_tables_mach_compar(const void *a, const void *b)
154{
155 const perf_table_t *ta = a, *tb = b;
156
157 ASSERT(!isnan(ta->mach));
158 ASSERT(!isnan(tb->mach));
159 if (ta->mach < tb->mach)
160 return (-1);
161 if (ta->mach > tb->mach)
162 return (1);
163
164 return (0);
165}
166
167static perf_table_set_t *
168perf_table_set_alloc(void)
169{
170 perf_table_set_t *ts = safe_calloc(1, sizeof (*ts));
171
172 avl_create(&ts->by_isa, perf_isas_compar, sizeof (perf_table_isa_t),
173 offsetof(perf_table_isa_t, ts_node));
174
175 return (ts);
176}
177
178static void
179perf_table_set_free(perf_table_set_t *ts)
180{
181 void *cookie = NULL;
182 perf_table_isa_t *isa;
183
184 while ((isa = avl_destroy_nodes(&ts->by_isa, &cookie)) != NULL) {
185 void *cookie2;
186 perf_table_t *table;
187
188 cookie2 = NULL;
189 while ((avl_destroy_nodes(&isa->by_ias, &cookie2)) != NULL)
190 ;
191 avl_destroy(&isa->by_ias);
192 cookie2 = NULL;
193 while ((avl_destroy_nodes(&isa->by_mach, &cookie2)) != NULL)
194 ;
195 avl_destroy(&isa->by_mach);
196 while ((table = list_remove_head(&isa->tables)) != NULL)
197 perf_table_free(table);
198 list_destroy(&isa->tables);
199 free(isa);
200 }
201 avl_destroy(&ts->by_isa);
202 free(ts);
203}
204
205static double
206parse_table_alt(const char *str)
207{
208 unsigned l, mtr;
209
210 ASSERT(str != NULL);
211 l = strlen(str);
212 ASSERT(l != 0);
213
214 if (l >= 3 && str[0] == 'F' && str[1] == 'L')
215 return (FEET2MET(atoi(&str[2]) * 100));
216 if (strcmp(str, "0") == 0)
217 return (0);
218 mtr = atoi(str);
219 if (mtr != 0)
220 return (mtr);
221
222 return (NAN);
223}
224
225static double
226perf_table_extrapolate(perf_table_t *table, unsigned col,
227 unsigned last_data_col, size_t offset)
228{
229 double v1, v2, m1, m2, m;
230 perf_table_cell_t *cell1, *cell2;
231
232 ASSERT(table != NULL);
233 ASSERT3U(col, <, table->num_wts);
234 ASSERT3U(last_data_col, <, col);
235 ASSERT3U(offset, <, sizeof (perf_table_cell_t));
236
237 if (last_data_col == 0) {
238 /*
239 * Single data element in table, can't extrapolate.
240 * So just copy the value.
241 */
242 cell1 = &table->rows[table->num_alts - 1][last_data_col];
243 return (*(double *)((void *)(cell1) + offset));
244 }
245
246 cell1 = &table->rows[table->num_alts - 1][last_data_col - 1];
247 cell2 = &table->rows[table->num_alts - 1][last_data_col];
248 v1 = *(double *)((void *)(cell1) + offset);
249 m1 = table->wts[last_data_col - 1];
250 v2 = *(double *)((void *)(cell2) + offset);
251 m2 = table->wts[last_data_col];
252 m = table->wts[col];
253
254 return (fx_lin(m, m1, v1, m2, v2));
255}
256
257static void
258perf_table_cells_populate(char ** comps, size_t n_comps, perf_table_t *table,
259 size_t offset, double conv_factor)
260{
261 ASSERT(comps != NULL);
262 ASSERT3U(n_comps, >, 1);
263 ASSERT(table != NULL);
264 ASSERT3U(offset, <, sizeof (perf_table_cell_t));
265
266 comps++;
267 n_comps--;
268
269 for (size_t i = 0; i < table->num_wts; i++) {
270 perf_table_cell_t *cell = &table->rows[table->num_alts - 1][i];
271 double *p = ((void *)(cell)) + offset;
272
273 if (i < n_comps) {
274 *p = atof(comps[i]) * conv_factor;
275 } else {
276 *p = perf_table_extrapolate(table, i, n_comps - 1,
277 offset);
278 }
279 }
280}
281
282static bool_t
283perf_table_parse(FILE *fp, perf_table_set_t *ts, unsigned num_eng,
284 double ff_corr, unsigned *line_num)
285{
286 perf_table_isa_t *isa, srch_isa;
287 perf_table_t *table = safe_calloc(1, sizeof (*table));
288 char *line = NULL;
289 size_t line_cap = 0;
290 avl_index_t where;
291
292 ASSERT(fp != NULL);
293 ASSERT(ts != NULL);
294 ASSERT(line_num != NULL);
295
296 table->isa = 0;
297 table->ias = NAN;
298 table->mach = NAN;
299
300 for (;;) {
301 ssize_t line_len =
302 parser_get_next_line(fp, &line, &line_cap, line_num);
303 size_t n_comps;
304 char **comps;
305 double alt;
306
307 if (line_len == 0)
308 break;
309 comps = strsplit(line, " ", B_TRUE, &n_comps);
310 if (strcmp(comps[0], "ISA") == 0 && n_comps == 2) {
311 table->isa = atof(comps[1]);
312 } else if (strcmp(comps[0], "IAS") == 0 && n_comps == 2) {
313 table->ias = atof(comps[1]);
314 } else if (strcmp(comps[0], "KIAS") == 0 && n_comps == 2) {
315 table->ias = KT2MPS(atof(comps[1]));
316 } else if (strcmp(comps[0], "MACH") == 0 && n_comps == 2) {
317 table->mach = atof(comps[1]);
318 } else if (strcmp(comps[0], "GWLBK") == 0 && n_comps >= 2) {
319 table->num_wts = n_comps - 1;
320 table->wts = safe_calloc(table->num_wts,
321 sizeof (*table->wts));
322 for (size_t i = 1; i < n_comps; i++) {
323 table->wts[i - 1] =
324 LBS2KG(1000 * atof(comps[i]));
325 }
326 } else if (!isnan(alt = parse_table_alt(comps[0]))) {
327 table->num_alts++;
328 table->alts = realloc(table->alts, table->num_alts *
329 sizeof (*table->alts));
330 table->alts[table->num_alts - 1] = alt;
331 table->rows = realloc(table->rows, table->num_alts *
332 sizeof (*table->rows));
333 table->rows[table->num_alts - 1] = safe_calloc(
334 table->num_wts, sizeof (perf_table_cell_t));
335 } else if (strcmp(comps[0], "FPM") == 0) {
336 perf_table_cells_populate(comps, n_comps, table,
337 offsetof(perf_table_cell_t, vs),
338 FPM2MPS(1));
339 } else if (strcmp(comps[0], "TIMM") == 0) {
340 perf_table_cells_populate(comps, n_comps, table,
341 offsetof(perf_table_cell_t, fused_t), 60);
342 } else if (strcmp(comps[0], "FULB") == 0) {
343 perf_table_cells_populate(comps, n_comps, table,
344 offsetof(perf_table_cell_t, fused),
345 LBS2KG(1) * ff_corr);
346 } else if (strcmp(comps[0], "FFLB/ENG") == 0) {
347 perf_table_cells_populate(comps, n_comps, table,
348 offsetof(perf_table_cell_t, ff),
349 (LBS2KG(1) / SECS_PER_HR) * num_eng * ff_corr);
350 } else if (strcmp(comps[0], "ENDTABLE") == 0) {
351 free_strlist(comps, n_comps);
352 break;
353 } else {
354 free_strlist(comps, n_comps);
355 goto errout;
356 }
357 free_strlist(comps, n_comps);
358 }
359
360 if ((isnan(table->ias) && isnan(table->mach)) || table->num_wts == 0 ||
361 table->num_alts == 0)
362 goto errout;
363
364 /*
365 * For climb/descent tables, we need to calculate the immediate
366 * fuel flow. The table contains aggregate climb time & fuel use
367 * figures. So to compute local fuel flow, we first subtract the
368 * fuel use and time-to-reach from one altitude lower. This then
369 * gives the fuel use & time delta to go from the lower altitude
370 * bracket to the altitude being examined. It isn't super-duper
371 * accurate, but should be reasonably close to immediate FF.
372 */
373 for (unsigned i_alt = 0; i_alt < table->num_alts; i_alt++) {
374 for (unsigned i_wt = 0; i_wt < table->num_wts; i_wt++) {
375 perf_table_cell_t *cell = &table->rows[i_alt][i_wt];
376
377 if (cell->fused_t == 0)
378 continue;
379 if (i_alt == 0) {
380 cell->ff = cell->fused / cell->fused_t;
381 } else {
382 perf_table_cell_t *subcell =
383 &table->rows[i_alt - 1][i_wt];
384 double fused, fused_t;
385
386 ASSERT3F(subcell->fused, >, 0);
387 ASSERT3F(subcell->fused_t, >, 0);
388 fused = cell->fused - subcell->fused;
389 fused_t = cell->fused_t - subcell->fused_t;
390 cell->ff = fused / fused_t;
391 }
392 ASSERT_MSG(cell->ff >= 0, "Malformed table with "
393 "negative fuel flow: ISA=%.0f KIAS=%.0f Mach=%.2f "
394 "ALT=%.0fft WT=%.0flbs", table->isa,
395 MPS2KT(table->ias), table->mach,
396 MET2FEET(table->alts[i_alt]),
397 KG2LBS(table->wts[i_wt]));
398 }
399 }
400
401 srch_isa.isa = table->isa;
402 isa = avl_find(&ts->by_isa, &srch_isa, &where);
403 if (isa == NULL) {
404 isa = safe_calloc(1, sizeof (*isa));
405 isa->isa = table->isa;
406 avl_create(&isa->by_ias, perf_tables_ias_compar,
407 sizeof (perf_table_t), offsetof(perf_table_t, ias_node));
408 avl_create(&isa->by_mach, perf_tables_mach_compar,
409 sizeof (perf_table_t), offsetof(perf_table_t, mach_node));
410 list_create(&isa->tables, sizeof (perf_table_t),
411 offsetof(perf_table_t, isa_node));
412 avl_insert(&ts->by_isa, isa, where);
413 }
414 if (!isnan(table->ias)) {
415 if (avl_find(&isa->by_ias, table, &where) != NULL) {
416 logMsg("Duplicate table for ISA %.1f/IAS %.1f",
417 table->isa, MPS2KT(table->ias));
418 goto errout;
419 }
420 avl_insert(&isa->by_ias, table, where);
421 }
422 if (!isnan(table->mach)) {
423 if (avl_find(&isa->by_mach, table, &where) != NULL) {
424 logMsg("Duplicate table for ISA %.1f/Mach %.3f",
425 table->isa, MPS2KT(table->mach));
426 goto errout;
427 }
428 avl_insert(&isa->by_mach, table, where);
429 }
430
431 return (B_TRUE);
432errout:
433 perf_table_free(table);
434 return (B_FALSE);
435}
436
437static void
438perf_table_free(perf_table_t *table)
439{
440 free(table->wts);
441 free(table->alts);
442 for (unsigned i = 0; i < table->num_alts; i++)
443 free(table->rows[i]);
444 free(table->rows);
445
446 free(table);
447}
448
449static bool_t
450parse_curve_lin(FILE *fp, vect2_t **curvep, size_t numpoints,
451 unsigned *line_num)
452{
453 vect2_t *curve;
454 char *line = NULL;
455 size_t line_cap = 0;
456 ssize_t line_len = 0;
457
458 ASSERT(curvep != NULL);
459 ASSERT3P(*curvep, ==, NULL);
460 curve = safe_calloc(numpoints + 1, sizeof (*curve));
461
462 for (size_t i = 0; i < numpoints; i++) {
463 char *comps[2];
464
465 line_len = parser_get_next_line(fp, &line, &line_cap, line_num);
466 if (line_len <= 0)
467 goto errout;
468 if (explode_line(line, ',', comps, 2) != 2)
469 goto errout;
470 curve[i] = VECT2(atof(comps[0]), atof(comps[1]));
471 if (i > 0 && curve[i - 1].x >= curve[i].x)
472 goto errout;
473 }
474 /* curve terminator */
475 curve[numpoints] = NULL_VECT2;
476
477 *curvep = curve;
478 free(line);
479
480 return (B_TRUE);
481errout:
482 free(line);
483 free(curve);
484 return (B_FALSE);
485}
486
487static void
488perf_tables_find_spd(perf_table_isa_t *isa, double spd, bool_t is_mach,
489 perf_table_t **t_min_p, perf_table_t **t_max_p)
490{
491 avl_index_t where;
492 perf_table_t srch;
493 perf_table_t *t_min, *t_max;
494 avl_tree_t *tree;
495
496 ASSERT(isa != NULL);
497 ASSERT(t_min_p != NULL);
498 ASSERT(t_max_p != NULL);
499
500 if (is_mach) {
501 srch.mach = spd;
502 tree = &isa->by_mach;
503 } else {
504 srch.ias = spd;
505 tree = &isa->by_ias;
506 }
507 ASSERT(avl_numnodes(tree) != 0);
508 t_min = avl_find(tree, &srch, &where);
509
510 if (t_min != NULL) {
511 t_max = t_min;
512 } else {
513 t_min = avl_nearest(tree, where, AVL_BEFORE);
514 t_max = avl_nearest(tree, where, AVL_AFTER);
515 ASSERT(t_min != NULL || t_max != NULL);
516 if (t_min == NULL) {
517 if (AVL_NEXT(tree, t_max) != NULL) {
518 t_min = t_max;
519 t_max = AVL_NEXT(tree, t_max);
520 } else {
521 t_min = t_max;
522 }
523 }
524 if (t_max == NULL) {
525 if (AVL_PREV(tree, t_min) != NULL) {
526 t_max = t_min;
527 t_min = AVL_PREV(tree, t_min);
528 } else {
529 t_max = t_min;
530 }
531 }
532 ASSERT(t_min != NULL);
533 ASSERT(t_max != NULL);
534 }
535
536 *t_min_p = t_min;
537 *t_max_p = t_max;
538}
539
540static bool_t
541perf_tables_find(perf_table_set_t *ts,
542 double isadev, double spd, bool_t is_mach,
543 perf_table_t **isa0_min, perf_table_t **isa0_max,
544 perf_table_t **isa1_min, perf_table_t **isa1_max)
545{
546 avl_index_t where;
547 perf_table_isa_t srch = { .isa = isadev };
548 perf_table_isa_t *isa0, *isa1;
549
550 ASSERT(ts != NULL);
551 ASSERT(isa0_min != NULL);
552 ASSERT(isa0_max != NULL);
553 ASSERT(isa1_min != NULL);
554 ASSERT(isa1_max != NULL);
555
556 if (avl_numnodes(&ts->by_isa) == 0)
557 return (B_FALSE);
558
559 isa0 = avl_find(&ts->by_isa, &srch, &where);
560 if (isa0 != NULL) {
561 /* Exact hit */
562 isa1 = isa0;
563 } else {
564 /*
565 * Try to find nearest two data points and interpolate, or
566 * even extrapolate from the nearest two data points.
567 */
568 isa0 = avl_nearest(&ts->by_isa, where, AVL_BEFORE);
569 isa1 = avl_nearest(&ts->by_isa, where, AVL_AFTER);
570 ASSERT(isa0 != NULL || isa1 != NULL);
571 /*
572 * We are at the edge of the data range. If we have more than
573 * one adjacent table to either side, we can try extrapolating.
574 */
575 if (isa0 == NULL) {
576 if (AVL_NEXT(&ts->by_isa, isa1) != NULL) {
577 isa0 = isa1;
578 isa1 = AVL_NEXT(&ts->by_isa, isa1);
579 } else {
580 isa0 = isa1;
581 }
582 }
583 if (isa1 == NULL) {
584 if (AVL_PREV(&ts->by_isa, isa0) != NULL) {
585 isa1 = isa0;
586 isa0 = AVL_PREV(&ts->by_isa, isa0);
587 } else {
588 isa1 = isa0;
589 }
590 }
591 ASSERT(isa0 != NULL);
592 ASSERT(isa1 != NULL);
593 }
594
595 perf_tables_find_spd(isa0, spd, is_mach, isa0_min, isa0_max);
596 perf_tables_find_spd(isa1, spd, is_mach, isa1_min, isa1_max);
597
598 return (B_FALSE);
599}
600
601static double
602perf_table_lookup_row(perf_table_t *table, perf_table_cell_t *row,
603 double mass, size_t field_offset)
604{
605 unsigned col1 = 0, col2 = 1;
606 double v1, v2, v;
607
608 ASSERT(table != NULL);
609 ASSERT(table->num_wts != 0);
610 ASSERT(row != NULL);
611 ASSERT3U(field_offset, <, sizeof (perf_table_cell_t));
612
613 /* clamp the mass to our tabulated range */
614 mass = clamp(mass, table->wts[0], table->wts[table->num_wts - 1] - 1);
615
616 for (unsigned i = 0; i + 1 < table->num_wts; i++) {
617 if (mass >= table->wts[i] && mass <= table->wts[i + 1]) {
618 col1 = i;
619 col2 = i + 1;
620 break;
621 }
622 }
623
624 v1 = *(double *)((void *)(&row[col1]) + field_offset);
625 v2 = *(double *)((void *)(&row[col2]) + field_offset);
626
627 if (col1 != col2)
628 v = fx_lin(mass, table->wts[col1], v1, table->wts[col2], v2);
629 else
630 v = v1;
631 ASSERT(!isnan(v));
632
633 return (v);
634}
635
636static double
637perf_table_lookup(perf_table_t *table, double mass, double alt,
638 size_t field_offset)
639{
640 unsigned row1 = UINT32_MAX, row2 = UINT32_MAX;
641 double row1_val, row2_val, value;
642
643 ASSERT(table != NULL);
644 ASSERT3U(table->num_alts, >, 1);
645 ASSERT3U(field_offset, <, sizeof (perf_table_cell_t));
646
647 /*
648 * If the requested altitude lies outside of our tabulated range,
649 * extrapolate to it from the nearest pair of rows.
650 */
651 if (alt > table->alts[0]) {
652 row1 = 0;
653 row2 = 1;
654 } else if (alt < table->alts[table->num_alts - 1]) {
655 row1 = table->num_alts - 2;
656 row2 = table->num_alts - 1;
657 } else {
658 for (unsigned i = 0; i + 1 < table->num_alts; i++) {
659 if (alt <= table->alts[i] &&
660 alt >= table->alts[i + 1]) {
661 row1 = i;
662 row2 = i + 1;
663 break;
664 }
665 }
666 }
667 ASSERT3U(row1 + 1, ==, row2);
668 ASSERT3U(row2, <, table->num_alts);
669
670 row1_val = perf_table_lookup_row(table, table->rows[row1], mass,
671 field_offset);
672 row2_val = perf_table_lookup_row(table, table->rows[row2], mass,
673 field_offset);
674 value = fx_lin(alt, table->alts[row1], row1_val, table->alts[row2],
675 row2_val);
676 ASSERT(!isnan(value));
677
678 return (value);
679}
680
681static double
682table_lookup_common(perf_table_set_t *ts, double isadev, double mass,
683 double spd_mps_or_mach, bool_t is_mach, double alt, size_t offset)
684{
685 perf_table_t *isa0_min = NULL, *isa0_max = NULL;
686 perf_table_t *isa1_min = NULL, *isa1_max = NULL;
687 double isa0_min_param, isa0_max_param, isa1_min_param, isa1_max_param;
688 double isa0_param, isa1_param, param;
689
690 ASSERT(ts != NULL);
691 ASSERT3U(offset + sizeof (double), <=, sizeof (perf_table_cell_t));
692
693 perf_tables_find(ts, isadev, spd_mps_or_mach, is_mach,
694 &isa0_min, &isa0_max, &isa1_min, &isa1_max);
695
696 if (isa0_min != isa0_max) {
697 double x0 = (is_mach ? isa0_min->mach : isa0_min->ias);
698 double x1 = (is_mach ? isa0_max->mach : isa0_max->ias);
699 double rat = iter_fract(spd_mps_or_mach, x0, x1, B_FALSE);
700 isa0_min_param = perf_table_lookup(isa0_min, mass, alt, offset);
701 isa0_max_param = perf_table_lookup(isa0_max, mass, alt, offset);
702 /*
703 * We need to be careful about extrapolating speed estimates
704 * too much. There are drag-nonlinearities inherent in this,
705 * so we limit the estimator to reasonable ranges only.
706 */
707 isa0_param = wavg2(isa0_min_param, isa0_max_param,
708 clamp(rat, -0.25, 2));
709 ASSERT(!isnan(isa0_param));
710 } else {
711 isa0_param = perf_table_lookup(isa0_min, mass, alt, offset);
712 }
713 if (isa1_min != isa1_max) {
714 double x0 = (is_mach ? isa1_min->mach : isa1_min->ias);
715 double x1 = (is_mach ? isa1_max->mach : isa1_max->ias);
716 double rat = iter_fract(spd_mps_or_mach, x0, x1, B_FALSE);
717 isa1_min_param = perf_table_lookup(isa1_min, mass, alt, offset);
718 isa1_max_param = perf_table_lookup(isa1_max, mass, alt, offset);
719 isa1_param = wavg2(isa1_min_param, isa1_max_param,
720 clamp(rat, -0.25, 2));
721 ASSERT(!isnan(isa1_param));
722 } else {
723 isa1_param = perf_table_lookup(isa1_min, mass, alt, offset);
724 }
725
726 if (isa0_param != isa1_param) {
727 double rat = clamp(iter_fract(isadev, isa0_min->isa,
728 isa1_min->isa, B_FALSE), -0.5, 1.5);
729 param = wavg2(isa0_param, isa1_param, rat);
730 } else {
731 param = isa0_param;
732 }
733
734 return (param);
735}
736
737#define PARSE_SCALAR(name, var) \
738 if (strcmp(comps[0], name) == 0) { \
739 if (ncomps != 2) { \
740 logMsg("Error parsing acft perf file %s:%d: " \
741 "malformed or duplicate " name " line.", \
742 filename, line_num); \
743 goto errout; \
744 } \
745 (var) = atof(comps[1]); \
746 if ((var) <= 0.0) { \
747 logMsg("Error parsing acft perf file %s:%d: " \
748 "invalid value for " name, filename, line_num); \
749 goto errout; \
750 } \
751 }
752
753/*
754 * Checks that comps[0] contains `name' and if it does, parses comps[1] number
755 * of curve points into `var'. The curve parsed is assumed to be composed of
756 * a sequential series of linear segments.
757 */
758#define PARSE_CURVE(name, var) \
759 if (strcmp(comps[0], name) == 0) { \
760 if (ncomps != 2 || atoi(comps[1]) < 2 || (var) != NULL) { \
761 logMsg("Error parsing acft perf file %s:%d: " \
762 "malformed or duplicate " name " line.", \
763 filename, line_num); \
764 goto errout; \
765 } \
766 if (!parse_curve_lin(fp, &(var), atoi(comps[1]), &line_num)) { \
767 logMsg("Error parsing acft perf file %s:%d: " \
768 "malformed or missing lines.", filename, \
769 line_num); \
770 goto errout; \
771 } \
772 }
773
774#define PARSE_TABLE(name, table_set) \
775 if (strcmp(comps[0], (name)) == 0) { \
776 if ((table_set) == NULL) \
777 (table_set) = perf_table_set_alloc(); \
778 if (!perf_table_parse(fp, (table_set), acft->num_eng, \
779 table_ff_corr, &line_num)) { \
780 logMsg("Error parsing acft perf file %s:%d: " \
781 "malformed or missing lines.", filename, \
782 line_num); \
783 goto errout; \
784 } \
785 }
786
788acft_perf_parse(const char *filename)
789{
790 acft_perf_t *acft = safe_calloc(sizeof (*acft), 1);
791 FILE *fp = fopen(filename, "r");
792 char *line = NULL;
793 size_t line_cap = 0;
794 unsigned line_num = 0;
795 ssize_t line_len = 0;
796 char *comps[MAX_LINE_COMPS];
797 bool_t version_check_completed = B_FALSE;
798 double table_ff_corr = 1;
799
800 if (fp == NULL)
801 goto errout;
802
803 for (int i = 0; i < FLT_PERF_NUM_SPD_LIMS; i++) {
804 acft->ref.clb_spd_lim[i] = (flt_spd_lim_t){NAN, NAN};
805 acft->ref.des_spd_lim[i] = (flt_spd_lim_t){NAN, NAN};
806 }
807 while ((line_len = parser_get_next_line(fp, &line, &line_cap,
808 &line_num)) != -1) {
809 ssize_t ncomps;
810
811 if (line_len == 0)
812 continue;
813 ncomps = explode_line(line, ',', comps, MAX_LINE_COMPS);
814 if (ncomps < 0) {
815 logMsg("Error parsing acft perf file %s:%d: "
816 "malformed line, too many line components.",
817 filename, line_num);
818 goto errout;
819 }
820 ASSERT(ncomps > 0);
821 if (strcmp(comps[0], "VERSION") == 0) {
822 int vers;
823
824 if (version_check_completed) {
825 logMsg("Error parsing acft perf file %s:%d: "
826 "duplicate VERSION line.", filename,
827 line_num);
828 goto errout;
829 }
830 if (ncomps != 2) {
831 logMsg("Error parsing acft perf file %s:%d: "
832 "malformed VERSION line.", filename,
833 line_num);
834 goto errout;
835 }
836 vers = atoi(comps[1]);
837 if (vers < ACFT_PERF_MIN_VERSION ||
838 vers > ACFT_PERF_MAX_VERSION) {
839 logMsg("Error parsing acft perf file %s:%d: "
840 "unsupported file version %d.", filename,
841 line_num, vers);
842 goto errout;
843 }
844 version_check_completed = B_TRUE;
845 continue;
846 }
847 if (!version_check_completed) {
848 logMsg("Error parsing acft perf file %s:%d: first "
849 "line was not VERSION.", filename, line_num);
850 goto errout;
851 }
852 if (strcmp(comps[0], "ACFTTYPE") == 0) {
853 if (ncomps != 2 || acft->acft_type != NULL) {
854 logMsg("Error parsing acft perf file %s:%d: "
855 "malformed or duplicate ACFTTYPE line.",
856 filename, line_num);
857 goto errout;
858 }
859 acft->acft_type = strdup(comps[1]);
860 } else if (strcmp(comps[0], "ENGTYPE") == 0) {
861 if (ncomps != 2 || acft->eng_type != NULL) {
862 logMsg("Error parsing acft perf file %s:%d: "
863 "malformed or duplicate ENGTYPE line.",
864 filename, line_num);
865 goto errout;
866 }
867 acft->eng_type = strdup(comps[1]);
868 }
869 else PARSE_SCALAR("NUMENG", acft->num_eng)
870 else PARSE_SCALAR("MAXTHR", acft->eng_max_thr)
871 else PARSE_SCALAR("MINTHR", acft->eng_min_thr)
872 else PARSE_SCALAR("SFC", acft->eng_sfc)
873 else PARSE_SCALAR("REFZFW", acft->ref.zfw)
874 else PARSE_SCALAR("REFFUEL", acft->ref.fuel)
875 else PARSE_SCALAR("REFCRZLVL", acft->ref.crz_lvl)
876 else PARSE_SCALAR("REFCLBIAS", acft->ref.clb_ias)
877 else PARSE_SCALAR("REFCLBIASINIT", acft->ref.clb_ias_init)
878 else PARSE_SCALAR("REFCLBMACH", acft->ref.clb_mach)
879 else PARSE_SCALAR("REFCRZIAS", acft->ref.crz_ias)
880 else PARSE_SCALAR("REFCRZMACH", acft->ref.crz_mach)
881 else PARSE_SCALAR("REFDESIAS", acft->ref.des_ias)
882 else PARSE_SCALAR("REFDESMACH", acft->ref.des_mach)
883 else PARSE_SCALAR("REFTOFLAP", acft->ref.to_flap)
884 else PARSE_SCALAR("REFACCELHT", acft->ref.accel_hgt)
885 else PARSE_SCALAR("REFCLBSPDLIM[0]",
886 acft->ref.clb_spd_lim[0].kias)
887 else PARSE_SCALAR("REFCLBSPDLIMALT[0]",
888 acft->ref.clb_spd_lim[0].alt_ft)
889 else PARSE_SCALAR("REFCLBSPDLIM[1]",
890 acft->ref.clb_spd_lim[1].kias)
891 else PARSE_SCALAR("REFCLBSPDLIMALT[1]",
892 acft->ref.clb_spd_lim[1].alt_ft)
893 else PARSE_SCALAR("REFDESSPDLIM[0]",
894 acft->ref.des_spd_lim[0].kias)
895 else PARSE_SCALAR("REFDESSPDLIMALT[0]",
896 acft->ref.des_spd_lim[0].alt_ft)
897 else PARSE_SCALAR("REFDESSPDLIM[1]",
898 acft->ref.des_spd_lim[1].kias)
899 else PARSE_SCALAR("REFDESSPDLIMALT[1]",
900 acft->ref.des_spd_lim[1].alt_ft)
901 else PARSE_SCALAR("WINGAREA", acft->wing_area)
902 else PARSE_SCALAR("CLMAX", acft->cl_max_aoa)
903 else PARSE_SCALAR("CLFLAPMAX", acft->cl_flap_max_aoa)
904 else PARSE_SCALAR("TABLEFFCORR", table_ff_corr)
905 else PARSE_CURVE("THRDENS", acft->thr_dens_curve)
906 else PARSE_CURVE("THRMACH", acft->thr_mach_curve)
907 else PARSE_CURVE("SFCTHRO", acft->sfc_thro_curve)
908 else PARSE_CURVE("SFCISA", acft->sfc_isa_curve)
909 else PARSE_CURVE("CL", acft->cl_curve)
910 else PARSE_CURVE("CLFLAP", acft->cl_flap_curve)
911 else PARSE_CURVE("CD", acft->cd_curve)
912 else PARSE_CURVE("CDFLAP", acft->cd_flap_curve)
913 else PARSE_CURVE("HALFBANK", acft->half_bank_curve)
914 else PARSE_CURVE("FULLBANK", acft->full_bank_curve)
915 else PARSE_TABLE("CLBTABLE", acft->clb_tables)
916 else PARSE_TABLE("CRZTABLE", acft->crz_tables)
917 else PARSE_TABLE("DESTABLE", acft->des_tables)
918 else {
919 logMsg("Error parsing acft perf file %s:%d: unknown "
920 "line", filename, line_num);
921 goto errout;
922 }
923 }
924
925 if (acft->acft_type == NULL ||
926 acft->ref.clb_ias <= 0 ||
927 acft->ref.clb_ias_init <= 0 ||
928 acft->ref.clb_mach <= 0 ||
929 acft->ref.crz_ias <= 0 ||
930 acft->ref.crz_mach <= 0 ||
931 acft->ref.des_ias <= 0 ||
932 acft->ref.des_mach <= 0 ||
933 acft->eng_type == NULL ||
934 acft->eng_max_thr <= 0 ||
935 acft->eng_min_thr <= 0 ||
936 acft->eng_sfc <= 0 ||
937 acft->num_eng <= 0 ||
938 acft->thr_mach_curve == NULL ||
939 acft->sfc_thro_curve == NULL ||
940 acft->sfc_isa_curve == NULL ||
941 acft->cl_curve == NULL ||
942 acft->cl_flap_curve == NULL ||
943 acft->cd_curve == NULL ||
944 acft->cd_flap_curve == NULL ||
945 acft->wing_area == 0 ||
946 acft->half_bank_curve == NULL ||
947 acft->full_bank_curve == NULL) {
948 logMsg("Error parsing acft perf file %s: missing or corrupt "
949 "data fields.", filename);
950 goto errout;
951 }
952
953 fclose(fp);
954 free(line);
955
956 acft->ref.thr_derate = 1;
957
958 return (acft);
959errout:
960 if (fp)
961 fclose(fp);
962 if (acft)
963 acft_perf_destroy(acft);
964 free(line);
965 return (NULL);
966}
967
968void
969acft_perf_destroy(acft_perf_t *acft)
970{
971 if (acft->acft_type)
972 free(acft->acft_type);
973 if (acft->eng_type)
974 free(acft->eng_type);
975 free(acft->thr_dens_curve);
976 free(acft->thr_mach_curve);
977 free(acft->sfc_thro_curve);
978 free(acft->sfc_isa_curve);
979 free(acft->cl_curve);
980 free(acft->cl_flap_curve);
981 free(acft->cd_curve);
982 free(acft->cd_flap_curve);
983 free(acft->half_bank_curve);
984 free(acft->full_bank_curve);
985 if (acft->clb_tables != NULL)
986 perf_table_set_free(acft->clb_tables);
987 if (acft->crz_tables != NULL)
988 perf_table_set_free(acft->crz_tables);
989 if (acft->des_tables != NULL)
990 perf_table_set_free(acft->des_tables);
991 free(acft);
992}
993
995flt_perf_new(const acft_perf_t *acft)
996{
997 flt_perf_t *flt = safe_calloc(sizeof (*flt), 1);
998 memcpy(flt, &acft->ref, sizeof (*flt));
999 return (flt);
1000}
1001
1002void
1003flt_perf_destroy(flt_perf_t *flt)
1004{
1005 free(flt);
1006}
1007
1008static double
1009get_num_eng(const flt_perf_t *flt, const acft_perf_t *acft)
1010{
1011 ASSERT(flt != NULL);
1012 ASSERT(acft != NULL);
1013 if (flt->num_eng > 0 && flt->num_eng <= acft->num_eng)
1014 return (flt->num_eng);
1015 return (acft->num_eng);
1016}
1017
1018/*
1019 * Estimates maximum available engine thrust in a given flight situation.
1020 * This takes into account atmospheric conditions as well as any currently
1021 * effective engine derates. Number of engines running is configured via
1022 * flt_perf->num_eng.
1023 *
1024 * @param flt Flight performance configuration.
1025 * @param acft Aircraft performance tables.
1026 * @param throttle Relative linear throttle position (0.0 to 1.0).
1027 * @param alt Altitude in feet.
1028 * @param ktas True air speed in knots.
1029 * @param qnh Barometric altimeter setting in hPa.
1030 * @param isadev ISA temperature deviation in degrees C.
1031 * @param tp_alt Altitude of the tropopause in feet.
1032 *
1033 * @return Maximum available engine thrust in Newtons.
1034 */
1035double
1036eng_get_thrust(const flt_perf_t *flt, const acft_perf_t *acft, double throttle,
1037 double alt, double ktas, double qnh, double isadev, double tp_alt)
1038{
1039 double Ps, D, dmod, mmod, mach, sat;
1040 unsigned num_eng;
1041 double min_thr, max_thr;
1042
1043 ASSERT(flt != NULL);
1044 ASSERT(acft != NULL);
1045 ASSERT3F(throttle, >=, 0);
1046 ASSERT3F(throttle, <=, 1);
1047
1048 num_eng = get_num_eng(flt, acft);
1049
1050 Ps = alt2press(alt, qnh);
1051 sat = isadev2sat(alt2fl(alt < tp_alt ? alt : tp_alt, qnh), isadev);
1052 D = air_density(Ps, isadev);
1053 dmod = D / ISA_SL_DENS;
1054 if (acft->thr_dens_curve != NULL)
1055 dmod *= fx_lin_multi(dmod, acft->thr_dens_curve, B_TRUE);
1056 mach = ktas2mach(ktas, sat);
1057 mmod = fx_lin_multi(mach, acft->thr_mach_curve, B_TRUE);
1058
1059 max_thr = num_eng * acft->eng_max_thr * dmod * mmod * flt->thr_derate;
1060 min_thr = num_eng * acft->eng_min_thr * dmod * mmod * flt->thr_derate;
1061
1062 return (wavg(min_thr, max_thr, throttle));
1063}
1064
1065double
1066eng_get_min_thr(const flt_perf_t *flt, const acft_perf_t *acft)
1067{
1068 ASSERT(flt != NULL);
1069 ASSERT(acft != NULL);
1070 return (get_num_eng(flt, acft) * acft->eng_min_thr);
1071}
1072
1073/*
1074 * Returns the maximum average thrust that the engines can attain between
1075 * two altitudes during a climb.
1076 *
1077 * @param flt Flight performance limits.
1078 * @param acft Aircraft performance limit curves.
1079 * @param alt1 First (lower) altitude in feet.
1080 * @param temp1 Estimated TAT in C at altitude alt1.
1081 * @param alt1 Second (higher) altitude in feet.
1082 * @param temp1 Estimated TAT in C at altitude alt2.
1083 * @param tp_alt Altitude of the tropopause in feet.
1084 *
1085 * @return The maximum average engine thrust (in Kilonewtons) attainable
1086 * between alt1 and alt2 while keeping the flight and aircraft limits.
1087 */
1088double
1089eng_max_thr_avg(const flt_perf_t *flt, const acft_perf_t *acft, double alt1,
1090 double alt2, double ktas, double qnh, double isadev, double tp_alt)
1091{
1092 double Ps, D, dmod, mmod, avg_temp, thr;
1093 double avg_alt = AVG(alt1, alt2);
1094 /* convert altitudes to flight levels to calculate avg temp */
1095 double alt1_fl = alt2fl(alt1, qnh);
1096 double alt2_fl = alt2fl(alt2, qnh);
1097 double tp_fl = alt2fl(tp_alt, qnh);
1098 unsigned num_eng = get_num_eng(flt, acft);
1099 double mach;
1100
1101 /*
1102 * FIXME: correctly weight the temp average when tp_alt < alt2.
1103 */
1104 avg_temp = AVG(isadev2sat(alt1_fl, isadev),
1105 isadev2sat(alt2_fl < tp_fl ? alt2_fl : tp_fl, isadev));
1106
1107 mach = ktas2mach(ktas, avg_temp);
1108 mmod = fx_lin_multi(mach, acft->thr_mach_curve, B_TRUE);
1109 /* Ps is the average static air pressure between alt1 and alt2. */
1110 Ps = alt2press(avg_alt, qnh);
1111 /*
1112 * Finally grab effective air density.
1113 */
1114 isadev = isadev2sat(alt2fl(avg_alt, qnh), avg_temp);
1115 D = air_density(Ps, isadev);
1116 dmod = D / ISA_SL_DENS;
1117 if (acft->thr_dens_curve != NULL)
1118 dmod *= fx_lin_multi(dmod, acft->thr_dens_curve, B_TRUE);
1119 /*
1120 * Derive engine performance.
1121 */
1122 thr = num_eng * dmod * mmod * flt->thr_derate;
1123
1124 return (thr);
1125}
1126
1127/*
1128 * Given a curve mapping angle-of-attack (AoA) to an aircraft's coefficient of
1129 * lift (Cl) and a target Cl, we attempt to find the lowest AoA on the curve
1130 * where the required Cl is produced. If no candidate can be found, we return
1131 * DEFAULT_AOA.
1132 *
1133 * @param Cl Required coefficient of lift.
1134 * @param curve Curve mapping AoA to Cl.
1135 *
1136 * @return The angle of attack (in degrees) at which the Cl is produced.
1137 */
1138static double
1139cl_curve_get_aoa(double Cl, const vect2_t *curve)
1140{
1141 double aoa = 2.5;
1142 double *candidates = NULL;
1143 size_t n;
1144
1145 ASSERT(curve != NULL);
1146
1147 candidates = fx_lin_multi_inv(Cl, curve, &n);
1148 if (n == 0 || n == SIZE_MAX) {
1149 /* No AoA will provide enough lift, guess at some value */
1150 return (10);
1151 }
1152
1153 aoa = candidates[0];
1154 for (size_t i = 1; i < n; i++) {
1155 if (aoa > candidates[i]) {
1156 ASSERT(!isnan(candidates[i]));
1157 aoa = candidates[i];
1158 }
1159 }
1160 lacf_free(candidates);
1161
1162 return (aoa);
1163}
1164
1165/*
1166 * Calculates total (kinetic + potential) energy of a moving object.
1167 * This simply computes: E = m.g.h + (1/2).m.v^2
1168 *
1169 * @param mass Mass in kg.
1170 * @param altm Altitude above sea level in meters.
1171 * @param tas True airspeed in m/s.
1172 *
1173 * @return The object's total energy in Joules.
1174 */
1175static inline double
1176calc_total_E(double mass, double altm, double tas)
1177{
1178 return (mass * EARTH_GRAVITY * altm + 0.5 * mass * POW2(tas));
1179}
1180
1181/*
1182 * Calculates the altitude above sea level an object needs to be at to have
1183 * a given total (kinetic + potential) energy. This simply computes:
1184 * h = (E - (1/2).m.v^2) / (m.g)
1185 *
1186 * @param E Total energy in Joules.
1187 * @param m Mass in kg.
1188 * @param tas True airspeed in m/s.
1189 *
1190 * @return The object's required elevation above sea level in meters.
1191 */
1192static inline double
1193total_E_to_alt(double E, double m, double tas)
1194{
1195 return ((E - (0.5 * m * POW2(tas))) / (m * EARTH_GRAVITY));
1196}
1197
1198/*
1199 * Calculates the angle of attack required to maintain level flight.
1200 *
1201 * @param Pd Dynamic pressure on the aircraft in Pa (see dyn_press()).
1202 * @param mass Aircrat mass in kg.
1203 * @param flap_ratio Active flat setting between 0 and 1 inclusive
1204 * (0 - flaps up, 1 - flaps fully deployed).
1205 * @param acft Performance tables of the aircraft.
1206 *
1207 * @return Angle of attack to airstream in degrees required to produce
1208 * lift equivalent to the weight of `mass' on Earth. If the aircraft
1209 * is unable to produce sufficient lift at any angle of attack to
1210 * sustain flight, returns NAN instead.
1211 */
1212static double
1213get_aoa(double Pd, double mass, double flap_ratio, const acft_perf_t *acft)
1214{
1215 double lift, Cl;
1216
1217 lift = MASS2GFORCE(mass);
1218 Cl = lift / (Pd * acft->wing_area);
1219 if (flap_ratio == 0) {
1220 return (cl_curve_get_aoa(Cl, acft->cl_curve));
1221 } else {
1222 double aoa_no_flap = cl_curve_get_aoa(Cl, acft->cl_curve);
1223 double aoa_flap = cl_curve_get_aoa(Cl, acft->cl_flap_curve);
1224 ASSERT3F(flap_ratio, <=, 1.0);
1225 return (wavg(aoa_no_flap, aoa_flap, flap_ratio));
1226 }
1227}
1228
1229/*
1230 * Calculates the amount of drag experienced by an aircraft.
1231 *
1232 * @param Pd Dynamic pressure on the airframe in Pa (see dyn_press()).
1233 * @param aoa Current angle of attack to the airstream in degrees.
1234 * @param flap_ratio Active flat setting between 0 and 1 inclusive
1235 * (0 - flaps up, 1 - flaps fully deployed).
1236 * @param acft Performance tables of the aircraft.
1237 *
1238 * @return Drag force on the aircraft's airframe in N.
1239 */
1240static inline double
1241get_drag(double Pd, double aoa, double flap_ratio, const acft_perf_t *acft)
1242{
1243 if (flap_ratio == 0)
1244 return (fx_lin_multi(aoa, acft->cd_curve, B_TRUE) * Pd *
1245 acft->wing_area);
1246 else
1247 return (wavg(fx_lin_multi(aoa, acft->cd_curve, B_TRUE),
1248 fx_lin_multi(aoa, acft->cd_flap_curve, B_TRUE),
1249 flap_ratio) * Pd * acft->wing_area);
1250}
1251
1252/*
1253 * Performs a level acceleration simulation step.
1254 *
1255 * @param isadev ISA temperature deviation in degrees C.
1256 * @param tp_alt Altitude of the tropopause.
1257 * @param qnh Barometric altimeter setting in Pa.
1258 * @param gnd Flag indicating whether the aircraft is currently positioned
1259 * on the ground (and hence no lift generation is required).
1260 * @param alt Current aircraft altitude above mean sea level in feet.
1261 * @param kcasp Input/output argument containing the aircraft's current
1262 * calibrated airspeed in knots. This will be updated with the new
1263 * airspeed after the function returns with success.
1264 * @param kcas_targp Input/output argument containing the acceleration
1265 * target calibrated airspeed in knots. If the airspeed exceeds
1266 * mach_lim, it will be down-adjusted, otherwise it remains unchanged.
1267 * @param mach_lim Limiting Mach number. If either the current airspeed
1268 * or target airspeeds exceed this value at the provided altitude,
1269 * we down-adjust both. This allows for continuously accelerating while
1270 * climbing until reaching the Mach limit and then slowly bleeding off
1271 * speed to maintain the Mach number.
1272 * @param wind_mps Wind component along flight path in m/s.
1273 * @param mass Aircraft total mass in kg.
1274 * @param flap_ratio Active flat setting between 0 and 1 inclusive
1275 * (0 - flaps up, 1 - flaps fully deployed).
1276 * @param acft Performance tables of the aircraft.
1277 * @param flt Flight performance settings.
1278 * @param distp Return parameter that will be incremented with the distance
1279 * covered during the acceleration phase. As such, it must be
1280 * pre-initialized.
1281 * @param timep Input/output argument that initially contains the number of
1282 * seconds that the acceleration step can be allowed to happen. This is
1283 * then adjusted with the actual amount of time that the acceleration
1284 * was happening. If the target airspeed is greater than what can be
1285 * achieved during the acceleration step, timep is left unmodified
1286 * (all of the available time was used for acceleration). If the
1287 * target speed is achievable, timep is modified to the proportion of
1288 * the time taken to accelerate to that speed. If the target speed is
1289 * less than the current speed, timep is set proportionally negative
1290 * (and kcasp adjusted to it), indicating to the caller that the energy
1291 * can be used in a climb.
1292 * @param burnp Return parameter that will be filled with the amount of
1293 * fuel consumed during the acceleration phase. If no acceleration was
1294 * performed or speed was even given up for reuse for climbing, no
1295 * adjustment to this value will be made.
1296 *
1297 * @return B_TRUE on success, B_FALSE on failure (insufficient thrust
1298 * to overcome drag and accelerate).
1299 */
1300static void
1301spd_chg_step(bool_t accel, double isadev, double tp_alt, double qnh, bool_t gnd,
1302 double alt, double *kcasp, double kcas_targ, double wind_mps, double mass,
1303 double flap_ratio, const acft_perf_t *acft, const flt_perf_t *flt,
1304 double *distp, double *timep, double *burnp)
1305{
1306 double aoa, drag, delta_v, E_now, E_lim, E_targ, tas_lim;
1307 double fl = alt2fl(alt, qnh);
1308 double Ps = alt2press(alt, qnh);
1309 double oat = isadev2sat(fl, isadev);
1310 ASSERT3F(*kcasp, >, 0);
1311 double ktas_now = kcas2ktas(*kcasp, Ps, oat);
1312 double tas_now = KT2MPS(ktas_now);
1313 double tas_targ = KT2MPS(kcas2ktas(kcas_targ, Ps, oat));
1314 double Pd = dyn_press(ktas_now, Ps, oat);
1315 double throttle = accel ? 1.0 : 0.0;
1316 double thr = eng_get_thrust(flt, acft, throttle, alt, ktas_now, qnh,
1317 isadev, tp_alt);
1318 double burn = *burnp;
1319 double t = *timep;
1320 double altm = FEET2MET(alt);
1321
1322 if (gnd) {
1323 aoa = 0;
1324 } else {
1325 aoa = get_aoa(Pd, mass, flap_ratio, acft);
1326 }
1327 ASSERT(!isnan(aoa));
1328 drag = get_drag(Pd, aoa, flap_ratio, acft);
1329 /* Prevent deceleration */
1330 delta_v = MAX((thr - drag) / mass, 0);
1331
1332 tas_lim = tas_now + delta_v * t;
1333 E_now = calc_total_E(mass, altm, tas_now);
1334 E_lim = calc_total_E(mass, altm, tas_lim);
1335 E_targ = calc_total_E(mass, altm, tas_targ);
1336
1337 if (accel ? E_targ > E_lim : E_targ < E_lim) {
1338 *kcasp = ktas2kcas(MPS2KT(tas_lim), Ps, oat);
1339 } else {
1340 t *= ((E_targ - E_now) / (E_lim - E_now));
1341 *kcasp = ktas2kcas(MPS2KT(tas_targ), Ps, oat);
1342 *timep = t;
1343 }
1344
1345 if (t > 0) {
1346 burn += acft_get_sfc(flt, acft, thr, alt, ktas_now, qnh,
1347 isadev, tp_alt) * (t / SECS_PER_HR);
1348 }
1349
1350 *burnp = burn;
1351 double dist = MET2NM(tas_now * t + 0.5 * delta_v * POW2(t) +
1352 wind_mps * t);
1353 (*distp) += MAX(dist, 0);
1354}
1355
1356static void
1357clb_table_step(const acft_perf_t *acft, double isadev, double qnh, double alt,
1358 double spd, bool_t is_mach, double mass, double wind_mps, double d_t,
1359 double *nalt, double *nburn, double *ndist)
1360{
1361 double ff, vs;
1362 double kcas, ktas_now, tas_now;
1363 double fl = alt2fl(alt, qnh);
1364 double Ps = alt2press(alt, qnh);
1365 double oat = isadev2sat(fl, isadev);
1366
1367 ASSERT(acft != NULL);
1368 ASSERT(acft->clb_tables != NULL);
1369 ASSERT3F(spd, >, 0);
1370 ASSERT(nalt != NULL);
1371 ASSERT(nburn != NULL);
1372 ASSERT3F(d_t, >=, 0);
1373
1374 ff = table_lookup_common(acft->clb_tables, isadev, mass, spd, is_mach,
1375 alt, offsetof(perf_table_cell_t, ff));
1376 ff = MAX(ff, 0);
1377 vs = table_lookup_common(acft->clb_tables, isadev, mass, spd, is_mach,
1378 alt, offsetof(perf_table_cell_t, vs));
1379 vs = MAX(vs, 0);
1380
1381 if (is_mach) {
1382 ktas_now = mach2ktas(spd, oat);
1383 kcas = ktas2kcas(ktas_now, Ps, oat);
1384 } else {
1385 kcas = MPS2KT(spd);
1386 ktas_now = kcas2ktas(kcas, Ps, oat);
1387 }
1388 tas_now = KT2MPS(ktas_now);
1389
1390 *nalt = alt + vs * d_t;
1391 *nburn = ff * d_t;
1392 *ndist = MAX(tas_now + wind_mps, 0) * d_t;
1393}
1394
1395static void
1396crz_step(double isadev, double tp_alt, double qnh, double alt_ft,
1397 double spd_mps_or_mach, bool_t is_mach, double wind_mps, double mass,
1398 const acft_perf_t *acft, const flt_perf_t *flt, double dist_nm,
1399 double d_t, double *distp, double *burnp, double *ttg_out)
1400{
1401 double burn, kcas, ktas_now, tas_now, burn_step, dist_step;
1402 double fl = alt2fl(alt_ft, qnh);
1403 double Ps = alt2press(alt_ft, qnh);
1404 double oat = isadev2sat(fl, isadev);
1405
1406 ASSERT(acft != NULL);
1407 ASSERT(flt != NULL);
1408 ASSERT3F(dist_nm, >=, 0);
1409 ASSERT(distp != NULL);
1410 ASSERT(burnp != NULL);
1411 ASSERT3F(mass, >, 0);
1412 burn = *burnp;
1413
1414 if (is_mach) {
1415 ktas_now = mach2ktas(spd_mps_or_mach, oat);
1416 kcas = ktas2kcas(ktas_now, Ps, oat);
1417 } else {
1418 kcas = MPS2KT(spd_mps_or_mach);
1419 ktas_now = kcas2ktas(kcas, Ps, oat);
1420 }
1421 tas_now = KT2MPS(ktas_now);
1422
1423 if (acft->crz_tables != NULL) {
1424 double ff = table_lookup_common(acft->crz_tables, isadev, mass,
1425 spd_mps_or_mach, is_mach, FEET2MET(alt_ft),
1426 offsetof(perf_table_cell_t, ff));
1427 ff = MAX(ff, 0);
1428 burn_step = ff * d_t;
1429 if (step_debug) {
1430 double spd_kias_or_mach = (is_mach ? spd_mps_or_mach :
1431 MPS2KT(spd_mps_or_mach));
1432 printf("CRZ:%5.0f ft m:%5.0f spd:%.*f lb ff:%4.0f "
1433 "lb/hr/eng\n", alt_ft, KG2LBS(mass),
1434 is_mach ? 3 : 0, spd_kias_or_mach,
1435 KG2LBS(ff) * SECS_PER_HR / get_num_eng(flt, acft));
1436 }
1437 } else {
1438 double aoa, drag, thr, sfc, Pd;
1439
1440 Pd = dyn_press(ktas_now, Ps, oat);
1441 aoa = get_aoa(Pd, mass, 0, acft);
1442 ASSERT(!isnan(aoa));
1443 drag = get_drag(Pd, aoa, 0, acft);
1444 thr = drag;
1445 sfc = acft_get_sfc(flt, acft, thr, alt_ft, ktas_now, qnh,
1446 isadev, tp_alt);
1447 printf("Ps: %.0f Pd: %.0f kcas: %.0f aoa: %.3f drag: %.2f "
1448 "sfc: %.1f gw: %.1f\n", Ps, Pd, kcas, aoa, drag / 1000,
1449 KG2LBS(sfc) / get_num_eng(flt, acft), KG2LBS(mass) / 1000);
1450 burn_step = sfc * (d_t / SECS_PER_HR);
1451 }
1452 /*
1453 * The MAX here is important to make sure we keep making forward
1454 * progress, otherwise the solver can soft-lock.
1455 */
1456 dist_step = MAX(tas_now + wind_mps, KT2MPS(60)) * d_t;
1457 if ((*distp) + MET2NM(dist_step) > dist_nm) {
1458 double rat = (dist_nm - (*distp)) / MET2NM(dist_step);
1459 burn_step *= rat;
1460 dist_step = NM2MET(dist_nm - (*distp));
1461 if (ttg_out != NULL)
1462 (*ttg_out) += d_t * rat;
1463 } else {
1464 if (ttg_out != NULL)
1465 (*ttg_out) += d_t;
1466 }
1467 burn += burn_step;
1468 *burnp = burn;
1469 (*distp) += MET2NM(dist_step);
1470}
1471
1472/*
1473 * Performs a climb simulation step.
1474 *
1475 * @param isadev Same as `isadev' in spd_chg_step.
1476 * @param tp_alt Same as `tp_alt' in spd_chg_step.
1477 * @param qnh Same as `qnh' in spd_chg_step.
1478 * @param altp Input/output argument containing the aircraft's current
1479 * altitude in feet. It will be adjusted with the altitude achieved
1480 * during the climb.
1481 * @param kcasp Input/output argument containing the aircraft's current
1482 * calibrated airspeed in knots. After climb, we recalculate the
1483 * effective calibrated airspeed at the new altitude and update kcasp.
1484 * @param alt_targ Climb target altitude in feet.
1485 * @param wind_mps Same as `wind_mps' in spd_chg_step.
1486 * @param mass Same as `mass' in spd_chg_step.
1487 * @param flap_ratio Same as `flap_ratio' in spd_chg_step.
1488 * @param acft Same as `acft' in spd_chg_step.
1489 * @param flt Same as `flt' in spd_chg_step.
1490 * @param distp Same as `distp' in spd_chg_step.
1491 * @param timep Same as `timep' in spd_chg_step.
1492 * @param burnp Same as `burnp' in spd_chg_step.
1493 *
1494 * @return B_TRUE on success, B_FALSE on failure (insufficient speed to
1495 * sustain flight or thrust to maintain speed).
1496 */
1497static void
1498alt_chg_step(bool_t clb, double isadev, double tp_alt, double qnh,
1499 double *altp, double *vsp, double *kcasp, double alt_targ, double wind_mps,
1500 double mass, double flap_ratio, const acft_perf_t *acft,
1501 const flt_perf_t *flt, double *distp, double *timep, double *burnp)
1502{
1503 double aoa, drag, E_now, E_lim, E_targ;
1504 double alt = *altp;
1505 double fl = alt2fl(alt, qnh);
1506 double Ps = alt2press(alt, qnh);
1507 double oat = isadev2sat(fl, isadev);
1508 ASSERT3F(*kcasp, >, 0);
1509 double ktas_now = kcas2ktas(*kcasp, Ps, oat);
1510 double tas_now = KT2MPS(ktas_now);
1511 double Pd = dyn_press(ktas_now, Ps, oat);
1512 double throttle = clb ? 1.0 : 0.0;
1513 double thr = eng_get_thrust(flt, acft, throttle, alt, ktas_now, qnh,
1514 isadev, tp_alt);
1515 double burn = *burnp;
1516 double t = *timep;
1517 double altm = FEET2MET(alt);
1518
1519 aoa = get_aoa(Pd, mass, flap_ratio, acft);
1520 ASSERT(!isnan(aoa));
1521 drag = get_drag(Pd, aoa, flap_ratio, acft);
1522 /* Prevent a trend reversal - worst case guesses */
1523 if (clb) {
1524 thr = MAX(thr, drag);
1525 } else {
1526 thr = MIN(thr, drag);
1527 }
1528
1529 E_now = calc_total_E(mass, altm, tas_now);
1530 E_lim = E_now + (thr - drag) * tas_now * t;
1531 E_targ = calc_total_E(mass, FEET2MET(alt_targ), tas_now);
1532
1533 if (clb ? E_targ > E_lim : E_targ < E_lim) {
1534 double nalt = total_E_to_alt(E_lim, mass, tas_now);
1535 double vs_tgt = (nalt - FEET2MET(*altp)) / t;
1536 double v_accel = (vs_tgt - (*vsp)) / t;
1537
1538 v_accel = clamp(v_accel, -2.5, 2.5);
1539 vs_tgt = (*vsp) + (v_accel * t);
1540 *altp = MET2FEET(FEET2MET(*altp) + vs_tgt * t);
1541 *vsp = vs_tgt;
1542 } else {
1543 t *= ((E_targ - E_now) / (E_lim - E_now));
1544 *altp = alt_targ;
1545 *timep = t;
1546 }
1547
1548 /* adjust kcas to new altitude */
1549 Ps = alt2press(*altp, qnh);
1550 fl = alt2fl(*altp, qnh);
1551 oat = isadev2sat(fl, isadev);
1552 *kcasp = ktas2kcas(ktas_now, Ps, oat);
1553
1554 /* use average air density to use in burn estimation */
1555 burn += acft_get_sfc(flt, acft, thr, alt, ktas_now, qnh, isadev,
1556 tp_alt) * (t / SECS_PER_HR);
1557
1558 *burnp = burn;
1559 double dist = MET2NM(sqrt(POW2(tas_now * t) +
1560 FEET2MET(POW2((*altp) - alt))) + wind_mps * t);
1561 (*distp) += MAX(dist, 0);
1562}
1563
1564static double
1565des_burn_step(double isadev, double alt_m, double vs_act_mps,
1566 double spd_mps_or_mach, bool_t is_mach, double mass,
1567 const acft_perf_t *acft, double d_t)
1568{
1569 double ff_des = table_lookup_common(acft->des_tables, isadev, mass,
1570 spd_mps_or_mach, is_mach, alt_m, offsetof(perf_table_cell_t, ff));
1571 double ff_crz = table_lookup_common(acft->crz_tables, isadev, mass,
1572 spd_mps_or_mach, is_mach, alt_m, offsetof(perf_table_cell_t, ff));
1573 double vs_des_mps = table_lookup_common(acft->des_tables, isadev, mass,
1574 spd_mps_or_mach, is_mach, alt_m, offsetof(perf_table_cell_t, vs));
1575 double rat = iter_fract(vs_act_mps, 0, vs_des_mps, B_TRUE);
1576 double burn;
1577 /*
1578 * Prevent sliding off the tables into nonsense.
1579 */
1580 ff_des = MAX(ff_des, 0);
1581 ff_crz = MAX(ff_crz, 0);
1582 burn = wavg(ff_crz, ff_des, rat) * d_t;
1583 if (step_debug) {
1584 printf("DES:%-5.0f ft m:%-5.0f lb vs:%-5.0f fpm "
1585 "ff_crz:%-4.0f lbs/hr ff_des:%-4.0f rat:%.3f\n",
1586 MET2FEET(alt_m), KG2LBS(mass), MPS2FPM(vs_des_mps),
1587 KG2LBS(ff_crz) * SECS_PER_HR, KG2LBS(ff_des) * SECS_PER_HR,
1588 rat);
1589 }
1590 ASSERT3F(burn, >=, 0);
1591 return (burn);
1592}
1593
1594/*
1595 * ACCEL_THEN_CLB first accelerates to kcas2 and then climbs.
1596 * ACCEL_TAKEOFF first accelerates to flt->clb_ias_init, then climbs until
1597 * reaching accel_alt, then does a 50/50 time split to reach target climb spd.
1598 * ACCEL_AND_CLB does a 50/50 time split.
1599 * If `flap_ratio_act' is not NULL, it is set to:
1600 * 1) `flap_ratio_takeoff', when in 1st and 2nd segment climb, or
1601 * 2) `flap_ratio', otherwise
1602 */
1603static double
1604accel_time_split(accelclb_t type, double kcas, double clbias, double alt,
1605 double accel_alt, double t, double flap_ratio, double flap_ratio_takeoff,
1606 double *flap_ratio_act)
1607{
1608 if (flap_ratio_act != NULL)
1609 *flap_ratio_act = flap_ratio;
1610
1611 switch (type) {
1612 case ACCEL_THEN_CLB:
1613 return (t);
1614 case ACCEL_TAKEOFF:
1615 if (kcas < clbias) {
1616 if (flap_ratio_act != NULL)
1617 *flap_ratio_act = flap_ratio_takeoff;
1618 return (t);
1619 }
1620 if (alt < accel_alt) {
1621 if (flap_ratio_act != NULL)
1622 *flap_ratio_act = flap_ratio_takeoff;
1623 return (0);
1624 }
1625 return (t / 2);
1626 default:
1627 VERIFY3U(type, ==, ACCEL_AND_CLB);
1628 return (t / 2);
1629 }
1630}
1631
1632static double
1633select_step(accelclb_t type)
1634{
1635 if (type == ACCEL_TAKEOFF)
1636 return (SECS_PER_STEP_TAKEOFF);
1637 else
1638 return (SECS_PER_STEP);
1639}
1640
1641static bool_t
1642should_use_clb_tables(const acft_perf_t *acft, accelclb_t type,
1643 double kcas, double kcas_lim)
1644{
1645 return (acft->clb_tables != NULL &&
1646 (type == ACCEL_AND_CLB || type == ACCEL_THEN_CLB ||
1647 kcas_lim - kcas < KCAS_TABLE_THRESH));
1648}
1649
1650/*
1651 * Calculates the linear distance covered by an aircraft in wings-level
1652 * flight attempting to climb and accelerate. This is used in climb distance
1653 * performance estimates, especially when constructing altitude-terminated
1654 * procedure legs. This function assumes the engines will be running at
1655 * maximum thrust during the climb/acceleration phase (subject to
1656 * environmental limitations and configured performance derates).
1657 *
1658 * @param flt Flight performance settings.
1659 * @param acft Aircraft performance tables.
1660 * @param isadev Estimated average ISA deviation during climb phase.
1661 * @param qnh Estimated average QNH during climb phase.
1662 * @param tp_alt Altitude of the tropopause in feet AMSL.
1663 * @param fuel Current fuel state in kg.
1664 * @param dir Unit vector pointing in the flight direction of the aircraft.
1665 * @param alt1 Climb/acceleration starting altitude in feet AMSL.
1666 * @param kcas1 Climb/acceleration starting calibrated airspeed in knots.
1667 * @param wind1 Wind vector at start of climb/acceleration phase. The
1668 * vector's direction expresses the wind direction and its magnitude
1669 * the wind's true speed in knots.
1670 * @param alt2 Climb/acceleration target altitude AMSL in feet. Must be
1671 * greater than or equal to alt1.
1672 * @param kcas2 Climb/acceleration target calibrated airspeed in knots.
1673 * @param wind2 Wind vector at end of climb/acceleration phase. The wind is
1674 * assumed to vary smoothly between alt1/wind1 and alt2/wind2.
1675 * @param flap_ratio Average flap setting between 0 and 1 (inclusive) during
1676 * climb/acceleration phase. This expresses how muct lift and drag is
1677 * produced by the wings as a fraction between CL/CLFLAP and CD/CDFLAP.
1678 * A setting of 0 means flaps up, whereas 1 is flaps fully extended.
1679 * @param type Type of acceleration/climb procedure to execute.
1680 * @param burnp Return parameter which if not NULL will be filled with the
1681 * amount of fuel consumed during the acceleration/climb phase (in kg).
1682 *
1683 * @return Distance over the ground covered during acceleration/climb
1684 * maneuver in NM.
1685 */
1686double
1687accelclb2dist(const flt_perf_t *flt, const acft_perf_t *acft, double isadev,
1688 double qnh, double tp_alt, double accel_alt, double fuel, vect2_t dir,
1689 double alt1_ft, double kcas1, vect2_t wind1,
1690 double alt2_ft, double kcas2, vect2_t wind2,
1691 double flap_ratio, double mach_lim, accelclb_t type, double *burnp,
1692 double *kcas_out)
1693{
1694 double alt = alt1_ft, kcas = kcas1, burn = 0, dist = 0;
1695 double step = select_step(type);
1696 double flap_ratio_act;
1697 int iter_counter = 0;
1698 double vs = 0;
1699
1700 ASSERT3F(alt1_ft, <=, alt2_ft);
1701 ASSERT3F(fuel, >=, 0);
1702 ASSERT(!isnan(accel_alt) || type != ACCEL_TAKEOFF);
1703 dir = vect2_unit(dir, NULL);
1704
1705 /* Iterate in steps of `step'. */
1706 while (alt2_ft - alt > ALT_THRESH && kcas2 - kcas > KCAS_THRESH) {
1707 double wind_mps, alt_fract, accel_t, clb_t, ktas_lim_mach,
1708 kcas_lim_mach, oat, kcas_lim;
1709 double Ps;
1710 vect2_t wind;
1711 /* debugging support */
1712 double old_alt = alt;
1713 double old_kcas = kcas;
1714 bool_t table = B_FALSE;
1715
1716 ASSERT3S(iter_counter, <, MAX_ITER_STEPS);
1717
1718 oat = isadev2sat(alt2fl(alt, qnh), isadev);
1719 Ps = alt2press(alt, qnh);
1720 ktas_lim_mach = mach2ktas(mach_lim, oat);
1721 kcas_lim_mach = ktas2kcas(ktas_lim_mach, Ps, oat);
1722
1723 kcas_lim = kcas2;
1724 for (int i = 0; i < FLT_PERF_NUM_SPD_LIMS; i++) {
1725 if (alt < flt->clb_spd_lim[i].alt_ft) {
1726 kcas_lim = MIN(kcas_lim,
1727 flt->clb_spd_lim[i].kias);
1728 }
1729 }
1730 if (kcas_lim > kcas_lim_mach)
1731 kcas_lim = kcas_lim_mach;
1732 if (alt2_ft - alt < ALT_THRESH && kcas_lim < kcas2)
1733 kcas2 = kcas_lim;
1734
1735 /*
1736 * Calculate the directional wind component. This will be
1737 * factored into the distance traveled estimation below.
1738 */
1739 alt_fract = (alt - alt1_ft) / (alt2_ft - alt1_ft);
1740 wind = VECT2(wavg(wind1.x, wind2.x, alt_fract),
1741 wavg(wind1.y, wind2.y, alt_fract));
1742 wind_mps = KT2MPS(vect2_dotprod(wind, dir));
1743 /*
1744 * Swap to accel-and-climb tabulated profiles when we're
1745 * 1000ft above the acceleration altitude.
1746 */
1747 if (type == ACCEL_TAKEOFF && alt > accel_alt + 1000)
1748 type = ACCEL_AND_CLB;
1749
1750 accel_t = accel_time_split(type, kcas, flt->clb_ias_init,
1751 alt, accel_alt, step, flap_ratio, flt->to_flap,
1752 &flap_ratio_act);
1753
1754 /*
1755 * We can try to use climb performance tables for a more
1756 * accurate estimate, provided that all of the following
1757 * conditions are satisfied:
1758 * 1) climb tables are available
1759 * 2) no more acceleration is required (in normal climb)
1760 * 3) our speed is within the airspeed target (acceleration
1761 * phase complete)
1762 */
1763 if (should_use_clb_tables(acft, type, kcas, kcas_lim)) {
1764 bool_t is_mach = (kcas2 > kcas_lim_mach);
1765 double spd = (is_mach ? kcas_lim_mach : KT2MPS(kcas2));
1766 double nalt, nburn, ndist;
1767
1768 clb_table_step(acft, isadev, qnh, FEET2MET(alt),
1769 spd, is_mach, flt->zfw + fuel - burn, wind_mps,
1770 step, &nalt, &nburn, &ndist);
1771 alt = MET2FEET(nalt);
1772 burn += nburn;
1773 dist += MET2NM(ndist);
1774 clb_t = step - accel_t;
1775 if (is_mach)
1776 kcas = kcas_lim_mach;
1777 if (step_debug)
1778 table = B_TRUE;
1779 } else {
1780 if (accel_t > 0) {
1781 spd_chg_step(B_TRUE, isadev, tp_alt,
1782 qnh, type == ACCEL_TAKEOFF &&
1783 alt == alt1_ft, alt, &kcas, kcas_lim,
1784 wind_mps, flt->zfw + fuel - burn,
1785 flap_ratio_act, acft, flt, &dist,
1786 &accel_t, &burn);
1787 }
1788
1789 clb_t = step - accel_t;
1790 if (clb_t > 0 && alt2_ft - alt > ALT_THRESH) {
1791 alt_chg_step(B_TRUE, isadev, tp_alt, qnh,
1792 &alt, &vs, &kcas, alt2_ft, wind_mps,
1793 flt->zfw + fuel - burn, flap_ratio_act,
1794 acft, flt, &dist, &clb_t, &burn);
1795 }
1796 }
1797
1798 if (step_debug) {
1799 double total_t;
1800
1801 total_t = accel_t + clb_t;
1802 oat = isadev2sat(alt2fl(alt, qnh), isadev);
1803
1804 printf("V:%3.0f KT +V:%5.02lf H:%5.0lf fpm:%4.0lf "
1805 "s:%6.0lf M:%5.03lf tab:%d\n", kcas,
1806 (kcas - old_kcas) / total_t, alt,
1807 ((alt - old_alt) / total_t) * 60, NM2MET(dist),
1808 ktas2mach(kcas2ktas(kcas, alt2press(alt, qnh), oat),
1809 oat), table);
1810 }
1811
1812 iter_counter++;
1813 }
1814 if (burnp != NULL)
1815 *burnp = burn;
1816 if (kcas_out != NULL)
1817 *kcas_out = kcas;
1818 ASSERT3F(dist, >=, 0);
1819
1820 return (dist);
1821}
1822
1824dist2accelclb(const flt_perf_t REQ_PTR(flt), const acft_perf_t REQ_PTR(acft),
1825 double isadev, double qnh, double tp_alt, double accel_alt,
1826 double fuel, vect2_t dir, double flap_ratio, double REQ_PTR(alt_ft_p),
1827 double REQ_PTR(kcas_p), vect2_t wind, double alt_tgt_ft, double kcas_tgt,
1828 double mach_lim, double dist_tgt, accelclb_t type, double *burnp,
1829 double *ttg_out)
1830{
1831 ASSERT3F(*alt_ft_p, <=, alt_tgt_ft);
1832 double alt_ft = *alt_ft_p;
1833 double alt1_ft = alt_ft;
1834 double dist = 0, burn = 0;
1835 double wind_mps = KT2MPS(vect2_dotprod(wind, dir));
1836 double step = select_step(type);
1837 double flap_ratio_act;
1838 int iter_counter = 0;
1839 double vs = 0;
1840
1841 ASSERT3F(*kcas_p, >, 0);
1842 ASSERT3F(*kcas_p, <=, kcas_tgt);
1843 double kcas = *kcas_p;
1844 ASSERT(!isnan(accel_alt) || type != ACCEL_TAKEOFF);
1845 double ttg = 0;
1846 /*
1847 * If the dist_tgt is very large, or we're flying very slowly, we
1848 * might run up against MAX_ITER_STEPS too early. So allow adjusting
1849 * the step size to hopefully make sure we reach the target before
1850 * running up against the iteration limit.
1851 */
1852 {
1853 double oat_guess = isadev2sat(alt2fl(alt_tgt_ft,
1854 ISA_SL_PRESS), 0);
1855 double pressure_guess = alt2press(alt_tgt_ft, ISA_SL_PRESS);
1856 double ktas_guess = kcas2ktas(kcas_tgt, pressure_guess,
1857 oat_guess);
1858 double min_step = ((dist_tgt / ktas_guess) * 3600) /
1859 MAX_ITER_STEPS;
1860 step = MAX(step, min_step * 2);
1861 }
1862 while (dist < dist_tgt && alt_tgt_ft - alt_ft > ALT_THRESH) {
1863 double tas_mps = KT2MPS(kcas2ktas(kcas, alt2press(alt_ft, qnh),
1864 isadev2sat(alt2fl(alt_ft, qnh), isadev)));
1865 double rmng = NM2MET(dist_tgt - dist);
1866 ASSERT3F(tas_mps, >, 0);
1867 double t_rmng = MIN(rmng / tas_mps, step);
1868 double accel_t, clb_t, oat, Ps, ktas_lim_mach, kcas_lim_mach,
1869 kcas_lim;
1870 /* step debug support */
1871 double old_alt = alt_ft;
1872 double old_kcas = kcas;
1873 bool_t table = B_FALSE;
1874
1875 if (iter_counter >= MAX_ITER_STEPS) {
1876 // Solution didn't converge, abort
1877 return (NONE(double));
1878 }
1879 oat = isadev2sat(alt2fl(alt_ft, qnh), isadev);
1880 Ps = alt2press(alt_ft, qnh);
1881 ktas_lim_mach = mach2ktas(mach_lim, oat);
1882 kcas_lim_mach = ktas2kcas(ktas_lim_mach, Ps, oat);
1883
1884 kcas_lim = kcas_tgt;
1885 for (int i = 0; i < FLT_PERF_NUM_SPD_LIMS; i++) {
1886 if (alt_ft < flt->clb_spd_lim[i].alt_ft) {
1887 kcas_lim = MIN(kcas_lim,
1888 flt->clb_spd_lim[i].kias);
1889 }
1890 }
1891 if (kcas_lim > kcas_lim_mach)
1892 kcas_lim = kcas_lim_mach;
1893 if (alt_tgt_ft - alt_ft < ALT_THRESH && kcas_lim < kcas_tgt)
1894 kcas_tgt = kcas_lim;
1895 /*
1896 * Swap to accel-and-climb tabulated profiles when we're
1897 * 1000ft above the acceleration altitude.
1898 */
1899 if (type == ACCEL_TAKEOFF && alt_ft > accel_alt + 1000)
1900 type = ACCEL_AND_CLB;
1901 /*
1902 * ACCEL_THEN_CLB and ACCEL_TAKEOFF first accelerate to kcas2
1903 * and then climb. ACCEL_AND_CLB does a 50/50 time split.
1904 */
1905 accel_t = accel_time_split(type, kcas, flt->clb_ias_init,
1906 alt_ft, accel_alt, t_rmng, flap_ratio, flt->to_flap,
1907 &flap_ratio_act);
1908
1909 if (should_use_clb_tables(acft, type, kcas, kcas_lim)) {
1910 bool_t is_mach = (kcas_tgt >= kcas_lim_mach);
1911 double spd = (is_mach ? mach_lim :
1912 KT2MPS(kcas_tgt));
1913 double nalt, nburn, ndist;
1914
1915 clb_table_step(acft, isadev, qnh, FEET2MET(alt_ft),
1916 spd, is_mach, flt->zfw + fuel - burn, wind_mps,
1917 step, &nalt, &nburn, &ndist);
1918 alt_ft = MET2FEET(nalt);
1919 burn += nburn;
1920 dist += MET2NM(ndist);
1921 clb_t = step - accel_t;
1922 if (is_mach)
1923 kcas = kcas_lim_mach;
1924 else
1925 kcas = kcas_lim;
1926 if (step_debug)
1927 table = B_TRUE;
1928 } else {
1929 if (accel_t > 0) {
1930 spd_chg_step(B_TRUE, isadev, tp_alt,
1931 qnh, type == ACCEL_TAKEOFF &&
1932 alt_ft == alt1_ft, alt_ft, &kcas, kcas_lim,
1933 wind_mps, flt->zfw + fuel - burn,
1934 flap_ratio_act, acft, flt, &dist,
1935 &accel_t, &burn);
1936 }
1937
1938 clb_t = t_rmng - accel_t;
1939 if (clb_t > 0 && alt_tgt_ft - alt_ft > ALT_THRESH) {
1940 alt_chg_step(B_TRUE, isadev, tp_alt, qnh,
1941 &alt_ft, &vs, &kcas, alt_tgt_ft, wind_mps,
1942 flt->zfw + fuel - burn, flap_ratio_act,
1943 acft, flt, &dist, &clb_t, &burn);
1944 }
1945 }
1946
1947 if (step_debug) {
1948 double total_t;
1949
1950 total_t = accel_t + clb_t;
1951 oat = isadev2sat(alt2fl(alt_ft, qnh), isadev);
1952
1953 printf("V:%5.01lf +V:%5.02lf H:%5.0lf fpm:%4.0lf "
1954 "s:%6.0lf M:%5.03lf tab:%d\n", kcas,
1955 ((kcas) - old_kcas) / total_t, alt_ft,
1956 (((alt_ft) - old_alt) / total_t) * 60,
1957 NM2MET(dist), ktas2mach(kcas2ktas(kcas,
1958 alt2press(alt_ft, qnh), oat), oat), table);
1959 }
1960 ttg += step;
1961 iter_counter++;
1962 }
1963 // write out state variables
1964 *alt_ft_p = alt_ft;
1965 *kcas_p = kcas;
1966 if (burnp != NULL)
1967 *burnp = burn;
1968 /* ttg_out can be NULL */
1969 if (ttg_out != NULL)
1970 *ttg_out = ttg;
1971
1972 ASSERT(!isnan(dist));
1973 ASSERT(isfinite(dist));
1974 ASSERT3F(dist, >=, 0);
1975 return (SOME(dist));
1976}
1977
1978double
1979decel2dist(const flt_perf_t *flt, const acft_perf_t *acft,
1980 double isadev, double qnh, double tp_alt, double fuel,
1981 double alt, double kcas1, double kcas2, double dist_tgt,
1982 double *kcas_out, double *burn_out)
1983{
1984 double dist = 0, burn = 0;
1985 double step = SECS_PER_STEP_DECEL;
1986 double kcas = kcas1;
1987 double oat = isadev2sat(alt2fl(alt, qnh), isadev);
1988
1989 while (dist < dist_tgt && kcas + KCAS_THRESH > kcas2) {
1990 double t = step;
1991 double old_kcas = kcas;
1992 double mach;
1993
1994 spd_chg_step(B_FALSE, isadev, tp_alt, qnh, B_FALSE,
1995 alt, &kcas, kcas2, 0, flt->zfw + fuel - burn,
1996 0, acft, flt, &dist, &t, &burn);
1997
1998 if (step_debug) {
1999 mach = ktas2mach(kcas2ktas(kcas, alt2press(alt, qnh),
2000 oat), oat);
2001 printf("V:%5.01lf +V:%5.02lf H:%5.0lf s:%6.0lf "
2002 "M:%5.03lf\n", kcas, (kcas - old_kcas) / t, alt,
2003 NM2MET(dist), mach);
2004 }
2005 }
2006
2007 if (kcas_out != NULL)
2008 *kcas_out = kcas;
2009 if (burn_out != NULL)
2010 *burn_out = burn;
2011
2012 ASSERT(!isnan(dist));
2013 ASSERT(isfinite(dist));
2014 ASSERT3F(dist, >=, 0);
2015 return (dist);
2016}
2017
2018/*
2019 * Estimates fuel burn in level, non-accelerated flight (cruise).
2020 * Flaps are assumed up in this configuration.
2021 *
2022 * @param isadev Average ISA deviation in degree C.
2023 * @param qnh Average QNH in Pa.
2024 * @param alt Cruise altitude in feet.
2025 * @param spd Airspeed, either as calibrated airspeed in knots, or Mach number.
2026 * @param is_mach B_TRUE if spd is mach, B_FALSE if spd is kcas.
2027 * @param hdg Cruise heading in degrees.
2028 * @param wind1 Cruise wind component at start of leg (direction of
2029 * vector is direction of wind, magnitude is wind velocity in knots).
2030 * @param wind2 Cruise wind component at end of leg.
2031 * @param fuel Fuel status at start of leg in kilograms.
2032 * @param dist_nm Cruise flight leg length in nautical miles.
2033 * @param acft Aircraft performance data structure.
2034 * @param flt Flight performance data structure.
2035 *
2036 * @return Amount of fuel burned in kilograms.
2037 */
2038double
2039perf_crz2burn(double isadev, double tp_alt, double qnh, double alt_ft,
2040 double spd, bool_t is_mach, double hdg, vect2_t wind1, vect2_t wind2,
2041 double fuel, double dist_nm, const acft_perf_t *acft, const flt_perf_t *flt,
2042 double *ttg_out)
2043{
2044 vect2_t fltdir;
2045 double burn = 0;
2046
2047 ASSERT(is_valid_alt_ft(alt_ft));
2048 ASSERT3F(spd, >, 0);
2049 ASSERT3F(spd, <, 1000);
2050 ASSERT(is_valid_hdg(hdg));
2051 ASSERT(!IS_NULL_VECT(wind1));
2052 ASSERT(!IS_NULL_VECT(wind2));
2053 ASSERT3F(dist_nm, >=, 0);
2054 ASSERT3F(dist_nm, <, 1e6);
2055 ASSERT(acft != NULL);
2056 ASSERT(flt != NULL);
2057 ASSERT3F(flt->zfw, >, 0);
2058 /* ttg_out can be NULL */
2059 if (ttg_out != NULL)
2060 *ttg_out = 0;
2061
2062 fltdir = hdg2dir(hdg);
2063 if (!is_mach)
2064 spd = KT2MPS(spd);
2065
2066 for (double dist_done = 0; dist_done < dist_nm;) {
2067 double rat = dist_done / dist_nm;
2068 double mass = flt->zfw + MAX(fuel - burn, 0);
2069 vect2_t wind = VECT2(wavg(wind1.x, wind2.y, rat),
2070 wavg(wind1.y, wind2.y, rat));
2071 double wind_mps = KT2MPS(vect2_dotprod(fltdir, wind));
2072
2073 crz_step(isadev, tp_alt, qnh, alt_ft, spd, is_mach,
2074 wind_mps, mass, acft, flt, dist_nm, SECS_PER_STEP_CRZ,
2075 &dist_done, &burn, ttg_out);
2076 }
2077
2078 ASSERT(!isnan(burn));
2079 ASSERT(isfinite(burn));
2080 ASSERT3F(burn, >=, 0);
2081 return (burn);
2082}
2083
2084double
2085perf_des2burn(const flt_perf_t *flt, const acft_perf_t *acft,
2086 double isadev, double qnh, double fuel, double hdgt,
2087 double dist_nm, double mach_lim,
2088 double alt1_ft, double kcas1, vect2_t wind1,
2089 double alt2_ft, double kcas2, vect2_t wind2,
2090 double *ttg_out)
2091{
2092 vect2_t fltdir;
2093 double burn = 0;
2094
2095 ASSERT(flt != NULL);
2096 ASSERT3F(flt->zfw, >, 0);
2097 ASSERT(acft != NULL);
2098 ASSERT(!isnan(isadev));
2099 ASSERT(!isnan(qnh));
2100 ASSERT(!isnan(fuel));
2101 ASSERT(is_valid_hdg(hdgt));
2102 ASSERT3F(dist_nm, >=, 0);
2103 ASSERT3F(mach_lim, >=, 0);
2104 ASSERT(is_valid_alt_ft(alt1_ft));
2105 ASSERT3F(kcas1, >, 0);
2106 ASSERT3F(kcas1, <, 1000);
2107 ASSERT(!IS_NULL_VECT(wind1));
2108 ASSERT(is_valid_alt_ft(alt2_ft));
2109 ASSERT3F(kcas2, >, 0);
2110 ASSERT3F(kcas2, <, 1000);
2111 ASSERT(!IS_NULL_VECT(wind2));
2112 ASSERT3F(alt1_ft, >=, alt2_ft);
2113 /* ttg_out can be NULL */
2114 if (ttg_out != NULL)
2115 *ttg_out = 0;
2116
2117 fltdir = hdg2dir(hdgt);
2118
2119 for (double dist_done = 0; dist_done < NM2MET(dist_nm);) {
2120 double rat = dist_done / NM2MET(dist_nm);
2121 double alt_ft = wavg(alt1_ft, alt2_ft, rat);
2122 double kcas = wavg(kcas1, kcas2, rat);
2123 double mass = flt->zfw + MAX(fuel - burn, 0);
2124 vect2_t wind = VECT2(wavg(wind1.x, wind2.y, rat),
2125 wavg(wind1.y, wind2.y, rat));
2126 double wind_mps = KT2MPS(vect2_dotprod(fltdir, wind));
2127 double p = alt2press(alt_ft, qnh);
2128 double fl = alt2fl(alt_ft, qnh);
2129 double oat = isadev2sat(fl, isadev);
2130 double kcas_lim_mach = mach2kcas(mach_lim, alt_ft, qnh, oat);
2131 bool_t is_mach;
2132 double tgt_spd, tas_mps, gs_mps, vs_mps, burn_step;
2133 double spd_mps_or_mach, dist_step;
2134
2135 for (int i = 0; i < FLT_PERF_NUM_SPD_LIMS; i++) {
2136 if (alt_ft <= flt->des_spd_lim[i].alt_ft)
2137 kcas = MIN(kcas, flt->des_spd_lim[i].kias);
2138 }
2139 is_mach = (kcas > kcas_lim_mach);
2140 tgt_spd = (is_mach ? mach_lim : kcas);
2141 if (is_mach) {
2142 tgt_spd = mach_lim;
2143 tas_mps = KT2MPS(kcas2ktas(kcas_lim_mach, p, oat));
2144 } else {
2145 tgt_spd = kcas;
2146 tas_mps = KT2MPS(kcas2ktas(kcas, p, oat));
2147 }
2148 /*
2149 * We must make sure we make forward progress,
2150 * otherwise the solver can soft-lock.
2151 */
2152 gs_mps = MAX(tas_mps + wind_mps, KT2MPS(60));
2153 vs_mps = (FEET2MET(alt2_ft) - FEET2MET(alt1_ft)) /
2154 (NM2MET(dist_nm) / gs_mps);
2155 spd_mps_or_mach = (is_mach ? tgt_spd : KT2MPS(tgt_spd));
2156
2157 burn_step = des_burn_step(isadev, FEET2MET(alt_ft), vs_mps,
2158 spd_mps_or_mach, is_mach, mass, acft, SECS_PER_STEP);
2159 ASSERT3F(burn_step, >=, 0);
2160 dist_step = gs_mps * SECS_PER_STEP;
2161 if (dist_done + dist_step > NM2MET(dist_nm)) {
2162 double act_dist_step = NM2MET(dist_nm) - dist_done;
2163 double rat = act_dist_step / dist_step;
2164 burn += burn_step * rat;
2165 dist_done += act_dist_step;
2166 if (ttg_out != NULL)
2167 (*ttg_out) += SECS_PER_STEP * rat;
2168 break;
2169 }
2170 burn += burn_step;
2171 dist_done += dist_step;
2172 if (ttg_out != NULL)
2173 (*ttg_out) += SECS_PER_STEP;
2174 }
2175
2176 return (burn);
2177}
2178
2179double
2180perf_TO_spd(const flt_perf_t *flt, const acft_perf_t *acft)
2181{
2182 double mass = flt->zfw + flt->fuel;
2183 double lift = MASS2GFORCE(mass);
2184 double Cl = wavg(fx_lin_multi(acft->cl_max_aoa, acft->cl_curve, B_TRUE),
2185 fx_lin_multi(acft->cl_flap_max_aoa, acft->cl_flap_curve, B_TRUE),
2186 flt->to_flap);
2187 double Pd = lift / (Cl * acft->wing_area);
2188 double tas = sqrt((2 * Pd) / ISA_SL_DENS);
2189 return (MPS2KT(tas));
2190}
2191
2192/*
2193 * Calculates the specific fuel consumption of the aircraft engines in
2194 * a given instant.
2195 *
2196 * @param acft Aircraft performance specification structure.
2197 * @param thr Total thrust requirement.
2198 *
2199 * @return The aircraft's engine's specific fuel consumption at the specified
2200 * conditions in kg/hr.
2201 */
2202double
2203acft_get_sfc(const flt_perf_t *flt, const acft_perf_t *acft, double thr,
2204 double alt, double ktas, double qnh, double isadev, double tp_alt)
2205{
2206 /* "_AE" means "all-engines" */
2207 double max_thr_AE, min_thr_AE, throttle;
2208 double ff_hr;
2209
2210 ASSERT(flt != NULL);
2211 ASSERT(acft != NULL);
2212
2213 ff_hr = acft->eng_sfc * thr * SECS_PER_HR;
2214 max_thr_AE = eng_get_thrust(flt, acft, 1.0, alt, ktas, qnh, isadev,
2215 tp_alt);
2216 min_thr_AE = eng_get_thrust(flt, acft, 0.0, alt, ktas, qnh, isadev,
2217 tp_alt);
2218 throttle = iter_fract(thr, min_thr_AE, max_thr_AE, B_TRUE);
2219
2220 return (ff_hr *
2221 fx_lin_multi(throttle, acft->sfc_thro_curve, B_TRUE) *
2222 fx_lin_multi(isadev, acft->sfc_isa_curve, B_TRUE));
2223}
2224
2225double
2226perf_get_turn_rate(double bank_ratio, double gs_kts, const flt_perf_t *flt,
2227 const acft_perf_t *acft)
2228{
2229 double half_bank_rate, full_bank_rate;
2230
2231 ASSERT3F(gs_kts, >=, 0);
2232 /* flt can be NULL */
2233 ASSERT(acft != NULL);
2234
2235 if (bank_ratio == 0) {
2236 ASSERT(flt != NULL);
2237 ASSERT3F(flt->bank_ratio, >, 0);
2238 ASSERT3F(flt->bank_ratio, <=, 1.0);
2239 bank_ratio = flt->bank_ratio;
2240 } else {
2241 ASSERT3F(bank_ratio, >, 0);
2242 ASSERT3F(bank_ratio, <=, 1.0);
2243 }
2244
2245 half_bank_rate = fx_lin_multi(gs_kts, acft->half_bank_curve, B_TRUE);
2246 if (bank_ratio <= 0.5)
2247 return (2 * bank_ratio * half_bank_rate);
2248 full_bank_rate = fx_lin_multi(gs_kts, acft->full_bank_curve, B_TRUE);
2249
2250 return (wavg(half_bank_rate, full_bank_rate,
2251 clamp(2 * (bank_ratio - 0.5), 0, 1)));
2252}
2253
2254/*
2255 * Converts a true airspeed to Mach number.
2256 *
2257 * @param ktas True airspeed in knots.
2258 * @param oat Static outside air temperature in degrees C.
2259 *
2260 * @return Mach number.
2261 */
2262double
2263ktas2mach(double ktas, double oat)
2264{
2265 return (KT2MPS(ktas) / speed_sound(oat));
2266}
2267
2268/*
2269 * Converts Mach number to true airspeed.
2270 *
2271 * @param ktas Mach number.
2272 * @param oat Static outside air temperature in degrees C.
2273 *
2274 * @return True airspeed in knots.
2275 */
2276double
2277mach2ktas(double mach, double oat)
2278{
2279 return (MPS2KT(mach * speed_sound(oat)));
2280}
2281
2282/*
2283 * Converts true airspeed to calibrated airspeed.
2284 *
2285 * @param ktas True airspeed in knots.
2286 * @param pressure Static air pressure in Pa.
2287 * @param oat Static outside air temperature in degrees C.
2288 *
2289 * @return Calibrated airspeed in knots.
2290 */
2291double
2292ktas2kcas(double ktas, double pressure, double oat)
2293{
2294 return (impact_press2kcas(impact_press(ktas2mach(ktas, oat),
2295 pressure)));
2296}
2297
2298/*
2299 * Converts impact pressure to calibrated airspeed.
2300 *
2301 * @param impact_pressure Impact air pressure in Pa.
2302 *
2303 * @return Calibrated airspeed in knots.
2304 */
2305double
2306impact_press2kcas(double impact_pressure)
2307{
2308 return (MPS2KT(ISA_SPEED_SOUND * sqrt(5 *
2309 (pow(impact_pressure / ISA_SL_PRESS + 1, 0.2857142857) - 1))));
2310}
2311
2312double
2313kcas2mach(double kcas, double alt_ft, double qnh, double oat)
2314{
2315 double p = alt2press(alt_ft, qnh);
2316 double ktas = kcas2ktas(kcas, p, oat);
2317 return (ktas2mach(ktas, oat));
2318}
2319
2320double mach2kcas(double mach, double alt_ft, double qnh, double oat)
2321{
2322 double ktas = mach2ktas(mach, oat);
2323 double p = alt2press(alt_ft, qnh);
2324 return (ktas2kcas(ktas, p, oat));
2325}
2326
2327/*
2328 * Converts calibrated airspeed to true airspeed.
2329 *
2330 * @param ktas Calibrated airspeed in knots.
2331 * @param pressure Static air pressure in Pa.
2332 * @param oat Static outside air temperature in degrees C.
2333 *
2334 * @return True airspeed in knots.
2335 */
2336double
2337kcas2ktas(double kcas, double pressure, double oat)
2338{
2339 double qc, mach;
2340
2341 /*
2342 * Take the CAS equation and solve for qc (impact pressure):
2343 *
2344 * qc = P0(((cas^2 / 5* a0^2) + 1)^3.5 - 1)
2345 *
2346 * Where P0 is pressure at sea level, cas is calibrated airspeed
2347 * in m.s^-1 and a0 speed of sound at ISA temperature.
2348 */
2349 qc = ISA_SL_PRESS * (pow((POW2(KT2MPS(kcas)) / (5 *
2350 POW2(ISA_SPEED_SOUND))) + 1, 3.5) - 1);
2351
2352 /*
2353 * Next take the impact pressure equation and solve for Mach number:
2354 *
2355 * M = sqrt(5 * (((qc / P) + 1)^(2/7) - 1))
2356 *
2357 * Where qc is impact pressure and P is local static pressure.
2358 */
2359 mach = sqrt(5 * (pow((qc / pressure) + 1, 0.2857142857142) - 1));
2360
2361 /*
2362 * Finally convert Mach number to true airspeed at local temperature.
2363 */
2364 return (mach2ktas(mach, oat));
2365}
2366
2367/*
2368 * Converts Mach number to equivalent airspeed (calibrated airspeed corrected
2369 * for compressibility).
2370 *
2371 * @param mach Mach number.
2372 * @param oat Static outside static air pressure in Pa.
2373 *
2374 * @return Equivalent airspeed in knots.
2375 */
2376double
2377mach2keas(double mach, double press)
2378{
2379 return (MPS2KT(ISA_SPEED_SOUND * mach * sqrt(press / ISA_SL_PRESS)));
2380}
2381
2382/*
2383 * Converts equivalent airspeed (calibrated airspeed corrected for
2384 * compressibility) to Mach number.
2385 *
2386 * @param mach Equivalent airspeed in knots.
2387 * @param oat Static outside static air pressure in Pa.
2388 *
2389 * @return Mach number.
2390 */
2391double
2392keas2mach(double keas, double press)
2393{
2394 /*
2395 * Take the mach-to-EAS equation and solve for Mach number:
2396 *
2397 * M = Ve / (a0 * sqrt(P / P0))
2398 *
2399 * Where Ve is equivalent airspeed in m.s^-1, P is local static
2400 * pressure and P0 is ISA sea level pressure (in Pa).
2401 */
2402 return (KT2MPS(keas) / (ISA_SPEED_SOUND * sqrt(press / ISA_SL_PRESS)));
2403}
2404
2405/*
2406 * Calculates static air pressure from pressure altitude under ISA conditions.
2407 *
2408 * @param alt Pressure altitude in feet.
2409 * @param qnh Local QNH in Pa.
2410 *
2411 * @return Air pressure in Pa.
2412 */
2413double
2414alt2press(double alt_ft, double qnh_Pa)
2415{
2416 return (alt2press_baro(FEET2MET(alt_ft), qnh_Pa, ISA_SL_TEMP_K,
2417 EARTH_GRAVITY));
2418}
2419
2420/*
2421 * Calculates pressure altitude from static air pressure under ISA conditions.
2422 *
2423 * @param press Static air pressure in Pa.
2424 * @param qnh Local QNH in Pa.
2425 *
2426 * @return Pressure altitude in feet.
2427 */
2428double
2429press2alt(double press_Pa, double qnh_Pa)
2430{
2431 return (MET2FEET(press2alt_baro(press_Pa, qnh_Pa, ISA_SL_TEMP_K,
2432 EARTH_GRAVITY)));
2433}
2434
2435double
2436alt2press_baro(double alt_m, double p0_Pa, double T0_K, double g_mss)
2437{
2438 ASSERT3F(p0_Pa, >, 0);
2439 ASSERT3F(T0_K, >, 0);
2440 ASSERT3F(g_mss, >, 0);
2441 /*
2442 * Standard barometric formula:
2443 * g.M
2444 * / L.h \^ ----
2445 * p = p0 * ( 1 - --- ) R0.L
2446 * \ T0 /
2447 * Where:
2448 * p current outside pressure [Pa]
2449 * p0 reference sea level pressure [Pa] (NOT QNH!)
2450 * L temperature lapse rate [K/m]
2451 * h height above mean sea level [m]
2452 * T0 reference sea level temperature [K]
2453 * g gravitational acceleration [m/s^2]
2454 * M molar mass of dry air [kg/mol]
2455 * R0 universal gas constant [J/(mol.K)]
2456 */
2457 return (p0_Pa * pow(1 - ((ISA_TLR_PER_1M * alt_m) / T0_K),
2458 (g_mss * DRY_AIR_MOL) / (R_univ * ISA_TLR_PER_1M)));
2459}
2460
2461double
2462press2alt_baro(double p_Pa, double p0_Pa, double T0_K, double g_mss)
2463{
2464 ASSERT3F(p0_Pa, >, 0);
2465 ASSERT3F(T0_K, >, 0);
2466 ASSERT3F(g_mss, >, 0);
2467 /*
2468 * This is the barometric formula, solved for 'h':
2469 * R0.L
2470 * / / p \^ ---- \
2471 * T0 * ( 1 - ( ---- ) g.M )
2472 * \ \ p0 / /
2473 * h = ----------------------------
2474 * L
2475 * Where:
2476 * h height above mean sea level [m]
2477 * p current outside pressure [Pa]
2478 * p0 reference sea level pressure [Pa] (NOT QNH!)
2479 * R0 universal gas constant [J/(mol.K)]
2480 * L temperature lapse rate [K/m]
2481 * g gravitational acceleration [m/s^2]
2482 * M molar mass of dry air [kg/mol]
2483 * T0 reference sea level temperature [K]
2484 */
2485 return ((T0_K * (1 - pow(p_Pa / p0_Pa, (R_univ * ISA_TLR_PER_1M) /
2486 (g_mss * DRY_AIR_MOL)))) / ISA_TLR_PER_1M);
2487}
2488
2489/*
2490 * Converts pressure altitude to flight level.
2491 *
2492 * @param alt Pressure altitude in feet.
2493 * @param qnh Local QNH in hPa.
2494 *
2495 * @return Flight level number.
2496 */
2497double
2498alt2fl(double alt_ft, double qnh)
2499{
2500 return (press2alt(alt2press(alt_ft, qnh), ISA_SL_PRESS) / 100);
2501}
2502
2503/*
2504 * Converts flight level to pressure altitude.
2505 *
2506 * @param fl Flight level number.
2507 * @param qnh Local QNH in hPa.
2508 *
2509 * @return Pressure altitude in feet.
2510 */
2511double
2512fl2alt(double fl, double qnh)
2513{
2514 return (press2alt(alt2press(fl * 100, ISA_SL_PRESS), qnh));
2515}
2516
2517/*
2518 * Converts static air temperature to total air temperature.
2519 *
2520 * @param sat Static air temperature in degrees C.
2521 * @param mach Flight mach number.
2522 *
2523 * @return Total air temperature in degrees C.
2524 */
2525double
2526sat2tat(double sat, double mach)
2527{
2528 return (KELVIN2C(C2KELVIN(sat) * (1 + ((GAMMA - 1) / 2) * POW2(mach))));
2529}
2530
2531/*
2532 * Converts total air temperature to static air temperature.
2533 *
2534 * @param tat Total air temperature in degrees C.
2535 * @param mach Flight mach number.
2536 *
2537 * @return Static air temperature in degrees C.
2538 */
2539double
2540tat2sat(double tat, double mach)
2541{
2542 return (KELVIN2C(C2KELVIN(tat) / (1 + ((GAMMA - 1) / 2) * POW2(mach))));
2543}
2544
2545/*
2546 * Converts static air temperature to ISA deviation. This function makes
2547 * no assumptions about a tropopause. To implement a tropopause, clamp
2548 * the passed `fl' value at the tropopause level (ISA_TP_ALT).
2549 *
2550 * @param fl Flight level (barometric altitude at QNE in 100s of ft).
2551 * @param sat Static air temperature in degrees C.
2552 *
2553 * @return ISA deviation in degress C.
2554 */
2555double
2556sat2isadev(double fl, double sat)
2557{
2558 fl = MIN(fl, ISA_TP_ALT / 100);
2559 return (sat - (ISA_SL_TEMP_C - ((fl / 10) * ISA_TLR_PER_1000FT)));
2560}
2561
2562/*
2563 * Converts ISA deviation to static air temperature.
2564 *
2565 * @param fl Flight level (barometric altitude at QNE in 100s of ft).
2566 * @param isadev ISA deviation in degrees C.
2567 *
2568 * @return Local static air temperature.
2569 */
2570double
2571isadev2sat(double fl, double isadev)
2572{
2573 fl = MIN(fl, ISA_TP_ALT / 100);
2574 return (isadev + ISA_SL_TEMP_C - ((fl / 10) * ISA_TLR_PER_1000FT));
2575}
2576
2577/*
2578 * Returns the speed of sound in m/s in dry air at `oat' degrees C (static).
2579 */
2580double
2581speed_sound(double oat)
2582{
2583 return (speed_sound_gas(C2KELVIN(oat), GAMMA, R_spec));
2584}
2585
2586/*
2587 * Returns the speed of sound in a specific gas. Unlike the speed_sound()
2588 * function, this function takes absolute temperature in Kelvin. You must
2589 * also pass the gas-specific constants (ratio of specific heats and gas
2590 * constant). These depend upon the molecular composition of the gas. For
2591 * dry air, these are defined in perf.h as GAMMA (1.4) and R_spec (287.058).
2592 */
2593double
2594speed_sound_gas(double T, double gamma, double R)
2595{
2596 return (sqrt(gamma * R * T));
2597}
2598
2599/*
2600 * Calculates air density of dry air.
2601 *
2602 * @param pressure Static air pressure in Pa.
2603 * @param oat Static outside air temperature in degrees C.
2604 *
2605 * @return Local air density in kg.m^-3.
2606 */
2607double
2608air_density(double pressure, double oat)
2609{
2610 return (gas_density(pressure, oat, R_spec));
2611}
2612
2613/*
2614 * Calculates density of an arbitrary gas.
2615 *
2616 * @param pressure Static air pressure in Pa.
2617 * @param oat Static outside air temperature in degrees C.
2618 * @param gas_const Specific gas constant of the gas in question (J/kg/K).
2619 *
2620 * @return Local gas density in kg.m^-3.
2621 */
2622double
2623gas_density(double pressure, double oat, double gas_const)
2624{
2625 /*
2626 * Density of a gas is:
2627 *
2628 * rho = p / (gas_const * T)
2629 *
2630 * Where p is local static gas pressure, gas_const is the specific gas
2631 * constant for the gas in question and T is absolute temperature.
2632 */
2633 return (pressure / (gas_const * C2KELVIN(oat)));
2634}
2635
2636/*
2637 * Calculates impact pressure. This is dynamic pressure with air
2638 * compressibility considered.
2639 *
2640 * @param pressure Static air pressure in Pa.
2641 * @param mach Flight mach number.
2642 *
2643 * @return Impact pressure in Pa.
2644 */
2645double
2646impact_press(double mach, double pressure)
2647{
2648 /*
2649 * In isentropic flow, impact pressure for air (gamma = 1.4) is:
2650 *
2651 * qc = P((1 + 0.2 * M^2)^(7/2) - 1)
2652 *
2653 * Where P is local static air pressure and M is flight mach number.
2654 */
2655 return (pressure * (pow(1 + 0.2 * POW2(mach), 3.5) - 1));
2656}
2657
2658/*
2659 * Calculates dynamic pressure in dry air.
2660 *
2661 * @param pressure True airspeed in knots.
2662 * @param press Static air pressure in Pa.
2663 * @param oat Static outside air temperature in degrees C.
2664 *
2665 * @return Dynamic pressure in Pa.
2666 */
2667double
2668dyn_press(double ktas, double press, double oat)
2669{
2670 return (dyn_gas_press(ktas, press, oat, R_spec));
2671}
2672
2673/*
2674 * Same as dyn_press, but takes an exclicit specific gas constant parameter
2675 * to allow for calculating dynamic pressure in other gases.
2676 */
2677double
2678dyn_gas_press(double ktas, double press, double oat, double gas_const)
2679{
2680 double p = (0.5 * gas_density(press, oat, gas_const) *
2681 POW2(KT2MPS(ktas)));
2682 if (ktas < 0)
2683 return (-p);
2684 return (p);
2685}
2686
2687/*
2688 * Computes static dry air pressure from air density and temperature.
2689 *
2690 * @param rho Air density in kg/m^3.
2691 * @param oat Static air temperature in Celsius.
2692 *
2693 * @return Static air pressure in Pascals.
2694 */
2695double
2696static_press(double rho, double oat)
2697{
2698 return (static_gas_press(rho, oat, R_spec));
2699}
2700
2701/*
2702 * Same as static_press, but takes an explicit specific gas constant
2703 * argument to allow computing static pressure in any gas.
2704 */
2705double
2706static_gas_press(double rho, double oat, double gas_const)
2707{
2708 /*
2709 * Static pressure of a gas is:
2710 *
2711 * p = rho * gas_const * T
2712 *
2713 * Where rho is the local gas density, gas_const is the specific gas
2714 * constant for the gas in question and T is absolute temperature.
2715 */
2716 return (rho * gas_const * C2KELVIN(oat));
2717}
2718
2719/*
2720 * Same as `adiabatic_heating_gas', but using the ratio of specific heats
2721 * for dry air at approximately room temperature.
2722 */
2723double
2724adiabatic_heating(double press_ratio, double start_temp)
2725{
2726 return (adiabatic_heating_gas(press_ratio, start_temp, GAMMA));
2727}
2728
2729/*
2730 * Computes the adiabatic heating experienced by air when compressed in a
2731 * turbine engine's compressor. The P-T relation for adiabatic heating is:
2732 *
2733 * P1^(1 - gamma) T1^(gamma) = P2^(1 - gamma) T2^(gamma)
2734 *
2735 * Solving for T2:
2736 *
2737 * T2 = ((P1^(1 - Gamma) T1^(Gamma)) / P2^(1 - Gamma))^(1 / Gamma)
2738 *
2739 * Since P2 / P1 is the compressor pressure ratio, we can cancel out P1
2740 * to '1' and simply replace P2 by the pressure ratio P_r:
2741 *
2742 * T2 = (T1^(Gamma) / P_r^(1 - Gamma))^(1 / Gamma)
2743 *
2744 * @param press_ratio Pressure ratio of the compressor (dimensionless).
2745 * @param start_temp Starting gas temperature (in degrees Celsius).
2746 * @param gamma The gas' ratio of specific heats.
2747 *
2748 * @return Compressed gas temperature (in degrees Celsius).
2749 */
2750double
2751adiabatic_heating_gas(double press_ratio, double start_temp, double gamma)
2752{
2753 return (KELVIN2C(pow(pow(C2KELVIN(start_temp), gamma) /
2754 pow(press_ratio, 1 - gamma), 1 / gamma)));
2755}
2756
2757double
2758air_kin_visc(double temp_K)
2759{
2760 const vect2_t table[] = {
2761 VECT2(200, 0.753e-5),
2762 VECT2(225, 0.935e-5),
2763 VECT2(250, 1.132e-5),
2764 VECT2(275, 1.343e-5),
2765 VECT2(300, 1.568e-5),
2766 VECT2(325, 1.807e-5),
2767 VECT2(350, 2.056e-5),
2768 VECT2(375, 2.317e-5),
2769 VECT2(400, 2.591e-5),
2770 NULL_VECT2 /* list terminator */
2771 };
2772 ASSERT3F(temp_K, >, 0);
2773 return (fx_lin_multi(temp_K, table, B_TRUE));
2774}
2775
2776double
2777air_reynolds(double vel, double chord, double temp_K)
2778{
2779 ASSERT(!isnan(vel));
2780 ASSERT3F(chord, >, 0);
2781 ASSERT3F(temp_K, >, 0);
2782 return ((vel * chord) / air_kin_visc(temp_K));
2783}
2784
2785double
2786lacf_gamma_air(double T)
2787{
2788 const vect2_t curve[] = {
2789 VECT2(250, 1.401),
2790 VECT2(300, 1.4),
2791 VECT2(350, 1.398),
2792 VECT2(400, 1.395),
2793 VECT2(450, 1.391),
2794 VECT2(500, 1.387),
2795 VECT2(550, 1.381),
2796 VECT2(600, 1.376),
2797 VECT2(650, 1.37),
2798 VECT2(700, 1.364),
2799 VECT2(750, 1.359),
2800 VECT2(800, 1.354),
2801 VECT2(900, 1.344),
2802 VECT2(1000, 1.336),
2803 VECT2(1100, 1.331),
2804 VECT2(1200, 1.324),
2805 VECT2(1300, 1.318),
2806 VECT2(1400, 1.313),
2807 VECT2(1500, 1.309),
2809 };
2810 return (fx_lin_multi(T, curve, B_TRUE));
2811}
2812
2813double
2814lacf_therm_cond_air(double T)
2815{
2816 /*
2817 * The thermal conductivity of air remains relatively constant
2818 * throughout its pressure range and deviates by less than 1%
2819 * down to about 1% of sea level pressure. Since we never really
2820 * encounter such pressures in aircraft (1% SL_p = ~84,000 ft),
2821 * we can just ignore the pressure component.
2822 */
2823 ASSERT3F(T, >, 0);
2824 return (fx_lin(T, 233.2, 0.0209, 498.15, 0.0398));
2825}
2826
2827double
2828lacf_therm_cond_aluminum(double T)
2829{
2830 const vect2_t curve[] = {
2831 VECT2(C2KELVIN(200), 237),
2832 VECT2(C2KELVIN(273), 236),
2833 VECT2(C2KELVIN(400), 240),
2834 VECT2(C2KELVIN(600), 232),
2835 VECT2(C2KELVIN(800), 220),
2836 NULL_VECT2 /* list terminator */
2837 };
2838 ASSERT3F(T, >, 0);
2839 return (fx_lin_multi(T, curve, B_TRUE));
2840}
2841
2842double
2843lacf_therm_cond_glass(double T)
2844{
2845 /* Based on Pyrex 7740, NBS, 1966 */
2846 const vect2_t curve[] = {
2847 VECT2(100, 0.58),
2848 VECT2(200, 0.90),
2849 VECT2(300, 1.11),
2850 VECT2(400, 1.25),
2851 VECT2(500, 1.36),
2852 VECT2(600, 1.50),
2853 VECT2(700, 1.62),
2854 VECT2(800, 1.89),
2855 NULL_VECT2 /* list terminator */
2856 };
2857 ASSERT3F(T, >, 0);
2858 return (fx_lin_multi(T, curve, B_TRUE));
2859}
2860
2861double
2862earth_gravity_accurate(double lat, double alt)
2863{
2864 /*
2865 * Based on https://www.engineeringtoolbox.com/docs/documents/1554/
2866 * acceleration-gravity-latitude-meter-second-second.png
2867 */
2868 const vect2_t lat_curve[] = {
2869 VECT2(0, 9.781),
2870 VECT2(10, 9.782),
2871 VECT2(20, 9.787),
2872 VECT2(30, 9.793),
2873 VECT2(60, 9.819),
2874 VECT2(70, 9.826),
2875 VECT2(80, 9.831),
2876 VECT2(90, 9.833),
2877 NULL_VECT2 /* list terminator */
2878 };
2879 const double delta_per_m = -0.00000305;
2880
2881 ASSERT3F(lat, >=, -90);
2882 ASSERT3F(lat, <=, 90);
2883 ASSERT(isfinite(alt));
2884
2885 return (fx_lin_multi(ABS(lat), lat_curve, B_FALSE) + alt * delta_per_m);
2886}
#define ASSERT3P(x, op, y)
Definition assert.h:212
#define ASSERT_MSG(x, fmt,...)
Definition assert.h:214
#define ASSERT3S(x, op, y)
Definition assert.h:209
#define ASSERT3U(x, op, y)
Definition assert.h:210
#define ASSERT(x)
Definition assert.h:208
#define ASSERT3F(x, op, y)
Definition assert.h:211
#define VERIFY3U(x, op, y)
Definition assert.h:136
void * avl_destroy_nodes(avl_tree_t *tree, void **cookie)
Definition avl.c:938
#define AVL_PREV(tree, node)
Definition avl.h:218
uintptr_t avl_index_t
Definition avl.h:119
void * avl_nearest(const avl_tree_t *tree, avl_index_t where, int direction)
Definition avl.c:215
#define AVL_AFTER
Definition avl.h:129
void avl_insert(avl_tree_t *tree, void *node, avl_index_t where)
Definition avl.c:471
void avl_create(avl_tree_t *tree, int(*compar)(const void *, const void *), size_t size, size_t offset)
Definition avl.c:867
#define AVL_NEXT(tree, node)
Definition avl.h:212
unsigned long avl_numnodes(const avl_tree_t *tree)
Definition avl.c:903
void avl_destroy(avl_tree_t *tree)
Definition avl.c:890
void * avl_find(const avl_tree_t *tree, const void *node, avl_index_t *where)
Definition avl.c:244
#define AVL_BEFORE
Definition avl.h:125
void lacf_free(void *buf)
Definition core.c:49
#define IS_NULL_VECT(a)
Definition geom.h:228
#define NULL_VECT2
Definition geom.h:214
double vect2_dotprod(vect2_t a, vect2_t b)
Definition geom.c:404
#define VECT2(x, y)
Definition geom.h:190
vect2_t vect2_unit(vect2_t a, double *l)
Definition geom.c:272
vect2_t hdg2dir(double truehdg)
Definition geom.c:1359
char ** strsplit(const char *input, const char *sep, bool_t skip_empty, size_t *num)
Definition helpers.c:650
ssize_t explode_line(char *line, char delim, char **comps, size_t capacity)
Definition helpers.c:588
static bool_t is_valid_alt_ft(double alt_ft)
Definition helpers.h:139
static ssize_t parser_get_next_line(FILE *fp, char **linep, size_t *linecap, unsigned *linenum)
Definition helpers.h:387
static bool_t is_valid_hdg(double hdg)
Definition helpers.h:169
void free_strlist(char **comps, size_t num)
Definition helpers.c:703
void list_destroy(list_t *)
Definition list.c:136
void list_create(list_t *, size_t, size_t)
Definition list.c:113
void * list_remove_head(list_t *)
Definition list.c:251
#define logMsg(...)
Definition log.h:112
double * fx_lin_multi_inv(double y, const struct vect2_s *points, size_t *num_out)
Definition math.c:167
#define POW2(x)
Definition math.h:36
double fx_lin(double x, double x1, double y1, double x2, double y2)
Definition math.c:69
static double iter_fract(double x, double min_val, double max_val, bool_t clamp_output)
Definition math.h:110
#define ABS(x)
Definition math.h:39
double fx_lin_multi(double x, const struct vect2_s *points, bool_t extrapolate)
Definition math.c:97
static double wavg2(double x, double y, double w)
Definition math.h:84
#define wavg(x, y, w)
Definition math.h:77
static double clamp(double x, double min_val, double max_val)
Definition math_core.h:60
#define NONE(type_name)
Constructs a new optional value in the NONE state.
Definition optional.h:996
#define SOME(expr)
Constructs a new optional value in the SOME state.
Definition optional.h:967
static void * safe_calloc(size_t nmemb, size_t size)
Definition safe_alloc.h:71
An optional type for wrapping a non-NAN double value.
Definition optional.h:761
#define REQ_PTR(x)
Definition sysmacros.h:334