propulsion.nas 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882
  1. # Propulsion system for YASIM helicopters
  2. # needs aircraft/Systems/propulsion.xml for configuration
  3. # needs aircraft/Systems/propulsion-rules.xml for time critical components, PID-controller
  4. # of turbines and transmission
  5. # litzi
  6. # based on b117.nas by Melchior FRANZ, < mfranz # aon : at >
  7. #
  8. io.include("/Aircraft/ec145/Nasal/lut-lib.nas");
  9. # these need to be accessible in the global scope
  10. var RPM2RADS = 0.104719755;
  11. var STARVED = -1;
  12. var OFF = 0;
  13. var START = 1;
  14. var IDLE = 2;
  15. var FLIGHT = 3;
  16. var TRAIN = 4;
  17. # fuel ==============================================================
  18. # density = 6.682 lb/gal [Flight Manual Section 9.2]
  19. # avtur/JET A-1/JP-8
  20. var FUEL_DENSITY = getprop("/consumables/fuel/tank/density-ppg"); # pound per gallon
  21. var GAL2LB = FUEL_DENSITY;
  22. var LB2GAL = 1 / GAL2LB;
  23. var KG2GAL = KG2LB * LB2GAL;
  24. var GAL2KG = 1 / KG2GAL;
  25. var fuelsystem = props.globals.getNode("/consumables/fuel");
  26. var configroot = "/fdm/yasim/";
  27. var pow = func(v, w) math.exp(math.ln(v) * w);
  28. var clamp = func(v, min = 0, max = 1) v < min ? min : v > max ? max : v;
  29. var max = func(a, b) a > b ? a : b;
  30. var min = func(a, b) a < b ? a : b;
  31. # a class that damps spool-up realistically by
  32. # limiting the rpm acceleration rate, but
  33. # uses a lowpass for the rpm decay.
  34. var DampRPM = {
  35. new: func (maxacc, decaytime) {
  36. var m = { parents: [DampRPM] };
  37. m.decay = 0;
  38. m.maxacc = maxacc;
  39. # instantanous acceleration
  40. m.acc = 0;
  41. # lowpass for rpm decay
  42. m.rpm = aircraft.lowpass.new(decaytime);
  43. m.rpm.set(0);
  44. m.delta_time = props.globals.getNode("/sim/time/elapsed-sec");
  45. m.t = m.delta_time.getValue();
  46. return m;
  47. },
  48. filter: func (targetrpm) {
  49. var val = me.rpm.get();
  50. var dt = me.delta_time.getValue() - me.t;
  51. var diff = targetrpm-val;
  52. # is the rpm decaying?
  53. me.decay = (diff<0);
  54. if (me.decay or diff<0.02) {
  55. # use the lowpass for decay of rmp
  56. # or when close to target
  57. me.rpm.filter(targetrpm);
  58. # reset acceleration
  59. me.acc = 0;
  60. } else {
  61. # smoothly ramp up acceleration to maxacc within 2 sec
  62. #me.acc = min(me.acc + me.maxacc/10*dt, me.maxacc);
  63. me.acc= me.maxacc;
  64. # increase rpm up to target
  65. val = min(val + me.acc*dt, targetrpm);
  66. # for later decay we need to start at this point
  67. me.rpm.set(val);
  68. }
  69. me.t = me.delta_time.getValue();
  70. return me.rpm.get();
  71. },
  72. set: func (x)
  73. return me.rpm.set(x),
  74. get: func
  75. return me.rpm.get()
  76. };
  77. var Tank = {
  78. new: func(n) {
  79. var m = { parents: [Tank] };
  80. m.node = fuelsystem.getNode("tank["~n~"]");
  81. m.emptyp = m.node.getNode("empty");
  82. m.level_lbN = m.node.getNode("level-lbs");
  83. m.prime = props.globals.initNode("/systems/electrical/outputs/fuelpump-prime["~n~"]");
  84. return m;
  85. },
  86. init: func() {
  87. me.capacity = me.node.getNode("capacity-gal_us").getValue();
  88. me.level_galN = me.node.initNode("level-gal_us", me.capacity);
  89. me.consume(0);
  90. },
  91. level: func {
  92. return me.level_galN.getValue();
  93. },
  94. consume: func(amount) { # US gal (neg. values for feeding)
  95. var level = me.level();
  96. if (amount > level)
  97. amount = level;
  98. level -= amount;
  99. if (level > me.capacity)
  100. level = me.capacity;
  101. me.level_galN.setValue(level);
  102. me.level_lbN.setValue(level * GAL2LB);
  103. me.empty = me.emptyp.getValue(); #(level <= 0);
  104. return amount;
  105. }
  106. };
  107. # Turbine class ==============================================================
  108. # the fuelsystem must have a Tank object as a member "supply[id]"
  109. var Turbine = {
  110. new: func(n, super) {
  111. var m = { parents: [Turbine] };
  112. m.n = n;
  113. m.table = super.table; # references into to super class propulsion
  114. m.rotor_rpm = super.rotor_rpm;
  115. m.torque = super.torque;
  116. m.collective = super.collective;
  117. m.target_rel_rpm = super.target_rel_rpm;
  118. m.max_rel_torque = super.max_rel_torque;
  119. m.oatnode = super.oatnode;
  120. m.iasnode = super.iasnode;
  121. var path = "/engines/engine[" ~ m.n ~ "]/";
  122. var cpath = "/controls/engines/engine[" ~ m.n ~ "]/";
  123. m.timeconst = 1;
  124. m.electric_prop="";
  125. # engine control props
  126. m.powerIn = props.globals.initNode(cpath ~ "power",1);
  127. m.ignitionIn = props.globals.initNode(cpath ~ "ignition", 1, "BOOL");
  128. # engine output props
  129. m.starterOut = props.globals.initNode(path ~ "starter", 1);
  130. m.runningOut= props.globals.initNode(path ~ "running", 1, "BOOL");
  131. m.outOfFuelOut= props.globals.initNode(path ~ "out-of-fuel", 1, "BOOL");
  132. m.freewheelOut = props.globals.initNode(path ~ "freewheel", 1);
  133. m.powerOut = props.globals.initNode(path ~ "power-pct",1);
  134. m.powerOutWatt = props.globals.initNode(path ~ "power-watts",1);
  135. m.maxRelTrqOut = props.globals.initNode(path ~ "max-rel-torque",1);
  136. m.n1pct = props.globals.initNode(path ~ "n1-pct", 1);
  137. m.n2pct = props.globals.initNode(path ~ "n2-pct", 1);
  138. m.n1rpm = props.globals.initNode(path ~ "n1-rpm", 1); # gas generator rpm
  139. m.n2rpm = props.globals.initNode(path ~ "n2-rpm", 1); # free power turbine rpm
  140. m.rpm = props.globals.initNode(path ~ "rpm", 1); # shaft output rpm
  141. m.trqpct = props.globals.initNode(path ~ "torque-pct",1);
  142. m.trqnm = props.globals.initNode(path ~ "torque-Nm",1);
  143. m.tot = props.globals.initNode(path ~ "tot-degc",1);
  144. m.ff = props.globals.initNode(path ~ "fuel-flow_pph",1);
  145. m.oilt = props.globals.initNode(path ~ "oil-temperature-degc",1);
  146. m.oilp = props.globals.initNode(path ~ "oil-pressure-bar",1);
  147. # engine parameters read from propulsion.xml
  148. m.starter = 0;
  149. m.decay = 0;
  150. m.state = OFF;
  151. m.maxreltorque = 0; # power setting , i.e. the target n1 value
  152. m.freewheeling = 0;
  153. m.n1=0; # current n1 normalized to 0..1
  154. m.n2=0; # current n2 normalized to 0..1
  155. m.n2old=0;
  156. m.trq=0; # torque normalized to 0..1
  157. m.dn1=0;
  158. m.n1old=0;
  159. m.fuelflow_kgh=0;
  160. m.power=0;
  161. m.power_max=0;
  162. m.n2set = 0; # the n2 target value
  163. m.n1set = 0; # the n1 target value
  164. m.fuelpump = nil;
  165. m.fuelpress = 0;
  166. m.model_name="";
  167. m.data_source="";
  168. m.starter_max=0;
  169. m.starter_is_running=0;
  170. m.starter_time_s=0;
  171. m.starter_spooltime_s=0;
  172. m.n1_min=0;
  173. m.n1_fuelpress=0;
  174. m.n1_idle=0;
  175. m.pwr_flight=0;
  176. m.n1_rpm_max=0;
  177. m.n2_idle=0;
  178. m.n2_train=0;
  179. m.n2_no_trq=0;
  180. m.n2_rpm_max=0;
  181. m.shaft_rpm_max=0;
  182. m.trq_max_Nm=0;
  183. m.pwr_idle=0;
  184. m.pwr_start=0;
  185. m.fuelflow_idle_kgh=0;
  186. m.fuelflow_max_kgh=0;
  187. m.spooltime_idle_s=0;
  188. m.decaytime_s=0;
  189. m.tot_ignite_deg=0;
  190. m.tot_timeconst_s=0;
  191. m.tot_cooltime_s=0;
  192. m.tot_dn1dt_factor=0;
  193. m.oiltemp_nominal_degc=0;
  194. m.oiltemp_cooltime_s=0;
  195. m.oilpress_nominal_bar=0;
  196. m.supply_tank=0;
  197. m.starter_prop="";
  198. m.train_prop="";
  199. m.flight_prop="";
  200. m.cutoff_prop="";
  201. m.ignition_prop="";
  202. m.starter_prop="";
  203. m.primer_prop="";
  204. m.timer = aircraft.timer.new("/sim/time/hobbs/turbines[" ~ n ~ "]", 10);
  205. return m;
  206. },
  207. init: func () {
  208. me.starterIn = props.globals.initNode(me.starter_prop, 0);
  209. me.cutoffIn = props.globals.initNode(me.cutoff_prop, 0, type="BOOL");
  210. me.flightIn = props.globals.initNode(me.flight_prop, 0, type="BOOL");
  211. me.trainIn = props.globals.initNode(me.train_prop, 0, type="BOOL");
  212. me.ignitionIn = props.globals.initNode(me.ignition_prop, 0, type="BOOL");
  213. me.fuelpump = props.globals.initNode(me.primer_prop, 0);
  214. me.supply=Tank.new(me.supply_tank);
  215. me.supply.init();
  216. me.n1LP = DampRPM.new(me.n1_idle/me.spooltime_idle_s, me.decaytime_s);
  217. me.n2LP = DampRPM.new(me.n2_idle/me.spooltime_idle_s, me.decaytime_s);
  218. me.pwrLP = DampRPM.new(me.spooltime_idle_s, 0.1);
  219. me.trqLP = aircraft.lowpass.new(1);
  220. me.fwLP = aircraft.lowpass.new(1);
  221. me.totLP = aircraft.lowpass.new(1);
  222. me.oiltLP = aircraft.lowpass.new(me.oiltemp_cooltime_s);
  223. me.oilpLP = aircraft.lowpass.new(5);
  224. me.n1LP.set(0);
  225. me.n2LP.set(0);
  226. me.trqLP.set(0);
  227. me.fwLP.set(0);
  228. me.oilpLP.set(0);
  229. var oat = me.oatnode.getValue() or 0;
  230. me.totLP.set(oat);
  231. me.oiltLP.set(oat);
  232. me.n1=0;
  233. me.n2=0;
  234. me.n2set=0;
  235. me.trq=0;
  236. me.decay=0;
  237. me.starterOut.setValue(0);
  238. me.ignitionIn.setValue(0);
  239. me.cutoffIn.setValue(0);
  240. me.flightIn.setValue(0);
  241. me.powerIn.setValue(0);
  242. me.state=OFF;
  243. me.freewheeling=0;
  244. me.power_max=me.shaft_rpm_max*me.trq_max_Nm*RPM2RADS;
  245. me.spoolup=1;
  246. me.pwr_adjust = 0;
  247. # if starter motor is activated start spoolup
  248. me.starterListener = setlistener(me.starterIn, func {me._check_start()} );
  249. me.flightListener = setlistener(me.flightIn, func {me._check_flight()} );
  250. me.cutoffListener = setlistener(me.cutoffIn, func {me._check_off()} );
  251. me.trainListener = setlistener(me.trainIn, func {me._check_train()} );
  252. me.timer.start();
  253. },
  254. _check_start: func () {
  255. if (me.starterIn.getValue() > 0) {
  256. if (me.starter_is_running) # do not restart while starter is running
  257. return;
  258. me.starter_is_running = 1;
  259. #starter motor spoolup
  260. interpolate(me.starterOut, me.starter_max , me.starter_spooltime_s);
  261. } else {
  262. #no more power on starter -> starter motor spooldown time 3s
  263. interpolate(me.starterOut, 0, 3);
  264. me.starter_is_running = 0;
  265. }
  266. },
  267. _check_flight: func () {
  268. # flight flag toggles between flight and idle
  269. if (me.flightIn.getValue() == 1) {
  270. if (me.state == IDLE)
  271. me.flight();
  272. } else {
  273. if (me.state == FLIGHT)
  274. me.idle();
  275. }
  276. },
  277. _check_train: func () {
  278. # flight flag toggles between flight and training mode
  279. if (me.trainIn.getValue() == 1) {
  280. if (me.state == FLIGHT)
  281. me.train();
  282. } else {
  283. if (me.state == TRAIN)
  284. me.flight();
  285. }
  286. },
  287. _check_off: func () {
  288. if (me.state == OFF)
  289. return;
  290. if (me.cutoffIn.getValue() == 1)
  291. me.off();
  292. },
  293. start: func () {
  294. if (me.state != OFF) {
  295. print("engine is running ", me.n);
  296. return;
  297. }
  298. print("starting engine ", me.n);
  299. me.state=START;
  300. me.decay=0;
  301. },
  302. idle: func () {
  303. print("idle engine ", me.n);
  304. # fast decay from FLIGHT to IDLE rpm
  305. me.decay=(me.state == FLIGHT);
  306. me.state=IDLE;
  307. },
  308. flight: func () {
  309. print("flight engine ", me.n);
  310. me.state=FLIGHT;
  311. me.decay=0;
  312. me.spoolup=1;
  313. },
  314. train: func () {
  315. print("training engine ", me.n);
  316. me.state=TRAIN;
  317. me.decay=1;
  318. me.spoolup=0;
  319. },
  320. off: func () {
  321. print("stopping engine ", me.n);
  322. me.state=OFF;
  323. me.decay=1;
  324. },
  325. quickstart: func () {
  326. me.state=FLIGHT;
  327. me.decay=0;
  328. me.n1_fuelpress = 0;
  329. me.n1=1;
  330. me.n1set=1;
  331. me.maxreltorque = 1;
  332. },
  333. update_n1: func (dt) {
  334. # check fuelpress from pump, n1, fuel tank
  335. me.fuelpress = ((me.n1 > me.n1_fuelpress and !me.starterIn.getValue()) or me.fuelpump.getValue()) and !me.supply.empty;
  336. me.outOfFuelOut.setValue(!me.fuelpress);
  337. var starterrpm = me.starterOut.getValue();
  338. var ias = me.iasnode.getValue() or 0;
  339. var oat = me.oatnode.getValue() or 0;
  340. if (me.fuelpress==0) {
  341. if (me.state!=OFF)
  342. print("no fuel pressure engine ", me.n);
  343. me.state=OFF;
  344. }
  345. if (me.state==OFF) {
  346. # off, simulate n1,n2 spooldown
  347. me.pwrLP.filter(0.0);
  348. me.n2set = 0.0;
  349. me.n1set = 0.0;
  350. # check if starting conditions are met
  351. if ( me.starterIn.getValue() and me.ignitionIn.getValue() and me.fuelpress ) {
  352. me.state = START;
  353. me.pwrLP.set( me.pwr_start );
  354. }
  355. }
  356. if (me.state==START) {
  357. # while starting, n1 must follow starter rpm
  358. me.n1set = starterrpm;
  359. me.n2set = 0.05; # try to reach max. 5% rotor rpm during startup, otherwise rotor will not move at all during fist seconds
  360. # starting, check starter and ignition
  361. if (me.n1 > me.n1_min and me.ignitionIn.getValue() ) {
  362. # switch to idle running engine if conditions are ok n1>idle
  363. me.state = IDLE;
  364. }
  365. }
  366. if (me.state==IDLE and me.n1 > me.n1_min) {
  367. # idle, hold idle n1, n2 datums,
  368. # in idle power can be adjusted from external modules
  369. me.n1set = me.n1_idle + me.pwr_adjust;
  370. me.n2set = me.n2_idle + me.pwr_adjust;
  371. me.pwrLP.filter( me.pwr_idle + me.pwr_adjust);
  372. # decay only until idle rpm is reached
  373. if (me.decay and me.n2<=me.n2_idle)
  374. me.decay=0;
  375. }
  376. if (me.state==FLIGHT and me.n1 > me.n1_min) {
  377. me.n2set = me.table["ias-vs-nr"].get(ias);
  378. if (me.n2 < 0.95 and me.spoolup) {
  379. # during spool-up
  380. me.pwrLP.filter( me.pwr_flight );
  381. } else {
  382. # in flight mode power is set to full power and directly controlled via (YASIM) governor!
  383. me.pwrLP.set( 1.0 );
  384. me.spoolup=0;
  385. }
  386. # n1 is fed back from yasim via trq
  387. me.n1set = me.table["pwr-vs-n1"].get(clamp(me.power));
  388. }
  389. if (me.state==TRAIN and me.n1 > me.n1_min) {
  390. me.n2set = me.n2_train;
  391. me.pwrLP.filter( me.pwr_flight );
  392. me.n1set = me.n1_idle;
  393. }
  394. # while starting, n1 must follow starter rpm
  395. if (me.state==START) {
  396. me.n1 = me.n1LP.set(me.n1set);
  397. } else {
  398. me.n1 = me.n1LP.filter(me.n1set);
  399. }
  400. #
  401. # max. available engine power
  402. # from engine state
  403. #
  404. me.maxreltorque = me.pwrLP.get();
  405. #
  406. # update engine temperature TOT
  407. # depends on table data of n1, d(n1)/dt and TOT at ignition value
  408. #
  409. var targetT = (me.table["n1-vs-tot"].get(me.n1)-oat) * (me.state>=IDLE) * (1.0+((me.dn1/dt)>0.01)*me.tot_dn1dt_factor) +
  410. (me.tot_ignite_deg-oat) * (me.state==START) * me.ignitionIn.getValue() +
  411. oat;
  412. #
  413. # TOT time constant: decay slowly when OFF but fast if engine running
  414. #
  415. if (me.state==OFF and me.n1<0.1)
  416. me.totLP.coeff = me.tot_cooltime_s;
  417. else
  418. me.totLP.coeff = me.tot_timeconst_s;
  419. me.tot.setValue( me.totLP.filter(targetT) );
  420. #
  421. # update fuel flow per hour, FF follows n1
  422. #
  423. var FF_kg = 0;
  424. if (me.state <= IDLE)
  425. FF_kg = (me.n1/me.n1_idle) * me.fuelflow_idle_kgh;
  426. else
  427. FF_kg = me.n1 * me.fuelflow_max_kgh;
  428. # LB/h
  429. me.ff.setValue(FF_kg * me.fuelpress * KG2LB);
  430. # consume fuel from supply tank in Gal. (FF is per hour):
  431. me.supply.consume(FF_kg * me.fuelpress * KG2GAL * dt / 3600);
  432. #
  433. # Oiltemperature: assume a weighted mix of engine TOT temp and n1
  434. # At TOT=700 deg C and at n1=100% the oiltemp reaches its nominal value
  435. #
  436. var targetOilT = clamp(0.5*me.totLP.get()/700 + 0.5*me.n1) * (me.state>=IDLE);
  437. me.oilt.setValue(me.oiltLP.filter(oat + targetOilT*(me.oiltemp_nominal_degc-oat)));
  438. #
  439. # Oilpressure :
  440. #
  441. var targetOilP = (me.n1 > me.n1_min)*me.oilpress_nominal_bar;
  442. me.oilp.setValue(me.oilpLP.filter(targetOilP));
  443. #
  444. # write to properties :
  445. #
  446. me.n1pct.setValue(me.n1*100);
  447. me.n1rpm.setValue(me.n1*me.n1_rpm_max);
  448. me.runningOut.setValue(me.state > START);
  449. me.powerIn.setValue(me.pwrLP.get());
  450. me.maxRelTrqOut.setValue(me.maxreltorque);
  451. me.dn1 = me.n1-me.n1old;
  452. me.n1old = me.n1;
  453. me.n2LP.filter( me.n2set );
  454. },
  455. update_n2: func (shaft) {
  456. # n2, freewheel and torque calc.
  457. #NEW: freewheeling state computation replaced by a prop-rule
  458. me.freewheeling = me.freewheelOut.getValue();
  459. # engine powers and follows rotor
  460. # torque normalized from power and rpm
  461. # (power must be devided by rpm to yield torque,
  462. # but this seems to strongly over estimate torque at low rpms)
  463. me.n2old = me.n2;
  464. # abs. shaft speed and pwr normalized
  465. var rpmN = shaft.rpm / me.shaft_rpm_max;
  466. var trq_Nm = 0;
  467. me.power = shaft.trq / me.power_max;
  468. # n2 cannot be faster than the rotor, force rotor speed
  469. # if in spoolup and if engine is loaded
  470. if (me.state==FLIGHT and me.freewheeling < 0.5)
  471. me.n2 = min(rpmN, me.n2LP.get());
  472. else
  473. me.n2 = clamp(me.n2LP.get(), 0, rpmN);
  474. # update trq from n2 relative to the rotor rpm
  475. # trq needs special treatment because of divison
  476. # by rpm
  477. if (rpmN>0.001)
  478. var trq_Nm = shaft.trq / (shaft.rpm * RPM2RADS) * (1-me.freewheeling);
  479. #
  480. # write trq, n2 and shaft speed properties
  481. #
  482. me.powerOutWatt.setValue(shaft.trq);
  483. me.powerOut.setValue(me.power*100);
  484. me.trqnm.setValue(me.trqLP.filter(trq_Nm * (rpmN>me.n2_no_trq)));
  485. me.trqpct.setValue(me.trqLP.get()/me.trq_max_Nm*100);
  486. me.n2pct.setValue(me.n2*100);
  487. me.n2rpm.setValue(me.n2*me.n2_rpm_max);
  488. me.rpm.setValue(me.n2*me.shaft_rpm_max);
  489. },
  490. adjust_power: func(p) {
  491. me.pwr_adjust = p;
  492. }
  493. };
  494. # Transmission class ==============================================================
  495. # connects the turbine(s) to the FDM via rotor rpm, torque and max_rel_torque
  496. var Transmission = {
  497. new: func (super) {
  498. var m = { parents: [Transmission] };
  499. #m.rotorN = param.rpm;
  500. m.gearratio = [1,1];
  501. m.totaltrq=0;
  502. m.rpm=0;
  503. m.numeng=0;
  504. m.power=0;
  505. m.yasim_rotor_rpm=0;
  506. m.mgb_oiltemp_nominal_degc=0;
  507. m.mgb_oiltemp_trq_influence=0;
  508. m.mgb_oilpress_nominal_bar=0;
  509. m.mgb_oilpress_min_nr=0;
  510. m.mgb_oiltemp_cooltime_s=0;
  511. m.kp = 17;
  512. m.kd = 900;
  513. # yasim needs engine 0 magneto on for start
  514. m.magneto = props.globals.initNode("/controls/engines/engine[0]/magnetos", 1, "INT");
  515. m.oilt = props.globals.initNode("/rotors/gear/mgb-oil-temperature-degc",1);
  516. m.oilp = props.globals.initNode("/rotors/gear/mgb-oil-pressure-bar",1);
  517. m.rotor_rpm = super.rotor_rpm;
  518. m.torque = super.torque;
  519. m.target_rel_rpm = super.target_rel_rpm;
  520. m.max_rel_torque = super.max_rel_torque;
  521. m.oatnode = super.oatnode;
  522. m.iasnode = super.iasnode;
  523. m.table = super.table;
  524. m.engine = super.engine;
  525. return m;
  526. },
  527. init: func (n, shaftrpm) {
  528. me.numeng = n;
  529. me.power = 0;
  530. # yasim needs engine 0 magneto on for start
  531. me.magneto.setValue(1);
  532. me.oiltLP = aircraft.lowpass.new(me.mgb_oiltemp_cooltime_s);
  533. me.oilpLP = aircraft.lowpass.new(5);
  534. foreach (var i; [0,1])
  535. me.gearratio[i] = me.yasim_rotor_rpm/shaftrpm[i];
  536. },
  537. update: func (dt, shaft) {
  538. # distribute power between engines
  539. var rpm = (me.rotor_rpm.getValue() or 0)/me.yasim_rotor_rpm;
  540. var torqueIn = me.torque.getValue() or 0;
  541. var oat = me.oatnode.getValue() or 0;
  542. var ias = me.iasnode.getValue() or 0;
  543. # difference to Nr datum:
  544. var rpmdiff = me.table["ias-vs-nr"].get(ias) - rpm;
  545. #
  546. # power split calc between engines
  547. # from engines freewheeling states
  548. #
  549. var trq = [0,0];
  550. var f = [0,0];
  551. var fw = [0,0]; # power per engine weighted by n2 diff
  552. me.power = 0;
  553. # sync based on engine freewheel states
  554. fw[1]=1-me.engine[1].freewheeling;
  555. fw[0]=1-me.engine[0].freewheeling;
  556. me.power = shaft[0].pw*fw[0] + shaft[1].pw*fw[1];
  557. if (me.power>0) {
  558. # get power returned (consumed) by Yasim
  559. me.totaltrq = clamp(torqueIn, 0, 1e10);
  560. } else {
  561. # prevent division by zero
  562. me.power=0.00001;
  563. me.totaltrq = 0;
  564. }
  565. #NEW: max. power limit from sum of both engines
  566. #setprop("controls/rotor/gov/maxrelpower", me.power/me.numeng);
  567. #alternatively, using YASIMs internal PD-controller
  568. setprop("controls/rotor/maxreltorque", me.power/me.numeng);
  569. # distribute yasim trq between engines,
  570. # relative between the two engines.
  571. var ratio = fw[0]/( fw[1]+fw[0] );
  572. if (debug.isnan(ratio)) {
  573. ratio = 0.5;
  574. }
  575. f[0]=ratio;
  576. f[1]=1-ratio;
  577. # distribute torque, return absolute rpm on shaft
  578. foreach (var i; [0,1])
  579. trq[i] = {rpm: rpm*me.yasim_rotor_rpm/me.gearratio[i],
  580. trq: me.totaltrq * f[i]};
  581. # MGB oil pressure
  582. var targetOilP = (rpm > me.mgb_oilpress_min_nr) * me.mgb_oilpress_nominal_bar;
  583. me.oilp.setValue( me.oilpLP.filter(targetOilP));
  584. # MGB oil temperature (trq scaling is arbitrary: 100 kW)
  585. var targetOilT = oat + rpm * (me.mgb_oiltemp_nominal_degc-oat) * (1 - me.mgb_oiltemp_trq_influence) +
  586. torqueIn/1e5 * me.mgb_oiltemp_trq_influence;
  587. me.oilt.setValue( me.oiltLP.filter(targetOilT));
  588. return trq;
  589. }
  590. };
  591. # Propulsion class helpers ==============================================================
  592. var parsetree = func (root) {
  593. var elements = {};
  594. proplist = root.getChildren();
  595. foreach (var x; proplist) {
  596. var a = x.getValue();
  597. var b = string.replace(x.getName(),"-","_");
  598. # new? make empty vector
  599. if (!contains(elements,b))
  600. elements[b] = [];
  601. if (a == nil)
  602. append(elements[ b ], parsetree(x));
  603. else
  604. append(elements[ b ], a);
  605. }
  606. # reduce where only one element
  607. foreach (var y; keys(elements))
  608. if (size(elements[y])==1)
  609. elements[y] = elements[y][0];
  610. return elements;
  611. };
  612. # import parameters from a hash into object if scalar
  613. var importto = func (obj, node) {
  614. var x=0;
  615. foreach(var a; keys(node))
  616. if (typeof(node[a]) == "scalar") {
  617. x = num(node[a]);
  618. if (contains(obj,a))
  619. obj[a] = (x==nil) ? node[a] : x;
  620. else
  621. die("propulsion: unknown parameter " ~ a);
  622. }
  623. };
  624. var parsetable = func (tbl) {
  625. var out = [];
  626. var x = split(" ",tbl);
  627. foreach (var y; x) {
  628. var i = split(",",y);
  629. var u = [];
  630. foreach (var k; i)
  631. append(u, num(k));
  632. append(out,u);
  633. }
  634. return out;
  635. };
  636. # Propulsion class ==============================================================
  637. var Propulsion = {
  638. new: func() {
  639. var m = { parents: [Propulsion] };
  640. m.table = {};
  641. # engines/rotor =====================================================
  642. m.rotor_rpm = props.globals.getNode("rotors/main/rpm");
  643. m.torque = props.globals.getNode("rotors/gear/total-torque", 1);
  644. m.collective = props.globals.getNode("controls/engines/engine[0]/throttle");
  645. m.target_rel_rpm = props.globals.getNode("controls/rotor/reltarget", 1);
  646. m.max_rel_torque = props.globals.getNode("controls/rotor/maxreltorque", 1);
  647. m.oatnode = props.globals.getNode("environment/temperature-degc");
  648. m.iasnode = props.globals.getNode("velocities/airspeed-kt");
  649. m.engine = [Turbine.new(0, m), Turbine.new(1, m)];
  650. m.transmission = Transmission.new(m);
  651. return m;
  652. },
  653. parseprops: func () {
  654. # read model config data from property tree
  655. var elements = parsetree(props.globals.getNode("sim/systems/propulsion"));
  656. if (size(elements.turbines.turbine) != elements.num_engines)
  657. die("propulsion: number of engines incorrect in XML!");
  658. # parse tables section and fill tables var
  659. if (contains(elements, "tables")) {
  660. for(var n=0; n<size(elements.tables.table); n+=1) {
  661. var atable = elements.tables.table[n];
  662. me.table[atable.name] = LUT.new( parsetable(atable.tableData) );
  663. }
  664. }
  665. # import parameters to engine objects
  666. if (size(elements.turbines.turbine) >= 1) {
  667. importto(me.engine[0], elements.turbines);
  668. importto(me.engine[0], elements.turbines.turbine[0]);
  669. }
  670. if (size(elements.turbines.turbine) == 2) {
  671. importto(me.engine[1], elements.turbines);
  672. importto(me.engine[1], elements.turbines.turbine[1]);
  673. }
  674. if (!contains(elements, "transmission")) {
  675. die("propulsion: transmission missing in XML!");
  676. } else {
  677. importto(me.transmission, elements.transmission);
  678. }
  679. me.numeng = size(me.engine);
  680. },
  681. init: func () {
  682. me.parseprops();
  683. me.transmission.init(me.numeng, [me.engine[0].shaft_rpm_max, me.engine[1].shaft_rpm_max]);
  684. foreach (var i; [0,1])
  685. me.engine[i].init();
  686. # NEW: disable YASIM rotor target RPM control
  687. setprop("controls/rotor/reltarget", 2.0);
  688. },
  689. quickstart: func () {
  690. foreach (var i; [0,1])
  691. me.engine[i].quickstart();
  692. },
  693. update: func (dt) {
  694. # get total power provided by engine(s)
  695. foreach (var i; [0,1])
  696. me.engine[i].update_n1(dt);
  697. var n2 = [me.engine[0].n2LP.get(), me.engine[1].n2LP.get()];
  698. #set point for the property rule PD-controller
  699. #setprop("controls/rotor/gov/reltarget-internal", max(n2[0], n2[1]) );
  700. # alternatively, with YASIMs internal PD-controller
  701. setprop("controls/rotor/reltarget", max(n2[0], n2[1]) );
  702. # feed total power to transmission
  703. var shaft = me.transmission.update(dt,[{state: me.engine[0].state, pw: me.engine[0].maxreltorque, rpm: n2[0] },
  704. {state: me.engine[1].state, pw: me.engine[1].maxreltorque, rpm: n2[1] } ] );
  705. # feed shaft rpm and torque back to engine(s)
  706. foreach (var i; [0,1]) {
  707. me.engine[i].update_n2(shaft[i]);
  708. }
  709. }
  710. };