source: ntrip/trunk/BNC/src/bncantex.cpp@ 10955

Last change on this file since 10955 was 10955, checked in by stuerze, 6 weeks ago

Possibility to select how satellite attitude is modelled when converting Antenna Phase Center (APC) corrections to
Center-of-Mass (CoM) positions required for SP3 output

File size: 32.7 KB
Line 
1// Part of BNC, a utility for retrieving decoding and
2// converting GNSS data streams from NTRIP broadcasters.
3//
4// Copyright (C) 2007
5// German Federal Agency for Cartography and Geodesy (BKG)
6// http://www.bkg.bund.de
7// Czech Technical University Prague, Department of Geodesy
8// http://www.fsv.cvut.cz
9//
10// Email: euref-ip@bkg.bund.de
11//
12// This program is free software; you can redistribute it and/or
13// modify it under the terms of the GNU General Public License
14// as published by the Free Software Foundation, version 2.
15//
16// This program is distributed in the hope that it will be useful,
17// but WITHOUT ANY WARRANTY; without even the implied warranty of
18// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19// GNU General Public License for more details.
20//
21// You should have received a copy of the GNU General Public License
22// along with this program; if not, write to the Free Software
23// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
24
25/* -------------------------------------------------------------------------
26 * BKG NTRIP Client
27 * -------------------------------------------------------------------------
28 *
29 * Class: bncAntex
30 *
31 * Purpose: Antenna Phase Centers and Variations from ANTEX File
32 *
33 * Author: L. Mervart
34 *
35 * Created: 26-Jan-2011
36 *
37 * Changes:
38 *
39 * -----------------------------------------------------------------------*/
40
41#include <iostream>
42#include <cmath>
43#include <newmatio.h>
44
45#include "bncantex.h"
46#include "pppModel.h"
47
48using namespace std;
49
50// Constructor
51////////////////////////////////////////////////////////////////////////////
52bncAntex::bncAntex() {
53}
54
55// Constructor
56////////////////////////////////////////////////////////////////////////////
57bncAntex::bncAntex(const char* fileName) {
58 readFile(QString(fileName));//print();
59}
60
61// Destructor
62////////////////////////////////////////////////////////////////////////////
63bncAntex::~bncAntex() {
64 QMapIterator<QString, t_antMap*> it(_maps);
65 while (it.hasNext()) {
66 it.next();
67 delete it.value();
68 }
69 _maps.clear();
70}
71
72// Print
73////////////////////////////////////////////////////////////////////////////
74void bncAntex::print() const {
75 QMapIterator<QString, t_antMap*> itAnt(_maps);
76 while (itAnt.hasNext()) {
77 itAnt.next();
78 t_antMap* map = itAnt.value();
79 cout << map->antName.toLatin1().data() << endl;
80 cout << " " << map->zen1 << " " << map->zen2 << " " << map->dZen << endl;
81 QMapIterator<t_frequency::type, t_frqMap*> itFrq(map->frqMap);
82 while (itFrq.hasNext()) {
83 itFrq.next();
84 const t_frqMap* frqMap = itFrq.value();
85 cout << t_frequency::toString(itFrq.key()) << ":\n"
86 << frqMap->neu[0] << " "
87 << frqMap->neu[1] << " "
88 << frqMap->neu[2] << endl;
89 cout << frqMap->pattern.t();
90 }
91 cout << endl;
92 }
93}
94
95// Print
96////////////////////////////////////////////////////////////////////////////
97QString bncAntex::pcoSinexString(const std::string& antName, t_frequency::type frqType) {
98
99 if (antName.find("NULLANTENNA") != string::npos) {
100 return QString(" ------ ------ ------");
101 }
102
103 QString antNameQ = antName.c_str();
104 if (_maps.find(antNameQ) == _maps.end()) {
105 return QString(" ------ ------ ------");
106 }
107
108 t_antMap* map = _maps[antNameQ];
109 if (map->frqMap.find(frqType) == map->frqMap.end()) {
110 return QString(" ------ ------ ------");
111 }
112
113 t_frqMap* frqMap = map->frqMap[frqType];
114
115 QString u = QString().asprintf("%+6.4f" ,frqMap->neu[2]); if (u.mid(1,1) == "0") {u.remove(1,1);}
116 QString n = QString().asprintf("%+6.4f" ,frqMap->neu[0]); if (n.mid(1,1) == "0") {n.remove(1,1);}
117 QString e = QString().asprintf("%+6.4f" ,frqMap->neu[1]); if (e.mid(1,1) == "0") {e.remove(1,1);}
118
119 return QString(" %1 %2 %3").arg(u).arg(n).arg(e);
120}
121
122// Print
123////////////////////////////////////////////////////////////////////////////
124QString bncAntex::snxCodeSinexString(const std::string& antName) {
125
126 if (antName.find("NULLANTENNA") != string::npos) {
127 return QString(" ----------");
128 }
129
130 QString antNameQ = antName.c_str();
131 if (_maps.find(antNameQ) == _maps.end()) {
132 return QString(" ----------");
133 }
134 else {
135 return QString(" %1").arg(_maps[antNameQ]->snxCode, 10, QLatin1Char(' '));
136 }
137}
138
139// Read ANTEX File
140////////////////////////////////////////////////////////////////////////////
141t_irc bncAntex::readFile(const QString& fileName) {
142
143 QFile inFile(fileName);
144 inFile.open(QIODevice::ReadOnly | QIODevice::Text);
145
146 QTextStream in(&inFile);
147
148 t_antMap* newAntMap = 0;
149 t_frqMap* newFrqMap = 0;
150
151 while ( !in.atEnd() ) {
152 QString line = in.readLine();
153
154 // Start of Antenna
155 // ----------------
156 if (line.indexOf("START OF ANTENNA") == 60) {
157 if (newAntMap) {
158 delete newAntMap;
159 return failure;
160 }
161 else {
162 delete newAntMap;
163 newAntMap = new t_antMap();
164 }
165 }
166
167 // End of Antenna
168 // --------------
169 else if (line.indexOf("END OF ANTENNA") == 60) {
170 if (newAntMap) {
171 if (_maps.contains(newAntMap->antName)) {
172 delete _maps[newAntMap->antName];
173 }
174 _maps[newAntMap->antName] = newAntMap;
175 newAntMap = 0;
176 }
177 else {
178 delete newAntMap;
179 return failure;
180 }
181 }
182
183 // Antenna Reading in Progress
184 // ---------------------------
185 else if (newAntMap) {
186 if (line.indexOf("TYPE / SERIAL NO") == 60) {
187 if (line.indexOf("BLOCK I") == 0 ||
188 line.indexOf("GLONASS") == 0 ||
189 line.indexOf("QZSS") == 0 ||
190 line.indexOf("BEIDOU") == 0 ||
191 line.indexOf("GALILEO") == 0 ||
192 line.indexOf("NavIC") == 0 ){
193 newAntMap->antName = line.mid(20,3);
194 if (line.indexOf("BLOCK I") == 0) {
195 // Extract GPS block type: "BLOCK IIF " → "IIF"
196 QString bt = line.mid(0, 20).trimmed();
197 if (bt.startsWith("BLOCK "))
198 newAntMap->blockType = bt.mid(6);
199 }
200 else if (line.indexOf("GALILEO") == 0) {
201 // "GALILEO-1" → "1" (IOV), "GALILEO-2" → "2" (FOC)
202 QString bt = line.mid(0, 20).trimmed();
203 if (bt.startsWith("GALILEO-"))
204 newAntMap->blockType = bt.mid(8);
205 }
206 else if (line.indexOf("BEIDOU") == 0) {
207 // "BEIDOU-2M" → "2M", "BEIDOU-3M-CAST" → "3M-CAST", etc.
208 QString bt = line.mid(0, 20).trimmed();
209 if (bt.startsWith("BEIDOU-"))
210 newAntMap->blockType = bt.mid(7);
211 }
212 }
213 else {
214 newAntMap->antName = line.mid(0,20);
215 }
216 }
217 else if (line.indexOf("ZEN1 / ZEN2 / DZEN") == 60) {
218 QTextStream inLine(&line, QIODevice::ReadOnly);
219 inLine >> newAntMap->zen1 >> newAntMap->zen2 >> newAntMap->dZen;
220 }
221 else if (line.indexOf("SINEX CODE") == 60) {
222 QTextStream inLine(&line, QIODevice::ReadOnly);
223 inLine >> newAntMap->snxCode;
224 }
225
226 // Start of Frequency
227 // ------------------
228 else if (line.indexOf("START OF FREQUENCY") == 60) {
229 if (newFrqMap) {
230 delete newFrqMap;
231 delete newAntMap;
232 return failure;
233 }
234 else {
235 newFrqMap = new t_frqMap();
236 }
237 }
238
239 // End of Frequency
240 // ----------------
241 else if (line.indexOf("END OF FREQUENCY") == 60) {
242 if (newFrqMap) {
243 t_frequency::type frqType = t_frequency::dummy;
244 // GPS
245 if (line.indexOf("G01") == 3) {
246 frqType = t_frequency::G1;
247 }
248 else if (line.indexOf("G02") == 3) {
249 frqType = t_frequency::G2;
250 }
251 else if (line.indexOf("G05") == 3) {
252 frqType = t_frequency::G5;
253 }
254 // GLONASS
255 else if (line.indexOf("R01") == 3) {
256 frqType = t_frequency::R1;
257 }
258 else if (line.indexOf("R02") == 3) {
259 frqType = t_frequency::R2;
260 }
261 // Galileo
262 else if (line.indexOf("E01") == 3) {
263 frqType = t_frequency::E1;
264 }
265 else if (line.indexOf("E05") == 3) {
266 frqType = t_frequency::E5;
267 }
268 else if (line.indexOf("E06") == 3) {
269 frqType = t_frequency::E6;
270 }
271 else if (line.indexOf("E07") == 3) {
272 frqType = t_frequency::E7;
273 }
274 else if (line.indexOf("E08") == 3) {
275 frqType = t_frequency::E8;
276 }
277 // QZSS
278 else if (line.indexOf("J01") == 3) {
279 frqType = t_frequency::J1;
280 }
281 else if (line.indexOf("J02") == 3) {
282 frqType = t_frequency::J2;
283 }
284 else if (line.indexOf("J05") == 3) {
285 frqType = t_frequency::J5;
286 }
287 else if (line.indexOf("J06") == 3) {
288 frqType = t_frequency::J6;
289 }
290 // BDS
291 else if (line.indexOf("C01") == 3) {
292 frqType = t_frequency::C1;
293 }
294 else if (line.indexOf("C02") == 3) {
295 frqType = t_frequency::C2;
296 }
297 else if (line.indexOf("C06") == 3) {
298 frqType = t_frequency::C6;
299 }
300 else if (line.indexOf("C07") == 3) {
301 frqType = t_frequency::C7;
302 }
303 if (frqType != t_frequency::dummy) {
304 if (newAntMap->frqMap.find(frqType) != newAntMap->frqMap.end()) {
305 delete newAntMap->frqMap[frqType];
306 }
307 newAntMap->frqMap[frqType] = newFrqMap;
308 }
309 else {
310 delete newFrqMap;
311 }
312 newFrqMap = 0;
313 }
314 else {
315 delete newAntMap;
316 return failure;
317 }
318 }
319
320 // Frequency Reading in Progress
321 // -----------------------------
322 else if (newFrqMap) {
323 if (line.indexOf("NORTH / EAST / UP") == 60) {
324 QTextStream inLine(&line, QIODevice::ReadOnly);
325 inLine >> newFrqMap->neu[0] >> newFrqMap->neu[1] >> newFrqMap->neu[2];
326 newFrqMap->neu[0] *= 1e-3;
327 newFrqMap->neu[1] *= 1e-3;
328 newFrqMap->neu[2] *= 1e-3;
329 }
330 else if (line.indexOf("NOAZI") == 3) {
331 QTextStream inLine(&line, QIODevice::ReadOnly);
332 int nPat = int((newAntMap->zen2-newAntMap->zen1)/newAntMap->dZen) + 1;
333 newFrqMap->pattern.ReSize(nPat);
334 QString dummy;
335 inLine >> dummy;
336 for (int ii = 0; ii < nPat; ii++) {
337 inLine >> newFrqMap->pattern[ii];
338 }
339 newFrqMap->pattern *= 1e-3;
340 }
341 }
342 }
343 }
344 inFile.close();
345 delete newFrqMap;
346 delete newAntMap;
347
348 return success;
349}
350
351// GLONASS Yaw Angle (Sun-pointing law overridden near noon/midnight when
352// the satellite cannot mechanically keep up, following the GLONASS-M
353// "yaw-fixed" behaviour described in Dilssner, Springer, Flohrer, Dow
354// (2011), "The GLONASS-M satellite yaw-attitude model", Advances in Space
355// Research 47(1), 160-171.
356//
357// Unlike GPS/Galileo/BeiDou, which are commonly approximated by the
358// nominal Sun-pointing yaw-steering law of Bar-Sever (1996) at all times,
359// GLONASS-M has been found to stop tracking that law and hold a constant
360// (frozen) yaw angle whenever the required yaw rate would exceed the
361// satellite's slew capability - which happens close to the orbit
362// noon/midnight points whenever the Sun's elevation above the orbital
363// plane (the "beta" angle) is small. The maximum yaw rate used below
364// (0.25 deg/s) and the general approach follow that paper; satellite
365// telemetry was not available to validate the exact rate against this
366// installation's GLONASS satellites, so it should be checked against
367// independently-known attitude/orbit residuals if high accuracy matters.
368////////////////////////////////////////////////////////////////////////////
369double bncAntex::glonassYawAngle(const QString& prn, double Mjd,
370 const ColumnVector& xSat,
371 const ColumnVector& vSat,
372 const ColumnVector& xSun) {
373
374 const double MAX_YAW_RATE = 0.25 * M_PI / 180.0; // [rad/s], approximate
375
376 // This bounds the gap BETWEEN CONSECUTIVE CALLS for a given satellite
377 // (not how long the freeze itself has lasted - a real low-beta passage
378 // can legitimately stay frozen far longer than this). If there was no
379 // call for longer than this, something else interrupted normal epoch-by-
380 // epoch processing (a stream outage, a maneuver-flagged-unhealthy
381 // period, ...), so the stored frozen value is more likely stale than a
382 // still-valid freeze, and we fall back to the current nominal value
383 // instead of trusting it indefinitely.
384 const double MAX_CALL_GAP = 1800.0 / 86400.0; // 30 minutes, in days
385
386 // Inertial-consistent velocity (xSat, vSat are Earth-fixed; remove the
387 // Earth-rotation contribution so that the orbit normal below is not
388 // contaminated by it)
389 // -------------------------------------------------------------------
390 ColumnVector Omega(3); Omega(1) = 0.0; Omega(2) = 0.0; Omega(3) = t_CST::omega;
391 ColumnVector vInert = vSat + crossproduct(Omega, xSat);
392
393 // Orbit normal and instantaneous orbital rate from r x v (exact, valid
394 // for any Keplerian orbit, not just circular ones)
395 // ---------------------------------------------------------------------
396 ColumnVector h = crossproduct(xSat, vInert);
397 double hNorm = sqrt(DotProduct(h, h));
398 ColumnVector orbNormal = h / hNorm;
399 double r = sqrt(DotProduct(xSat, xSat));
400 double nRate = hNorm / (r * r); // [rad/s]
401
402 // Beta angle (Sun elevation above the orbital plane)
403 // ----------------------------------------------------
404 double beta = asin(DotProduct(orbNormal, xSun));
405
406 // Orbit angle mu, measured from the orbit midnight point, increasing in
407 // the direction of satellite motion
408 // -----------------------------------------------------------------------
409 ColumnVector sunProj = xSun - DotProduct(xSun, orbNormal) * orbNormal;
410 sunProj /= sqrt(DotProduct(sunProj, sunProj));
411 ColumnVector eX = -1.0 * sunProj; // midnight direction, mu = 0
412 ColumnVector eY = crossproduct(orbNormal, eX);
413 ColumnVector rHat = xSat / r;
414 double mu = atan2(DotProduct(rHat, eY), DotProduct(rHat, eX));
415
416 // Nominal Sun-pointing yaw angle and its rate (Bar-Sever 1996)
417 // ----------------------------------------------------------------
418 double tanBeta = tan(beta);
419 double sinMu = sin(mu);
420 double psiNom = atan2(-tanBeta, sinMu);
421 double denom = tanBeta * tanBeta + sinMu * sinMu;
422 double psiRate = (denom > 1e-12)
423 ? nRate * fabs(tanBeta * cos(mu)) / denom
424 : 1e9;
425
426 t_glonassYaw& st = _glonassYaw[prn];
427 double callGap = st.valid ? (Mjd - st.lastCallMjd) : 0.0;
428 double psiEff;
429 if (psiRate > MAX_YAW_RATE) {
430 if (!st.valid || callGap > MAX_CALL_GAP) {
431 if (st.valid) { // i.e. it was a genuine gap, not cold-start
432 _glonassYawLog += QString().asprintf(
433 "%s glonassYaw STALE-RESET Mjd=%.6f beta=%6.3f mu=%7.2f rate=%6.3f"
434 " old=%7.2f new=%7.2f gap=%.1fmin\n",
435 prn.toLatin1().data(), Mjd, beta*180.0/M_PI, mu*180.0/M_PI,
436 psiRate*180.0/M_PI, st.yaw*180.0/M_PI, psiNom*180.0/M_PI,
437 callGap*1440.0);
438 }
439 st.yaw = psiNom; // no prior history, or the gap is too long to trust it
440 }
441 if (!st.fixed) {
442 _glonassYawLog += QString().asprintf(
443 "%s glonassYaw FIXED-ENTER Mjd=%.6f beta=%6.3f mu=%7.2f rate=%6.3f yaw=%7.2f\n",
444 prn.toLatin1().data(), Mjd, beta*180.0/M_PI, mu*180.0/M_PI,
445 psiRate*180.0/M_PI, st.yaw*180.0/M_PI);
446 st.fixed = true;
447 }
448 psiEff = st.yaw; // hold the frozen yaw angle
449 }
450 else {
451 if (st.fixed) {
452 _glonassYawLog += QString().asprintf(
453 "%s glonassYaw FIXED-EXIT Mjd=%.6f beta=%6.3f mu=%7.2f rate=%6.3f"
454 " frozen=%7.2f nominal=%7.2f\n",
455 prn.toLatin1().data(), Mjd, beta*180.0/M_PI, mu*180.0/M_PI,
456 psiRate*180.0/M_PI, st.yaw*180.0/M_PI, psiNom*180.0/M_PI);
457 st.fixed = false;
458 }
459 st.yaw = psiNom;
460 psiEff = psiNom;
461 }
462 st.lastCallMjd = Mjd; // update on every call, fixed or not, to detect gaps
463 st.valid = true;
464
465 return psiEff;
466}
467
468// GPS satellite yaw angle during nominal tracking and noon/midnight turns.
469//
470// GPS satellites perform a "noon turn" (and, for some blocks, a "midnight
471// turn") when the Sun's elevation angle above the orbital plane (beta) is
472// small: the required nominal yaw rate then exceeds the satellite's
473// mechanical maximum, so the satellite yaws at that maximum rate until it
474// catches back up to the nominal Sun-pointing orientation (Kouba 2009/2015,
475// Bar-Sever 1996).
476//
477// The max yaw rates below are the best published estimates per block type:
478// IIA: 0.12 °/s (Kouba 2009)
479// IIR: 0.20 °/s (Bar-Sever 1996)
480// IIR-M: 0.20 °/s
481// IIF: 0.11 °/s (Kouba 2015)
482// IIIA: 0.15 °/s (tentative)
483//
484// Returns the effective yaw angle [rad] in the velocity-referenced frame,
485// for use with the same Rodrigues rotation as the GLONASS model. During
486// nominal tracking this equals psiNom and the result is identical to the
487// simple sz×xSun formula.
488////////////////////////////////////////////////////////////////////////////
489double bncAntex::gpsYawAngle(const QString& prn, const QString& blockType,
490 double Mjd,
491 const ColumnVector& xSat,
492 const ColumnVector& vSat,
493 const ColumnVector& xSun) {
494
495 // Max yaw rate [rad/s] by GPS block type
496 double psiDotMax;
497 if (blockType == "IIA") psiDotMax = 0.12 * M_PI / 180.0;
498 else if (blockType == "IIR") psiDotMax = 0.20 * M_PI / 180.0;
499 else if (blockType == "IIR-M") psiDotMax = 0.20 * M_PI / 180.0;
500 else if (blockType == "IIF") psiDotMax = 0.11 * M_PI / 180.0;
501 else if (blockType == "IIIA") psiDotMax = 0.15 * M_PI / 180.0;
502 else return 0.0; // unknown block: caller uses simple Sun-pointing
503
504 const double MAX_CALL_GAP = 1800.0 / 86400.0; // 30 min in days
505
506 // Inertial velocity
507 ColumnVector Omega(3); Omega(1) = 0.0; Omega(2) = 0.0; Omega(3) = t_CST::omega;
508 ColumnVector vInert = vSat + crossproduct(Omega, xSat);
509
510 // Orbital angular momentum vector → orbital rate
511 ColumnVector h = crossproduct(xSat, vInert);
512 double hNorm = sqrt(DotProduct(h, h));
513 ColumnVector orbNormal = h / hNorm;
514 double r = sqrt(DotProduct(xSat, xSat));
515 double nRate = hNorm / (r * r); // [rad/s]
516
517 // Beta angle
518 double beta = asin(DotProduct(orbNormal, xSun));
519
520 // Mu: orbit angle from midnight (same geometry as GLONASS)
521 ColumnVector sunProj = xSun - DotProduct(xSun, orbNormal) * orbNormal;
522 sunProj /= sqrt(DotProduct(sunProj, sunProj));
523 ColumnVector eX = -1.0 * sunProj; // midnight direction
524 ColumnVector eY = crossproduct(orbNormal, eX);
525 ColumnVector rHat = xSat / r;
526 double mu = atan2(DotProduct(rHat, eY), DotProduct(rHat, eX));
527
528 // Nominal yaw and its rate
529 double tanBeta = tan(beta);
530 double sinMu = sin(mu);
531 double psiNom = atan2(-tanBeta, sinMu);
532 double denom = tanBeta * tanBeta + sinMu * sinMu;
533 // |dPsi/dt| (always non-negative, sign comes from sign of tanBeta*cos(mu))
534 double psiDotNomAbs = (denom > 1e-12)
535 ? nRate * fabs(tanBeta * cos(mu)) / denom
536 : 1e9;
537 // Sign of the nominal yaw rate: dPsi/dt = nRate * tanBeta * cos(mu) / denom
538 double psiDotNomSign = (tanBeta * cos(mu) >= 0.0) ? 1.0 : -1.0;
539
540 t_gpsYaw& st = _gpsYaw[prn];
541 double callGap = st.valid ? (Mjd - st.lastCallMjd) : 0.0;
542
543 // Stale state: reset if there has been a gap in calls
544 if (st.valid && callGap > MAX_CALL_GAP) {
545 _gpsYawLog += QString().asprintf(
546 "%s gpsYaw STALE-RESET Mjd=%.6f beta=%6.3f mu=%7.2f"
547 " old=%7.2f new=%7.2f gap=%.1fmin\n",
548 prn.toLatin1().data(), Mjd, beta*180.0/M_PI, mu*180.0/M_PI,
549 st.yaw*180.0/M_PI, psiNom*180.0/M_PI, callGap*1440.0);
550 st.valid = false;
551 st.inTurn = false;
552 }
553
554 double psiEff;
555 if (psiDotNomAbs > psiDotMax) {
556 // Constrained: satellite yaws at psiDotMax in the nominal direction
557 if (!st.valid || !st.inTurn) {
558 // Entering a noon/midnight turn
559 psiEff = st.valid ? st.yaw : psiNom;
560 _gpsYawLog += QString().asprintf(
561 "%s gpsYaw TURN-ENTER Mjd=%.6f beta=%6.3f mu=%7.2f"
562 " psiNom=%7.2f psiEff=%7.2f psiDotNom=%6.3f max=%5.3f\n",
563 prn.toLatin1().data(), Mjd, beta*180.0/M_PI, mu*180.0/M_PI,
564 psiNom*180.0/M_PI, psiEff*180.0/M_PI,
565 psiDotNomAbs*psiDotNomSign*180.0/M_PI, psiDotMax*180.0/M_PI);
566 st.inTurn = true;
567 } else {
568 // Continuing the turn: integrate at constrained rate
569 double dt = callGap * 86400.0; // [s]
570 psiEff = st.yaw + psiDotNomSign * psiDotMax * dt;
571 }
572 // Wrap to [-pi, pi]
573 while (psiEff > M_PI) psiEff -= 2.0 * M_PI;
574 while (psiEff < -M_PI) psiEff += 2.0 * M_PI;
575 }
576 else {
577 // Nominal tracking
578 psiEff = psiNom;
579 if (st.inTurn) {
580 _gpsYawLog += QString().asprintf(
581 "%s gpsYaw TURN-EXIT Mjd=%.6f beta=%6.3f mu=%7.2f"
582 " frozen=%7.2f nominal=%7.2f\n",
583 prn.toLatin1().data(), Mjd, beta*180.0/M_PI, mu*180.0/M_PI,
584 st.yaw*180.0/M_PI, psiNom*180.0/M_PI);
585 st.inTurn = false;
586 }
587 st.yaw = psiNom;
588 }
589
590 st.yaw = psiEff;
591 st.lastCallMjd = Mjd;
592 st.valid = true;
593
594 return psiEff;
595}
596
597//
598// Orbit-Normal Mode Yaw Angle — shared model for Galileo and BDS
599//
600// When |beta| < betaThr the satellite rotates toward yaw = 0 (orbit-normal)
601// at the block-specific maximum yaw rate. When |beta| >= betaThr it returns
602// to nominal yaw-steering (psiNom). Transitions are rate-limited in both
603// directions to match physical satellite behaviour.
604//
605// References:
606// Galileo IOV/FOC: Kouba (2017), Steigenberger et al. (2018)
607// BDS MEO/IGSO: Dai et al. (2015), Wang et al. (2018)
608////////////////////////////////////////////////////////////////////////////
609double bncAntex::onModeYawAngle(const QString& prn,
610 double betaThr, double psiDotMax, double Mjd,
611 const ColumnVector& xSat,
612 const ColumnVector& vSat,
613 const ColumnVector& xSun) {
614
615 const double MAX_CALL_GAP = 1800.0 / 86400.0; // 30 min [days]
616
617 // Inertial velocity
618 ColumnVector Omega(3); Omega(1) = 0.0; Omega(2) = 0.0; Omega(3) = t_CST::omega;
619 ColumnVector vInert = vSat + crossproduct(Omega, xSat);
620
621 // Orbit geometry
622 ColumnVector h = crossproduct(xSat, vInert);
623 double hNorm = sqrt(DotProduct(h, h));
624 ColumnVector orbNormal = h / hNorm;
625 double r = sqrt(DotProduct(xSat, xSat));
626
627 double beta = asin(DotProduct(orbNormal, xSun));
628
629 ColumnVector sunProj = xSun - DotProduct(xSun, orbNormal) * orbNormal;
630 sunProj /= sqrt(DotProduct(sunProj, sunProj));
631 ColumnVector eX = -1.0 * sunProj;
632 ColumnVector eY = crossproduct(orbNormal, eX);
633 ColumnVector rHat = xSat / r;
634 double mu = atan2(DotProduct(rHat, eY), DotProduct(rHat, eX));
635 double psiNom = atan2(-tan(beta), sin(mu));
636
637 // Target yaw: orbit-normal (0) when below threshold, nominal otherwise
638 bool wantsON = (fabs(beta) < betaThr);
639 double psiTarget = wantsON ? 0.0 : psiNom;
640
641 t_onYaw& st = _onYaw[prn];
642 double callGap = st.valid ? (Mjd - st.lastCallMjd) : 0.0;
643
644 if (st.valid && callGap > MAX_CALL_GAP) {
645 _onYawLog += QString().asprintf(
646 "%s onYaw STALE-RESET Mjd=%.6f beta=%5.2f gap=%.1fmin\n",
647 prn.toLatin1().data(), Mjd, beta*180.0/M_PI, callGap*1440.0);
648 st.valid = false;
649 }
650
651 double psiEff;
652 if (!st.valid) {
653 // Cold start: initialise at psiNom regardless of mode
654 psiEff = psiNom;
655 st.inON = false;
656 }
657 else {
658 double dt = callGap * 86400.0; // [s]
659 double dpsi = psiTarget - st.yaw;
660 while (dpsi > M_PI) dpsi -= 2.0 * M_PI;
661 while (dpsi < -M_PI) dpsi += 2.0 * M_PI;
662 double maxDpsi = psiDotMax * dt;
663
664 if (fabs(dpsi) <= maxDpsi) {
665 // Target reached this step
666 psiEff = psiTarget;
667 bool wasON = st.inON;
668 st.inON = wantsON;
669 if (!wasON && wantsON && fabs(st.yaw) < 0.5*M_PI/180.0) {
670 _onYawLog += QString().asprintf(
671 "%s onYaw ON-ENTER Mjd=%.6f beta=%5.2f psiNom=%7.2f\n",
672 prn.toLatin1().data(), Mjd, beta*180.0/M_PI, psiNom*180.0/M_PI);
673 }
674 else if (wasON && !wantsON) {
675 _onYawLog += QString().asprintf(
676 "%s onYaw ON-EXIT Mjd=%.6f beta=%5.2f psiNom=%7.2f\n",
677 prn.toLatin1().data(), Mjd, beta*180.0/M_PI, psiNom*180.0/M_PI);
678 st.inON = false;
679 }
680 }
681 else {
682 // Still rotating toward target
683 psiEff = st.yaw + (dpsi > 0.0 ? 1.0 : -1.0) * maxDpsi;
684 }
685 }
686
687 while (psiEff > M_PI) psiEff -= 2.0 * M_PI;
688 while (psiEff < -M_PI) psiEff += 2.0 * M_PI;
689
690 st.yaw = psiEff;
691 st.lastCallMjd = Mjd;
692 st.valid = true;
693
694 return psiEff;
695}
696
697//
698////////////////////////////////////////////////////////////////////////////
699double bncAntex::galileoYawAngle(const QString& prn, const QString& blockType,
700 double Mjd,
701 const ColumnVector& xSat,
702 const ColumnVector& vSat,
703 const ColumnVector& xSun) {
704 // IOV (type "1") and FOC (type "2"): orbit-normal for |beta| < 2 deg.
705 // Max yaw rate 0.20 deg/s applies to both generations.
706 // Kouba (2017), Steigenberger et al. (2018)
707 Q_UNUSED(blockType);
708 const double betaThr = 2.0 * M_PI / 180.0;
709 const double psiDotMax = 0.20 * M_PI / 180.0;
710 return onModeYawAngle(prn, betaThr, psiDotMax, Mjd, xSat, vSat, xSun);
711}
712
713//
714////////////////////////////////////////////////////////////////////////////
715double bncAntex::bdsYawAngle(const QString& prn, const QString& blockType,
716 double Mjd,
717 const ColumnVector& xSat,
718 const ColumnVector& vSat,
719 const ColumnVector& xSun) {
720 // GEO satellites are always in orbit-normal mode (yaw = 0).
721 if (blockType == "2G" || blockType == "3G-CAST")
722 return 0.0;
723
724 double betaThr, psiDotMax;
725 if (blockType == "2M" || blockType == "2I") {
726 // BDS-2 MEO / IGSO: orbit-normal for |beta| < 4 deg (Dai et al. 2015)
727 betaThr = 4.0 * M_PI / 180.0;
728 psiDotMax = 0.10 * M_PI / 180.0;
729 }
730 else {
731 // BDS-3 (CAST and SECM MEO/IGSO): orbit-normal for |beta| < 3 deg
732 betaThr = 3.0 * M_PI / 180.0;
733 psiDotMax = 0.15 * M_PI / 180.0;
734 }
735 return onModeYawAngle(prn, betaThr, psiDotMax, Mjd, xSat, vSat, xSun);
736}
737
738//
739// Satellite Antenna Offset
740////////////////////////////////////////////////////////////////////////////
741t_irc bncAntex::satCoMcorrection(const QString& prn, double Mjd,
742 const ColumnVector& xSat,
743 const ColumnVector& vSat, ColumnVector& dx,
744 e_attMode mode, double externalYaw) {
745
746 t_frequency::type frqType = t_frequency::dummy;
747
748 if (prn[0] == 'G') {
749 frqType = t_frequency::G1;
750 }
751 else if (prn[0] == 'R') {
752 frqType = t_frequency::R1;
753 }
754 else if (prn[0] == 'E') {
755 frqType = t_frequency::E1;
756 }
757 else if (prn[0] == 'C') {
758 frqType = t_frequency::C2;
759 }
760 else if (prn[0] == 'S') {
761 frqType = t_frequency::S1;
762 }
763 else if (prn[0] == 'J') {
764 frqType = t_frequency::J1;
765 }
766 else if (prn[0] == 'I') {
767 frqType = t_frequency::I5;
768 }
769
770 QMap<QString, t_antMap*>::const_iterator it = _maps.find(prn.mid(0,3));
771 if (it != _maps.end()) {
772 t_antMap* map = it.value();
773 if (map->frqMap.find(frqType) != map->frqMap.end()) {
774
775 double* neu = map->frqMap[frqType]->neu;
776
777 // Unit Vectors sz, sy, sx
778 // -----------------------
779 ColumnVector sz = -xSat;
780 sz /= sqrt(DotProduct(sz,sz));
781
782 ColumnVector xSun = BNC_PPP::t_astro::Sun(Mjd);
783 xSun /= sqrt(DotProduct(xSun,xSun));
784
785 ColumnVector sy, sx;
786
787 // Determine the satellite body frame orientation.
788 //
789 // ATT_NOMINAL: simple nominal Sun-pointing for all systems.
790 // ATT_COMPUTED or ATT_EXTERNAL: use per-system attitude models.
791 // GLONASS → yaw-fixed model (Dilssner et al. 2011)
792 // GPS → noon/midnight turn model (Kouba 2009/2015)
793 // others → simple nominal Sun-pointing
794 // ATT_EXTERNAL: caller supplies the yaw angle [rad] directly
795 // (velocity-referenced frame, same convention as GLONASS/GPS models).
796 // -----------------------------------------------------------------------
797 bool useVelocityFrame = false;
798 double psiEff = 0.0;
799
800 if (mode == ATT_COMPUTED && prn[0] == 'R' && vSat.size() == 3) {
801 psiEff = glonassYawAngle(prn, Mjd, xSat, vSat, xSun);
802 useVelocityFrame = true;
803 }
804 else if (mode == ATT_COMPUTED && prn[0] == 'G' && vSat.size() == 3
805 && !map->blockType.isEmpty()) {
806 psiEff = gpsYawAngle(prn, map->blockType, Mjd, xSat, vSat, xSun);
807 useVelocityFrame = true;
808 }
809 else if (mode == ATT_COMPUTED && prn[0] == 'E' && vSat.size() == 3
810 && !map->blockType.isEmpty()) {
811 psiEff = galileoYawAngle(prn, map->blockType, Mjd, xSat, vSat, xSun);
812 useVelocityFrame = true;
813 }
814 else if (mode == ATT_COMPUTED && prn[0] == 'C' && vSat.size() == 3
815 && !map->blockType.isEmpty()) {
816 psiEff = bdsYawAngle(prn, map->blockType, Mjd, xSat, vSat, xSun);
817 useVelocityFrame = true;
818 }
819 else if (mode == ATT_EXTERNAL && vSat.size() == 3) {
820 psiEff = externalYaw;
821 useVelocityFrame = true;
822 }
823
824 if (useVelocityFrame) {
825 ColumnVector Omega(3); Omega(1) = 0.0; Omega(2) = 0.0; Omega(3) = t_CST::omega;
826 ColumnVector vInert = vSat + crossproduct(Omega, xSat);
827
828 ColumnVector sy0 = crossproduct(sz, vInert);
829 sy0 /= sqrt(DotProduct(sy0,sy0));
830 ColumnVector sx0 = crossproduct(sy0, sz);
831
832 // Rodrigues rotation of (sx0, sy0) around sz by psiEff
833 double cosY = cos(psiEff);
834 double sinY = sin(psiEff);
835 sx = sx0 * cosY + crossproduct(sz, sx0) * sinY;
836 sy = sy0 * cosY + crossproduct(sz, sy0) * sinY;
837 }
838 else {
839 // Nominal Sun-pointing: direct formula (ATT_NOMINAL, or computed
840 // with no block-type-specific model available)
841 sy = crossproduct(sz, xSun);
842 sy /= sqrt(DotProduct(sy,sy));
843 sx = crossproduct(sy, sz);
844 }
845
846 dx[0] = sx[0] * neu[0] + sy[0] * neu[1] + sz[0] * neu[2];
847 dx[1] = sx[1] * neu[0] + sy[1] * neu[1] + sz[1] * neu[2];
848 dx[2] = sx[2] * neu[0] + sy[2] * neu[1] + sz[2] * neu[2];
849
850 return success;
851 }
852 }
853
854 return failure;
855}
856
857//
858////////////////////////////////////////////////////////////////////////////
859double bncAntex::satCorr(const QString& prn, t_frequency::type frqType,
860 double elTx, double azTx, bool& found) const {
861
862 if (_maps.find(prn.mid(0,3)) == _maps.end()) {
863 found = false;
864 return 0.0;
865 };
866
867 t_antMap* map = _maps[prn.mid(0,3)];
868
869 if (map->frqMap.find(frqType) == map->frqMap.end()) {
870 found = false;
871 return 0.0;
872 };
873
874 t_frqMap* frqMap = map->frqMap[frqType];
875
876 double var = 0.0;
877 if (frqMap->pattern.ncols() > 0) {
878 double zenDiff = 999.999;
879 double zenTx = 90.0 - elTx * 180.0 / M_PI;
880 unsigned iZen = 0;
881 for (double zen = map->zen1; zen <= map->zen2; zen += map->dZen) {
882 iZen += 1;
883 double newZenDiff = fabs(zen - zenTx);
884 if (newZenDiff < zenDiff) {
885 zenDiff = newZenDiff;
886 var = frqMap->pattern(iZen);
887 }
888 }
889 }
890
891 found = true;
892 return var - frqMap->neu[0] * cos(azTx)*cos(elTx)
893 - frqMap->neu[1] * sin(azTx)*cos(elTx)
894 - frqMap->neu[2] * sin(elTx);
895
896}
897
898//
899////////////////////////////////////////////////////////////////////////////
900double bncAntex::rcvCorr(const string& antName, t_frequency::type frqType,
901 double eleSat, double azSat, bool& found) const {
902
903 if (antName.find("NULLANTENNA") != string::npos) {
904 found = true;
905 return 0.0;
906 }
907
908 QString antNameQ = antName.c_str();
909
910 if (_maps.find(antNameQ) == _maps.end()) {
911 found = false;
912 return 0.0;
913 }
914
915 t_antMap* map = _maps[antNameQ];
916 if (map->frqMap.find(frqType) == map->frqMap.end()) {
917 found = false;
918 return 0.0;
919 }
920
921 t_frqMap* frqMap = map->frqMap[frqType];
922
923 double var = 0.0;
924 if (frqMap->pattern.ncols() > 0) {
925 double zenDiff = 999.999;
926 double zenSat = 90.0 - eleSat * 180.0 / M_PI;
927 unsigned iZen = 0;
928 for (double zen = map->zen1; zen <= map->zen2; zen += map->dZen) {
929 iZen += 1;
930 double newZenDiff = fabs(zen - zenSat);
931 if (newZenDiff < zenDiff) {
932 zenDiff = newZenDiff;
933 var = frqMap->pattern(iZen);
934 }
935 }
936 }
937
938 found = true;
939 return var - frqMap->neu[0] * cos(azSat)*cos(eleSat)
940 - frqMap->neu[1] * sin(azSat)*cos(eleSat)
941 - frqMap->neu[2] * sin(eleSat);
942
943}
Note: See TracBrowser for help on using the repository browser.